@akagilnc/pi-workflow-roles 0.1.5504 → 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 (89) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +5965 -5994
  4. package/dist/analyst-gate-cycles-read.js +23 -55
  5. package/dist/analyst-ledger.js +21 -35
  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-pointer.js +38 -30
  9. package/dist/auditor-dossier-tool.js +4 -3
  10. package/dist/auditor-soul.js +3 -11
  11. package/dist/doctor-evidence.js +3 -3
  12. package/dist/engine-detour-usage.js +6 -14
  13. package/dist/headless-host/production-host.js +5316 -5324
  14. package/dist/host-descriptions.js +0 -4
  15. package/dist/host-session-record.js +266 -267
  16. package/dist/migrate-book-topology.js +1314 -696
  17. package/dist/notary-source-run.js +1 -1
  18. package/dist/packaged-role-registry.js +5 -44
  19. package/dist/pi/role-turn-host.js +53 -3
  20. package/dist/public-cli/auto-resume.js +8 -47
  21. package/dist/public-cli/instruction-seat-run.js +5 -3
  22. package/dist/public-cli/invocation.js +51 -56
  23. package/dist/public-cli/main.js +1916 -1748
  24. package/dist/public-cli/post-admission.js +96 -55
  25. package/dist/public-cli/run-lifecycle.js +27 -53
  26. package/dist/public-cli/settlement.js +157 -302
  27. package/dist/role-run-placement.js +1 -11
  28. package/dist/role-run-relocation.js +72 -50
  29. package/dist/run-dossier-files.js +30 -0
  30. package/dist/run-dossier.js +322 -0
  31. package/dist/run-terminal-artifacts.js +68 -197
  32. package/dist/run-ticket-number.js +16 -3
  33. package/dist/session-identity.js +49 -18
  34. package/dist/sitian-appender.js +13 -16
  35. package/dist/sitian-facade.js +11 -0
  36. package/dist/sitian-reader.js +44 -29
  37. package/dist/submission-gate.js +1 -1
  38. package/dist/ticket-provenance.js +22 -7
  39. package/package.json +1 -1
  40. package/scripts/build-package.mjs +2 -0
  41. package/src/acp-host/production-host.ts +1 -1
  42. package/src/acp-host/role-turn-host.ts +9 -2
  43. package/src/analyst-gate-cycles-read.ts +26 -60
  44. package/src/analyst-ledger.ts +27 -42
  45. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  46. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  47. package/src/analyst-metric-families/round-timeline.ts +2 -5
  48. package/src/analyst-page.ts +3 -3
  49. package/src/archivist-record-entry.ts +2 -2
  50. package/src/archivist-record-pointer.ts +50 -36
  51. package/src/auditor-dossier-tool.ts +7 -5
  52. package/src/auditor-soul.ts +3 -9
  53. package/src/book-topology-migration-placement.ts +16 -3
  54. package/src/book-topology-record-class-migrators.ts +4 -2
  55. package/src/book-topology-runs-migrator.ts +6 -3
  56. package/src/doctor-contracts.ts +1 -1
  57. package/src/doctor-evidence.ts +2 -2
  58. package/src/engine-detour-tool.ts +1 -1
  59. package/src/engine-detour-usage.ts +6 -16
  60. package/src/external-host-turn-loop.ts +25 -0
  61. package/src/headless-host/production-host.ts +1 -4
  62. package/src/headless-host/role-turn-host.ts +54 -20
  63. package/src/host-descriptions.ts +0 -6
  64. package/src/host-session-record.ts +224 -140
  65. package/src/notary-source-run.ts +1 -1
  66. package/src/packaged-role-registry.ts +1 -50
  67. package/src/pi/adapter.ts +50 -7
  68. package/src/pi/role-turn-host.ts +51 -3
  69. package/src/public-cli/auto-resume.ts +7 -43
  70. package/src/public-cli/instruction-seat-run.ts +9 -2
  71. package/src/public-cli/invocation.ts +61 -87
  72. package/src/public-cli/post-admission.ts +111 -72
  73. package/src/public-cli/run-lifecycle.ts +27 -72
  74. package/src/public-cli/settlement.ts +193 -418
  75. package/src/public-cli/terminal.ts +1 -1
  76. package/src/role-run-placement.ts +2 -13
  77. package/src/role-run-relocation.ts +71 -68
  78. package/src/role-runtime.ts +6 -10
  79. package/src/run-dossier-files.ts +33 -0
  80. package/src/run-dossier.ts +357 -0
  81. package/src/run-terminal-artifacts.ts +83 -237
  82. package/src/run-ticket-number.ts +41 -11
  83. package/src/session-identity.ts +52 -20
  84. package/src/sitian-appender.ts +17 -18
  85. package/src/sitian-facade.ts +22 -0
  86. package/src/sitian-reader.ts +62 -32
  87. package/src/submission-gate.ts +1 -1
  88. package/src/ticket-provenance.ts +20 -7
  89. package/src/ticket-trajectory.ts +10 -10
