@akagilnc/pi-workflow-roles 0.1.5483 → 0.1.5571

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 (112) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +6312 -6376
  4. package/dist/analyst-gate-cycles-read.js +28 -78
  5. package/dist/analyst-ledger.js +26 -42
  6. package/dist/analyst-metric-families/acceptance-success-rework.js +25 -1
  7. package/dist/analyst-metric-families/round-timeline.js +2 -5
  8. package/dist/archivist-record-entry.js +4 -3
  9. package/dist/archivist-record-pointer.js +71 -36
  10. package/dist/atomic-write.js +2 -2
  11. package/dist/auditor-dossier-tool.js +11 -6
  12. package/dist/auditor-soul.js +5 -12
  13. package/dist/collector-config.js +6 -5
  14. package/dist/doctor-evidence.js +8 -4
  15. package/dist/engine-detour-usage.js +8 -22
  16. package/dist/headless-host/production-host.js +5969 -6011
  17. package/dist/host-descriptions.js +0 -4
  18. package/dist/host-session-record.js +266 -267
  19. package/dist/ledger-session-read.js +5 -0
  20. package/dist/migrate-book-topology.js +1369 -722
  21. package/dist/navigator-public-session.js +13 -17
  22. package/dist/notary-source-run.js +8 -16
  23. package/dist/packaged-role-registry.js +5 -56
  24. package/dist/pi/durable-principal.js +2 -3
  25. package/dist/pi/role-turn-host.js +79 -15
  26. package/dist/public-cli/auto-resume.js +8 -47
  27. package/dist/public-cli/instruction-seat-run.js +9 -7
  28. package/dist/public-cli/invocation.js +67 -100
  29. package/dist/public-cli/main.js +2689 -2532
  30. package/dist/public-cli/post-admission.js +96 -55
  31. package/dist/public-cli/run-lifecycle.js +44 -100
  32. package/dist/public-cli/settlement.js +184 -429
  33. package/dist/public-cli/terminal.js +0 -12
  34. package/dist/role-run-placement.js +53 -16
  35. package/dist/role-run-relocation.js +111 -52
  36. package/dist/run-dossier-files.js +30 -0
  37. package/dist/run-dossier.js +322 -0
  38. package/dist/run-terminal-artifacts.js +69 -202
  39. package/dist/run-ticket-number.js +49 -7
  40. package/dist/session-assistant-usage.js +9 -18
  41. package/dist/session-identity.js +49 -19
  42. package/dist/sitian-appender.js +29 -18
  43. package/dist/sitian-facade.js +11 -0
  44. package/dist/sitian-reader.js +44 -29
  45. package/dist/submission-gate.js +1 -1
  46. package/dist/submission-ledger.js +4 -4
  47. package/dist/ticket-provenance-contracts.js +2 -8
  48. package/dist/ticket-provenance.js +25 -9
  49. package/package.json +1 -1
  50. package/scripts/build-package.mjs +2 -0
  51. package/src/acp-host/production-host.ts +1 -1
  52. package/src/acp-host/role-turn-host.ts +11 -4
  53. package/src/analyst-gate-cycles-read.ts +31 -88
  54. package/src/analyst-ledger.ts +36 -51
  55. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  56. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  57. package/src/analyst-metric-families/round-timeline.ts +2 -5
  58. package/src/analyst-page.ts +3 -3
  59. package/src/archivist-record-entry.ts +6 -5
  60. package/src/archivist-record-pointer.ts +92 -42
  61. package/src/atomic-write.ts +3 -1
  62. package/src/auditor-dossier-tool.ts +14 -8
  63. package/src/auditor-soul.ts +5 -10
  64. package/src/book-topology-migration-placement.ts +16 -3
  65. package/src/book-topology-record-class-migrators.ts +4 -2
  66. package/src/book-topology-runs-migrator.ts +6 -3
  67. package/src/collector-config.ts +5 -3
  68. package/src/doctor-contracts.ts +1 -1
  69. package/src/doctor-evidence.ts +7 -3
  70. package/src/engine-detour-tool.ts +1 -1
  71. package/src/engine-detour-usage.ts +12 -25
  72. package/src/external-host-turn-loop.ts +25 -0
  73. package/src/factory-board.ts +16 -81
  74. package/src/headless-host/production-host.ts +1 -4
  75. package/src/headless-host/role-turn-host.ts +56 -21
  76. package/src/host-descriptions.ts +0 -6
  77. package/src/host-session-record.ts +224 -140
  78. package/src/ledger-session-read.ts +6 -0
  79. package/src/navigator-public-session.ts +13 -18
  80. package/src/navigator-work-context.ts +3 -1
  81. package/src/notary-role.ts +2 -1
  82. package/src/notary-source-run.ts +8 -18
  83. package/src/packaged-role-registry.ts +1 -65
  84. package/src/pi/adapter.ts +50 -7
  85. package/src/pi/durable-principal.ts +2 -4
  86. package/src/pi/role-turn-host.ts +85 -14
  87. package/src/public-cli/auto-resume.ts +7 -43
  88. package/src/public-cli/instruction-seat-run.ts +13 -6
  89. package/src/public-cli/invocation.ts +90 -144
  90. package/src/public-cli/post-admission.ts +111 -72
  91. package/src/public-cli/run-lifecycle.ts +47 -115
  92. package/src/public-cli/settlement.ts +221 -589
  93. package/src/public-cli/terminal.ts +1 -61
  94. package/src/role-envelope.ts +4 -2
  95. package/src/role-run-placement.ts +65 -17
  96. package/src/role-run-relocation.ts +110 -70
  97. package/src/role-runtime.ts +46 -69
  98. package/src/run-dossier-files.ts +33 -0
  99. package/src/run-dossier.ts +357 -0
  100. package/src/run-terminal-artifacts.ts +84 -240
  101. package/src/run-ticket-number.ts +82 -14
  102. package/src/session-assistant-usage.ts +8 -20
  103. package/src/session-identity.ts +52 -21
  104. package/src/sitian-appender.ts +33 -26
  105. package/src/sitian-facade.ts +22 -0
  106. package/src/sitian-reader.ts +62 -32
  107. package/src/submission-gate.ts +1 -1
  108. package/src/submission-ledger.ts +4 -4
  109. package/src/ticket-provenance-contracts.ts +3 -6
  110. package/src/ticket-provenance.ts +23 -11
  111. package/src/ticket-snapshot.ts +3 -2
  112. package/src/ticket-trajectory.ts +95 -109
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Sole nested-volume reader for gate-cycle facts under session/auditor-roles/.
2
+ * Sole reader for gate-cycle facts of the officers pointed at from current.json `officers`.
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
11
9
  * the current English officer identity (inspector | notary).
