@akagilnc/pi-workflow-roles 0.1.4035 → 0.1.4193

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 (108) hide show
  1. package/dist/acp-host/production-host.js +2368 -1155
  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-auditor.js +1 -1
  7. package/dist/doctor-evidence.js +3 -2
  8. package/dist/dossier-resolution.js +21 -8
  9. package/dist/gatekeeper-role.js +34 -27
  10. package/dist/headless-host/description.js +7 -8
  11. package/dist/headless-host/production-host.js +2375 -1149
  12. package/dist/host-contracts.js +16 -0
  13. package/dist/inspector-contracts.js +8 -0
  14. package/dist/migrate-book-topology.js +278917 -0
  15. package/dist/navigator-public-session.js +22 -25
  16. package/dist/notary-source-run.js +7 -4
  17. package/dist/packaged-role-registry.js +1 -1
  18. package/dist/pi/durable-principal.js +9 -6
  19. package/dist/pi/role-turn-host.js +43 -13
  20. package/dist/public-cli/auto-resume.js +2 -5
  21. package/dist/public-cli/case-dossier-delivery.js +60 -1
  22. package/dist/public-cli/diarist-run.js +27 -25
  23. package/dist/public-cli/inspector-run.js +17 -3
  24. package/dist/public-cli/instruction-seat-run.js +17 -17
  25. package/dist/public-cli/invocation.js +127 -63
  26. package/dist/public-cli/main.js +1685 -696
  27. package/dist/public-cli/notary-run.js +7 -4
  28. package/dist/public-cli/option-definitions.js +2 -2
  29. package/dist/public-cli/post-admission.js +113 -71
  30. package/dist/public-cli/run-lifecycle.js +209 -58
  31. package/dist/public-cli/seat-ticket-binding.js +3 -6
  32. package/dist/public-cli/settlement.js +29 -10
  33. package/dist/public-role-summons.js +8 -0
  34. package/dist/role-run-placement.js +84 -0
  35. package/dist/role-run-relocation.js +358 -0
  36. package/dist/run-terminal-artifacts.js +2 -1
  37. package/dist/run-ticket-number.js +51 -32
  38. package/dist/sitian-appender.js +11 -26
  39. package/dist/submission-ledger.js +106 -25
  40. package/dist/ticket-provenance.js +3 -1
  41. package/dist/user-dialogue-stdin.js +33 -0
  42. package/extensions/role-runtime.ts +1 -0
  43. package/package.json +3 -2
  44. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  45. package/scripts/build-package.mjs +34 -2
  46. package/souls/fixer.md +3 -2
  47. package/src/analyst-ledger.ts +14 -15
  48. package/src/archivist-record-entry.ts +285 -38
  49. package/src/archivist-record-topology.ts +43 -18
  50. package/src/auditor-dossier-tool.ts +2 -1
  51. package/src/book-topology-migration-placement.ts +493 -0
  52. package/src/book-topology-migration.ts +220 -0
  53. package/src/book-topology-migrators-t11.ts +856 -0
  54. package/src/book-topology-mixed-volume-migrators.ts +470 -0
  55. package/src/book-topology-partition-migrators.ts +25 -0
  56. package/src/book-topology-record-class-migrators.ts +779 -0
  57. package/src/book-topology-runs-migrator.ts +244 -0
  58. package/src/diarist-contracts.ts +116 -0
  59. package/src/diarist-role.ts +6 -0
  60. package/src/diarist.ts +2 -0
  61. package/src/doctor-auditor.ts +1 -1
  62. package/src/doctor-evidence.ts +4 -1
  63. package/src/doctor-role.ts +1 -1
  64. package/src/dossier-resolution.ts +26 -8
  65. package/src/external-host-turn-loop.ts +9 -0
  66. package/src/gatekeeper-pass-envelope.ts +6 -0
  67. package/src/gatekeeper-role.ts +47 -28
  68. package/src/headless-host/description.ts +8 -11
  69. package/src/headless-host/role-turn-host.ts +20 -5
  70. package/src/host-contracts.ts +22 -3
  71. package/src/inspector-contracts.ts +7 -0
  72. package/src/judge-role.ts +4 -0
  73. package/src/navigator-public-session.ts +33 -30
  74. package/src/navigator-work-context.ts +3 -4
  75. package/src/notary-role.ts +1 -0
  76. package/src/notary-source-run.ts +9 -4
  77. package/src/packaged-role-registry.ts +1 -1
  78. package/src/pi/adapter.ts +19 -4
  79. package/src/pi/durable-principal.ts +9 -13
  80. package/src/pi/role-turn-host.ts +48 -14
  81. package/src/public-cli/auto-resume.ts +2 -6
  82. package/src/public-cli/case-dossier-delivery.ts +86 -1
  83. package/src/public-cli/countersign-run.ts +182 -32
  84. package/src/public-cli/diarist-run.ts +34 -26
  85. package/src/public-cli/inspector-run.ts +26 -2
  86. package/src/public-cli/instruction-seat-run.ts +24 -20
  87. package/src/public-cli/invocation.ts +194 -58
  88. package/src/public-cli/notary-run.ts +13 -4
  89. package/src/public-cli/option-definitions.ts +2 -2
  90. package/src/public-cli/post-admission.ts +147 -78
  91. package/src/public-cli/run-lifecycle.ts +258 -65
  92. package/src/public-cli/seat-ticket-binding.ts +3 -6
  93. package/src/public-cli/settlement.ts +38 -10
  94. package/src/public-role-summons.ts +31 -7
  95. package/src/role-envelope.ts +9 -30
  96. package/src/role-run-placement.ts +128 -0
  97. package/src/role-run-relocation.ts +491 -0
  98. package/src/role-runtime-dependencies.ts +1 -0
  99. package/src/role-runtime.ts +106 -24
  100. package/src/run-terminal-artifacts.ts +3 -1
  101. package/src/run-ticket-number.ts +80 -20
  102. package/src/sitian-appender.ts +25 -28
  103. package/src/submission-ledger.ts +148 -30
  104. package/src/ticket-provenance.ts +4 -1
  105. package/src/ticket-trajectory.ts +22 -35
  106. package/src/user-dialogue-stdin.ts +37 -0
  107. package/src/worker-role.ts +8 -4
  108. 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 & {
@@ -131,6 +159,8 @@ export type AdmittedGleanerLeftInvocation = AdmittedRoleInvocationBase & {
131
159
 
132
160
  export type AdmittedInspectorInvocation = AdmittedRoleInvocationBase & {
133
161
  readonly role: "inspector";
162
+ /** Parent run directory (#747 / #879); independent of dialogue instruction. */
163
+ readonly sourceRunPath?: string;
134
164
  };
135
165
 
136
166
  export type AdmittedGatekeeperInvocation = AdmittedRoleInvocationBase & {
@@ -259,30 +289,36 @@ export type AdmissionPlacement = {
259
289
  readonly sessionDirectory: string;
260
290
  readonly sessionFile: string;
261
291
  readonly runDirectory: string;
292
+ readonly artifactsDirectory: string;
293
+ readonly attachmentsDirectory: string;
262
294
  readonly ledgerHome: string;
263
295
  readonly bookKey: string;
264
296
  };
265
297
 
266
- /** Issue principal + derive ledger placement through the injected authority only. */
298
+ /** Issue a principal from the single authoritative role-run placement. */
267
299
  export function issueAdmissionPlacement(
268
300
  authority: DurablePrincipalAuthority,
269
301
  request: {
270
302
  readonly cwd: string;
271
303
  readonly runId: string;
272
304
  readonly role: AdmittedRoleInvocation["role"];
305
+ /** Identity already asserted by 起居郎; never derive this from CLI parameters. */
306
+ readonly subject: RoleRunSubject;
273
307
  readonly home?: string;
274
308
  },
275
309
  ): AdmissionPlacement {
276
- const principal = authority.issue(request);
277
- const { sessionDirectory, sessionFile } = authority.decode(principal);
278
- const runDirectory = join(sessionDirectory, "..");
279
310
  const ledgerHome = resolveActivationLedgerHome(request.home);
280
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);
281
319
  return {
282
- principal,
283
- sessionDirectory,
284
- sessionFile,
285
- runDirectory,
320
+ principal: authority.seal(placement),
321
+ ...placement,
286
322
  ledgerHome,
287
323
  bookKey,
288
324
  };
@@ -474,17 +510,50 @@ export async function bindAdmittedTicketNumber(
474
510
  admitted: AdmittedRoleInvocation,
475
511
  ticketNumber: number,
476
512
  ): Promise<void> {
477
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
478
- throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
479
- }
480
513
  if (admitted.ticketNumber !== undefined) {
481
514
  if (admitted.ticketNumber === ticketNumber) return;
482
515
  throw new Error(
483
516
  `bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`,
484
517
  );
485
518
  }
519
+ await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
486
520
  (admitted as { ticketNumber?: number }).ticketNumber = ticketNumber;
487
- 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]);
488
557
  const admittedPath = admitted.admittedRequestPath;
489
558
  const current = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
490
559
  string,
@@ -492,9 +561,64 @@ export async function bindAdmittedTicketNumber(
492
561
  >;
493
562
  await writeFile(
494
563
  admittedPath,
495
- `${JSON.stringify({ ...current, ticketNumber }, null, 2)}\n`,
564
+ `${JSON.stringify({ ...current, courtTicketNumbers: frozen }, null, 2)}\n`,
496
565
  "utf8",
497
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
+ });
498
622
  }
