@akagilnc/pi-workflow-roles 0.1.5153 → 0.1.5196

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 (52) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +2 -2
  3. package/dist/acp-host/production-host.js +13086 -13034
  4. package/dist/archivist-record-entry.js +4 -20
  5. package/dist/archivist-record-pointer.js +28 -0
  6. package/dist/audit-escalation.js +3 -119
  7. package/dist/auditor-soul.js +32 -0
  8. package/dist/doctor-auditor.js +26 -0
  9. package/dist/headless-host/production-host.js +11218 -11166
  10. package/dist/judge-role.js +73 -0
  11. package/dist/migrate-book-topology.js +8 -0
  12. package/dist/package-contracts/judge-output.js +1 -18
  13. package/dist/package-contracts/terminating-tools.js +2 -2
  14. package/dist/public-cli/cli.js +2 -2
  15. package/dist/public-cli/countersign-run.js +11 -0
  16. package/dist/public-cli/instruction-seat-run.js +428 -48
  17. package/dist/public-cli/invocation.js +10 -5
  18. package/dist/public-cli/main.js +10049 -8911
  19. package/dist/public-cli/settlement.js +28 -8
  20. package/dist/public-role-summons.js +6 -4
  21. package/dist/submission-errors.js +1 -1
  22. package/dist/submission-gate.js +165 -0
  23. package/dist/submission-ledger.js +0 -27
  24. package/dist/worker-submission-contracts.js +8 -0
  25. package/package.json +1 -1
  26. package/src/archivist-record-entry.ts +5 -49
  27. package/src/archivist-record-pointer.ts +49 -0
  28. package/src/audit-escalation.ts +4 -217
  29. package/src/auditor-soul.ts +33 -0
  30. package/src/doctor-role.ts +11 -5
  31. package/src/gatekeeper-role.ts +2 -2
  32. package/src/host-contracts.ts +0 -25
  33. package/src/judge-role.ts +63 -90
  34. package/src/package-contracts/judge-output.ts +0 -17
  35. package/src/package-contracts/terminating-tools.ts +0 -2
  36. package/src/pi/adapter.ts +1 -33
  37. package/src/public-cli/cli.ts +2 -2
  38. package/src/public-cli/countersign-run.ts +11 -0
  39. package/src/public-cli/instruction-seat-run.ts +433 -16
  40. package/src/public-cli/invocation.ts +9 -3
  41. package/src/public-cli/post-admission.ts +0 -1
  42. package/src/public-cli/settlement.ts +32 -8
  43. package/src/public-role-summons.ts +9 -6
  44. package/src/role-envelope.ts +0 -32
  45. package/src/role-runtime-dependencies.ts +0 -4
  46. package/src/role-runtime.ts +15 -218
  47. package/src/submission-errors.ts +1 -1
  48. package/src/submission-gate.ts +46 -10
  49. package/src/submission-ledger.ts +0 -27
  50. package/src/worker-role.ts +23 -102
  51. package/src/worker-submission-contracts.ts +8 -0
  52. package/src/worker-submission-gates.ts +2 -2
