@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

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 (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -6,54 +6,28 @@
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { dirname, join } from "node:path";
8
8
  import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
- import { writeHardenedArtifactFile } from "./auto-resume.js";
10
- import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
9
+ import { sessionFileOf } from "../role-run-placement.js";
11
10
  import { sitianReport } from "../sitian-facade.js";
11
+ import { ATTEMPT_HISTORY_IDENTITY_FIELD, TERMINAL_COURT_ATTEMPT_FIELD, writeRunTerminal, } from "../run-terminal-artifacts.js";
12
12
  import { readAttemptScopedSubmissionRows, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
13
13
  import { CONTROLLED_FAILURE_CAUSES } from "../host-contracts.js";
14
14
  import { childSignalDeathDiagnostic, processCancelDiagnostic, } from "./process-cancel.js";
15
+ import { readStrictPiSessionJsonl } from "../ledger-session-read.js";
15
16
  import { serializeThrownValue } from "../serialize-thrown-value.js";
16
17
  import { describeErrorIdentity } from "./run-lifecycle.js";
17
18
  import { readEngineDetourToolUsage, readInvocationEngineMounted, runDirectoryFromSessionDirectory, sessionFileFromSessionDirectory, withEngineDetourToolUsageFact, } from "../engine-detour-usage.js";
18
19
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.js";
19
- import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
20
+ import { homeFromRunDirectory, } from "./invocation.js";
20
21
  import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } from "../secretariat-contracts.js";
21
22
  import { findLatestDurablePackagedRoleTerminal, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, } from "../navigator-invocation-identity.js";
22
- import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, packagedSkipsGateOnInfrastructureStage, } from "../packaged-role-registry.js";
23
+ import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, } from "../packaged-role-registry.js";
23
24
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, priorReceiptContinuation, receiptAttemptPointer, } from "../receipt-delivery-policy.js";
24
- import { roleRunArtifactsDirectory } from "../role-run-placement.js";
25
- import { listSeamOwnedUniqueErrorFacePaths, uniqueErrorFallbackName, UNIQUE_ERROR_FALLBACK_STEM, RUN_TERMINAL_ARTIFACT_FILES, RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS, RUN_TERMINAL_REPORT_FILE, RUN_TERMINAL_ERROR_FILE, RUN_TERMINAL_EVIDENCE_FILE, RUN_TERMINAL_ERROR_SETTLEMENT_FILE, } from "../run-terminal-artifacts.js";
25
+ import { readStateRowsSync, runCurrentPath } from "../run-dossier.js";
26
+ import { reportRunRecord } from "../sitian-facade.js";
26
27
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
27
28
  function sealedLedgerHome(admitted) {
28
29
  return homeFromRunDirectory(admitted.runDirectory);
29
30
  }
