@akagilnc/pi-workflow-roles 0.1.4673 → 0.1.4698

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.
@@ -4,7 +4,7 @@
4
4
  * Controlled failures and audit human decisions settle here without washing causes.
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
- import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { appendFile, lstat, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises";
8
8
  import { dirname, join } from "node:path";
9
9
 
10
10
  import {
@@ -138,6 +138,11 @@ import type {
138
138
  DurablePrincipalCoordinates,
139
139
  } from "../host-contracts.ts";
140
140
  import { roleRunArtifactsDirectory } from "../role-run-placement.ts";
141
+ import {
142
+ listSeamOwnedUniqueErrorFacePaths,
143
+ RUN_TERMINAL_ARTIFACT_FILES,
144
+ RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS,
145
+ } from "../run-terminal-artifacts.ts";
141
146
  import {
142
147
  ensureRunArtifactsDir,
143
148
  homeFromRunDirectory,
@@ -280,14 +285,21 @@ export function withSubmissions<T extends TerminalResult>(
280
285
  ): T {
281
286
  if (submissions.length === 0) return terminal;
282
287
  const roleOutcome = terminal.roleOutcome;
283
- // #879 keeps an already scoped this-court result; #881 full run history is
284
- // presented independently on terminal.submissions. Only fill a missing result payload.
288
+ // #881 / #836: full run history always lands on terminal.submissions.
289
+ // #879: accepted/audit_escalation may still fill a missing this-court
290
+ // payloads face from that history. #953: failure must not — prior receipts
291
+ // stay on the historical carrier only, never as the current failure's
292
+ // ordinary payloads/receipt face.
293
+ if (roleOutcome.kind === "failure") {
294
+ return { ...terminal, submissions };
295
+ }
285
296
  const withPayloads =
286
297
  roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
287
- ? { ...roleOutcome, payloads: (roleOutcome.payloads ?? []).length > 0 ? roleOutcome.payloads : submissions }
288
- : roleOutcome.kind === "failure"
289
- ? { ...roleOutcome, payloads: roleOutcome.payloads ?? submissions }
290
- : roleOutcome;
298
+ ? {
299
+ ...roleOutcome,
300
+ payloads: coalesceSubmissionRows(roleOutcome.payloads, submissions),
301
+ }
302
+ : roleOutcome;
291
303
  return { ...terminal, roleOutcome: withPayloads, submissions };
292
304
  }
293
305
 
@@ -310,6 +322,7 @@ export async function attachRecordedSubmissions<T extends TerminalResult>(
310
322
  /**
311
323
  * Host ended with no recorded submission — lawful no_receipt (exit 0).
312
324
  * Does not invent an output failure for an empty ledger.
325
+ * #953: empty public terminal face — clear every reader-adoptable prior face.
313
326
  */
314
327
  export async function settleHostEndedNoReceipt(
315
328
  admitted: AdmittedRoleInvocation,
@@ -324,6 +337,7 @@ export async function settleHostEndedNoReceipt(
324
337
  attemptPointer: `current:${admitted.runDirectory}`,
325
338
  });
326
339
  const coordinates = coordinatesFromAdmitted(authority, admitted);
340
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
327
341
  return withOptionalGateProjection(
328
342
  {
329
343
  roleOutcome: {
@@ -358,6 +372,7 @@ function coordinatesFromAdmitted(
358
372
  return authority.decode(admitted.principal);
359
373
  }
360
374
  import {
375
+ coalesceSubmissionRows,
361
376
  exitCodeForTerminalOutcome,
362
377
  formatTerminalResult,
363
378
  isLawfulTypedTerminalOutcome,
@@ -840,6 +855,16 @@ function isMissingPathError(error: unknown): boolean {
840
855
  );
841
856
  }
842
857
 
858
+ /** Face clear only: path not enterable as a face (ENOENT or file mid-path). */
859
+ function isAbsentFacePathError(error: unknown): boolean {
860
+ return (
861
+ error instanceof Error &&
862
+ "code" in error &&
863
+ ((error as { code?: unknown }).code === "ENOENT" ||
864
+ (error as { code?: unknown }).code === "ENOTDIR")
865
+ );
866
+ }
867
+
843
868
  /**
844
869
  * Preserve session-read failure identity as a typed session cause.
845
870
  * SyntaxError keeps its name so durable settlement does not wash malformed JSONL
@@ -2317,48 +2342,120 @@ async function extractNavigatorFactFromAdmittedSession(
2317
2342
  }
2318
2343
  }
2319
2344
 
2320
- export async function publishJudgeArtifacts(
2321
- admitted: AdmittedJudgeInvocation,
2345
+ /**
2346
+ * Remove one face path (file or directory collision plant).
2347
+ * Only missing target is silent — other errno stay loud (失败诚实).
2348
+ * recursive: publication may have occupied a face name as a directory (EISDIR plant).
2349
+ */
2350
+ async function removeFaceIfPresent(path: string): Promise<void> {
2351
+ try {
2352
+ await rm(path, { recursive: true, force: true });
2353
+ } catch (error) {
2354
+ // force:true already ignores ENOENT; ENOTDIR = face path not enterable
2355
+ // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
2356
+ // Other errno stay loud (失败诚实).
2357
+ if (isAbsentFacePathError(error)) return;
2358
+ throw error;
2359
+ }
2360
+ }
2361
+
2362
+ /**
2363
+ * #953: artifact face reflects the current terminal only.
2364
+ * Reader contract owns the full adoptable set (conventional trio + fixed
2365
+ * fallbacks + seam-owned unique). Clear every seam-owned face before the
2366
+ * new current publishes — including the conventional name about to be
2367
+ * rewritten. Retaining that name left directory collision plants in place
2368
+ * (supported publish input); writer then fell back while the reader stopped
2369
+ * at EISDIR on the stale conventional path and never adopted the fallback.
2370
+ * report / error / empty share one method (no per-branch face list).
2371
+ * empty publishes nothing; does not create a no_receipt-shaped public artifact.
2372
+ * Non-terminal materials stay. Face names may be directory collision plants —
2373
+ * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
2374
+ * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
2375
+ * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
2376
+ */
2377
+ async function clearOppositeTerminalArtifactFace(
2378
+ runDirectory: string,
2379
+ ): Promise<void> {
2380
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
2381
+ for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
2382
+ await removeFaceIfPresent(join(artifactsDir, file));
2383
+ }
2384
+ // Fixed fallbacks + unique clear too; subsequent publish writes the new
2385
+ // durable path after this clear (conventional first when writable).
2386
+ for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
2387
+ await removeFaceIfPresent(join(runDirectory, relative));
2388
+ }
2389
+ for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
2390
+ await removeFaceIfPresent(path);
2391
+ }
2392
+ }
2393
+
2394
+ async function ensureTerminalArtifactFace(
2395
+ runDirectory: string,
2396
+ ): Promise<string> {
2397
+ const artifactsDir = await ensureRunArtifactsDir(runDirectory);
2398
+ await clearOppositeTerminalArtifactFace(runDirectory);
2399
+ return artifactsDir;
2400
+ }
2401
+
2402
+ type AcceptedArtifactAttachment = {
2403
+ readonly provenancePath: string;
2404
+ readonly frozenPath: string;
2405
+ readonly sha256: string;
2406
+ readonly byteLength: number;
2407
+ };
2408
+
2409
+ function acceptedArtifactAttachmentRefs(
2410
+ attachments: readonly AcceptedArtifactAttachment[],
2411
+ ): AcceptedArtifactAttachment[] {
2412
+ return attachments.map((a) => ({
2413
+ provenancePath: a.provenancePath,
2414
+ frozenPath: a.frozenPath,
2415
+ sha256: a.sha256,
2416
+ byteLength: a.byteLength,
2417
+ }));
2418
+ }
2419
+
2420
+ /**
2421
+ * Sole success-terminal artifact publisher (#953): append attempt history,
2422
+ * refresh the public terminal face, write report/evidence, return refs.
2423
+ * Seat-specific structured fields stay in callers; do not fork this flow.
2424
+ */
2425
+ async function publishAcceptedTerminalArtifacts(
2426
+ admitted: {
2427
+ readonly role: TerminalRoleName;
2428
+ readonly runId: string;
2429
+ readonly runDirectory: string;
2430
+ },
2322
2431
  roleOutcome: TerminalRoleOutcome,
2323
2432
  coordinates: DurablePrincipalCoordinates,
2433
+ bodies: {
2434
+ readonly report: Record<string, unknown>;
2435
+ readonly evidence: Record<string, unknown>;
2436
+ },
2324
2437
  ): Promise<TerminalArtifactRef[]> {
2325
2438
  // #419: history first — report/evidence stay last-write-wins views only
2326
2439
  // because every attempt's complete result has already been appended.
2327
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2328
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2440
+ await appendRunAttemptHistory(
2441
+ {
2442
+ role: admitted.role,
2443
+ runId: admitted.runId,
2444
+ sessionFile: coordinates.sessionFile,
2445
+ },
2446
+ roleOutcome,
2447
+ );
2448
+ const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
2329
2449
  const reportPath = join(artifactsDir, "report.json");
2330
2450
  const evidencePath = join(artifactsDir, "evidence.json");
2331
2451
  await writeFile(
2332
2452
  reportPath,
2333
- `${JSON.stringify(
2334
- {
2335
- role: "judge",
2336
- runId: admitted.runId,
2337
- outcome: roleOutcome,
2338
- },
2339
- null,
2340
- 2,
2341
- )}\n`,
2453
+ `${JSON.stringify(bodies.report, null, 2)}\n`,
2342
2454
  "utf8",
2343
2455
  );
2344
2456
  await writeFile(
2345
2457
  evidencePath,
2346
- `${JSON.stringify(
2347
- {
2348
- runId: admitted.runId,
2349
- sessionDirectory: coordinates.sessionDirectory,
2350
- sessionFile: coordinates.sessionFile,
2351
- admittedRequestPath: admitted.admittedRequestPath,
2352
- attachments: admitted.attachments.map((a) => ({
2353
- provenancePath: a.provenancePath,
2354
- frozenPath: a.frozenPath,
2355
- sha256: a.sha256,
2356
- byteLength: a.byteLength,
2357
- })),
2358
- },
2359
- null,
2360
- 2,
2361
- )}\n`,
2458
+ `${JSON.stringify(bodies.evidence, null, 2)}\n`,
2362
2459
  "utf8",
2363
2460
  );
2364
2461
  return [
@@ -2367,6 +2464,27 @@ export async function publishJudgeArtifacts(
2367
2464
  ];
2368
2465
  }
2369
2466
 
2467
+ export async function publishJudgeArtifacts(
2468
+ admitted: AdmittedJudgeInvocation,
2469
+ roleOutcome: TerminalRoleOutcome,
2470
+ coordinates: DurablePrincipalCoordinates,
2471
+ ): Promise<TerminalArtifactRef[]> {
2472
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2473
+ report: {
2474
+ role: "judge",
2475
+ runId: admitted.runId,
2476
+ outcome: roleOutcome,
2477
+ },
2478
+ evidence: {
2479
+ runId: admitted.runId,
2480
+ sessionDirectory: coordinates.sessionDirectory,
2481
+ sessionFile: coordinates.sessionFile,
2482
+ admittedRequestPath: admitted.admittedRequestPath,
2483
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2484
+ },
2485
+ });
2486
+ }
2487
+
2370
2488
  /**
2371
2489
  * Publish lawful Coder success Artifacts on the shared #106 success interface.
2372
2490
  * Evidence records package method provenance without ambient home Skill paths.
@@ -2379,54 +2497,27 @@ export async function publishCoderArtifacts(
2379
2497
  readonly methodProvenance?: PackagedMethodSkillProvenance;
2380
2498
  } = {},
2381
2499
  ): Promise<TerminalArtifactRef[]> {
2382
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2383
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2384
- const reportPath = join(artifactsDir, "report.json");
2385
- const evidencePath = join(artifactsDir, "evidence.json");
2386
- await writeFile(
2387
- reportPath,
2388
- `${JSON.stringify(
2389
- {
2390
- role: "coder",
2391
- runId: admitted.runId,
2392
- phase: admitted.phase,
2393
- outcome: roleOutcome,
2394
- },
2395
- null,
2396
- 2,
2397
- )}\n`,
2398
- "utf8",
2399
- );
2400
- await writeFile(
2401
- evidencePath,
2402
- `${JSON.stringify(
2403
- {
2404
- runId: admitted.runId,
2405
- role: "coder",
2406
- phase: admitted.phase,
2407
- sessionDirectory: coordinates.sessionDirectory,
2408
- sessionFile: coordinates.sessionFile,
2409
- admittedRequestPath: admitted.admittedRequestPath,
2410
- taskPath: admitted.taskPath,
2411
- attachments: admitted.attachments.map((a) => ({
2412
- provenancePath: a.provenancePath,
2413
- frozenPath: a.frozenPath,
2414
- sha256: a.sha256,
2415
- byteLength: a.byteLength,
2416
- })),
2417
- ...(options.methodProvenance === undefined
2418
- ? {}
2419
- : { methodProvenance: options.methodProvenance }),
2420
- },
2421
- null,
2422
- 2,
2423
- )}\n`,
2424
- "utf8",
2425
- );
2426
- return [
2427
- { kind: "report", path: reportPath },
2428
- { kind: "evidence", path: evidencePath },
2429
- ];
2500
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2501
+ report: {
2502
+ role: "coder",
2503
+ runId: admitted.runId,
2504
+ phase: admitted.phase,
2505
+ outcome: roleOutcome,
2506
+ },
2507
+ evidence: {
2508
+ runId: admitted.runId,
2509
+ role: "coder",
2510
+ phase: admitted.phase,
2511
+ sessionDirectory: coordinates.sessionDirectory,
2512
+ sessionFile: coordinates.sessionFile,
2513
+ admittedRequestPath: admitted.admittedRequestPath,
2514
+ taskPath: admitted.taskPath,
2515
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2516
+ ...(options.methodProvenance === undefined
2517
+ ? {}
2518
+ : { methodProvenance: options.methodProvenance }),
2519
+ },
2520
+ });
2430
2521
  }
2431
2522
 
2432
2523
  /** Lawful Coder accepted outcome extracted from session (shared success interface). */
@@ -2648,59 +2739,32 @@ export async function publishFixerArtifacts(
2648
2739
  readonly methodInvocations?: readonly ObservedPackagedMethodSkillInvocation[];
2649
2740
  },
2650
2741
  ): Promise<TerminalArtifactRef[]> {
2651
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2652
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2653
- const reportPath = join(artifactsDir, "report.json");
2654
- const evidencePath = join(artifactsDir, "evidence.json");
2655
- await writeFile(
2656
- reportPath,
2657
- `${JSON.stringify(
2658
- {
2659
- role: "fixer",
2660
- runId: admitted.runId,
2661
- phase: admitted.phase,
2662
- outcome: roleOutcome,
2663
- },
2664
- null,
2665
- 2,
2666
- )}\n`,
2667
- "utf8",
2668
- );
2669
- await writeFile(
2670
- evidencePath,
2671
- `${JSON.stringify(
2672
- {
2673
- runId: admitted.runId,
2674
- role: "fixer",
2675
- phase: admitted.phase,
2676
- sessionDirectory: coordinates.sessionDirectory,
2677
- sessionFile: coordinates.sessionFile,
2678
- admittedRequestPath: admitted.admittedRequestPath,
2679
- packetPath: admitted.packetPath,
2680
- ...(admitted.prerequisitesPath === undefined
2681
- ? {}
2682
- : { prerequisitesPath: admitted.prerequisitesPath }),
2683
- prerequisites: admitted.prerequisites,
2684
- attachments: admitted.attachments.map((a) => ({
2685
- provenancePath: a.provenancePath,
2686
- frozenPath: a.frozenPath,
2687
- sha256: a.sha256,
2688
- byteLength: a.byteLength,
2689
- })),
2690
- methodProvenance: options.methodProvenance,
2691
- // Optional diagnosis: availability is package-bound; invocation only when observed.
2692
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2693
- methodInvocations: options.methodInvocations ?? [],
2694
- },
2695
- null,
2696
- 2,
2697
- )}\n`,
2698
- "utf8",
2699
- );
2700
- return [
2701
- { kind: "report", path: reportPath },
2702
- { kind: "evidence", path: evidencePath },
2703
- ];
2742
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2743
+ report: {
2744
+ role: "fixer",
2745
+ runId: admitted.runId,
2746
+ phase: admitted.phase,
2747
+ outcome: roleOutcome,
2748
+ },
2749
+ evidence: {
2750
+ runId: admitted.runId,
2751
+ role: "fixer",
2752
+ phase: admitted.phase,
2753
+ sessionDirectory: coordinates.sessionDirectory,
2754
+ sessionFile: coordinates.sessionFile,
2755
+ admittedRequestPath: admitted.admittedRequestPath,
2756
+ packetPath: admitted.packetPath,
2757
+ ...(admitted.prerequisitesPath === undefined
2758
+ ? {}
2759
+ : { prerequisitesPath: admitted.prerequisitesPath }),
2760
+ prerequisites: admitted.prerequisites,
2761
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2762
+ methodProvenance: options.methodProvenance,
2763
+ // Optional diagnosis: availability is package-bound; invocation only when observed.
2764
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2765
+ methodInvocations: options.methodInvocations ?? [],
2766
+ },
2767
+ });
2704
2768
  }
