@akagilnc/pi-workflow-roles 0.1.5504 → 0.1.5575

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 (89) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +5965 -5994
  4. package/dist/analyst-gate-cycles-read.js +23 -55
  5. package/dist/analyst-ledger.js +21 -35
  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-pointer.js +38 -30
  9. package/dist/auditor-dossier-tool.js +4 -3
  10. package/dist/auditor-soul.js +3 -11
  11. package/dist/doctor-evidence.js +3 -3
  12. package/dist/engine-detour-usage.js +6 -14
  13. package/dist/headless-host/production-host.js +5316 -5324
  14. package/dist/host-descriptions.js +0 -4
  15. package/dist/host-session-record.js +266 -267
  16. package/dist/migrate-book-topology.js +1314 -696
  17. package/dist/notary-source-run.js +1 -1
  18. package/dist/packaged-role-registry.js +5 -44
  19. package/dist/pi/role-turn-host.js +53 -3
  20. package/dist/public-cli/auto-resume.js +8 -47
  21. package/dist/public-cli/instruction-seat-run.js +5 -3
  22. package/dist/public-cli/invocation.js +51 -56
  23. package/dist/public-cli/main.js +1916 -1748
  24. package/dist/public-cli/post-admission.js +96 -55
  25. package/dist/public-cli/run-lifecycle.js +27 -53
  26. package/dist/public-cli/settlement.js +157 -302
  27. package/dist/role-run-placement.js +1 -11
  28. package/dist/role-run-relocation.js +72 -50
  29. package/dist/run-dossier-files.js +30 -0
  30. package/dist/run-dossier.js +322 -0
  31. package/dist/run-terminal-artifacts.js +68 -197
  32. package/dist/run-ticket-number.js +16 -3
  33. package/dist/session-identity.js +49 -18
  34. package/dist/sitian-appender.js +13 -16
  35. package/dist/sitian-facade.js +11 -0
  36. package/dist/sitian-reader.js +44 -29
  37. package/dist/submission-gate.js +1 -1
  38. package/dist/ticket-provenance.js +22 -7
  39. package/package.json +1 -1
  40. package/scripts/build-package.mjs +2 -0
  41. package/src/acp-host/production-host.ts +1 -1
  42. package/src/acp-host/role-turn-host.ts +9 -2
  43. package/src/analyst-gate-cycles-read.ts +26 -60
  44. package/src/analyst-ledger.ts +27 -42
  45. package/src/analyst-metric-families/acceptance-success-rework.ts +24 -1
  46. package/src/analyst-metric-families/gate-cycles.ts +1 -1
  47. package/src/analyst-metric-families/round-timeline.ts +2 -5
  48. package/src/analyst-page.ts +3 -3
  49. package/src/archivist-record-entry.ts +2 -2
  50. package/src/archivist-record-pointer.ts +50 -36
  51. package/src/auditor-dossier-tool.ts +7 -5
  52. package/src/auditor-soul.ts +3 -9
  53. package/src/book-topology-migration-placement.ts +16 -3
  54. package/src/book-topology-record-class-migrators.ts +4 -2
  55. package/src/book-topology-runs-migrator.ts +6 -3
  56. package/src/doctor-contracts.ts +1 -1
  57. package/src/doctor-evidence.ts +2 -2
  58. package/src/engine-detour-tool.ts +1 -1
  59. package/src/engine-detour-usage.ts +6 -16
  60. package/src/external-host-turn-loop.ts +25 -0
  61. package/src/headless-host/production-host.ts +1 -4
  62. package/src/headless-host/role-turn-host.ts +54 -20
  63. package/src/host-descriptions.ts +0 -6
  64. package/src/host-session-record.ts +224 -140
  65. package/src/notary-source-run.ts +1 -1
  66. package/src/packaged-role-registry.ts +1 -50
  67. package/src/pi/adapter.ts +50 -7
  68. package/src/pi/role-turn-host.ts +51 -3
  69. package/src/public-cli/auto-resume.ts +7 -43
  70. package/src/public-cli/instruction-seat-run.ts +9 -2
  71. package/src/public-cli/invocation.ts +61 -87
  72. package/src/public-cli/post-admission.ts +111 -72
  73. package/src/public-cli/run-lifecycle.ts +27 -72
  74. package/src/public-cli/settlement.ts +193 -418
  75. package/src/public-cli/terminal.ts +1 -1
  76. package/src/role-run-placement.ts +2 -13
  77. package/src/role-run-relocation.ts +71 -68
  78. package/src/role-runtime.ts +6 -10
  79. package/src/run-dossier-files.ts +33 -0
  80. package/src/run-dossier.ts +357 -0
  81. package/src/run-terminal-artifacts.ts +83 -237
  82. package/src/run-ticket-number.ts +41 -11
  83. package/src/session-identity.ts +52 -20
  84. package/src/sitian-appender.ts +17 -18
  85. package/src/sitian-facade.ts +22 -0
  86. package/src/sitian-reader.ts +62 -32
  87. package/src/submission-gate.ts +1 -1
  88. package/src/ticket-provenance.ts +20 -7
  89. package/src/ticket-trajectory.ts +10 -10