12
10
  *
13
- * Missing auditor-roles directory (ENOENT only) → empty rounds (lawful zero).
14
- * Path present but not a directory (ENOTDIR) and discovered nested JSONL that
15
- * fails canonical read/parse must fail loudly (never silently under-count).
11
+ * Absent `officers` section → empty rounds (lawful zero). A pointer whose
12
+ * officer session fails canonical read/parse must fail loudly (never silently under-count).
16
13
  * An accepted gate terminating receipt (isError:false pair on dispatch/officer
17
14
  * tool) whose required typed facts are unusable — status, dispatch officer, or
18
15
  * first/last span missing/unknown/unparseable/inverted — also fails loudly via
@@ -25,11 +22,12 @@
25
22
  * `ak_auditor_parent_attempt_binding.parent.attemptEntryId` — never seat/time guessing.
26
23
  * An orphan accepted dispatch must not consume a later same-seat direct officer.
27
24
  */
28
- import { readdir } from "node:fs/promises";
29
- import { join } from "node:path";
30
25
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, } from "./compliance-transport.js";
31
26
  import { extractSessionTimestampSpan, intervalRowsAroundAnchor, readLedgerSessionJsonl, } from "./ledger-session-read.js";
32
- import { isRecord, errorText, isEnoent } from "./unknown-value.js";
27
+ import { isRecord, isEnoent } from "./unknown-value.js";
28
+ import { retainedRunPathsMatch } from "./role-run-relocation.js";
29
+ import { runDirectoryOfSessionFile } from "./role-run-placement.js";
30
+ import { readOfficerPointers } from "./archivist-record-pointer.js";
33
31
  const DISPATCH_TOOLS = new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
34
32
  /** Officer terminating tool → current English officer identity. */