@@ -7,8 +7,8 @@ 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
9
  import { sessionFileOf } from "../role-run-placement.js";
10
- import { writeHardenedArtifactFile } from "./auto-resume.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";
@@ -17,44 +17,17 @@ import { serializeThrownValue } from "../serialize-thrown-value.js";
17
17
  import { describeErrorIdentity } from "./run-lifecycle.js";
18
18
  import { readEngineDetourToolUsage, readInvocationEngineMounted, runDirectoryFromSessionDirectory, sessionFileFromSessionDirectory, withEngineDetourToolUsageFact, } from "../engine-detour-usage.js";
19
19
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.js";
20
- import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
20
+ import { homeFromRunDirectory, } from "./invocation.js";
21
21
  import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } from "../secretariat-contracts.js";
22
22
  import { findLatestDurablePackagedRoleTerminal, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, } from "../navigator-invocation-identity.js";
23
23
  import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, } from "../packaged-role-registry.js";
24
24
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, priorReceiptContinuation, receiptAttemptPointer, } from "../receipt-delivery-policy.js";
25
- import { roleRunArtifactsDirectory } from "../role-run-placement.js";
26
- 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";
27
27
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
28
28
  function sealedLedgerHome(admitted) {
29
29
  return homeFromRunDirectory(admitted.runDirectory);
30
30
  }
31
- /**
32
- * Hardened artifacts directory. A planted symlink at the run directory or the
33
- * artifacts path must not receive a durable run artifact.
34
- */
35
- export async function ensureRealArtifactsDirectory(runDirectory) {
36
- const runStat = await lstat(runDirectory);
37
- if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
38
- throw new Error("run artifact retention: run directory is not a real directory");
39
- }
40
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
41
- try {
42
- const existing = await lstat(artifactsDir);
43
- if (existing.isSymbolicLink() || !existing.isDirectory()) {
44
- throw new Error("run artifact retention: artifacts path is not a real directory");
45
- }
46
- }
47
- catch (error) {
48
- if (!isEnoent(error))
49
- throw error;
50
- await mkdir(artifactsDir, { recursive: true });
51
- const created = await lstat(artifactsDir);
52
- if (created.isSymbolicLink() || !created.isDirectory()) {
53
- throw new Error("run artifact retention: artifacts directory is not a real directory");
54
- }
55
- }
56
- return artifactsDir;
57
- }
58
31
  /**
59
32
  * One retention path for a package fault beside an already chosen terminal.
60
33
  * Session append is first when the caller has that channel. The artifact file
@@ -68,13 +41,14 @@ export async function retainPackageFault(input) {
68
41
  ...(Object.hasOwn(input, "error") ? { failure: projectThrownFailureLeaf(input.error) } : {}),
69
42
  };
70
43
  let retentionFailure;
71
- const writeArtifact = async (appendFailure) => {
72
- const artifactsDir = await ensureRealArtifactsDirectory(input.runDirectory);
73
- 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", {
74
48
  version: 1,
75
49
  ...payload,
76
50
  ...(appendFailure === undefined ? {} : { retentionFailure: appendFailure }),
77
- });
51
+ }, "settlement");
78
52
  };
79
53
  if (input.appendSession !== undefined) {
80
54
  try {
@@ -84,21 +58,21 @@ export async function retainPackageFault(input) {
84
58
  retentionFailure =
85
59
  `post-dispatch diagnostic session append failed (best-effort continue): dossier=${describeErrorIdentity(appendError)}`;
86
60
  try {
87
- await writeArtifact(projectThrownFailureLeaf(appendError));
61
+ writeLog(projectThrownFailureLeaf(appendError));
88
62
  }
89
- catch (artifactError) {
63
+ catch (logError) {
90
64
  retentionFailure =
91
- `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)}`;
92
66
  }
93
67
  }
94
68
  }
95
69
  else {
96
70
  try {
97
- await writeArtifact();
71
+ writeLog();
98
72
  }
99
- catch (artifactError) {
73
+ catch (logError) {
100
74
  retentionFailure =
101
- `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)}`;
102
76
  }
103
77
  }
104
78
  const stderr = input.stderr ?? ((text) => {
@@ -293,11 +267,24 @@ async function settleNoReceiptTerminal(admitted, authority, scope, facts) {
293
267
  kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt",
294
268
  ...facts, decisiveFacts: facts,
295
269
  };
270
+ let attemptHistoryIdentity;
296
271
  if (scope?.recordAttemptHistory === true) {
297
- 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
+ });
298
287
  }
299
- if (scope?.previewOnly !== true)
300
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
301
288
  return attachEngineDetourToolUsage({
302
289
  roleOutcome,
303
290
  navigator: await extractNavigatorFactFromAdmittedSession(coordinates.sessionFile, admitted.runDirectory, scope),
@@ -648,9 +635,16 @@ export async function readBoundSessionEntries(sessionFile) {
648
635
  * second ledger mechanism is introduced.
649
636
  */
