@akagilnc/pi-workflow-roles 0.1.5614 → 0.1.5650

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 (35) hide show
  1. package/dist/acp-host/production-host.js +960 -465
  2. package/dist/activation-ledger-topology.js +12 -0
  3. package/dist/external-host-turn-loop.js +281 -0
  4. package/dist/headless-host/production-host.js +900 -406
  5. package/dist/migrate-book-topology.js +8 -5
  6. package/dist/pi/role-turn-host.js +33 -3
  7. package/dist/public-cli/instruction-seat-run.js +177 -46
  8. package/dist/public-cli/invocation.js +5 -12
  9. package/dist/public-cli/main.js +541 -165
  10. package/dist/public-cli/post-admission.js +171 -18
  11. package/dist/public-cli/settlement.js +20 -10
  12. package/dist/report-ticket-tool.js +136 -0
  13. package/dist/role-run-placement.js +1 -2
  14. package/dist/role-run-relocation.js +24 -4
  15. package/package.json +1 -1
  16. package/resources/engine-dispatch.md +6 -3
  17. package/resources/engines/claude-code.md +4 -5
  18. package/resources/missing-ticket-reask.md +1 -0
  19. package/src/acp-host/role-turn-host.ts +41 -31
  20. package/src/activation-ledger-topology.ts +16 -0
  21. package/src/external-host-turn-loop.ts +103 -12
  22. package/src/headless-host/role-turn-host.ts +41 -25
  23. package/src/pi/role-turn-host.ts +37 -3
  24. package/src/public-cli/instruction-seat-run.ts +248 -50
  25. package/src/public-cli/invocation.ts +15 -27
  26. package/src/public-cli/post-admission.ts +250 -33
  27. package/src/public-cli/run-lifecycle.ts +5 -5
  28. package/src/public-cli/settlement.ts +38 -11
  29. package/src/public-cli/terminal.ts +6 -0
  30. package/src/report-ticket-tool.ts +176 -0
  31. package/src/role-envelope.ts +28 -3
  32. package/src/role-run-placement.ts +0 -2
  33. package/src/role-run-relocation.ts +45 -4
  34. package/src/role-runtime.ts +6 -0
  35. package/src/worker-submission-gates.ts +26 -3
@@ -4,7 +4,10 @@
4
4
  * starts two ordinary single-axis runs here.
5
5
  */
6
6
  import { dirname, resolve } from "node:path";
7
+ import { MISSING_TICKET_REASK_MATERIAL } from "../report-ticket-tool.ts";
7
8
  import { isUnboundRunDirectory, sessionFileOf } from "../role-run-placement.ts";
9
+ import { rewriteRunDirectoryPathValue } from "../role-run-relocation.ts";
10
+ import { readPackageMaterial } from "../session-opening-materials.ts";
8
11
  import { bookDirectOfficerRunPointer, readDirectOfficerRunPointer } from "../archivist-record-pointer.ts";
9
12
  import { createPiDoctorAuditor } from "../doctor-auditor.ts";
10
13
 
@@ -26,7 +29,10 @@ import {
26
29
  latestQueuePayload,
27
30
  latestQueueStatus,
28
31
  } from "../submission-gate.ts";
29
- import { isSafePositiveTicketNumber, readBoardTicketNumber } from "../run-ticket-number.ts";
32
+ import {
33
+ isSafePositiveTicketNumber,
34
+ readBoardTicketNumber,
35
+ } from "../run-ticket-number.ts";
30
36
  import type { NotarySourceRunLocator } from "../notary-contracts.ts";
31
37
  import { NotarySourceRunError, resolveNotarySourceRunLocator } from "../notary-source-run.ts";
32
38
  import type { PackagedRole } from "../packaged-role-registry.ts";
