@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,25 +1,16 @@
1
- import { join } from "node:path";
1
+ import { ROLE_RUN_SESSION_FILENAME, sessionFileOf } from "./role-run-placement.js";
2
+ import { readStrictPiSessionJsonl } from "./ledger-session-read.js";
2
3
  import { errorText } from "./unknown-value.js";
3
4
  export async function readAssistantUsageFromSessionFile(sessionFile) {
4
- const { readFile } = await import("node:fs/promises");
5
- let text;
5
+ let rows;
6
6
  try {
7
- text = await readFile(sessionFile, "utf8");
7
+ rows = await readStrictPiSessionJsonl(sessionFile);
8
8
  }
9
9
  catch (error) {
10
10
  if (error?.code === "ENOENT")
11
11
  return undefined;
12
- throw error;
13
- }
14
- let rows;
15
- try {
16
- rows = text
17
- .trim()
18
- .split("\n")
19
- .filter(Boolean)
20
- .map((line) => JSON.parse(line));
21
- }
22
- catch (error) {
12
+ if (!(error instanceof SyntaxError))
13
+ throw error;
23
14
  throw new Error(`session usage parse failed (${sessionFile}): ${errorText(error)}`, { cause: error });
24
15
  }
25
16
  let total = 0;
@@ -82,11 +73,11 @@ export async function readAssistantUsageFromSessionFile(sessionFile) {
82
73
  */
83
74
  export function sessionFileFromPublicSummon(summoned) {
84
75
  if (typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== "") {
85
- return join(summoned.runDirectory, "session", "session.jsonl");
76
+ return sessionFileOf(summoned.runDirectory);
86
77
  }
87
78
  const fromArtifacts = summoned.terminal?.artifacts
88
79
  ?.map((a) => a.path)
89
- .find((p) => typeof p === "string" && p.endsWith("session.jsonl"));
80
+ .find((p) => typeof p === "string" && p.endsWith(ROLE_RUN_SESSION_FILENAME));
90
81
  if (fromArtifacts !== undefined)
91
82
  return fromArtifacts;
92
83
  const outcome = summoned.terminal?.roleOutcome;
@@ -95,7 +86,7 @@ export function sessionFileFromPublicSummon(summoned) {
95
86
  const facts = outcome.decisiveFacts;
96
87
  const pointer = facts?.runPointer;
97
88
  if (typeof pointer === "string" && pointer.trim() !== "") {
98
- return join(pointer, "session", "session.jsonl");
89
+ return sessionFileOf(pointer);
99
90
  }
100
91
  return undefined;
101
92
  }
@@ -1,5 +1,6 @@
1
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import { writeFileAtomically } from "./atomic-write.js";
3
4
  import { DEFAULT_ROLE_TURN_HOST, lookupHeadlessHostDescription, lookupHostDescription, } from "./host-descriptions.js";
4
5
  /** Durable session binding stored beside the host-owned session principal. */
5
6
  export function createSessionIdentityAuthority(authority, sessionBindingFile) {
@@ -25,9 +26,7 @@ export function createSessionIdentityAuthority(authority, sessionBindingFile) {
25
26
  async bind(principal, sessionId) {
26
27
  const target = bindingPath(principal);
27
28
  await mkdir(dirname(target), { recursive: true });
28
- const temporary = `${target}.${process.pid}.tmp`;
29
- await writeFile(temporary, `${JSON.stringify({ sessionId })}\n`, { encoding: "utf8", mode: 0o600 });
30
- await rename(temporary, target);
29
+ await writeFileAtomically(target, `${JSON.stringify({ sessionId })}\n`, { mode: 0o600 });
31
30
  },
32
31
  };
33
32
  }
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { appendFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ import { sessionDirectoryOf } from "./role-run-placement.js";
5
+ import { ticketNumberFromSitianSubject } from "./run-ticket-number.js";
4
6
  import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
5
7
  import {
6
8
  activationBookDirectory,
@@ -41,13 +43,22 @@ function resolveSitianVolumeCategory(kind) {
41
43
  return kind;
42
44
  }
43
45
  const SITIAN_RECORDS_LEAF = "records.jsonl";
46
+ function sitianVolumeDirectory(sessionDirectory, category) {
47
+ return join(sessionDirectory, category);
48
+ }
49
+ function sitianRunVolumeDirectory(runDirectory, category) {
50
+ return sitianVolumeDirectory(sessionDirectoryOf(runDirectory), category);
51
+ }
52
+ function sitianVolumeRecordsFile(volumeDirectory) {
53
+ return join(volumeDirectory, SITIAN_RECORDS_LEAF);
54
+ }
44
55
  function ticketProvenanceUnderBookPaths(ledgerHome, bookKey, ticketId) {
45
56
  const sessionDir = join(activationBookDirectory(ledgerHome, bookKey), ticketId);
46
- return { sessionDir, recordFile: join(sessionDir, SITIAN_RECORDS_LEAF) };
57
+ return { sessionDir, recordFile: sitianVolumeRecordsFile(sessionDir) };
47
58
  }
48
59
  function resolveSitianRecordPathInLedger(input, ledgerHome) {
49
60
  const category = resolveSitianVolumeCategory(input.kind);
50
- 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;
61
+ const ticketNumber = category === "ticket-provenance" ? ticketNumberFromSitianSubject(input.subject) : void 0;
51
62
  let sessionDir;
52
63
  let recordFile;
53
64
  if (ticketNumber !== void 0) {
@@ -60,13 +71,13 @@ function resolveSitianRecordPathInLedger(input, ledgerHome) {
60
71
  recordFile = paths.recordFile;
61
72
  } else if (category === "ticket-provenance" && input.runDirectory !== void 0) {
62
73
  sessionDir = input.runDirectory;
63
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
74
+ recordFile = sitianVolumeRecordsFile(sessionDir);
64
75
  } else {
65
76
  if (input.sessionParent === void 0 || input.sessionParent.length === 0 || !physicallyContainedIn(ledgerHome, input.sessionParent)) {
66
77
  throw new Error("Sitian record ownership requires a parent session inside the ledger home");
67
78
  }
68
- sessionDir = join(dirname(input.sessionParent), category);
69
- recordFile = join(sessionDir, SITIAN_RECORDS_LEAF);
79
+ sessionDir = sitianVolumeDirectory(dirname(input.sessionParent), category);
80
+ recordFile = sitianVolumeRecordsFile(sessionDir);
70
81
  }
71
82
  if (!physicallyContainedIn(ledgerHome, sessionDir)) {
72
83
  throw new Error("Sitian record ownership requires a directory inside the ledger home");
@@ -120,5 +131,8 @@ export {
120
131
  appendSitianRecordBlock,
121
132
  resolveSitianRecordPath,
122
133
  resolveSitianRecordPathInLedger,
123
- resolveSitianVolumeCategory
134
+ resolveSitianVolumeCategory,
135
+ sitianRunVolumeDirectory,
136
+ sitianVolumeDirectory,
137
+ sitianVolumeRecordsFile
124
138
  };
@@ -1,4 +1,4 @@
1
- import { join } from "node:path";
1
+ import { sessionFileIn, sessionFileOf } from "./role-run-placement.js";
2
2
  import { resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
3
3
  import { courtAttemptIdFromHostContext, runDirectoryFromHostContext, } from "./host-contracts.js";
4
4
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
@@ -49,7 +49,7 @@ async function submissionRecordFile(cwd, runId, scope) {
49
49
  const discoveredRun = await findRunDirectoryById(scope.home, runId);
50
50
  if (discoveredRun === undefined)
51
51
  return undefined;
52
- sessionParent = join(discoveredRun, "session", "session.jsonl");
52
+ sessionParent = sessionFileOf(discoveredRun);
53
53
  }
54
54
  return resolveSitianRecordPathInLedger({
55
55
  level: "event",
@@ -265,13 +265,13 @@ async function restoreState(cwd, runId, scope) {
265
265
  function sessionParentFromHostContext(context) {
266
266
  const runDirectory = runDirectoryFromHostContext(context);
267
267
  if (runDirectory !== undefined)
268
- return join(runDirectory, "session", "session.jsonl");
268
+ return sessionFileOf(runDirectory);
269
269
  const sessionFile = context.sessionManager.getSessionFile?.();
270
270
  if (typeof sessionFile === "string" && sessionFile.length > 0)
271
271
  return sessionFile;
272
272
  const sessionDir = context.sessionManager.getSessionDir?.();
273
273
  if (typeof sessionDir === "string" && sessionDir.length > 0) {
274
- return join(sessionDir, "session.jsonl");
274
+ return sessionFileIn(sessionDir);
275
275
  }
276
276
  return undefined;
277
277
  }
@@ -1,3 +1,4 @@
1
+ import { parseTicketNumber } from "./run-ticket-number.js";
1
2
  import { isRecord } from "./unknown-value.js";
2
3
  /**
3
4
  * 起居录(ticket-provenance)typed 形状 —— ADR 0075 / ADR 0081 / #1090。
@@ -9,14 +10,7 @@ import { isRecord } from "./unknown-value.js";
9
10
  /** Sitian kind for per-ticket court diary volumes. */
10
11
  export const TICKET_PROVENANCE_KIND = "ticket-provenance";
11
12
  function positiveInteger(value) {
12
- if (typeof value === "number" && Number.isSafeInteger(value) && value >= 1)
13
- return value;
14
- if (typeof value === "string" && /^[1-9][0-9]*$/.test(value)) {
15
- const parsed = Number(value);
16
- if (Number.isSafeInteger(parsed))
17
- return parsed;
18
- }
19
- return undefined;
13
+ return parseTicketNumber(value);
20
14
  }
21
15
  function projectBound(value) {
22
16
  if (!isRecord(value))
@@ -4,10 +4,12 @@
4
4
  * 纯追加、不回读历史去重、不折叠。
5
5
  */
6
6
  import { readFile, unlink } from "node:fs/promises";
7
- import { basename, dirname, join, resolve } from "node:path";
7
+ import { join, resolve } from "node:path";
8
8
  import { errnoCode, packageMachineHome, physicallyContainedIn, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
9
9
  import { readLedgerSessionJsonlLines, } from "./ledger-session-read.js";
10
10
  import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
11
+ import { runDirectoryOfSessionFile, sessionFileOf } from "./role-run-placement.js";
12
+ import { sitianVolumeRecordsFile } from "./sitian-appender.js";
11
13
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.js";
12
14
  import { appendSitianRecordBlock, appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
13
15
  import { TICKET_PROVENANCE_KIND, } from "./ticket-provenance-contracts.js";
@@ -48,8 +50,7 @@ function isLedgerRoleSessionFile(absolute, home) {
48
50
  const ledgerHome = resolveActivationLedgerHome(machineHome);
49
51
  if (!physicallyContainedIn(ledgerHome, absolute))
50
52
  return false;
51
- return (basename(absolute) === "session.jsonl" &&
52
- basename(dirname(absolute)) === "session");
53
+ return sessionFileOf(runDirectoryOfSessionFile(absolute)) === absolute;
53
54
  }
54
55
  /** Real I/O seam gate: only host session stores or sitian role-run session.jsonl. */
55
56
  function assertDialogueSessionSourcePath(path, home) {
@@ -87,7 +88,7 @@ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
87
88
  }
88
89
  /** Assign a run-owned unbound diary as its original block through Sitian. */
89
90
  export async function rehomeUnboundTicketProvenance(runDirectory, ticketNumber, cwd, home) {
90
- const source = join(runDirectory, "records.jsonl");
91
+ const source = sitianVolumeRecordsFile(runDirectory);
91
92
  let content;
92
93
  try {
93
94
  content = await readFile(source, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5478",
3
+ "version": "0.1.5504",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,8 +1,8 @@
1
- import { join } from "node:path";
2
1
  import { spawn } from "node:child_process";
3
2
  import { createInterface } from "node:readline";
4
3
 
5
4
  import type { RoleTurnHost, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
5
+ import { sessionDirectoryOf } from "../role-run-placement.ts";
6
6
  import {
7
7
  createSerializedRoleTurnHost,
8
8
  driveExternalRoleTurnRounds,
@@ -496,7 +496,7 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
496
496
  host: config.hostName,
497
497
  sessionId,
498
498
  cwd: request.cwd,
499
- sessionDirectory: join(request.runDirectory, "session"),
499
+ sessionDirectory: sessionDirectoryOf(request.runDirectory),
500
500
  sessionParent,
501
501
  continuation: request.continuation,
502
502
  ...(request.model !== undefined ? { model: request.model } : {}),
@@ -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
@@ -38,7 +36,10 @@ import {
38
36
  type LedgerSessionRow,
39
37
  } from "./ledger-session-read.ts";
40
38
 
41
- import { isRecord, errorText, isEnoent } from "./unknown-value.ts";
39
+ import { isRecord, isEnoent } from "./unknown-value.ts";
40
+ import { retainedRunPathsMatch } from "./role-run-relocation.ts";
41
+ import { runDirectoryOfSessionFile } from "./role-run-placement.ts";
42
+ import { resolveOfficerSessionFromPointerFile } from "./archivist-record-pointer.ts";
42
43
 
43
44
  /** One completed gate round: direct officer receipt or historical province/officer pair. */
44
45
  /** Honest origin discriminant: direct summons vs historical province dispatch. */
@@ -416,32 +417,6 @@ function pairGateRounds(
416
417
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
417
418
  }
418
419
 
419
- /** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
420
- async function resolveOfficerSessionFromPointerFile(
421
- pointerPath: string,
422
- ): Promise<{ sessionFile: string; officer?: "inspector" | "notary" }> {
423
- const { readFile } = await import("node:fs/promises");
424
- let raw: unknown;
425
- try {
426
- raw = JSON.parse(await readFile(pointerPath, "utf8"));
427
- } catch (error) {
428
- throw new Error(
429
- `direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`,
430
- { cause: error },
431
- );
432
- }
433
- if (!isRecord(raw) || raw.kind !== "direct-officer-run-pointer" || raw.version !== 1) {
434
- throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
435
- }
436
- const sessionFile = raw.sessionFile;
437
- if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
438
- throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
439
- }
440
- const officer = raw.officer === "inspector" || raw.officer === "notary"
441
- ? raw.officer : undefined;
442
- return { sessionFile, ...(officer === undefined ? {} : { officer }) };
443
- }
444
-
445
420
  /**
446
421
  * Read and pair gate-cycle rounds from one or more auditor-roles directories.
447
422
  * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
@@ -463,7 +438,7 @@ export async function readAnalystGateCyclesFromAuditorRoles(
463
438
  const volumes: ClassifiedVolume[] = [];
464
439
  // Same officer session pointed at N times (historical multi-mint pointers, or
465
440
  // one parent booking many leaves) must classify once — else each accepted
466
- // seal is counted ×N in terminal gate-round (#753 acceptance A).
441
+ // seal is counted ×N in gate-cycle statistics (#753 acceptance A).
467
442
  const classifiedOfficerSessions = new Set<string>();
468
443
  for (const directory of directories) {
469
444
  let names: string[];
@@ -492,12 +467,14 @@ export async function readAnalystGateCyclesFromAuditorRoles(
492
467
  if (classifiedOfficerSessions.has(sessionPath)) continue;
493
468
  classifiedOfficerSessions.add(sessionPath);
494
469
  }
495
- const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
470
+ const officer = pointer?.officer === "inspector" || pointer?.officer === "notary"
471
+ ? pointer.officer : undefined;
472
+ const classified = await classifyAuditorVolume(sessionPath, officer);
496
473
  for (const volume of classified) {
497
474
  if (
498
475
  options.parentSessionFile !== undefined &&
499
476
  volume.parentSessionFile !== undefined &&
500
- volume.parentSessionFile !== options.parentSessionFile
477
+ !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))
501
478
  ) {
502
479
  continue;
503
480
  }
@@ -21,9 +21,15 @@ import {
21
21
  physicalPathIdentity,
22
22
  resolveActivationLedgerHome,
23
23
  } from "./activation-ledger-topology.ts";
24
- import { listBookRunDirectories } from "./role-run-placement.ts";
24
+ import {
25
+ listBookRunDirectories,
26
+ parseRunLeaf,
27
+ sessionFileOf,
28
+ } from "./role-run-placement.ts";
29
+ import { sitianRunVolumeDirectory } from "./sitian-appender.ts";
25
30
  import { autopsyWriterLock, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
26
31
  import { readRunTicketNumber } from "./run-ticket-number.ts";
32
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
27
33
  import {
28
34
  extractSessionModelSequence,
29
35
  extractSessionTimestampSpan,
@@ -124,14 +130,6 @@ async function classifyGhostCandidate(input: {
124
130
  };
125
131
  }
126
132
 
127
- function parseRunDirectoryName(
128
- name: string,
129
- ): { runId: string; role: string } | undefined {
130
- const at = name.lastIndexOf("@");
131
- if (at <= 0 || at === name.length - 1) return undefined;
132
- return { runId: name.slice(0, at), role: name.slice(at + 1) };
133
- }
134
-
135
133
  /**
136
134
  * Invocation scope faces used for issue 圈定 (C4 / #399).
137
135
  * projectRoot is retained for narrow path match and conflict facts;
@@ -222,12 +220,14 @@ async function resolveSessionFile(
222
220
  && typeof parsed.sessionFile === "string"
223
221
  && parsed.sessionFile.trim() !== ""
224
222
  ) {
225
- return parsed.sessionFile;
223
+ return typeof parsed.runDirectory === "string"
224
+ ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory) as string
225
+ : parsed.sessionFile;
226
226
  }
227
227
  } catch (error) {
228
228
  if (!isMissingPathError(error)) throw error;
229
229
  }
230
- return join(runDirectory, "session", "session.jsonl");
230
+ return sessionFileOf(runDirectory);
231
231
  }
232
232
 
233
233
  /**
@@ -579,9 +579,9 @@ async function classifyScopedRun(input: {
579
579
  // nested JSONL is page-local unreadable — never silently under-count rounds.
580
580
  let gateCycles: readonly AnalystGateCycleRound[];
581
581
  try {
582
- const parentSessionFile = join(input.runDirectory, "session", "session.jsonl");
582
+ const parentSessionFile = sessionFileOf(input.runDirectory);
583
583
  gateCycles = await readAnalystGateCyclesFromAuditorRoles(
584
- join(input.runDirectory, "session", "auditor-roles"),
584
+ sitianRunVolumeDirectory(input.runDirectory, "auditor-roles"),
585
585
  { parentSessionFile },
586
586
  );
587
587
  } catch (error) {
@@ -697,7 +697,7 @@ export async function scanAnalystIssueRuns(input: {
697
697
 
698
698
  for (const runDirectory of runDirectories) {
699
699
  const runName = basename(runDirectory);
700
- const parsed = parseRunDirectoryName(runName);
700
+ const parsed = parseRunLeaf(runName);
701
701
  if (parsed === undefined) continue;
702
702
 
703
703
  let scopeFields: InvocationScopeFields | undefined;
@@ -31,6 +31,7 @@ import {
31
31
  } from "./archivist-record-topology.ts";
32
32
  import type { HostRecordSession, RecordSessionHost } from "./host-contracts.ts";
33
33
  import { piRecordSessionHost } from "./pi/record-session-host.ts";
34
+ import { sitianVolumeDirectory } from "./sitian-appender.ts";
34
35
  export {
35
36
  DIRECT_OFFICER_RUN_POINTER_KIND,
36
37
  bookDirectOfficerRunPointer,
@@ -47,7 +48,7 @@ const CURRENT_SESSION_LEDGER = "current-session.json";
47
48
  type CurrentSessionRecord = { readonly sessionFile: string };
48
49
 
49
50
  function readCurrentSession(sessionDir: string): string {
50
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
51
+ const ledger = currentSessionLedgerPath(sessionDir);
51
52
  try {
52
53
  const value: unknown = JSON.parse(readFileSync(ledger, "utf8"));
53
54
  if (
@@ -68,7 +69,7 @@ function readCurrentSession(sessionDir: string): string {
68
69
  }
69
70
 
70
71
  function writeCurrentSession(sessionDir: string, sessionFile: string): void {
71
- const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
72
+ const ledger = currentSessionLedgerPath(sessionDir);
72
73
  try {
73
74
  writeFileSync(ledger, `${JSON.stringify({ sessionFile })}\n`, { flag: "wx" });
74
75
  } catch (error) {
@@ -420,7 +421,7 @@ export function createRecordSessionOpen(
420
421
  // Ordinary kinds: durable parent's file is the sole nesting authority. A divergent
421
422
  // SessionManager directory must not create a second placement route.
422
423
  // Do not restore generic bookDir/<kind> fallback for foreign/unrooted parents.
423
- sessionDir = join(dirname(parentResolved), options.kind);
424
+ sessionDir = sitianVolumeDirectory(dirname(parentResolved), options.kind);
424
425
  parentSession = parentFile;
425
426
  mayResumeSameNest = options.kind === WORKER_SUBMISSION_GATE_KIND;
426
427
  }
@@ -1,7 +1,13 @@
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.ts";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { isRecord, errorText } from "./unknown-value.ts";
5
+
6
+ import { homeFromRunDirectory } from "./activation-ledger-topology.ts";
7
+ import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
8
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
9
+ import { parseRunLeaf, runDirectoryOfSessionFile, runDirectoryFromSessionDirectory } from "./role-run-placement.ts";
10
+ import { sitianVolumeDirectory } from "./sitian-appender.ts";
5
11
 
6
12
  /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
7
13
  export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
@@ -21,6 +27,13 @@ export type DirectOfficerRunPointer = {
21
27
  readonly submissionToolCallId?: string;
22
28
  };
23
29
 
30
+ function directOfficerRunPointerFile(
31
+ parentSessionFile: string,
32
+ officer: DirectOfficerRunPointer["officer"],
33
+ ): string {
34
+ return join(sitianVolumeDirectory(dirname(parentSessionFile), "auditor-roles"), `${officer}.pointer.json`);
35
+ }
36
+
24
37
  /**
25
38
  * Book a typed pointer under parent session/auditor-roles (same nest owner as
26
39
  * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
@@ -28,7 +41,7 @@ export type DirectOfficerRunPointer = {
28
41
  *
29
42
  * Stable leaf per officer under one parent (#753 gate-round accounting):
30
43
  * same-parent re-summons upsert the same pointer instead of minting N files that
31
- * each re-scan the full officer session and multiply terminal gate-round counts.
44
+ * each re-scan the full officer session and multiply gate-cycle counts.
32
45
  */
33
46
  export function bookDirectOfficerRunPointer(options: {
34
47
  readonly parentSessionFile: string;
@@ -37,8 +50,8 @@ export function bookDirectOfficerRunPointer(options: {
37
50
  readonly runDirectory?: string;
38
51
  readonly submissionToolCallId?: string;
39
52
  }): DirectOfficerRunPointer {
40
- const nest = join(dirname(options.parentSessionFile), "auditor-roles");
41
- mkdirSync(nest, { recursive: true });
53
+ const pointerFile = directOfficerRunPointerFile(options.parentSessionFile, options.officer);
54
+ mkdirSync(dirname(pointerFile), { recursive: true });
42
55
  const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
43
56
  const pointer: DirectOfficerRunPointer = {
44
57
  version: 1,
@@ -51,46 +64,69 @@ export function bookDirectOfficerRunPointer(options: {
51
64
  ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
52
65
  };
53
66
  writeFileSync(
54
- join(nest, `${options.officer}.pointer.json`),
67
+ pointerFile,
55
68
  `${JSON.stringify(pointer)}\n`,
56
69
  "utf8",
57
70
  );
58
71
  return pointer;
59
72
  }
60
73
 
61
- /**
62
- * Read the one officer pointer. Missing or unreadable is not a pass:
63
- * the caller re-runs the gate.
64
- */
74
+ /** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
75
+ export async function resolveOfficerSessionFromPointerFile(pointerPath: string): Promise<{
76
+ sessionFile: string;
77
+ officer?: DirectOfficerRunPointer["officer"];
78
+ runDirectory?: string;
79
+ submissionToolCallId?: string;
80
+ }> {
81
+ let raw: unknown;
82
+ try {
83
+ raw = JSON.parse(await readFile(pointerPath, "utf8"));
84
+ } catch (error) {
85
+ throw new Error(`direct officer run pointer unreadable in ${pointerPath}: ${errorText(error)}`, { cause: error });
86
+ }
87
+ if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
88
+ throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
89
+ }
90
+ if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
91
+ throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
92
+ }
93
+ const officer = raw.officer === "inspector" || raw.officer === "notary"
94
+ || raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
95
+ let runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
96
+ ? raw.runDirectory : undefined;
97
+ let sessionFile = raw.sessionFile;
98
+ const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
99
+ const leaf = parseRunLeaf(basename(recordedRunDirectory));
100
+ const parentRunDirectory = runDirectoryFromSessionDirectory(dirname(dirname(pointerPath)));
101
+ const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
102
+ if (leaf !== undefined && parentIdentity !== undefined) {
103
+ const currentRunDirectory = await findRunDirectoryById(
104
+ homeFromRunDirectory(parentRunDirectory), leaf.runId, parentIdentity.bookKey, leaf.role,
105
+ );
106
+ if (currentRunDirectory !== undefined) {
107
+ sessionFile = rewriteRunDirectoryPathValue(sessionFile, recordedRunDirectory, currentRunDirectory) as string;
108
+ if (runDirectory !== undefined) runDirectory = currentRunDirectory;
109
+ }
110
+ }
111
+ const submissionToolCallId = typeof raw.submissionToolCallId === "string"
112
+ && raw.submissionToolCallId.trim() !== "" ? raw.submissionToolCallId : undefined;
113
+ return {
114
+ sessionFile,
115
+ ...(officer === undefined ? {} : { officer }),
116
+ ...(runDirectory === undefined ? {} : { runDirectory }),
117
+ ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
118
+ };
119
+ }
120
+
121
+ /** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
65
122
  export async function readDirectOfficerRunPointer(
66
123
  parentSessionFile: string,
67
124
  officer: DirectOfficerRunPointer["officer"],
68
125
  ): Promise<DirectOfficerRunPointer | undefined> {
69
126
  try {
70
- const raw: unknown = JSON.parse(await readFile(
71
- join(dirname(parentSessionFile), "auditor-roles", `${officer}.pointer.json`),
72
- "utf8",
73
- ));
74
- if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
75
- return undefined;
76
- }
77
- if (raw.officer !== officer) return undefined;
78
- if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") return undefined;
79
- const runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
80
- ? raw.runDirectory
81
- : undefined;
82
- const submissionToolCallId = typeof raw.submissionToolCallId === "string"
83
- && raw.submissionToolCallId.trim() !== ""
84
- ? raw.submissionToolCallId
85
- : undefined;
86
- return {
87
- version: 1,
88
- kind: DIRECT_OFFICER_RUN_POINTER_KIND,
89
- officer,
90
- sessionFile: raw.sessionFile,
91
- ...(runDirectory === undefined ? {} : { runDirectory }),
92
- ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
93
- };
127
+ const pointer = await resolveOfficerSessionFromPointerFile(directOfficerRunPointerFile(parentSessionFile, officer));
128
+ if (pointer.officer !== officer) return undefined;
129
+ return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
94
130
  } catch {
95
131
  return undefined;
96
132
  }
@@ -7,17 +7,19 @@
7
7
  * ledger home own confinement via ensureRealDirectoryTree (ADR 0038).
8
8
  */
9
9
  import { randomUUID } from "node:crypto";
10
+ import type { Mode } from "node:fs";
10
11
  import { rename, rm, writeFile } from "node:fs/promises";
11
12
  import { dirname, join } from "node:path";
12
13
 
13
14
  export async function writeFileAtomically(
14
15
  destination: string,
15
16
  contents: string | Uint8Array,
17
+ options?: { readonly mode?: Mode },
16
18
  ): Promise<void> {
17
19
  const parent = dirname(destination);
18
20
  const temporary = join(parent, `.atomic-write-${randomUUID()}.tmp`);
19
21
  try {
20
- await writeFile(temporary, contents);
22
+ await writeFile(temporary, contents, options);
21
23
  await rename(temporary, destination);
22
24
  } catch (error) {
23
25
  await rm(temporary, { force: true }).catch(() => undefined);
@@ -1,8 +1,12 @@
1
- import { dirname, join, resolve } from "node:path";
1
+ import { join, resolve } from "node:path";
2
2
 
3
3
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import type { HostContext } from "./host-contracts.ts";
5
- import { roleRunArtifactsDirectory } from "./role-run-placement.ts";
5
+ import {
6
+ roleRunArtifactsDirectory,
7
+ runDirectoryOfSessionFile,
8
+ sessionFileOf,
9
+ } from "./role-run-placement.ts";
6
10
  import { Type } from "typebox";
7
11
 
8
12
  export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
@@ -20,7 +24,7 @@ export function auditorRunDirectory(context: ExtensionContext | HostContext): st
20
24
  const sessionFile = context.sessionManager?.getSessionFile?.();
21
25
  if (sessionFile === undefined) return undefined;
22
26
  // Sole layout: <run>/session/session.jsonl → climb two levels to the run directory.
23
- return resolve(dirname(dirname(sessionFile)));
27
+ return resolve(runDirectoryOfSessionFile(sessionFile));
24
28
  }
25
29
 
26
30
  /** The one shared, run-bound dossier locator exposed to every auditor seat. */
@@ -44,7 +48,7 @@ export function createAuditorDossierTool(
44
48
  const details: AuditorDossierLocation = {
45
49
  runDirectory,
46
50
  admittedRequest: join(runDirectory, "admitted-request.json"),
47
- parentSessionCandidate: join(runDirectory, "session", "session.jsonl"),
51
+ parentSessionCandidate: sessionFileOf(runDirectory),
48
52
  attachments: join(runDirectory, "attachments"),
49
53
  artifacts: roleRunArtifactsDirectory(runDirectory),
50
54
  };
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
3
 
4
+ import { parseRunLeaf } from "./role-run-placement.ts";
4
5
  import { isRecord } from "./unknown-value.ts";
5
6
 
6
7
  import {
@@ -109,7 +110,7 @@ export function loadAuditorReferenceMaterialsFromSubjectInput(raw?: string): Pro
109
110
  }
110
111
 
111
112
  function subjectFromSourceDirectory(sourceRunDirectory: string): AuditorSoulRole | undefined {
112
- const role = basename(sourceRunDirectory).split("@")[1];
113
+ const role = parseRunLeaf(basename(sourceRunDirectory))?.role;
113
114
  return isAuditorSoulRole(role) ? role : undefined;
114
115
  }
115
116