@@ -54,16 +54,18 @@ function roleOutcomeFromRows(role, rows) {
54
54
  const terminal = mine.filter((row) => row.kind === "accepted" || row.kind === "audit-escalation");
55
55
  if (terminal.length === 0)
56
56
  return undefined;
57
- // Role-result block is the full recorded sequence for this seat, not a sole pick.
57
+ // Full sequence stays on payloads. The queue kind follows the latest terminal
58
+ // row only — an earlier escalation must not outrank a later acceptance or
59
+ // keep that earlier receipt (#1057).
58
60
  const payloads = mine.map((row) => row.accepted);
59
- if (terminal.some((row) => row.kind === "audit-escalation")) {
60
- const audited = terminal.slice().reverse().find((row) => row.kind === "audit-escalation" && Object.hasOwn(row, "auditReceipt"));
61
+ const latest = terminal[terminal.length - 1];
62
+ if (latest.kind === "audit-escalation") {
61
63
  return {
62
64
  kind: "audit_escalation", role, status: "audit_escalation", payloads,
63
- ...(audited === undefined ? {} : { decisiveFacts: {
64
- auditEscalationReceipt: audited.auditReceipt,
65
- ...(Object.hasOwn(audited, "auditOfficer") ? { auditEscalationOfficer: audited.auditOfficer } : {}),
66
- } }),
65
+ ...(Object.hasOwn(latest, "auditReceipt") ? { decisiveFacts: {
66
+ auditEscalationReceipt: latest.auditReceipt,
67
+ ...(Object.hasOwn(latest, "auditOfficer") ? { auditEscalationOfficer: latest.auditOfficer } : {}),
68
+ } } : {}),
67
69
  };
68
70
  }
69
71
  return { kind: "accepted", role, payloads };
@@ -535,7 +537,7 @@ function sessionReadFailure(error, fallbackMessage) {
535
537
  * Read the exact bound Pi session file principal.
536
538
  * Does not scan the session directory for "latest" — resume identity is the file.
537
539
  */
538
- async function readBoundSessionEntries(sessionFile) {
540
+ export async function readBoundSessionEntries(sessionFile) {
539
541
  const text = await readFile(sessionFile, "utf8");
540
542
  const entries = [];
541
543
  for (const line of text.trim().split("\n").filter(Boolean)) {
@@ -1286,6 +1288,8 @@ function snapshotAuditDetails(details) {
1286
1288
  * terminal identity; the retained response must be this seat's real escalate
1287
1289
  * decision and its projected audit-owned fields must agree with it.
1288
1290
  */
1291
+ // Historical pre-#1057 session artifacts may still contain this old projection;
1292
+ // no current submission path writes it, but resume/settlement must read it.
1289
1293
  function boundAuditEscalationForResult(entries, resultIndex, message, role, outputToolName) {
1290
1294
  const roleCall = boundRoleToolCallForResult(entries, resultIndex, message, outputToolName);
1291
1295
  if (roleCall === undefined)
@@ -2216,6 +2220,22 @@ async function applySecretariatCountersignTerminal(admitted, authority, settled)
2216
2220
  }
2217
2221
  return settled;
2218
2222
  }
2223
+ /**
2224
+ * After the audit gate returns, attach gate rounds and the secretariat officer
2225
+ * fact onto the terminal this turn already settled. Does not publish again —
2226
+ * a second publish would append another attempt-history row for the same attempt.
2227
+ */
2228
+ export async function attachPostAuditProjection(admitted, authority, terminal) {
2229
+ const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
2230
+ const gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
2231
+ runDirectory: admitted.runDirectory,
2232
+ parentSessionFile: sessionFile,
2233
+ });
2234
+ const withGate = gate === undefined ? terminal : { ...terminal, gate };
2235
+ if (admitted.role !== "secretariat")
2236
+ return withGate;
2237
+ return applySecretariatCountersignTerminal(admitted, authority, withGate);
2238
+ }
2219
2239
  function publicationAttemptFromError(path, error) {
2220
2240
  if (error instanceof Error) {
2221
2241
  const identity = {
@@ -439,6 +439,8 @@ async function summonGateOfficer(options) {
439
439
  const { readableGateItem } = await import("./readable-gate-item.js");
440
440
  gateReviewInstruction = readableGateItem(options.submission);
441
441
  }
442
+ const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.js");
443
+ const parentRunId = runIdFromRunDirectory(options.sourceRunDirectory);
442
444
  const common = {
443
445
  cwd: options.cwd,
444
446
  ...home === void 0 ? {} : { home },
@@ -449,7 +451,8 @@ async function summonGateOfficer(options) {
449
451
  ...gateReviewInstruction === void 0 ? {} : { gateReviewInstruction },
450
452
  ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
451
453
  ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
452
- ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
454
+ ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId },
455
+ ...parentRunId === void 0 || parentRunId.trim() === "" ? {} : { correlationId: parentRunId }
453
456
  };
454
457
  const { packagedGateSummon } = await import("./packaged-role-registry.js");
455
458
  const gateSummon = packagedGateSummon(options.officer);
@@ -474,13 +477,12 @@ async function summonGateOfficer(options) {
474
477
  });
475
478
  }
476
479
  if (gateSummon === "parent-instruction") {
477
- const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.js");
478
- const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
479
- if (correlationId === void 0 || correlationId.trim() === "") {
480
+ if (parentRunId === void 0 || parentRunId.trim() === "") {
480
481
  throw new Error(
481
482
  `countersign gate summon requires parent runId from sourceRunDirectory: ${options.sourceRunDirectory}`
482
483
  );
483
484
  }
485
+ const correlationId = parentRunId;
484
486
  const { isSafePositiveTicketNumber, readBoardTicketNumber } = await import("./run-ticket-number.js");
485
487
  const parentTicket = await readBoardTicketNumber(options.sourceRunDirectory);
486
488
  const submittedTicket = options.submission !== null && typeof options.submission === "object" && !Array.isArray(options.submission) ? options.submission.ticketNumber : void 0;
@@ -9,7 +9,7 @@ function serializeReceipt(receipt) {
9
9
  return readableGateItem(receipt);
10
10
  }
11
11
  function gatekeeperNonPassMessage(result) {
12
- if (result.status === "continue" || result.status === "escalate") {
12
+ if (result.status === "continue") {
13
13
  return serializeReceipt(result.receipt);
14
14
  }
15
15
  if (result.status === "transport_failure") {
@@ -0,0 +1,165 @@
1
+ import { bookDirectOfficerRunPointer } from "./archivist-record-pointer.js";
2
+ import { GatekeeperDecisionError, officerConclusionReask, projectGatekeeperRun, } from "./gatekeeper-role.js";
3
+ /** Queue word of the latest this-terminal payload. History does not outrank it. */
4
+ export function latestQueueStatus(terminal) {
5
+ const outcome = terminal?.roleOutcome;
6
+ if (outcome === undefined)
7
+ return undefined;
8
+ if (outcome.kind === "audit_escalation")
9
+ return "escalate";
10
+ if (outcome.kind !== "accepted")
11
+ return undefined;
12
+ const latest = outcome.payloads?.[outcome.payloads.length - 1];
13
+ if (latest !== null && typeof latest === "object" && !Array.isArray(latest)) {
14
+ const status = latest.status;
15
+ if (typeof status === "string" && status.trim() !== "")
16
+ return status;
17
+ }
18
+ return typeof outcome.status === "string" && outcome.status.trim() !== ""
19
+ ? outcome.status
20
+ : undefined;
21
+ }
22
+ export function latestQueuePayload(terminal) {
23
+ const outcome = terminal?.roleOutcome;
24
+ if (outcome === undefined)
25
+ return undefined;
26
+ if (outcome.kind !== "accepted" && outcome.kind !== "audit_escalation")
27
+ return undefined;
28
+ const payloads = outcome.payloads ?? [];
29
+ return payloads.length === 0 ? undefined : payloads[payloads.length - 1];
30
+ }
31
+ import { sessionFileFromPublicSummon } from "./session-assistant-usage.js";
32
+ /**
33
+ * Shared-envelope default officer summon (ADR 0018).
34
+ * Role modules must not own this drive seam — only project results.
35
+ */
36
+ export function createDefaultGateOfficerSummon(options) {
37
+ return async (officer, sourceRunDirectory, signal, reask, submission) => {
38
+ const { summonGateOfficer } = await import("./public-role-summons.js");
39
+ return summonGateOfficer({
40
+ officer,
41
+ sourceRunDirectory,
42
+ cwd: options.cwd,
43
+ ...(signal === undefined ? {} : { signal }),
44
+ ...(reask === undefined ? {} : { reask }),
45
+ ...(submission === undefined ? {} : { submission }),
46
+ ...(options.home === undefined ? {} : { home: options.home }),
47
+ ...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
48
+ ...(options.io === undefined ? {} : { io: options.io }),
49
+ ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
50
+ ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
51
+ ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
52
+ });
53
+ };
54
+ }
55
+ /**
56
+ * Book a typed pointer under parent session/auditor-roles (archivist-owned write).
57
+ * Offline mocks without a real session leave no nested volume (lawful zero).
58
+ */
59
+ function bookDirectOfficerPointer(context, officer, result, summoned) {
60
+ if (result.status !== "converged"
61
+ && result.status !== "continue"
62
+ && result.status !== "escalate"
63
+ && result.status !== "needs_reask"
64
+ && result.status !== "transport_failure") {
65
+ return;
66
+ }
67
+ const parentFile = context.sessionManager?.getSessionFile?.();
68
+ if (typeof parentFile !== "string" || parentFile.trim() === "")
69
+ return;
70
+ const sessionFile = sessionFileFromPublicSummon(summoned);
71
+ if (sessionFile === undefined) {
72
+ // No independent 正本 to point at — do not synthesize a parallel session.
73
+ return;
74
+ }
75
+ bookDirectOfficerRunPointer({
76
+ parentSessionFile: parentFile,
77
+ officer,
78
+ sessionFile,
79
+ ...(typeof summoned.runDirectory === "string" && summoned.runDirectory.trim() !== ""
80
+ ? { runDirectory: summoned.runDirectory }
81
+ : {}),
82
+ });
83
+ }
84
+ /**
85
+ * Shared envelope: project gate, book officer pointer, map onto host actions.
86
+ * Review loop has no round cap (#753 no-round-cap).
87
+ * On converged, returns the officer snapshot (receipt + nested runId) for seat projection.
88
+ */
89
+ export async function requireSubmissionGate(options) {
90
+ let reask;
91
+ // No round cap — end only on converged, continue/escalate, or real failure.
92
+ const summonOfficer = options.summonOfficer ??
93
+ createDefaultGateOfficerSummon({
94
+ cwd: options.context.cwd ?? process.cwd(),
95
+ });
96
+ for (;;) {
97
+ const projected = await projectGatekeeperRun({
98
+ context: options.context,
99
+ subject: options.subject,
100
+ summonOfficer,
101
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
102
+ ...(options.submission === undefined ? {} : { submission: options.submission }),
103
+ ...(reask === undefined ? {} : { reask }),
104
+ });
105
+ const gatekeeper = projected.result;
106
+ // Envelope-owned pointer book. Failure is host infrastructure — single face.
107
+ if (projected.summoned !== undefined) {
108
+ try {
109
+ bookDirectOfficerPointer(options.context, projected.officer, gatekeeper, projected.summoned);
110
+ }
111
+ catch (error) {
112
+ options.hostActions.failInfrastructure(error, options.context, options.toolCallId);
113
+ }
114
+ }
115
+ if (gatekeeper.status === "converged") {
116
+ return {
117
+ status: "converged",
118
+ officer: projected.officer,
119
+ receipt: gatekeeper.receipt,
120
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
121
+ ? { runId: gatekeeper.runId }
122
+ : {}),
123
+ };
124
+ }
125
+ if (gatekeeper.status === "continue") {
126
+ return {
127
+ status: "continue",
128
+ officer: projected.officer,
129
+ receipt: gatekeeper.receipt,
130
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
131
+ ? { runId: gatekeeper.runId }
132
+ : {}),
133
+ };
134
+ }
135
+ if (gatekeeper.status === "needs_reask") {
136
+ reask = officerConclusionReask(gatekeeper.receivedStatus);
137
+ continue;
138
+ }
139
+ if (gatekeeper.status === "transport_failure") {
140
+ const failure = Object.assign(new Error(gatekeeper.reason), {
141
+ name: "GatekeeperTransportFailure",
142
+ ...(gatekeeper.submission === undefined ? {} : { submission: gatekeeper.submission }),
143
+ });
144
+ options.hostActions.failInfrastructure(failure, options.context, options.toolCallId);
145
+ }
146
+ if (gatekeeper.status === "escalate") {
147
+ return {
148
+ status: "escalate",
149
+ officer: projected.officer,
150
+ receipt: gatekeeper.receipt,
151
+ ...(typeof gatekeeper.runId === "string" && gatekeeper.runId.trim() !== ""
152
+ ? { runId: gatekeeper.runId }
153
+ : {}),
154
+ ...(typeof projected.summoned?.runDirectory === "string"
155
+ && projected.summoned.runDirectory.trim() !== ""
156
+ ? { runDirectory: projected.summoned.runDirectory }
157
+ : {}),
158
+ };
159
+ }
160
+ // no_receipt: keep the lifecycle failure channel; continue is an ordinary
161
+ // nonterminal tool result above, not an exception or correctable rejection.
162
+ options.hostActions.bindSubmissionNonPass(options.toolCallId, gatekeeper);
163
+ throw new GatekeeperDecisionError(gatekeeper);
164
+ }
165
+ }
@@ -1,7 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
3
3
  import { courtAttemptIdFromHostContext, runDirectoryFromHostContext, } from "./host-contracts.js";
4
- import { isAuditEscalationProjection } from "./audit-escalation.js";
5
4
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
6
5
  import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from "./sitian-facade.js";
7
6
  import { findRunDirectoryById } from "./public-cli/run-lifecycle.js";
@@ -528,32 +527,6 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
528
527
  }
529
528
  // #836: ledger authority is the LLM tool-call params as-is (角色原话), never result.details.
530
529
  // Machine facts on result.details stay on the tool-result face returned to the model.
531
- if (isAuditEscalationProjection(result.details) || isAuditEscalationProjection(params)) {
532
- const audit = isAuditEscalationProjection(result.details)
533
- && typeof result.details.audit === "object"
534
- && result.details.audit !== null
535
- ? result.details.audit
536
- : undefined;
537
- const closed = {
538
- role,
539
- kind: "audit_escalation",
540
- accepted: params,
541
- };
542
- append({
543
- type: "outcome",
544
- attemptId,
545
- toolCallId,
546
- outcome: "audit-escalation",
547
- role,
548
- accepted: params,
549
- ...(audit !== undefined && (Object.hasOwn(audit, "receipt") || Object.hasOwn(audit, "conflicts"))
550
- ? { auditReceipt: Object.hasOwn(audit, "receipt") ? audit.receipt : audit.conflicts }
551
- : {}),
552
- ...(audit !== undefined && Object.hasOwn(audit, "officer") ? { auditOfficer: audit.officer } : {}),
553
- });
554
- await projectClosure(closed, context);
555
- return result;
556
- }
557
530
  // A continuing gate leaves the candidate recorded but has not accepted it.
558
531
  if (result.terminate === false)
559
532
  return result;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Statuses that enter the worker officer submission gate (ADR 0066):
3
+ * completed/partially_completed enter gate ①; planned/refused/unfinished do not.
4
+ */
5
+ export const WORKER_DONE_STATUSES = new Set([
6
+ "completed",
7
+ "partially_completed",
8
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5153",
3
+ "version": "0.1.5196",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -6,7 +6,6 @@
6
6
  import {
7
7
  closeSync,
8
8
  existsSync,
9
- mkdirSync,
10
9
  openSync,
11
10
  readdirSync,
12
11
  readFileSync,
@@ -32,6 +31,11 @@ import {
32
31
  } from "./archivist-record-topology.ts";
33
32
  import type { HostRecordSession, RecordSessionHost } from "./host-contracts.ts";
34
33
  import { piRecordSessionHost } from "./pi/record-session-host.ts";
34
+ export {
35
+ DIRECT_OFFICER_RUN_POINTER_KIND,
36
+ bookDirectOfficerRunPointer,
37
+ } from "./archivist-record-pointer.ts";
38
+ export type { DirectOfficerRunPointer } from "./archivist-record-pointer.ts";
35
39
 
36
40
  export {
37
41
  NAVIGATOR_RECORD_KIND,
@@ -473,51 +477,3 @@ export function createRecordSessionOpen(
473
477
  export function createRecordSession(options: CreateRecordSessionOptions): HostRecordSession {
474
478
  return createRecordSessionOpen(options).session;
475
479
  }
476
-
477
-
478
- /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
479
- export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
480
-
481
- export type DirectOfficerRunPointer = {
482
- readonly version: 1;
483
- readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
484
- readonly officer: "inspector" | "notary" | "auditor" | "countersign";
485
- /** Absolute path to the officer session.jsonl 正本. */
486
- readonly sessionFile: string;
487
- /** Officer run directory when known. */
488
- readonly runDirectory?: string;
489
- };
490
-
491
- /**
492
- * Book a typed pointer under parent session/auditor-roles (same nest owner as
493
- * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
494
- * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
495
- *
496
- * Stable leaf per officer under one parent (#753 gate-round accounting):
497
- * same-parent re-summons upsert the same pointer instead of minting N files that
498
- * each re-scan the full officer session and multiply terminal gate-round counts.
499
- */
500
- export function bookDirectOfficerRunPointer(options: {
501
- readonly parentSessionFile: string;
502
- readonly officer: "inspector" | "notary" | "auditor" | "countersign";
503
- readonly sessionFile: string;
504
- readonly runDirectory?: string;
505
- }): DirectOfficerRunPointer {
506
- const nest = join(dirname(options.parentSessionFile), "auditor-roles");
507
- mkdirSync(nest, { recursive: true });
508
- const pointer: DirectOfficerRunPointer = {
509
- version: 1,
510
- kind: DIRECT_OFFICER_RUN_POINTER_KIND,
511
- officer: options.officer,
512
- sessionFile: options.sessionFile,
513
- ...(options.runDirectory !== undefined && options.runDirectory.trim() !== ""
514
- ? { runDirectory: options.runDirectory }
515
- : {}),
516
- };
517
- writeFileSync(
518
- join(nest, `${options.officer}.pointer.json`),
519
- `${JSON.stringify(pointer)}\n`,
520
- "utf8",
521
- );
522
- return pointer;
523
- }
@@ -0,0 +1,49 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
5
+ export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
6
+
7
+ export type DirectOfficerRunPointer = {
8
+ readonly version: 1;
9
+ readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
10
+ readonly officer: "inspector" | "notary" | "auditor" | "countersign";
11
+ /** Absolute path to the officer session.jsonl 正本. */
12
+ readonly sessionFile: string;
13
+ /** Officer run directory when known. */
14
+ readonly runDirectory?: string;
15
+ };
16
+
17
+ /**
18
+ * Book a typed pointer under parent session/auditor-roles (same nest owner as
19
+ * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
20
+ * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
21
+ *
22
+ * Stable leaf per officer under one parent (#753 gate-round accounting):
23
+ * same-parent re-summons upsert the same pointer instead of minting N files that
24
+ * each re-scan the full officer session and multiply terminal gate-round counts.
25
+ */
26
+ export function bookDirectOfficerRunPointer(options: {
27
+ readonly parentSessionFile: string;
28
+ readonly officer: "inspector" | "notary" | "auditor" | "countersign";
29
+ readonly sessionFile: string;
30
+ readonly runDirectory?: string;
31
+ }): DirectOfficerRunPointer {
32
+ const nest = join(dirname(options.parentSessionFile), "auditor-roles");
33
+ mkdirSync(nest, { recursive: true });
34
+ const pointer: DirectOfficerRunPointer = {
35
+ version: 1,
36
+ kind: DIRECT_OFFICER_RUN_POINTER_KIND,
37
+ officer: options.officer,
38
+ sessionFile: options.sessionFile,
39
+ ...(options.runDirectory !== undefined && options.runDirectory.trim() !== ""
40
+ ? { runDirectory: options.runDirectory }
41
+ : {}),
42
+ };
43
+ writeFileSync(
44
+ join(nest, `${options.officer}.pointer.json`),
45
+ `${JSON.stringify(pointer)}\n`,
46
+ "utf8",
47
+ );
48
+ return pointer;
49
+ }