2705
2769
 
2706
2770
  /** Lawful Fixer accepted outcome extracted from session (no LLM auditor after #242). */
@@ -2774,51 +2838,24 @@ export async function publishCollectorArtifacts(
2774
2838
  roleOutcome: TerminalRoleOutcome,
2775
2839
  coordinates: DurablePrincipalCoordinates,
2776
2840
  ): Promise<TerminalArtifactRef[]> {
2777
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2778
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2779
- const reportPath = join(artifactsDir, "report.json");
2780
- const evidencePath = join(artifactsDir, "evidence.json");
2781
- await writeFile(
2782
- reportPath,
2783
- `${JSON.stringify(
2784
- {
2785
- role: "collector",
2786
- runId: admitted.runId,
2787
- outcome: roleOutcome,
2788
- },
2789
- null,
2790
- 2,
2791
- )}\n`,
2792
- "utf8",
2793
- );
2794
- await writeFile(
2795
- evidencePath,
2796
- `${JSON.stringify(
2797
- {
2798
- runId: admitted.runId,
2799
- role: "collector",
2800
- ...(admitted.prNumber === undefined ? {} : { prNumber: admitted.prNumber }),
2801
- repository: admitted.repository.canonical,
2802
- manifestDigest: admitted.manifestDigest,
2803
- sessionDirectory: coordinates.sessionDirectory,
2804
- sessionFile: coordinates.sessionFile,
2805
- admittedRequestPath: admitted.admittedRequestPath,
2806
- attachments: admitted.attachments.map((a) => ({
2807
- provenancePath: a.provenancePath,
2808
- frozenPath: a.frozenPath,
2809
- sha256: a.sha256,
2810
- byteLength: a.byteLength,
2811
- })),
2812
- },
2813
- null,
2814
- 2,
2815
- )}\n`,
2816
- "utf8",
2817
- );
2818
- return [
2819
- { kind: "report", path: reportPath },
2820
- { kind: "evidence", path: evidencePath },
2821
- ];
2841
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2842
+ report: {
2843
+ role: "collector",
2844
+ runId: admitted.runId,
2845
+ outcome: roleOutcome,
2846
+ },
2847
+ evidence: {
2848
+ runId: admitted.runId,
2849
+ role: "collector",
2850
+ ...(admitted.prNumber === undefined ? {} : { prNumber: admitted.prNumber }),
2851
+ repository: admitted.repository.canonical,
2852
+ manifestDigest: admitted.manifestDigest,
2853
+ sessionDirectory: coordinates.sessionDirectory,
2854
+ sessionFile: coordinates.sessionFile,
2855
+ admittedRequestPath: admitted.admittedRequestPath,
2856
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2857
+ },
2858
+ });
2822
2859
  }