35
33
  const OFFICER_TOOL_TO_FACE = {
@@ -309,81 +307,33 @@ function pairGateRounds(volumes) {
309
307
  .sort((a, b) => a.officerStartedAt.localeCompare(b.officerStartedAt))
310
308
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
311
309
  }
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
310
  /**
334
- * Read and pair gate-cycle rounds from one or more auditor-roles directories.
335
- * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
336
- * files that name the independent officer session 正本.
337
- * ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
338
- * (failure honesty — damaged topology must not wash to zero rounds).
311
+ * Read and pair gate-cycle rounds from the officer pointers booked in one
312
+ * run's current.json `officers` section; each pointer names the independent
313
+ * officer session 正本. Absent section → [] (lawful zero). A damaged pointer or
314
+ * officer session propagates — damaged topology must not wash to zero rounds.
339
315
  *
340
316
  * Pass `parentSessionFile` to keep only rounds whose attempt binding names that parent.
341
317
  */
342
- export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirectory, options = {}) {
343
- const directories = typeof auditorRolesDirectory === "string"
344
- ? [auditorRolesDirectory]
345
- : auditorRolesDirectory;
318
+ export async function readAnalystGateCyclesFromOfficers(parentRunDirectory, options = {}) {
346
319
  const volumes = [];
347
- // Same officer session pointed at N times (historical multi-mint pointers, or
348
- // one parent booking many leaves) must classify once — else each accepted
349
- // seal is counted ×N in terminal gate-round (#753 acceptance A).
320
+ // Same officer session pointed at N times must classify once — else each
321
+ // accepted seal is counted ×N in gate-cycle statistics (#753 acceptance A).
350
322
  const classifiedOfficerSessions = new Set();
351
- for (const directory of directories) {
352
- let names;
353
- try {
354
- const entries = await readdir(directory, { withFileTypes: true });
355
- names = entries
356
- .filter((e) => e.isFile()
357
- && (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")))
358
- .map((e) => e.name)
359
- .sort();
360
- }
361
- catch (error) {
362
- if (isEnoent(error))
323
+ for (const pointer of await readOfficerPointers(parentRunDirectory)) {
324
+ if (classifiedOfficerSessions.has(pointer.sessionFile))
325
+ continue;
326
+ classifiedOfficerSessions.add(pointer.sessionFile);
327
+ const officer = pointer.officer === "inspector" || pointer.officer === "notary"
328
+ ? pointer.officer : undefined;
329
+ const classified = await classifyAuditorVolume(pointer.sessionFile, officer);
330
+ for (const volume of classified) {
331
+ if (options.parentSessionFile !== undefined &&
332
+ volume.parentSessionFile !== undefined &&
333
+ !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))) {
363
334
  continue;
364
- throw error;
365
- }
366
- for (const name of names) {
367
- const path = join(directory, name);
368
- const fromPointer = name.endsWith(".pointer.json");
369
- const pointer = fromPointer
370
- ? await resolveOfficerSessionFromPointerFile(path)
371
- : undefined;
372
- const sessionPath = pointer?.sessionFile ?? path;
373
- if (fromPointer) {
374
- if (classifiedOfficerSessions.has(sessionPath))
375
- continue;
376
- classifiedOfficerSessions.add(sessionPath);
377
- }
378
- const classified = await classifyAuditorVolume(sessionPath, pointer?.officer);
379
- for (const volume of classified) {
380
- if (options.parentSessionFile !== undefined &&
381
- volume.parentSessionFile !== undefined &&
382
- volume.parentSessionFile !== options.parentSessionFile) {
383
- continue;
384
- }
385
- volumes.push(volume);
386
335
  }
336
+ volumes.push(volume);
387
337
  }
388
338
  }
389
339
  return pairGateRounds(volumes);
@@ -12,21 +12,23 @@
12
12
  * A2: classifyScopedRun retains typed per-run facts (frame span, tool intervals,
13
13
  * terminal face) for metric-family modules — no longer discarded after checks.
14
14
  */
15
+ import { readSectionSync } from "./run-dossier.js";
15
16
  import { readdir, readFile } from "node:fs/promises";
16
17
  import { basename, join } from "node:path";
17
18
  import { isSyntheticAnalystBookKey, resolveAnalystBookKey } from "./analyst-book-key.js";
18
19
  import { pathContainedIn, physicalPathIdentity, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
19
- import { listBookRunDirectories } from "./role-run-placement.js";
20
+ import { listBookRunDirectories, parseRunLeaf, sessionFileOf, } from "./role-run-placement.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
- import { readRunTerminalArtifact, } from "./run-terminal-artifacts.js";
24
- import { readAnalystGateCyclesFromAuditorRoles, } from "./analyst-gate-cycles-read.js";
25
+ import { readRunTerminal, } from "./run-terminal-artifacts.js";
26
+ import { readAnalystGateCyclesFromOfficers, } from "./analyst-gate-cycles-read.js";
25
27
  import { isRecord, errorText, isMissingPathError } from "./unknown-value.js";
26
28
  /** Existing public-cli run-state live phases — not a new state machine. */
27
29
  const LIVE_RUN_STATES = new Set(["admitted", "running", "resumable"]);
28
30
  /**
29
- * Read lifecycle state from the existing run-state.json face.
31
+ * Read lifecycle state from the state.jsonl run-state fact row.
30
32
  * Used only to distinguish live in-flight runs from terminal no-receipt.
31
33
  */
32
34
  async function readExistingRunLifecycleState(runDirectory) {
@@ -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 });
@@ -83,17 +79,8 @@ async function listLedgerBookNames(booksRoot) {
83
79
  }
84
80
  }
85
81
  async function readInvocationScopeFields(runDirectory) {
86
- let raw;
87
- try {
88
- raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
89
- }
90
- catch (error) {
91
- if (isMissingPathError(error))
92
- return undefined;
93
- throw error;
94
- }
95
- const parsed = JSON.parse(raw);
96
- if (!isRecord(parsed))
82
+ const parsed = readSectionSync(runDirectory, "invocation");
83
+ if (parsed === undefined)
97
84
  return undefined;
98
85
  if (typeof parsed.projectRoot !== "string" || parsed.projectRoot.trim() === "") {
99
86
  return undefined;
@@ -130,20 +117,13 @@ function decideIssueScope(input) {
130
117
  }
131
118
  async function resolveSessionFile(runDirectory) {
132
119
  // Prefer invocation.sessionFile when present; fall back to S-family principal.
133
- try {
134
- const raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
135
- const parsed = JSON.parse(raw);
136
- if (isRecord(parsed)
137
- && typeof parsed.sessionFile === "string"
138
- && parsed.sessionFile.trim() !== "") {
139
- return parsed.sessionFile;
140
- }
141
- }
142
- catch (error) {
143
- if (!isMissingPathError(error))
144
- throw error;
120
+ const parsed = readSectionSync(runDirectory, "invocation");
121
+ if (parsed !== undefined && typeof parsed.sessionFile === "string" && parsed.sessionFile.trim() !== "") {
122
+ return typeof parsed.runDirectory === "string"
123
+ ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory)
124
+ : parsed.sessionFile;
145
125
  }
146
- return join(runDirectory, "session", "session.jsonl");
126
+ return sessionFileOf(runDirectory);
147
127
  }
148
128
  /**
149
129
  * Attribute session rows to one run.
@@ -333,10 +313,14 @@ async function classifyScopedRun(input) {
333
313
  ghostLeg = ghost.leg;
334
314
  }
335
315
  try {
336
- const artifact = await readRunTerminalArtifact(input.runDirectory);
316
+ const read = readRunTerminal(input.runDirectory);
317
+ // A no_receipt terminal is the explicit form of "no accepted receipt".
318
+ const artifact = read.status === "present" && read.face === "no_receipt"
319
+ ? { status: "absent" }
320
+ : read;
337
321
  if (artifact.status === "unreadable") {
338
322
  missingSources.push("terminal-artifact");
339
- reasons.push(`${artifact.file}: ${artifact.reason}`);
323
+ reasons.push(`terminal: ${artifact.reason}`);
340
324
  }
341
325
  else if (artifact.status === "absent") {
342
326
  if (lifecycle === "admitted" || lifecycle === "running") {
@@ -351,10 +335,10 @@ async function classifyScopedRun(input) {
351
335
  terminal = { status: "absent" };
352
336
  }
353
337
  else {
354
- // role already required nonblank by readRunTerminalArtifact (single owner).
338
+ // role already required nonblank by readRunTerminal (single owner).
355
339
  terminal = {
356
340
  status: "present",
357
- file: artifact.file,
341
+ face: artifact.face,
358
342
  body: artifact.body,
359
343
  role: artifact.body.role,
360
344
  };
@@ -389,13 +373,13 @@ async function classifyScopedRun(input) {
389
373
  if (frameSpan === undefined || toolIntervals === undefined || terminal === undefined) {
390
374
  throw new Error(`classifyScopedRun internal invariant: missing retained facts for ${input.runId}`);
391
375
  }
392
- // Nested auditor-roles gate pairs stay inside the sole scan (families must
376
+ // Officer gate pairs stay inside the sole scan (families must
393
377
  // not readdir this tree again). Missing directory → []. Damaged discovered
394
378
  // nested JSONL is page-local unreadable — never silently under-count rounds.
395
379
  let gateCycles;
396
380
  try {
397
- const parentSessionFile = join(input.runDirectory, "session", "session.jsonl");
398
- gateCycles = await readAnalystGateCyclesFromAuditorRoles(join(input.runDirectory, "session", "auditor-roles"), { parentSessionFile });
381
+ const parentSessionFile = sessionFileOf(input.runDirectory);
382
+ gateCycles = await readAnalystGateCyclesFromOfficers(input.runDirectory, { parentSessionFile });
399
383
  }
400
384
  catch (error) {
401
385
  return {
@@ -497,7 +481,7 @@ export async function scanAnalystIssueRuns(input) {
497
481
  }
498
482
  for (const runDirectory of runDirectories) {
499
483
  const runName = basename(runDirectory);
500
- const parsed = parseRunDirectoryName(runName);
484
+ const parsed = parseRunLeaf(runName);
501
485
  if (parsed === undefined)
502
486
  continue;
503
487
  let scopeFields;
@@ -33,7 +33,10 @@ const SUCCESS_STATUS = {
33
33
  doctor: new Set(["completed"]),
34
34
  merger: new Set(["completed"]),
35
35
  };
36
- /** Locate collector groups array: receipt.groups or top-level groups. */
36
+ /**
37
+ * Locate collector groups array from the canonical typed receipt faces.
38
+ * Same leaf order as extractStatus: body / receipt / decisiveFacts / payloads.
39
+ */
37
40
  function findCollectorGroups(body) {
38
41
  if (Array.isArray(body.groups))
39
42
  return body.groups;
@@ -45,6 +48,16 @@ function findCollectorGroups(body) {
45
48
  const facts = outcome.decisiveFacts;
46
49
  if (isRecord(facts) && Array.isArray(facts.groups))
47
50
  return facts.groups;
51
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
52
+ // terminal reader rehydrates them via attemptHistoryIdentity. Groups are the
53
+ // collector leaf on that receipt — never invent a second discriminator.
54
+ if (Array.isArray(outcome.payloads)) {
55
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
56
+ const payload = outcome.payloads[index];
57
+ if (isRecord(payload) && Array.isArray(payload.groups))
58
+ return payload.groups;
59
+ }
60
+ }
48
61
  }
49
62
  return undefined;
50
63
  }
@@ -54,6 +67,17 @@ export function extractStatus(body) {
54
67
  if (isRecord(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
55
68
  return outcome.status;
56
69
  }
70
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
71
+ // terminal reader rehydrates them via attemptHistoryIdentity. Status is the role's
72
+ // own leaf on that receipt — never invent a second mapping.
73
+ if (isRecord(outcome) && Array.isArray(outcome.payloads)) {
74
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
75
+ const payload = outcome.payloads[index];
76
+ if (isRecord(payload) && typeof payload.status === "string" && payload.status.trim() !== "") {
77
+ return payload.status;
78
+ }
79
+ }
80
+ }
57
81
  const receipt = body.receipt;
58
82
  if (isRecord(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
59
83
  return receipt.status;
@@ -15,13 +15,10 @@ function projectTerminal(facts) {
15
15
  if (facts.terminal.status === "absent") {
16
16
  return { kind: "death", channel: "no-receipt" };
17
17
  }
18
- if (facts.terminal.file === "error.json") {
18
+ if (facts.terminal.face === "error") {
19
19
  return { kind: "death", channel: "error" };
20
20
  }
21
- if (facts.terminal.file === "audit-incomplete.json") {
22
- return { kind: "death", channel: "audit-incomplete" };
23
- }
24
- // report.json — receipt face; classCount only when producer wrote a number.
21
+ // report face — receipt; classCount only when producer wrote a number.
25
22
  const status = extractStatus(facts.terminal.body);
26
23
  const classCount = readClassCount(facts.terminal.body);
27
24
  const receiptStatus = status ?? "unparsed";
@@ -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,21 +1,26 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
1
+ import { basename, join } from "node:path";
4
2
  import { isRecord } from "./unknown-value.js";
3
+ import { latestOfficerPointersFromRecords, RUN_HISTORY_FILE } from "./run-dossier.js";
4
+ import { OFFICER_POINTER_RECORD_KIND } from "./run-dossier-files.js";
5
+ import { readSitianRecords, reportRunRecord } from "./sitian-facade.js";
6
+ import { homeFromRunDirectory } from "./activation-ledger-topology.js";
7
+ import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
8
+ import { rewriteRunDirectoryPathValue } from "./role-run-relocation.js";
9
+ import { parseRunLeaf, runDirectoryOfSessionFile } from "./role-run-placement.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
+ /** History record kind of a booked officer pointer (appender routes it to history.jsonl). */
13
+ export { OFFICER_POINTER_RECORD_KIND };
7
14
  /**
8
- * Book a typed pointer under parent session/auditor-roles (same nest owner as
9
- * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
10
- * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
15
+ * Book a typed pointer as one history record of the parent run; the settlement
16
+ * seam projects the latest per officer into current.json `officers`.
17
+ * Never fabricates user/assistant/toolResult rows (#675).
11
18
  *
12
- * Stable leaf per officer under one parent (#753 gate-round accounting):
13
- * 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.
19
+ * Latest record wins per officer under one parent (#753 gate-round accounting):
20
+ * a same-parent re-summons supersedes the earlier pointer instead of adding a
21
+ * second one that re-scans the full officer session and multiplies gate-cycle counts.
15
22
  */
16
23
  export function bookDirectOfficerRunPointer(options) {
17
- const nest = join(dirname(options.parentSessionFile), "auditor-roles");
18
- mkdirSync(nest, { recursive: true });
19
24
  const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
20
25
  const pointer = {
21
26
  version: 1,
@@ -27,38 +32,68 @@ export function bookDirectOfficerRunPointer(options) {
27
32
  : {}),
28
33
  ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
29
34
  };
30
- writeFileSync(join(nest, `${options.officer}.pointer.json`), `${JSON.stringify(pointer)}\n`, "utf8");
35
+ reportRunRecord(runDirectoryOfSessionFile(options.parentSessionFile), OFFICER_POINTER_RECORD_KIND, pointer, "submission-gate");
31
36
  return pointer;
32
37
  }
38
+ /** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
39
+ export async function resolveOfficerPointer(raw, parentRunDirectory) {
40
+ const where = `${parentRunDirectory} officers`;
41
+ if (!isRecord(raw) || raw.kind !== DIRECT_OFFICER_RUN_POINTER_KIND || raw.version !== 1) {
42
+ throw new Error(`direct officer run pointer has unknown shape in ${where}`);
43
+ }
44
+ if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
45
+ throw new Error(`direct officer run pointer missing sessionFile in ${where}`);
46
+ }
47
+ const officer = raw.officer === "inspector" || raw.officer === "notary"
48
+ || raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
49
+ let runDirectory = typeof raw.runDirectory === "string" && raw.runDirectory.trim() !== ""
50
+ ? raw.runDirectory : undefined;
51
+ let sessionFile = raw.sessionFile;
52
+ const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
53
+ const leaf = parseRunLeaf(basename(recordedRunDirectory));
54
+ const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
55
+ if (leaf !== undefined && parentIdentity !== undefined) {
56
+ const currentRunDirectory = await findRunDirectoryById(homeFromRunDirectory(parentRunDirectory), leaf.runId, parentIdentity.bookKey, leaf.role);
57
+ if (currentRunDirectory !== undefined) {
58
+ sessionFile = rewriteRunDirectoryPathValue(sessionFile, recordedRunDirectory, currentRunDirectory);
59
+ if (runDirectory !== undefined)
60
+ runDirectory = currentRunDirectory;
61
+ }
62
+ }
63
+ const submissionToolCallId = typeof raw.submissionToolCallId === "string"
64
+ && raw.submissionToolCallId.trim() !== "" ? raw.submissionToolCallId : undefined;
65
+ return {
66
+ sessionFile,
67
+ ...(officer === undefined ? {} : { officer }),
68
+ ...(runDirectory === undefined ? {} : { runDirectory }),
69
+ ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
70
+ };
71
+ }
33
72
  /**
34
- * Read the one officer pointer. Missing or unreadable is not a pass:
35
- * the caller re-runs the gate.
73
+ * The latest pointer booked per officer on one run, as stored. History absent → empty.
74
+ * Canonical malformed diagnostics are not discarded: a damaged officer source must
75
+ * not wash into lawful zero rounds (#1161 O1).
36
76
  */
77
+ export async function readBookedOfficerPointers(parentRunDirectory) {
78
+ const { records, diagnostics } = await readSitianRecords(join(parentRunDirectory, RUN_HISTORY_FILE));
79
+ if (diagnostics.length > 0) {
80
+ throw new Error(`${RUN_HISTORY_FILE} has ${diagnostics.length} malformed row(s); officer pointers unreadable: ${parentRunDirectory}`);
81
+ }
82
+ return latestOfficerPointersFromRecords(records);
83
+ }
84
+ /** Every pointer booked on one run, in officer-name order. No pointers → []. */
85
+ export async function readOfficerPointers(parentRunDirectory) {
86
+ const officers = await readBookedOfficerPointers(parentRunDirectory);
87
+ return Promise.all(Object.keys(officers).sort().map((name) => resolveOfficerPointer(officers[name], parentRunDirectory)));
88
+ }
89
+ /** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
37
90
  export async function readDirectOfficerRunPointer(parentSessionFile, officer) {
38
91
  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;
42
- }
43
- if (raw.officer !== officer)
44
- return undefined;
45
- if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "")
92
+ const parentRunDirectory = runDirectoryOfSessionFile(parentSessionFile);
93
+ const pointer = await resolveOfficerPointer((await readBookedOfficerPointers(parentRunDirectory))[officer], parentRunDirectory);
94
+ if (pointer.officer !== officer)
46
95
  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
- };
96
+ return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
62
97
  }
63
98
  catch {
64
99
  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 { RUN_HISTORY_FILE, RUN_LOG_FILE, runCurrentPath } from "./run-dossier.js";
3
+ import {
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 {
@@ -23,10 +27,11 @@ function createAuditorDossierTool(runDirectory) {
23
27
  }
24
28
  const details = {
25
29
  runDirectory,
26
- admittedRequest: join(runDirectory, "admitted-request.json"),
27
- parentSessionCandidate: join(runDirectory, "session", "session.jsonl"),
30
+ current: runCurrentPath(runDirectory),
31
+ parentSessionCandidate: sessionFileOf(runDirectory),
28
32
  attachments: join(runDirectory, "attachments"),
29
- artifacts: roleRunArtifactsDirectory(runDirectory)
33
+ log: join(runDirectory, RUN_LOG_FILE),
34
+ history: join(runDirectory, RUN_HISTORY_FILE)
30
35
  };
31
36
  return {
32
37
  content: [{ type: "text", text: JSON.stringify(details) }],
@@ -1,5 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { readPageSync } from "./run-dossier.js";
2
3
  import { basename, join } from "node:path";
4
+ import { parseRunLeaf } from "./role-run-placement.js";
3
5
  import { isRecord } from "./unknown-value.js";
4
6
  import { joinPackageMaterials, readPackageMaterial, } from "./session-opening-materials.js";
5
7
  /**
@@ -84,7 +86,7 @@ export function loadAuditorReferenceMaterialsFromSubjectInput(raw) {
84
86
  return loadAuditorReferenceMaterials(resolveAuditorSubject(raw));
85
87
  }
86
88
  function subjectFromSourceDirectory(sourceRunDirectory) {
87
- const role = basename(sourceRunDirectory).split("@")[1];
89
+ const role = parseRunLeaf(basename(sourceRunDirectory))?.role;
88
90
  return isAuditorSoulRole(role) ? role : undefined;
89
91
  }
90
92
  /**
@@ -92,18 +94,9 @@ function subjectFromSourceDirectory(sourceRunDirectory) {
92
94
  * Explicit subject wins; otherwise the source-run role is the audited object.
93
95
  */
94
96
  export async function readAuditorResumeBinding(runDirectory) {
95
- let raw;
96
- try {
97
- raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
98
- }
99
- catch (error) {
100
- if (error.code === "ENOENT")
101
- return undefined;
102
- throw error;
103
- }
104
- if (!isRecord(raw))
97
+ const record = readPageSync(runDirectory, "admitted");
98
+ if (record === undefined)
105
99
  return undefined;
106
- const record = raw;
107
100
  if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "")
108
101
  return undefined;
109
102
  const subject = isAuditorSoulRole(record.auditorSubject)
@@ -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) {