@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
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Sole nested-volume reader for gate-cycle facts under session/auditor-roles/.
3
3
  *
4
- * Consumers: Analyst sole ledger scan (classifyScopedRun) and Terminal gate
5
- * projection (#478). Metric families must not open a second disk scan — they
6
- * consume retained facts. Terminal settlement reuses this same pairing seam
7
- * (no second auditor-roles scanner).
4
+ * Consumer: Analyst sole ledger scan (classifyScopedRun). Metric families must
5
+ * not open a second disk scan — they consume retained facts.
8
6
  *
9
7
  * Naming: records may carry pre-#440 menxia/jishizhong/fubaolang tool faces or
10
8
  * the current gatekeeper/inspector/notary English face. Projection always uses
@@ -29,7 +27,10 @@ import { readdir } from "node:fs/promises";
29
27
  import { join } from "node:path";
30
28
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, } from "./compliance-transport.js";
31
29
  import { extractSessionTimestampSpan, intervalRowsAroundAnchor, readLedgerSessionJsonl, } from "./ledger-session-read.js";
32
- import { isRecord, errorText, isEnoent } from "./unknown-value.js";
30
+ import { isRecord, isEnoent } from "./unknown-value.js";
31
+ import { retainedRunPathsMatch } from "./role-run-relocation.js";
32
+ import { runDirectoryOfSessionFile } from "./role-run-placement.js";
33
+ import { resolveOfficerSessionFromPointerFile } from "./archivist-record-pointer.js";
33
34
  const DISPATCH_TOOLS = new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
34
35
  /** Officer terminating tool → current English officer identity. */