30
- /**
31
- * Hardened artifacts directory. A planted symlink at the run directory or the
32
- * artifacts path must not receive a durable run artifact.
33
- */
34
- export async function ensureRealArtifactsDirectory(runDirectory) {
35
- const runStat = await lstat(runDirectory);
36
- if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
37
- throw new Error("run artifact retention: run directory is not a real directory");
38
- }
39
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
40
- try {
41
- const existing = await lstat(artifactsDir);
42
- if (existing.isSymbolicLink() || !existing.isDirectory()) {
43
- throw new Error("run artifact retention: artifacts path is not a real directory");
44
- }
45
- }
46
- catch (error) {
47
- if (!isEnoent(error))
48
- throw error;
49
- await mkdir(artifactsDir, { recursive: true });
50
- const created = await lstat(artifactsDir);
51
- if (created.isSymbolicLink() || !created.isDirectory()) {
52
- throw new Error("run artifact retention: artifacts directory is not a real directory");
53
- }
54
- }
55
- return artifactsDir;
56
- }
57
31
  /**
58
32
  * One retention path for a package fault beside an already chosen terminal.
59
33
  * Session append is first when the caller has that channel. The artifact file
@@ -67,13 +41,14 @@ export async function retainPackageFault(input) {
67
41
  ...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
68
42
  };
69
43
  let retentionFailure;
70
- const writeArtifact = async (appendFailure) => {
71
- const artifactsDir = await ensureRealArtifactsDirectory(input.runDirectory);
72
- await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
44
+ // The run's log is the fallback after the session append throws, and the only
45
+ // channel when the caller has none.
46
+ const writeLog = (appendFailure) => {
47
+ reportRunRecord(input.runDirectory, "post-admission-diagnostic", {
73
48
  version: 1,
74
49
  ...payload,
75
50
  ...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
76
- });
51
+ }, "settlement");
77
52
  };
78
53
  if (input.appendSession !== undefined) {
79
54
  try {
@@ -83,21 +58,21 @@ export async function retainPackageFault(input) {
83
58
  retentionFailure =
84
59
  `post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
85
60
  try {
86
- await writeArtifact(projectThrownFailureLeaf(appendError));
61
+ writeLog(projectThrownFailureLeaf(appendError));
87
62
  }
88
- catch (artifactError) {
63
+ catch (logError) {
89
64
  retentionFailure =
90
- `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
65
+ `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; log=${describeErrorIdentity(logError)}`;
91
66
  }
92
67
  }
93
68
  }
94
69
  else {
95
70
  try {
96
- await writeArtifact();
71
+ writeLog();
97
72
  }
98
- catch (artifactError) {
73
+ catch (logError) {
99
74
  retentionFailure =
100
- `post-dispatch diagnostic durable retention failed (best-effort continue): artifact=${describeErrorIdentity(artifactError)}`;
75
+ `post-dispatch diagnostic durable retention failed (best-effort continue): log=${describeErrorIdentity(logError)}`;
101
76
  }
102
77
  }
103
78
  const stderr = input.stderr ?? ((text) => {
@@ -123,7 +98,7 @@ export async function noteSettlementFault(runDirectory, scope, diagnostic) {
123
98
  export function ledgerReadScope(admitted, scope) {
124
99
  return {
125
100
  home: sealedLedgerHome(admitted),
126
- sessionParent: join(admitted.runDirectory, "session", "session.jsonl"),
101
+ sessionParent: sessionFileOf(admitted.runDirectory),
127
102
  ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
128
103
  ? {}
129
104
  : { attemptId: scope.courtAttemptId }),
@@ -292,17 +267,30 @@ async function settleNoReceiptTerminal(admitted, authority, scope, facts) {
292
267
  kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
293
268
  ...facts, decisiveFacts: facts,
294
269
  };
270
+ let attemptHistoryIdentity;
295
271
  if (scope?.recordAttemptHistory === true) {
296
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
272
+ const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), roleOutcome);
273
+ attemptHistoryIdentity = pointer.identity;
274
+ }
275
+ else {
276
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
277
+ }
278
+ if (scope?.previewOnly !== true) {
279
+ // Belonging settlement only (#1161 甲 / R2): never inherit another court's
280
+ // latest terminal pointer, and never invent a history row on re-projection (N1).
281
+ writeRunTerminal(admitted.runDirectory, "no_receipt", {
282
+ role: admitted.role,
283
+ runId: admitted.runId,
284
+ outcome: roleOutcome,
285
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
286
+ });
297
287
  }
298
- if (scope?.previewOnly !== true)
299
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
300
- return withOptionalGateProjection({
288
+ return attachEngineDetourToolUsage({
301
289
  roleOutcome,
302
290
  navigator: await extractNavigatorFactFromAdmittedSession(coordinates.sessionFile, admitted.runDirectory, scope),
303
291
  artifacts: [],
304
292
  runId: admitted.runId,
305
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
293
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
306
294
  }
307
295
  /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
308
296
  function coordinatesFromAdmitted(authority, admitted) {
@@ -638,12 +626,7 @@ export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
638
626
  * terminal note it; they do not relabel it as a host session cause.
639
627
  */
640
628
  export async function readBoundSessionEntries(sessionFile) {
641
- const text = await readFile(sessionFile, "utf8");
642
- const entries = [];
643
- for (const line of text.trim().split("\n").filter(Boolean)) {
644
- entries.push(JSON.parse(line));
645
- }
646
- return entries;
629
+ return await readStrictPiSessionJsonl(sessionFile);
647
630
  }
648
631
  /**
649
632
  * #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
@@ -652,9 +635,16 @@ export async function readBoundSessionEntries(sessionFile) {
652
635
  * second ledger mechanism is introduced.
653
636
  */
654
637
  export const ATTEMPT_HISTORY_ENTRY_TYPE = "ak_run_attempt_history";
