@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
@@ -39,6 +39,7 @@ import { AUDITOR_OUTPUT_TOOL_NAME, } from "../package-contracts/auditor-output.j
39
39
  import { observePackagedMethodSkillInvocation, } from "../package-resources/method-skill.js";
40
40
  import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isReceiptSettlementBindingClear, NAVIGATOR_INVOCATION_ENTRY, parseInvocationMarkerIdentity, } from "../navigator-invocation-identity.js";
41
41
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_TURN_LIMIT, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, } from "../receipt-delivery-policy.js";
42
+ import { roleRunArtifactsDirectory } from "../role-run-placement.js";
42
43
  import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
43
44
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
44
45
  function sealedLedgerHome(admitted) {
@@ -47,6 +48,7 @@ function sealedLedgerHome(admitted) {
47
48
  function ledgerReadScope(admitted, scope) {
48
49
  return {
49
50
  home: sealedLedgerHome(admitted),
51
+ sessionParent: join(admitted.runDirectory, "session", "session.jsonl"),
50
52
  ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
51
53
  ? {}
52
54
  : { attemptId: scope.courtAttemptId }),
@@ -94,7 +96,7 @@ async function sealedLedgerOutcome(admitted, role, scope) {
94
96
  export async function attemptProducedFreshSubmission(admitted, scope) {
95
97
  if (scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0)
96
98
  return true;
97
- return hasFreshAttemptSubmission(admitted.projectRoot, admitted.runId, scope.courtAttemptId, sealedLedgerHome(admitted));
99
+ return hasFreshAttemptSubmission(admitted.projectRoot, admitted.runId, scope.courtAttemptId, ledgerReadScope(admitted, scope));
98
100
  }
99
101
  /** #836: every raw role payload in settle scope (调几次记几次). */
100
102
  export async function recordedSubmissionPayloads(admitted, scope) {
@@ -2591,7 +2593,7 @@ function uniqueFailureFallbackDirs(runDirectory, baseDir) {
2591
2593
  * (e.g. a file occupies that name), fall back to the run directory itself.
2592
2594
  */
2593
2595
  async function resolveFailureArtifactsBase(runDirectory) {
2594
- const artifactsDir = join(runDirectory, "artifacts");
2596
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
2595
2597
  try {
2596
2598
  await ensureRunArtifactsDir(runDirectory);
2597
2599
  return { baseDir: artifactsDir };
@@ -2660,7 +2662,7 @@ export async function publishFailureArtifacts(admitted, failure, authority) {
2660
2662
  // Prefer conventional names; unique fallback dirs keep colliding fixed paths
2661
2663
  // from stranding the original failure outside settlement. Include the ledger
2662
2664
  // runs/ parent so a locked run directory (EACCES) cannot exhaust durability.
2663
- const underArtifacts = baseDir === join(admitted.runDirectory, "artifacts");
2665
+ const underArtifacts = baseDir === roleRunArtifactsDirectory(admitted.runDirectory);
2664
2666
  const uniqueFallbackDirs = uniqueFailureFallbackDirs(admitted.runDirectory, baseDir);
2665
2667
  const errorCandidates = underArtifacts
2666
2668
  ? [
@@ -0,0 +1,84 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import {
4
+ activationBookDirectory,
5
+ ensureRealDirectoryTree
6
+ } from "./activation-ledger-topology.js";
7
+ import { requireSafePositiveTicketNumber } from "./run-ticket-number.js";
8
+ function isMissingPathError(error) {
9
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
10
+ }
11
+ async function listBookRunDirectories(bookDir) {
12
+ const out = [];
13
+ const seen = /* @__PURE__ */ new Set();
14
+ const collect = async (runsDir) => {
15
+ let entries;
16
+ try {
17
+ entries = await readdir(runsDir, { withFileTypes: true });
18
+ } catch (error) {
19
+ if (isMissingPathError(error)) return;
20
+ throw error;
21
+ }
22
+ for (const entry of entries) {
23
+ if (!entry.isDirectory()) continue;
24
+ const runDir = join(runsDir, entry.name);
25
+ if (seen.has(runDir)) continue;
26
+ seen.add(runDir);
27
+ out.push(runDir);
28
+ }
29
+ };
30
+ await collect(join(bookDir, "runs"));
31
+ let subjects;
32
+ try {
33
+ subjects = await readdir(bookDir, { withFileTypes: true });
34
+ } catch (error) {
35
+ if (isMissingPathError(error)) return out.sort();
36
+ throw error;
37
+ }
38
+ for (const subject of subjects) {
39
+ if (!subject.isDirectory() || subject.name === "runs") continue;
40
+ await collect(join(bookDir, subject.name, "runs"));
41
+ }
42
+ return out.sort();
43
+ }
44
+ function roleRunPlacement(ledgerHome, input) {
45
+ const subjectDirectory = "ticketNumber" in input.subject ? String(requireSafePositiveTicketNumber(
46
+ input.subject.ticketNumber,
47
+ "roleRunPlacement subject.ticketNumber"
48
+ )) : "unbound";
49
+ const runDirectory = join(
50
+ activationBookDirectory(ledgerHome, input.bookKey),
51
+ subjectDirectory,
52
+ "runs",
53
+ `${input.runId}@${input.role}`
54
+ );
55
+ const sessionDirectory = join(runDirectory, "session");
56
+ return {
57
+ runDirectory,
58
+ sessionDirectory,
59
+ sessionFile: join(sessionDirectory, "session.jsonl"),
60
+ artifactsDirectory: roleRunArtifactsDirectory(runDirectory),
61
+ attachmentsDirectory: join(runDirectory, "attachments")
62
+ };
63
+ }
64
+ function roleRunArtifactsDirectory(runDirectory) {
65
+ return join(runDirectory, "artifacts");
66
+ }
67
+ function ensureRoleRunDirectory(ledgerHome, directory) {
68
+ return ensureRealDirectoryTree(ledgerHome, directory);
69
+ }
70
+ function ensureRoleRunPlacement(ledgerHome, placement) {
71
+ for (const directory of [
72
+ placement.sessionDirectory,
73
+ placement.attachmentsDirectory
74
+ ]) {
75
+ ensureRoleRunDirectory(ledgerHome, directory);
76
+ }
77
+ }
78
+ export {
79
+ ensureRoleRunDirectory,
80
+ ensureRoleRunPlacement,
81
+ listBookRunDirectories,
82
+ roleRunArtifactsDirectory,
83
+ roleRunPlacement
84
+ };
@@ -0,0 +1,358 @@
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
+ import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
13
+ const ADMITTED_PAGE_FIELDS = [
14
+ "runDirectory",
15
+ "admittedRequestPath",
16
+ "sessionDirectory",
17
+ "sessionFile",
18
+ "taskPath",
19
+ "packetPath",
20
+ "prerequisitesPath",
21
+ "requestManifestPath",
22
+ "mergerInputPath",
23
+ "sourceRunPath",
24
+ ];
25
+ const INVOCATION_PAGE_FIELDS = [
26
+ "runDirectory",
27
+ "sessionDirectory",
28
+ "sessionFile",
29
+ ];
30
+ const RUN_STATE_PAGE_FIELDS = [
31
+ "runDirectory",
32
+ "admittedRequestPath",
33
+ "sessionDirectory",
34
+ "sessionFile",
35
+ ];
36
+ const SOURCE_RUN_LOCATOR_FIELDS = ["runDirectory"];
37
+ const SUMMONS_PATH_FIELDS = ["sourceRunPath"];
38
+ const CURRENT_SESSION_FIELDS = ["sessionFile"];
39
+ const OFFICER_POINTER_FIELDS = ["sessionFile", "runDirectory"];
40
+ const SITIAN_RECORD_FIELDS = ["sessionParent"];
41
+ const SESSION_HEADER_FIELDS = ["parentSession"];
42
+ const BINDING_PARENT_FIELDS = ["sessionFile"];
43
+ function isEnoent(error) {
44
+ return (error instanceof Error &&
45
+ "code" in error &&
46
+ error.code === "ENOENT");
47
+ }
48
+ /** Rewrite one path value that points at or under oldRunDirectory. */
49
+ export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirectory) {
50
+ if (typeof value !== "string")
51
+ return value;
52
+ if (value === oldRunDirectory)
53
+ return newRunDirectory;
54
+ const prefix = `${oldRunDirectory}${sep}`;
55
+ if (value.startsWith(prefix)) {
56
+ return `${newRunDirectory}${value.slice(oldRunDirectory.length)}`;
57
+ }
58
+ return value;
59
+ }
60
+ /**
61
+ * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
62
+ * nested run path is not partially claimed by a shorter sibling prefix.
63
+ */
64
+ export function rewriteRunDirectoryPathValueAgainstRewrites(value, rewrites) {
65
+ if (typeof value !== "string" || rewrites.length === 0)
66
+ return value;
67
+ let best;
68
+ for (const rewrite of rewrites) {
69
+ if (value === rewrite.oldRunDirectory ||
70
+ value.startsWith(`${rewrite.oldRunDirectory}${sep}`)) {
71
+ if (best === undefined ||
72
+ rewrite.oldRunDirectory.length > best.oldRunDirectory.length) {
73
+ best = rewrite;
74
+ }
75
+ }
76
+ }
77
+ if (best === undefined)
78
+ return value;
79
+ return rewriteRunDirectoryPathValue(value, best.oldRunDirectory, best.newRunDirectory);
80
+ }
81
+ /** In-place rewrite of selected fields on one record (single pair). */
82
+ export function rewriteRunDirectoryPathFields(record, fields, oldRunDirectory, newRunDirectory) {
83
+ rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, [
84
+ { oldRunDirectory, newRunDirectory },
85
+ ]);
86
+ }
87
+ /** In-place rewrite of selected fields against a rewrite set. */
88
+ export function rewriteRunDirectoryPathFieldsAgainstRewrites(record, fields, rewrites) {
89
+ for (const field of fields) {
90
+ if (field in record) {
91
+ record[field] = rewriteRunDirectoryPathValueAgainstRewrites(record[field], rewrites);
92
+ }
93
+ }
94
+ }
95
+ function collectRewrites(input) {
96
+ const own = {
97
+ oldRunDirectory: input.oldRunDirectory,
98
+ newRunDirectory: input.newRunDirectory,
99
+ };
100
+ if (input.crossRunRewrites === undefined ||
101
+ input.crossRunRewrites.length === 0) {
102
+ return [own];
103
+ }
104
+ // Own pair first; duplicates of the same old path keep the first (own) target.
105
+ const seen = new Set([own.oldRunDirectory]);
106
+ const out = [own];
107
+ for (const rewrite of input.crossRunRewrites) {
108
+ if (seen.has(rewrite.oldRunDirectory))
109
+ continue;
110
+ seen.add(rewrite.oldRunDirectory);
111
+ out.push(rewrite);
112
+ }
113
+ return out;
114
+ }
115
+ function isPlainObject(value) {
116
+ return value !== null && typeof value === "object" && !Array.isArray(value);
117
+ }
118
+ /** Typed notary/source-run locator: only runDirectory is a path. */
119
+ function rewriteSourceRunLocator(value, rewrites) {
120
+ if (!isPlainObject(value))
121
+ return;
122
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SOURCE_RUN_LOCATOR_FIELDS, rewrites);
123
+ }
124
+ /** Same-ticket summons materials: typed source-run + frozen attachment pointers. */
125
+ function rewriteSummonsMaterials(value, rewrites) {
126
+ if (!isPlainObject(value))
127
+ return;
128
+ rewriteRunDirectoryPathFieldsAgainstRewrites(value, SUMMONS_PATH_FIELDS, rewrites);
129
+ rewriteSourceRunLocator(value.sourceRun, rewrites);
130
+ // Bare-resume frozen attachment pointers (string[]), not free text / bytes.
131
+ if (Array.isArray(value.attachmentPaths)) {
132
+ value.attachmentPaths = value.attachmentPaths.map((path) => rewriteRunDirectoryPathValueAgainstRewrites(path, rewrites));
133
+ }
134
+ }
135
+ async function rewriteJsonObjectFile(path, fields, rewrites) {
136
+ if (!existsSync(path))
137
+ return;
138
+ const page = JSON.parse(await readFile(path, "utf8"));
139
+ if (!isPlainObject(page))
140
+ return;
141
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, fields, rewrites);
142
+ await writeFile(path, `${JSON.stringify(page, null, 2)}\n`, "utf8");
143
+ }
144
+ async function rewriteOfficerPointerFile(path, rewrites) {
145
+ if (!existsSync(path))
146
+ return;
147
+ const page = JSON.parse(await readFile(path, "utf8"));
148
+ if (!isPlainObject(page))
149
+ return;
150
+ // Only the typed direct-officer pointer shape — never arbitrary .pointer.json.
151
+ if (page.kind !== "direct-officer-run-pointer")
152
+ return;
153
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, OFFICER_POINTER_FIELDS, rewrites);
154
+ await writeFile(path, `${JSON.stringify(page)}\n`, "utf8");
155
+ }
156
+ async function rewriteSitianRecordsJsonl(path, rewrites) {
157
+ if (!existsSync(path))
158
+ return;
159
+ const raw = await readFile(path, "utf8");
160
+ if (raw.length === 0)
161
+ return;
162
+ const endsWithNewline = raw.endsWith("\n");
163
+ const lines = raw.split("\n");
164
+ // split keeps a trailing empty slot when file ends with \n — preserve it.
165
+ let changed = false;
166
+ const out = [];
167
+ for (let i = 0; i < lines.length; i += 1) {
168
+ const line = lines[i];
169
+ if (line === "" && i === lines.length - 1 && endsWithNewline) {
170
+ out.push("");
171
+ continue;
172
+ }
173
+ if (line.trim() === "") {
174
+ out.push(line);
175
+ continue;
176
+ }
177
+ let parsed;
178
+ try {
179
+ parsed = JSON.parse(line);
180
+ }
181
+ catch {
182
+ out.push(line);
183
+ continue;
184
+ }
185
+ if (!isPlainObject(parsed)) {
186
+ out.push(line);
187
+ continue;
188
+ }
189
+ if (!("sessionParent" in parsed)) {
190
+ out.push(line);
191
+ continue;
192
+ }
193
+ const before = parsed.sessionParent;
194
+ rewriteRunDirectoryPathFieldsAgainstRewrites(parsed, SITIAN_RECORD_FIELDS, rewrites);
195
+ if (parsed.sessionParent !== before)
196
+ changed = true;
197
+ out.push(JSON.stringify(parsed));
198
+ }
199
+ if (!changed)
200
+ return;
201
+ const body = out.join("\n");
202
+ await writeFile(path, endsWithNewline && !body.endsWith("\n") ? `${body}\n` : body, "utf8");
203
+ }
204
+ /**
205
+ * Session transcript typed locators only: header.parentSession and
206
+ * ak_auditor_parent_attempt_binding data.parent.sessionFile. Never message text.
207
+ */
208
+ async function rewriteSessionTranscriptBindings(path, rewrites) {
209
+ if (!existsSync(path))
210
+ return;
211
+ const raw = await readFile(path, "utf8");
212
+ if (raw.length === 0)
213
+ return;
214
+ const endsWithNewline = raw.endsWith("\n");
215
+ const lines = raw.split("\n");
216
+ let changed = false;
217
+ const out = [];
218
+ for (let i = 0; i < lines.length; i += 1) {
219
+ const line = lines[i];
220
+ if (line === "" && i === lines.length - 1 && endsWithNewline) {
221
+ out.push("");
222
+ continue;
223
+ }
224
+ if (line.trim() === "") {
225
+ out.push(line);
226
+ continue;
227
+ }
228
+ let parsed;
229
+ try {
230
+ parsed = JSON.parse(line);
231
+ }
232
+ catch {
233
+ out.push(line);
234
+ continue;
235
+ }
236
+ if (!isPlainObject(parsed)) {
237
+ out.push(line);
238
+ continue;
239
+ }
240
+ let lineChanged = false;
241
+ if (parsed.type === "session" && "parentSession" in parsed) {
242
+ const before = parsed.parentSession;
243
+ rewriteRunDirectoryPathFieldsAgainstRewrites(parsed, SESSION_HEADER_FIELDS, rewrites);
244
+ if (parsed.parentSession !== before)
245
+ lineChanged = true;
246
+ }
247
+ else if (parsed.type === "custom" &&
248
+ parsed.customType === AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE &&
249
+ isPlainObject(parsed.data) &&
250
+ isPlainObject(parsed.data.parent)) {
251
+ const parent = parsed.data.parent;
252
+ const before = parent.sessionFile;
253
+ rewriteRunDirectoryPathFieldsAgainstRewrites(parent, BINDING_PARENT_FIELDS, rewrites);
254
+ if (parent.sessionFile !== before)
255
+ lineChanged = true;
256
+ }
257
+ if (lineChanged)
258
+ changed = true;
259
+ out.push(lineChanged ? JSON.stringify(parsed) : line);
260
+ }
261
+ if (!changed)
262
+ return;
263
+ const body = out.join("\n");
264
+ await writeFile(path, endsWithNewline && !body.endsWith("\n") ? `${body}\n` : body, "utf8");
265
+ }
266
+ /**
267
+ * Package-owned nested seams under session/ only:
268
+ * current-session.json, direct-officer *.pointer.json, sitian records.jsonl,
269
+ * session transcript typed parent bindings. Never walks attachments/ or artifacts/.
270
+ */
271
+ async function rewriteNestedMachinePathPages(pagesDirectory, rewrites) {
272
+ const sessionRoot = join(pagesDirectory, "session");
273
+ async function walk(directory) {
274
+ let entries;
275
+ try {
276
+ entries = await readdir(directory, { withFileTypes: true });
277
+ }
278
+ catch (error) {
279
+ if (isEnoent(error))
280
+ return;
281
+ throw error;
282
+ }
283
+ for (const entry of entries) {
284
+ const path = join(directory, entry.name);
285
+ if (entry.isDirectory()) {
286
+ await walk(path);
287
+ continue;
288
+ }
289
+ if (!entry.isFile())
290
+ continue;
291
+ if (entry.name === "current-session.json") {
292
+ await rewriteJsonObjectFile(path, CURRENT_SESSION_FIELDS, rewrites);
293
+ }
294
+ else if (entry.name.endsWith(".pointer.json")) {
295
+ await rewriteOfficerPointerFile(path, rewrites);
296
+ }
297
+ else if (entry.name === "records.jsonl") {
298
+ await rewriteSitianRecordsJsonl(path, rewrites);
299
+ }
300
+ else if (entry.name.endsWith(".jsonl")) {
301
+ await rewriteSessionTranscriptBindings(path, rewrites);
302
+ }
303
+ }
304
+ }
305
+ await walk(sessionRoot);
306
+ }
307
+ /**
308
+ * Rewrite admitted-request / invocation / run-state path fields (attachment
309
+ * frozenPath / summons.attachmentPaths pointers only), then nested package-owned
310
+ * session seams, after the run directory has already been moved or copied.
311
+ * `pagesDirectory` is where the pages now live; path strings still naming an
312
+ * old run directory become the matching new directory. `crossRunRewrites`
313
+ * covers officer/parent pointers that landed under a different final placement.
314
+ */
315
+ export async function rewriteRoleRunDurablePages(input) {
316
+ const { pagesDirectory } = input;
317
+ const rewrites = collectRewrites(input);
318
+ const admittedPath = join(pagesDirectory, "admitted-request.json");
319
+ if (existsSync(admittedPath)) {
320
+ const page = JSON.parse(await readFile(admittedPath, "utf8"));
321
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, ADMITTED_PAGE_FIELDS, rewrites);
322
+ // Nested notary typed locator — same value family as top-level sourceRunPath.
323
+ rewriteSourceRunLocator(page.sourceRun, rewrites);
324
+ if (Array.isArray(page.attachments)) {
325
+ for (const attachment of page.attachments) {
326
+ if (isPlainObject(attachment)) {
327
+ // Pointer only — never open or mutate frozen attachment bytes.
328
+ rewriteRunDirectoryPathFieldsAgainstRewrites(attachment, ["frozenPath"], rewrites);
329
+ }
330
+ }
331
+ }
332
+ // Nested principal wire (sessionDirectory/sessionFile) when present.
333
+ if (isPlainObject(page.principal)) {
334
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, ["sessionDirectory", "sessionFile"], rewrites);
335
+ }
336
+ await writeFile(admittedPath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
337
+ }
338
+ const invocationPath = join(pagesDirectory, "invocation.json");
339
+ if (existsSync(invocationPath)) {
340
+ const page = JSON.parse(await readFile(invocationPath, "utf8"));
341
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, INVOCATION_PAGE_FIELDS, rewrites);
342
+ await writeFile(invocationPath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
343
+ }
344
+ const statePath = join(pagesDirectory, "run-state.json");
345
+ if (existsSync(statePath)) {
346
+ const page = JSON.parse(await readFile(statePath, "utf8"));
347
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page, RUN_STATE_PAGE_FIELDS, rewrites);
348
+ if (isPlainObject(page.principal)) {
349
+ rewriteRunDirectoryPathFieldsAgainstRewrites(page.principal, ["sessionDirectory", "sessionFile"], rewrites);
350
+ }
351
+ // Open court summons: source-run locators + frozen attachment path pointers.
352
+ if (isPlainObject(page.currentCourt)) {
353
+ rewriteSummonsMaterials(page.currentCourt.summons, rewrites);
354
+ }
355
+ await writeFile(statePath, `${JSON.stringify(page, null, 2)}\n`, "utf8");
356
+ }
357
+ await rewriteNestedMachinePathPages(pagesDirectory, rewrites);
358
+ }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { readdir, readFile } from "node:fs/promises";
10
10
  import { basename, dirname, join } from "node:path";
11
+ import { roleRunArtifactsDirectory } from "./role-run-placement.js";
11
12
  export const RUN_TERMINAL_ARTIFACT_FILES = [
12
13
  "report.json",
13
14
  "error.json",
@@ -154,7 +155,7 @@ function presentUniqueFallbackBoundToRun(body, expectedRunId) {
154
155
  * A present file that cannot be parsed as a usable typed JSON object is unreadable.
155
156
  */
156
157
  export async function readRunTerminalArtifact(runDirectory) {
157
- const artifactsDir = join(runDirectory, "artifacts");
158
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
158
159
  for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
159
160
  const path = join(artifactsDir, file);
160
161
  const read = await readTerminalArtifactAtPath(path, file);
@@ -1,40 +1,59 @@
1
- /**
2
- * Typed ticketNumber reader for a retained run's durable pages.
3
- * admitted-request.json first, invocation.json fallback. Missing page (ENOENT)
4
- * → try next / undefined; damage and non-ENOENT IO failures propagate.
5
- */
6
1
  import { readFile } from "node:fs/promises";
7
2
  import { join } from "node:path";
3
+ const MIGRATION_TICKET_DERIVATION_PAGE = "migration-ticket-derivation.json";
8
4
  function isEnoent(error) {
9
- return (error instanceof Error &&
10
- "code" in error &&
11
- error.code === "ENOENT");
5
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
6
+ }
7
+ function isSafePositiveTicketNumber(value) {
8
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
9
+ }
10
+ function requireSafePositiveTicketNumber(ticketNumber, context = "ticketNumber") {
11
+ if (!isSafePositiveTicketNumber(ticketNumber)) {
12
+ throw new Error(
13
+ `${context} requires a safe positive integer, got ${String(ticketNumber)}`
14
+ );
15
+ }
16
+ return ticketNumber;
12
17
  }
13
18
  function ticketFromRecord(record) {
14
- const ticketNumber = record.ticketNumber;
15
- if (typeof ticketNumber === "number" &&
16
- Number.isSafeInteger(ticketNumber) &&
17
- ticketNumber >= 1) {
18
- return ticketNumber;
19
- }
20
- return undefined;
21
- }
22
- async function readPageTicketNumber(runDirectory, page) {
23
- try {
24
- const raw = JSON.parse(await readFile(join(runDirectory, page), "utf8"));
25
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
26
- return undefined;
27
- }
28
- return ticketFromRecord(raw);
29
- }
30
- catch (error) {
31
- if (isEnoent(error))
32
- return undefined;
33
- throw error;
19
+ return isSafePositiveTicketNumber(record.ticketNumber) ? record.ticketNumber : void 0;
20
+ }
21
+ async function readJsonObject(path) {
22
+ try {
23
+ const raw = JSON.parse(await readFile(path, "utf8"));
24
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
25
+ return void 0;
34
26
  }
27
+ return raw;
28
+ } catch (error) {
29
+ if (isEnoent(error)) return void 0;
30
+ throw error;
31
+ }
32
+ }
33
+ async function readBoardPageTicketNumber(runDirectory, page) {
34
+ const record = await readJsonObject(join(runDirectory, page));
35
+ if (record === void 0) return void 0;
36
+ return ticketFromRecord(record);
37
+ }
38
+ async function readBoardTicketNumber(runDirectory) {
39
+ return await readBoardPageTicketNumber(runDirectory, "admitted-request.json") ?? await readBoardPageTicketNumber(runDirectory, "invocation.json");
40
+ }
41
+ async function readMigrationDerivedTicketNumber(runDirectory) {
42
+ const record = await readJsonObject(
43
+ join(runDirectory, MIGRATION_TICKET_DERIVATION_PAGE)
44
+ );
45
+ if (record === void 0) return void 0;
46
+ if (record.derivation !== "worktree-path-basename") return void 0;
47
+ return ticketFromRecord(record);
35
48
  }
36
- /** Sole reader of ticketNumber from a retained run's admitted then invocation pages. */
37
- export async function readRunTicketNumber(runDirectory) {
38
- return ((await readPageTicketNumber(runDirectory, "admitted-request.json")) ??
39
- (await readPageTicketNumber(runDirectory, "invocation.json")));
49
+ async function readRunTicketNumber(runDirectory) {
50
+ return await readBoardTicketNumber(runDirectory) ?? await readMigrationDerivedTicketNumber(runDirectory);
40
51
  }
52
+ export {
53
+ MIGRATION_TICKET_DERIVATION_PAGE,
54
+ isSafePositiveTicketNumber,
55
+ readBoardTicketNumber,
56
+ readMigrationDerivedTicketNumber,
57
+ readRunTicketNumber,
58
+ requireSafePositiveTicketNumber
59
+ };
@@ -6,7 +6,7 @@ import {
6
6
  unlinkSync,
7
7
  writeFileSync
8
8
  } from "node:fs";
9
- import { basename, dirname, join, resolve } from "node:path";
9
+ import { dirname, join } from "node:path";
10
10
  import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
11
11
  import {
12
12
  activationBookDirectory,
@@ -152,36 +152,21 @@ function resolveSitianVolumeCategory(kind) {
152
152
  }
153
153
  return kind;
154
154
  }
155
- function safeBookKey(cwd) {
156
- try {
157
- return resolveBookKeyFromGit(cwd);
158
- } catch {
159
- return basename(resolve(cwd)) || "default";
160
- }
161
- }
162
155
  function resolveSitianRecordPathInLedger(input, ledgerHome) {
163
- const cwd = input.cwd ?? process.cwd();
164
156
  const category = resolveSitianVolumeCategory(input.kind);
157
+ const ticketNumber = category === "ticket-provenance" ? typeof input.subject === "string" && /^[1-9][0-9]*$/.test(input.subject) ? input.subject : typeof input.subject === "object" && typeof input.subject.ticketNumber === "number" && Number.isSafeInteger(input.subject.ticketNumber) && input.subject.ticketNumber > 0 ? String(input.subject.ticketNumber) : void 0 : void 0;
165
158
  let sessionDir;
166
- if (input.sessionParent !== void 0 && input.sessionParent.length > 0 && physicallyContainedIn(ledgerHome, input.sessionParent)) {
167
- sessionDir = join(dirname(input.sessionParent), category);
159
+ if (ticketNumber !== void 0) {
160
+ const bookDir = activationBookDirectory(
161
+ ledgerHome,
162
+ resolveBookKeyFromGit(input.cwd ?? process.cwd())
163
+ );
164
+ sessionDir = join(bookDir, ticketNumber);
168
165
  } else {
169
- const bookKey = safeBookKey(cwd);
170
- const bookDir = activationBookDirectory(ledgerHome, bookKey);
171
- if (input.subject !== void 0) {
172
- let subjectStr;
173
- if (typeof input.subject === "string") {
174
- subjectStr = input.subject;
175
- } else if (typeof input.subject.runId === "string" && input.subject.runId.length > 0) {
176
- subjectStr = input.subject.runId;
177
- } else {
178
- subjectStr = JSON.stringify(input.subject);
179
- }
180
- const digest = createHash("sha256").update(subjectStr).digest("hex").slice(0, 32);
181
- sessionDir = join(bookDir, category, digest);
182
- } else {
183
- sessionDir = join(bookDir, category);
166
+ if (input.sessionParent === void 0 || input.sessionParent.length === 0 || !physicallyContainedIn(ledgerHome, input.sessionParent)) {
167
+ throw new Error("Sitian record ownership requires a parent session inside the ledger home");
184
168
  }
169
+ sessionDir = join(dirname(input.sessionParent), category);
185
170
  }
186
171
  const recordFile = join(sessionDir, "records.jsonl");
187
172
  return { sessionDir, recordFile, ledgerHome };