35
36
  const OFFICER_TOOL_TO_FACE = {
@@ -309,27 +310,6 @@ function pairGateRounds(volumes) {
309
310
  .sort((a, b) => a.officerStartedAt.localeCompare(b.officerStartedAt))
310
311
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
311
312
  }
312
- /** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
313
- async function resolveOfficerSessionFromPointerFile(pointerPath) {
314
- const { readFile } = await import("node:fs/promises");
315
- let raw;
316
- try {
317
- raw = JSON.parse(await readFile(pointerPath, "utf8"));
318
- }
319
- catch (error) {
320
- throw new Error(`direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`, { cause: error });
321
- }
322
- if (!isRecord(raw) || raw.kind !== "direct-officer-run-pointer" || raw.version !== 1) {
323
- throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
324
- }
325
- const sessionFile = raw.sessionFile;
326
- if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
327
- throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
328
- }
329
- const officer = raw.officer === "inspector" || raw.officer === "notary"
330
- ? raw.officer : undefined;
331
- return { sessionFile, ...(officer === undefined ? {} : { officer }) };
332
- }
333
313
  /**
334
314
  * Read and pair gate-cycle rounds from one or more auditor-roles directories.
335
315
  * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
@@ -346,7 +326,7 @@ export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirector
346
326
  const volumes = [];
347
327
  // Same officer session pointed at N times (historical multi-mint pointers, or
348
328
  // one parent booking many leaves) must classify once — else each accepted
349
- // seal is counted ×N in terminal gate-round (#753 acceptance A).
329
+ // seal is counted ×N in gate-cycle statistics (#753 acceptance A).
350
330
  const classifiedOfficerSessions = new Set();
351
331
  for (const directory of directories) {
352
332
  let names;
@@ -375,11 +355,13 @@ export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirector
375
355
  continue;
376
356
  classifiedOfficerSessions.add(sessionPath);
377
357
  }
378
- const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
358
+ const officer = pointer?.officer === "inspector" || pointer?.officer === "notary"
359
+ ? pointer.officer : undefined;
360
+ const classified = await classifyAuditorVolume(sessionPath, officer);
379
361
  for (const volume of classified) {
380
362
  if (options.parentSessionFile !== undefined &&
381
363
  volume.parentSessionFile !== undefined &&
382
- volume.parentSessionFile !== options.parentSessionFile) {
364
+ !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))) {
383
365
  continue;
384
366
  }
385
367
  volumes.push(volume);
@@ -16,9 +16,11 @@ import { readdir, readFile } from "node:fs/promises";
16
16
  import { basename, join } from "node:path";
17
17
  import { isSyntheticAnalystBookKey, resolveAnalystBookKey } from "./analyst-book-key.js";
18
18
  import { pathContainedIn, physicalPathIdentity, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
19
- import { listBookRunDirectories } from "./role-run-placement.js";
19
+ import { listBookRunDirectories, parseRunLeaf, sessionFileOf, } from "./role-run-placement.js";
20
+ import { sitianRunVolumeDirectory } from "./sitian-appender.js";
20
21
  import { autopsyWriterLock, readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
21
22
  import { readRunTicketNumber } from "./run-ticket-number.js";
23
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.js";
22
24
  import { extractSessionModelSequence, extractSessionTimestampSpan, extractSessionToolIntervals, LedgerSessionJsonlError, readLedgerSessionJsonl, } from "./ledger-session-read.js";
23
25
  import { readRunTerminalArtifact, } from "./run-terminal-artifacts.js";
24
26
  import { readAnalystGateCyclesFromAuditorRoles, } from "./analyst-gate-cycles-read.js";
@@ -65,12 +67,6 @@ async function classifyGhostCandidate(input) {
65
67
  },
66
68
  };
67
69
  }
68
- function parseRunDirectoryName(name) {
69
- const at = name.lastIndexOf("@");
70
- if (at <= 0 || at === name.length - 1)
71
- return undefined;
72
- return { runId: name.slice(0, at), role: name.slice(at + 1) };
73
- }
74
70
  async function listLedgerBookNames(booksRoot) {
75
71
  try {
76
72
  const entries = await readdir(booksRoot, { withFileTypes: true });
@@ -136,14 +132,16 @@ async function resolveSessionFile(runDirectory) {
136
132
  if (isRecord(parsed)
137
133
  && typeof parsed.sessionFile === "string"
138
134
  && parsed.sessionFile.trim() !== "") {
139
- return parsed.sessionFile;
135
+ return typeof parsed.runDirectory === "string"
136
+ ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory)
137
+ : parsed.sessionFile;
140
138
  }
141
139
  }
142
140
  catch (error) {
143
141
  if (!isMissingPathError(error))
144
142
  throw error;
145
143
  }
146
- return join(runDirectory, "session", "session.jsonl");
144
+ return sessionFileOf(runDirectory);
147
145
  }
148
146
  /**
149
147
  * Attribute session rows to one run.
@@ -394,8 +392,8 @@ async function classifyScopedRun(input) {
394
392
  // nested JSONL is page-local unreadable — never silently under-count rounds.
395
393
  let gateCycles;
396
394
  try {
397
- const parentSessionFile = join(input.runDirectory, "session", "session.jsonl");
398
- gateCycles = await readAnalystGateCyclesFromAuditorRoles(join(input.runDirectory, "session", "auditor-roles"), { parentSessionFile });
395
+ const parentSessionFile = sessionFileOf(input.runDirectory);
396
+ gateCycles = await readAnalystGateCyclesFromAuditorRoles(sitianRunVolumeDirectory(input.runDirectory, "auditor-roles"), { parentSessionFile });
399
397
  }
400
398
  catch (error) {
401
399
  return {
@@ -497,7 +495,7 @@ export async function scanAnalystIssueRuns(input) {
497
495
  }
498
496
  for (const runDirectory of runDirectories) {
499
497
  const runName = basename(runDirectory);
500
- const parsed = parseRunDirectoryName(runName);
498
+ const parsed = parseRunLeaf(runName);
501
499
  if (parsed === undefined)
502
500
  continue;
503
501
  let scopeFields;
@@ -25,6 +25,7 @@ import {
25
25
  resolveNavigatorWorkSubjectPlacement
26
26
  } from "./archivist-record-topology.js";
27
27
  import { piRecordSessionHost } from "./pi/record-session-host.js";
28
+ import { sitianVolumeDirectory } from "./sitian-appender.js";
28
29
  import {
29
30
  DIRECT_OFFICER_RUN_POINTER_KIND,
30
31
  bookDirectOfficerRunPointer
@@ -35,7 +36,7 @@ import {
35
36
  } from "./archivist-record-topology.js";
36
37
  const CURRENT_SESSION_LEDGER = "current-session.json";
37
38
  function readCurrentSession(sessionDir) {
38
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
39
+ const ledger = currentSessionLedgerPath(sessionDir);
39
40
  try {
40
41
  const value = JSON.parse(readFileSync(ledger, "utf8"));
41
42
  if (typeof value !== "object" || value === null || typeof value.sessionFile !== "string" || value.sessionFile.length === 0) {
@@ -50,7 +51,7 @@ function readCurrentSession(sessionDir) {
50
51
  }
51
52
  }
52
53
  function writeCurrentSession(sessionDir, sessionFile) {
53
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
54
+ const ledger = currentSessionLedgerPath(sessionDir);
54
55
  try {
55
56
  writeFileSync(ledger, `${JSON.stringify({ sessionFile })}
56
57
  `, { flag: "wx" });
@@ -268,7 +269,7 @@ function createRecordSessionOpen(options, host = piRecordSessionHost) {
268
269
  if (!physicallyContainedIn(ledgerHome, parentResolved)) {
269
270
  throw new Error("Durable record ownership requires a parent session inside the ledger home");
270
271
  }
271
- sessionDir = join(dirname(parentResolved), options.kind);
272
+ sessionDir = sitianVolumeDirectory(dirname(parentResolved), options.kind);
272
273
  parentSession = parentFile;
273
274
  mayResumeSameNest = options.kind === WORKER_SUBMISSION_GATE_KIND;
274
275
  }
@@ -1,9 +1,17 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { isRecord } from "./unknown-value.js";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { isRecord, errorText } from "./unknown-value.js";
5
+ import { homeFromRunDirectory } from "./activation-ledger-topology.js";
6
+ import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
7
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.js";
8
+ import { parseRunLeaf, runDirectoryOfSessionFile, runDirectoryFromSessionDirectory } from "./role-run-placement.js";
9
+ import { sitianVolumeDirectory } from "./sitian-appender.js";
5
10
  /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
6
11
  export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer";
12
+ function directOfficerRunPointerFile(parentSessionFile, officer) {
13
+ return join(sitianVolumeDirectory(dirname(parentSessionFile), "auditor-roles"), `${officer}.pointer.json`);
14
+ }
7
15
  /**
8
16
  * Book a typed pointer under parent session/auditor-roles (same nest owner as
9
17
  * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
@@ -11,11 +19,11 @@ export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer";
11
19
  *
12
20
  * Stable leaf per officer under one parent (#753 gate-round accounting):
13
21
  * same-parent re-summons upsert the same pointer instead of minting N files that
14
- * each re-scan the full officer session and multiply terminal gate-round counts.
22
+ * each re-scan the full officer session and multiply gate-cycle counts.
15
23
  */
16
24
  export function bookDirectOfficerRunPointer(options) {
17
- const nest = join(dirname(options.parentSessionFile), "auditor-roles");
18
- mkdirSync(nest, { recursive: true });
25
+ const pointerFile = directOfficerRunPointerFile(options.parentSessionFile, options.officer);
26
+ mkdirSync(dirname(pointerFile), { recursive: true });
19
27
  const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
20
28
  const pointer = {
21
29
  version: 1,
@@ -27,38 +35,57 @@ export function bookDirectOfficerRunPointer(options) {
27
35
  : {}),
28
36
  ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
29
37
  };
30
- writeFileSync(join(nest, `${options.officer}.pointer.json`), `${JSON.stringify(pointer)}\n`, "utf8");
38
+ writeFileSync(pointerFile, `${JSON.stringify(pointer)}\n`, "utf8");
31
39
  return pointer;
32
40
  }
33
- /**
34
- * Read the one officer pointer. Missing or unreadable is not a pass:
35
- * the caller re-runs the gate.
36
- */
37
- export async function readDirectOfficerRunPointer(parentSessionFile, officer) {
41
+ /** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
42
+ export async function resolveOfficerSessionFromPointerFile(pointerPath) {
43
+ let raw;
38
44
  try {
39
- const raw = JSON.parse(await readFile(join(dirname(parentSessionFile), "auditor-roles", `${officer}.pointer.json`), "utf8"));
40
- if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
41
- return undefined;
45
+ raw = JSON.parse(await readFile(pointerPath, "utf8"));
46
+ }
47
+ catch (error) {
48
+ throw new Error(`direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`, { cause: error });
49
+ }
50
+ if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
51
+ throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
52
+ }
53
+ if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
54
+ throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
55
+ }
56
+ const officer = raw.officer === "inspector" || raw.officer === "notary"
57
+ || raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
58
+ let runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
59
+ ? raw.runDirectory : undefined;
60
+ let sessionFile = raw.sessionFile;
61
+ const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
62
+ const leaf = parseRunLeaf(basename(recordedRunDirectory));
63
+ const parentRunDirectory = runDirectoryFromSessionDirectory(dirname(dirname(pointerPath)));
64
+ const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
65
+ if (leaf !== undefined && parentIdentity !== undefined) {
66
+ const currentRunDirectory = await findRunDirectoryById(homeFromRunDirectory(parentRunDirectory), leaf.runId, parentIdentity.bookKey, leaf.role);
67
+ if (currentRunDirectory !== undefined) {
68
+ sessionFile = rewriteRunDirectoryPathValue(sessionFile, recordedRunDirectory, currentRunDirectory);
69
+ if (runDirectory !== undefined)
70
+ runDirectory = currentRunDirectory;
42
71
  }
43
- if (raw.officer !== officer)
44
- return undefined;
45
- if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "")
72
+ }
73
+ const submissionToolCallId = typeof raw.submissionToolCallId === "string"
74
+ && raw.submissionToolCallId.trim() !== "" ? raw.submissionToolCallId : undefined;
75
+ return {
76
+ sessionFile,
77
+ ...(officer === undefined ? {} : { officer }),
78
+ ...(runDirectory === undefined ? {} : { runDirectory }),
79
+ ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
80
+ };
81
+ }
82
+ /** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
83
+ export async function readDirectOfficerRunPointer(parentSessionFile, officer) {
84
+ try {
85
+ const pointer = await resolveOfficerSessionFromPointerFile(directOfficerRunPointerFile(parentSessionFile, officer));
86
+ if (pointer.officer !== officer)
46
87
  return undefined;
47
- const runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
48
- ? raw.runDirectory
49
- : undefined;
50
- const submissionToolCallId = typeof raw.submissionToolCallId === "string"
51
- && raw.submissionToolCallId.trim() !== ""
52
- ? raw.submissionToolCallId
53
- : undefined;
54
- return {
55
- version: 1,
56
- kind: DIRECT_OFFICER_RUN_POINTER_KIND,
57
- officer,
58
- sessionFile: raw.sessionFile,
59
- ...(runDirectory === undefined ? {} : { runDirectory }),
60
- ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
61
- };
88
+ return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
62
89
  }
63
90
  catch {
64
91
  return undefined;
@@ -1,11 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
- async function writeFileAtomically(destination, contents) {
4
+ async function writeFileAtomically(destination, contents, options) {
5
5
  const parent = dirname(destination);
6
6
  const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
7
7
  try {
8
- await writeFile(temporary, contents);
8
+ await writeFile(temporary, contents, options);
9
9
  await rename(temporary, destination);
10
10
  } catch (error) {
11
11
  await rm(temporary, { force: true }).catch(() => void 0);
@@ -1,11 +1,15 @@
1
- import { dirname, join, resolve } from "node:path";
2
- import { roleRunArtifactsDirectory } from "./role-run-placement.js";
1
+ import { join, resolve } from "node:path";
2
+ import {
3
+ roleRunArtifactsDirectory,
4
+ runDirectoryOfSessionFile,
5
+ sessionFileOf
6
+ } from "./role-run-placement.js";
3
7
  import { Type } from "typebox";
4
8
  const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier";
5
9
  function auditorRunDirectory(context) {
6
10
  const sessionFile = context.sessionManager?.getSessionFile?.();
7
11
  if (sessionFile === void 0) return void 0;
8
- return resolve(dirname(dirname(sessionFile)));
12
+ return resolve(runDirectoryOfSessionFile(sessionFile));
9
13
  }
10
14
  function createAuditorDossierTool(runDirectory) {
11
15
  return {
@@ -24,7 +28,7 @@ function createAuditorDossierTool(runDirectory) {
24
28
  const details = {
25
29
  runDirectory,
26
30
  admittedRequest: join(runDirectory, "admitted-request.json"),
27
- parentSessionCandidate: join(runDirectory, "session", "session.jsonl"),
31
+ parentSessionCandidate: sessionFileOf(runDirectory),
28
32
  attachments: join(runDirectory, "attachments"),
29
33
  artifacts: roleRunArtifactsDirectory(runDirectory)
30
34
  };
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
+ import { parseRunLeaf } from "./role-run-placement.js";
3
4
  import { isRecord } from "./unknown-value.js";
4
5
  import { joinPackageMaterials, readPackageMaterial, } from "./session-opening-materials.js";
5
6
  /**
@@ -84,7 +85,7 @@ export function loadAuditorReferenceMaterialsFromSubjectInput(raw) {
84
85
  return loadAuditorReferenceMaterials(resolveAuditorSubject(raw));
85
86
  }
86
87
  function subjectFromSourceDirectory(sourceRunDirectory) {
87
- const role = basename(sourceRunDirectory).split("@")[1];
88
+ const role = parseRunLeaf(basename(sourceRunDirectory))?.role;
88
89
  return isAuditorSoulRole(role) ? role : undefined;
89
90
  }
90
91
  /**
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { sha256Hex } from "./sha256.js";
3
3
  import { isRecord } from "./unknown-value.js";
4
+ import { isTicketNumberString, parseTicketNumber } from "./run-ticket-number.js";
4
5
  export const COLLECTOR_OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
5
6
  export const COLLECTOR_REPO_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,98}[A-Za-z0-9])?$/;
6
7
  function fail(message, cause) {
@@ -30,13 +31,13 @@ export function parseCollectorRepository(raw) {
30
31
  return { display: raw, canonical: `${owner}/${repo}`, owner, repo };
31
32
  }
32
33
  export function parseCollectorPrNumber(raw) {
33
- if (typeof raw === "string" && !/^[1-9][0-9]*$/.test(raw))
34
- fail("Collector pull request number must be a positive safe integer string");
35
34
  if (typeof raw !== "string" && typeof raw !== "number")
36
35
  fail("Collector pull request number is required");
37
- const value = Number(raw);
38
- if (!Number.isSafeInteger(value) || value < 1)
39
- fail("Collector pull request number must be a positive safe integer");
36
+ const value = parseTicketNumber(raw);
37
+ if (value === undefined)
38
+ fail(typeof raw === "string" && !isTicketNumberString(raw)
39
+ ? "Collector pull request number must be a positive safe integer string"
40
+ : "Collector pull request number must be a positive safe integer");
40
41
  return value;
41
42
  }
42
43
  function canonicalManifest(requests) {
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { dirname, relative, resolve, sep } from "node:path";
3
3
  import { sha256Hex } from "./sha256.js";
4
+ import { physicallyContainedIn } from "./activation-ledger-topology.js";
4
5
  import { AcceptedDetailsContractError, acceptedFacts, isTerminatingToolName, validateAcceptedDetails } from "./package-contracts/terminating-tools.js";
5
6
  import { isRecord, isMissingPathError } from "./unknown-value.js";
6
7
  async function discoverCaseFiles(root) { const found = []; async function walk(dir, depth) { for (const item of await readdir(dir, { withFileTypes: true })) {
@@ -106,8 +107,11 @@ function deriveSession(content, id) {
106
107
  return { session, turns, calls, tokens, statuses, commits };
107
108
  }
108
109
  /** Read Pi's retained session directory as the sole raw material for one case. */
109
- export async function loadDoctorCase(runsPath) {
110
+ export async function loadDoctorCase(runsPath, projectRoot) {
110
111
  const root = await realpath(runsPath);
112
+ if (projectRoot !== undefined && !physicallyContainedIn(projectRoot, root)) {
113
+ throw new Error("doctor --runs escapes the project root");
114
+ }
111
115
  // Canonical <book>/<ticket>/runs; legacy issues/<n>/runs remains read-only compatible.
112
116
  const match = root.split(sep).join("/").match(/\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/);
113
117
  if (!match)
@@ -17,8 +17,10 @@
17
17
  */
18
18
  import { randomUUID } from "node:crypto";
19
19
  import { readFileSync } from "node:fs";
20
- import { dirname, join } from "node:path";
20
+ import { join } from "node:path";
21
+ export { runDirectoryFromSessionDirectory, sessionFileIn as sessionFileFromSessionDirectory, } from "./role-run-placement.js";
21
22
  import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.js";
23
+ import { sitianRunVolumeDirectory, sitianVolumeRecordsFile } from "./sitian-appender.js";
22
24
  import { readSitianRecords, resolveSitianRecordPath, sitianReport, } from "./sitian-facade.js";
23
25
  import { isRecord } from "./unknown-value.js";
24
26
  /** Sitian event kind for one detour-tool call (volume under session/). */
@@ -30,7 +32,7 @@ export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage";
30
32
  * Openable once the run directory is known from the terminal's run id.
31
33
  * Contains no runId bytes itself (#108 + #537).
32
34
  */
33
- export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = `session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl`;
35
+ export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = sitianVolumeRecordsFile(sitianRunVolumeDirectory("", ENGINE_DETOUR_CALL_KIND));
34
36
  /** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
35
37
  export function engineDetourStdoutByteLength(stdout) {
36
38
  return Buffer.byteLength(stdout, "utf8");
@@ -212,14 +214,6 @@ export function withEngineDetourToolUsageFact(outcome, usage) {
212
214
  },
213
215
  };
214
216
  }
215
- /** runDirectory owning a session directory (.../runs/<id>@role/session). */
216
- export function runDirectoryFromSessionDirectory(sessionDirectory) {
217
- return dirname(sessionDirectory);
218
- }
219
- /** session.jsonl under a session directory. */
220
- export function sessionFileFromSessionDirectory(sessionDirectory) {
221
- return join(sessionDirectory, "session.jsonl");
222
- }
223
217
  /**
224
218
  * Mint one public-invocation scope id.
225
219
  * Call once at the public-entry boundary — never inside the auto-resume loop.