638
+ function attemptHistorySource(admitted, coordinates) {
639
+ return {
640
+ role: admitted.role,
641
+ runId: admitted.runId,
642
+ sessionFile: coordinates.sessionFile,
643
+ };
644
+ }
655
645
  /** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
656
646
  export async function appendRunAttemptHistory(source, outcome) {
657
- sitianReport({
647
+ return sitianReport({
658
648
  level: "event",
659
649
  kind: "attempt-history",
660
650
  subject: { runId: source.runId },
@@ -737,61 +727,6 @@ function parseNavigatorAttendanceDetails(details) {
737
727
  reason: "Navigator attendance disposition is unparseable",
738
728
  };
739
729
  }
740
- /**
741
- * Project direct and historical paired gate rounds onto the public Terminal.
742
- * actualSeats derive only from accepted receipts, never expected/missing seats.
743
- */
744
- export function projectTerminalGateFact(rounds) {
745
- if (rounds.length === 0)
746
- return undefined;
747
- const seen = new Set();
748
- for (const round of rounds) {
749
- if (round.origin.kind === "historical_dispatch")
750
- seen.add("gatekeeper");
751
- seen.add(round.officer);
752
- }
753
- const actualSeats = ["gatekeeper", "inspector", "notary"].filter((seat) => seen.has(seat));
754
- return {
755
- actualSeats,
756
- rounds: rounds.map((round) => ({
757
- roundIndex: round.roundIndex,
758
- dispatch: round.origin.kind === "direct"
759
- ? { kind: "direct", officer: round.officer }
760
- : {
761
- kind: "historical_dispatch",
762
- officer: round.officer,
763
- ...(round.origin.reason === undefined
764
- ? {}
765
- : { reason: round.origin.reason }),
766
- },
767
- officer: {
768
- seat: round.officer,
769
- status: round.status,
770
- findings: round.findings,
771
- },
772
- })),
773
- };
774
- }
775
- /**
776
- * Read gate facts from the run's session/auditor-roles nest (#446/#478).
777
- * Missing directories → undefined (no-gate zero change).
778
- * Damaged discovered volumes propagate — never wash to "no gate".
779
- */
780
- export async function extractGateFactFromSessionDirectory(sessionDirectory, options = {}) {
781
- const directories = [join(sessionDirectory, "auditor-roles")];
782
- const parentSessionFile = options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
783
- const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
784
- parentSessionFile,
785
- });
786
- return projectTerminalGateFact(rounds);
787
- }
788
- /**
789
- * Attach optional gate projection onto a settled Terminal base.
790
- * Shared by every settle path so auditor-roles is scanned once here only.
791
- * `runId` is optional only for a batch projection that has no parent run.
792
- * Gate read damage is a package note beside the host terminal. It does not
793
- * replace that terminal and it does not omit the gate without a trace.
794
- */
795
730
  /**
796
731
  * #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
797
732
  * Absent when engine is not mounted; callCount 0 when mounted with zero calls.
@@ -800,27 +735,27 @@ export async function extractGateFactFromSessionDirectory(sessionDirectory, opti
800
735
  * courtAttemptId and never Pi session.jsonl toolResult join. Session damage
801
736
  * therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
802
737
  */
803
- async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext = {}) {
804
- const runDirectory = typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
805
- ? gateContext.runDirectory
738
+ async function attachEngineDetourToolUsage(base, sessionDirectory, detourContext = {}) {
739
+ const runDirectory = typeof detourContext.runDirectory === "string" && detourContext.runDirectory.length > 0
740
+ ? detourContext.runDirectory
806
741
  : runDirectoryFromSessionDirectory(sessionDirectory);
807
742
  let engineMounted = false;
808
743
  try {
809
744
  engineMounted = await readInvocationEngineMounted(runDirectory);
810
745
  }
811
746
  catch (error) {
812
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
747
+ await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
813
748
  ? undefined
814
- : { notePackageFault: gateContext.notePackageFault }, `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`);
749
+ : { notePackageFault: detourContext.notePackageFault }, `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`);
815
750
  return base;
816
751
  }
817
752
  if (!engineMounted)
818
753
  return base;
819
754
  // Public-invocation scope from the shared Host envelope (settlement scope), never
820
755
  // courtAttemptId and never a detour sidecar file.
821
- const invocationScopeId = typeof gateContext.invocationScopeId === "string" &&
822
- gateContext.invocationScopeId.length > 0
823
- ? gateContext.invocationScopeId
756
+ const invocationScopeId = typeof detourContext.invocationScopeId === "string" &&
757
+ detourContext.invocationScopeId.length > 0
758
+ ? detourContext.invocationScopeId
824
759
  : undefined;
825
760
  const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
826
761
  let usage;
@@ -833,9 +768,9 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
833
768
  });
834
769
  }
835
770
  catch (error) {
836
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
771
+ await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
837
772
  ? undefined
838
- : { notePackageFault: gateContext.notePackageFault }, `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`);
773
+ : { notePackageFault: detourContext.notePackageFault }, `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`);
839
774
  return base;
840
775
  }
841
776
  return {
@@ -843,45 +778,16 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
843
778
  roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, usage),
844
779
  };
845
780
  }
846
- /** Gate + detour projection context from admitted run + settlement scope. */
847
- function detourGateContext(admitted, scope) {
781
+ /** Engine-detour usage context from admitted run + settlement scope. */
782
+ function detourUsageContext(admitted, scope) {
848
783
  return {
849
784
  runDirectory: admitted.runDirectory,
850
- ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
851
- ? {}
852
- : { courtAttemptId: scope.courtAttemptId }),
853
785
  ...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
854
786
  ? {}
855
787
  : { invocationScopeId: scope.invocationScopeId }),
856
788
  ...(scope?.notePackageFault === undefined ? {} : { notePackageFault: scope.notePackageFault }),
857
789
  };
858
790
  }
