@akagilnc/pi-workflow-roles 0.1.5478 → 0.1.5504

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 (79) hide show
  1. package/dist/acp-host/production-host.js +4945 -4980
  2. package/dist/analyst-gate-cycles-read.js +11 -29
  3. package/dist/analyst-ledger.js +10 -12
  4. package/dist/archivist-record-entry.js +4 -3
  5. package/dist/archivist-record-pointer.js +59 -32
  6. package/dist/atomic-write.js +2 -2
  7. package/dist/auditor-dossier-tool.js +8 -4
  8. package/dist/auditor-soul.js +2 -1
  9. package/dist/collector-config.js +6 -5
  10. package/dist/doctor-evidence.js +5 -1
  11. package/dist/engine-detour-usage.js +4 -10
  12. package/dist/headless-host/description.js +251 -0
  13. package/dist/headless-host/production-host.js +2787 -2658
  14. package/dist/ledger-session-read.js +5 -0
  15. package/dist/migrate-book-topology.js +129 -100
  16. package/dist/navigator-public-session.js +13 -17
  17. package/dist/notary-source-run.js +7 -15
  18. package/dist/packaged-role-registry.js +0 -12
  19. package/dist/pi/durable-principal.js +2 -3
  20. package/dist/pi/role-turn-host.js +27 -13
  21. package/dist/public-cli/instruction-seat-run.js +4 -4
  22. package/dist/public-cli/invocation.js +17 -45
  23. package/dist/public-cli/main.js +1074 -1085
  24. package/dist/public-cli/run-lifecycle.js +17 -47
  25. package/dist/public-cli/settlement.js +28 -128
  26. package/dist/public-cli/terminal.js +0 -12
  27. package/dist/role-run-placement.js +52 -5
  28. package/dist/role-run-relocation.js +39 -2
  29. package/dist/run-terminal-artifacts.js +2 -6
  30. package/dist/run-ticket-number.js +33 -4
  31. package/dist/session-assistant-usage.js +9 -18
  32. package/dist/session-identity.js +3 -4
  33. package/dist/sitian-appender.js +20 -6
  34. package/dist/submission-ledger.js +4 -4
  35. package/dist/ticket-provenance-contracts.js +2 -8
  36. package/dist/ticket-provenance.js +5 -4
  37. package/package.json +1 -1
  38. package/src/acp-host/role-turn-host.ts +2 -2
  39. package/src/analyst-gate-cycles-read.ts +11 -34
  40. package/src/analyst-ledger.ts +14 -14
  41. package/src/archivist-record-entry.ts +4 -3
  42. package/src/archivist-record-pointer.ts +70 -34
  43. package/src/atomic-write.ts +3 -1
  44. package/src/auditor-dossier-tool.ts +8 -4
  45. package/src/auditor-soul.ts +2 -1
  46. package/src/collector-config.ts +5 -3
  47. package/src/doctor-evidence.ts +5 -1
  48. package/src/engine-detour-usage.ts +10 -13
  49. package/src/factory-board.ts +16 -81
  50. package/src/headless-host/description.ts +248 -1
  51. package/src/headless-host/production-host.ts +1 -2
  52. package/src/headless-host/role-turn-host.ts +10 -9
  53. package/src/ledger-session-read.ts +6 -0
  54. package/src/navigator-public-session.ts +13 -18
  55. package/src/navigator-work-context.ts +3 -1
  56. package/src/notary-role.ts +2 -1
  57. package/src/notary-source-run.ts +7 -17
  58. package/src/packaged-role-registry.ts +0 -15
  59. package/src/pi/durable-principal.ts +2 -4
  60. package/src/pi/role-turn-host.ts +35 -12
  61. package/src/public-cli/instruction-seat-run.ts +4 -4
  62. package/src/public-cli/invocation.ts +29 -57
  63. package/src/public-cli/run-lifecycle.ts +20 -43
  64. package/src/public-cli/settlement.ts +30 -173
  65. package/src/public-cli/terminal.ts +0 -60
  66. package/src/role-envelope.ts +4 -2
  67. package/src/role-run-placement.ts +63 -4
  68. package/src/role-run-relocation.ts +39 -2
  69. package/src/role-runtime.ts +40 -59
  70. package/src/run-terminal-artifacts.ts +3 -5
  71. package/src/run-ticket-number.ts +41 -3
  72. package/src/session-assistant-usage.ts +8 -20
  73. package/src/session-identity.ts +3 -4
  74. package/src/sitian-appender.ts +20 -12
  75. package/src/submission-ledger.ts +4 -4
  76. package/src/ticket-provenance-contracts.ts +3 -6
  77. package/src/ticket-provenance.ts +5 -6
  78. package/src/ticket-snapshot.ts +3 -2
  79. package/src/ticket-trajectory.ts +85 -99
