@akagilnc/pi-workflow-roles 0.1.4062 → 0.1.4196

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 (72) hide show
  1. package/dist/acp-host/production-host.js +2075 -1088
  2. package/dist/archivist-record-entry.js +204 -20
  3. package/dist/archivist-record-topology.js +36 -16
  4. package/dist/auditor-dossier-tool.js +2 -1
  5. package/dist/diarist-contracts.js +96 -0
  6. package/dist/doctor-evidence.js +3 -2
  7. package/dist/headless-host/production-host.js +2215 -1228
  8. package/dist/migrate-book-topology.js +278917 -0
  9. package/dist/navigator-public-session.js +22 -25
  10. package/dist/notary-source-run.js +7 -4
  11. package/dist/pi/durable-principal.js +9 -6
  12. package/dist/public-cli/auto-resume.js +2 -5
  13. package/dist/public-cli/diarist-run.js +27 -25
  14. package/dist/public-cli/instruction-seat-run.js +8 -11
  15. package/dist/public-cli/invocation.js +127 -63
  16. package/dist/public-cli/main.js +1507 -645
  17. package/dist/public-cli/option-definitions.js +2 -2
  18. package/dist/public-cli/post-admission.js +47 -12
  19. package/dist/public-cli/run-lifecycle.js +206 -58
  20. package/dist/public-cli/seat-ticket-binding.js +3 -6
  21. package/dist/public-cli/settlement.js +5 -3
  22. package/dist/role-run-placement.js +84 -0
  23. package/dist/role-run-relocation.js +358 -0
  24. package/dist/run-terminal-artifacts.js +2 -1
  25. package/dist/run-ticket-number.js +51 -32
  26. package/dist/sitian-appender.js +11 -26
  27. package/dist/submission-ledger.js +78 -19
  28. package/dist/ticket-provenance.js +3 -1
  29. package/package.json +3 -2
  30. package/scripts/build-package.mjs +34 -2
  31. package/souls/countersign.md +2 -2
  32. package/souls/fixer.md +3 -2
  33. package/src/analyst-ledger.ts +14 -15
  34. package/src/archivist-record-entry.ts +285 -38
  35. package/src/archivist-record-topology.ts +43 -18
  36. package/src/auditor-dossier-tool.ts +2 -1
  37. package/src/book-topology-migration-placement.ts +493 -0
  38. package/src/book-topology-migration.ts +220 -0
  39. package/src/book-topology-migrators-t11.ts +856 -0
  40. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  41. package/src/book-topology-partition-migrators.ts +25 -0
  42. package/src/book-topology-record-class-migrators.ts +779 -0
  43. package/src/book-topology-runs-migrator.ts +244 -0
  44. package/src/diarist-contracts.ts +116 -0
  45. package/src/diarist-role.ts +6 -0
  46. package/src/diarist.ts +2 -0
  47. package/src/doctor-evidence.ts +4 -1
  48. package/src/doctor-role.ts +1 -1
  49. package/src/navigator-public-session.ts +33 -30
  50. package/src/notary-source-run.ts +9 -4
  51. package/src/pi/durable-principal.ts +9 -13
  52. package/src/public-cli/auto-resume.ts +2 -6
  53. package/src/public-cli/countersign-run.ts +182 -32
  54. package/src/public-cli/diarist-run.ts +34 -26
  55. package/src/public-cli/instruction-seat-run.ts +7 -11
  56. package/src/public-cli/invocation.ts +192 -58
  57. package/src/public-cli/option-definitions.ts +2 -2
  58. package/src/public-cli/post-admission.ts +73 -13
  59. package/src/public-cli/run-lifecycle.ts +255 -65
  60. package/src/public-cli/seat-ticket-binding.ts +3 -6
  61. package/src/public-cli/settlement.ts +6 -4
  62. package/src/role-run-placement.ts +128 -0
  63. package/src/role-run-relocation.ts +491 -0
  64. package/src/role-runtime.ts +11 -5
  65. package/src/run-terminal-artifacts.ts +3 -1
  66. package/src/run-ticket-number.ts +80 -20
  67. package/src/sitian-appender.ts +25 -28
  68. package/src/submission-ledger.ts +102 -19
  69. package/src/ticket-provenance.ts +4 -1
  70. package/src/ticket-trajectory.ts +22 -35
  71. package/src/worker-role.ts +4 -4
  72. package/src/worker-submission-gates.ts +6 -6