859
- async function withOptionalGateProjection(base, sessionDirectory, gateContext = {}) {
860
- // A gate transport failure is already represented by typed evidence and has no
861
- // accepted gate cycle to project. Re-reading that rejected receipt as an
862
- // accepted cycle would replace the original failure with a projection error.
863
- const secondaryEvidence = base.roleOutcome.kind === "failure"
864
- ? base.roleOutcome.decisiveFacts.secondaryEvidence
865
- : undefined;
866
- const skipGate = isRecord(secondaryEvidence)
867
- && secondaryEvidence.kind === "role_infrastructure_failure"
868
- && packagedSkipsGateOnInfrastructureStage(secondaryEvidence.stage);
869
- let next = base;
870
- if (!skipGate) {
871
- try {
872
- const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
873
- if (gate !== undefined)
874
- next = { ...base, gate };
875
- }
876
- catch (error) {
877
- const runDirectory = gateContext.runDirectory ?? runDirectoryFromSessionDirectory(sessionDirectory);
878
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
879
- ? undefined
880
- : { notePackageFault: gateContext.notePackageFault }, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
881
- }
882
- }
883
- return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
884
- }
885
791
  function routePlaybookFailureMessage(entries) {
886
792
  for (let i = entries.length - 1; i >= 0; i -= 1) {
887
793
  const entry = entries[i];
@@ -956,85 +862,92 @@ async function extractNavigatorFactFromAdmittedSession(sessionFile, runDirectory
956
862
  }
957
863
  }
958
864
  /**
959
- * Remove one face path (file or directory collision plant).
960
- * Only missing target is silent — other errno stay loud (失败诚实).
961
- * recursive: publication may have occupied a face name as a directory (EISDIR plant).
865
+ * Sole success-terminal publisher (#953): replace the leg's terminal in
866
+ * current.json. The report carries the outcome's verdict facts; the role's
867
+ * submitted payloads live in history.jsonl. The terminal names the
868
+ * attempt-history row that belongs to this settlement — from this settle's
869
+ * append when recording, otherwise that court's prior terminal row in
870
+ * state.jsonl (#1161 甲 / R2). Never content-match payloads, never inherit the
871
+ * leg's latest terminal, never invent a history row on re-projection (N1).
962
872
  */
963
- async function removeFaceIfPresent(path) {
964
- try {
965
- await rm(path, { recursive: true, force: true });
873
+ async function publishAcceptedTerminal(admitted, roleOutcome, coordinates, scope, report) {
874
+ // #419: a dispatched turn appends before rewriting last-write-wins views;
875
+ // a later projection may refresh views but must not invent another attempt.
876
+ let attemptHistoryIdentity;
877
+ if (scope?.recordAttemptHistory === true) {
878
+ const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), roleOutcome);
879
+ attemptHistoryIdentity = pointer.identity;
966
880
  }
967
- catch (error) {
968
- // force:true already ignores ENOENT; ENOTDIR = face path not enterable
969
- // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
970
- // Other errno stay loud (失败诚实).
971
- if (isMissingPathError(error))
972
- return;
973
- throw error;
881
+ else {
882
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
974
883
  }
884
+ writeRunTerminal(admitted.runDirectory, "report", {
885
+ ...report,
886
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
887
+ });
888
+ return [{ kind: "report", path: runCurrentPath(admitted.runDirectory) }];
975
889
  }
976
- /**
977
- * #953: artifact face reflects the current terminal only.
978
- * Reader contract owns the full adoptable set (conventional trio + fixed
979
- * fallbacks + seam-owned unique). Clear every seam-owned face before the
980
- * new current publishes — including the conventional name about to be
981
- * rewritten. Retaining that name left directory collision plants in place
982
- * (supported publish input); writer then fell back while the reader stopped
983
- * at EISDIR on the stale conventional path and never adopted the fallback.
984
- * report / error / empty share one method (no per-branch face list).
985
- * empty publishes nothing; does not create a no_receipt-shaped public artifact.
986
- * Non-terminal materials stay. Face names may be directory collision plants —
987
- * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
988
- * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
989
- * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
990
- */
991
- async function clearOppositeTerminalArtifactFace(runDirectory) {
992
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
993
- for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
994
- await removeFaceIfPresent(join(artifactsDir, file));
995
- }
996
- // Fixed fallbacks + unique clear too; subsequent publish writes the new
997
- // durable path after this clear (conventional first when writable).
998
- for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
999
- await removeFaceIfPresent(join(runDirectory, relative));
1000
- }
1001
- for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
1002
- await removeFaceIfPresent(path);
1003
- }
1004
- }
1005
- async function ensureTerminalArtifactFace(runDirectory) {
1006
- const artifactsDir = await ensureRunArtifactsDir(runDirectory);
1007
- await clearOppositeTerminalArtifactFace(runDirectory);
1008
- return artifactsDir;
1009
- }
1010
- function acceptedArtifactAttachmentRefs(attachments) {
1011
- return attachments.map((a) => ({
1012
- provenancePath: a.provenancePath,
1013
- frozenPath: a.frozenPath,
1014
- sha256: a.sha256,
1015
- byteLength: a.byteLength,
1016
- }));
890
+ /** 甲 pointer + court tag carried on the terminal body for later re-projection. */
891
+ function terminalBelongingFields(attemptHistoryIdentity, scope) {
892
+ const courtAttemptId = scope?.courtAttemptId;
893
+ return {
894
+ ...(attemptHistoryIdentity === undefined
895
+ ? {}
896
+ : { [ATTEMPT_HISTORY_IDENTITY_FIELD]: attemptHistoryIdentity }),
897
+ ...(courtAttemptId === undefined || courtAttemptId.length === 0
898
+ ? {}
899
+ : { [TERMINAL_COURT_ATTEMPT_FIELD]: courtAttemptId }),
900
+ };
1017
901
  }
1018
902
  /**
1019
- * Sole success-terminal artifact publisher (#953): append attempt history,
1020
- * refresh the public terminal face, write report/evidence, return refs.
1021
- * Seat-specific structured fields stay in callers; do not fork this flow.
903
+ * Belonging attempt-history identity for a non-recording re-projection (#1161 甲 / R2).
904
+ * Court-tagged settle recovers from that court's own prior terminal row in
905
+ * state.jsonl. Untagged normal rewrite (bare resume after a sealed face, no open
906
+ * court) keeps only the face being rewritten — the last terminal row's own
907
+ * identity. If that face has none (approved gap), return undefined: never walk
908
+ * older terminals for a non-empty pointer, never invent-append (N1), never
909
+ * subject.attemptId on attempt-history. Bare resume therefore cannot restore
910
+ * court B's pointer while the current face is A (or A's gap). Court-tagged
911
+ * lookup that finds no row still returns undefined — the approved gap.
1022
912
  */
1023
- async function publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, bodies) {
1024
- // #419: a dispatched turn appends before rewriting last-write-wins views;
1025
- // a later projection may refresh views but must not invent another attempt.
1026
- if (recordAttemptHistory) {
1027
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1028
- }
1029
- const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
1030
- const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
1031
- const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
1032
- await writeFile(reportPath, `${JSON.stringify(bodies.report, null, 2)}\n`, "utf8");
1033
- await writeFile(evidencePath, `${JSON.stringify(bodies.evidence, null, 2)}\n`, "utf8");
1034
- return [
1035
- { kind: "report", path: reportPath },
1036
- { kind: "evidence", path: evidencePath },
1037
- ];
913
+ async function belongingAttemptHistoryIdentity(runDirectory, scope) {
914
+ const courtAttemptId = scope?.courtAttemptId;
915
+ let rows;
916
+ try {
917
+ rows = readStateRowsSync(runDirectory);
918
+ }
919
+ catch (error) {
920
+ // Documented: history/state failure domain does not block the terminal face
921
+ // (#1161 / main history disposition). True cause must leave a trace.
922
+ await noteSettlementFault(runDirectory, scope, `terminal state read failed beside terminal projection: ${describeErrorIdentity(error)}`);
923
+ return undefined;
924
+ }
925
+ if (courtAttemptId !== undefined && courtAttemptId.length > 0) {
926
+ let found;
927
+ for (const row of rows) {
928
+ if (row.kind !== "terminal" || !isRecord(row.payload))
929
+ continue;
930
+ const body = row.payload.body;
931
+ if (!isRecord(body) || body[TERMINAL_COURT_ATTEMPT_FIELD] !== courtAttemptId)
932
+ continue;
933
+ const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
934
+ if (typeof identity === "string" && identity.length > 0)
935
+ found = identity;
936
+ }
937
+ return found;
938
+ }
939
+ // Untagged: only the current face (last terminal). Gap face stays a gap.
940
+ for (let index = rows.length - 1; index >= 0; index -= 1) {
941
+ const row = rows[index];
942
+ if (row.kind !== "terminal" || !isRecord(row.payload))
943
+ continue;
944
+ const body = row.payload.body;
945
+ if (!isRecord(body))
946
+ return undefined;
947
+ const identity = body[ATTEMPT_HISTORY_IDENTITY_FIELD];
948
+ return typeof identity === "string" && identity.length > 0 ? identity : undefined;
949
+ }
950
+ return undefined;
1038
951
  }
1039
952
  /**
1040
953
  * Read session entries for lawful settlement.
@@ -1070,13 +983,13 @@ async function settleSealedSeat(admitted, authority, scope, options) {
1070
983
  async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, scope) {
1071
984
  const artifacts = scope?.previewOnly === true
1072
985
  ? []
1073
- : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
1074
- const terminal = await withOptionalGateProjection({
986
+ : await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
987
+ const terminal = await attachEngineDetourToolUsage({
1075
988
  roleOutcome,
1076
989
  navigator: extractNavigatorFact(entries),
1077
990
  artifacts,
1078
991
  runId: admitted.runId,
1079
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
992
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
1080
993
  return attachRecordedSubmissions(admitted, terminal, scope);
1081
994
  }
1082
995
  /**
@@ -1118,8 +1031,8 @@ function extractDoctorCandidateFacts(entries) {
1118
1031
  }
1119
1032
  return {};
1120
1033
  }
1121
- const EMPTY_ARTIFACT_FACE = { leaves: [] };
1122
- /** Artifact face for one seat. Absent means report/evidence carry only shared leaves. */
1034
+ const EMPTY_ARTIFACT_FACE = {};
1035
+ /** Terminal face for one seat. Absent means the report carries only the shared facts. */
1123
1036
  function seatArtifactFace(role) {
1124
1037
  const record = packagedRoleMetadata(role);
1125
1038
  if (record !== undefined && "artifactFace" in record && record.artifactFace !== undefined) {
@@ -1127,30 +1040,6 @@ function seatArtifactFace(role) {
1127
1040
  }
1128
1041
  return EMPTY_ARTIFACT_FACE;
1129
1042
  }
1130
- function readAdmittedPath(source, path) {
1131
- let current = source;
1132
- for (const part of path.split(".")) {
1133
- if (typeof current !== "object" || current === null)
1134
- return undefined;
1135
- current = current[part];
1136
- }
1137
- return current;
1138
- }
1139
- function evidenceLeaves(admitted, leaves) {
1140
- const evidence = {};
1141
- for (const leaf of leaves) {
1142
- if (leaf.callerProvenance === true) {
1143
- if (!admitted.instructionEmpty)
1144
- evidence[leaf.key] = admitted.instruction;
1145
- continue;
1146
- }
1147
- const value = readAdmittedPath(admitted, leaf.from ?? leaf.key);
1148
- if (value === undefined && leaf.omitUndefined === true)
1149
- continue;
1150
- evidence[leaf.key] = leaf.copyArray === true && Array.isArray(value) ? [...value] : value;
1151
- }
1152
- return evidence;
1153
- }
1154
1043
  function doctorReportFacts(face, roleOutcome, entries) {
1155
1044
  if (face.doctorReportFacts !== true || roleOutcome.kind === "audit_escalation")
1156
1045
  return {};
@@ -1160,28 +1049,19 @@ function doctorReportFacts(face, roleOutcome, entries) {
1160
1049
  ...(facts.auditNoReceipt === undefined ? {} : { auditNoReceipt: facts.auditNoReceipt }),
1161
1050
  };
1162
1051
  }
1163
- async function publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, recordAttemptHistory) {
1052
+ async function publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope) {
1164
1053
  const face = seatArtifactFace(admitted.role);
1165
1054
  const phase = face.reportPhase === true
1166
- ? { phase: readAdmittedPath(admitted, "phase") }
1055
+ ? { phase: admitted.phase }
1167
1056
  : {};
1168
- return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
1169
- report: {
1170
- role: admitted.role,
1171
- runId: admitted.runId,
1172
- ...phase,
1173
- outcome: roleOutcome,
1174
- ...doctorReportFacts(face, roleOutcome, entries),
1175
- },
1176
- evidence: {
1177
- runId: admitted.runId,
1178
- ...(face.evidenceRole === true ? { role: admitted.role } : {}),
1179
- ...evidenceLeaves(admitted, face.leaves),
1180
- sessionDirectory: coordinates.sessionDirectory,
1181
- sessionFile: coordinates.sessionFile,
1182
- admittedRequestPath: admitted.admittedRequestPath,
1183
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1184
- },
1057
+ // Payloads are the role's submitted words: they live in history.jsonl, not here.
1058
+ const { payloads: _payloads, ...verdict } = roleOutcome;
1059
+ return publishAcceptedTerminal(admitted, roleOutcome, coordinates, scope, {
1060
+ role: admitted.role,
1061
+ runId: admitted.runId,
1062
+ ...phase,
1063
+ outcome: verdict,
1064
+ ...doctorReportFacts(face, roleOutcome, entries),
1185
1065
  });
1186
1066
  }
1187
1067
  /** Latest top-level user message index; 0 when the session has none (initial attempt). */
@@ -1317,154 +1197,40 @@ async function applySecretariatCountersignTerminal(admitted, authority, settled,
1317
1197
  return settled;
1318
1198
  }
1319
1199
  /**
1320
- * After the audit gate returns, attach gate rounds and the secretariat officer
1321
- * fact onto the terminal this turn already settled. Does not publish again —
1322
- * a second publish would append another attempt-history row for the same attempt.
1200
+ * After the audit gate returns, attach the secretariat officer fact onto the
1201
+ * terminal this turn already settled. Does not publish again — a second publish
1202
+ * would settle the same attempt twice.
1323
1203
  */
1324
- export async function attachPostAuditProjection(admitted, authority, terminal, scope) {
1325
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1326
- let gate;
1327
- try {
1328
- gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
1329
- runDirectory: admitted.runDirectory,
1330
- parentSessionFile: sessionFile,
1331
- });
1332
- }
1333
- catch (error) {
1334
- await noteSettlementFault(admitted.runDirectory, scope, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
1335
- }
1336
- const withGate = gate === undefined ? terminal : { ...terminal, gate };
1204
+ export async function attachPostAuditCountersignFact(admitted, authority, terminal, scope) {
1337
1205
  if (admitted.role !== "secretariat")
1338
- return withGate;
1339
- return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
1340
- }
1341
- function publicationAttemptFromError(path, error) {
1342
- if (error instanceof Error) {
1343
- return {
1344
- path,
1345
- diagnostic: error.message || error.name || "write failed",
1346
- identity: thrownIdentity(error),
1347
- };
1348
- }
1349
- return { path, diagnostic: String(error) };
1350
- }
1351
- /**
1352
- * Directories eligible for open-ended unique failure-artifact placement.
1353
- * Always includes the ledger runs/ parent of the run directory so an
1354
- * unwritable run tree cannot strand the original controlled failure.
1355
- */
1356
- function uniqueFailureFallbackDirs(runDirectory, baseDir) {
1357
- const dirs = [];
1358
- for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
1359
- if (!dirs.includes(dir))
1360
- dirs.push(dir);
1361
- }
1362
- return dirs;
1363
- }
1364
- /**
1365
- * Resolve a writable artifacts base directory. If `artifacts/` cannot be created
1366
- * (e.g. a file occupies that name), fall back to the run directory itself.
1367
- */
1368
- async function resolveFailureArtifactsBase(runDirectory) {
1369
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1370
- try {
1371
- await ensureRunArtifactsDir(runDirectory);
1372
- return { baseDir: artifactsDir };
1373
- }
1374
- catch (error) {
1375
- return {
1376
- baseDir: runDirectory,
1377
- attempt: publicationAttemptFromError(artifactsDir, error),
1378
- };
1379
- }
1206
+ return terminal;
1207
+ return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
1380
1208
  }