499
623
 
500
624
  /**
@@ -506,11 +630,10 @@ export async function bindTicketNumberOnRunDirectory(
506
630
  runDirectory: string,
507
631
  ticketNumber: number,
508
632
  ): Promise<void> {
509
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
510
- throw new Error(
511
- `bindTicketNumberOnRunDirectory requires a safe positive integer, got ${String(ticketNumber)}`,
512
- );
513
- }
633
+ requireSafePositiveTicketNumber(
634
+ ticketNumber,
635
+ "bindTicketNumberOnRunDirectory",
636
+ );
514
637
  const admittedPath = join(runDirectory, "admitted-request.json");
515
638
  const invocationPath = join(runDirectory, "invocation.json");
516
639
  const admitted = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
@@ -1087,7 +1210,13 @@ export async function freezeAttachmentsIntoRun(
1087
1210
  function ticketAdmissionFields(
1088
1211
  ticketNumber: number | undefined,
1089
1212
  ): { ticketNumber?: number } {
1090
- return ticketNumber === undefined ? {} : { ticketNumber };
1213
+ if (ticketNumber === undefined) return {};
1214
+ return {
1215
+ ticketNumber: requireSafePositiveTicketNumber(
1216
+ ticketNumber,
1217
+ "assertedTicketNumber",
1218
+ ),
1219
+ };
1091
1220
  }
1092
1221
 
1093
1222
  export type AdmitJudgeInvocationOptions = {
@@ -1105,6 +1234,8 @@ export type AdmitJudgeInvocationOptions = {
1105
1234
 
1106
1235
  export type AdmitInspectorInvocationOptions = AdmitJudgeInvocationOptions & {
1107
1236
  correlationId?: string;
1237
+ /** Typed identity handed off by 起居郎 or inherited from an already-bound run. */
1238
+ assertedTicketNumber?: number;
1108
1239
  };