@@ -7,11 +7,9 @@ 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
 
10
+ import { sessionFileOf } from "../role-run-placement.ts";
11
+
10
12
  import { writeHardenedArtifactFile } from "./auto-resume.ts";
11
- import {
12
- readAnalystGateCyclesFromAuditorRoles,
13
- type AnalystGateCycleRound,
14
- } from "../analyst-gate-cycles-read.ts";
15
13
  import { sitianReport } from "../sitian-facade.ts";
16
14
 
17
15
  import {
@@ -26,6 +24,7 @@ import {
26
24
  processCancelDiagnostic,
27
25
  type CatchableProcessSignal,
28
26
  } from "./process-cancel.ts";
27
+ import { readStrictPiSessionJsonl } from "../ledger-session-read.ts";
29
28
  import { serializeThrownValue } from "../serialize-thrown-value.ts";
30
29
  import { describeErrorIdentity } from "./run-lifecycle.ts";
31
30
  import {
@@ -55,7 +54,6 @@ import {
55
54
  packagedDurableOfficerEntry,
56
55
  packagedRoleAcceptedOutputTool,
57
56
  packagedRoleMetadata,
58
- packagedSkipsGateOnInfrastructureStage,
59
57
  type PackagedArtifactFace,
60
58
  type PackagedArtifactLeaf,
61
59
  } from "../packaged-role-registry.ts";
@@ -217,7 +215,7 @@ export function ledgerReadScope(
217
215
  ): { home: string; sessionParent: string; attemptId?: string } {
218
216
  return {
219
217
  home: sealedLedgerHome(admitted),
220
- sessionParent: join(admitted.runDirectory, "session", "session.jsonl"),
218
+ sessionParent: sessionFileOf(admitted.runDirectory),
221
219
  ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
222
220
  ? {}
223
221
  : { attemptId: scope.courtAttemptId }),
@@ -469,7 +467,7 @@ async function settleNoReceiptTerminal(
469
467
  );
470
468
  }
471
469
  if (scope?.previewOnly !== true) await clearOppositeTerminalArtifactFace(admitted.runDirectory);
472
- return withOptionalGateProjection({
470
+ return attachEngineDetourToolUsage({
473
471
  roleOutcome,
474
472
  navigator: await extractNavigatorFactFromAdmittedSession(
475
473
  coordinates.sessionFile,
@@ -478,7 +476,7 @@ async function settleNoReceiptTerminal(
478
476
  ),
479
477
  artifacts: [],
480
478
  runId: admitted.runId,
481
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
479
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
482
480
  }
483
481
 
484
482
  /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
@@ -495,8 +493,6 @@ import {
495
493
  adviceNavigatorFact,
496
494
  type ControlledFailureCause,
497
495
  type TerminalArtifactRef,
498
- type TerminalGateFact,
499
- type TerminalGateSeat,
500
496
  type TerminalNavigatorFact,
501
497
  type TerminalResult,
502
498
  type TerminalRoleName,
@@ -1034,12 +1030,7 @@ type SessionEntry = {
1034
1030
  export async function readBoundSessionEntries(
1035
1031
  sessionFile: string,
1036
1032
  ): Promise<SessionEntry[]> {
1037
- const text = await readFile(sessionFile, "utf8");
1038
- const entries: SessionEntry[] = [];
1039
- for (const line of text.trim().split("\n").filter(Boolean)) {
1040
- entries.push(JSON.parse(line) as SessionEntry);
1041
- }
1042
- return entries;
1033
+ return await readStrictPiSessionJsonl(sessionFile) as SessionEntry[];
1043
1034
  }
1044
1035
 
1045
1036
  /**
@@ -1151,73 +1142,6 @@ function parseNavigatorAttendanceDetails(
1151
1142
  };
1152
1143
  }
1153
1144
 
1154
- /**
1155
- * Project direct and historical paired gate rounds onto the public Terminal.
1156
- * actualSeats derive only from accepted receipts, never expected/missing seats.
1157
- */
1158
- export function projectTerminalGateFact(
1159
- rounds: readonly AnalystGateCycleRound[],
1160
- ): TerminalGateFact | undefined {
1161
- if (rounds.length === 0) return undefined;
1162
- const seen = new Set<TerminalGateSeat>();
1163
- for (const round of rounds) {
1164
- if (round.origin.kind === "historical_dispatch") seen.add("gatekeeper");
1165
- seen.add(round.officer);
1166
- }
1167
- const actualSeats = (["gatekeeper", "inspector", "notary"] as const).filter(
1168
- (seat) => seen.has(seat),
1169
- );
1170
- return {
1171
- actualSeats,
1172
- rounds: rounds.map((round) => ({
1173
- roundIndex: round.roundIndex,
1174
- dispatch:
1175
- round.origin.kind === "direct"
1176
- ? { kind: "direct" as const, officer: round.officer }
1177
- : {
1178
- kind: "historical_dispatch" as const,
1179
- officer: round.officer,
1180
- ...(round.origin.reason === undefined
1181
- ? {}
1182
- : { reason: round.origin.reason }),
1183
- },
1184
- officer: {
1185
- seat: round.officer,
1186
- status: round.status,
1187
- findings: round.findings,
1188
- },
1189
- })),
1190
- };
1191
- }
1192
-
1193
- /**
1194
- * Read gate facts from the run's session/auditor-roles nest (#446/#478).
1195
- * Missing directories → undefined (no-gate zero change).
1196
- * Damaged discovered volumes propagate — never wash to "no gate".
1197
- */
1198
- export async function extractGateFactFromSessionDirectory(
1199
- sessionDirectory: string,
1200
- options: {
1201
- readonly runDirectory?: string;
1202
- readonly parentSessionFile?: string;
1203
- } = {},
1204
- ): Promise<TerminalGateFact | undefined> {
1205
- const directories = [join(sessionDirectory, "auditor-roles")];
1206
- const parentSessionFile =
1207
- options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
1208
- const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
1209
- parentSessionFile,
1210
- });
1211
- return projectTerminalGateFact(rounds);
1212
- }
1213
-
1214
- /**
1215
- * Attach optional gate projection onto a settled Terminal base.
1216
- * Shared by every settle path so auditor-roles is scanned once here only.
1217
- * `runId` is optional only for a batch projection that has no parent run.
1218
- * Gate read damage is a package note beside the host terminal. It does not
1219
- * replace that terminal and it does not omit the gate without a trace.
1220
- */
1221
1145
  /**
1222
1146
  * #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
1223
1147
  * Absent when engine is not mounted; callCount 0 when mounted with zero calls.
@@ -1231,16 +1155,15 @@ async function attachEngineDetourToolUsage<
1231
1155
  >(
1232
1156
  base: T,
1233
1157
  sessionDirectory: string,
1234
- gateContext: {
1158
+ detourContext: {
1235
1159
  readonly runDirectory?: string;
1236
- readonly courtAttemptId?: string;
1237
1160
  readonly invocationScopeId?: string;
1238
1161
  readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1239
1162
  } = {},
1240
1163
  ): Promise<T> {
1241
1164
  const runDirectory =
1242
- typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
1243
- ? gateContext.runDirectory
1165
+ typeof detourContext.runDirectory === "string" && detourContext.runDirectory.length > 0
1166
+ ? detourContext.runDirectory
1244
1167
  : runDirectoryFromSessionDirectory(sessionDirectory);
1245
1168
  let engineMounted = false;
1246
1169
  try {
@@ -1248,9 +1171,9 @@ async function attachEngineDetourToolUsage<
1248
1171
  } catch (error) {
1249
1172
  await noteSettlementFault(
1250
1173
  runDirectory,
1251
- gateContext.notePackageFault === undefined
1174
+ detourContext.notePackageFault === undefined
1252
1175
  ? undefined
1253
- : { notePackageFault: gateContext.notePackageFault },
1176
+ : { notePackageFault: detourContext.notePackageFault },
1254
1177
  `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`,
1255
1178
  );
1256
1179
  return base;
@@ -1260,9 +1183,9 @@ async function attachEngineDetourToolUsage<
1260
1183
  // Public-invocation scope from the shared Host envelope (settlement scope), never
1261
1184
  // courtAttemptId and never a detour sidecar file.
1262
1185
  const invocationScopeId =
1263
- typeof gateContext.invocationScopeId === "string" &&
1264
- gateContext.invocationScopeId.length > 0
1265
- ? gateContext.invocationScopeId
1186
+ typeof detourContext.invocationScopeId === "string" &&
1187
+ detourContext.invocationScopeId.length > 0
1188
+ ? detourContext.invocationScopeId
1266
1189
  : undefined;
1267
1190
 
1268
1191
  const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
@@ -1277,9 +1200,9 @@ async function attachEngineDetourToolUsage<
1277
1200
  } catch (error) {
1278
1201
  await noteSettlementFault(
1279
1202
  runDirectory,
1280
- gateContext.notePackageFault === undefined
1203
+ detourContext.notePackageFault === undefined
1281
1204
  ? undefined
1282
- : { notePackageFault: gateContext.notePackageFault },
1205
+ : { notePackageFault: detourContext.notePackageFault },
1283
1206
  `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`,
1284
1207
  );
1285
1208
  return base;
@@ -1290,20 +1213,17 @@ async function attachEngineDetourToolUsage<
1290
1213
  };
1291
1214
  }
1292
1215
 
1293
- /** Gate + detour projection context from admitted run + settlement scope. */
1294
- function detourGateContext(
1216
+ /** Engine-detour usage context from admitted run + settlement scope. */
1217
+ function detourUsageContext(
1295
1218
  admitted: { readonly runDirectory: string },
1296
1219
  scope?: SettlementCourtScope,
1297
1220
  ): {
1298
1221
  readonly runDirectory: string;
1299
- readonly courtAttemptId?: string;
1300
1222
  readonly invocationScopeId?: string;
1223
+ readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1301
1224
  } {
1302
1225
  return {
1303
1226
  runDirectory: admitted.runDirectory,
1304
- ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
1305
- ? {}
1306
- : { courtAttemptId: scope.courtAttemptId }),
1307
1227
  ...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
1308
1228
  ? {}
1309
1229
  : { invocationScopeId: scope.invocationScopeId }),
@@ -1311,54 +1231,6 @@ function detourGateContext(
1311
1231
  };
1312
1232
  }
1313
1233
 
1314
- async function withOptionalGateProjection<
1315
- T extends {
1316
- roleOutcome: TerminalRoleOutcome;
1317
- navigator: TerminalNavigatorFact;
1318
- artifacts: readonly TerminalArtifactRef[];
1319
- },
1320
- >(
1321
- base: T,
1322
- sessionDirectory: string,
1323
- gateContext: {
1324
- readonly runDirectory?: string;
1325
- readonly parentSessionFile?: string;
1326
- readonly courtAttemptId?: string;
1327
- readonly invocationScopeId?: string;
1328
- readonly notePackageFault?: SettlementCourtScope["notePackageFault"];
1329
- } = {},
1330
- ): Promise<T & { gate?: TerminalGateFact }> {
1331
- // A gate transport failure is already represented by typed evidence and has no
1332
- // accepted gate cycle to project. Re-reading that rejected receipt as an
1333
- // accepted cycle would replace the original failure with a projection error.
1334
- const secondaryEvidence = base.roleOutcome.kind === "failure"
1335
- ? base.roleOutcome.decisiveFacts.secondaryEvidence
1336
- : undefined;
1337
- const skipGate =
1338
- isRecord(secondaryEvidence)
1339
- && secondaryEvidence.kind === "role_infrastructure_failure"
1340
- && packagedSkipsGateOnInfrastructureStage(secondaryEvidence.stage);
1341
-
1342
- let next: T & { gate?: TerminalGateFact } = base;
1343
- if (!skipGate) {
1344
- try {
1345
- const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
1346
- if (gate !== undefined) next = { ...base, gate };
1347
- } catch (error) {
1348
- const runDirectory = gateContext.runDirectory ?? runDirectoryFromSessionDirectory(sessionDirectory);
1349
- await noteSettlementFault(
1350
- runDirectory,
1351
- gateContext.notePackageFault === undefined
1352
- ? undefined
1353
- : { notePackageFault: gateContext.notePackageFault },
1354
- `gate read failed beside host terminal: ${describeErrorIdentity(error)}`,
1355
- );
1356
- }
1357
- }
1358
-
1359
- return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
1360
- }
1361
-
1362
1234
  function routePlaybookFailureMessage(entries: readonly SessionEntry[]): string | undefined {
1363
1235
  for (let i = entries.length - 1; i >= 0; i -= 1) {
1364
1236
  const entry = entries[i];
@@ -1620,12 +1492,12 @@ async function finishLawfulSeat(
1620
1492
  const artifacts = scope?.previewOnly === true
1621
1493
  ? []
1622
1494
  : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
1623
- const terminal = await withOptionalGateProjection({
1495
+ const terminal = await attachEngineDetourToolUsage({
1624
1496
  roleOutcome,
1625
1497
  navigator: extractNavigatorFact(entries),
1626
1498
  artifacts,
1627
1499
  runId: admitted.runId,
1628
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
1500
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
1629
1501
  return attachRecordedSubmissions(admitted, terminal, scope);
1630
1502
  }
1631
1503
 
@@ -1933,33 +1805,18 @@ async function applySecretariatCountersignTerminal(
1933
1805
  }
1934
1806
 
1935
1807
  /**
1936
- * After the audit gate returns, attach gate rounds and the secretariat officer
1937
- * fact onto the terminal this turn already settled. Does not publish again —
1938
- * a second publish would append another attempt-history row for the same attempt.
1808
+ * After the audit gate returns, attach the secretariat officer fact onto the
1809
+ * terminal this turn already settled. Does not publish again — a second publish
1810
+ * would append another attempt-history row for the same attempt.
1939
1811
  */
1940
- export async function attachPostAuditProjection(
1812
+ export async function attachPostAuditCountersignFact(
1941
1813
  admitted: AdmittedRoleInvocation,
1942
1814
  authority: DurablePrincipalAuthority,
1943
1815
  terminal: TerminalResult,
1944
1816
  scope?: SettlementCourtScope,
1945
1817
  ): Promise<TerminalResult> {
1946
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1947
- let gate: TerminalGateFact | undefined;
1948
- try {
1949
- gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
1950
- runDirectory: admitted.runDirectory,
1951
- parentSessionFile: sessionFile,
1952
- });
1953
- } catch (error) {
1954
- await noteSettlementFault(
1955
- admitted.runDirectory,
1956
- scope,
1957
- `gate read failed beside host terminal: ${describeErrorIdentity(error)}`,
1958
- );
1959
- }
1960
- const withGate = gate === undefined ? terminal : { ...terminal, gate };
1961
- if (admitted.role !== "secretariat") return withGate;
1962
- return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
1818
+ if (admitted.role !== "secretariat") return terminal;
1819
+ return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
1963
1820
  }
1964
1821
 
1965
1822
  /** One failed attempt to place a durable failure artifact (path is private layout). */
@@ -2246,10 +2103,10 @@ export async function settleFailureTerminalResult(
2246
2103
  diagnostic: failure.diagnostic,
2247
2104
  decisiveFacts,
2248
2105
  };
2249
- return withOptionalGateProjection(
2106
+ return attachEngineDetourToolUsage(
2250
2107
  { roleOutcome, navigator, artifacts, runId: admitted.runId },
2251
2108
  sessionDirectory,
2252
- detourGateContext(admitted, options),
2109
+ detourUsageContext(admitted, options),
2253
2110
  );
2254
2111
  }
2255
2112
 
@@ -103,44 +103,6 @@ export type TerminalNavigatorFact =
103
103
  advisoryDiagnostic?: string;
104
104
  };
105
105
 
106
- /** Current English gate seat faces projected on Terminal (#478). */
107
- export type TerminalGateSeat = "gatekeeper" | "inspector" | "notary";
108
-
109
- /** Honest discriminant: direct summons vs historical province dispatch. */
110
- export type TerminalGateDispatch =
111
- | { readonly kind: "direct"; readonly officer: "inspector" | "notary" }
112
- | {
113
- readonly kind: "historical_dispatch";
114
- readonly officer: "inspector" | "notary";
115
- /** Present only when the accepted dispatch wrote a non-empty reason. */
116
- readonly reason?: string;
117
- };
118
-
119
- /** One accepted officer report: seat, status, full typed findings. */
120
- export type TerminalGateOfficerReport = {
121
- readonly seat: "inspector" | "notary";
122
- readonly status: string;
123
- readonly findings: readonly unknown[];
124
- };
125
-
126
- /** One direct or historical paired gate round on the public Terminal. */
127
- export type TerminalGateRound = {
128
- readonly roundIndex: number;
129
- readonly dispatch: TerminalGateDispatch;
130
- readonly officer: TerminalGateOfficerReport;
131
- };
132
-
133
- /**
134
- * Optional gate projection (#478).
135
- * Absent when no accepted officer rounds exist. Only durable accepted child
136
- * receipts — never soul-derived expected/missing seats.
137
- */
138
- export type TerminalGateFact = {
139
- /** Seats that actually ran, derived from accepted receipts. */
140
- readonly actualSeats: readonly TerminalGateSeat[];
141
- readonly rounds: readonly TerminalGateRound[];
142
- };
143
-
144
106
  /**
145
107
  * Parent-facing current reply: a court's own accepted/audit payloads only.
146
108
  * Run-scoped history stays on TerminalResult.submissions (#879 / #836), and a
@@ -177,11 +139,6 @@ export type TerminalResult = {
177
139
  * (#953 — not copied onto failure.payloads).
178
140
  */
179
141
  submissions?: readonly unknown[];
180
- /**
181
- * Optional gate facts (#478). Present when accepted direct or historical
182
- * paired rounds exist under session/auditor-roles; omitted on no-gate runs.
183
- */
184
- gate?: TerminalGateFact;
185
142
  /** Call-local auto-resume observation (0..2) for this single LLM call; read-only, not persisted. */
186
143
  autoResumeCount?: number;
187
144
  } & (
@@ -272,23 +229,6 @@ export function formatTerminalResult(result: TerminalResult): string {
272
229
  for (const artifact of result.artifacts) {
273
230
  lines.push(`artifact\t${artifact.kind}\t${encodeTerminalField(artifact.path)}`);
274
231
  }
275
- // Gate region is unfrozen presentation of typed facts (ADR 0052) — machines
276
- // read result.gate, never these row labels.
277
- if (result.gate !== undefined) {
278
- lines.push(
279
- `gate\t${encodeTerminalField(result.gate.actualSeats.join(","))}\t${result.gate.rounds.length}`,
280
- );
281
- for (const round of result.gate.rounds) {
282
- const reason =
283
- round.dispatch.kind === "historical_dispatch"
284
- && round.dispatch.reason !== undefined
285
- ? encodeTerminalField(round.dispatch.reason)
286
- : "";
287
- lines.push(
288
- `gate-round\t${round.roundIndex}\t${round.dispatch.kind}\t${round.dispatch.officer}\t${reason}\t${round.officer.seat}\t${encodeTerminalField(round.officer.status)}\t${encodeTerminalField(JSON.stringify(round.officer.findings))}`,
289
- );
290
- }
291
- }
292
232
  if (result.reviewerChildOutcomes !== undefined) {
293
233
  for (const axis of ["completeness", "correctness"] as const) {
294
234
  const child = result.reviewerChildOutcomes[axis];
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFile, mkdir, writeFile } from "node:fs/promises";
3
3
  import { createServer, type Server, type Socket } from "node:net";
4
- import { dirname, join } from "node:path";
4
+ import { dirname } from "node:path";
5
+
6
+ import { sessionFileOf } from "./role-run-placement.ts";
5
7
  import { fileURLToPath } from "node:url";
6
8
 
7
9
  import {
@@ -124,7 +126,7 @@ export async function prepareRoleEnvelope(options: {
124
126
  // navigator attendance, no-receipt) MUST flush so parent settlement can read
125
127
  // them after the headless process ends — memory-only books left extractNavigatorFact
126
128
  // with no durable terminal / attendance on codex/claude/ACP parents.
127
- let sessionFile = options.sessionFile ?? join(request.runDirectory, "session", "session.jsonl");
129
+ let sessionFile = options.sessionFile ?? sessionFileOf(request.runDirectory);
128
130
  await mkdir(dirname(sessionFile), { recursive: true });
129
131
  if (request.continuation.kind !== "resume") {
130
132
  try {
@@ -1,5 +1,5 @@
1
1
  import { readdir } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
 
4
4
  import {
5
5
  activationBookDirectory,
@@ -21,6 +21,45 @@ export type RoleRunPlacement = {
21
21
  readonly attachmentsDirectory: string;
22
22
  };
23
23
 
24
+ /** Writer leaf `<runId>@<role>`. Empty sides stay empty; this is not a validator. */
25
+ export function formatRunLeaf(runId: string, role: string): string {
26
+ return `${runId}@${role}`;
27
+ }
28
+
29
+ /**
30
+ * Inverse of formatRunLeaf: last `@`, both sides non-empty.
31
+ * `a@b@c` is runId `a@b`, role `c`. Charset is not a second grammar.
32
+ */
33
+ export function parseRunLeaf(name: string): { readonly runId: string; readonly role: string } | undefined {
34
+ const at = name.lastIndexOf("@");
35
+ if (at <= 0 || at === name.length - 1) return undefined;
36
+ return { runId: name.slice(0, at), role: name.slice(at + 1) };
37
+ }
38
+
39
+ export function sessionDirectoryOf(runDirectory: string): string {
40
+ return join(runDirectory, "session");
41
+ }
42
+
43
+ export const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
44
+
45
+ export function sessionFileIn(sessionDirectory: string): string {
46
+ return join(sessionDirectory, ROLE_RUN_SESSION_FILENAME);
47
+ }
48
+
49
+ export function sessionFileOf(runDirectory: string): string {
50
+ return sessionFileIn(sessionDirectoryOf(runDirectory));
51
+ }
52
+
53
+ /** `<run>/session/session.jsonl` → run directory. */
54
+ export function runDirectoryOfSessionFile(sessionFile: string): string {
55
+ return dirname(dirname(sessionFile));
56
+ }
57
+
58
+ /** `<run>/session` → run directory. */
59
+ export function runDirectoryFromSessionDirectory(sessionDirectory: string): string {
60
+ return dirname(sessionDirectory);
61
+ }
62
+
24
63
  /**
25
64
  * Sole book-level run directory walk: flat legacy `runs/` plus each
26
65
  * `subject/runs/` child (ticket / unbound). One authority for read-side
@@ -64,6 +103,26 @@ export async function listBookRunDirectories(bookDir: string): Promise<string[]>
64
103
  return out.sort();
65
104
  }
66
105
 
106
+ /** Resolve a complete run identity across the caller's books; ambiguity is never readdir-first. */
107
+ export async function findRoleRunDirectory(
108
+ bookDirectories: readonly string[],
109
+ runId: string,
110
+ onlyRole?: string,
111
+ ): Promise<string | undefined> {
112
+ const matches: string[] = [];
113
+ for (const bookDirectory of bookDirectories) {
114
+ for (const runDirectory of await listBookRunDirectories(bookDirectory)) {
115
+ const parsed = parseRunLeaf(basename(runDirectory));
116
+ if (parsed === undefined || parsed.runId !== runId) continue;
117
+ if (onlyRole !== undefined && parsed.role !== onlyRole) continue;
118
+ matches.push(runDirectory);
119
+ }
120
+ }
121
+ if (matches.length === 0) return undefined;
122
+ if (matches.length === 1) return matches[0];
123
+ throw new Error(`ambiguous role run id ${runId}: ${matches.join(", ")}`);
124
+ }
125
+
67
126
  /** The single authority for every path belonging to an admitted role run. */
68
127
  export function roleRunPlacement(
69
128
  ledgerHome: string,
@@ -84,13 +143,13 @@ export function roleRunPlacement(
84
143
  activationBookDirectory(ledgerHome, input.bookKey),
85
144
  subjectDirectory,
86
145
  "runs",
87
- `${input.runId}@${input.role}`,
146
+ formatRunLeaf(input.runId, input.role),
88
147
  );
89
- const sessionDirectory = join(runDirectory, "session");
148
+ const sessionDirectory = sessionDirectoryOf(runDirectory);
90
149
  return {
91
150
  runDirectory,
92
151
  sessionDirectory,
93
- sessionFile: join(sessionDirectory, "session.jsonl"),
152
+ sessionFile: sessionFileIn(sessionDirectory),
94
153
  artifactsDirectory: roleRunArtifactsDirectory(runDirectory),
95
154
  attachmentsDirectory: join(runDirectory, "attachments"),
96
155
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Single authority for rewriting durable run pages after a run directory moves.
3
- * Live unbound→ticket relocate and book-topology migration both call this.
3
+ * Live callers project typed fields; only historical migrators walk durable pages.
4
4
  *
5
5
  * Scope is typed machine-consumed path fields only — never free text, never
6
6
  * frozen attachment/artifact bytes. Nested walk is confined to package-owned
@@ -8,7 +8,11 @@
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
10
  import { readdir, readFile, writeFile } from "node:fs/promises";
11
- import { join, sep } from "node:path";
11
+ import { dirname, join, sep } from "node:path";
12
+
13
+ import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.ts";
14
+ import { readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
15
+ import { findRoleRunDirectory, roleRunPlacement } from "./role-run-placement.ts";
12
16
 
13
17
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.ts";
14
18
 
@@ -67,6 +71,39 @@ export function rewriteRunDirectoryPathValue(
67
71
  return value;
68
72
  }
69
73
 
74
+ /** Match physical aliases or a normal unbound→ticket bind, never historical migration layouts. */
75
+ export async function retainedRunPathsMatch(
76
+ recordedPath: unknown,
77
+ currentPath: string,
78
+ currentRunDirectory?: string,
79
+ ): Promise<boolean> {
80
+ if (recordedPath === currentPath) return true;
81
+ if (typeof recordedPath !== "string") return false;
82
+ const recordedPathIdentity = physicalPathIdentity(recordedPath);
83
+ const currentPathIdentity = physicalPathIdentity(currentPath);
84
+ if (recordedPathIdentity === currentPathIdentity) return true;
85
+ if (currentRunDirectory === undefined) return false;
86
+ const runDirectory = physicalPathIdentity(currentRunDirectory);
87
+ const identity = await readRoleRunIdentity(runDirectory);
88
+ if (identity === undefined) return false;
89
+ for (let directory = runDirectory; dirname(directory) !== directory; directory = dirname(directory)) {
90
+ const ledgerHome = dirname(dirname(directory));
91
+ if (directory !== activationBookDirectory(ledgerHome, identity.bookKey)) continue;
92
+ const unbound = roleRunPlacement(ledgerHome, {
93
+ bookKey: identity.bookKey,
94
+ subject: { unbound: true },
95
+ runId: identity.runId,
96
+ role: identity.role,
97
+ });
98
+ if (rewriteRunDirectoryPathValue(recordedPathIdentity, unbound.runDirectory, runDirectory) !== currentPathIdentity) {
99
+ return false;
100
+ }
101
+ const placed = await findRoleRunDirectory([directory], identity.runId, identity.role);
102
+ return placed !== undefined && physicalPathIdentity(placed) === runDirectory;
103
+ }
104
+ return false;
105
+ }
106
+
70
107
  /**
71
108
  * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
72
109
  * nested run path is not partially claimed by a shorter sibling prefix.