650
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
+ }
651
645
  /** Append the complete attempt to the package ledger before overwriting pointer artifacts (#419). */
652
646
  export async function appendRunAttemptHistory(source, outcome) {
653
- sitianReport({
647
+ return sitianReport({
654
648
  level: "event",
655
649
  kind: "attempt-history",
656
650
  subject: { runId: source.runId },
@@ -868,85 +862,92 @@ async function extractNavigatorFactFromAdmittedSession(sessionFile, runDirectory
868
862
  }
869
863
  }
870
864
  /**
871
- * Remove one face path (file or directory collision plant).
872
- * Only missing target is silent — other errno stay loud (失败诚实).
873
- * 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).
874
872
  */
875
- async function removeFaceIfPresent(path) {
876
- try {
877
- 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;
878
880
  }
879
- catch (error) {
880
- // force:true already ignores ENOENT; ENOTDIR = face path not enterable
881
- // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
882
- // Other errno stay loud (失败诚实).
883
- if (isMissingPathError(error))
884
- return;
885
- throw error;
881
+ else {
882
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
886
883
  }
884
+ writeRunTerminal(admitted.runDirectory, "report", {
885
+ ...report,
886
+ ...terminalBelongingFields(attemptHistoryIdentity, scope),
887
+ });
888
+ return [{ kind: "report", path: runCurrentPath(admitted.runDirectory) }];
887
889
  }
888
- /**
889
- * #953: artifact face reflects the current terminal only.
890
- * Reader contract owns the full adoptable set (conventional trio + fixed
891
- * fallbacks + seam-owned unique). Clear every seam-owned face before the
892
- * new current publishes — including the conventional name about to be
893
- * rewritten. Retaining that name left directory collision plants in place
894
- * (supported publish input); writer then fell back while the reader stopped
895
- * at EISDIR on the stale conventional path and never adopted the fallback.
896
- * report / error / empty share one method (no per-branch face list).
897
- * empty publishes nothing; does not create a no_receipt-shaped public artifact.
898
- * Non-terminal materials stay. Face names may be directory collision plants —
899
- * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
900
- * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
901
- * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
902
- */
903
- async function clearOppositeTerminalArtifactFace(runDirectory) {
904
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
905
- for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
906
- await removeFaceIfPresent(join(artifactsDir, file));
907
- }
908
- // Fixed fallbacks + unique clear too; subsequent publish writes the new
909
- // durable path after this clear (conventional first when writable).
910
- for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
911
- await removeFaceIfPresent(join(runDirectory, relative));
912
- }
913
- for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
914
- await removeFaceIfPresent(path);
915
- }
916
- }
917
- async function ensureTerminalArtifactFace(runDirectory) {
918
- const artifactsDir = await ensureRunArtifactsDir(runDirectory);
919
- await clearOppositeTerminalArtifactFace(runDirectory);
920
- return artifactsDir;
921
- }
922
- function acceptedArtifactAttachmentRefs(attachments) {
923
- return attachments.map((a) => ({
924
- provenancePath: a.provenancePath,
925
- frozenPath: a.frozenPath,
926
- sha256: a.sha256,
927
- byteLength: a.byteLength,
928
- }));
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
+ };
929
901
  }
930
902
  /**
931
- * Sole success-terminal artifact publisher (#953): append attempt history,
932
- * refresh the public terminal face, write report/evidence, return refs.
933
- * 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.
934
912
  */
935
- async function publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, bodies) {
936
- // #419: a dispatched turn appends before rewriting last-write-wins views;
937
- // a later projection may refresh views but must not invent another attempt.
938
- if (recordAttemptHistory) {
939
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
940
- }
941
- const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
942
- const reportPath = join(artifactsDir, RUN_TERMINAL_REPORT_FILE);
943
- const evidencePath = join(artifactsDir, RUN_TERMINAL_EVIDENCE_FILE);
944
- await writeFile(reportPath, `${JSON.stringify(bodies.report, null, 2)}\n`, "utf8");
945
- await writeFile(evidencePath, `${JSON.stringify(bodies.evidence, null, 2)}\n`, "utf8");
946
- return [
947
- { kind: "report", path: reportPath },
948
- { kind: "evidence", path: evidencePath },
949
- ];
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;
950
951
  }
