@akagilnc/pi-workflow-roles 0.1.4021 → 0.1.4062

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 (74) hide show
  1. package/dist/acp-host/production-host.js +682 -355
  2. package/dist/doctor-auditor.js +1 -1
  3. package/dist/dossier-resolution.js +21 -8
  4. package/dist/gatekeeper-role.js +34 -27
  5. package/dist/headless-host/description.js +7 -8
  6. package/dist/headless-host/production-host.js +709 -369
  7. package/dist/host-contracts.js +16 -0
  8. package/dist/inspector-contracts.js +8 -0
  9. package/dist/navigator-attendance.js +2 -0
  10. package/dist/navigator-public-session.js +18 -34
  11. package/dist/navigator-session-contracts.js +19 -0
  12. package/dist/packaged-role-registry.js +1 -1
  13. package/dist/pi/known-failure.js +3 -4
  14. package/dist/pi/role-turn-host.js +43 -13
  15. package/dist/public-cli/auto-resume.js +20 -7
  16. package/dist/public-cli/case-dossier-delivery.js +60 -1
  17. package/dist/public-cli/diarist-run.js +9 -23
  18. package/dist/public-cli/inspector-run.js +17 -3
  19. package/dist/public-cli/instruction-seat-run.js +12 -7
  20. package/dist/public-cli/judge-run.js +3 -1
  21. package/dist/public-cli/main.js +440 -222
  22. package/dist/public-cli/notary-run.js +7 -4
  23. package/dist/public-cli/post-admission.js +66 -59
  24. package/dist/public-cli/run-lifecycle.js +3 -0
  25. package/dist/public-cli/settlement.js +78 -58
  26. package/dist/public-cli/terminal.js +1 -11
  27. package/dist/public-role-summons.js +8 -0
  28. package/dist/submission-ledger.js +139 -19
  29. package/dist/user-dialogue-stdin.js +33 -0
  30. package/extensions/role-runtime.ts +1 -0
  31. package/package.json +1 -1
  32. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  33. package/src/doctor-auditor.ts +1 -1
  34. package/src/dossier-resolution.ts +26 -8
  35. package/src/external-host-turn-loop.ts +9 -0
  36. package/src/gatekeeper-pass-envelope.ts +6 -0
  37. package/src/gatekeeper-role.ts +47 -28
  38. package/src/headless-host/description.ts +8 -11
  39. package/src/headless-host/role-turn-host.ts +20 -5
  40. package/src/host-contracts.ts +30 -7
  41. package/src/inspector-contracts.ts +7 -0
  42. package/src/judge-role.ts +4 -0
  43. package/src/navigator-attendance.ts +2 -0
  44. package/src/navigator-public-session.ts +19 -55
  45. package/src/navigator-session-contracts.ts +39 -0
  46. package/src/navigator-work-context.ts +3 -4
  47. package/src/notary-role.ts +1 -0
  48. package/src/packaged-role-registry.ts +1 -1
  49. package/src/pi/adapter.ts +19 -4
  50. package/src/pi/known-failure.ts +3 -4
  51. package/src/pi/role-turn-host.ts +48 -14
  52. package/src/public-cli/auto-resume.ts +57 -20
  53. package/src/public-cli/case-dossier-delivery.ts +86 -1
  54. package/src/public-cli/cli.ts +2 -2
  55. package/src/public-cli/collector-run.ts +3 -1
  56. package/src/public-cli/countersign-run.ts +34 -27
  57. package/src/public-cli/diarist-run.ts +10 -27
  58. package/src/public-cli/inspector-run.ts +26 -2
  59. package/src/public-cli/instruction-seat-run.ts +20 -10
  60. package/src/public-cli/invocation.ts +2 -0
  61. package/src/public-cli/judge-run.ts +3 -1
  62. package/src/public-cli/notary-run.ts +13 -4
  63. package/src/public-cli/post-admission.ts +74 -65
  64. package/src/public-cli/reviewer-run.ts +3 -1
  65. package/src/public-cli/run-lifecycle.ts +3 -0
  66. package/src/public-cli/settlement.ts +105 -80
  67. package/src/public-cli/terminal.ts +7 -16
  68. package/src/public-role-summons.ts +31 -7
  69. package/src/role-envelope.ts +9 -30
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +95 -19
  72. package/src/submission-ledger.ts +195 -26
  73. package/src/user-dialogue-stdin.ts +37 -0
  74. package/src/worker-role.ts +4 -0
@@ -52,10 +52,10 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
52
52
  throw error;
53
53
  }
54
54
  // #747: officer resume key is this parent source-run path (not ticket number).