2823
2860
 
2824
2861
  /** Lawful Collector accepted outcome extracted from session. */
@@ -2952,53 +2989,28 @@ export async function publishDoctorArtifacts(
2952
2989
  readonly auditNoReceipt?: unknown;
2953
2990
  } = {},
2954
2991
  ): Promise<TerminalArtifactRef[]> {
2955
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2956
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2957
- const reportPath = join(artifactsDir, "report.json");
2958
- const evidencePath = join(artifactsDir, "evidence.json");
2959
- await writeFile(
2960
- reportPath,
2961
- `${JSON.stringify(
2962
- {
2963
- role: "doctor",
2964
- runId: admitted.runId,
2965
- outcome: roleOutcome,
2966
- ...(options.cost === undefined ? {} : { cost: options.cost }),
2967
- ...(options.auditNoReceipt === undefined ? {} : { auditNoReceipt: options.auditNoReceipt }),
2968
- },
2969
- null,
2970
- 2,
2971
- )}\n`,
2972
- "utf8",
2973
- );
2974
- await writeFile(
2975
- evidencePath,
2976
- `${JSON.stringify(
2977
- {
2978
- runId: admitted.runId,
2979
- role: "doctor",
2980
- issueNumber: admitted.issueNumber,
2981
- caseRunsPath: admitted.caseRunsPath,
2982
- caseIdentity: admitted.caseIdentity,
2983
- sessionDirectory: coordinates.sessionDirectory,
2984
- sessionFile: coordinates.sessionFile,
2985
- admittedRequestPath: admitted.admittedRequestPath,
2986
- attachments: admitted.attachments.map((a) => ({
2987
- provenancePath: a.provenancePath,
2988
- frozenPath: a.frozenPath,
2989
- sha256: a.sha256,
2990
- byteLength: a.byteLength,
2991
- })),
2992
- },
2993
- null,
2994
- 2,
2995
- )}\n`,
2996
- "utf8",
2997
- );
2998
- return [
2999
- { kind: "report", path: reportPath },
3000
- { kind: "evidence", path: evidencePath },
3001
- ];
2992
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2993
+ report: {
2994
+ role: "doctor",
2995
+ runId: admitted.runId,
2996
+ outcome: roleOutcome,
2997
+ ...(options.cost === undefined ? {} : { cost: options.cost }),
2998
+ ...(options.auditNoReceipt === undefined
2999
+ ? {}
3000
+ : { auditNoReceipt: options.auditNoReceipt }),
3001
+ },
3002
+ evidence: {
3003
+ runId: admitted.runId,
3004
+ role: "doctor",
3005
+ issueNumber: admitted.issueNumber,
3006
+ caseRunsPath: admitted.caseRunsPath,
3007
+ caseIdentity: admitted.caseIdentity,
3008
+ sessionDirectory: coordinates.sessionDirectory,
3009
+ sessionFile: coordinates.sessionFile,
3010
+ admittedRequestPath: admitted.admittedRequestPath,
3011
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
3012
+ },
3013
+ });
3002
3014
  }
