@akagilnc/pi-workflow-roles 0.1.4062 → 0.1.4196

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 (72) hide show
  1. package/dist/acp-host/production-host.js +2075 -1088
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-evidence.js +3 -2
  7. package/dist/headless-host/production-host.js +2215 -1228
  8. package/dist/migrate-book-topology.js +278917 -0
  9. package/dist/navigator-public-session.js +22 -25
  10. package/dist/notary-source-run.js +7 -4
  11. package/dist/pi/durable-principal.js +9 -6
  12. package/dist/public-cli/auto-resume.js +2 -5
  13. package/dist/public-cli/diarist-run.js +27 -25
  14. package/dist/public-cli/instruction-seat-run.js +8 -11
  15. package/dist/public-cli/invocation.js +127 -63
  16. package/dist/public-cli/main.js +1507 -645
  17. package/dist/public-cli/option-definitions.js +2 -2
  18. package/dist/public-cli/post-admission.js +47 -12
  19. package/dist/public-cli/run-lifecycle.js +206 -58
  20. package/dist/public-cli/seat-ticket-binding.js +3 -6
  21. package/dist/public-cli/settlement.js +5 -3
  22. package/dist/role-run-placement.js +84 -0
  23. package/dist/role-run-relocation.js +358 -0
  24. package/dist/run-terminal-artifacts.js +2 -1
  25. package/dist/run-ticket-number.js +51 -32
  26. package/dist/sitian-appender.js +11 -26
  27. package/dist/submission-ledger.js +78 -19
  28. package/dist/ticket-provenance.js +3 -1
  29. package/package.json +3 -2
  30. package/scripts/build-package.mjs +34 -2
  31. package/souls/countersign.md +2 -2
  32. package/souls/fixer.md +3 -2
  33. package/src/analyst-ledger.ts +14 -15
  34. package/src/archivist-record-entry.ts +285 -38
  35. package/src/archivist-record-topology.ts +43 -18
  36. package/src/auditor-dossier-tool.ts +2 -1
  37. package/src/book-topology-migration-placement.ts +493 -0
  38. package/src/book-topology-migration.ts +220 -0
  39. package/src/book-topology-migrators-t11.ts +856 -0
  40. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  41. package/src/book-topology-partition-migrators.ts +25 -0
  42. package/src/book-topology-record-class-migrators.ts +779 -0
  43. package/src/book-topology-runs-migrator.ts +244 -0
  44. package/src/diarist-contracts.ts +116 -0
  45. package/src/diarist-role.ts +6 -0
  46. package/src/diarist.ts +2 -0
  47. package/src/doctor-evidence.ts +4 -1
  48. package/src/doctor-role.ts +1 -1
  49. package/src/navigator-public-session.ts +33 -30
  50. package/src/notary-source-run.ts +9 -4
  51. package/src/pi/durable-principal.ts +9 -13
  52. package/src/public-cli/auto-resume.ts +2 -6
  53. package/src/public-cli/countersign-run.ts +182 -32
  54. package/src/public-cli/diarist-run.ts +34 -26
  55. package/src/public-cli/instruction-seat-run.ts +7 -11
  56. package/src/public-cli/invocation.ts +192 -58
  57. package/src/public-cli/option-definitions.ts +2 -2
  58. package/src/public-cli/post-admission.ts +73 -13
  59. package/src/public-cli/run-lifecycle.ts +255 -65
  60. package/src/public-cli/seat-ticket-binding.ts +3 -6
  61. package/src/public-cli/settlement.ts +6 -4
  62. package/src/role-run-placement.ts +128 -0
  63. package/src/role-run-relocation.ts +491 -0
  64. package/src/role-runtime.ts +11 -5
  65. package/src/run-terminal-artifacts.ts +3 -1
  66. package/src/run-ticket-number.ts +80 -20
  67. package/src/sitian-appender.ts +25 -28
  68. package/src/submission-ledger.ts +102 -19
  69. package/src/ticket-provenance.ts +4 -1
  70. package/src/ticket-trajectory.ts +22 -35
  71. package/src/worker-role.ts +4 -4
  72. package/src/worker-submission-gates.ts +6 -6