1109
1240
 
1110
1241
  export type AdmitGatekeeperInvocationOptions = AdmitInspectorInvocationOptions;
@@ -1121,7 +1252,7 @@ async function admitStandardMaterialInvocation<
1121
1252
  R extends "judge" | "inspector" | "gatekeeper" | "navigator" | "auditor" | "diarist",
1122
1253
  >(
1123
1254
  role: R,
1124
- options: AdmitJudgeInvocationOptions & { correlationId?: string },
1255
+ options: AdmitInspectorInvocationOptions,
1125
1256
  ): Promise<AdmittedRoleInvocationBase & { readonly role: R }> {
1126
1257
  // Empty project override must not reach resolve("") → cwd (silent default).
1127
1258
  if (options.project !== undefined) {
@@ -1129,22 +1260,25 @@ async function admitStandardMaterialInvocation<
1129
1260
  }
1130
1261
  const projectRoot = resolve(options.project ?? options.cwd);
1131
1262
  const runId = (options.createRunId ?? uuidv7)();
1263
+ // Validate asserted ticket before placement so 0/NaN never become subjects.
1264
+ const ticketFields = ticketAdmissionFields(options.assertedTicketNumber);
1132
1265
  const {
1133
1266
  principal,
1134
1267
  sessionDirectory,
1135
1268
  sessionFile,
1136
1269
  runDirectory,
1270
+ attachmentsDirectory,
1137
1271
  ledgerHome,
1138
1272
  bookKey,
1139
1273
  } = issueAdmissionPlacement(options.principalAuthority, {
1140
1274
  cwd: projectRoot,
1141
1275
  runId,
1142
1276
  role,
1277
+ subject: ticketFields.ticketNumber === undefined
1278
+ ? { unbound: true }
1279
+ : { ticketNumber: ticketFields.ticketNumber },
1143
1280
  home: options.home,
1144
1281
  });
1145
- const attachmentsDirectory = join(runDirectory, "attachments");
1146
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1147
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1148
1282
 
1149
1283
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1150
1284
  const correlationFields =
@@ -1171,6 +1305,7 @@ async function admitStandardMaterialInvocation<
1171
1305
  sha256: a.sha256,
1172
1306
  mediaKind: a.mediaKind,
1173
1307
  })),