1381
1209
  /**
1382
- * Write JSON across preferred paths, then unique open-ended fallbacks.
1383
- * Finite fixed names must not be able to exhaust durability and strand the
1384
- * original controlled failure outside settlement.
1210
+ * Replace the leg's terminal with the controlled failure. A write failure
1211
+ * propagates: the caller notes it beside the host terminal.
1385
1212
  */
1386
- async function writeFailureJsonRetainingCause(preferredCandidates, uniqueFallbackDirs, stem, basePayload, priorIssues) {
1387
- const issues = [...priorIssues];
1388
- const candidates = [
1389
- ...preferredCandidates,
1390
- // One unique name per fallback dir — collisions on fixed names cannot exhaust this.
1391
- ...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
1392
- ];
1393
- for (let i = 0; i < candidates.length; i += 1) {
1394
- const path = candidates[i];
1395
- const payload = issues.length === 0
1396
- ? basePayload
1397
- : { ...basePayload, publicationIssues: issues };
1398
- try {
1399
- await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1400
- return { path, issues };
1401
- }
1402
- catch (error) {
1403
- issues.push(publicationAttemptFromError(path, error));
1404
- }
1405
- }
1406
- const last = issues.at(-1);
1407
- const error = new Error(last?.diagnostic ?? "unable to write durable failure artifact");
1408
- if (last?.identity?.name !== undefined && last.identity.name !== "") {
1409
- error.name = last.identity.name;
1410
- }
1411
- if (last?.identity?.code !== undefined) {
1412
- error.code = last.identity.code;
1413
- }
1414
- error.publicationAttempts = issues;
1415
- throw error;
1416
- }
1417
- export async function publishFailureArtifacts(admitted, failure, authority, onErrorPublished, recordAttemptHistory = false) {
1418
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1419
- const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
1420
- const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
1421
- // #419: a dispatched failure joins history before fixed-name views change.
1422
- // Re-projecting an existing failure does not append. History write failure
1423
- // rides publicationIssues rather than stranding the controlled failure.
1424
- if (recordAttemptHistory) {
1213
+ export async function publishFailureTerminal(admitted, failure, coordinates, onErrorPublished, scope) {
1214
+ // #419: a dispatched failure joins history before the terminal changes. A history
1215
+ // write failure never strands the failure terminal; it is rethrown after it.
1216
+ let historyFailure;
1217
+ let historyFailed = false;
1218
+ const failureOutcome = { kind: "failure", role: admitted.role, ...failure };
1219
+ let attemptHistoryIdentity;
1220
+ if (scope?.recordAttemptHistory === true) {
1425
1221
  try {
1426
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
1427
- kind: "failure", role: admitted.role, ...failure,
1428
- });
1222
+ const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), failureOutcome);
1223
+ attemptHistoryIdentity = pointer.identity;
1429
1224
  }
