@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
@@ -27,9 +27,9 @@ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
27
27
  import {
28
28
  ensureRoleRunDirectory,
29
29
  ensureRoleRunPlacement,
30
+ formatRunLeaf,
30
31
  isUnboundRunDirectory,
31
32
  listBookRunDirectories,
32
- roleRunArtifactsDirectory,
33
33
  roleRunPlacement,
34
34
  type RoleRunSubject,
35
35
  } from "../role-run-placement.ts";
@@ -51,6 +51,7 @@ import {
51
51
  } from "../packaged-role-registry.ts";
52
52
  import {
53
53
  isSafePositiveTicketNumber,
54
+ parseTicketNumber,
54
55
  readBoardTicketNumber,
55
56
  requireSafePositiveTicketNumber,
56
57
  } from "../run-ticket-number.ts";
@@ -58,6 +59,7 @@ import {
58
59
  rewriteRunDirectoryPathFields,
59
60
  rewriteRunDirectoryPathValue,
60
61
  } from "../role-run-relocation.ts";
62
+ import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.ts";
61
63
  import {
62
64
  loadDoctorCase,
63
65
  } from "../doctor-evidence.ts";
@@ -135,7 +137,6 @@ export type AdmittedRoleInvocationBase = {
135
137
  readonly runDirectory: string;
136
138
  /** Host-issued opaque durable principal (coordinates only via authority.decode). */
137
139
  readonly principal: DurablePrincipal;
138
- readonly admittedRequestPath: string;
139
140
  /**
140
141
  * Optional opaque invocation correlation restored from a prior admitted page
141
142
  * (ADR 0049 host channel). Admission does not mint ticket-binding ids.
@@ -316,7 +317,6 @@ export type AdmissionPlacement = {
316
317
  readonly sessionDirectory: string;
317
318
  readonly sessionFile: string;
318
319
  readonly runDirectory: string;
319
- readonly artifactsDirectory: string;
320
320
  readonly attachmentsDirectory: string;
321
321
  readonly ledgerHome: string;
322
322
  readonly bookKey: string;
@@ -354,25 +354,20 @@ export function issueAdmissionPlacement(
354
354
  }
355
355
 
356
356
  /**
357
- * Unique admitted-request.json persistence projection: top-level sessionDirectory/sessionFile
357
+ * Unique `admitted` section projection of current.json: top-level sessionDirectory/sessionFile
358
358
  * (base wire shape). Memory Admitted keeps only the opaque principal — never dual-carry.
359
359
  */
360
- async function writeAdmittedRequestPersistence(
361
- admittedRequestPath: string,
360
+ function writeAdmittedRequestPersistence(
361
+ runDirectory: string,
362
362
  body: Record<string, unknown>,
363
363
  coordinates: { readonly sessionDirectory: string; readonly sessionFile: string },
364
- ): Promise<void> {
364
+ ): void {
365
365
  const { principal: _omitPrincipal, ...rest } = body;
366
- const projection = {
366
+ writeSectionSync(runDirectory, "admitted", {
367
367
  ...rest,
368
368
  sessionDirectory: coordinates.sessionDirectory,
369
369
  sessionFile: coordinates.sessionFile,
370
- };
371
- await writeFile(
372
- admittedRequestPath,
373
- `${JSON.stringify(projection, null, 2)}\n`,
374
- "utf8",
375
- );
370
+ });
376
371
  }
377
372
 
378
373
  /**
@@ -401,7 +396,7 @@ function effectiveModelLedgerFields(
401
396
  export { homeFromRunDirectory };
402
397
 
403
398
  /**
404
- * Persist one `invocation.json` identity page for the public run.
399
+ * Persist the `invocation` identity section of current.json for the public run.
405
400
  * Admission is the sole source for every field; this is the only identity
406
401
  * projection and callers never provide an independent ledger shape.
407
402
  * When an effective model is known at admission, provider/model (and thinking
@@ -424,11 +419,7 @@ async function writeRoleInvocationLedger(
424
419
  ...(source.ticketNumber === undefined ? {} : { ticketNumber: source.ticketNumber }),
425
420
  ...effectiveModelLedgerFields(effectiveModel),
426
421
  };
427
- await writeFile(
428
- join(source.runDirectory, "invocation.json"),
429
- `${JSON.stringify(identity, null, 2)}\n`,
430
- "utf8",
431
- );
422
+ writeSectionSync(source.runDirectory, "invocation", identity);
432
423
  }
433
424
 
434
425
  /**
@@ -448,7 +439,7 @@ export async function recordEffectiveInvocationModel(
448
439
  host?: string,
449
440
  engineModel?: string | null,
450
441
  ): Promise<void> {
451
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => {
442
+ updateSectionSync(runDirectory, "invocation", (current) => {
452
443
  const next: Record<string, unknown> = { ...current };
453
444
  if (model !== undefined) {
454
445
  next.provider = model.provider;
@@ -476,33 +467,19 @@ export async function recordEffectiveInvocationModel(
476
467
  });
477
468
  }
478
469
 
479
- /**
480
- * Read one durable JSON object, let the caller keep its field rules, and write
481
- * the page back. `undefined` means the caller decided not to change the page.
482
- */
483
- async function updateDurableJsonPage(
484
- path: string,
485
- update: (current: Record<string, unknown>) => Record<string, unknown> | undefined,
486
- ): Promise<void> {
487
- const current = JSON.parse(await readFile(path, "utf8")) as Record<string, unknown>;
488
- const next = update(current);
489
- if (next === undefined) return;
490
- await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
491
- }
492
-
493
- /** Merge observed launch-time fields into the single existing invocation.json identity page. */
470
+ /** Merge observed launch-time fields into the single existing invocation section of current.json. */
494
471
  async function mergeInvocationIdentityPage(
495
472
  runDirectory: string,
496
473
  fields: Record<string, unknown>,
497
474
  ): Promise<void> {
498
- await updateDurableJsonPage(join(runDirectory, "invocation.json"), (current) => ({
475
+ updateSectionSync(runDirectory, "invocation", (current) => ({
499
476
  ...current,
500
477
  ...fields,
501
478
  }));
502
479
  }
503
480
 
504
481
  /**
505
- * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
482
+ * Persist parent --source-run path onto the admitted section for officer resume lookup (#747).
506
483
  * Reuses the existing notary sourceRunPath key; does not invent a new field name.
507
484
  */
508
485
  export async function persistAdmittedSourceRunPath(
@@ -513,7 +490,7 @@ export async function persistAdmittedSourceRunPath(
513
490
  if (sourceRunPath.trim() === "") {
514
491
  throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
515
492
  }
516
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
493
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
517
494
  if (typeof current.sourceRunPath === "string" && current.sourceRunPath !== sourceRunPath) {
518
495
  throw new Error(
519
496
  `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`,
@@ -531,7 +508,7 @@ export async function persistAdmittedSourceRunPath(
531
508
 
532
509
  /**
533
510
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
534
- * object and both durable pages (invocation.json + admitted-request.json).
511
+ * object and both durable sections (invocation + admitted).
535
512
  * A later typed role receipt replaces an earlier assertion (#1025).
536
513
  */
537
514
  export async function bindAdmittedTicketNumber(
@@ -549,7 +526,7 @@ export async function recordAdmittedCorrelation(
549
526
  correlationId: string,
550
527
  ): Promise<void> {
551
528
  let correlationIds: string[] = [];
552
- await updateDurableJsonPage(admitted.admittedRequestPath, (current) => {
529
+ updateSectionSync(admitted.runDirectory, "admitted", (current) => {
553
530
  const prior = [
554
531
  ...(Array.isArray(current.correlationIds)
555
532
  ? current.correlationIds.filter((value): value is string =>
@@ -596,11 +573,10 @@ export async function relocateAdmittedRunToTicket(
596
573
  const principal = authority.seal(target);
597
574
 
598
575
  if (admitted.role !== "diarist") {
599
- const parentPage = JSON.parse(await readFile(admitted.admittedRequestPath, "utf8")) as Record<string, unknown>;
600
- const childRunIds = parentPage.childDiaristRunIds;
576
+ const childRunIds = readPageSync(oldRunDirectory, "admitted")?.childDiaristRunIds;
601
577
  for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
602
578
  if (typeof childRunId !== "string") continue;
603
- const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
579
+ const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
604
580
  // A child that already filed under a ticket keeps its own assertion.
605
581
  if (!existsSync(childDirectory)) continue;
606
582
  const childTarget = roleRunPlacement(ledgerHome, {
@@ -614,6 +590,16 @@ export async function relocateAdmittedRunToTicket(
614
590
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
615
591
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
616
592
  await rename(childDirectory, childTarget.runDirectory);
593
+ // Finished child legs will not settle again; refresh derived host.original
594
+ // after the rename commit. A derived render fault must not undo or block
595
+ // the committed placement (#1161 L1) — note and keep the true cause.
596
+ try {
597
+ renderCurrentSync(childTarget.runDirectory);
598
+ } catch (error) {
599
+ process.stderr.write(
600
+ `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
601
+ );
602
+ }
617
603
  }
618
604
  }
619
605
 
@@ -627,8 +613,9 @@ export async function relocateAdmittedRunToTicket(
627
613
  // Persisted paths are resolved from typed run identity on read.
628
614
  await rename(oldRunDirectory, target.runDirectory);
629
615
 
630
- // rename moved the open lock inode with the directory. Transfer cleanup
631
- // ownership immediately after the commit.
616
+ // rename moved the open lock inode with the directory. Transfer cleanup and
617
+ // admitted path ownership immediately after the commit — before any derived
618
+ // render that may refuse (#1161 L1 / BASE order).
632
619
  heldLease?.relocate(target.runDirectory);
633
620
 
634
621
  const admittedRecord = admitted as unknown as Record<string, unknown>;
@@ -636,7 +623,6 @@ export async function relocateAdmittedRunToTicket(
636
623
  admittedRecord,
637
624
  [
638
625
  "runDirectory",
639
- "admittedRequestPath",
640
626
  "taskPath",
641
627
  "packetPath",
642
628
  "prerequisitesPath",
@@ -655,6 +641,11 @@ export async function relocateAdmittedRunToTicket(
655
641
  }
656
642
  (admitted as { principal: DurablePrincipal }).principal = principal;
657
643
 
644
+ // current.json projects host.original from this run directory. Ownership is
645
+ // already transferred; a refuse still carries the true cause to the caller
646
+ // (#1161 L1) without leaving the leg under the unbound path.
647
+ renderCurrentSync(target.runDirectory);
648
+
658
649
  return { oldRunDirectory, newRunDirectory: target.runDirectory };
659
650
  }
660
651
 
@@ -663,7 +654,7 @@ export async function recordChildDiaristRun(
663
654
  parent: AdmittedRoleInvocation,
664
655
  childRunId: string,
665
656
  ): Promise<void> {
666
- await updateDurableJsonPage(parent.admittedRequestPath, (page) => {
657
+ updateSectionSync(parent.runDirectory, "admitted", (page) => {
667
658
  const existing = Array.isArray(page.childDiaristRunIds)
668
659
  ? page.childDiaristRunIds.filter((runId): runId is string => typeof runId === "string")
669
660
  : [];
@@ -685,9 +676,8 @@ export async function bindTicketNumberOnRunDirectory(
685
676
  ticketNumber,
686
677
  "bindTicketNumberOnRunDirectory",
687
678
  );
688
- const admittedPath = join(runDirectory, "admitted-request.json");
689
679
  let unchanged = false;
690
- await updateDurableJsonPage(admittedPath, (admitted) => {
680
+ updateSectionSync(runDirectory, "admitted", (admitted) => {
691
681
  if (admitted.ticketNumber === ticketNumber) {
692
682
  unchanged = true;
693
683
  return undefined;
@@ -710,7 +700,7 @@ export async function recordLaunchedPiIdentity(
710
700
  }
711
701
 
712
702
  /**
713
- * Observed role-package launch provenance written onto the same invocation.json page.
703
+ * Observed role-package launch provenance written onto the same state.jsonl invocation row.
714
704
  * Values are field observations from the public CLI activation seam — never fixed schema markers.
715
705
  */
716
706
  export type LaunchedRolePackageIdentity = {
@@ -759,20 +749,22 @@ export async function recordLaunchedRolePackageIdentity(
759
749
  });
760
750
  }
761
751
 
752
+ /** Trim, then the shared ticket spelling. Callers keep their own diagnostic text. */
753
+ function rejectUnlessTicketNumber(raw: string, message: string): number {
754
+ const parsed = parseTicketNumber(raw.trim());
755
+ if (parsed === undefined) throw new CliUsageError(message);
756
+ return parsed;
757
+ }
758
+
762
759
  /** Positive ticket number for analyst query-scope face (and shared integer parse). */
763
760
  export function parsePositiveTicketNumber(
764
761
  raw: string,
765
762
  flag: string,
766
763
  ): number {
767
- const trimmed = raw.trim();
768
- if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
769
- throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
770
- }
771
- const value = Number(trimmed);
772
- if (!Number.isSafeInteger(value) || value < 1) {
773
- throw new CliUsageError(`${flag} must be a positive integer, got ${raw}`);
774
- }
775
- return value;
764
+ return rejectUnlessTicketNumber(
765
+ raw,
766
+ `${flag} must be a positive integer, got ${raw}`,
767
+ );
776
768
  }
777
769
 
778
770
  /**
@@ -1299,10 +1291,16 @@ async function freezeRegularFileAttachment(
1299
1291
 
1300
1292
  /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
1301
1293
  async function freezeAttachments(
1294
+ ledgerHome: string,
1302
1295
  attachmentPaths: readonly string[],
1303
1296
  attachmentsDirectory: string,
1304
1297
  ): Promise<readonly FrozenAttachment[]> {
1305
1298
  const attachments: FrozenAttachment[] = [];
1299
+ // attachments/ appears only when something is frozen; created through the
1300
+ // ledger's symlink-rejecting tree walk, never a plain mkdir.
1301
+ if (attachmentPaths.length > 0) {
1302
+ ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1303
+ }
1306
1304
  for (let i = 0; i < attachmentPaths.length; i += 1) {
1307
1305
  const frozen = await freezeRegularFileAttachment(
1308
1306
  attachmentPaths[i]!,
@@ -1328,7 +1326,7 @@ export async function freezeAttachmentsIntoRun(
1328
1326
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1329
1327
  const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
1330
1328
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1331
- return freezeAttachments(attachmentPaths, attachmentsDirectory);
1329
+ return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
1332
1330
  }
1333
1331
 
1334
1332
  /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
@@ -1650,7 +1648,10 @@ export async function admitPublicRole(
1650
1648
  }
1651
1649
  let caseIdentity: DoctorCaseIdentity;
1652
1650
  try {
1653
- const patient = await loadDoctorCase(caseRunsPath);
1651
+ const patient = await loadDoctorCase(
1652
+ caseRunsPath,
1653
+ parsed.runs === undefined ? undefined : placed.projectRoot,
1654
+ );
1654
1655
  if (patient.identity.issueNumber !== issueNumber) {
1655
1656
  throw new CliUsageError(
1656
1657
  `doctor case issue ${patient.identity.issueNumber} does not match --issue ${issueNumber}`,
@@ -1666,7 +1667,7 @@ export async function admitPublicRole(
1666
1667
  { cause: error },
1667
1668
  );
1668
1669
  }
1669
- frozenAttachments = await freezeAttachments(attachmentPaths, placed.attachmentsDirectory);
1670
+ frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
1670
1671
  return {
1671
1672
  issueNumber,
1672
1673
  caseRunsPath,
@@ -1825,7 +1826,7 @@ export type AdmitJudgeInvocationOptions = {
1825
1826
  /** Injectable clock/id for tests. */
1826
1827
  createRunId?: () => string;
1827
1828
  principalAuthority: DurablePrincipalAuthority;
1828
- /** Effective model for this invocation — written onto invocation.json. */
1829
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
1829
1830
  model?: InvocationEffectiveModel;
1830
1831
  /** Typed ticket already on this summons (起居录 / parent board). Never parsed from prose. */
1831
1832
  assertedTicketNumber?: number;
@@ -1897,7 +1898,7 @@ async function placeRoleAdmission(options: {
1897
1898
  });
1898
1899
  const attachments = options.freezeAttachments === false
1899
1900
  ? []
1900
- : await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1901
+ : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1901
1902
  return {
1902
1903
  runId,
1903
1904
  bookKey,
@@ -1947,9 +1948,8 @@ async function persistPlacedAdmission(
1947
1948
  },
1948
1949
  placed: PlacedRoleAdmission,
1949
1950
  model: InvocationEffectiveModel | undefined,
1950
- ): Promise<string> {
1951
- const admittedRequestPath = join(placed.runDirectory, "admitted-request.json");
1952
- await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
1951
+ ): Promise<void> {
1952
+ writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
1953
1953
  sessionDirectory: placed.sessionDirectory,
1954
1954
  sessionFile: placed.sessionFile,
1955
1955
  });
@@ -1962,12 +1962,11 @@ async function persistPlacedAdmission(
1962
1962
  admitted.role,
1963
1963
  model,
1964
1964
  );
1965
- return admittedRequestPath;
1966
1965
  }
1967
1966
 
1968
1967
  /**
1969
1968
  * Shared admission: project check, placement, attachment freeze,
1970
- * admitted-request and invocation ledger write.
1969
+ * admitted and invocation section write.
1971
1970
  * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1972
1971
  * before materializeCountersignInvocation writes the page.
1973
1972
  * Seats whose extra facts are known before placement pass them as admittedFields.
@@ -1981,7 +1980,7 @@ async function admitStandardMaterialInvocation<
1981
1980
  >(
1982
1981
  role: R,
1983
1982
  options: AdmitInspectorInvocationOptions & {
1984
- /** Reserve coordinates; skip freeze, placement disk, and admitted-request write. */
1983
+ /** Reserve coordinates; skip freeze, placement disk, and admitted section write. */
1985
1984
  readonly deferPersistence?: boolean;
1986
1985
  /** Skip attachment freeze. Gleaner-left admits no caller attachments. */
1987
1986
  readonly freezeAttachments?: boolean;
@@ -2032,9 +2031,7 @@ async function admitStandardMaterialInvocation<
2032
2031
  attachments: persistedAttachmentRefs(placed.attachments),
2033
2032
  ...placed.ticketFields,
2034
2033
  };
2035
- const admittedRequestPath = defer
2036
- ? join(placed.runDirectory, "admitted-request.json")
2037
- : await persistPlacedAdmission(admitted, placed, options.model);
2034
+ if (!defer) await persistPlacedAdmission(admitted, placed, options.model);
2038
2035
  return {
2039
2036
  role,
2040
2037
  runId: placed.runId,
@@ -2045,7 +2042,6 @@ async function admitStandardMaterialInvocation<
2045
2042
  attachments: placed.attachments,
2046
2043
  runDirectory: placed.runDirectory,
2047
2044
  principal: placed.principal,
2048
- admittedRequestPath,
2049
2045
  ...correlationFields,
2050
2046
  ...placed.ticketFields,
2051
2047
  ...admittedFields,
@@ -2138,7 +2134,7 @@ export type AdmitCountersignInvocationOptions = {
2138
2134
  /** Injectable clock/id for tests. */
2139
2135
  createRunId?: () => string;
2140
2136
  principalAuthority: DurablePrincipalAuthority;
2141
- /** Effective model for this invocation — written onto invocation.json. */
2137
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2142
2138
  model?: InvocationEffectiveModel;
2143
2139
  correlationId?: string;
2144
2140
  /** Same-ticket lookup may select an existing run before a new run is persisted. */
@@ -2168,10 +2164,6 @@ export async function materializeCountersignInvocation(
2168
2164
  });
2169
2165
  const placement = placed;
2170
2166
  (admitted as { runDirectory: string }).runDirectory = placement.runDirectory;
2171
- (admitted as { admittedRequestPath: string }).admittedRequestPath = join(
2172
- placement.runDirectory,
2173
- "admitted-request.json",
2174
- );
2175
2167
  (admitted as { principal: DurablePrincipal }).principal = placement.principal;
2176
2168
  const ticketFields = ticketAdmissionFields(options.ticketNumber);
2177
2169
  if (ticketFields.ticketNumber !== undefined) {
@@ -2182,7 +2174,7 @@ export async function materializeCountersignInvocation(
2182
2174
  admitted.runDirectory,
2183
2175
  );
2184
2176
  (admitted as { attachments: readonly FrozenAttachment[] }).attachments = attachments;
2185
- await writeAdmittedRequestPersistence(admitted.admittedRequestPath, admitted, {
2177
+ writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
2186
2178
  sessionDirectory: placement.sessionDirectory,
2187
2179
  sessionFile: placement.sessionFile,
2188
2180
  });
@@ -2193,7 +2185,7 @@ export async function materializeCountersignInvocation(
2193
2185
  );
2194
2186
  }
2195
2187
 
2196
- /** Load admitted-request.json written at admission (Navigator work-context seam). */
2188
+ /** Load the admitted section written at admission (Navigator work-context seam). */
2197
2189
  export async function loadAdmittedJudgeRequest(
2198
2190
  runDirectory: string,
2199
2191
  ): Promise<{
@@ -2202,11 +2194,8 @@ export async function loadAdmittedJudgeRequest(
2202
2194
  attachments: readonly FrozenAttachment[];
2203
2195
  } | undefined> {
2204
2196
  try {
2205
- const raw = JSON.parse(
2206
- await readFile(join(runDirectory, "admitted-request.json"), "utf8"),
2207
- ) as unknown;
2208
- if (!isRecord(raw)) return undefined;
2209
- const record = raw;
2197
+ const record = readPageSync(runDirectory, "admitted");
2198
+ if (record === undefined) return undefined;
2210
2199
  if (!packagedPublicInstructionSubject(record.role)) return undefined;
2211
2200
  if (typeof record.instruction !== "string") return undefined;
2212
2201
  if (typeof record.instructionEmpty !== "boolean") return undefined;
@@ -2221,14 +2210,6 @@ export async function loadAdmittedJudgeRequest(
2221
2210
  }
2222
2211
  }
2223
2212
 
2224
- export async function ensureRunArtifactsDir(runDirectory: string): Promise<string> {
2225
- const directory = roleRunArtifactsDirectory(runDirectory);
2226
- return ensureRoleRunDirectory(
2227
- resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)),
2228
- directory,
2229
- );
2230
- }
2231
-
2232
2213
  export type AdmitFixerInvocationOptions = {
2233
2214
  home: string;
2234
2215
  principalAuthority: DurablePrincipalAuthority;
@@ -2240,7 +2221,7 @@ export type AdmitFixerInvocationOptions = {
2240
2221
  prerequisitesPath?: string;
2241
2222
  project?: string;
2242
2223
  createRunId?: () => string;
2243
- /** Effective model for this invocation — written onto invocation.json. */
2224
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2244
2225
  model?: InvocationEffectiveModel;
2245
2226
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2246
2227
  assertedTicketNumber?: number;
@@ -2311,32 +2292,21 @@ function isGitRemoteMissing(error: unknown): boolean {
2311
2292
  return status === 2;
2312
2293
  }
2313
2294
 
2314
- /** Positive Issue number grammar shared with Doctor case path identity. */
2315
- const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
2316
-
2317
- /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
2318
- /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
2319
- const DOCTOR_CASE_RUNS_PATH_PATTERN =
2320
- /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
2321
-
2322
2295
  /**
2323
2296
  * Parse a positive Issue number for public Doctor admission.
2324
2297
  * Leading zeros and non-integers are structural rejects.
2325
2298
  */
2326
2299
  export function parseDoctorIssueNumber(raw: string): number {
2327
- const trimmed = raw.trim();
2328
- if (!DOCTOR_ISSUE_NUMBER_PATTERN.test(trimmed)) {
2329
- throw new CliUsageError(
2330
- `doctor --issue must be a positive integer, got ${raw}`,
2331
- );
2332
- }
2333
- return Number(trimmed);
2300
+ return rejectUnlessTicketNumber(
2301
+ raw,
2302
+ `doctor --issue must be a positive integer, got ${raw}`,
2303
+ );
2334
2304
  }
2335
2305
 
2336
2306
  /**
2337
2307
  * Resolve the retained Doctor case runs root from Issue identity.
2338
- * Default is the #78 book locator; optional --runs must stay project-confined
2339
- * and match Doctor case grammar for the same issue number.
2308
+ * Default is the #78 book locator; optional --runs must stay project-confined.
2309
+ * loadDoctorCase owns retained-root grammar at IO; admission matches its case identity.
2340
2310
  */
2341
2311
  export async function resolveDoctorCaseRunsPath(options: {
2342
2312
  home: string;
@@ -2388,18 +2358,6 @@ export async function resolveDoctorCaseRunsPath(options: {
2388
2358
  );
2389
2359
  }
2390
2360
 
2391
- const normalized = real.split(sep).join("/");
2392
- const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
2393
- if (!match) {
2394
- throw new CliUsageError(
2395
- "doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory",
2396
- );
2397
- }
2398
- if (Number(match[1]) !== options.issueNumber) {
2399
- throw new CliUsageError(
2400
- `doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`,
2401
- );
2402
- }
2403
2361
  return real;
2404
2362
  }
2405
2363
 
@@ -2418,7 +2376,7 @@ export type AdmitReviewerInvocationOptions = {
2418
2376
  project?: string;
2419
2377
  createRunId?: () => string;
2420
2378
  correlationId?: string;
2421
- /** Effective model for this invocation — written onto invocation.json. */
2379
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2422
2380
  model?: InvocationEffectiveModel;
2423
2381
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2424
2382
  assertedTicketNumber?: number;
@@ -2471,14 +2429,12 @@ export type AdmitMergerInvocationOptions = {
2471
2429
  attachmentPaths: readonly string[];
2472
2430
  project?: string;
2473
2431
  createRunId?: () => string;
2474
- /** Effective model for this invocation — written onto invocation.json. */
2432
+ /** Effective model for this invocation — written onto the state.jsonl invocation row. */
2475
2433
  model?: InvocationEffectiveModel;
2476
2434
  /** Typed ticket already on this summons. Placement uses it; code does not infer one. */
2477
2435
  assertedTicketNumber?: number;
2478
2436
  };
2479
2437
 
2480
- const ANALYST_TICKET_NUMBER_PATTERN = /^[1-9]\d*$/;
2481
-
2482
2438
  /**
2483
2439
  * Parse a positive ticket / issue number for public analyst admission.
2484
2440
  * Leading zeros and non-integers are structural rejects (same face as #176).
@@ -2488,20 +2444,10 @@ export function parseAnalystTicketNumber(
2488
2444
  raw: string,
2489
2445
  flag: string = "--ticket",
2490
2446
  ): number {
2491
- const trimmed = raw.trim();
2492
- if (!ANALYST_TICKET_NUMBER_PATTERN.test(trimmed)) {
2493
- throw new CliUsageError(
2494
- `analyst ${flag} must be a positive integer, got ${raw}`,
2495
- );
2496
- }
2497
- const value = Number(trimmed);
2498
- // Digit-only strings beyond MAX_SAFE_INTEGER round or become Infinity — reject.
2499
- if (!Number.isSafeInteger(value) || value < 1) {
2500
- throw new CliUsageError(
2501
- `analyst ${flag} must be a positive integer, got ${raw}`,
2502
- );
2503
- }
2504
- return value;
2447
+ return rejectUnlessTicketNumber(
2448
+ raw,
2449
+ `analyst ${flag} must be a positive integer, got ${raw}`,
2450
+ );
2505
2451
  }
2506
2452
 
2507
2453
  /**
@@ -2535,7 +2481,7 @@ export function parseAnalystCohortIssueToken(
2535
2481
  const sep = trimmed.lastIndexOf(":");
2536
2482
  if (sep > 0) {
2537
2483
  const rhs = trimmed.slice(sep + 1);
2538
- if (ANALYST_TICKET_NUMBER_PATTERN.test(rhs)) {
2484
+ if (parseTicketNumber(rhs) !== undefined) {
2539
2485
  const bookKey = trimmed.slice(0, sep);
2540
2486
  if (bookKey.trim() === "") {
2541
2487
  throw new CliUsageError(