@@ -0,0 +1,128 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import {
5
+ activationBookDirectory,
6
+ ensureRealDirectoryTree,
7
+ } from "./activation-ledger-topology.ts";
8
+ import { requireSafePositiveTicketNumber } from "./run-ticket-number.ts";
9
+
10
+ export type RoleRunSubject =
11
+ | { readonly ticketNumber: number }
12
+ | { readonly unbound: true };
13
+
14
+ export type RoleRunPlacement = {
15
+ readonly runDirectory: string;
16
+ readonly sessionDirectory: string;
17
+ readonly sessionFile: string;
18
+ readonly artifactsDirectory: string;
19
+ readonly attachmentsDirectory: string;
20
+ };
21
+
22
+ function isMissingPathError(error: unknown): boolean {
23
+ return (
24
+ error instanceof Error &&
25
+ "code" in error &&
26
+ (error as NodeJS.ErrnoException).code === "ENOENT"
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Sole book-level run directory walk: flat legacy `runs/` plus each
32
+ * `subject/runs/` child (ticket / unbound). One authority for read-side
33
+ * enumeration under a book directory — findRunDirectoryById, analyst scan,
34
+ * and ticket trajectory must not hardcode a second subject-tree walk.
35
+ */
36
+ export async function listBookRunDirectories(bookDir: string): Promise<string[]> {
37
+ const out: string[] = [];
38
+ const seen = new Set<string>();
39
+
40
+ const collect = async (runsDir: string): Promise<void> => {
41
+ let entries;
42
+ try {
43
+ entries = await readdir(runsDir, { withFileTypes: true });
44
+ } catch (error) {
45
+ if (isMissingPathError(error)) return;
46
+ throw error;
47
+ }
48
+ for (const entry of entries) {
49
+ if (!entry.isDirectory()) continue;
50
+ const runDir = join(runsDir, entry.name);
51
+ if (seen.has(runDir)) continue;
52
+ seen.add(runDir);
53
+ out.push(runDir);
54
+ }
55
+ };
56
+
57
+ await collect(join(bookDir, "runs"));
58
+
59
+ let subjects;
60
+ try {
61
+ subjects = await readdir(bookDir, { withFileTypes: true });
62
+ } catch (error) {
63
+ if (isMissingPathError(error)) return out.sort();
64
+ throw error;
65
+ }
66
+ for (const subject of subjects) {
67
+ if (!subject.isDirectory() || subject.name === "runs") continue;
68
+ await collect(join(bookDir, subject.name, "runs"));
69
+ }
70
+ return out.sort();
71
+ }
72
+
73
+ /** The single authority for every path belonging to an admitted role run. */
74
+ export function roleRunPlacement(
75
+ ledgerHome: string,
76
+ input: {
77
+ readonly bookKey: string;
78
+ readonly subject: RoleRunSubject;
79
+ readonly runId: string;
80
+ readonly role: string;
81
+ },
82
+ ): RoleRunPlacement {
83
+ const subjectDirectory = "ticketNumber" in input.subject
84
+ ? String(requireSafePositiveTicketNumber(
85
+ input.subject.ticketNumber,
86
+ "roleRunPlacement subject.ticketNumber",
87
+ ))
88
+ : "unbound";
89
+ const runDirectory = join(
90
+ activationBookDirectory(ledgerHome, input.bookKey),
91
+ subjectDirectory,
92
+ "runs",
93
+ `${input.runId}@${input.role}`,
94
+ );
95
+ const sessionDirectory = join(runDirectory, "session");
96
+ return {
97
+ runDirectory,
98
+ sessionDirectory,
99
+ sessionFile: join(sessionDirectory, "session.jsonl"),
100
+ artifactsDirectory: roleRunArtifactsDirectory(runDirectory),
101
+ attachmentsDirectory: join(runDirectory, "attachments"),
102
+ };
103
+ }
104
+
105
+ /** The single artifacts subpath definition for new and resumed role runs. */
106
+ export function roleRunArtifactsDirectory(runDirectory: string): string {
107
+ return join(runDirectory, "artifacts");
108
+ }
109
+
110
+ /** The placement seam owns creation for both new runs and resumed legacy runs. */
111
+ export function ensureRoleRunDirectory(
112
+ ledgerHome: string,
113
+ directory: string,
114
+ ): string {
115
+ return ensureRealDirectoryTree(ledgerHome, directory);
116
+ }
117
+
118
+ export function ensureRoleRunPlacement(
119
+ ledgerHome: string,
120
+ placement: RoleRunPlacement,
121
+ ): void {
122
+ for (const directory of [
123
+ placement.sessionDirectory,
124
+ placement.attachmentsDirectory,
125
+ ]) {
126
+ ensureRoleRunDirectory(ledgerHome, directory);
127
+ }
128
+ }
@@ -0,0 +1,491 @@
1
+ /**
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.
4
+ *
5
+ * Scope is typed machine-consumed path fields only — never free text, never
6
+ * frozen attachment/artifact bytes. Nested walk is confined to package-owned
7
+ * `session/` seams.
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { readdir, readFile, writeFile } from "node:fs/promises";
11
+ import { join, sep } from "node:path";
12
+
13
+ import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.ts";
14
+
15
+ const ADMITTED_PAGE_FIELDS = [
16
+ "runDirectory",
17
+ "admittedRequestPath",
18
+ "sessionDirectory",
19
+ "sessionFile",
20
+ "taskPath",
21
+ "packetPath",
22
+ "prerequisitesPath",
23
+ "requestManifestPath",
24
+ "mergerInputPath",
25
+ "sourceRunPath",
26
+ ] as const;
27
+
28
+ const INVOCATION_PAGE_FIELDS = [
29
+ "runDirectory",
30
+ "sessionDirectory",
31
+ "sessionFile",
32
+ ] as const;
33
+
34
+ const RUN_STATE_PAGE_FIELDS = [
35
+ "runDirectory",
36
+ "admittedRequestPath",
37
+ "sessionDirectory",
38
+ "sessionFile",
39
+ ] as const;
40
+
41
+ const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"] as const;
42
+ const SUMMONS_PATH_FIELDS = ["sourceRunPath"] as const;
43
+ const CURRENT_SESSION_FIELDS = ["sessionFile"] as const;
44
+ const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"] as const;
45
+ const SITIAN_RECORD_FIELDS = ["sessionParent"] as const;
46
+ const SESSION_HEADER_FIELDS = ["parentSession"] as const;
47
+ const BINDING_PARENT_FIELDS = ["sessionFile"] as const;
48
+
49
+ export type RunDirectoryPathRewrite = {
50
+ readonly oldRunDirectory: string;
51
+ readonly newRunDirectory: string;
52
+ };
53
+
54
+ function isEnoent(error: unknown): boolean {
55
+ return (
56
+ error instanceof Error &&
57
+ "code" in error &&
58
+ (error as NodeJS.ErrnoException).code === "ENOENT"
59
+ );
60
+ }
61
+
62
+ /** Rewrite one path value that points at or under oldRunDirectory. */
63
+ export function rewriteRunDirectoryPathValue(
64
+ value: unknown,
65
+ oldRunDirectory: string,
66
+ newRunDirectory: string,
67
+ ): unknown {
68
+ if (typeof value !== "string") return value;
69
+ if (value === oldRunDirectory) return newRunDirectory;
70
+ const prefix = `${oldRunDirectory}${sep}`;
71
+ if (value.startsWith(prefix)) {
72
+ return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
73
+ }
74
+ return value;
75
+ }
76
+
77
+ /**
78
+ * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
79
+ * nested run path is not partially claimed by a shorter sibling prefix.
80
+ */
81
+ export function rewriteRunDirectoryPathValueAgainstRewrites(
82
+ value: unknown,
83
+ rewrites: readonly RunDirectoryPathRewrite[],
84
+ ): unknown {
85
+ if (typeof value !== "string" || rewrites.length === 0) return value;
86
+ let best: RunDirectoryPathRewrite | undefined;
87
+ for (const rewrite of rewrites) {
88
+ if (
89
+ value === rewrite.oldRunDirectory ||
90
+ value.startsWith(`${rewrite.oldRunDirectory}${sep}`)
91
+ ) {
92
+ if (
93
+ best === undefined ||
94
+ rewrite.oldRunDirectory.length > best.oldRunDirectory.length
95
+ ) {
96
+ best = rewrite;
97
+ }
98
+ }
99
+ }
100
+ if (best === undefined) return value;
101
+ return rewriteRunDirectoryPathValue(
102
+ value,
103
+ best.oldRunDirectory,
104
+ best.newRunDirectory,
105
+ );
106
+ }
107
+
108
+ /** In-place rewrite of selected fields on one record (single pair). */
109
+ export function rewriteRunDirectoryPathFields(
110
+ record: Record<string, unknown>,
111
+ fields: readonly string[],
112
+ oldRunDirectory: string,
113
+ newRunDirectory: string,
114
+ ): void {
115
+ rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
116
+ { oldRunDirectory, newRunDirectory },
117
+ ]);
118
+ }
119
+
120
+ /** In-place rewrite of selected fields against a rewrite set. */
121
+ export function rewriteRunDirectoryPathFieldsAgainstRewrites(
122
+ record: Record<string, unknown>,
123
+ fields: readonly string[],
124
+ rewrites: readonly RunDirectoryPathRewrite[],
125
+ ): void {
126
+ for (const field of fields) {
127
+ if (field in record) {
128
+ record[field] = rewriteRunDirectoryPathValueAgainstRewrites(
129
+ record[field],
130
+ rewrites,
131
+ );
132
+ }
133
+ }
134
+ }
135
+
136
+ function collectRewrites(input: {
137
+ readonly oldRunDirectory: string;
138
+ readonly newRunDirectory: string;
139
+ readonly crossRunRewrites?: readonly RunDirectoryPathRewrite[];
140
+ }): readonly RunDirectoryPathRewrite[] {
141
+ const own: RunDirectoryPathRewrite = {
142
+ oldRunDirectory: input.oldRunDirectory,
143
+ newRunDirectory: input.newRunDirectory,
144
+ };
145
+ if (
146
+ input.crossRunRewrites === undefined ||
147
+ input.crossRunRewrites.length === 0
148
+ ) {
149
+ return [own];
150
+ }
151
+ // Own pair first; duplicates of the same old path keep the first (own) target.
152
+ const seen = new Set<string>([own.oldRunDirectory]);
153
+ const out: RunDirectoryPathRewrite[] = [own];
154
+ for (const rewrite of input.crossRunRewrites) {
155
+ if (seen.has(rewrite.oldRunDirectory)) continue;
156
+ seen.add(rewrite.oldRunDirectory);
157
+ out.push(rewrite);
158
+ }
159
+ return out;
160
+ }
161
+
162
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
163
+ return value !== null && typeof value === "object" && !Array.isArray(value);
164
+ }
165
+
166
+ /** Typed notary/source-run locator: only runDirectory is a path. */
167
+ function rewriteSourceRunLocator(
168
+ value: unknown,
169
+ rewrites: readonly RunDirectoryPathRewrite[],
170
+ ): void {
171
+ if (!isPlainObject(value)) return;
172
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
173
+ value,
174
+ SOURCE_RUN_LOCATOR_FIELDS,
175
+ rewrites,
176
+ );
177
+ }
178
+
179
+ /** Same-ticket summons materials: typed source-run + frozen attachment pointers. */
180
+ function rewriteSummonsMaterials(
181
+ value: unknown,
182
+ rewrites: readonly RunDirectoryPathRewrite[],
183
+ ): void {
184
+ if (!isPlainObject(value)) return;
185
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
186
+ value,
187
+ SUMMONS_PATH_FIELDS,
188
+ rewrites,
189
+ );
190
+ rewriteSourceRunLocator(value.sourceRun, rewrites);
191
+ // Bare-resume frozen attachment pointers (string[]), not free text / bytes.
192
+ if (Array.isArray(value.attachmentPaths)) {
193
+ value.attachmentPaths = value.attachmentPaths.map((path) =>
194
+ rewriteRunDirectoryPathValueAgainstRewrites(path, rewrites),
195
+ );
196
+ }
197
+ }
198
+
199
+ async function rewriteJsonObjectFile(
200
+ path: string,
201
+ fields: readonly string[],
202
+ rewrites: readonly RunDirectoryPathRewrite[],
203
+ ): Promise<void> {
204
+ if (!existsSync(path)) return;
205
+ const page = JSON.parse(await readFile(path, "utf8")) as unknown;
206
+ if (!isPlainObject(page)) return;
207
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, fields, rewrites);
208
+ await writeFile(path, `${JSON.stringify(page, null, 2)}\n`, "utf8");
209
+ }
210
+
211
+ async function rewriteOfficerPointerFile(
212
+ path: string,
213
+ rewrites: readonly RunDirectoryPathRewrite[],
214
+ ): Promise<void> {
215
+ if (!existsSync(path)) return;
216
+ const page = JSON.parse(await readFile(path, "utf8")) as unknown;
217
+ if (!isPlainObject(page)) return;
218
+ // Only the typed direct-officer pointer shape — never arbitrary .pointer.json.
219
+ if (page.kind !== "direct-officer-run-pointer") return;
220
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
221
+ page,
222
+ OFFICER_POINTER_FIELDS,
223
+ rewrites,
224
+ );
225
+ await writeFile(path, `${JSON.stringify(page)}\n`, "utf8");
226
+ }
227
+
228
+ async function rewriteSitianRecordsJsonl(
229
+ path: string,
230
+ rewrites: readonly RunDirectoryPathRewrite[],
231
+ ): Promise<void> {
232
+ if (!existsSync(path)) return;
233
+ const raw = await readFile(path, "utf8");
234
+ if (raw.length === 0) return;
235
+ const endsWithNewline = raw.endsWith("\n");
236
+ const lines = raw.split("\n");
237
+ // split keeps a trailing empty slot when file ends with \n — preserve it.
238
+ let changed = false;
239
+ const out: string[] = [];
240
+ for (let i = 0; i < lines.length; i += 1) {
241
+ const line = lines[i]!;
242
+ if (line === "" && i === lines.length - 1 && endsWithNewline) {
243
+ out.push("");
244
+ continue;
245
+ }
246
+ if (line.trim() === "") {
247
+ out.push(line);
248
+ continue;
249
+ }
250
+ let parsed: unknown;
251
+ try {
252
+ parsed = JSON.parse(line);
253
+ } catch {
254
+ out.push(line);
255
+ continue;
256
+ }
257
+ if (!isPlainObject(parsed)) {
258
+ out.push(line);
259
+ continue;
260
+ }
261
+ if (!("sessionParent" in parsed)) {
262
+ out.push(line);
263
+ continue;
264
+ }
265
+ const before = parsed.sessionParent;
266
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
267
+ parsed,
268
+ SITIAN_RECORD_FIELDS,
269
+ rewrites,
270
+ );
271
+ if (parsed.sessionParent !== before) changed = true;
272
+ out.push(JSON.stringify(parsed));
273
+ }
274
+ if (!changed) return;
275
+ const body = out.join("\n");
276
+ await writeFile(
277
+ path,
278
+ endsWithNewline && !body.endsWith("\n") ? `${body}\n` : body,
279
+ "utf8",
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Session transcript typed locators only: header.parentSession and
285
+ * ak_auditor_parent_attempt_binding data.parent.sessionFile. Never message text.
286
+ */
287
+ async function rewriteSessionTranscriptBindings(
288
+ path: string,
289
+ rewrites: readonly RunDirectoryPathRewrite[],
290
+ ): Promise<void> {
291
+ if (!existsSync(path)) return;
292
+ const raw = await readFile(path, "utf8");
293
+ if (raw.length === 0) return;
294
+ const endsWithNewline = raw.endsWith("\n");
295
+ const lines = raw.split("\n");
296
+ let changed = false;
297
+ const out: string[] = [];
298
+ for (let i = 0; i < lines.length; i += 1) {
299
+ const line = lines[i]!;
300
+ if (line === "" && i === lines.length - 1 && endsWithNewline) {
301
+ out.push("");
302
+ continue;
303
+ }
304
+ if (line.trim() === "") {
305
+ out.push(line);
306
+ continue;
307
+ }
308
+ let parsed: unknown;
309
+ try {
310
+ parsed = JSON.parse(line);
311
+ } catch {
312
+ out.push(line);
313
+ continue;
314
+ }
315
+ if (!isPlainObject(parsed)) {
316
+ out.push(line);
317
+ continue;
318
+ }
319
+ let lineChanged = false;
320
+ if (parsed.type === "session" && "parentSession" in parsed) {
321
+ const before = parsed.parentSession;
322
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
323
+ parsed,
324
+ SESSION_HEADER_FIELDS,
325
+ rewrites,
326
+ );
327
+ if (parsed.parentSession !== before) lineChanged = true;
328
+ } else if (
329
+ parsed.type === "custom" &&
330
+ parsed.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE &&
331
+ isPlainObject(parsed.data) &&
332
+ isPlainObject(parsed.data.parent)
333
+ ) {
334
+ const parent = parsed.data.parent;
335
+ const before = parent.sessionFile;
336
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
337
+ parent,
338
+ BINDING_PARENT_FIELDS,
339
+ rewrites,
340
+ );
341
+ if (parent.sessionFile !== before) lineChanged = true;
342
+ }
343
+ if (lineChanged) changed = true;
344
+ out.push(lineChanged ? JSON.stringify(parsed) : line);
345
+ }
346
+ if (!changed) return;
347
+ const body = out.join("\n");
348
+ await writeFile(
349
+ path,
350
+ endsWithNewline && !body.endsWith("\n") ? `${body}\n` : body,
351
+ "utf8",
352
+ );
353
+ }
354
+
355
+ /**
356
+ * Package-owned nested seams under session/ only:
357
+ * current-session.json, direct-officer *.pointer.json, sitian records.jsonl,
358
+ * session transcript typed parent bindings. Never walks attachments/ or artifacts/.
359
+ */
360
+ async function rewriteNestedMachinePathPages(
361
+ pagesDirectory: string,
362
+ rewrites: readonly RunDirectoryPathRewrite[],
363
+ ): Promise<void> {
364
+ const sessionRoot = join(pagesDirectory, "session");
365
+ async function walk(directory: string): Promise<void> {
366
+ let entries;
367
+ try {
368
+ entries = await readdir(directory, { withFileTypes: true });
369
+ } catch (error) {
370
+ if (isEnoent(error)) return;
371
+ throw error;
372
+ }
373
+ for (const entry of entries) {
374
+ const path = join(directory, entry.name);
375
+ if (entry.isDirectory()) {
376
+ await walk(path);
377
+ continue;
378
+ }
379
+ if (!entry.isFile()) continue;
380
+ if (entry.name === "current-session.json") {
381
+ await rewriteJsonObjectFile(path, CURRENT_SESSION_FIELDS, rewrites);
382
+ } else if (entry.name.endsWith(".pointer.json")) {
383
+ await rewriteOfficerPointerFile(path, rewrites);
384
+ } else if (entry.name === "records.jsonl") {
385
+ await rewriteSitianRecordsJsonl(path, rewrites);
386
+ } else if (entry.name.endsWith(".jsonl")) {
387
+ await rewriteSessionTranscriptBindings(path, rewrites);
388
+ }
389
+ }
390
+ }
391
+ await walk(sessionRoot);
392
+ }
393
+
394
+ /**
395
+ * Rewrite admitted-request / invocation / run-state path fields (attachment
396
+ * frozenPath / summons.attachmentPaths pointers only), then nested package-owned
397
+ * session seams, after the run directory has already been moved or copied.
398
+ * `pagesDirectory` is where the pages now live; path strings still naming an
399
+ * old run directory become the matching new directory. `crossRunRewrites`
400
+ * covers officer/parent pointers that landed under a different final placement.
401
+ */
402
+ export async function rewriteRoleRunDurablePages(input: {
403
+ readonly pagesDirectory: string;
404
+ readonly oldRunDirectory: string;
405
+ readonly newRunDirectory: string;
406
+ readonly crossRunRewrites?: readonly RunDirectoryPathRewrite[];
407
+ }): Promise<void> {
408
+ const { pagesDirectory } = input;
409
+ const rewrites = collectRewrites(input);
410
+
411
+ const admittedPath = join(pagesDirectory, "admitted-request.json");
412
+ if (existsSync(admittedPath)) {
413
+ const page = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
414
+ string,
415
+ unknown
416
+ >;
417
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
418
+ page,
419
+ ADMITTED_PAGE_FIELDS,
420
+ rewrites,
421
+ );
422
+ // Nested notary typed locator — same value family as top-level sourceRunPath.
423
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
424
+ if (Array.isArray(page.attachments)) {
425
+ for (const attachment of page.attachments) {
426
+ if (isPlainObject(attachment)) {
427
+ // Pointer only — never open or mutate frozen attachment bytes.
428
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
429
+ attachment,
430
+ ["frozenPath"],
431
+ rewrites,
432
+ );
433
+ }
434
+ }
435
+ }
436
+ // Nested principal wire (sessionDirectory/sessionFile) when present.
437
+ if (isPlainObject(page.principal)) {
438
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
439
+ page.principal,
440
+ ["sessionDirectory", "sessionFile"],
441
+ rewrites,
442
+ );
443
+ }
444
+ await writeFile(admittedPath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
445
+ }
446
+
447
+ const invocationPath = join(pagesDirectory, "invocation.json");
448
+ if (existsSync(invocationPath)) {
449
+ const page = JSON.parse(await readFile(invocationPath, "utf8")) as Record<
450
+ string,
451
+ unknown
452
+ >;
453
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
454
+ page,
455
+ INVOCATION_PAGE_FIELDS,
456
+ rewrites,
457
+ );
458
+ await writeFile(
459
+ invocationPath,
460
+ `${JSON.stringify(page, null, 2)}\n`,
461
+ "utf8",
462
+ );
463
+ }
464
+
465
+ const statePath = join(pagesDirectory, "run-state.json");
466
+ if (existsSync(statePath)) {
467
+ const page = JSON.parse(await readFile(statePath, "utf8")) as Record<
468
+ string,
469
+ unknown
470
+ >;
471
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
472
+ page,
473
+ RUN_STATE_PAGE_FIELDS,
474
+ rewrites,
475
+ );
476
+ if (isPlainObject(page.principal)) {
477
+ rewriteRunDirectoryPathFieldsAgainstRewrites(
478
+ page.principal,
479
+ ["sessionDirectory", "sessionFile"],
480
+ rewrites,
481
+ );
482
+ }
483
+ // Open court summons: source-run locators + frozen attachment path pointers.
484
+ if (isPlainObject(page.currentCourt)) {
485
+ rewriteSummonsMaterials(page.currentCourt.summons, rewrites);
486
+ }
487
+ await writeFile(statePath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
488
+ }
489
+
490
+ await rewriteNestedMachinePathPages(pagesDirectory, rewrites);
491
+ }
@@ -422,7 +422,7 @@ export type { MergerRoleDependencies } from "./merger-role.ts";
422
422
 