3003
3015
 
3004
3016
  /** Lawful Doctor accepted/refused/audit_escalation outcome extracted from session. */
@@ -3108,6 +3120,36 @@ function currentAttemptStartIndex(entries: readonly SessionEntry[]): number {
3108
3120
  return 0;
3109
3121
  }
3110
3122
 
3123
+ /**
3124
+ * Filed-seat success face via the sole accepted publisher (#953).
3125
+ */
3126
+ async function publishSeatAcceptedArtifacts(
3127
+ admitted: {
3128
+ readonly role: TerminalRoleName;
3129
+ readonly runId: string;
3130
+ readonly runDirectory: string;
3131
+ readonly admittedRequestPath: string;
3132
+ readonly attachments: readonly AcceptedArtifactAttachment[];
3133
+ },
3134
+ roleOutcome: TerminalRoleOutcome,
3135
+ coordinates: DurablePrincipalCoordinates,
3136
+ ): Promise<TerminalArtifactRef[]> {
3137
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
3138
+ report: {
3139
+ role: admitted.role,
3140
+ runId: admitted.runId,
3141
+ outcome: roleOutcome,
3142
+ },
3143
+ evidence: {
3144
+ runId: admitted.runId,
3145
+ sessionDirectory: coordinates.sessionDirectory,
3146
+ sessionFile: coordinates.sessionFile,
3147
+ admittedRequestPath: admitted.admittedRequestPath,
3148
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
3149
+ },
3150
+ });
3151
+ }
3152
+
3111
3153
  async function settleLawfulSeatAcceptedTerminalResult(
3112
3154
  admitted:
3113
3155
  | AdmittedNotaryInvocation
@@ -3164,12 +3206,17 @@ async function settleLawfulSeatAcceptedTerminalResult(
3164
3206
  const roleOutcome = await closedLedgerOutcome(admitted, spec.role as TerminalRoleName, scope);
3165
3207
  if (roleOutcome !== undefined && (thisAttemptHasSeatSuccess || residual === undefined)) {
3166
3208
  const navigator = extractNavigatorFact(entries);
3209
+ const artifacts = await publishSeatAcceptedArtifacts(
3210
+ admitted,
3211
+ roleOutcome,
3212
+ coordinates,
3213
+ );
3167
3214
  return withSubmissions(
3168
3215
  await withOptionalGateProjection(
3169
3216
  {
3170
3217
  roleOutcome,
3171
3218
  navigator,
3172
- artifacts: [],
3219
+ artifacts,
3173
3220
  runId: admitted.runId,
3174
3221
  },
3175
3222
  sessionDirectory,
@@ -3625,58 +3672,31 @@ export async function publishReviewerArtifacts(
3625
3672
  readonly methodInvocations?: readonly ObservedPackagedMethodSkillInvocation[];
3626
3673
  },
3627
3674
  ): Promise<TerminalArtifactRef[]> {
3628
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
3629
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
3630
- const reportPath = join(artifactsDir, "report.json");
3631
- const evidencePath = join(artifactsDir, "evidence.json");
3632
- await writeFile(
3633
- reportPath,
3634
- `${JSON.stringify(
3635
- {
3636
- role: "reviewer",
3637
- runId: admitted.runId,
3638
- outcome: roleOutcome,
3639
- },
3640
- null,
3641
- 2,
3642
- )}\n`,
3643
- "utf8",
3644
- );
3645
- await writeFile(
3646
- evidencePath,
3647
- `${JSON.stringify(
3648
- {
3649
- runId: admitted.runId,
3650
- role: "reviewer",
3651
- sessionDirectory: coordinates.sessionDirectory,
3652
- sessionFile: coordinates.sessionFile,
3653
- admittedRequestPath: admitted.admittedRequestPath,
3654
- baseRevision: admitted.baseRevision,
3655
- lens: admitted.lens,
3656
- authorityRefs: [...admitted.authorityRefs],
3657
- ...(admitted.instructionEmpty
3658
- ? {}
3659
- : { callerProvenance: admitted.instruction }),
3660
- attachments: admitted.attachments.map((a) => ({
3661
- provenancePath: a.provenancePath,
3662
- frozenPath: a.frozenPath,
3663
- sha256: a.sha256,
3664
- byteLength: a.byteLength,
3665
- })),
3666
- methodProvenance: options.methodProvenance,
3667
- // Forced package method: availability is package-bound; expansion only when observed.
3668
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
3669
- methodInvocations: options.methodInvocations ?? [],
3670
- },
3671
- null,
3672
- 2,
3673
- )}\n`,
3674
- "utf8",
3675
- );
3676
- return [
3677
- { kind: "report", path: reportPath },
3678
- { kind: "evidence", path: evidencePath },
3679
- ];
3675
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
3676
+ report: {
3677
+ role: "reviewer",
3678
+ runId: admitted.runId,
3679
+ outcome: roleOutcome,
3680
+ },
3681
+ evidence: {
3682
+ runId: admitted.runId,
3683
+ role: "reviewer",
3684
+ sessionDirectory: coordinates.sessionDirectory,
3685
+ sessionFile: coordinates.sessionFile,
3686
+ admittedRequestPath: admitted.admittedRequestPath,
3687
+ baseRevision: admitted.baseRevision,
3688
+ lens: admitted.lens,
3689
+ authorityRefs: [...admitted.authorityRefs],
3690
+ ...(admitted.instructionEmpty
3691
+ ? {}
3692
+ : { callerProvenance: admitted.instruction }),
3693
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
3694
+ methodProvenance: options.methodProvenance,
3695
+ // Forced package method: availability is package-bound; expansion only when observed.
3696
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
3697
+ methodInvocations: options.methodInvocations ?? [],
3698
+ },
3699
+ });
3680
3700
  }
3681
3701
 
3682
3702
  /** Lawful Reviewer accepted outcome extracted from session (no LLM auditor after #495 S6). */
@@ -3799,53 +3819,26 @@ export async function publishMergerArtifacts(
3799
3819
  readonly methodInvocations?: readonly ObservedPackagedMethodSkillInvocation[];
3800
3820
  },
3801
3821
  ): Promise<TerminalArtifactRef[]> {
3802
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
3803
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
3804
- const reportPath = join(artifactsDir, "report.json");
3805
- const evidencePath = join(artifactsDir, "evidence.json");
3806
- await writeFile(
3807
- reportPath,
3808
- `${JSON.stringify(
3809
- {
3810
- role: "merger",
3811
- runId: admitted.runId,
3812
- outcome: roleOutcome,
3813
- },
3814
- null,
3815
- 2,
3816
- )}\n`,
3817
- "utf8",
3818
- );
3819
- await writeFile(
3820
- evidencePath,
3821
- `${JSON.stringify(
3822
- {
3823
- runId: admitted.runId,
3824
- role: "merger",
3825
- sessionDirectory: coordinates.sessionDirectory,
3826
- sessionFile: coordinates.sessionFile,
3827
- admittedRequestPath: admitted.admittedRequestPath,
3828
- mergerInputPath: admitted.mergerInputPath,
3829
- derived: admitted.derived,
3830
- attachments: admitted.attachments.map((a) => ({
3831
- provenancePath: a.provenancePath,
3832
- frozenPath: a.frozenPath,
3833
- sha256: a.sha256,
3834
- byteLength: a.byteLength,
3835
- })),
3836
- methodProvenance: options.methodProvenance,
3837
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
3838
- methodInvocations: options.methodInvocations ?? [],
3839
- },
3840
- null,
3841
- 2,
3842
- )}\n`,
3843
- "utf8",
3844
- );
3845
- return [
3846
- { kind: "report", path: reportPath },
3847
- { kind: "evidence", path: evidencePath },
3848
- ];
3822
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
3823
+ report: {
3824
+ role: "merger",
3825
+ runId: admitted.runId,
3826
+ outcome: roleOutcome,
3827
+ },
3828
+ evidence: {
3829
+ runId: admitted.runId,
3830
+ role: "merger",
3831
+ sessionDirectory: coordinates.sessionDirectory,
3832
+ sessionFile: coordinates.sessionFile,
3833
+ admittedRequestPath: admitted.admittedRequestPath,
3834
+ mergerInputPath: admitted.mergerInputPath,
3835
+ derived: admitted.derived,
3836
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
3837
+ methodProvenance: options.methodProvenance,
3838
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
3839
+ methodInvocations: options.methodInvocations ?? [],
3840
+ },
3841
+ });
3849
3842
  }