@@ -6,12 +6,12 @@ import { execFileSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
7
  import {
8
8
  lstat,
9
- mkdir,
10
9
  readFile,
11
10
  realpath,
11
+ rename,
12
12
  writeFile,
13
13
  } from "node:fs/promises";
14
- import { basename, isAbsolute, join, resolve, sep } from "node:path";
14
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
15
15
 
16
16
  import {
17
17
  activationBookDirectory,
@@ -21,14 +21,31 @@ import {
21
21
  resolveActivationLedgerHome,
22
22
  } from "../activation-ledger-topology.ts";
23
23
  import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
24
+ import {
25
+ ensureRoleRunDirectory,
26
+ ensureRoleRunPlacement,
27
+ roleRunArtifactsDirectory,
28
+ roleRunPlacement,
29
+ type RoleRunSubject,
30
+ } from "../role-run-placement.ts";
24
31
  import type {
25
32
  DurablePrincipal,
26
33
  DurablePrincipalAuthority,
27
34
  } from "../host-contracts.ts";
28
- import { readRunTicketNumber } from "../run-ticket-number.ts";
35
+ import {
36
+ isSafePositiveTicketNumber,
37
+ readBoardTicketNumber,
38
+ requireSafePositiveTicketNumber,
39
+ } from "../run-ticket-number.ts";
40
+ import {
41
+ rewriteRoleRunDurablePages,
42
+ rewriteRunDirectoryPathFields,
43
+ rewriteRunDirectoryPathValue,
44
+ } from "../role-run-relocation.ts";
29
45
  import {
30
46
  loadDoctorCase,
31
47
  } from "../doctor-evidence.ts";
48
+ import { projectCourtTicketNumbers } from "../diarist-contracts.ts";
32
49
  import type { DoctorCaseIdentity } from "../doctor-contracts.ts";
33
50
  import {
34
51
  emptyCollectorManifest,
@@ -121,6 +138,17 @@ export type AdmittedJudgeInvocation = AdmittedRoleInvocationBase & {
121
138
 
122
139
  export type AdmittedCountersignInvocation = AdmittedRoleInvocationBase & {
123
140
  readonly role: "countersign";
141
+ /**
142
+ * #871 typed co-review set for this countersign run (durable run fact).
143
+ * Main ticket remains `ticketNumber`; this set drives per-ticket court diarist refresh.
144
+ * Whole-set replace on new typed submission — never union with history.
145
+ */
146
+ courtTicketNumbers?: readonly number[];
147
+ /**
148
+ * Load-time durable set damage diagnostic (#871 B7). Resume must settle as
149
+ * countersign controlled failure with this text — never structural exit 2 or main-only.
150
+ */
151
+ courtTicketNumbersDamage?: string;
124
152
  };
125
153
 
126
154
  export type AdmittedGleanerLeftInvocation = AdmittedRoleInvocationBase & {
@@ -261,30 +289,36 @@ export type AdmissionPlacement = {
261
289
  readonly sessionDirectory: string;
262
290
  readonly sessionFile: string;
263
291
  readonly runDirectory: string;
292
+ readonly artifactsDirectory: string;
293
+ readonly attachmentsDirectory: string;
264
294
  readonly ledgerHome: string;
265
295
  readonly bookKey: string;
266
296
  };
267
297
 
268
- /** Issue principal + derive ledger placement through the injected authority only. */
298
+ /** Issue a principal from the single authoritative role-run placement. */
269
299
  export function issueAdmissionPlacement(
270
300
  authority: DurablePrincipalAuthority,
271
301
  request: {
272
302
  readonly cwd: string;
273
303
  readonly runId: string;
274
304
  readonly role: AdmittedRoleInvocation["role"];
305
+ /** Identity already asserted by 起居郎; never derive this from CLI parameters. */
306
+ readonly subject: RoleRunSubject;
275
307
  readonly home?: string;
276
308
  },
277
309
  ): AdmissionPlacement {
278
- const principal = authority.issue(request);
279
- const { sessionDirectory, sessionFile } = authority.decode(principal);
280
- const runDirectory = join(sessionDirectory, "..");
281
310
  const ledgerHome = resolveActivationLedgerHome(request.home);
282
311
  const bookKey = resolveBookKeyFromGit(request.cwd);
312
+ const placement = roleRunPlacement(ledgerHome, {
313
+ bookKey,
314
+ subject: request.subject,
315
+ runId: request.runId,
316
+ role: request.role,
317
+ });
318
+ ensureRoleRunPlacement(ledgerHome, placement);
283
319
  return {
284
- principal,
285
- sessionDirectory,
286
- sessionFile,
287
- runDirectory,
320
+ principal: authority.seal(placement),
321
+ ...placement,
288
322
  ledgerHome,
289
323
  bookKey,
290
324
  };
@@ -476,17 +510,50 @@ export async function bindAdmittedTicketNumber(
476
510
  admitted: AdmittedRoleInvocation,
477
511
  ticketNumber: number,
478
512
  ): Promise<void> {
479
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
480
- throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
481
- }
482
513
  if (admitted.ticketNumber !== undefined) {
483
514
  if (admitted.ticketNumber === ticketNumber) return;
484
515
  throw new Error(
485
516
  `bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`,
486
517
  );
487
518
  }
519
+ await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
488
520
  (admitted as { ticketNumber?: number }).ticketNumber = ticketNumber;
489
- await mergeInvocationIdentityPage(admitted.runDirectory, { ticketNumber });
521
+ }
522
+
523
+ /**
524
+ * #871: persist the typed co-review set as a countersign run fact (admitted-request +
525
+ * invocation.json). Whole-set replace — never union with a prior set. Main ticket
526
+ * binding stays on `ticketNumber` alone. Projection reuses the sole contract helper;
527
+ * principal membership is mandatory.
528
+ */
529
+ export async function bindCourtTicketNumbersOnAdmitted(
530
+ admitted: AdmittedCountersignInvocation,
531
+ courtTicketNumbers: readonly number[],
532
+ ): Promise<void> {
533
+ if (admitted.ticketNumber === undefined) {
534
+ throw new Error(
535
+ "bindCourtTicketNumbersOnAdmitted requires a bound principal ticketNumber",
536
+ );
537
+ }
538
+ const principal = requireSafePositiveTicketNumber(
539
+ admitted.ticketNumber,
540
+ "bindCourtTicketNumbersOnAdmitted principal ticketNumber",
541
+ );
542
+ // Strict write path: every member must already be a lawful number (no soft filter).
543
+ for (const item of courtTicketNumbers) {
544
+ if (!isSafePositiveTicketNumber(item)) {
545
+ throw new Error(
546
+ `bindCourtTicketNumbersOnAdmitted requires safe positive integers, got ${String(item)}`,
547
+ );
548
+ }
549
+ }
550
+ const projected = projectCourtTicketNumbers(courtTicketNumbers, {
551
+ principalTicket: principal,
552
+ });
553
+ if (projected === null || projected.length === 0) {
554
+ throw new Error("bindCourtTicketNumbersOnAdmitted requires a non-empty ticket set");
555
+ }
556
+ const frozen = Object.freeze([...projected]);
490
557
  const admittedPath = admitted.admittedRequestPath;
491
558
  const current = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
492
559
  string,
@@ -494,9 +561,64 @@ export async function bindAdmittedTicketNumber(
494
561
  >;
495
562
  await writeFile(
496
563
  admittedPath,
497
- `${JSON.stringify({ ...current, ticketNumber }, null, 2)}\n`,
564
+ `${JSON.stringify({ ...current, courtTicketNumbers: frozen }, null, 2)}\n`,
498
565
  "utf8",
499
566
  );
567
+ await mergeInvocationIdentityPage(admitted.runDirectory, {
568
+ courtTicketNumbers: frozen,
569
+ });
570
+ admitted.courtTicketNumbers = frozen;
571
+ }
572
+
573
+ /** Move a settled first-entry run from unbound to its asserted ticket directory. */
574
+ export async function relocateAdmittedRunToTicket(
575
+ admitted: AdmittedRoleInvocation,
576
+ authority: DurablePrincipalAuthority,
577
+ heldLease?: { relocate(runDirectory: string): void },
578
+ ): Promise<void> {
579
+ if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return;
580
+ const oldRunDirectory = admitted.runDirectory;
581
+ const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
582
+ const target = roleRunPlacement(ledgerHome, {
583
+ bookKey: admitted.bookKey,
584
+ subject: { ticketNumber: admitted.ticketNumber },
585
+ runId: admitted.runId,
586
+ role: admitted.role,
587
+ });
588
+ ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
589
+ await rename(oldRunDirectory, target.runDirectory);
590
+ heldLease?.relocate(target.runDirectory);
591
+
592
+ const admittedRecord = admitted as unknown as Record<string, unknown>;
593
+ rewriteRunDirectoryPathFields(
594
+ admittedRecord,
595
+ [
596
+ "runDirectory",
597
+ "admittedRequestPath",
598
+ "taskPath",
599
+ "packetPath",
600
+ "prerequisitesPath",
601
+ "requestManifestPath",
602
+ "mergerInputPath",
603
+ ],
604
+ oldRunDirectory,
605
+ target.runDirectory,
606
+ );
607
+ for (const attachment of admitted.attachments) {
608
+ (attachment as { frozenPath: string }).frozenPath = rewriteRunDirectoryPathValue(
609
+ attachment.frozenPath,
610
+ oldRunDirectory,
611
+ target.runDirectory,
612
+ ) as string;
613
+ }
614
+ const principal = authority.seal(target);
615
+ (admitted as { principal: DurablePrincipal }).principal = principal;
616
+
617
+ await rewriteRoleRunDurablePages({
618
+ pagesDirectory: target.runDirectory,
619
+ oldRunDirectory,
620
+ newRunDirectory: target.runDirectory,
621
+ });
500
622
  }
501
623
 
502
624
  /**
@@ -508,11 +630,10 @@ export async function bindTicketNumberOnRunDirectory(
508
630
  runDirectory: string,
509
631
  ticketNumber: number,
510
632
  ): Promise<void> {
511
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
512
- throw new Error(
513
- `bindTicketNumberOnRunDirectory requires a safe positive integer, got ${String(ticketNumber)}`,
514
- );
515
- }
633
+ requireSafePositiveTicketNumber(
634
+ ticketNumber,
635
+ "bindTicketNumberOnRunDirectory",
636
+ );
516
637
  const admittedPath = join(runDirectory, "admitted-request.json");
517
638
  const invocationPath = join(runDirectory, "invocation.json");
518
639
  const admitted = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
@@ -1089,7 +1210,13 @@ export async function freezeAttachmentsIntoRun(
1089
1210
  function ticketAdmissionFields(
1090
1211
  ticketNumber: number | undefined,
1091
1212
  ): { ticketNumber?: number } {
1092
- return ticketNumber === undefined ? {} : { ticketNumber };
1213
+ if (ticketNumber === undefined) return {};
1214
+ return {
1215
+ ticketNumber: requireSafePositiveTicketNumber(
1216
+ ticketNumber,
1217
+ "assertedTicketNumber",
1218
+ ),
1219
+ };
1093
1220
  }
1094
1221
 
1095
1222
  export type AdmitJudgeInvocationOptions = {
@@ -1107,6 +1234,8 @@ export type AdmitJudgeInvocationOptions = {
1107
1234
 
1108
1235
  export type AdmitInspectorInvocationOptions = AdmitJudgeInvocationOptions & {
1109
1236
  correlationId?: string;
1237
+ /** Typed identity handed off by 起居郎 or inherited from an already-bound run. */
1238
+ assertedTicketNumber?: number;
1110
1239
  };
1111
1240
 
1112
1241
  export type AdmitGatekeeperInvocationOptions = AdmitInspectorInvocationOptions;
@@ -1123,7 +1252,7 @@ async function admitStandardMaterialInvocation<
1123
1252
  R extends "judge" | "inspector" | "gatekeeper" | "navigator" | "auditor" | "diarist",
1124
1253
  >(
1125
1254
  role: R,
1126
- options: AdmitJudgeInvocationOptions & { correlationId?: string },
1255
+ options: AdmitInspectorInvocationOptions,
1127
1256
  ): Promise<AdmittedRoleInvocationBase & { readonly role: R }> {
1128
1257
  // Empty project override must not reach resolve("") → cwd (silent default).
1129
1258
  if (options.project !== undefined) {
@@ -1131,22 +1260,25 @@ async function admitStandardMaterialInvocation<
1131
1260
  }
1132
1261
  const projectRoot = resolve(options.project ?? options.cwd);
1133
1262
  const runId = (options.createRunId ?? uuidv7)();
1263
+ // Validate asserted ticket before placement so 0/NaN never become subjects.
1264
+ const ticketFields = ticketAdmissionFields(options.assertedTicketNumber);
1134
1265
  const {
1135
1266
  principal,
1136
1267
  sessionDirectory,
1137
1268
  sessionFile,
1138
1269
  runDirectory,
1270
+ attachmentsDirectory,
1139
1271
  ledgerHome,
1140
1272
  bookKey,
1141
1273
  } = issueAdmissionPlacement(options.principalAuthority, {
1142
1274
  cwd: projectRoot,
1143
1275
  runId,
1144
1276
  role,
1277
+ subject: ticketFields.ticketNumber === undefined
1278
+ ? { unbound: true }
1279
+ : { ticketNumber: ticketFields.ticketNumber },
1145
1280
  home: options.home,
1146
1281
  });
1147
- const attachmentsDirectory = join(runDirectory, "attachments");
1148
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1149
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1150
1282
 
1151
1283
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1152
1284
  const correlationFields =
@@ -1173,6 +1305,7 @@ async function admitStandardMaterialInvocation<
1173
1305
  sha256: a.sha256,
1174
1306
  mediaKind: a.mediaKind,
1175
1307
  })),
1308
+ ...ticketFields,
1176
1309
  };
1177
1310
  const admittedRequestPath = join(runDirectory, "admitted-request.json");
1178
1311
  await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
@@ -1197,6 +1330,7 @@ async function admitStandardMaterialInvocation<
1197
1330
  principal,
1198
1331
  admittedRequestPath,
1199
1332
  ...correlationFields,
1333
+ ...ticketFields,
1200
1334
  };
1201
1335
  }