423
423
  type WorkerArmable = {
424
424
  activate(context?: HostContext): Promise<void>;
425
- armSubmissionGate(cwd: string, parent?: { getSessionFile(): string | undefined }): void;
425
+ armSubmissionGate(cwd: string, parent: { getSessionFile(): string | undefined }): void;
426
426
  };
427
427
 
428
428
  type ActivationRuntime = {
@@ -974,6 +974,7 @@ export function createDiaristRoleRuntime(
974
974
  await commitDiaristEntries({
975
975
  ticketNumber,
976
976
  cwd: coords.projectRoot,
977
+ sessionParent: join(coords.runDirectory, "session", "session.jsonl"),
977
978
  home: coords.home,
978
979
  entries: projectDiaristEntries(parameters),
979
980
  });
@@ -1102,6 +1103,8 @@ export function createRoleRuntimeExtension(
1102
1103
  let pendingNavigatorPresentation: { event: import("./navigator-attendance.ts").NavigatorEvent; report: import("./navigator-attendance.ts").NavigatorReport } | undefined;
1103
1104
  let pendingNavigatorSettlement: Promise<void> | undefined;
1104
1105
  let navigatorWorkContext: NavigatorWorkContext | undefined;
1106
+ let navigatorSessionParent: string | undefined;
1107
+ let navigatorCwd: string | undefined;
1105
1108
  /** toolCallId → fact+evidence; one-shot projected onto durable tool_result (#475). */
1106
1109
  const pendingInfrastructureFailures = new Map<string, PendingInfrastructureFailure>();
1107
1110
  // Envelope-owned execute→tool_result bridge for submission non-pass (ADR 0018 / #525).
@@ -1157,18 +1160,19 @@ export function createRoleRuntimeExtension(
1157
1160
  void Promise.resolve(attendance.dispose()).then(
1158
1161
  undefined,
1159
1162
  (error) => {
1163
+ const diagnostic = error instanceof Error ? error.message : String(error);
1160
1164
  try {
1161
1165
  sitianReport({
1162
1166
  level: "event",
1163
1167
  kind: "navigator-dispose-failure",
1164
- payload: {
1165
- diagnostic: error instanceof Error ? error.message : String(error),
1166
- },
1168
+ cwd: navigatorCwd,
1169
+ sessionParent: navigatorSessionParent,
1170
+ payload: { diagnostic },
1167
1171
  source: "role-runtime",
1168
1172
  });
1169
1173
  } catch (recordError) {
1170
1174
  envelopeHost.appendEntry?.("ak-navigator-dispose-failure", {
1171
- diagnostic: error instanceof Error ? error.message : String(error),
1175
+ diagnostic,
1172
1176
  recordFailure: recordError instanceof Error ? recordError.message : String(recordError),
1173
1177
  });
1174
1178
  }
@@ -1963,6 +1967,8 @@ export function createRoleRuntimeExtension(
1963
1967
  && entry.role !== "navigator"
1964
1968
  && !isStationChild
1965
1969
  ) {
1970
+ navigatorSessionParent = ctx.sessionManager.getSessionFile();
1971
+ navigatorCwd = ctx.cwd;
1966
1972
  let work: NavigatorWorkContext;
1967
1973
  let contextError: unknown;
1968
1974
  if (dependencies.loadNavigatorWorkContext === undefined) {
@@ -9,6 +9,8 @@
9
9
  import { readdir, readFile } from "node:fs/promises";
10
10
  import { basename, dirname, join } from "node:path";
11
11
 
12
+ import { roleRunArtifactsDirectory } from "./role-run-placement.ts";
13
+
12
14
  export const RUN_TERMINAL_ARTIFACT_FILES = [
13
15
  "report.json",
14
16
  "error.json",
@@ -191,7 +193,7 @@ function presentUniqueFallbackBoundToRun(
191
193
  export async function readRunTerminalArtifact(
192
194
  runDirectory: string,
193
195
  ): Promise<RunTerminalArtifactRead> {
194
- const artifactsDir = join(runDirectory, "artifacts");
196
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
195
197
  for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
196
198
  const path = join(artifactsDir, file);
197
199
  const read = await readTerminalArtifactAtPath(path, file);