3850
3843
 
3851
3844
  /** Lawful Merger accepted outcome extracted from session (shared success interface). */
@@ -4080,6 +4073,21 @@ export async function publishFailureArtifacts(
4080
4073
  );
4081
4074
  const priorIssues: PublicationAttempt[] =
4082
4075
  baseAttempt === undefined ? [] : [baseAttempt];
4076
+ // #953: failure face must not leave a prior success report as the durable view.
4077
+ // Clear failure must not strand the original controlled failure outside
4078
+ // settlement — same as history failure below, it rides publicationIssues
4079
+ // and unique fallback still places the durable error (locked artifacts/ +
4080
+ // prior face must not abort publishFailureArtifacts).
4081
+ try {
4082
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
4083
+ } catch (error) {
4084
+ priorIssues.push(
4085
+ publicationAttemptFromError(
4086
+ roleRunArtifactsDirectory(admitted.runDirectory),
4087
+ error,
4088
+ ),
4089
+ );
4090
+ }
4083
4091
  // #419: each attempt's complete failure result joins the appended history
4084
4092
  // before any fixed-name artifact view is rewritten. History failure must not
4085
4093
  // strand the original controlled failure outside settlement — it rides
@@ -4197,35 +4205,52 @@ export async function settleFailureTerminalResult(
4197
4205
  entry.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE || entry.message?.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE);