@@ -173,10 +179,21 @@ function unreadablePostSubmissionStatus(
173
179
  * status: the caller presents the volumes already in hand and does not advance
174
180
  * review. ADR 0007 audit continues are not this shape.
175
181
  */
182
+ /**
183
+ * Drop turn-scoped soft-reask identity; keep cross-turn budgets and ownership cell.
184
+ * #1171 F2-R5 / F2-R9: ordinary status reask and court continue must not inherit
185
+ * softTicketReaskTurn (that flag only governs the soft-reask turn's own settle).
186
+ */
187
+ function withoutSoftTicketReaskTurn(env: InstructionSeatRunEnv): InstructionSeatRunEnv {
188
+ const { softTicketReaskTurn: _softTicketReaskTurn, ...rest } = env;
189
+ return rest;
190
+ }
191
+
176
192
  function withUnreadableReask(env: InstructionSeatRunEnv): InstructionSeatRunEnv | undefined {
177
193
  const spent = env.unreadableReasksSpent ?? 0;
178
194
  if (spent >= deliveryLimitFromConfig(env.autoResumeLimit)) return undefined;
179
- return { ...env, unreadableReasksSpent: spent + 1 };
195
+ // Ordinary status reask: strip soft-reask turn identity; keep spent budget (#1171 F2-R9).
196
+ return { ...withoutSoftTicketReaskTurn(env), unreadableReasksSpent: spent + 1 };
180
197
  }
181
198
 
182
199
  /** Budget spent on an unreadable status. Distinct from "status is readable". */
@@ -270,6 +287,203 @@ async function reaskUnreadablePostSubmissionStatus(
270
287
  );
271
288
  }
272
289
 
