@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
@@ -4,12 +4,15 @@
4
4
  */
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
- import { lstat, mkdir, readFile, realpath, writeFile, } from "node:fs/promises";
8
- import { basename, isAbsolute, join, resolve, sep } from "node:path";
7
+ import { lstat, readFile, realpath, rename, writeFile, } from "node:fs/promises";
8
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
9
9
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
10
10
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
11
- import { readRunTicketNumber } from "../run-ticket-number.js";
11
+ import { ensureRoleRunDirectory, ensureRoleRunPlacement, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
12
+ import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
13
+ import { rewriteRoleRunDurablePages, rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
12
14
  import { loadDoctorCase, } from "../doctor-evidence.js";
15
+ import { projectCourtTicketNumbers } from "../diarist-contracts.js";
13
16
  import { emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
14
17
  import { resolveCollectorTarget } from "../collector-target.js";
15
18
  import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.js";
@@ -23,18 +26,20 @@ import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-
23
26
  import { appendEngineSessionMaterial, } from "../package-resources/engine-material.js";
24
27
  import { CliUsageError } from "./cli-errors.js";
25
28
  import { REJECTED_PUBLIC_SPELLINGS, createTypedOptionConsumer, evaluateAnalystModeOptionContract, optionsForOwner, resolveAnalystMode, } from "./option-definitions.js";
26
- /** Issue principal + derive ledger placement through the injected authority only. */
29
+ /** Issue a principal from the single authoritative role-run placement. */
27
30
  export function issueAdmissionPlacement(authority, request) {
28
- const principal = authority.issue(request);
29
- const { sessionDirectory, sessionFile } = authority.decode(principal);
30
- const runDirectory = join(sessionDirectory, "..");
31
31
  const ledgerHome = resolveActivationLedgerHome(request.home);
32
32
  const bookKey = resolveBookKeyFromGit(request.cwd);
33
+ const placement = roleRunPlacement(ledgerHome, {
34
+ bookKey,
35
+ subject: request.subject,
36
+ runId: request.runId,
37
+ role: request.role,
38
+ });
39
+ ensureRoleRunPlacement(ledgerHome, placement);
33
40
  return {
34
- principal,
35
- sessionDirectory,
36
- sessionFile,
37
- runDirectory,
41
+ principal: authority.seal(placement),
42
+ ...placement,
38
43
  ledgerHome,
39
44
  bookKey,
40
45
  };
@@ -159,19 +164,81 @@ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
159
164
  * Never clears an existing binding.
160
165
  */
161
166
  export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
162
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
163
- throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
164
- }
165
167
  if (admitted.ticketNumber !== undefined) {
166
168
  if (admitted.ticketNumber === ticketNumber)
167
169
  return;
168
170
  throw new Error(`bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`);
169
171
  }
172
+ await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
170
173
  admitted.ticketNumber = ticketNumber;
171
- await mergeInvocationIdentityPage(admitted.runDirectory, { ticketNumber });
174
+ }
175
+ /**
176
+ * #871: persist the typed co-review set as a countersign run fact (admitted-request +
177
+ * invocation.json). Whole-set replace — never union with a prior set. Main ticket
178
+ * binding stays on `ticketNumber` alone. Projection reuses the sole contract helper;
179
+ * principal membership is mandatory.
180
+ */
181
+ export async function bindCourtTicketNumbersOnAdmitted(admitted, courtTicketNumbers) {
182
+ if (admitted.ticketNumber === undefined) {
183
+ throw new Error("bindCourtTicketNumbersOnAdmitted requires a bound principal ticketNumber");
184
+ }
185
+ const principal = requireSafePositiveTicketNumber(admitted.ticketNumber, "bindCourtTicketNumbersOnAdmitted principal ticketNumber");
186
+ // Strict write path: every member must already be a lawful number (no soft filter).
187
+ for (const item of courtTicketNumbers) {
188
+ if (!isSafePositiveTicketNumber(item)) {
189
+ throw new Error(`bindCourtTicketNumbersOnAdmitted requires safe positive integers, got ${String(item)}`);
190
+ }
191
+ }
192
+ const projected = projectCourtTicketNumbers(courtTicketNumbers, {
193
+ principalTicket: principal,
194
+ });
195
+ if (projected === null || projected.length === 0) {
196
+ throw new Error("bindCourtTicketNumbersOnAdmitted requires a non-empty ticket set");
197
+ }
198
+ const frozen = Object.freeze([...projected]);
172
199
  const admittedPath = admitted.admittedRequestPath;
173
200
  const current = JSON.parse(await readFile(admittedPath, "utf8"));
174
- await writeFile(admittedPath, `${JSON.stringify({ ...current, ticketNumber }, null, 2)}\n`, "utf8");
201
+ await writeFile(admittedPath, `${JSON.stringify({ ...current, courtTicketNumbers: frozen }, null, 2)}\n`, "utf8");
202
+ await mergeInvocationIdentityPage(admitted.runDirectory, {
203
+ courtTicketNumbers: frozen,
204
+ });
205
+ admitted.courtTicketNumbers = frozen;
206
+ }
207
+ /** Move a settled first-entry run from unbound to its asserted ticket directory. */
208
+ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease) {
209
+ if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`))
210
+ return;
211
+ const oldRunDirectory = admitted.runDirectory;
212
+ const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
213
+ const target = roleRunPlacement(ledgerHome, {
214
+ bookKey: admitted.bookKey,
215
+ subject: { ticketNumber: admitted.ticketNumber },
216
+ runId: admitted.runId,
217
+ role: admitted.role,
218
+ });
219
+ ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
220
+ await rename(oldRunDirectory, target.runDirectory);
221
+ heldLease?.relocate(target.runDirectory);
222
+ const admittedRecord = admitted;
223
+ rewriteRunDirectoryPathFields(admittedRecord, [
224
+ "runDirectory",
225
+ "admittedRequestPath",
226
+ "taskPath",
227
+ "packetPath",
228
+ "prerequisitesPath",
229
+ "requestManifestPath",
230
+ "mergerInputPath",
231
+ ], oldRunDirectory, target.runDirectory);
232
+ for (const attachment of admitted.attachments) {
233
+ attachment.frozenPath = rewriteRunDirectoryPathValue(attachment.frozenPath, oldRunDirectory, target.runDirectory);
234
+ }
235
+ const principal = authority.seal(target);
236
+ admitted.principal = principal;
237
+ await rewriteRoleRunDurablePages({
238
+ pagesDirectory: target.runDirectory,
239
+ oldRunDirectory,
240
+ newRunDirectory: target.runDirectory,
241
+ });
175
242
  }
176
243
  /**
177
244
  * Bind ticket identity onto a run directory's durable pages when the admitted
@@ -179,9 +246,7 @@ export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
179
246
  * Idempotent when the same number is already on the pages.
180
247
  */
181
248
  export async function bindTicketNumberOnRunDirectory(runDirectory, ticketNumber) {
182
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
183
- throw new Error(`bindTicketNumberOnRunDirectory requires a safe positive integer, got ${String(ticketNumber)}`);
184
- }
249
+ requireSafePositiveTicketNumber(ticketNumber, "bindTicketNumberOnRunDirectory");
185
250
  const admittedPath = join(runDirectory, "admitted-request.json");
186
251
  const invocationPath = join(runDirectory, "invocation.json");
187
252
  const admitted = JSON.parse(await readFile(admittedPath, "utf8"));
@@ -526,7 +591,11 @@ export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, su
526
591
  return freezeAttachments(attachmentPaths, attachmentsDirectory);
527
592
  }
528
593
  function ticketAdmissionFields(ticketNumber) {
529
- return ticketNumber === undefined ? {} : { ticketNumber };
594
+ if (ticketNumber === undefined)
595
+ return {};
596
+ return {
597
+ ticketNumber: requireSafePositiveTicketNumber(ticketNumber, "assertedTicketNumber"),
598
+ };
530
599
  }
531
600
  /**
532
601
  * Shared instruction-seat admission for Judge and Inspector: project check,
@@ -541,15 +610,17 @@ async function admitStandardMaterialInvocation(role, options) {
541
610
  }
542
611
  const projectRoot = resolve(options.project ?? options.cwd);
543
612
  const runId = (options.createRunId ?? uuidv7)();
544
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
613
+ // Validate asserted ticket before placement so 0/NaN never become subjects.
614
+ const ticketFields = ticketAdmissionFields(options.assertedTicketNumber);
615
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
545
616
  cwd: projectRoot,
546
617
  runId,
547
618
  role,
619
+ subject: ticketFields.ticketNumber === undefined
620
+ ? { unbound: true }
621
+ : { ticketNumber: ticketFields.ticketNumber },
548
622
  home: options.home,
549
623
  });
550
- const attachmentsDirectory = join(runDirectory, "attachments");
551
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
552
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
553
624
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
554
625
  const correlationFields = options.correlationId === undefined
555
626
  ? {}
@@ -573,6 +644,7 @@ async function admitStandardMaterialInvocation(role, options) {
573
644
  sha256: a.sha256,
574
645
  mediaKind: a.mediaKind,
575
646
  })),
647
+ ...ticketFields,
576
648
  };
577
649
  const admittedRequestPath = join(runDirectory, "admitted-request.json");
578
650
  await writeAdmittedRequestPersistence(admittedRequestPath, admitted, {
@@ -592,6 +664,7 @@ async function admitStandardMaterialInvocation(role, options) {
592
664
  principal,
593
665
  admittedRequestPath,
594
666
  ...correlationFields,
667
+ ...ticketFields,
595
668
  };
596
669
  }
597
670
  /**
@@ -664,15 +737,13 @@ export async function admitCountersignInvocation(options) {
664
737
  }
665
738
  const projectRoot = resolve(options.project ?? options.cwd);
666
739
  const runId = (options.createRunId ?? uuidv7)();
667
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
740
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
668
741
  cwd: projectRoot,
669
742
  runId,
670
743
  role: "countersign",
744
+ subject: { unbound: true },
671
745
  home: options.home,
672
746
  });
673
- const attachmentsDirectory = join(runDirectory, "attachments");
674
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
675
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
676
747
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
677
748
  // Ticket binding is LLM-only post-admission (#635); admission stays unbound.
678
749
  const instruction = options.instruction;
@@ -745,9 +816,8 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
745
816
  }
746
817
  }
747
818
  export async function ensureRunArtifactsDir(runDirectory) {
748
- const dir = join(runDirectory, "artifacts");
749
- await mkdir(dir, { recursive: true });
750
- return dir;
819
+ const directory = roleRunArtifactsDirectory(runDirectory);
820
+ return ensureRoleRunDirectory(resolveActivationLedgerHome(homeFromRunDirectory(runDirectory)), directory);
751
821
  }
752
822
  /**
753
823
  * Admit a Coder Role run on the common Invocation request.
@@ -767,15 +837,13 @@ export async function admitCoderInvocation(options) {
767
837
  }
768
838
  const projectRoot = resolve(options.project ?? options.cwd);
769
839
  const runId = (options.createRunId ?? uuidv7)();
770
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
840
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
771
841
  cwd: projectRoot,
772
842
  runId,
773
843
  role: "coder",
844
+ subject: { unbound: true },
774
845
  home: options.home,
775
846
  });
776
- const attachmentsDirectory = join(runDirectory, "attachments");
777
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
778
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
779
847
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
780
848
  const taskPath = join(runDirectory, "task.md");
781
849
  await writeFile(taskPath, instruction, "utf8");
@@ -876,15 +944,13 @@ export async function admitFixerInvocation(options) {
876
944
  }
877
945
  const projectRoot = resolve(options.project ?? options.cwd);
878
946
  const runId = (options.createRunId ?? uuidv7)();
879
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
947
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
880
948
  cwd: projectRoot,
881
949
  runId,
882
950
  role: "fixer",
951
+ subject: { unbound: true },
883
952
  home: options.home,
884
953
  });
885
- const attachmentsDirectory = join(runDirectory, "attachments");
886
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
887
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
888
954
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
889
955
  let prerequisitesPath;
890
956
  if (prerequisitesSource !== undefined) {
@@ -1138,15 +1204,13 @@ export async function admitCollectorInvocation(options) {
1138
1204
  }
1139
1205
  const manifestDigest = manifest.digest;
1140
1206
  const runId = (options.createRunId ?? uuidv7)();
1141
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1207
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1142
1208
  cwd: projectRoot,
1143
1209
  runId,
1144
1210
  role: "collector",
1211
+ subject: { unbound: true },
1145
1212
  home: options.home,
1146
1213
  });
1147
- const attachmentsDirectory = join(runDirectory, "attachments");
1148
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1149
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1150
1214
  // #676 A: freeze task materials BEFORE target resolution so the role receives
1151
1215
  // real instruction + attachments. Admission binds only explicit --pr or unique
1152
1216
  // head/commit association — task-text scrape is not a target lock.
@@ -1223,7 +1287,8 @@ export function buildCollectorTransportPrompt(admitted, engineMaterial) {
1223
1287
  /** Positive Issue number grammar shared with Doctor case path identity. */
1224
1288
  const DOCTOR_ISSUE_NUMBER_PATTERN = /^[1-9]\d*$/;
1225
1289
  /** Match retained Doctor case runs roots (ADR 0017 / loadDoctorCase). */
1226
- const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/issues\/([1-9]\d*)\/runs$/;
1290
+ /** Canonical Doctor case: `<book>/<ticket>/runs`. Legacy `issues/<n>/runs` is read-only compat. */
1291
+ const DOCTOR_CASE_RUNS_PATH_PATTERN = /\/\.ak-roles\/books\/[^/]+\/(?:issues\/)?([1-9]\d*)\/runs$/;
1227
1292
  /**
1228
1293
  * Parse a positive Issue number for public Doctor admission.
1229
1294
  * Leading zeros and non-integers are structural rejects.
@@ -1308,7 +1373,8 @@ export function parseDoctorArgv(args) {
1308
1373
  */
1309
1374
  export async function resolveDoctorCaseRunsPath(options) {
1310
1375
  const ledgerHome = resolveActivationLedgerHome(options.home);
1311
- const defaultRuns = join(activationBookDirectory(ledgerHome, options.bookKey), "issues", String(options.issueNumber), "runs");
1376
+ // Canonical topology: <book>/<ticket>/runs (docs/dossier-topology.md).
1377
+ const defaultRuns = join(activationBookDirectory(ledgerHome, options.bookKey), String(options.issueNumber), "runs");
1312
1378
  if (options.runs === undefined) {
1313
1379
  return defaultRuns;
1314
1380
  }
@@ -1336,7 +1402,7 @@ export async function resolveDoctorCaseRunsPath(options) {
1336
1402
  const normalized = real.split(sep).join("/");
1337
1403
  const match = normalized.match(DOCTOR_CASE_RUNS_PATH_PATTERN);
1338
1404
  if (!match) {
1339
- throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/issues/<n>/runs directory");
1405
+ throw new CliUsageError("doctor --runs must be an .ak-roles/books/<book>/<n>/runs directory");
1340
1406
  }
1341
1407
  if (Number(match[1]) !== options.issueNumber) {
1342
1408
  throw new CliUsageError(`doctor --runs issue ${match[1]} does not match --issue ${options.issueNumber}`);
@@ -1360,10 +1426,11 @@ export async function admitDoctorInvocation(options) {
1360
1426
  }
1361
1427
  const projectRoot = resolve(options.project ?? options.cwd);
1362
1428
  const runId = (options.createRunId ?? uuidv7)();
1363
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1429
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1364
1430
  cwd: projectRoot,
1365
1431
  runId,
1366
1432
  role: "doctor",
1433
+ subject: { unbound: true },
1367
1434
  home: options.home,
1368
1435
  });
1369
1436
  let caseRunsPath;
@@ -1402,9 +1469,6 @@ export async function admitDoctorInvocation(options) {
1402
1469
  const detail = error instanceof Error ? error.message : String(error);
1403
1470
  throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
1404
1471
  }
1405
- const attachmentsDirectory = join(runDirectory, "attachments");
1406
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1407
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1408
1472
  const attachments = await freezeAttachments(options.attachmentPaths ?? [], attachmentsDirectory);
1409
1473
  const instruction = options.instruction ?? "";
1410
1474
  const instructionEmpty = instruction.trim() === "";
@@ -1529,15 +1593,19 @@ export async function admitNotaryInvocation(options) {
1529
1593
  }
1530
1594
  throw error;
1531
1595
  }
1596
+ // Notary inherits board identity only — migration-derived stays display/placement.
1597
+ const inheritedTicketNumber = await readBoardTicketNumber(sourceRun.runDirectory);
1532
1598
  const runId = options.createRunId?.() ?? uuidv7();
1533
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1599
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1534
1600
  cwd: projectRoot,
1535
1601
  runId,
1536
1602
  role: "notary",
1603
+ subject: inheritedTicketNumber === undefined
1604
+ ? { unbound: true }
1605
+ : { ticketNumber: inheritedTicketNumber },
1537
1606
  home: options.home,
1538
1607
  });
1539
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1540
- const ticketFields = ticketAdmissionFields(await readRunTicketNumber(sourceRun.runDirectory));
1608
+ const ticketFields = ticketAdmissionFields(inheritedTicketNumber);
1541
1609
  const admitted = {
1542
1610
  role: "notary",
1543
1611
  runId,
@@ -1647,13 +1715,13 @@ export async function admitGleanerLeftInvocation(options) {
1647
1715
  }
1648
1716
  const projectRoot = resolve(options.project ?? options.cwd);
1649
1717
  const runId = (options.createRunId ?? uuidv7)();
1650
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1718
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1651
1719
  cwd: projectRoot,
1652
1720
  runId,
1653
1721
  role: "gleaner-left",
1722
+ subject: { unbound: true },
1654
1723
  home: options.home,
1655
1724
  });
1656
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1657
1725
  const instruction = options.instruction;
1658
1726
  const instructionEmpty = instruction.trim() === "";
1659
1727
  const admitted = {
@@ -1767,15 +1835,13 @@ export async function admitReviewerInvocation(options) {
1767
1835
  const authorityRefs = Object.freeze((options.authorityRefs ?? []).map((ref) => requireAuthorityRef(ref)));
1768
1836
  const projectRoot = resolve(options.project ?? options.cwd);
1769
1837
  const runId = (options.createRunId ?? uuidv7)();
1770
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1838
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1771
1839
  cwd: projectRoot,
1772
1840
  runId,
1773
1841
  role: "reviewer",
1842
+ subject: { unbound: true },
1774
1843
  home: options.home,
1775
1844
  });
1776
- const attachmentsDirectory = join(runDirectory, "attachments");
1777
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1778
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1779
1845
  // Public parse already rejects attachments; keep freeze loop for structural symmetry.
1780
1846
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1781
1847
  const instruction = options.instruction;
@@ -1928,15 +1994,13 @@ export async function admitMergerInvocation(options) {
1928
1994
  const projectRoot = resolve(options.project ?? options.cwd);
1929
1995
  const derived = await deriveMergerEnvelopeFromActiveMerge(projectRoot, options.gitState ?? createProductionMergerGitState(projectRoot));
1930
1996
  const runId = (options.createRunId ?? uuidv7)();
1931
- const { principal, sessionDirectory, sessionFile, runDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1997
+ const { principal, sessionDirectory, sessionFile, runDirectory, attachmentsDirectory, ledgerHome, bookKey, } = issueAdmissionPlacement(options.principalAuthority, {
1932
1998
  cwd: projectRoot,
1933
1999
  runId,
1934
2000
  role: "merger",
2001
+ subject: { unbound: true },
1935
2002
  home: options.home,
1936
2003
  });
1937
- const attachmentsDirectory = join(runDirectory, "attachments");
1938
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1939
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1940
2004
  const attachments = await freezeAttachments(options.attachmentPaths, attachmentsDirectory);
1941
2005
  // Intent materials seed primary-source investigation; the method owns the work.
1942
2006
  const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;