4198
4206
  const raw = lifecycleEntry?.data ?? lifecycleEntry?.message?.details;
4199
4207
  if (raw !== undefined) {
4208
+ // Catch only covers lifecycle-byte parse. Clear / navigator / gate I/O
4209
+ // after a valid parse must keep their real failure identity (#953).
4210
+ let facts: NoReceiptLifecycleFacts | undefined;
4200
4211
  try {
4201
- const facts = parseNoReceiptLifecycleFacts(raw);
4202
- if (facts.runPointer === admitted.runDirectory && facts.attemptPointer === `current:${admitted.runDirectory}`) {
4203
- let decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
4204
- // #676 D1/J3: project durable bind-target rejection onto public no_receipt facts.
4205
- if (admitted.role === "collector") {
4206
- const bindRejection = extractCollectorTargetBindRejection(entries.slice(attemptStart));
4207
- if (bindRejection !== undefined) {
4208
- decisiveFacts = {
4209
- ...facts,
4210
- targetBindRejected: true,
4211
- targetBindDiagnostic: bindRejection.diagnostic,
4212
- ...(bindRejection.code === undefined ? {} : { targetBindCode: bindRejection.code }),
4213
- };
4214
- }
4212
+ facts = parseNoReceiptLifecycleFacts(raw);
4213
+ } catch {
4214
+ /* malformed lifecycle bytes remain the existing nonzero output failure */
4215
+ }
4216
+ if (
4217
+ facts !== undefined &&
4218
+ facts.runPointer === admitted.runDirectory &&
4219
+ facts.attemptPointer === `current:${admitted.runDirectory}`
4220
+ ) {
4221
+ let decisiveFacts: NoReceiptLifecycleFacts & Record<string, unknown> = facts;
4222
+ // #676 D1/J3: project durable bind-target rejection onto public no_receipt facts.
4223
+ if (admitted.role === "collector") {
4224
+ const bindRejection = extractCollectorTargetBindRejection(entries.slice(attemptStart));
4225
+ if (bindRejection !== undefined) {
4226
+ decisiveFacts = {
4227
+ ...facts,
4228
+ targetBindRejected: true,
4229
+ targetBindDiagnostic: bindRejection.diagnostic,
4230
+ ...(bindRejection.code === undefined ? {} : { targetBindCode: bindRejection.code }),
4231
+ };
4215
4232
  }
4216
- // #478: no_receipt is still a public Terminal — project accepted gate facts.
4217
- return withOptionalGateProjection(
4218
- {
4219
- roleOutcome: { kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt", ...facts, decisiveFacts },
4220
- navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
4221
- artifacts: [],
4222
- runId: admitted.runId,
4223
- },
4224
- sessionDirectory,
4225
- detourGateContext(admitted, options),
4226
- );
4227
4233
  }
4228
- } catch { /* malformed lifecycle bytes remain the existing nonzero output failure */ }
4234
+ // #478: no_receipt is still a public Terminal — project accepted gate facts.
4235
+ // #953: empty public terminal face — clear every reader-adoptable prior face.
4236
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
4237
+ return withOptionalGateProjection(
4238
+ {
4239
+ roleOutcome: {
4240
+ kind: "no_receipt",
4241
+ role: admitted.role,
4242
+ status: "no-accepted-receipt",
4243
+ ...facts,
4244
+ decisiveFacts,
4245
+ },
4246
+ navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
4247
+ artifacts: [],
4248
+ runId: admitted.runId,
4249
+ },
4250
+ sessionDirectory,
4251
+ detourGateContext(admitted, options),
4252
+ );
4253
+ }
4229
4254
  }
4230
4255
  }
4231
4256
  }