@akagilnc/pi-workflow-roles 0.1.5504 → 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 (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
@@ -3,15 +3,16 @@
3
3
  * 一册=一个追加式 records.jsonl。每轮新投影经司天台 appender 追加为不可变提交;
4
4
  * 纯追加、不回读历史去重、不折叠。
5
5
  */
6
- import { readFile, unlink } from "node:fs/promises";
6
+ import { readFile } from "node:fs/promises";
7
7
  import { join, resolve } from "node:path";
8
8
  import { errnoCode, packageMachineHome, physicallyContainedIn, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
9
9
  import { readLedgerSessionJsonlLines, } from "./ledger-session-read.js";
10
10
  import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
11
11
  import { runDirectoryOfSessionFile, sessionFileOf } from "./role-run-placement.js";
12
- import { sitianVolumeRecordsFile } from "./sitian-appender.js";
12
+ import { RUN_HISTORY_FILE } from "./run-dossier-files.js";
13
13
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.js";
14
14
  import { appendSitianRecordBlock, appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
15
+ import { decodeSitianRecordLine } from "./sitian-reader.js";
15
16
  import { TICKET_PROVENANCE_KIND, } from "./ticket-provenance-contracts.js";
16
17
  import { errorText } from "./unknown-value.js";
17
18
  /**
@@ -88,18 +89,32 @@ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
88
89
  }
89
90
  /** Assign a run-owned unbound diary as its original block through Sitian. */
90
91
  export async function rehomeUnboundTicketProvenance(runDirectory, ticketNumber, cwd, home) {
91
- const source = sitianVolumeRecordsFile(runDirectory);
92
- let content;
92
+ const ticketInput = ticketProvenanceRecordInput(ticketNumber, cwd, home);
93
+ // Live staging: ticket-provenance rows in the run's history.jsonl. Append-only
94
+ // history keeps the staging rows; the ticket-root diary is the home.
95
+ // Old run/records.jsonl is not a runtime input (#1161); only migration tools read it.
96
+ let history;
93
97
  try {
94
- content = await readFile(source, "utf8");
98
+ history = await readFile(join(runDirectory, RUN_HISTORY_FILE), "utf8");
95
99
  }
96
100
  catch (error) {
97
101
  if (errnoCode(error) === "ENOENT")
98
102
  return;
99
103
  throw error;
100
104
  }
101
- appendSitianRecordBlock(ticketProvenanceRecordInput(ticketNumber, cwd, home), content);
102
- await unlink(source);
105
+ const block = history
106
+ .split("\n")
107
+ .flatMap((line, index) => {
108
+ const decoded = decodeSitianRecordLine(line, index + 1);
109
+ if (decoded === undefined || !decoded.ok)
110
+ return [];
111
+ if (decoded.record.kind !== TICKET_PROVENANCE_KIND)
112
+ return [];
113
+ return [`${line}\n`];
114
+ })
115
+ .join("");
116
+ if (block !== "")
117
+ appendSitianRecordBlock(ticketInput, block);
103
118
  }
104
119
  /**
105
120
  * Resolve a bound endpoint against this round's session lines.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5504",
3
+ "version": "0.1.5571",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,6 +14,8 @@ const entries = [
14
14
  // navigator-session-contracts is a static import of the published attendance root
15
15
  // (#590: published non-bundle graph must stay closed under its own relative edges).
16
16
  "navigator-public-session",
17
+ // Static import of navigator-public-session (sitianReportSafe).
18
+ "host-session-record",
17
19
  "navigator-session-contracts",
18
20
  // Static import of navigator-attendance and navigator-public-session.
19
21
  "navigator-work-base",
@@ -61,7 +61,7 @@ export function createProductionAcpRoleTurnHost(options: ProductionAcpHostOption
61
61
 
62
62
  return createComposedAcpRoleTurnHost({
63
63
  hostName,
64
- sessionIdentity: createSessionIdentityAuthority(principalAuthority, description.sessionBindingFile),
64
+ sessionIdentity: createSessionIdentityAuthority(principalAuthority, hostName),
65
65
  modelPassing: description.modelPassing,
66
66
  ...(description.setModelId === undefined ? {} : { setModelId: description.setModelId }),
67
67
  roleRuntimeDependencies: createRoleRuntimeDependencies(packageRoot),
@@ -9,6 +9,7 @@ import {
9
9
  disposeExternalRoleTurn,
10
10
  externalHostFailure as failure,
11
11
  raceAgainstHostAbort,
12
+ recordTurnDelivery,
12
13
  } from "../external-host-turn-loop.ts";
13
14
  import { describeErrorIdentity } from "../public-cli/run-lifecycle.ts";
14
15
  import { projectThrownFailureLeaf, retainPackageFault } from "../public-cli/settlement.ts";
@@ -372,6 +373,14 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
372
373
  roundLimitName: "AcpRoundLimit",
373
374
  currentSessionId: () => sessionId,
374
375
  async runRound({ prompt, abortSignal }) {
376
+ // One history row per actual session/prompt start (催交回合各算一次).
377
+ // MCP advertises tool.parameters; prepared.jsonSchema is the headless
378
+ // draft-07 stamp of that clone — record the delivered parameters face.
379
+ const { $schema: _headlessStamp, ...deliveredSchema } = prepared.jsonSchema;
380
+ await recordTurnDelivery(request.runDirectory, {
381
+ systemPrompt: systemPromptOverride,
382
+ outputSchema: deliveredSchema,
383
+ }, "acp-host");
375
384
  let result: Readonly<Record<string, unknown>>;
376
385
  // Open the prose gate only for this prompt round; clear any stale chunks first.
377
386
  agentProseChunks.length = 0;
@@ -498,8 +507,6 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
498
507
  cwd: request.cwd,
499
508
  sessionDirectory: sessionDirectoryOf(request.runDirectory),
500
509
  sessionParent,
501
- continuation: request.continuation,
502
- ...(request.model !== undefined ? { model: request.model } : {}),
503
510
  ...(request.home !== undefined ? { home: request.home } : {}),
504
511
  });
505
512
  } catch (error) {
@@ -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,9 +22,6 @@
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
-
29
25
  import {
30
26
  AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
31
27
  } from "./compliance-transport.ts";
@@ -39,7 +35,7 @@ import {
39
35
  import { isRecord, isEnoent } from "./unknown-value.ts";
40
36
  import { retainedRunPathsMatch } from "./role-run-relocation.ts";
41
37
  import { runDirectoryOfSessionFile } from "./role-run-placement.ts";
42
- import { resolveOfficerSessionFromPointerFile } from "./archivist-record-pointer.ts";
38
+ import { readOfficerPointers } from "./archivist-record-pointer.ts";
43
39
 
44
40
  /** One completed gate round: direct officer receipt or historical province/officer pair. */
45
41
  /** Honest origin discriminant: direct summons vs historical province dispatch. */
@@ -418,68 +414,38 @@ function pairGateRounds(
418
414
  }
419
415
 
420
416
  /**
421
- * Read and pair gate-cycle rounds from one or more auditor-roles directories.
422
- * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
423
- * files that name the independent officer session 正本.
424
- * ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
425
- * (failure honesty — damaged topology must not wash to zero rounds).
417
+ * Read and pair gate-cycle rounds from the officer pointers booked in one
418
+ * run's current.json `officers` section; each pointer names the independent
419
+ * officer session 正本. Absent section → [] (lawful zero). A damaged pointer or
420
+ * officer session propagates — damaged topology must not wash to zero rounds.
426
421
  *
427
422
  * Pass `parentSessionFile` to keep only rounds whose attempt binding names that parent.
428
423
  */
429
- export async function readAnalystGateCyclesFromAuditorRoles(
430
- auditorRolesDirectory: string | readonly string[],
424
+ export async function readAnalystGateCyclesFromOfficers(
425
+ parentRunDirectory: string,
431
426
  options: {
432
427
  readonly parentSessionFile?: string;
433
428
  } = {},
434
429
  ): Promise<readonly AnalystGateCycleRound[]> {
435
- const directories = typeof auditorRolesDirectory === "string"
436
- ? [auditorRolesDirectory]
437
- : auditorRolesDirectory;
438
430
  const volumes: ClassifiedVolume[] = [];
439
- // Same officer session pointed at N times (historical multi-mint pointers, or
440
- // one parent booking many leaves) must classify once — else each accepted
441
- // seal is counted ×N in gate-cycle statistics (#753 acceptance A).
431
+ // Same officer session pointed at N times must classify once — else each
432
+ // accepted seal is counted ×N in gate-cycle statistics (#753 acceptance A).
442
433
  const classifiedOfficerSessions = new Set<string>();
443
- for (const directory of directories) {
444
- let names: string[];
445
- try {
446
- const entries = await readdir(directory, { withFileTypes: true });
447
- names = entries
448
- .filter(
449
- (e) =>
450
- e.isFile()
451
- && (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")),
452
- )
453
- .map((e) => e.name)
454
- .sort();
455
- } catch (error) {
456
- if (isEnoent(error)) continue;
457
- throw error;
458
- }
459
- for (const name of names) {
460
- const path = join(directory, name);
461
- const fromPointer = name.endsWith(".pointer.json");
462
- const pointer = fromPointer
463
- ? await resolveOfficerSessionFromPointerFile(path)
464
- : undefined;
465
- const sessionPath = pointer?.sessionFile ?? path;
466
- if (fromPointer) {
467
- if (classifiedOfficerSessions.has(sessionPath)) continue;
468
- classifiedOfficerSessions.add(sessionPath);
469
- }
470
- const officer = pointer?.officer === "inspector" || pointer?.officer === "notary"
471
- ? pointer.officer : undefined;
472
- const classified = await classifyAuditorVolume(sessionPath, officer);
473
- for (const volume of classified) {
474
- if (
475
- options.parentSessionFile !== undefined &&
476
- volume.parentSessionFile !== undefined &&
477
- !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))
478
- ) {
479
- continue;
480
- }
481
- volumes.push(volume);
434
+ for (const pointer of await readOfficerPointers(parentRunDirectory)) {
435
+ if (classifiedOfficerSessions.has(pointer.sessionFile)) continue;
436
+ classifiedOfficerSessions.add(pointer.sessionFile);
437
+ const officer = pointer.officer === "inspector" || pointer.officer === "notary"
438
+ ? pointer.officer : undefined;
439
+ const classified = await classifyAuditorVolume(pointer.sessionFile, officer);
440
+ for (const volume of classified) {
441
+ if (
442
+ options.parentSessionFile !== undefined &&
443
+ volume.parentSessionFile !== undefined &&
444
+ !await retainedRunPathsMatch(volume.parentSessionFile, options.parentSessionFile, runDirectoryOfSessionFile(options.parentSessionFile))
445
+ ) {
446
+ continue;
482
447
  }
448
+ volumes.push(volume);
483
449
  }
484
450
  }
485
451
  return pairGateRounds(volumes);
@@ -12,6 +12,7 @@
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.ts";
15
16
  import { readdir, readFile } from "node:fs/promises";
16
17
  import { basename, join } from "node:path";
17
18
 
@@ -26,7 +27,6 @@ import {
26
27
  parseRunLeaf,
27
28
  sessionFileOf,
28
29
  } from "./role-run-placement.ts";
29
- import { sitianRunVolumeDirectory } from "./sitian-appender.ts";
30
30
  import { autopsyWriterLock, readRoleRunIdentity } from "./public-cli/run-lifecycle.ts";
31
31
  import { readRunTicketNumber } from "./run-ticket-number.ts";
32
32
  import { rewriteRunDirectoryPathValue } from "./role-run-relocation.ts";
@@ -40,8 +40,8 @@ import {
40
40
  type SessionToolInterval,
41
41
  } from "./ledger-session-read.ts";
42
42
  import {
43
- readRunTerminalArtifact,
44
- type RunTerminalArtifactFile,
43
+ readRunTerminal,
44
+ type RunTerminalFace,
45
45
  } from "./run-terminal-artifacts.ts";
46
46
  import type {
47
47
  AnalystFirstFrameAt,
@@ -51,7 +51,7 @@ import type {
51
51
  AnalystUnreadableRun,
52
52
  } from "./analyst-page.ts";
53
53
  import {
54
- readAnalystGateCyclesFromAuditorRoles,
54
+ readAnalystGateCyclesFromOfficers,
55
55
  type AnalystGateCycleRound,
56
56
  } from "./analyst-gate-cycles-read.ts";
57
57
 
@@ -63,7 +63,7 @@ export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
63
63
  const LIVE_RUN_STATES = new Set(["admitted", "running", "resumable"]);
64
64
 
65
65
  /**
66
- * Read lifecycle state from the existing run-state.json face.
66
+ * Read lifecycle state from the state.jsonl run-state fact row.
67
67
  * Used only to distinguish live in-flight runs from terminal no-receipt.
68
68
  */
69
69
  async function readExistingRunLifecycleState(
@@ -134,7 +134,7 @@ async function classifyGhostCandidate(input: {
134
134
  * Invocation scope faces used for issue 圈定 (C4 / #399).
135
135
  * projectRoot is retained for narrow path match and conflict facts;
136
136
  * ticketNumber is the #176 typed face when present (integer ≥ 1).
137
- * Single read of invocation.json — no second parse kernel.
137
+ * Single read of the current.json invocation section — no second parse kernel.
138
138
  */
139
139
  type InvocationScopeFields = {
140
140
  readonly projectRoot: string;
@@ -154,15 +154,8 @@ async function listLedgerBookNames(booksRoot: string): Promise<string[]> {
154
154
  async function readInvocationScopeFields(
155
155
  runDirectory: string,
156
156
  ): Promise<InvocationScopeFields | undefined> {
157
- let raw: string;
158
- try {
159
- raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
160
- } catch (error) {
161
- if (isMissingPathError(error)) return undefined;
162
- throw error;
163
- }
164
- const parsed: unknown = JSON.parse(raw);
165
- if (!isRecord(parsed)) return undefined;
157
+ const parsed = readSectionSync(runDirectory, "invocation");
158
+ if (parsed === undefined) return undefined;
166
159
  if (typeof parsed.projectRoot !== "string" || parsed.projectRoot.trim() === "") {
167
160
  return undefined;
168
161
  }
@@ -212,20 +205,11 @@ async function resolveSessionFile(
212
205
  runDirectory: string,
213
206
  ): Promise<string> {
214
207
  // Prefer invocation.sessionFile when present; fall back to S-family principal.
215
- try {
216
- const raw = await readFile(join(runDirectory, "invocation.json"), "utf8");
217
- const parsed: unknown = JSON.parse(raw);
218
- if (
219
- isRecord(parsed)
220
- && typeof parsed.sessionFile === "string"
221
- && parsed.sessionFile.trim() !== ""
222
- ) {
223
- return typeof parsed.runDirectory === "string"
224
- ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory) as string
225
- : parsed.sessionFile;
226
- }
227
- } catch (error) {
228
- if (!isMissingPathError(error)) throw error;
208
+ const parsed = readSectionSync(runDirectory, "invocation");
209
+ if (parsed !== undefined && typeof parsed.sessionFile === "string" && parsed.sessionFile.trim() !== "") {
210
+ return typeof parsed.runDirectory === "string"
211
+ ? rewriteRunDirectoryPathValue(parsed.sessionFile, parsed.runDirectory, runDirectory) as string
212
+ : parsed.sessionFile;
229
213
  }
230
214
  return sessionFileOf(runDirectory);
231
215
  }
@@ -327,7 +311,7 @@ export type AnalystRunTerminalFace =
327
311
  | { readonly status: "absent" }
328
312
  | {
329
313
  readonly status: "present";
330
- readonly file: RunTerminalArtifactFile;
314
+ readonly face: RunTerminalFace;
331
315
  readonly body: Record<string, unknown>;
332
316
  /** Nonblank producer role — sole owner is readRunTerminalArtifact. */
333
317
  readonly role: string;
@@ -352,9 +336,9 @@ export type AnalystReadableRunFacts = {
352
336
  */
353
337
  readonly models: readonly string[];
354
338
  /**
355
- * Paired gate-cycle rounds from session/auditor-roles/ (#446).
356
- * Missing directory → empty (lawful zero rounds).
357
- * Damaged discovered nested JSONL → leg unreadable (`auditor-roles` source).
339
+ * Paired gate-cycle rounds of the officers booked in current.json (#446).
340
+ * No officers booked → empty (lawful zero rounds).
341
+ * A damaged officer pointer or session → leg unreadable (`auditor-roles` source).
358
342
  */
359
343
  readonly gateCycles: readonly AnalystGateCycleRound[];
360
344
  };
@@ -515,10 +499,14 @@ async function classifyScopedRun(input: {
515
499
  }
516
500
 
517
501
  try {
518
- const artifact = await readRunTerminalArtifact(input.runDirectory);
502
+ const read = readRunTerminal(input.runDirectory);
503
+ // A no_receipt terminal is the explicit form of "no accepted receipt".
504
+ const artifact = read.status === "present" && read.face === "no_receipt"
505
+ ? { status: "absent" as const }
506
+ : read;
519
507
  if (artifact.status === "unreadable") {
520
508
  missingSources.push("terminal-artifact");
521
- reasons.push(`${artifact.file}: ${artifact.reason}`);
509
+ reasons.push(`terminal: ${artifact.reason}`);
522
510
  } else if (artifact.status === "absent") {
523
511
  if (lifecycle === "admitted" || lifecycle === "running") {
524
512
  // No terminal yet: ghost report only, or live in-flight omit.
@@ -530,10 +518,10 @@ async function classifyScopedRun(input: {
530
518
  }
531
519
  terminal = { status: "absent" };
532
520
  } else {
533
- // role already required nonblank by readRunTerminalArtifact (single owner).
521
+ // role already required nonblank by readRunTerminal (single owner).
534
522
  terminal = {
535
523
  status: "present",
536
- file: artifact.file,
524
+ face: artifact.face,
537
525
  body: artifact.body,
538
526
  role: artifact.body.role as string,
539
527
  };
@@ -574,16 +562,13 @@ async function classifyScopedRun(input: {
574
562
  );
575
563
  }
576
564
 
577
- // Nested auditor-roles gate pairs stay inside the sole scan (families must
565
+ // Officer gate pairs stay inside the sole scan (families must
578
566
  // not readdir this tree again). Missing directory → []. Damaged discovered
579
567
  // nested JSONL is page-local unreadable — never silently under-count rounds.
580
568
  let gateCycles: readonly AnalystGateCycleRound[];
581
569
  try {
582
570
  const parentSessionFile = sessionFileOf(input.runDirectory);
583
- gateCycles = await readAnalystGateCyclesFromAuditorRoles(
584
- sitianRunVolumeDirectory(input.runDirectory, "auditor-roles"),
585
- { parentSessionFile },
586
- );
571
+ gateCycles = await readAnalystGateCyclesFromOfficers(input.runDirectory, { parentSessionFile });
587
572
  } catch (error) {
588
573
  return {
589
574
  kind: "unreadable",
@@ -97,7 +97,10 @@ export type AnalystAcceptanceSuccessReworkSection = {
97
97
  readonly rework: AnalystReworkLens;
98
98
  };
99
99
 
100
- /** Locate collector groups array: receipt.groups or top-level groups. */
100
+ /**
101
+ * Locate collector groups array from the canonical typed receipt faces.
102
+ * Same leaf order as extractStatus: body / receipt / decisiveFacts / payloads.
103
+ */
101
104
  function findCollectorGroups(body: Record<string, unknown>): unknown {
102
105
  if (Array.isArray(body.groups)) return body.groups;
103
106
  const receipt = body.receipt;
@@ -106,6 +109,15 @@ function findCollectorGroups(body: Record<string, unknown>): unknown {
106
109
  if (isRecord(outcome)) {
107
110
  const facts = outcome.decisiveFacts;
108
111
  if (isRecord(facts) && Array.isArray(facts.groups)) return facts.groups;
112
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
113
+ // terminal reader rehydrates them via attemptHistoryIdentity. Groups are the
114
+ // collector leaf on that receipt — never invent a second discriminator.
115
+ if (Array.isArray(outcome.payloads)) {
116
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
117
+ const payload = outcome.payloads[index];
118
+ if (isRecord(payload) && Array.isArray(payload.groups)) return payload.groups;
119
+ }
120
+ }
109
121
  }
110
122
  return undefined;
111
123
  }
@@ -116,6 +128,17 @@ export function extractStatus(body: Record<string, unknown>): string | undefined
116
128
  if (isRecord(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
117
129
  return outcome.status;
118
130
  }
131
+ // Typed sealed receipt: settlement keeps payloads on attempt-history; the
132
+ // terminal reader rehydrates them via attemptHistoryIdentity. Status is the role's
133
+ // own leaf on that receipt — never invent a second mapping.
134
+ if (isRecord(outcome) && Array.isArray(outcome.payloads)) {
135
+ for (let index = outcome.payloads.length - 1; index >= 0; index -= 1) {
136
+ const payload = outcome.payloads[index];
137
+ if (isRecord(payload) && typeof payload.status === "string" && payload.status.trim() !== "") {
138
+ return payload.status;
139
+ }
140
+ }
141
+ }
119
142
  const receipt = body.receipt;
120
143
  if (isRecord(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
121
144
  return receipt.status;
@@ -26,7 +26,7 @@ export type AnalystGateCyclesLeg = {
26
26
  readonly runId: string;
27
27
  readonly book: string;
28
28
  readonly role: string;
29
- /** Paired dispatch↔officer volume count (0 when no auditor-roles). */
29
+ /** Paired dispatch↔officer volume count (0 when no officers booked). */
30
30
  readonly roundCount: number;
31
31
  readonly rounds: readonly AnalystGateCyclesRoundRow[];
32
32
  };
@@ -82,13 +82,10 @@ function projectTerminal(facts: AnalystReadableRunFacts): AnalystRoundTimelineTe
82
82
  if (facts.terminal.status === "absent") {
83
83
  return { kind: "death", channel: "no-receipt" };
84
84
  }
85
- if (facts.terminal.file === "error.json") {
85
+ if (facts.terminal.face === "error") {
86
86
  return { kind: "death", channel: "error" };
87
87
  }
88
- if (facts.terminal.file === "audit-incomplete.json") {
89
- return { kind: "death", channel: "audit-incomplete" };
90
- }
91
- // report.json — receipt face; classCount only when producer wrote a number.
88
+ // report face — receipt; classCount only when producer wrote a number.
92
89
  const status = extractStatus(facts.terminal.body);
93
90
  const classCount = readClassCount(facts.terminal.body);
94
91
  const receiptStatus = status ?? "unparsed";
@@ -28,9 +28,9 @@ export type AnalystMissingSource =
28
28
  /** Model-groups mode: leg has no usable session model identity. */
29
29
  | "session-model"
30
30
  /**
31
- * Nested session/auditor-roles volume was discovered but failed canonical
32
- * read/parse (#446). Missing auditor-roles directory is lawful zero rounds,
33
- * not this face.
31
+ * A booked officer pointer or its session failed canonical read/parse (#446).
32
+ * No officers booked is lawful zero rounds, not this face.
33
+ * Public label stays `auditor-roles` (终局来源标签以外不得改名).
34
34
  */
35
35
  | "auditor-roles";
36
36
 
@@ -293,7 +293,7 @@ export type RecordSessionParent = {
293
293
  export type CreateRecordSessionOptions = {
294
294
  /** Role working directory passed to the record session; not a placement input. */
295
295
  readonly cwd: string;
296
- /** What kind of record this is (e.g. "auditor-roles"). Single path segment; not a destination path. */
296
+ /** What kind of record this is (e.g. "attendance"). Single path segment; not a destination path. */
297
297
  readonly kind: string;
298
298
  /** Parent session — nesting authority for ordinary kinds; optional parentSession link for navigator. */
299
299
  readonly parent?: RecordSessionParent;
@@ -373,7 +373,7 @@ export type RecordSessionOpen = {
373
373
  * symlink). New principals mint under the already-validated sessionDir.
374
374
  * Navigator work-subject nests use their AK ledger; worker-submission-gate asks the
375
375
  * host to continue its recent native session without selecting a stored file path.
376
- * Other ordinary no-subject children (auditor-roles, …) always mint fresh.
376
+ * Other ordinary no-subject children (attendance, …) always mint fresh.
377
377
  * New persisted principals materialize their deferred session header before return so
378
378
  * custom-entry-only writers do not need a parallel delayed-header helper.
379
379
  *