1308
+ ...ticketFields,
1174
1309
  };
1175
1310
  const admittedRequestPath = join(runDirectory, "admitted-request.json");
1176
1311
  await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
@@ -1195,6 +1330,7 @@ async function admitStandardMaterialInvocation<
1195
1330
  principal,
1196
1331
  admittedRequestPath,
1197
1332
  ...correlationFields,
1333
+ ...ticketFields,
1198
1334
  };
1199
1335
  }
1200
1336
 
@@ -1322,17 +1458,16 @@ export async function admitCountersignInvocation(
1322
1458
  sessionDirectory,
1323
1459
  sessionFile,
1324
1460
  runDirectory,
1461
+ attachmentsDirectory,
1325
1462
  ledgerHome,
1326
1463
  bookKey,
1327
1464
  } = issueAdmissionPlacement(options.principalAuthority, {
1328
1465
  cwd: projectRoot,
1329
1466
  runId,
1330
1467
  role: "countersign",
1468
+ subject: { unbound: true },
1331
1469
  home: options.home,
1332
1470
  });
1333
- const attachmentsDirectory = join(runDirectory, "attachments");
1334
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1335
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1336
1471
 
1337
1472
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1338
1473
  // Ticket binding is LLM-only post-admission (#635); admission stays unbound.
@@ -1416,9 +1551,11 @@ export async function loadAdmittedJudgeRequest(
1416
1551
  }
1417
1552
 
1418
1553
  export async function ensureRunArtifactsDir(runDirectory: string): Promise<string> {
1419
- const dir = join(runDirectory, "artifacts");
1420
- await mkdir(dir, { recursive: true });
1421
- return dir;
1554
+ const directory = roleRunArtifactsDirectory(runDirectory);
1555
+ return ensureRoleRunDirectory(
1556
+ resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)),
1557
+ directory,
1558
+ );
1422
1559
  }
1423
1560
 