951
952
  /**
952
953
  * Read session entries for lawful settlement.
@@ -982,7 +983,7 @@ async function settleSealedSeat(admitted, authority, scope, options) {
982
983
  async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, scope) {
983
984
  const artifacts = scope?.previewOnly === true
984
985
  ? []
985
- : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
986
+ : await publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope);
986
987
  const terminal = await attachEngineDetourToolUsage({
987
988
  roleOutcome,
988
989
  navigator: extractNavigatorFact(entries),
@@ -1030,8 +1031,8 @@ function extractDoctorCandidateFacts(entries) {
1030
1031
  }
1031
1032
  return {};
1032
1033
  }
1033
- const EMPTY_ARTIFACT_FACE = { leaves: [] };
1034
- /** 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. */
1035
1036
  function seatArtifactFace(role) {
1036
1037
  const record = packagedRoleMetadata(role);
1037
1038
  if (record !== undefined && "artifactFace" in record && record.artifactFace !== undefined) {
@@ -1039,30 +1040,6 @@ function seatArtifactFace(role) {
1039
1040
  }
1040
1041
  return EMPTY_ARTIFACT_FACE;
1041
1042
  }
1042
- function readAdmittedPath(source, path) {
1043
- let current = source;
1044
- for (const part of path.split(".")) {
1045
- if (typeof current !== "object" || current === null)
1046
- return undefined;
1047
- current = current[part];
1048
- }
1049
- return current;
1050
- }
1051
- function evidenceLeaves(admitted, leaves) {
1052
- const evidence = {};
1053
- for (const leaf of leaves) {
1054
- if (leaf.callerProvenance === true) {
1055
- if (!admitted.instructionEmpty)
1056
- evidence[leaf.key] = admitted.instruction;
1057
- continue;
1058
- }
1059
- const value = readAdmittedPath(admitted, leaf.from ?? leaf.key);
1060
- if (value === undefined && leaf.omitUndefined === true)
1061
- continue;
1062
- evidence[leaf.key] = leaf.copyArray === true && Array.isArray(value) ? [...value] : value;
1063
- }
1064
- return evidence;
1065
- }
1066
1043
  function doctorReportFacts(face, roleOutcome, entries) {
1067
1044
  if (face.doctorReportFacts !== true || roleOutcome.kind === "audit_escalation")
1068
1045
  return {};
@@ -1072,28 +1049,19 @@ function doctorReportFacts(face, roleOutcome, entries) {
1072
1049
  ...(facts.auditNoReceipt === undefined ? {} : { auditNoReceipt: facts.auditNoReceipt }),
1073
1050
  };
1074
1051
  }
1075
- async function publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, recordAttemptHistory) {
1052
+ async function publishDeclaredSeatTerminal(admitted, roleOutcome, coordinates, entries, scope) {
1076
1053
  const face = seatArtifactFace(admitted.role);
1077
1054
  const phase = face.reportPhase === true
1078
- ? { phase: readAdmittedPath(admitted, "phase") }
1055
+ ? { phase: admitted.phase }
1079
1056
  : {};
1080
- return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, recordAttemptHistory, {
1081
- report: {
1082
- role: admitted.role,
1083
- runId: admitted.runId,
1084
- ...phase,
1085
- outcome: roleOutcome,
1086
- ...doctorReportFacts(face, roleOutcome, entries),
1087
- },
1088
- evidence: {
1089
- runId: admitted.runId,
1090
- ...(face.evidenceRole === true ? { role: admitted.role } : {}),
1091
- ...evidenceLeaves(admitted, face.leaves),
1092
- sessionDirectory: coordinates.sessionDirectory,
1093
- sessionFile: coordinates.sessionFile,
1094
- admittedRequestPath: admitted.admittedRequestPath,
1095
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1096
- },
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),
1097
1065
  });