1202
1336
 
@@ -1324,17 +1458,16 @@ export async function admitCountersignInvocation(
1324
1458
  sessionDirectory,
1325
1459
  sessionFile,
1326
1460
  runDirectory,
1461
+ attachmentsDirectory,
1327
1462
  ledgerHome,
1328
1463
  bookKey,
1329
1464
  } = issueAdmissionPlacement(options.principalAuthority, {
1330
1465
  cwd: projectRoot,
1331
1466
  runId,
1332
1467
  role: "countersign",
1468
+ subject: { unbound: true },
1333
1469
  home: options.home,
1334
1470
  });
1335
- const attachmentsDirectory = join(runDirectory, "attachments");
1336
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1337
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1338
1471
 
1339
1472
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1340
1473
  // Ticket binding is LLM-only post-admission (#635); admission stays unbound.
@@ -1418,9 +1551,11 @@ export async function loadAdmittedJudgeRequest(
1418
1551
  }
1419
1552
 
1420
1553
  export async function ensureRunArtifactsDir(runDirectory: string): Promise<string> {
1421
- const dir = join(runDirectory, "artifacts");
1422
- await mkdir(dir, { recursive: true });
1423
- return dir;
1554
+ const directory = roleRunArtifactsDirectory(runDirectory);
1555
+ return ensureRoleRunDirectory(
1556
+ resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)),
1557
+ directory,
1558
+ );
1424
1559
  }