@@ -1,5 +1,5 @@
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
4
  * Consumer: Analyst sole ledger scan (classifyScopedRun). Metric families must
5
5
  * not open a second disk scan — they consume retained facts.
@@ -8,9 +8,8 @@
8
8
  * the current gatekeeper/inspector/notary English face. Projection always uses
9
9
  * the current English officer identity (inspector | notary).
10
10
  *
11
- * Missing auditor-roles directory (ENOENT only) → empty rounds (lawful zero).
12
- * Path present but not a directory (ENOTDIR) and discovered nested JSONL that
13
- * 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).
14
13
  * An accepted gate terminating receipt (isError:false pair on dispatch/officer
15
14
  * tool) whose required typed facts are unusable — status, dispatch officer, or
16
15
  * first/last span missing/unknown/unparseable/inverted — also fails loudly via
@@ -23,14 +22,12 @@
23
22
  * `ak_auditor_parent_attempt_binding.parent.attemptEntryId` — never seat/time guessing.
24
23
  * An orphan accepted dispatch must not consume a later same-seat direct officer.
25
24
  */
26
- import { readdir } from "node:fs/promises";
27
- import { join } from "node:path";
28
25
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE, } from "./compliance-transport.js";
29
26
  import { extractSessionTimestampSpan, intervalRowsAroundAnchor, readLedgerSessionJsonl, } from "./ledger-session-read.js";
30
27
  import { isRecord, isEnoent } from "./unknown-value.js";
31
28
  import { retainedRunPathsMatch } from "./role-run-relocation.js";
32
29
  import { runDirectoryOfSessionFile } from "./role-run-placement.js";
33
- import { resolveOfficerSessionFromPointerFile } from "./archivist-record-pointer.js";
30
+ import { readOfficerPointers } from "./archivist-record-pointer.js";
34
31
  const DISPATCH_TOOLS = new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
35
32
  /** Officer terminating tool → current English officer identity. */
36
33
  const OFFICER_TOOL_TO_FACE = {
@@ -311,61 +308,32 @@ function pairGateRounds(volumes) {
311
308
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
312
309
  }
313
310
  /**
314
- * Read and pair gate-cycle rounds from one or more auditor-roles directories.
315
- * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
316
- * files that name the independent officer session 正本.
317
- * ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
318
- * (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.
319
315
  *
320
316
  * Pass `parentSessionFile` to keep only rounds whose attempt binding names that parent.
321
317
  */
322
- export async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirectory, options = {}) {
323
- const directories = typeof auditorRolesDirectory === "string"
324
- ? [auditorRolesDirectory]
325
- : auditorRolesDirectory;
318
+ export async function readAnalystGateCyclesFromOfficers(parentRunDirectory, options = {}) {
326
319
  const volumes = [];
327
- // Same officer session pointed at N times (historical multi-mint pointers, or
328
- // one parent booking many leaves) must classify once — else each accepted
329
- // seal is counted ×N in gate-cycle statistics (#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).
330
322
  const classifiedOfficerSessions = new Set();
331
- for (const directory of directories) {
332
- let names;
333
- try {
334
- const entries = await readdir(directory, { withFileTypes: true });
335
- names = entries
336
- .filter((e) => e.isFile()
337
- && (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")))
338
- .map((e) => e.name)
339
- .sort();
340
- }
341
- catch (error) {
342
- 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))) {
343
334
  continue;
344
- throw error;
345
- }
346
- for (const name of names) {
347
- const path = join(directory, name);
348
- const fromPointer = name.endsWith(".pointer.json");
349
- const pointer = fromPointer
350
- ? await resolveOfficerSessionFromPointerFile(path)
351
- : undefined;
352
- const sessionPath = pointer?.sessionFile ?? path;
353
- if (fromPointer) {
354
- if (classifiedOfficerSessions.has(sessionPath))
355
- continue;
356
- classifiedOfficerSessions.add(sessionPath);
357
- }
358
- const officer = pointer?.officer === "inspector" || pointer?.officer === "notary"
359
- ? pointer.officer : undefined;
360
- const classified = await classifyAuditorVolume(sessionPath, officer);
361
- for (const volume of classified) {
362
- if (options.parentSessionFile !== undefined &&
363
- volume.parentSessionFile !== undefined &&
364
- !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))) {
365
- continue;
366
- }
367
- volumes.push(volume);
368
335
  }
336
+ volumes.push(volume);
369
337
  }
370
338
  }