1424
1561
  export type AdmitCoderInvocationOptions = {
@@ -1462,17 +1599,16 @@ export async function admitCoderInvocation(
1462
1599
  sessionDirectory,
1463
1600
  sessionFile,
1464
1601
  runDirectory,
1602
+ attachmentsDirectory,
1465
1603
  ledgerHome,
1466
1604
  bookKey,
1467
1605
  } = issueAdmissionPlacement(options.principalAuthority, {
1468
1606
  cwd: projectRoot,
1469
1607
  runId,
1470
1608
  role: "coder",
1609
+ subject: { unbound: true },
1471
1610
  home: options.home,
1472
1611
  });
1473
- const attachmentsDirectory = join(runDirectory, "attachments");
1474
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1475
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1476
1612
 
1477
1613
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1478
1614
 
@@ -1609,17 +1745,16 @@ export async function admitFixerInvocation(
1609
1745
  sessionDirectory,
1610
1746
  sessionFile,
1611
1747
  runDirectory,
1748
+ attachmentsDirectory,
1612
1749
  ledgerHome,
1613
1750
  bookKey,
1614
1751
  } = issueAdmissionPlacement(options.principalAuthority, {
1615
1752
  cwd: projectRoot,
1616
1753
  runId,
1617
1754
  role: "fixer",
1755
+ subject: { unbound: true },
1618
1756
  home: options.home,
1619
1757
  });
1620
- const attachmentsDirectory = join(runDirectory, "attachments");
1621
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1622
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1623
1758
 
1624
1759
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1625
1760
 
@@ -1916,17 +2051,16 @@ export async function admitCollectorInvocation(
1916
2051
  sessionDirectory,
1917
2052
  sessionFile,
1918
2053
  runDirectory,
2054
+ attachmentsDirectory,
1919
2055
  ledgerHome,
1920
2056
  bookKey,
1921
2057
  } = issueAdmissionPlacement(options.principalAuthority, {
1922
2058
  cwd: projectRoot,
1923
2059
  runId,
1924
2060
  role: "collector",
2061
+ subject: { unbound: true },
1925
2062
  home: options.home,
1926
2063
  });
1927
- const attachmentsDirectory = join(runDirectory, "attachments");
1928
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1929
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1930
2064
 
1931
2065
  // #676 A: freeze task materials BEFORE target resolution so the role receives
1932
2066
  // real instruction + attachments. Admission binds only explicit --pr or unique
@@ -2013,8 +2147,9 @@ export function buildCollectorTransportPrompt(
2013
2147
  const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
2014
2148
 
2015
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. */
2016
2151
  const DOCTOR_CASE_RUNS_PATH_PATTERN =
2017
- /\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/;
2152
+ /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
2018
2153
 
2019
2154
  /**
2020
2155
  * Parse a positive Issue number for public Doctor admission.
@@ -2129,9 +2264,9 @@ export async function resolveDoctorCaseRunsPath(options: {
2129
2264
  runs?: string;
2130
2265
  }): Promise<string> {
2131
2266
  const ledgerHome = resolveActivationLedgerHome(options.home);
2267
+ // Canonical topology: <book>/<ticket>/runs (docs/dossier-topology.md).
2132
2268
  const defaultRuns = join(
2133
2269
  activationBookDirectory(ledgerHome, options.bookKey),
2134
- "issues",
2135
2270
  String(options.issueNumber),
2136
2271
  "runs",
2137
2272
  );
@@ -2175,7 +2310,7 @@ export async function resolveDoctorCaseRunsPath(options: {
2175
2310
  const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
2176
2311
  if (!match) {
2177
2312
  throw new CliUsageError(
2178
- "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",
2179
2314
  );
2180
2315
  }
2181
2316
  if (Number(match[1]) !== options.issueNumber) {
@@ -2215,12 +2350,14 @@ export async function admitDoctorInvocation(
2215
2350
  sessionDirectory,
2216
2351
  sessionFile,
2217
2352
  runDirectory,
2353
+ attachmentsDirectory,
2218
2354
  ledgerHome,
2219
2355
  bookKey,
2220
2356
  } = issueAdmissionPlacement(options.principalAuthority, {
2221
2357
  cwd: projectRoot,
2222
2358
  runId,
2223
2359
  role: "doctor",
2360
+ subject: { unbound: true },
2224
2361
  home: options.home,
2225
2362
  });
2226
2363
 
@@ -2264,9 +2401,6 @@ export async function admitDoctorInvocation(
2264
2401
  );
2265
2402
  }
2266
2403
 
2267
- const attachmentsDirectory = join(runDirectory, "attachments");
2268
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2269
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2270
2404
 
2271
2405
  const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
2272
2406
 
@@ -2421,24 +2555,27 @@ export async function admitNotaryInvocation(options: {
2421
2555
  throw error;
2422
2556
  }
2423
2557
 
2558
+ // Notary inherits board identity only — migration-derived stays display/placement.
2559
+ const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
2424
2560
  const runId = options.createRunId?.() ?? uuidv7();
2425
2561
  const {
2426
2562
  principal,
2427
2563
  sessionDirectory,
2428
2564
  sessionFile,
2429
2565
  runDirectory,
2566
+ attachmentsDirectory,
2430
2567
  ledgerHome,
2431
2568
  bookKey,
2432
2569
  } = issueAdmissionPlacement(options.principalAuthority, {
2433
2570
  cwd: projectRoot,
2434
2571
  runId,
2435
2572
  role: "notary",
2573
+ subject: inheritedTicketNumber === undefined
2574
+ ? { unbound: true }
2575
+ : { ticketNumber: inheritedTicketNumber },
2436
2576
  home: options.home,
2437
2577
  });
2438
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2439
- const ticketFields = ticketAdmissionFields(
2440
- await readRunTicketNumber(sourceRun.runDirectory),
2441
- );
2578
+ const ticketFields = ticketAdmissionFields(inheritedTicketNumber);
2442
2579
 
2443
2580
  const admitted = {
2444
2581
  role: "notary" as const,
@@ -2580,15 +2717,16 @@ export async function admitGleanerLeftInvocation(
2580
2717
  sessionDirectory,
2581
2718
  sessionFile,
2582
2719
  runDirectory,
2720
+ attachmentsDirectory,
2583
2721
  ledgerHome,
2584
2722
  bookKey,
2585
2723
  } = issueAdmissionPlacement(options.principalAuthority, {
2586
2724
  cwd: projectRoot,
2587
2725
  runId,
2588
2726
  role: "gleaner-left",
2727
+ subject: { unbound: true },
2589
2728
  home: options.home,
2590
2729
  });
2591
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2592
2730
 
2593
2731
  const instruction = options.instruction;
2594
2732
  const instructionEmpty = instruction.trim() === "";
@@ -2744,17 +2882,16 @@ export async function admitReviewerInvocation(
2744
2882
  sessionDirectory,
2745
2883
  sessionFile,
2746
2884
  runDirectory,
2885
+ attachmentsDirectory,
2747
2886
  ledgerHome,
2748
2887
  bookKey,
2749
2888
  } = issueAdmissionPlacement(options.principalAuthority, {
2750
2889
  cwd: projectRoot,
2751
2890
  runId,
2752
2891
  role: "reviewer",
2892
+ subject: { unbound: true },
2753
2893
  home: options.home,
2754
2894
  });
2755
- const attachmentsDirectory = join(runDirectory, "attachments");
2756
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2757
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2758
2895
 
2759
2896
  // Public parse already rejects attachments; keep freeze loop for structural symmetry.
2760
2897
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
@@ -2953,17 +3090,16 @@ export async function admitMergerInvocation(
2953
3090
  sessionDirectory,
2954
3091
  sessionFile,
2955
3092
  runDirectory,
3093
+ attachmentsDirectory,
2956
3094
  ledgerHome,
2957
3095
  bookKey,
2958
3096
  } = issueAdmissionPlacement(options.principalAuthority, {
2959
3097
  cwd: projectRoot,
2960
3098
  runId,
2961
3099
  role: "merger",
3100
+ subject: { unbound: true },
2962
3101
  home: options.home,
2963
3102
  });
2964
- const attachmentsDirectory = join(runDirectory, "attachments");
2965
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
2966
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
2967
3103
 
2968
3104
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
2969
3105
 
@@ -52,6 +52,12 @@ export type NotaryRunEnv = PostAdmissionEnv & {
52
52
  * Never a public CLI argv — external callers still have zero prompt.
53
53
  */
54
54
  reviewReask?: string;
55
+ /**
56
+ * #879 gate summons: verbatim parent-submission body for officer dialogue content.
57
+ * Rides summons.instruction / first-mint prompt when reviewReask is absent.
58
+ * Binding pointer stays --source-run (independent machine material).
59
+ */
60
+ gateReviewInstruction?: string;
55
61
  };
56
62
 
57
63
  /** Project admitted invocation onto the host-neutral turn request. */
@@ -115,10 +121,10 @@ export async function runPublicNotary(
115
121
  throw error;
116
122
  }
117
123
  // #747: officer resume key is this parent source-run path (not ticket number).
118
- // #753/#786: gate re-ask and verbatim submission body share summons.instruction
119
- // (reask wins when both present; no parallel stack).
124
+ // #753/#879: gate re-ask and verbatim submission body share summons.instruction
125
+ // (reask wins when both present; no parallel stack). Binding stays sourceRunPath.
120
126
  {
121
- const resumeInstruction = env.reviewReask;
127
+ const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
122
128
  const summons: SameTicketSummonsMaterials = {
123
129
  sourceRunPath: source.runDirectory,
124
130
  sourceRun: source,
@@ -181,7 +187,10 @@ export async function runPublicNotary(
181
187
  : { correlationId: env.correlationId }),
182
188
  continuation: {
183
189
  kind: "initial",
184
- prompt: buildNotaryTransportPrompt(admitted, engineMaterial),
190
+ // #879: gate path first mint carries parent payload as dialogue content;
191
+ // external zero-prompt kickoff unchanged when no body/reask.
192
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
193
+ ?? buildNotaryTransportPrompt(admitted, engineMaterial),
185
194
  },
186
195
  });
187
196
 
@@ -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[];