1098
1066
  }
1099
1067
  /** Latest top-level user message index; 0 when the session has none (initial attempt). */
@@ -1231,140 +1199,38 @@ async function applySecretariatCountersignTerminal(admitted, authority, settled,
1231
1199
  /**
1232
1200
  * After the audit gate returns, attach the secretariat officer fact onto the
1233
1201
  * terminal this turn already settled. Does not publish again — a second publish
1234
- * would append another attempt-history row for the same attempt.
1202
+ * would settle the same attempt twice.
1235
1203
  */
1236
1204
  export async function attachPostAuditCountersignFact(admitted, authority, terminal, scope) {
1237
1205
  if (admitted.role !== "secretariat")
1238
1206
  return terminal;
1239
1207
  return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
1240
1208
  }
1241
- function publicationAttemptFromError(path, error) {
1242
- if (error instanceof Error) {
1243
- return {
1244
- path,
1245
- diagnostic: error.message || error.name || "write failed",
1246
- identity: thrownIdentity(error),
1247
- };
1248
- }
1249
- return { path, diagnostic: String(error) };
1250
- }
1251
1209
  /**
1252
- * Directories eligible for open-ended unique failure-artifact placement.
1253
- * Always includes the ledger runs/ parent of the run directory so an
1254
- * unwritable run tree cannot strand the original controlled failure.
1210
+ * Replace the leg's terminal with the controlled failure. A write failure
1211
+ * propagates: the caller notes it beside the host terminal.
1255
1212
  */
1256
- function uniqueFailureFallbackDirs(runDirectory, baseDir) {
1257
- const dirs = [];
1258
- for (const dir of [baseDir, runDirectory, dirname(runDirectory)]) {
1259
- if (!dirs.includes(dir))
1260
- dirs.push(dir);
1261
- }
1262
- return dirs;
1263
- }
1264
- /**
1265
- * Resolve a writable artifacts base directory. If `artifacts/` cannot be created
1266
- * (e.g. a file occupies that name), fall back to the run directory itself.
1267
- */
1268
- async function resolveFailureArtifactsBase(runDirectory) {
1269
- const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1270
- try {
1271
- await ensureRunArtifactsDir(runDirectory);
1272
- return { baseDir: artifactsDir };
1273
- }
1274
- catch (error) {
1275
- return {
1276
- baseDir: runDirectory,
1277
- attempt: publicationAttemptFromError(artifactsDir, error),
1278
- };
1279
- }
1280
- }
1281
- /**
1282
- * Write JSON across preferred paths, then unique open-ended fallbacks.
1283
- * Finite fixed names must not be able to exhaust durability and strand the
1284
- * original controlled failure outside settlement.
1285
- */
1286
- async function writeFailureJsonRetainingCause(preferredCandidates, uniqueFallbackDirs, stem, basePayload, priorIssues) {
1287
- const issues = [...priorIssues];
1288
- const candidates = [
1289
- ...preferredCandidates,
1290
- // One unique name per fallback dir — collisions on fixed names cannot exhaust this.
1291
- ...uniqueFallbackDirs.map((dir) => join(dir, stem === UNIQUE_ERROR_FALLBACK_STEM ? uniqueErrorFallbackName() : `${stem}.${randomUUID()}.json`)),
1292
- ];
1293
- for (let i = 0; i < candidates.length; i += 1) {
1294
- const path = candidates[i];
1295
- const payload = issues.length === 0
1296
- ? basePayload
1297
- : { ...basePayload, publicationIssues: issues };
1298
- try {
1299
- await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1300
- return { path, issues };
1301
- }
1302
- catch (error) {
1303
- issues.push(publicationAttemptFromError(path, error));
1304
- }
1305
- }
1306
- const last = issues.at(-1);
1307
- const error = new Error(last?.diagnostic ?? "unable to write durable failure artifact");
1308
- if (last?.identity?.name !== undefined && last.identity.name !== "") {
1309
- error.name = last.identity.name;
1310
- }
1311
- if (last?.identity?.code !== undefined) {
1312
- error.code = last.identity.code;
1313
- }
1314
- error.publicationAttempts = issues;
1315
- throw error;
1316
- }
1317
- export async function publishFailureArtifacts(admitted, failure, authority, onErrorPublished, recordAttemptHistory = false) {
1318
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1319
- const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
1320
- const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
1321
- // #419: a dispatched failure joins history before fixed-name views change.
1322
- // Re-projecting an existing failure does not append. History write failure
1323
- // rides publicationIssues rather than stranding the controlled failure.
1324
- 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) {
1325
1221
  try {
1326
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile }, {
1327
- kind: "failure", role: admitted.role, ...failure,
1328
- });
1222
+ const pointer = await appendRunAttemptHistory(attemptHistorySource(admitted, coordinates), failureOutcome);
1223
+ attemptHistoryIdentity = pointer.identity;
1329
1224
  }