371
339
  return pairGateRounds(volumes);
@@ -12,23 +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
20
  import { listBookRunDirectories, parseRunLeaf, sessionFileOf, } from "./role-run-placement.js";
20
- import { sitianRunVolumeDirectory } from "./sitian-appender.js";
21
21
  import { autopsyWriterLock, readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
22
22
  import { readRunTicketNumber } from "./run-ticket-number.js";
23
23
  import { rewriteRunDirectoryPathValue } from "./role-run-relocation.js";
24
24
  import { extractSessionModelSequence, extractSessionTimestampSpan, extractSessionToolIntervals, LedgerSessionJsonlError, readLedgerSessionJsonl, } from "./ledger-session-read.js";
25
- import { readRunTerminalArtifact, } from "./run-terminal-artifacts.js";
26
- 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";
27
27
  import { isRecord, errorText, isMissingPathError } from "./unknown-value.js";
28
28
  /** Existing public-cli run-state live phases — not a new state machine. */
29
29
  const LIVE_RUN_STATES = new Set(["admitted", "running", "resumable"]);
30
30
  /**
31
- * Read lifecycle state from the existing run-state.json face.
31
+ * Read lifecycle state from the state.jsonl run-state fact row.
32
32
  * Used only to distinguish live in-flight runs from terminal no-receipt.
33
33
  */
34
34
  async function readExistingRunLifecycleState(runDirectory) {
@@ -79,17 +79,8 @@ async function listLedgerBookNames(booksRoot) {
79
79
  }
80
80
  }
81
81
  async function readInvocationScopeFields(runDirectory) {
82
- let raw;
83
- try {
84
- raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
85
- }
86
- catch (error) {
87
- if (isMissingPathError(error))
88
- return undefined;
89
- throw error;
90
- }
91
- const parsed = JSON.parse(raw);
92
- if (!isRecord(parsed))
82
+ const parsed = readSectionSync(runDirectory, "invocation");
83
+ if (parsed === undefined)
93
84
  return undefined;
94
85
  if (typeof parsed.projectRoot !== "string" || parsed.projectRoot.trim() === "") {
95
86
  return undefined;
@@ -126,20 +117,11 @@ function decideIssueScope(input) {
126
117
  }
127
118
  async function resolveSessionFile(runDirectory) {
128
119
  // Prefer invocation.sessionFile when present; fall back to S-family principal.
129
- try {
130
- const raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
131
- const parsed = JSON.parse(raw);
132
- if (isRecord(parsed)
133
- && typeof parsed.sessionFile === "string"
134
- && parsed.sessionFile.trim() !== "") {
135
- return typeof parsed.runDirectory === "string"
136
- ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory)
137
- : parsed.sessionFile;
138
- }
139
- }
140
- catch (error) {
141
- if (!isMissingPathError(error))
142
- 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;
143
125
  }
144
126
  return sessionFileOf(runDirectory);
145
127
  }
@@ -331,10 +313,14 @@ async function classifyScopedRun(input) {
331
313
  ghostLeg = ghost.leg;
332
314
  }
333
315
  try {
334
- 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;
335
321
  if (artifact.status === "unreadable") {
336
322
  missingSources.push("terminal-artifact");
337
- reasons.push(`${artifact.file}: ${artifact.reason}`);
323
+ reasons.push(`terminal: ${artifact.reason}`);
338
324
  }
339
325
  else if (artifact.status === "absent") {
340
326
  if (lifecycle === "admitted" || lifecycle === "running") {
@@ -349,10 +335,10 @@ async function classifyScopedRun(input) {
349
335
  terminal = { status: "absent" };
350
336
  }
351
337
  else {
352
- // role already required nonblank by readRunTerminalArtifact (single owner).
338
+ // role already required nonblank by readRunTerminal (single owner).
353
339
  terminal = {
354
340
  status: "present",
355
- file: artifact.file,
341
+ face: artifact.face,
356
342
  body: artifact.body,
357
343
  role: artifact.body.role,
358
344
  };
@@ -387,13 +373,13 @@ async function classifyScopedRun(input) {
387
373
  if (frameSpan === undefined || toolIntervals === undefined || terminal === undefined) {
388
374
  throw new Error(`classifyScopedRun internal invariant: missing retained facts for ${input.runId}`);
389
375
  }
390
- // Nested auditor-roles gate pairs stay inside the sole scan (families must
376
+ // Officer gate pairs stay inside the sole scan (families must
391
377
  // not readdir this tree again). Missing directory → []. Damaged discovered
392
378
  // nested JSONL is page-local unreadable — never silently under-count rounds.
393
379
  let gateCycles;
394
380
  try {
395
381
  const parentSessionFile = sessionFileOf(input.runDirectory);
396
- gateCycles = await readAnalystGateCyclesFromAuditorRoles(sitianRunVolumeDirectory(input.runDirectory, "auditor-roles"), { parentSessionFile });
382
+ gateCycles = await readAnalystGateCyclesFromOfficers(input.runDirectory, { parentSessionFile });
397
383
  }
398
384
  catch (error) {
399
385
  return {
@@ -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";
@@ -1,29 +1,26 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
4
- import { isRecord, errorText } from "./unknown-value.js";
1
+ import { basename, join } from "node:path";
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";
5
6
  import { homeFromRunDirectory } from "./activation-ledger-topology.js";
6
7
  import { findRunDirectoryById, readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
7
8
  import { rewriteRunDirectoryPathValue } from "./role-run-relocation.js";
8
- import { parseRunLeaf, runDirectoryOfSessionFile, runDirectoryFromSessionDirectory } from "./role-run-placement.js";
9
- import { sitianVolumeDirectory } from "./sitian-appender.js";
9
+ import { parseRunLeaf, runDirectoryOfSessionFile } from "./role-run-placement.js";
10
10
  /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
11
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
- }
12
+ /** History record kind of a booked officer pointer (appender routes it to history.jsonl). */
13
+ export { OFFICER_POINTER_RECORD_KIND };
15
14
  /**
16
- * Book a typed pointer under parent session/auditor-roles (same nest owner as
17
- * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
18
- * 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).
19
18
  *
20
- * Stable leaf per officer under one parent (#753 gate-round accounting):
21
- * same-parent re-summons upsert the same pointer instead of minting N files that
22
- * each re-scan the full officer session and multiply gate-cycle 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.
23
22
  */
24
23
  export function bookDirectOfficerRunPointer(options) {
25
- const pointerFile = directOfficerRunPointerFile(options.parentSessionFile, options.officer);
26
- mkdirSync(dirname(pointerFile), { recursive: true });
27
24
  const submissionToolCallId = options.submissionToolCallId?.trim() ?? "";
28
25
  const pointer = {
29
26
  version: 1,
@@ -35,23 +32,17 @@ export function bookDirectOfficerRunPointer(options) {
35
32
  : {}),
36
33
  ...(submissionToolCallId === "" ? {} : { submissionToolCallId }),
37
34
  };
38
- writeFileSync(pointerFile, `${JSON.stringify(pointer)}\n`, "utf8");
35
+ reportRunRecord(runDirectoryOfSessionFile(options.parentSessionFile), OFFICER_POINTER_RECORD_KIND, pointer, "submission-gate");
39
36
  return pointer;
40
37
  }
41
38
  /** Shared strict pointer IO; internal audit wraps unreadability as a re-summons. */
42
- export async function resolveOfficerSessionFromPointerFile(pointerPath) {
43
- let raw;
44
- try {
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
- }
39
+ export async function resolveOfficerPointer(raw, parentRunDirectory) {
40
+ const where = `${parentRunDirectory} officers`;
50
41
  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}`);
42
+ throw new Error(`direct officer run pointer has unknown shape in ${where}`);
52
43
  }
53
44
  if (typeof raw.sessionFile !== "string" || raw.sessionFile.trim() === "") {
54
- throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
45
+ throw new Error(`direct officer run pointer missing sessionFile in ${where}`);
55
46
  }
56
47
  const officer = raw.officer === "inspector" || raw.officer === "notary"
57
48
  || raw.officer === "auditor" || raw.officer === "countersign" ? raw.officer : undefined;
@@ -60,7 +51,6 @@ export async function resolveOfficerSessionFromPointerFile(pointerPath) {
60
51
  let sessionFile = raw.sessionFile;
61
52
  const recordedRunDirectory = runDirectory ?? runDirectoryOfSessionFile(sessionFile);
62
53
  const leaf = parseRunLeaf(basename(recordedRunDirectory));
63
- const parentRunDirectory = runDirectoryFromSessionDirectory(dirname(dirname(pointerPath)));
64
54
  const parentIdentity = await readRoleRunIdentity(parentRunDirectory);
65
55
  if (leaf !== undefined && parentIdentity !== undefined) {
66
56
  const currentRunDirectory = await findRunDirectoryById(homeFromRunDirectory(parentRunDirectory), leaf.runId, parentIdentity.bookKey, leaf.role);
@@ -79,10 +69,28 @@ export async function resolveOfficerSessionFromPointerFile(pointerPath) {
79
69
  ...(submissionToolCallId === undefined ? {} : { submissionToolCallId }),
80
70
  };
81
71
  }
72
+ /**
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).
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
+ }
82
89
  /** Missing or unreadable is not a pass: the internal audit caller re-runs the gate. */
83
90
  export async function readDirectOfficerRunPointer(parentSessionFile, officer) {
84
91
  try {
85
- const pointer = await resolveOfficerSessionFromPointerFile(directOfficerRunPointerFile(parentSessionFile, officer));
92
+ const parentRunDirectory = runDirectoryOfSessionFile(parentSessionFile);
93
+ const pointer = await resolveOfficerPointer((await readBookedOfficerPointers(parentRunDirectory))[officer], parentRunDirectory);
86
94
  if (pointer.officer !== officer)
87
95
  return undefined;
88
96
  return { ...pointer, officer, version: 1, kind: DIRECT_OFFICER_RUN_POINTER_KIND };
@@ -1,6 +1,6 @@
1
1
  import { join, resolve } from "node:path";
2
+ import { RUN_HISTORY_FILE, RUN_LOG_FILE, runCurrentPath } from "./run-dossier.js";
2
3
  import {
3
- roleRunArtifactsDirectory,
4
4
  runDirectoryOfSessionFile,
5
5
  sessionFileOf
6
6
  } from "./role-run-placement.js";
@@ -27,10 +27,11 @@ function createAuditorDossierTool(runDirectory) {
27
27
  }
28
28
  const details = {
29
29
  runDirectory,
30
- admittedRequest: join(runDirectory, "admitted-request.json"),
30
+ current: runCurrentPath(runDirectory),
31
31
  parentSessionCandidate: sessionFileOf(runDirectory),
32
32
  attachments: join(runDirectory, "attachments"),
33
- artifacts: roleRunArtifactsDirectory(runDirectory)
33
+ log: join(runDirectory, RUN_LOG_FILE),
34
+ history: join(runDirectory, RUN_HISTORY_FILE)
34
35
  };
35
36
  return {
36
37
  content: [{ type: "text", text: JSON.stringify(details) }],
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { readPageSync } from "./run-dossier.js";
2
3
  import { basename, join } from "node:path";
3
4
  import { parseRunLeaf } from "./role-run-placement.js";
4
5
  import { isRecord } from "./unknown-value.js";
@@ -93,18 +94,9 @@ function subjectFromSourceDirectory(sourceRunDirectory) {
93
94
  * Explicit subject wins; otherwise the source-run role is the audited object.
94
95
  */
95
96
  export async function readAuditorResumeBinding(runDirectory) {
96
- let raw;
97
- try {
98
- raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
99
- }
100
- catch (error) {
101
- if (error.code === "ENOENT")
102
- return undefined;
103
- throw error;
104
- }
105
- if (!isRecord(raw))
97
+ const record = readPageSync(runDirectory, "admitted");
98
+ if (record === undefined)
106
99
  return undefined;
107
- const record = raw;
108
100
  if (typeof record.sourceRunPath !== "string" || record.sourceRunPath.trim() === "")
109
101
  return undefined;
110
102
  const subject = isAuditorSoulRole(record.auditorSubject)
@@ -9,7 +9,7 @@ async function discoverCaseFiles(root) { const found = []; async function walk(d
9
9
  if (item.isDirectory()) {
10
10
  await walk(path, depth + 1);
11
11
  }
12
- else if (item.isFile() && (item.name.endsWith(".jsonl") || (item.name === "stderr.log" && depth === 1)))
12
+ else if (item.isFile() && ((item.name.endsWith(".jsonl") && item.name !== "state.jsonl" && !(item.name === "log.jsonl" && depth === 1)) || (item.name === "log.jsonl" && depth === 1)))
13
13
  found.push(path);
14
14
  } } await walk(root, 0); return found.sort(); }
15
15
  function sourceList(count, sources) { return { count, sources: [...new Set(sources)].sort() }; }
@@ -122,9 +122,9 @@ export async function loadDoctorCase(runsPath, projectRoot) {
122
122
  const id = relative(root, path).split(sep).join("/");
123
123
  const bytes = await readFile(path);
124
124
  const content = bytes.toString("utf8");
125
- const kind = id.endsWith(".jsonl") ? "session" : "stderr";
125
+ const kind = id.split("/").length === 2 && id.endsWith("/log.jsonl") ? "log" : "session";
126
126
  evidence.push({ id, kind, byteLength: bytes.byteLength, contentLength: content.length, sha256: sha256Hex(bytes), content });
127
- if (kind === "stderr")
127
+ if (kind === "log")
128
128
  continue;
129
129
  const result = deriveSession(content, id);
130
130
  sessions.push(result.session);
@@ -15,12 +15,12 @@
15
15
  * `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
16
16
  * toolResult join keys, and never a detour-owned sidecar file.
17
17
  */
18
+ import { readPageSync, RUN_LOG_FILE } from "./run-dossier.js";
18
19
  import { randomUUID } from "node:crypto";
19
20
  import { readFileSync } from "node:fs";
20
21
  import { join } from "node:path";
21
22
  export { runDirectoryFromSessionDirectory, sessionFileIn as sessionFileFromSessionDirectory, } from "./role-run-placement.js";
22
23
  import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.js";
23
- import { sitianRunVolumeDirectory, sitianVolumeRecordsFile } from "./sitian-appender.js";
24
24
  import { readSitianRecords, resolveSitianRecordPath, sitianReport, } from "./sitian-facade.js";
25
25
  import { isRecord } from "./unknown-value.js";
26
26
  /** Sitian event kind for one detour-tool call (volume under session/). */
@@ -28,11 +28,11 @@ export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call";
28
28
  /** decisiveFacts key — name states detour-tool scope, not full engine usage. */
29
29
  export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage";
30
30
  /**
31
- * Run-relative sitian volume path for one detour call.
31
+ * Run-relative path of the log that holds one detour call's record.
32
32
  * Openable once the run directory is known from the terminal's run id.
33
33
  * Contains no runId bytes itself (#108 + #537).
34
34
  */
35
- export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = sitianVolumeRecordsFile(sitianRunVolumeDirectory("", ENGINE_DETOUR_CALL_KIND));
35
+ export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE = RUN_LOG_FILE;
36
36
  /** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
37
37
  export function engineDetourStdoutByteLength(stdout) {
38
38
  return Buffer.byteLength(stdout, "utf8");
@@ -182,19 +182,11 @@ export async function readEngineDetourToolUsage(input) {
182
182
  const calls = Array.from(callsByToolCallId.values());
183
183
  return { callCount: calls.length, calls };
184
184
  }
185
- /** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
185
+ /** One shared state.jsonl invocation-row reader (absent → undefined; other errors propagate). */
186
186
  function readInvocationRecord(runDirectory) {
187
- try {
188
- const raw = JSON.parse(readFileSync(join(runDirectory, "invocation.json"), "utf8"));
189
- return isRecord(raw) ? raw : undefined;
190
- }
191
- catch (error) {
192
- if (error?.code === "ENOENT")
193
- return undefined;
194
- throw error;
195
- }
187
+ return readPageSync(runDirectory, "invocation");
196
188
  }
197
- /** True when invocation.json carries a non-empty engine axis. */
189
+ /** True when the invocation section carries a non-empty engine axis. */
198
190
  export async function readInvocationEngineMounted(runDirectory) {
199
191
  const raw = readInvocationRecord(runDirectory);
200
192
  if (raw === undefined)