1425
1560
 
1426
1561
  export type AdmitCoderInvocationOptions = {
@@ -1464,17 +1599,16 @@ export async function admitCoderInvocation(
1464
1599
  sessionDirectory,
1465
1600
  sessionFile,
1466
1601
  runDirectory,
1602
+ attachmentsDirectory,
1467
1603
  ledgerHome,
1468
1604
  bookKey,
1469
1605
  } = issueAdmissionPlacement(options.principalAuthority, {
1470
1606
  cwd: projectRoot,
1471
1607
  runId,
1472
1608
  role: "coder",
1609
+ subject: { unbound: true },
1473
1610
  home: options.home,
1474
1611
  });
1475
- const attachmentsDirectory = join(runDirectory, "attachments");
1476
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1477
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1478
1612
 
1479
1613
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1480
1614
 
@@ -1611,17 +1745,16 @@ export async function admitFixerInvocation(
1611
1745
  sessionDirectory,
1612
1746
  sessionFile,
1613
1747
  runDirectory,
1748
+ attachmentsDirectory,
1614
1749
  ledgerHome,
1615
1750
  bookKey,
1616
1751
  } = issueAdmissionPlacement(options.principalAuthority, {
1617
1752
  cwd: projectRoot,
1618
1753
  runId,
1619
1754
  role: "fixer",
1755
+ subject: { unbound: true },
1620
1756
  home: options.home,
1621
1757
  });
1622
- const attachmentsDirectory = join(runDirectory, "attachments");
1623
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1624
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1625
1758
 
1626
1759
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1627
1760
 
@@ -1918,17 +2051,16 @@ export async function admitCollectorInvocation(
1918
2051
  sessionDirectory,
1919
2052
  sessionFile,
1920
2053
  runDirectory,
2054
+ attachmentsDirectory,
1921
2055
  ledgerHome,
1922
2056
  bookKey,
1923
2057
  } = issueAdmissionPlacement(options.principalAuthority, {
1924
2058
  cwd: projectRoot,
1925
2059
  runId,
1926
2060
  role: "collector",
2061
+ subject: { unbound: true },
1927
2062
  home: options.home,
1928
2063
  });
1929
- const attachmentsDirectory = join(runDirectory, "attachments");
1930
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1931
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1932
2064
 
1933
2065
  // #676 A: freeze task materials BEFORE target resolution so the role receives
1934
2066
  // real instruction + attachments. Admission binds only explicit --pr or unique
@@ -2015,8 +2147,9 @@ export function buildCollectorTransportPrompt(
2015
2147
  const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
2016
2148
 
2017
2149
  /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
2150
+ /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
2018
2151
  const DOCTOR_CASE_RUNS_PATH_PATTERN =
2019
- /\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/;
2152
+ /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
2020
2153
 
2021
2154
  /**
2022
2155
  * Parse a positive Issue number for public Doctor admission.
@@ -2131,9 +2264,9 @@ export async function resolveDoctorCaseRunsPath(options: {
2131
2264
  runs?: string;
2132
2265
  }): Promise<string> {
2133
2266
  const ledgerHome = resolveActivationLedgerHome(options.home);
2267
+ // Canonical topology: <book>/<ticket>/runs (docs/dossier-topology.md).
2134
2268
  const defaultRuns = join(
2135
2269
  activationBookDirectory(ledgerHome, options.bookKey),
2136
- "issues",
2137
2270
  String(options.issueNumber),
2138
2271
  "runs",
2139
2272
  );
@@ -2177,7 +2310,7 @@ export async function resolveDoctorCaseRunsPath(options: {
2177
2310
  const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
2178
2311
  if (!match) {
2179
2312
  throw new CliUsageError(
2180
- "doctor --runs must be an .ak-roles/books/<book>/issues/<n>/runs directory",
2313
+ "doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory",
2181
2314
  );
2182
2315
  }
2183
2316
  if (Number(match[1]) !== options.issueNumber) {
@@ -2217,12 +2350,14 @@ export async function admitDoctorInvocation(
2217
2350
  sessionDirectory,
2218
2351
  sessionFile,
2219
2352
  runDirectory,
2353
+ attachmentsDirectory,
2220
2354
  ledgerHome,
2221
2355
  bookKey,
2222
2356
  } = issueAdmissionPlacement(options.principalAuthority, {
2223
2357
  cwd: projectRoot,
2224
2358
  runId,
2225
2359
  role: "doctor",
2360
+ subject: { unbound: true },
2226
2361
  home: options.home,
2227
2362
  });
2228
2363
 
@@ -2266,9 +2401,6 @@ export async function admitDoctorInvocation(
2266
2401
  );
2267
2402
  }
2268
2403
 
2269
- const attachmentsDirectory = join(runDirectory, "attachments");
2270
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2271
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2272
2404
 
2273
2405
  const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
2274
2406
 
@@ -2423,24 +2555,27 @@ export async function admitNotaryInvocation(options: {
2423
2555
  throw error;
2424
2556
  }
2425
2557
 
2558
+ // Notary inherits board identity only — migration-derived stays display/placement.
2559
+ const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
2426
2560
  const runId = options.createRunId?.() ?? uuidv7();
2427
2561
  const {
2428
2562
  principal,
2429
2563
  sessionDirectory,
2430
2564
  sessionFile,
2431
2565
  runDirectory,
2566
+ attachmentsDirectory,
2432
2567
  ledgerHome,
2433
2568
  bookKey,
2434
2569
  } = issueAdmissionPlacement(options.principalAuthority, {
2435
2570
  cwd: projectRoot,
2436
2571
  runId,
2437
2572
  role: "notary",
2573
+ subject: inheritedTicketNumber === undefined
2574
+ ? { unbound: true }
2575
+ : { ticketNumber: inheritedTicketNumber },
2438
2576
  home: options.home,
2439
2577
  });
2440
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2441
- const ticketFields = ticketAdmissionFields(
2442
- await readRunTicketNumber(sourceRun.runDirectory),
2443
- );
2578
+ const ticketFields = ticketAdmissionFields(inheritedTicketNumber);
2444
2579
 
2445
2580
  const admitted = {
2446
2581
  role: "notary" as const,
@@ -2582,15 +2717,16 @@ export async function admitGleanerLeftInvocation(
2582
2717
  sessionDirectory,
2583
2718
  sessionFile,
2584
2719
  runDirectory,
2720
+ attachmentsDirectory,
2585
2721
  ledgerHome,
2586
2722
  bookKey,
2587
2723
  } = issueAdmissionPlacement(options.principalAuthority, {
2588
2724
  cwd: projectRoot,
2589
2725
  runId,
2590
2726
  role: "gleaner-left",
2727
+ subject: { unbound: true },
2591
2728
  home: options.home,
2592
2729
  });
2593
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2594
2730
 
2595
2731
  const instruction = options.instruction;
2596
2732
  const instructionEmpty = instruction.trim() === "";
@@ -2746,17 +2882,16 @@ export async function admitReviewerInvocation(
2746
2882
  sessionDirectory,
2747
2883
  sessionFile,
2748
2884
  runDirectory,
2885
+ attachmentsDirectory,
2749
2886
  ledgerHome,
2750
2887
  bookKey,
2751
2888
  } = issueAdmissionPlacement(options.principalAuthority, {
2752
2889
  cwd: projectRoot,
2753
2890
  runId,
2754
2891
  role: "reviewer",
2892
+ subject: { unbound: true },
2755
2893
  home: options.home,
2756
2894
  });
2757
- const attachmentsDirectory = join(runDirectory, "attachments");
2758
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2759
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2760
2895
 
2761
2896
  // Public parse already rejects attachments; keep freeze loop for structural symmetry.
2762
2897
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
@@ -2955,17 +3090,16 @@ export async function admitMergerInvocation(
2955
3090
  sessionDirectory,
2956
3091
  sessionFile,
2957
3092
  runDirectory,
3093
+ attachmentsDirectory,
2958
3094
  ledgerHome,
2959
3095
  bookKey,
2960
3096
  } = issueAdmissionPlacement(options.principalAuthority, {
2961
3097
  cwd: projectRoot,
2962
3098
  runId,
2963
3099
  role: "merger",
3100
+ subject: { unbound: true },
2964
3101
  home: options.home,
2965
3102
  });
2966
- const attachmentsDirectory = join(runDirectory, "attachments");
2967
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2968
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2969
3103
 
2970
3104
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
2971
3105
 
@@ -645,8 +645,8 @@ const DOCTOR_OPTIONS = [
645
645
  repeatable: false,
646
646
  form: "option",
647
647
  description: {
648
- en: "Optional project-relative .ak-roles/books/<book>/issues/<n>/runs override matching --issue.",
649
- zh: "可选项目相对 .ak-roles/books/<book>/issues/<n>/runs 覆盖,且须匹配 --issue。",
648
+ en: "Optional project-relative .ak-roles/books/<book>/<n>/runs override matching --issue.",
649
+ zh: "可选项目相对 .ak-roles/books/<book>/<n>/runs 覆盖,且须匹配 --issue。",
650
650
  },
651
651
  },
652
652
  ] as const satisfies readonly PublicOptionDefinition[];