1430
1225
  catch (error) {
1431
- priorIssues.push(publicationAttemptFromError(sessionFile, error));
1226
+ historyFailure = error;
1227
+ historyFailed = true;
1432
1228
  }
1433
1229
  }
1434
- // #953: clear the prior success face, but a failure here must not strand
1435
- // the original controlled cause; preserve the issue beside the fallback.
1436
- try {
1437
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
1230
+ else {
1231
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
1438
1232
  }
1439
- catch (error) {
1440
- priorIssues.push(publicationAttemptFromError(roleRunArtifactsDirectory(admitted.runDirectory), error));
1441
- }
1442
- // Prefer conventional names; unique fallback dirs keep colliding fixed paths
1443
- // from stranding the original failure outside settlement. Include the ledger
1444
- // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
1445
- const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
1446
- const uniqueFallbackDirs = uniqueFailureFallbackDirs(admitted.runDirectory, baseDir);
1447
- const errorCandidates = underArtifacts
1448
- ? [
1449
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1450
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1451
- join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1452
- ]
1453
- : [
1454
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1455
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1456
- ];
1457
- const evidenceCandidates = underArtifacts
1458
- ? [
1459
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1460
- join(baseDir, "evidence.settlement.json"),
1461
- join(admitted.runDirectory, "evidence.settlement.json"),
1462
- ]
1463
- : [
1464
- join(baseDir, "evidence.settlement.json"),
1465
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1466
- ];
1467
- const errorPayloadBase = {
1233
+ writeRunTerminal(admitted.runDirectory, "error", {
1468
1234
  kind: "error",
1469
1235
  role: admitted.role,
1470
1236
  runId: admitted.runId,
@@ -1475,24 +1241,13 @@ export async function publishFailureArtifacts(admitted, failure, authority, onEr
1475
1241
  // This package's own facts, kept beside the host's report.
1476
1242
  ...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
1477
1243
  ...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
1478
- };
1479
- const errorWrite = await writeFailureJsonRetainingCause(errorCandidates, uniqueFallbackDirs, UNIQUE_ERROR_FALLBACK_STEM, errorPayloadBase, priorIssues);
1480
- onErrorPublished?.(errorWrite.path);
1481
- const evidencePayload = {
1482
- runId: admitted.runId,
1483
- sessionDirectory: sessionDirectory,
1484
- sessionFile: sessionFile,
1485
- admittedRequestPath: admitted.admittedRequestPath,
1486
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1487
- ...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
1488
- };
1489
- const evidenceWrite = await writeFailureJsonRetainingCause(evidenceCandidates, uniqueFallbackDirs, "evidence", evidencePayload,
1490
- // Evidence records the same publication collisions observed placing the error body.
1491
- errorWrite.issues);
1492
- return [
1493
- { kind: "error", path: errorWrite.path },
1494
- { kind: "evidence", path: evidenceWrite.path },
1495
- ];
1244
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
1245
+ });
1246
+ const path = runCurrentPath(admitted.runDirectory);
1247
+ onErrorPublished?.(path);
1248
+ if (historyFailed)
1249
+ throw historyFailure;
1250
+ return [{ kind: "error", path }];
1496
1251
  }
1497
1252
  /**
1498
1253
  * Shared controlled-failure Terminal settlement (#107 ownership).
@@ -1506,7 +1261,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
1506
1261
  let artifacts = [];
1507
1262
  if (options.previewOnly !== true) {
1508
1263
  try {
1509
- artifacts = await publishFailureArtifacts(admitted, failure, authority, options.onErrorPublished, options.recordAttemptHistory === true);
1264
+ artifacts = await publishFailureTerminal(admitted, failure, coordinates, options.onErrorPublished, options);
1510
1265
  }
1511
1266
  catch (error) {
1512
1267
  await noteSettlementFault(admitted.runDirectory, options, `failure artifact publication failed beside host terminal: ${describeErrorIdentity(error)}`);
@@ -1538,7 +1293,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
1538
1293
  diagnostic: failure.diagnostic,
1539
1294
  decisiveFacts,
1540
1295
  };
1541
- return withOptionalGateProjection({ roleOutcome, navigator, artifacts, runId: admitted.runId }, sessionDirectory, detourGateContext(admitted, options));
1296
+ return attachEngineDetourToolUsage({ roleOutcome, navigator, artifacts, runId: admitted.runId }, sessionDirectory, detourUsageContext(admitted, options));
1542
1297
  }
1543
1298
  /**
1544
1299
  * Emit one complete failure Terminal on stdout and one concise stderr diagnostic.