55
- // #753/#786: gate re-ask and verbatim submission body share summons.instruction
56
- // (reask wins when both present; no parallel stack).
55
+ // #753/#879: gate re-ask and verbatim submission body share summons.instruction
56
+ // (reask wins when both present; no parallel stack). Binding stays sourceRunPath.
57
57
  {
58
- const resumeInstruction = env.reviewReask;
58
+ const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
59
59
  const summons = {
60
60
  sourceRunPath: source.runDirectory,
61
61
  sourceRun: source,
@@ -112,7 +112,10 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
112
112
  : { correlationId: env.correlationId }),
113
113
  continuation: {
114
114
  kind: "initial",
115
- prompt: buildNotaryTransportPrompt(admitted, engineMaterial),
115
+ // #879: gate path first mint carries parent payload as dialogue content;
116
+ // external zero-prompt kickoff unchanged when no body/reask.
117
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
118
+ ?? buildNotaryTransportPrompt(admitted, engineMaterial),
116
119
  },
117
120
  });
118
121
  return await runPostAdmissionOneShot({
@@ -8,13 +8,14 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { readFile, writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildResumeContinuationPrompt, GATE_DOSSIER_POINTER_PREFIX, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
14
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
15
  import { pickEngineAxis } from "../package-resources/engine-material.js";
16
16
  import { resolveHostAwareSessionAvailability } from "../session-identity.js";
17
- import { projectCaseDossierPointerSection } from "./case-dossier-delivery.js";
17
+ import { isOfficerReviewSeat } from "../host-contracts.js";
18
+ import { deliverCaseDossierAsAttachment, projectCaseDossierPointerSection, } from "./case-dossier-delivery.js";
18
19
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
19
20
  function describeCaughtError(error) {
20
21
  if (error instanceof Error) {
@@ -30,6 +31,14 @@ function appendContinuationSection(continuation, section) {
30
31
  ? { kind: "initial", prompt }
31
32
  : { kind: "resume", prompt };
32
33
  }
34
+ /**
35
+ * Nested gate summons (station child) on an officer seat: dialogue content is
36
+ * peer words only (#879). ADR 0081 case dossier still hangs via the existing
37
+ * attachments freeze seam — never into the peer body, never RoleTurnRequest.materials.
38
+ */
39
+ function isStationChildOfficerDialogue(role, env) {
40
+ return env.stationChild === true && isOfficerReviewSeat(role);
41
+ }
33
42
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
34
43
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
35
44
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
@@ -324,9 +333,9 @@ export async function dispatchPostAdmissionTurn(input) {
324
333
  // Turn request is assembled after beforeDispatch so this turn sees whatever it
325
334
  // settled — the seat's ticket bind re-projection and any court diarist station
326
335
  // writes (#742). Case dossier delivery (ADR 0081 / #709) rides here once for
327
- // every public entry: first call, same-ticket re-summons and manual resume
328
- // alike. System refs append their own neutral section; caller frozen
329
- // attachments and the seat's own prompt bytes are never rewritten.
336
+ // every public entry. #879: station-child officer dialogue keeps peer body
337
+ // intact — 起居录 hangs via existing attachments freeze (not prompt wrap,
338
+ // not RoleTurnRequest.materials). Other entries keep the neutral prompt section.
330
339
  let turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
331
340
  if (env.stationChild !== undefined) {
332
341
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
@@ -334,16 +343,31 @@ export async function dispatchPostAdmissionTurn(input) {
334
343
  if (hostTransition !== undefined) {
335
344
  turnRequest = { ...turnRequest, hostTransition };
336
345
  }
337
- const dossierSection = await projectCaseDossierPointerSection({
338
- ticketNumber: admitted.ticketNumber,
339
- projectRoot: admitted.projectRoot,
340
- home: env.home,
341
- });
342
- if (dossierSection !== undefined) {
343
- turnRequest = {
344
- ...turnRequest,
345
- continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
346
- };
346
+ if (isStationChildOfficerDialogue(admitted.role, env)) {
347
+ // 0081 non-body face: freeze pointer section under run/attachments/.
348
+ // Peer dialogue continuation.prompt stays parent payload only — the seat
349
+ // consumes the freeze via loadCaseDossierReadingMaterial → existing
350
+ // agent-start readingMaterial / systemPrompt.materials fold (not prompt splice,
351
+ // not RoleTurnRequest.materials).
352
+ await deliverCaseDossierAsAttachment({
353
+ ticketNumber: admitted.ticketNumber,
354
+ projectRoot: admitted.projectRoot,
355
+ home: env.home,
356
+ runDirectory: admitted.runDirectory,
357
+ });
358
+ }
359
+ else {
360
+ const dossierSection = await projectCaseDossierPointerSection({
361
+ ticketNumber: admitted.ticketNumber,
362
+ projectRoot: admitted.projectRoot,
363
+ home: env.home,
364
+ });
365
+ if (dossierSection !== undefined) {
366
+ turnRequest = {
367
+ ...turnRequest,
368
+ continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
369
+ };
370
+ }
347
371
  }
348
372
  // Authoritative host write happens here, at the real dispatch boundary —
349
373
  // immediately before the turn actually starts, after every retryable
@@ -643,46 +667,40 @@ export async function dispatchPostAdmissionTurn(input) {
643
667
  }
644
668
  }
645
669
  /**
646
- * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755):
670
+ * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
647
671
  * seat-table model/engine/timeout axes, restored correlation, and either
648
672
  * - manual resume (no same-ticket summons): package envelope / optional caller
649
673
  * message, with engine-axis handbook via buildResumeContinuationPrompt, or
650
674
  * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
651
- * attachment paths only — no「重新读」、no engine handbook packaging
652
- * (#750/#755), whether or not attachments are present.
675
+ * attachment paths only — no「请重读」、no code-authored content substitute,
676
+ * no engine handbook packaging (#750/#755/#879).
653
677
  * Caller message wins as prompt base when supplied (bytes unchanged, including
654
- * blank/whitespace); else summons instruction. Attachment projection must not
655
- * re-interpret the caller message as instructionEmpty.
678
+ * blank/whitespace); else summons instruction (parent payload or reask).
679
+ * Binding pointer stays on summons.sourceRunPath / activation — not dialogue content.
656
680
  * Seats add only their activation projection. Call prepareSummonsResumeMaterials
657
681
  * first when request.summons carries instruction or attachment paths.
658
682
  */
659
683
  export function resumeTurnRequestProjectionOptions(admitted, request, env, summonsPrepared) {
660
- const officerSourcePath = request.summons?.sourceRunPath;
661
- const withReread = (body) => {
662
- if (officerSourcePath === undefined
663
- || (admitted.role !== "notary"
664
- && admitted.role !== "inspector"
665
- && admitted.role !== "auditor")) {
666
- return body;
667
- }
668
- if (body.startsWith("请重读"))
669
- return body;
670
- return `请重读\n${body}`;
671
- };
684
+ // #879: officer dialogue content is caller/peer words only — no「请重读」,
685
+ // no code-authored constant substitute, no attachment-list wrap of peer body.
686
+ // Binding pointer stays on summons sourceRunPath / activation / attachments.
687
+ const officerDialogue = isStationChildOfficerDialogue(admitted.role, env);
672
688
  let prompt;
673
689
  if (request.message !== undefined) {
674
690
  if (summonsPrepared !== undefined) {
675
- // #755: same-ticket review / open-court — caller words + optional paths.
676
- // Attachments are not a gate: message-only summons must stay plain too.
677
- prompt = withReread(buildInstructionTransportPrompt({
678
- instruction: request.message,
679
- instructionEmpty: false,
680
- attachments: summonsPrepared.attachments,
681
- }));
691
+ // #755: same-ticket review / open-court — caller words.
692
+ // #879 station-child officer: words only (attachments are independent freeze).
693
+ prompt = officerDialogue
694
+ ? request.message
695
+ : buildInstructionTransportPrompt({
696
+ instruction: request.message,
697
+ instructionEmpty: false,
698
+ attachments: summonsPrepared.attachments,
699
+ });
682
700
  }
683
701
  else if (request.summons !== undefined) {
684
702
  // #755: same-ticket summons without prepared materials — caller words only.
685
- prompt = withReread(request.message);
703
+ prompt = request.message;
686
704
  }
687
705
  else {
688
706
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -694,27 +712,16 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
694
712
  }
695
713
  }
696
714
  else if (summonsPrepared !== undefined) {
697
- // #755: same-ticket review summons — instruction/attachments only.
698
- prompt = withReread(buildInstructionTransportPrompt(summonsPrepared));
715
+ // #879 station-child officer: instruction bytes === peer body/reask (no wrap).
716
+ // Other seats keep #755 instruction + optional attachment path listing.
717
+ prompt = officerDialogue
718
+ ? (summonsPrepared.instructionEmpty ? "" : summonsPrepared.instruction)
719
+ : buildInstructionTransportPrompt(summonsPrepared);
699
720
  }
700
721
  else if (request.summons !== undefined) {
701
- // #755: same-ticket summons with no instruction/attachments (e.g. notary
702
- // source-run pointer). #836: officer resume opening adds「请重读」+ path pointer.
703
- const path = request.summons.sourceRunPath;
704
- if (path !== undefined &&
705
- (admitted.role === "notary" ||
706
- admitted.role === "inspector" ||
707
- admitted.role === "auditor")) {
708
- prompt = `请重读\n${GATE_DOSSIER_POINTER_PREFIX}${path}`;
709
- }
710
- else if (admitted.role === "notary" ||
711
- admitted.role === "inspector" ||
712
- admitted.role === "auditor") {
713
- prompt = "请重读";
714
- }
715
- else {
716
- prompt = "";
717
- }
722
+ // #879: same-ticket summons with no instruction (e.g. notary source-run binding
723
+ // only). Pointer is activation/sourceRun material — not dialogue content.
724
+ prompt = "";
718
725
  }
719
726
  else {
720
727
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -1389,6 +1389,9 @@ export async function loadResumableInspectorRun(home, runId, authority) {
1389
1389
  const admitted = {
1390
1390
  role: "inspector",
1391
1391
  ...resumedBaseAdmitted(loaded),
1392
+ ...(loaded.admittedFields.sourceRunPath === undefined
1393
+ ? {}
1394
+ : { sourceRunPath: loaded.admittedFields.sourceRunPath }),
1392
1395
  };
1393
1396
  return seatLoadedResult(loaded, admitted);
1394
1397
  }
@@ -8,7 +8,7 @@ import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/
8
8
  import { dirname, join } from "node:path";
9
9
  import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
10
10
  import { readSitianRecords, resolveSitianRecordPath, sitianReport } from "../sitian-facade.js";
11
- import { hasFreshAttemptSubmission, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
11
+ import { hasFreshAttemptSubmission, readAttemptScopedSubmissionRows, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
12
12
  import { isAuditEscalationResult } from "../audit-escalation.js";
13
13
  import { AUDITOR_SOUL_ROLES } from "../auditor-soul.js";
14
14
  import { DOCTOR_AUDIT_TOOL_NAME } from "../doctor-auditor.js";
@@ -54,15 +54,30 @@ function ledgerReadScope(admitted, scope) {
54
54
  }
55
55
  function roleOutcomeFromRows(role, rows) {
56
56
  const mine = rows.filter((row) => row.role === role);
57
- if (mine.length === 0)
57
+ // Terminal acceptance kind still only follows sealed / audit-escalation (#881):
58
+ // correctable-rejection / infrastructure / candidate stay payloads, not acceptance.
59
+ const terminal = mine.filter((row) => row.kind === "accepted" || row.kind === "audit-escalation");
60
+ if (terminal.length === 0)
58
61
  return undefined;
62
+ // Role-result block is the full recorded sequence for this seat, not a sole pick.
59
63
  const payloads = mine.map((row) => row.accepted);
60
- if (mine.some((row) => row.kind === "audit-escalation")) {
64
+ if (terminal.some((row) => row.kind === "audit-escalation")) {
61
65
  return { kind: "audit_escalation", role, status: "audit_escalation", payloads };
62
66
  }
63
67
  return { kind: "accepted", role, payloads };
64
68
  }
65
69
  async function sealedLedgerOutcome(admitted, role, scope) {
70
+ const home = sealedLedgerHome(admitted);
71
+ // #879: when court scope is present, roleOutcome is this-court original only —
72
+ // never last-wins over an undivided historical array, and never falls back to
73
+ // full-run history when this court sealed zero rows. #836 presentation of full
74
+ // history stays on attachRecordedSubmissions / terminal.submissions (run-scoped).
75
+ if (scope?.courtAttemptId !== undefined && scope.courtAttemptId.length > 0) {
76
+ const thisCourt = await readAttemptScopedSubmissionRows(admitted.projectRoot, admitted.runId, scope.courtAttemptId, home);
77
+ // Scope present + zero this-court rows → no this-court outcome (not run history).
78
+ return roleOutcomeFromRows(role, thisCourt);
79
+ }
80
+ // No court scope: keep run-scoped ledger face (#836).
66
81
  const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
67
82
  return roleOutcomeFromRows(role, rows);
68
83
  }
@@ -89,6 +104,8 @@ export function withSubmissions(terminal, submissions) {
89
104
  if (submissions.length === 0)
90
105
  return terminal;
91
106
  const roleOutcome = terminal.roleOutcome;
107
+ // #879 keeps an already scoped this-court result; #881 full run history is
108
+ // presented independently on terminal.submissions. Only fill a missing result payload.
92
109
  const withPayloads = roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
93
110
  ? { ...roleOutcome, payloads: (roleOutcome.payloads ?? []).length > 0 ? roleOutcome.payloads : submissions }
94
111
  : roleOutcome.kind === "failure"
@@ -98,7 +115,11 @@ export function withSubmissions(terminal, submissions) {
98
115
  }
99
116
  /** Attach full ledger submissions onto any settled terminal (#836). */
100
117
  export async function attachRecordedSubmissions(admitted, terminal, scope) {
101
- return withSubmissions(terminal, await recordedSubmissionPayloads(admitted, scope));
118
+ // #836 / #879: submissions presentation is always run-scoped history. Do not
119
+ // pass courtAttemptId here — roleOutcome already carries this-court payloads
120
+ // from sealedLedgerOutcome when scoped; withSubmissions keeps them.
121
+ void scope;
122
+ return withSubmissions(terminal, await recordedSubmissionPayloads(admitted, undefined));
102
123
  }
103
124
  /**
104
125
  * Host ended with no recorded submission — lawful no_receipt (exit 0).
@@ -133,7 +154,7 @@ async function closedLedgerOutcome(admitted, role, scope) {
133
154
  function coordinatesFromAdmitted(authority, admitted) {
134
155
  return authority.decode(admitted.principal);
135
156
  }
136
- import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, lastRolePayloadRecord, recommendationNavigatorFact, } from "./terminal.js";
157
+ import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, recommendationNavigatorFact, } from "./terminal.js";
137
158
  export { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, };
138
159
  /**
139
160
  * Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
@@ -223,8 +244,7 @@ function isTypedActivationError(error) {
223
244
  cause === "activation" ||
224
245
  cause === "session" ||
225
246
  cause === "output" ||
226
- cause === "timeout" ||
227
- cause === "unrecognized");
247
+ cause === "timeout");
228
248
  }
229
249
  /** Flatten nested AggregateError leaves; non-aggregate values stay as one fact. */
230
250
  function flattenThrownFailureLeaves(error) {
@@ -250,21 +270,20 @@ export function projectThrownFailureLeaf(error) {
250
270
  }
251
271
  return {
252
272
  cause: error.knownCause,
253
- diagnostic: error.message || error.name || "unrecognized exception",
273
+ diagnostic: error.message || error.name || "exception",
254
274
  identity,
255
275
  ...(error.details === undefined ? {} : { details: error.details }),
256
276
  };
257
277
  }
258
278
  if (error instanceof Error) {
259
279
  const identity = thrownIdentity(error);
280
+ // No typed confirmation → keep original diagnostic/identity; do not mint a class (#881).
260
281
  return {
261
- cause: "unrecognized",
262
- diagnostic: error.message || error.name || "unrecognized exception",
282
+ diagnostic: error.message || error.name || "exception",
263
283
  identity,
264
284
  };
265
285
  }
266
286
  return {
267
- cause: "unrecognized",
268
287
  diagnostic: String(error),
269
288
  };
270
289
  }
@@ -294,7 +313,7 @@ function classifyThrownFailure(error) {
294
313
  ...leaves.slice(1).map((leaf) => {
295
314
  const secondary = projectThrownFailureLeaf(leaf);
296
315
  return {
297
- cause: secondary.cause,
316
+ ...(secondary.cause === undefined ? {} : { cause: secondary.cause }),
298
317
  diagnostic: secondary.diagnostic,
299
318
  ...(secondary.identity === undefined ? {} : { identity: secondary.identity }),
300
319
  ...(secondary.details === undefined ? {} : { details: secondary.details }),
@@ -302,7 +321,7 @@ function classifyThrownFailure(error) {
302
321
  }),
303
322
  ];
304
323
  return {
305
- cause: primary.cause,
324
+ ...(primary.cause === undefined ? {} : { cause: primary.cause }),
306
325
  diagnostic: primary.diagnostic,
307
326
  ...(primary.identity === undefined ? {} : { identity: primary.identity }),
308
327
  details: {
@@ -325,7 +344,7 @@ function withKnownDetails(failure, knownDetails) {
325
344
  };
326
345
  }
327
346
  /**
328
- * Classify a controlled post-admission failure without washing unrecognized identities.
347
+ * Classify a controlled post-admission failure without washing original identities.
329
348
  * Cause classes are closed; diagnostic text retains the original identity when known.
330
349
  *
331
350
  * Order: thrown → knownCause → timeout → activation (nonzero) → session → output.
@@ -365,6 +384,30 @@ export function classifyPostAdmissionFailure(input) {
365
384
  : { identity: input.knownIdentity }),
366
385
  };
367
386
  }
387
+ // #881: original failure testimony without a typed class — keep diagnostic/identity;
388
+ // do not fall through to activation/output wash that would mint a substitute class.
389
+ // Bare knownDetails alone is secondary evidence for later branches (withKnownDetails),
390
+ // not a stand-in primary failure record.
391
+ if ((input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== "") ||
392
+ input.knownIdentity !== undefined) {
393
+ const diagnostic = input.knownDiagnostic !== undefined && input.knownDiagnostic.trim() !== ""
394
+ ? input.knownDiagnostic
395
+ : conciseChildDiagnostic(input.stderr, "role run failed");
396
+ const { timedOut: _knownTimedOut, ...knownDetails } = input.knownDetails ?? {};
397
+ const remoteCode = knownDetails.code;
398
+ return withKnownDetails({
399
+ diagnostic,
400
+ details: {
401
+ ...knownDetails,
402
+ ...(remoteCode === undefined ? {} : { code: remoteCode }),
403
+ exitCode: input.code,
404
+ ...(input.timedOut ? { timedOut: true } : {}),
405
+ },
406
+ ...(input.knownIdentity === undefined
407
+ ? {}
408
+ : { identity: input.knownIdentity }),
409
+ }, undefined);
410
+ }
368
411
  if (input.timedOut) {
369
412
  return withKnownDetails({
370
413
  cause: "timeout",
@@ -405,7 +448,7 @@ export function explicitInternalKnownFailureClassificationInput(failure) {
405
448
  if (failure === undefined)
406
449
  return {};
407
450
  return {
408
- knownCause: failure.cause,
451
+ ...(failure.cause === undefined ? {} : { knownCause: failure.cause }),
409
452
  ...(failure.identity === undefined ? {} : { knownIdentity: failure.identity }),
410
453
  ...(failure.diagnostic === undefined ? {} : { knownDiagnostic: failure.diagnostic }),
411
454
  ...(failure.details === undefined ? {} : { knownDetails: failure.details }),
@@ -755,11 +798,21 @@ function complianceFailureFromAuditorVolumes(volumes) {
755
798
  continue;
756
799
  const parent = isRecord(entry.data.parent) ? entry.data.parent : undefined;
757
800
  const failure = isRecord(entry.data.failure) ? entry.data.failure : undefined;
758
- if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId || (failure?.cause !== "provider" && failure?.cause !== "unrecognized"))
801
+ if (parent?.sessionId !== parentId || parent.sessionFile !== sessionFile || parent.attemptEntryId !== attemptEntryId)
802
+ continue;
803
+ // #881: keep the recorded failure as written — typed cause when present, else raw diagnostic only.
804
+ if (failure === undefined)
759
805
  continue;
760
806
  const identity = isRecord(failure.identity) ? failure.identity : undefined;
807
+ const typedCause = failure.cause === "provider" ||
808
+ failure.cause === "activation" ||
809
+ failure.cause === "session" ||
810
+ failure.cause === "output" ||
811
+ failure.cause === "timeout"
812
+ ? failure.cause
813
+ : undefined;
761
814
  return {
762
- cause: failure.cause === "provider" ? "provider" : "unrecognized",
815
+ ...(typedCause === undefined ? {} : { cause: typedCause }),
763
816
  ...(identity === undefined ? {} : { identity: {
764
817
  ...(typeof identity.name === "string" ? { name: identity.name } : {}),
765
818
  ...(typeof identity.code === "string" || typeof identity.code === "number" ? { code: identity.code } : {}),
@@ -1683,9 +1736,6 @@ export async function publishCoderArtifacts(admitted, roleOutcome, coordinates,
1683
1736
  runId: admitted.runId,
1684
1737
  phase: admitted.phase,
1685
1738
  outcome: roleOutcome,
1686
- ...(options.coderOutput === undefined
1687
- ? {}
1688
- : { receipt: options.coderOutput }),
1689
1739
  }, null, 2)}\n`, "utf8");
1690
1740
  await writeFile(evidencePath, `${JSON.stringify({
1691
1741
  runId: admitted.runId,
@@ -1789,14 +1839,10 @@ async function settleLawfulCoderTerminalResult(admitted, authority, options = {}
1789
1839
  if (ledgerOutcome === undefined)
1790
1840
  return undefined;
1791
1841
  const roleOutcome = ledgerOutcome;
1792
- const output = ledgerOutcome.kind === "accepted"
1793
- ? lastRolePayloadRecord(ledgerOutcome.payloads ?? [])
1794
- : undefined;
1795
1842
  const coordinates = coordinatesFromAdmitted(authority, admitted);
1796
1843
  const entries = await readLawfulSettlementEntries(coordinates.sessionFile) ?? [];
1797
1844
  const navigator = extractNavigatorFact(entries);
1798
1845
  const artifacts = await publishCoderArtifacts(admitted, roleOutcome, coordinates, {
1799
- ...(output === undefined ? {} : { coderOutput: output }),
1800
1846
  ...(options.methodProvenance === undefined
1801
1847
  ? {}
1802
1848
  : { methodProvenance: options.methodProvenance }),
@@ -1873,9 +1919,6 @@ export async function publishFixerArtifacts(admitted, roleOutcome, coordinates,
1873
1919
  runId: admitted.runId,
1874
1920
  phase: admitted.phase,
1875
1921
  outcome: roleOutcome,
1876
- ...(options.fixerOutput === undefined
1877
- ? {}
1878
- : { receipt: options.fixerOutput }),
1879
1922
  }, null, 2)}\n`, "utf8");
1880
1923
  await writeFile(evidencePath, `${JSON.stringify({
1881
1924
  runId: admitted.runId,
@@ -1910,9 +1953,6 @@ async function settleLawfulFixerTerminalResult(admitted, authority, options, sco
1910
1953
  if (ledgerOutcome === undefined)
1911
1954
  return undefined;
1912
1955
  const roleOutcome = ledgerOutcome;
1913
- const output = ledgerOutcome.kind === "accepted"
1914
- ? lastRolePayloadRecord(ledgerOutcome.payloads ?? [])
1915
- : undefined;
1916
1956
  const coordinates = coordinatesFromAdmitted(authority, admitted);
1917
1957
  const { sessionDirectory, sessionFile } = coordinates;
1918
1958
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
@@ -1924,7 +1964,6 @@ async function settleLawfulFixerTerminalResult(admitted, authority, options, sco
1924
1964
  ],
1925
1965
  });
1926
1966
  const artifacts = await publishFixerArtifacts(admitted, roleOutcome, coordinates, {
1927
- ...(output === undefined ? {} : { fixerOutput: output }),
1928
1967
  methodProvenance: options.methodProvenance,
1929
1968
  methodInvocations,
1930
1969
  });
@@ -1943,7 +1982,7 @@ export async function settleFixerTerminalResult(admitted, authority, options, sc
1943
1982
  }
1944
1983
  return settled;
1945
1984
  }
1946
- export async function publishCollectorArtifacts(admitted, roleOutcome, coordinates, options = {}) {
1985
+ export async function publishCollectorArtifacts(admitted, roleOutcome, coordinates) {
1947
1986
  await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1948
1987
  const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1949
1988
  const reportPath = join(artifactsDir, "report.json");
@@ -1952,9 +1991,6 @@ export async function publishCollectorArtifacts(admitted, roleOutcome, coordinat
1952
1991
  role: "collector",
1953
1992
  runId: admitted.runId,
1954
1993
  outcome: roleOutcome,
1955
- ...(options.collectorReceipt === undefined
1956
- ? {}
1957
- : { receipt: options.collectorReceipt }),
1958
1994
  }, null, 2)}\n`, "utf8");
1959
1995
  await writeFile(evidencePath, `${JSON.stringify({
1960
1996
  runId: admitted.runId,
@@ -2004,10 +2040,9 @@ async function settleLawfulCollectorTerminalResult(admitted, authority, scope) {
2004
2040
  }
2005
2041
  return undefined;
2006
2042
  }
2007
- const receipt = (lastRolePayloadRecord(roleOutcome.payloads ?? []) ?? {});
2008
2043
  const accepted = roleOutcome;
2009
2044
  const navigator = extractNavigatorFact(entries);
2010
- const artifacts = await publishCollectorArtifacts(admitted, accepted, coordinates, { collectorReceipt: receipt });
2045
+ const artifacts = await publishCollectorArtifacts(admitted, accepted, coordinates);
2011
2046
  return attachRecordedSubmissions(admitted, await withOptionalGateProjection({
2012
2047
  roleOutcome: accepted,
2013
2048
  navigator,
@@ -2069,10 +2104,6 @@ export async function publishDoctorArtifacts(admitted, roleOutcome, coordinates,
2069
2104
  role: "doctor",
2070
2105
  runId: admitted.runId,
2071
2106
  outcome: roleOutcome,
2072
- ...(options.doctorOutput === undefined
2073
- ? {}
2074
- : { receipt: options.doctorOutput }),
2075
- // Independent machine fields beside the receipt — not merged into it.
2076
2107
  ...(options.cost === undefined ? {} : { cost: options.cost }),
2077
2108
  ...(options.auditNoReceipt === undefined ? {} : { auditNoReceipt: options.auditNoReceipt }),
2078
2109
  }, null, 2)}\n`, "utf8");
@@ -2113,13 +2144,11 @@ async function settleLawfulDoctorTerminalResult(admitted, authority, scope) {
2113
2144
  runId: admitted.runId,
2114
2145
  }, sessionDirectory);
2115
2146
  }
2116
- const output = (lastRolePayloadRecord(sealed.payloads ?? []) ?? {});
2117
2147
  const roleOutcome = sealed;
2118
2148
  const navigator = extractNavigatorFact(entries);
2119
2149
  const cost = extractDoctorCandidateCostFact(entries);
2120
2150
  const auditNoReceipt = extractDoctorCandidateAuditNoReceiptFact(entries);
2121
2151
  const artifacts = await publishDoctorArtifacts(admitted, roleOutcome, coordinates, {
2122
- doctorOutput: output,
2123
2152
  ...(cost === undefined ? {} : { cost }),
2124
2153
  ...(auditNoReceipt === undefined ? {} : { auditNoReceipt }),
2125
2154
  });
@@ -2346,9 +2375,6 @@ export async function publishReviewerArtifacts(admitted, roleOutcome, coordinate
2346
2375
  role: "reviewer",
2347
2376
  runId: admitted.runId,
2348
2377
  outcome: roleOutcome,
2349
- ...(options.reviewerReceipt === undefined
2350
- ? {}
2351
- : { receipt: options.reviewerReceipt }),
2352
2378
  }, null, 2)}\n`, "utf8");
2353
2379
  await writeFile(evidencePath, `${JSON.stringify({
2354
2380
  runId: admitted.runId,
@@ -2384,7 +2410,6 @@ async function settleLawfulReviewerTerminalResult(admitted, authority, options,
2384
2410
  const coordinates = coordinatesFromAdmitted(authority, admitted);
2385
2411
  const { sessionDirectory, sessionFile } = coordinates;
2386
2412
  const entries = await readLawfulSettlementEntries(sessionFile) ?? [];
2387
- const receipt = lastRolePayloadRecord(sealed.payloads ?? []);
2388
2413
  const roleOutcome = sealed;
2389
2414
  const navigator = extractNavigatorFact(entries);
2390
2415
  const methodInvocations = extractReviewerMethodInvocations(entries, {
@@ -2394,7 +2419,6 @@ async function settleLawfulReviewerTerminalResult(admitted, authority, options,
2394
2419
  ],
2395
2420
  });
2396
2421
  const artifacts = await publishReviewerArtifacts(admitted, roleOutcome, coordinates, {
2397
- ...(receipt === undefined ? {} : { reviewerReceipt: receipt }),
2398
2422
  methodProvenance: options.methodProvenance,
2399
2423
  methodInvocations,
2400
2424
  });
@@ -2455,9 +2479,6 @@ export async function publishMergerArtifacts(admitted, roleOutcome, coordinates,
2455
2479
  role: "merger",
2456
2480
  runId: admitted.runId,
2457
2481
  outcome: roleOutcome,
2458
- ...(options.mergerOutput === undefined
2459
- ? {}
2460
- : { receipt: options.mergerOutput }),
2461
2482
  }, null, 2)}\n`, "utf8");
2462
2483
  await writeFile(evidencePath, `${JSON.stringify({
2463
2484
  runId: admitted.runId,
@@ -2513,7 +2534,6 @@ async function settleLawfulMergerTerminalResult(admitted, authority, options, sc
2513
2534
  allowedLocations: [options.methodSkillPath, options.methodSkillConfiguredPath],
2514
2535
  });
2515
2536
  const artifacts = await publishMergerArtifacts(admitted, accepted, coordinates, {
2516
- mergerOutput: (lastRolePayloadRecord(accepted.payloads ?? []) ?? {}),
2517
2537
  methodProvenance: options.methodProvenance,
2518
2538
  methodInvocations,
2519
2539
  });
@@ -2666,7 +2686,7 @@ export async function publishFailureArtifacts(admitted, failure, authority) {
2666
2686
  kind: "error",
2667
2687
  role: admitted.role,
2668
2688
  runId: admitted.runId,
2669
- cause: failure.cause,
2689
+ ...(failure.cause === undefined ? {} : { cause: failure.cause }),
2670
2690
  diagnostic: failure.diagnostic,
2671
2691
  ...(failure.identity === undefined ? {} : { identity: failure.identity }),
2672
2692
  ...(failure.details === undefined ? {} : { details: failure.details }),
@@ -2683,7 +2703,7 @@ export async function publishFailureArtifacts(admitted, failure, authority) {
2683
2703
  sha256: a.sha256,
2684
2704
  byteLength: a.byteLength,
2685
2705
  })),
2686
- failureCause: failure.cause,
2706
+ ...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
2687
2707
  };
2688
2708
  const evidenceWrite = await writeFailureJsonRetainingCause(evidenceCandidates, uniqueFallbackDirs, "evidence", evidencePayload,
2689
2709
  // Evidence records the same publication collisions observed placing the error body.
@@ -2750,7 +2770,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2750
2770
  // Private durable artifacts retain the original diagnostic identity (including run ID).
2751
2771
  const artifacts = await publishFailureArtifacts(admitted, failure, authority);
2752
2772
  const decisiveFacts = {
2753
- cause: failure.cause,
2773
+ ...(failure.cause === undefined ? {} : { cause: failure.cause }),
2754
2774
  diagnostic: failure.diagnostic,
2755
2775
  };
2756
2776
  if (failure.identity?.name !== undefined) {
@@ -2770,7 +2790,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2770
2790
  const roleOutcome = {
2771
2791
  kind: "failure",
2772
2792
  role: admitted.role,
2773
- cause: failure.cause,
2793
+ ...(failure.cause === undefined ? {} : { cause: failure.cause }),
2774
2794
  diagnostic: failure.diagnostic,
2775
2795
  decisiveFacts,
2776
2796
  };
@@ -2784,7 +2804,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2784
2804
  const roleOutcome = {
2785
2805
  kind: "failure",
2786
2806
  role: admitted.role,
2787
- cause: failure.cause,
2807
+ ...(failure.cause === undefined ? {} : { cause: failure.cause }),
2788
2808
  diagnostic: failure.diagnostic,
2789
2809
  decisiveFacts,
2790
2810
  };
@@ -2811,7 +2831,7 @@ export function presentFailureTerminal(terminal, io) {
2811
2831
  io.stdout(formatTerminalResult(terminal));
2812
2832
  if (terminal.roleOutcome.kind === "failure") {
2813
2833
  io.stderr(formatFailureStderrDiagnostic({
2814
- cause: terminal.roleOutcome.cause,
2834
+ ...(terminal.roleOutcome.cause === undefined ? {} : { cause: terminal.roleOutcome.cause }),
2815
2835
  diagnostic: terminal.roleOutcome.diagnostic,
2816
2836
  }));
2817
2837
  return;
@@ -33,16 +33,6 @@ export function roleResultPayloads(outcome) {
33
33
  return outcome.payloads ?? [];
34
34
  return [];
35
35
  }
36
- /** Last object payload the role actually wrote. No field remapping. */
37
- export function lastRolePayloadRecord(payloads) {
38
- for (let index = payloads.length - 1; index >= 0; index -= 1) {
39
- const payload = payloads[index];
40
- if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
41
- return payload;
42
- }
43
- }
44
- return undefined;
45
- }
46
36
  /**
47
37
  * Build a recommendation navigator fact. Model command is kept when present;
48
38
  * registry render is fallback only. Unknown seats stay recommendations (#836 B4.1).
@@ -71,7 +61,7 @@ export function formatTerminalResult(result) {
71
61
  const lines = [];
72
62
  lines.push("role\toutcome\tstatus");
73
63
  const outcomeStatus = result.roleOutcome.kind === "failure"
74
- ? result.roleOutcome.cause
64
+ ? result.roleOutcome.cause ?? ""
75
65
  : result.roleOutcome.kind === "accepted"
76
66
  ? "accepted"
77
67
  : result.roleOutcome.status;