290
+ /**
291
+ * Soft-reask call outcome at the missing-ticket seam (#1171 F2-R6 / F2-R7).
292
+ * Ownership is decided here once from this nested chain's settlement evidence
293
+ * (soft-reask turn sealed → ownership cell; or returned terminal still carries volume);
294
+ * callers must not re-judge the same criterion.
295
+ */
296
+ type SoftTicketReaskOutcome =
297
+ | { readonly kind: "none" }
298
+ | { readonly kind: "nested_owns"; readonly result: SeatRunResult }
299
+ | { readonly kind: "no_substitute"; readonly result: SeatRunResult };
300
+
301
+ /**
302
+ * #1171: sealed submission still leaves the leg unbound → soft reask once.
303
+ * Does not reject the sealed receipt; exhaustion leaves unbound as filed.
304
+ * Reask copy lives in package resources (ADR 0073).
305
+ * #1171 F2-R7: ownership comes from this nested resume's settlement seam
306
+ * (softTicketReaskOwnership.sealedSubstitute set when the soft-reask turn itself seals),
307
+ * not full-leg sealed count / time-window deltas that can absorb another court's seal.
308
+ */
309
+ async function reaskMissingTicketOnce(
310
+ admitted: AdmittedRoleInvocation,
311
+ terminal: TerminalResult | undefined,
312
+ env: InstructionSeatRunEnv,
313
+ io: CliIo,
314
+ ): Promise<SoftTicketReaskOutcome> {
315
+ if ((env.ticketReasksSpent ?? 0) >= 1) return { kind: "none" };
316
+ if (admitted.ticketNumber !== undefined) return { kind: "none" };
317
+ if (!isUnboundRunDirectory(admitted.runDirectory)) return { kind: "none" };
318
+ if (terminal?.roleOutcome.kind !== "accepted" && terminal?.roleOutcome.kind !== "audit_escalation") {
319
+ return { kind: "none" };
320
+ }
321
+ const payloads = terminal.roleOutcome.payloads ?? [];
322
+ if (payloads.length === 0) return { kind: "none" };
323
+ // Sealed-with-ticket skips via admitted.ticketNumber after bindSealedTicketNumber
324
+ // (post-admission). Do not re-parse payload.ticketNumber here — that would
325
+ // duplicate the existing ticket seam (#1171 notary: no rule copy).
326
+ const instruction = (await readPackageMaterial(MISSING_TICKET_REASK_MATERIAL)).trim();
327
+ // Spend budget and mark this turn as the soft reask (settlement identity).
328
+ // Ownership cell is set at that turn's settlement; env spreads keep the same
329
+ // ref through audit continue — see envForCourtContinue (#1171 F2-R5 / F2-R7).
330
+ const softTicketReaskOwnership = { sealedSubstitute: false };
331
+ const softReaskEnv: InstructionSeatRunEnv = {
332
+ ...env,
333
+ ticketReasksSpent: 1,
334
+ softTicketReaskTurn: true,
335
+ softTicketReaskOwnership,
336
+ };
337
+ const result = await runPublicInstructionSeatResume(
338
+ { runId: admitted.runId, summons: { instruction } },
339
+ softReaskEnv,
340
+ io,
341
+ );
342
+ if (
343
+ softTicketReaskOwnership.sealedSubstitute
344
+ || terminalCarriesSealedSubmission(result.terminal)
345
+ ) {
346
+ return { kind: "nested_owns", result };
347
+ }
348
+ return { kind: "no_substitute", result };
349
+ }
350
+
351
+ /** Court continue: keep cross-turn budgets + ownership cell; drop turn-scoped soft-reask identity. */
352
+ function envForCourtContinue(env: InstructionSeatRunEnv): InstructionSeatRunEnv {
353
+ return { ...withoutSoftTicketReaskTurn(env), unreadableReasksSpent: 0 };
354
+ }
355
+
356
+ /** True when a terminal still carries a sealed accepted / audit_escalation volume. */
357
+ function terminalCarriesSealedSubmission(terminal: TerminalResult | undefined): boolean {
358
+ const kind = terminal?.roleOutcome.kind;
359
+ if (kind !== "accepted" && kind !== "audit_escalation") return false;
360
+ return (terminal?.roleOutcome.payloads ?? []).length > 0;
361
+ }
362
+
363
+ /**
364
+ * #1171 F2-R2: kept original terminal may still name pre-relocate artifact
365
+ * paths after a report-only reask moved the leg. Project onto live admitted
366
+ * via the same rewrite seam post-admission uses after relocate.
367
+ */
368
+ function projectKeptOriginalArtifactsOntoLiveAdmitted(
369
+ original: SeatRunResult,
370
+ kept: SeatRunResult,
371
+ ): void {
372
+ const oldRunDirectory = original.admitted?.runDirectory;
373
+ const newRunDirectory = kept.admitted?.runDirectory;
374
+ if (
375
+ oldRunDirectory === undefined
376
+ || newRunDirectory === undefined
377
+ || oldRunDirectory === newRunDirectory
378
+ || kept.terminal === undefined
379
+ ) {
380
+ return;
381
+ }
382
+ for (const artifact of kept.terminal.artifacts) {
383
+ artifact.path = rewriteRunDirectoryPathValue(
384
+ artifact.path,
385
+ oldRunDirectory,
386
+ newRunDirectory,
387
+ ) as string;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * #1171 F2: missing-ticket reask did not seal a substitute — keep original
393
+ * terminal; take live placement from the reask when it relocated.
394
+ * #1171 F2-R1: silence / report-only stays on the original chain; a real host
395
+ * failure (nonzero exit) must be delivered honestly — do not wash it into the
396
+ * original accepted result. Sealed original volume remains on disk either way.
397
+ * #1171 F2-R2: after keep, project report artifact paths onto the live
398
+ * admitted directory so public result, durable face, and artifact refs agree.
399
+ * Substitute ownership is decided once at the soft-reask call seam (F2-R6);
400
+ * this helper only finishes the keep path.
401
+ */
402
+ function mergeMissingTicketReaskKeepOriginal(
403
+ original: SeatRunResult,
404
+ ticketReask: SeatRunResult,
405
+ ): SeatRunResult {
406
+ if (ticketReask.exitCode !== 0) return ticketReask;
407
+ const admitted = ticketReask.admitted ?? original.admitted;
408
+ if (admitted === undefined) return { ...original };
409
+ const kept = { ...original, admitted };
410
+ projectKeptOriginalArtifactsOntoLiveAdmitted(original, kept);
411
+ return kept;
412
+ }
413
+
414
+ /**
415
+ * Post-submission status reask, then missing-ticket soft reask, then audit.
416
+ * #1171 F3: status exhaustion must not skip the one missing-ticket reask.
417
+ * #1171 F2: ticket reask without a substitute seal continues the original chain.
418
+ */
419
+ async function continueAfterPostSubmissionGuards(
420
+ result: SeatRunResult,
421
+ admittedRole: PackagedRole,
422
+ env: InstructionSeatRunEnv,
423
+ io: CliIo,
424
+ held: readonly string[],
425
+ ): Promise<SeatRunResult> {
426
+ const live = result.admitted;
427
+ if (live === undefined) {
428
+ await presentOriginalVolume(held, result.terminal, io);
429
+ return result;
430
+ }
431
+ const unreadableReask = await reaskUnreadablePostSubmissionStatus(
432
+ live,
433
+ result.terminal,
434
+ env,
435
+ io,
436
+ );
437
+ let afterStatus = result;
438
+ if (isUnreadableReaskExhausted(unreadableReask)) {
439
+ const terminal = result.terminal === undefined
440
+ ? undefined
441
+ : withUnsettledDirection(result.terminal);
442
+ afterStatus = terminal === undefined
443
+ ? { ...result, admitted: live }
444
+ : { ...result, admitted: live, terminal };
445
+ // Fall through: missing-ticket reask still owed on the original sealed volume.
446
+ } else if (unreadableReask !== undefined) {
447
+ return unreadableReask;
448
+ }
449
+ const forTicket = afterStatus.admitted ?? live;
450
+ // Ownership is decided once inside reaskMissingTicketOnce (#1171 F2-R6).
451
+ const softReask = await reaskMissingTicketOnce(
452
+ forTicket,
453
+ afterStatus.terminal ?? result.terminal,
454
+ env,
455
+ io,
456
+ );
457
+ if (softReask.kind === "nested_owns") {
458
+ // Nested resume already finished guards + audit/present for the substitute.
459
+ // Do not run a second outer settlement of the same public call (#1171 F2-R4b).
460
+ return softReask.result;
461
+ }
462
+ const continued = softReask.kind === "none"
463
+ ? afterStatus
464
+ : mergeMissingTicketReaskKeepOriginal(afterStatus, softReask.result);
465
+ const continuedAdmitted = continued.admitted ?? live;
466
+ // Spent budget rides the outer audit/continue chain (#1171 F2-R4a). Soft-reask
467
+ // settlement identity does not — ordinary continue turns publish no_receipt (#1171 F2-R5).
468
+ const continuedEnv = softReask.kind !== "none"
469
+ ? { ...env, ticketReasksSpent: 1 }
470
+ : env;
471
+ if (AUDITED_ROLES.has(admittedRole) && continued.terminal?.roleOutcome.kind === "accepted") {
472
+ return auditSubmittedRole(
473
+ { ...continued, admitted: continuedAdmitted },
474
+ continuedEnv,
475
+ io,
476
+ );
477
+ }
478
+ await presentOriginalVolume(
479
+ held,
480
+ continued.terminal,
481
+ io,
482
+ continuedAdmitted.runDirectory,
483
+ );
484
+ return { ...continued, admitted: continuedAdmitted };
485
+ }
486
+
273
487
  function roleRecord(role: PackagedRole) {
274
488
  const record = packagedRoleMetadata(role);
275
489
  if (record === undefined) {
@@ -523,25 +737,15 @@ async function dispatchAdmitted(
523
737
  });
524
738
  };
525
739
  const result = await execute(env);
526
- const unreadableReask = await reaskUnreadablePostSubmissionStatus(
527
- admitted,
528
- result.terminal,
740
+ // Prefer the live admitted object returned from the turn (may have relocated).
741
+ const live = result.admitted ?? admitted;
742
+ return continueAfterPostSubmissionGuards(
743
+ { ...result, admitted: live },
744
+ admitted.role,
529
745
  env,
530
746
  io,
747
+ held,
531
748
  );
532
- if (isUnreadableReaskExhausted(unreadableReask)) {
533
- const terminal = result.terminal === undefined
534
- ? undefined
535
- : withUnsettledDirection(result.terminal);
536
- await presentOriginalVolume(held, terminal, io, admitted.runDirectory);
537
- return terminal === undefined ? result : { ...result, terminal };
538
- }
539
- if (unreadableReask !== undefined) return unreadableReask;
540
- if (AUDITED_ROLES.has(admitted.role) && result.terminal?.roleOutcome.kind === "accepted") {
541
- return auditSubmittedRole(result, env, io);
542
- }
543
- await presentOriginalVolume(held, result.terminal, io, admitted.runDirectory);
544
- return result;
545
749
  }
546
750
 
547
751
  /**
@@ -797,9 +1001,9 @@ export async function runPublicInstructionSeat(
797
1001
  const result = await runCountersignBody(parsed, env, {
798
1002
  stdout: (value) => held.push(value), stderr: io.stderr,
799
1003
  });
800
- if (result.terminal?.roleOutcome.kind === "accepted") return auditSubmittedRole(result, env, io);
801
- await presentOriginalVolume(held, result.terminal, io, result.admitted?.runDirectory);
802
- return result;
1004
+ // Same post-submission guards as every other seat (#1171 F8): missing-ticket
1005
+ // soft reask once, then audit — no early return that skips the reask.
1006
+ return continueAfterPostSubmissionGuards(result, "countersign", env, io, held);
803
1007
  }
804
1008
 
805
1009
  if ("parallelLenses" in record && record.parallelLenses === true) {
@@ -1075,28 +1279,17 @@ export async function runPublicInstructionSeatResume(
1075
1279
  },
1076
1280
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
1077
1281
  });
1078
- const unreadableReask = result.admitted === undefined
1079
- ? undefined
1080
- : await reaskUnreadablePostSubmissionStatus(
1081
- result.admitted,
1082
- result.terminal,
1083
- env,
1084
- io,
1085
- );
1086
- if (isUnreadableReaskExhausted(unreadableReask)) {
1087
- const terminal = result.terminal === undefined
1088
- ? undefined
1089
- : withUnsettledDirection(result.terminal);
1090
- await presentOriginalVolume(held, terminal, io, result.admitted?.runDirectory);
1091
- return terminal === undefined ? result : { ...result, terminal };
1092
- }
1093
- if (unreadableReask !== undefined) return unreadableReask;
1094
- if (result.admitted !== undefined && AUDITED_ROLES.has(result.admitted.role)
1095
- && result.terminal?.roleOutcome.kind === "accepted") {
1096
- return auditSubmittedRole(result, env, io);
1282
+ if (result.admitted === undefined) {
1283
+ await presentOriginalVolume(held, result.terminal, io);
1284
+ return result;
1097
1285
  }
1098
- await presentOriginalVolume(held, result.terminal, io, result.admitted?.runDirectory);
1099
- return result;
1286
+ return continueAfterPostSubmissionGuards(
1287
+ result,
1288
+ result.admitted.role,
1289
+ env,
1290
+ io,
1291
+ held,
1292
+ );
1100
1293
  };
1101
1294
  let binding: Awaited<ReturnType<typeof readAuditorResumeBinding>>;
1102
1295
  try {
@@ -1151,7 +1344,7 @@ async function queueConclusionFromChild(
1151
1344
  // Notary, auditor, and inspector have no seat-local status reask. This loop
1152
1345
  // is their one budget. The child resume keeps the caller's env so delivery,
1153
1346
  // failure recovery, and other reask loops stay separate.
1154
- let budgetEnv: InstructionSeatRunEnv = { ...env, unreadableReasksSpent: 0 };
1347
+ let budgetEnv: InstructionSeatRunEnv = envForCourtContinue(env);
1155
1348
  for (;;) {
1156
1349
  const terminal = await trySettlePublicSeat(
1157
1350
  current,
@@ -1234,7 +1427,7 @@ export async function continueParentAfterChild(
1234
1427
  return runPublicInstructionSeatResume({
1235
1428
  runId: parentRunId,
1236
1429
  summons: { instruction: readableGateItem(latestQueuePayload(resolved.terminal)) },
1237
- }, { ...env, unreadableReasksSpent: 0 }, io);
1430
+ }, envForCourtContinue(env), io);
1238
1431
  }
1239
1432
  const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
1240
1433
  showResumeErrorPointer(io, result.exitCode, result.terminal);
@@ -1285,6 +1478,8 @@ async function auditSubmittedRole(
1285
1478
  resumedOfficer?: AdmittedRoleInvocation,
1286
1479
  passedReceipt?: unknown,
1287
1480
  ): Promise<SeatRunResult> {
1481
+ // Ordinary audit chain: drop soft-reask turn identity; keep budgets/ownership (#1171 F2-R9).
1482
+ env = withoutSoftTicketReaskTurn(env);
1288
1483
  const admitted = turn.admitted;
1289
1484
  if (admitted === undefined || !AUDITED_ROLES.has(admitted.role)
1290
1485
  || turn.terminal?.roleOutcome.kind !== "accepted") return turn;
@@ -1329,10 +1524,13 @@ async function auditSubmittedRole(
1329
1524
  await presentTerminal(turn.terminal, { ...io, omitFailureStderrDiagnostic: true }, admitted.runDirectory);
1330
1525
  return turn;
1331
1526
  }
1332
- const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, env.home);
1333
- const acceptedRow = [...rows].reverse().find((row) => row.role === admitted.role && row.kind === "accepted");
1334
- const toolCallId = acceptedRow?.toolCallId;
1335
- if (toolCallId === undefined) throw new Error("accepted submission has no tool call identity");
1527
+ // #1171 F2-R8: bind audit to the sealed identity carried on this turn's
1528
+ // settlement terminal — never the whole-leg latest accepted row (another
1529
+ // lawful public call may have sealed and passed台院 meanwhile).
1530
+ const toolCallId = turn.terminal.submissionToolCallId;
1531
+ if (toolCallId === undefined || toolCallId.length === 0) {
1532
+ throw new Error("accepted submission has no tool call identity");
1533
+ }
1336
1534
  const sessionFile = env.principalAuthority.decode(admitted.principal).sessionFile;
1337
1535
  const entries = admitted.role === "doctor"
1338
1536
  ? await readBoundSessionEntries(sessionFile)
@@ -1393,7 +1591,7 @@ async function auditSubmittedRole(
1393
1591
  if (decision.status === "continue") {
1394
1592
  return runPublicInstructionSeatResume({
1395
1593
  runId: admitted.runId, summons: { instruction: readableGateItem(decision.receipt ?? decision.violations) },
1396
- }, { ...env, unreadableReasksSpent: 0 }, io);
1594
+ }, envForCourtContinue(env), io);
1397
1595
  }
1398
1596
  if (decision.status === "received") {
1399
1597
  if (lastSummon?.terminal === undefined) {
@@ -1495,7 +1693,7 @@ async function auditSubmittedRole(
1495
1693
  return runPublicInstructionSeatResume({
1496
1694
  runId: admitted.runId,
1497
1695
  summons: { instruction: readableGateItem(chain.passes.at(-1)?.receipt) },
1498
- }, { ...env, unreadableReasksSpent: 0 }, io);
1696
+ }, envForCourtContinue(env), io);
1499
1697
  }
1500
1698
  if (admitted.role === "secretariat") {
1501
1699
  const pass = chain.passes.at(-1);
@@ -52,7 +52,7 @@ import {
52
52
  requireSafePositiveTicketNumber,
53
53
  } from "../run-ticket-number.ts";
54
54
  import {
55
- rewriteRunDirectoryPathFields,
55
+ rewriteAdmittedRoleRunPage,
56
56
  } from "../role-run-relocation.ts";
57
57
  import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.ts";
58
58
  import {
@@ -99,12 +99,12 @@ import type { PublicThinkingLevel } from "./registry.ts";
99
99
  import { errorText, isRecord } from "../unknown-value.ts";
100
100
 
101
101
  /** Caller-supplied attachment path recorded as-is (#1165). */
102
- export type FrozenAttachment = {
102
+ export type AdmittedAttachment = {
103
103
  readonly path: string;
104
104
  };
105
105
 
106
106
  /** Normalize admitted attachment records (caller path only; #1165). */
107
- export function normalizeAdmittedAttachment(raw: unknown): FrozenAttachment | undefined {
107
+ export function normalizeAdmittedAttachment(raw: unknown): AdmittedAttachment | undefined {
108
108
  if (typeof raw !== "object" || raw === null) return undefined;
109
109
  const record = raw as Record<string, unknown>;
110
110
  const path = record.path;
@@ -115,7 +115,7 @@ export function normalizeAdmittedAttachment(raw: unknown): FrozenAttachment | un
115
115
 
116
116
  function admitCallerAttachments(
117
117
  attachmentPaths: readonly string[],
118
- ): readonly FrozenAttachment[] {
118
+ ): readonly AdmittedAttachment[] {
119
119
  return attachmentPaths.map((path) => ({ path }));
120
120
  }
121
121
 
@@ -128,7 +128,7 @@ export type AdmittedRoleInvocationBase = {
128
128
  readonly instruction: string;
129
129
  /** True when the caller supplied a literally empty instruction (length 0). */
130
130
  readonly instructionEmpty: boolean;
131
- readonly attachments: readonly FrozenAttachment[];
131
+ readonly attachments: readonly AdmittedAttachment[];
132
132
  readonly runDirectory: string;
133
133
  /** Host-issued opaque durable principal (coordinates only via authority.decode). */
134
134
  readonly principal: DurablePrincipal;
@@ -307,7 +307,6 @@ export type AdmissionPlacement = {
307
307
  readonly sessionDirectory: string;
308
308
  readonly sessionFile: string;
309
309
  readonly runDirectory: string;
310
- readonly attachmentsDirectory: string;
311
310
  readonly ledgerHome: string;
312
311
  readonly bookKey: string;
313
312
  };
@@ -608,18 +607,10 @@ export async function relocateAdmittedRunToTicket(
608
607
  // render that may refuse (#1161 L1 / BASE order).
609
608
  heldLease?.relocate(target.runDirectory);
610
609
 
611
- const admittedRecord = admitted as unknown as Record<string, unknown>;
612
- rewriteRunDirectoryPathFields(
613
- admittedRecord,
614
- [
615
- "runDirectory",
616
- "mergerInputPath",
617
- ],
618
- oldRunDirectory,
619
- target.runDirectory,
610
+ rewriteAdmittedRoleRunPage(
611
+ admitted as unknown as Record<string, unknown>,
612
+ [{ oldRunDirectory, newRunDirectory: target.runDirectory }],
620
613
  );
621
- // #1165/#1168: caller file-flag paths stay as given. Obsolete copy fields
622
- // (taskPath/packetPath) are not rewritten — stock volumes keep their bytes.
623
614
  (admitted as { principal: DurablePrincipal }).principal = principal;
624
615
 
625
616
  // current.json projects host.original from this run directory. Ownership is
@@ -1610,8 +1601,7 @@ type PlacedRoleAdmission = {
1610
1601
  readonly principal: DurablePrincipal;
1611
1602
  readonly sessionDirectory: string;
1612
1603
  readonly sessionFile: string;
1613
- readonly attachments: readonly FrozenAttachment[];
1614
- readonly attachmentsDirectory: string;
1604
+ readonly attachments: readonly AdmittedAttachment[];
1615
1605
  readonly ledgerHome: string;
1616
1606
  readonly ticketFields: ReturnType<typeof ticketAdmissionFields>;
1617
1607
  };
@@ -1644,7 +1634,6 @@ async function placeRoleAdmission(options: {
1644
1634
  sessionDirectory,
1645
1635
  sessionFile,
1646
1636
  runDirectory,
1647
- attachmentsDirectory,
1648
1637
  ledgerHome,
1649
1638
  bookKey,
1650
1639
  } = issueAdmissionPlacement(options.principalAuthority, {
@@ -1667,14 +1656,13 @@ async function placeRoleAdmission(options: {
1667
1656
  sessionDirectory,
1668
1657
  sessionFile,
1669
1658
  attachments,
1670
- attachmentsDirectory,
1671
1659
  ledgerHome,
1672
1660
  ticketFields,
1673
1661
  };
1674
1662
  }
1675
1663
 
1676
1664
  function persistedAttachmentRefs(
1677
- attachments: readonly FrozenAttachment[],
1665
+ attachments: readonly AdmittedAttachment[],
1678
1666
  ): ReadonlyArray<{ path: string }> {
1679
1667
  return attachments.map((attachment) => ({ path: attachment.path }));
1680
1668
  }
@@ -1712,8 +1700,8 @@ async function persistPlacedAdmission(
1712
1700
  }
1713
1701
 
1714
1702
  /**
1715
- * Shared admission: project check, placement, attachment freeze,
1716
- * admitted and invocation section write.
1703
+ * Shared admission: project check, placement, admitted and invocation section write.
1704
+ * Caller file-flag paths are recorded opaque on the admitted page (ADR 0087).
1717
1705
  * Countersign passes deferPersistence so same-ticket lookup can reserve coordinates
1718
1706
  * before materializeCountersignInvocation writes the page.
1719
1707
  * Seats whose extra facts are known before placement pass them as admittedFields.
@@ -1937,7 +1925,7 @@ export async function materializeCountersignInvocation(
1937
1925
  if (ticketFields.ticketNumber !== undefined) {
1938
1926
  (admitted as { ticketNumber?: number }).ticketNumber = ticketFields.ticketNumber;
1939
1927
  }
1940
- (admitted as { attachments: readonly FrozenAttachment[] }).attachments = placement.attachments;
1928
+ (admitted as { attachments: readonly AdmittedAttachment[] }).attachments = placement.attachments;
1941
1929
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
1942
1930
  sessionDirectory: placement.sessionDirectory,
1943
1931
  sessionFile: placement.sessionFile,
@@ -1955,7 +1943,7 @@ export async function loadAdmittedJudgeRequest(
1955
1943
  ): Promise<{
1956
1944
  instruction: string;
1957
1945
  instructionEmpty: boolean;
1958
- attachments: readonly FrozenAttachment[];
1946
+ attachments: readonly AdmittedAttachment[];
1959
1947
  } | undefined> {
1960
1948
  try {
1961
1949
  const record = readPageSync(runDirectory, "admitted");
@@ -1966,7 +1954,7 @@ export async function loadAdmittedJudgeRequest(
1966
1954
  if (!Array.isArray(record.attachments)) return undefined;
1967
1955
  const attachments = record.attachments
1968
1956
  .map((item) => normalizeAdmittedAttachment(item))
1969
- .filter((item): item is FrozenAttachment => item !== undefined);
1957
+ .filter((item): item is AdmittedAttachment => item !== undefined);
1970
1958
  return {
1971
1959
  instruction: record.instruction,
1972
1960
  instructionEmpty: record.instructionEmpty,