1330
1225
  catch (error) {
1331
- priorIssues.push(publicationAttemptFromError(sessionFile, error));
1226
+ historyFailure = error;
1227
+ historyFailed = true;
1332
1228
  }
1333
1229
  }
1334
- // #953: clear the prior success face, but a failure here must not strand
1335
- // the original controlled cause; preserve the issue beside the fallback.
1336
- try {
1337
- await clearOppositeTerminalArtifactFace(admitted.runDirectory);
1230
+ else {
1231
+ attemptHistoryIdentity = await belongingAttemptHistoryIdentity(admitted.runDirectory, scope);
1338
1232
  }
1339
- catch (error) {
1340
- priorIssues.push(publicationAttemptFromError(roleRunArtifactsDirectory(admitted.runDirectory), error));
1341
- }
1342
- // Prefer conventional names; unique fallback dirs keep colliding fixed paths
1343
- // from stranding the original failure outside settlement. Include the ledger
1344
- // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
1345
- const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
1346
- const uniqueFallbackDirs = uniqueFailureFallbackDirs(admitted.runDirectory, baseDir);
1347
- const errorCandidates = underArtifacts
1348
- ? [
1349
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1350
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1351
- join(admitted.runDirectory, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1352
- ]
1353
- : [
1354
- join(baseDir, RUN_TERMINAL_ERROR_SETTLEMENT_FILE),
1355
- join(baseDir, RUN_TERMINAL_ERROR_FILE),
1356
- ];
1357
- const evidenceCandidates = underArtifacts
1358
- ? [
1359
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1360
- join(baseDir, "evidence.settlement.json"),
1361
- join(admitted.runDirectory, "evidence.settlement.json"),
1362
- ]
1363
- : [
1364
- join(baseDir, "evidence.settlement.json"),
1365
- join(baseDir, RUN_TERMINAL_EVIDENCE_FILE),
1366
- ];
1367
- const errorPayloadBase = {
1233
+ writeRunTerminal(admitted.runDirectory, "error", {
1368
1234
  kind: "error",
1369
1235
  role: admitted.role,
1370
1236
  runId: admitted.runId,
@@ -1375,24 +1241,13 @@ export async function publishFailureArtifacts(admitted, failure, authority, onEr
1375
1241
  // This package's own facts, kept beside the host's report.
1376
1242
  ...(failure.packageFact === undefined ? {} : { packageFact: failure.packageFact }),
1377
1243
  ...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
1378
- };
1379
- const errorWrite = await writeFailureJsonRetainingCause(errorCandidates, uniqueFallbackDirs, UNIQUE_ERROR_FALLBACK_STEM, errorPayloadBase, priorIssues);
1380
- onErrorPublished?.(errorWrite.path);
1381
- const evidencePayload = {
1382
- runId: admitted.runId,
1383
- sessionDirectory: sessionDirectory,
1384
- sessionFile: sessionFile,
1385
- admittedRequestPath: admitted.admittedRequestPath,
1386
- attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1387
- ...(failure.cause === undefined ? {} : { failureCause: failure.cause }),
1388
- };
1389
- const evidenceWrite = await writeFailureJsonRetainingCause(evidenceCandidates, uniqueFallbackDirs, "evidence", evidencePayload,
1390
- // Evidence records the same publication collisions observed placing the error body.
1391
- errorWrite.issues);
1392
- return [
1393
- { kind: "error", path: errorWrite.path },
1394
- { kind: "evidence", path: evidenceWrite.path },
1395
- ];
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 }];
1396
1251
  }
1397
1252
  /**
1398
1253
  * Shared controlled-failure Terminal settlement (#107 ownership).
@@ -1406,7 +1261,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
1406
1261
  let artifacts = [];
1407
1262
  if (options.previewOnly !== true) {
1408
1263
  try {
1409
- artifacts = await publishFailureArtifacts(admitted, failure, authority, options.onErrorPublished, options.recordAttemptHistory === true);
1264
+ artifacts = await publishFailureTerminal(admitted, failure, coordinates, options.onErrorPublished, options);
1410
1265
  }
1411
1266
  catch (error) {
1412
1267
  await noteSettlementFault(admitted.runDirectory, options, `failure artifact publication failed beside host terminal: ${describeErrorIdentity(error)}`);
@@ -93,13 +93,9 @@ function roleRunPlacement(ledgerHome, input) {
93
93
  runDirectory,
94
94
  sessionDirectory,
95
95
  sessionFile: sessionFileIn(sessionDirectory),
96
- artifactsDirectory: roleRunArtifactsDirectory(runDirectory),
97
96
  attachmentsDirectory: join(runDirectory, "attachments")
98
97
  };
99
98
  }
100
- function roleRunArtifactsDirectory(runDirectory) {
101
- return join(runDirectory, "artifacts");
102
- }
103
99
  function isUnboundRunDirectory(runDirectory) {
104
100
  return runDirectory.replaceAll("\\", "/").includes("/unbound/runs/");
105
101
  }
@@ -107,12 +103,7 @@ function ensureRoleRunDirectory(ledgerHome, directory) {
107
103
  return ensureRealDirectoryTree(ledgerHome, directory);
108
104
  }
109
105
  function ensureRoleRunPlacement(ledgerHome, placement) {
110
- for (const directory of [
111
- placement.sessionDirectory,
112
- placement.attachmentsDirectory
113
- ]) {
114
- ensureRoleRunDirectory(ledgerHome, directory);
115
- }
106
+ ensureRoleRunDirectory(ledgerHome, placement.sessionDirectory);
116
107
  }
117
108
  export {
118
109
  ROLE_RUN_SESSION_FILENAME,
@@ -123,7 +114,6 @@ export {
123
114
  isUnboundRunDirectory,
124
115
  listBookRunDirectories,
125
116
  parseRunLeaf,
126
- roleRunArtifactsDirectory,
127
117
  roleRunPlacement,
128
118
  runDirectoryFromSessionDirectory,
129
119
  runDirectoryOfSessionFile,