@akagilnc/pi-workflow-roles 0.1.5762 → 0.1.5797

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 (52) hide show
  1. package/dist/acp-host/production-host.js +685 -285
  2. package/dist/external-host-turn-loop.js +3 -0
  3. package/dist/headless-host/production-host.js +658 -258
  4. package/dist/migrate-book-topology.js +70 -29
  5. package/dist/navigator-attendance.js +1 -1
  6. package/dist/packaged-role-registry.js +3 -2
  7. package/dist/pi/role-turn-host.js +6 -1
  8. package/dist/public-cli/instruction-seat-run.js +152 -60
  9. package/dist/public-cli/invocation.js +98 -46
  10. package/dist/public-cli/main.js +482 -222
  11. package/dist/public-cli/option-definitions.js +4 -4
  12. package/dist/public-cli/post-admission.js +19 -28
  13. package/dist/public-cli/run-lifecycle.js +10 -21
  14. package/dist/public-cli/turn-request.js +3 -0
  15. package/dist/public-role-summons.js +11 -8
  16. package/dist/report-ticket-tool.js +3 -3
  17. package/dist/role-run-placement.js +14 -2
  18. package/dist/role-run-relocation.js +2 -2
  19. package/dist/run-dossier.js +5 -4
  20. package/dist/run-terminal-artifacts.js +0 -7
  21. package/dist/session-identity.js +3 -2
  22. package/dist/sitian-appender.js +12 -3
  23. package/dist/submission-ledger.js +24 -0
  24. package/dist/ticket-progress.js +306 -0
  25. package/dist/user-dialogue-stdin.js +23 -17
  26. package/package.json +1 -1
  27. package/src/external-host-turn-loop.ts +7 -1
  28. package/src/host-contracts.ts +1 -1
  29. package/src/navigator-attendance.ts +2 -1
  30. package/src/packaged-role-registry.ts +3 -2
  31. package/src/pi/adapter.ts +57 -45
  32. package/src/pi/role-turn-host.ts +6 -1
  33. package/src/prepared-role-turn.ts +6 -0
  34. package/src/public-cli/instruction-seat-run.ts +166 -68
  35. package/src/public-cli/invocation.ts +120 -46
  36. package/src/public-cli/option-definitions.ts +5 -4
  37. package/src/public-cli/post-admission.ts +19 -30
  38. package/src/public-cli/run-lifecycle.ts +10 -27
  39. package/src/public-cli/turn-request.ts +3 -0
  40. package/src/public-role-summons.ts +27 -16
  41. package/src/report-ticket-tool.ts +7 -3
  42. package/src/role-envelope.ts +5 -0
  43. package/src/role-run-placement.ts +19 -2
  44. package/src/role-run-relocation.ts +2 -2
  45. package/src/role-runtime.ts +19 -2
  46. package/src/run-dossier.ts +5 -3
  47. package/src/run-terminal-artifacts.ts +0 -8
  48. package/src/session-identity.ts +3 -2
  49. package/src/sitian-appender.ts +14 -3
  50. package/src/submission-ledger.ts +27 -0
  51. package/src/ticket-progress.ts +383 -0
  52. package/src/user-dialogue-stdin.ts +21 -14
@@ -75,7 +75,6 @@ import {
75
75
  } from "./post-admission.ts";
76
76
  import { presentTerminal } from "./auto-resume.ts";
77
77
  import {
78
- buildAutoResumeContinuationPrompt,
79
78
  loadResumablePublicRole,
80
79
  markRunAdmitted,
81
80
  readCurrentCourt,
@@ -138,6 +137,20 @@ const POST_SUBMISSION_ROUTING: Partial<Record<PackagedRole, {
138
137
  judge: {
139
138
  statuses: REVIEW_QUEUE_STATUSES,
140
139
  },
140
+ // #1208 C1 / ADR 0055: every review-queue seat uses the shared
141
+ // unreadable-status seam (officerConclusionReask). Independent public
142
+ // entry for notary/auditor/inspector was the same miss — route here, do
143
+ // not invent a parallel loop. Gate summons still own the outer budget
144
+ // via nestedIncomplete when this seat exhausts unsettled.
145
+ notary: {
146
+ statuses: REVIEW_QUEUE_STATUSES,
147
+ },
148
+ auditor: {
149
+ statuses: REVIEW_QUEUE_STATUSES,
150
+ },
151
+ inspector: {
152
+ statuses: REVIEW_QUEUE_STATUSES,
153
+ },
141
154
  coder: {
142
155
  statuses: WORKER_ROUTING_STATUSES,
143
156
  reask: WORKER_STATUS_REASK,
@@ -271,11 +284,14 @@ async function reaskUnreadablePostSubmissionStatus(
271
284
  if (message === undefined) return undefined;
272
285
  const next = withUnreadableReask(env);
273
286
  if (next === undefined) return UNREADABLE_REASK_EXHAUSTED;
287
+ // Named status-reask: summons opens a new court; message carries the diagnostic
288
+ // (not arbitrary summons.instruction passthrough through the review resolver).
274
289
  return runPublicInstructionSeatResume(
275
290
  {
276
291
  runId: admitted.runId,
277
292
  runDirectory: admitted.runDirectory,
278
- summons: { instruction: message },
293
+ message,
294
+ summons: { instructionEmpty: true },
279
295
  },
280
296
  next,
281
297
  io,
@@ -329,11 +345,14 @@ async function reaskMissingTicketOnce(
329
345
  softTicketReaskTurn: true,
330
346
  softTicketReaskOwnership,
331
347
  };
348
+ // Named #1171 missing-ticket: summons opens a new court; message carries the
349
+ // package reask copy (not arbitrary summons.instruction passthrough).
332
350
  const result = await runPublicInstructionSeatResume(
333
351
  {
334
352
  runId: admitted.runId,
335
353
  runDirectory: admitted.runDirectory,
336
- summons: { instruction },
354
+ message: instruction,
355
+ summons: { instructionEmpty: true },
337
356
  },
338
357
  softReaskEnv,
339
358
  io,
@@ -505,8 +524,8 @@ export function buildInstructionSeatTurnRequest(
505
524
 
506
525
  /**
507
526
  * Direct review-seat call: reconnect ledger peer-body delivery (ADR 0085 / #1166).
508
- * Gate summons already ride gateReviewInstruction; direct --source-run must not
509
- * leave a blank fixed-kickoff.
527
+ * Gate summons already ride gateReviewInstruction; direct --source-run falls
528
+ * through the shared dialogue resolver (notary: fixed dispatch).
510
529
  */
511
530
  async function loadLedgerPeerBody(
512
531
  sourceRunDirectory: string,
@@ -529,48 +548,59 @@ async function loadLedgerPeerBody(
529
548
  return readableGateItem(latest.accepted);
530
549
  }
531
550
 
532
- /** Owner-approved fixed Notary dispatch (2026-10-09); never the parent verdict. */
551
+ /** Owner-approved fixed Notary review dispatch (2026-10-09 #1208); never the parent verdict. */
533
552
  export const NOTARY_FIXED_DISPATCH = "请审本轮受审对象,按符宝郎职掌交卷。";
534
553
 
535
554
  /**
536
555
  * One delivery rule for review-seat dialogue on new turns and same-parent resume
537
- * (#1166 / ADR 0085 / ADR 0087): explicit reask → non-empty caller dispatch →
538
- * source-only ledger peer body. Identity stays in startup materials.
539
- * #1195: ticket-court notary skips peer-body preload (countersign source only) and
540
- * receives the fixed dispatch instead (codex rejects an empty prompt).
556
+ * (#1166 / ADR 0085 / ADR 0087 / #1208): notary review uses fixed dispatch;
557
+ * named operational reasks ride reviewReask (first-mint / gate) or the resume
558
+ * request message seam — never arbitrary non-empty caller/summons passthrough.
559
+ * Other seats prefer format reask, then gate body, then caller dispatch, then
560
+ * ledger peer. Identity stays in startup materials. Notary never takes
561
+ * gate/parent verdict as dialogue (gateReviewInstruction stays unread for notary).
541
562
  */
542
- async function resolveReviewSeatDialogueBody(input: {
563
+ export async function resolveReviewSeatDialogueBody(input: {
543
564
  readonly role?: string;
544
- readonly reask?: string;
565
+ readonly reviewReask?: string;
566
+ readonly gateReviewInstruction?: string;
545
567
  readonly callerInstruction?: string;
546
568
  readonly sourceRunPath?: string;
547
569
  readonly projectRoot: string;
548
570
  readonly home: string;
549
571
  }): Promise<string | undefined> {
550
- const reask = input.reask;
551
- if (reask !== undefined && reask.length > 0) return reask;
572
+ // #1208: fixed sentence is the review function only (owner 37bfe80d /
573
+ // 393daf98). reviewReask is the named operational override; caller/summons
574
+ // body is not a free passthrough (countersign rejected that prescription).
575
+ if (input.role === "notary") {
576
+ if (input.reviewReask !== undefined) return input.reviewReask;
577
+ return NOTARY_FIXED_DISPATCH;
578
+ }
579
+ const preferred = input.reviewReask ?? input.gateReviewInstruction;
580
+ if (preferred !== undefined && preferred.length > 0) return preferred;
552
581
  const caller = input.callerInstruction ?? "";
553
582
  if (caller.length > 0) return caller;
554
583
  const sourceRunPath = admittedSourceRunPath(input)?.trim();
555
584
  if (sourceRunPath === undefined) return undefined;
556
- if (input.role === "notary") {
557
- const { isTicketCourtCountersignSource } = await import("../run-terminal-artifacts.ts");
558
- if (isTicketCourtCountersignSource(sourceRunPath)) return NOTARY_FIXED_DISPATCH;
559
- }
560
585
  return await loadLedgerPeerBody(sourceRunPath, input.projectRoot, input.home);
561
586
  }
562
587
 
563
- async function resolveInitialPrompt(
588
+ /**
589
+ * Initial host prompt for this turn (#1199: progress instruction = these bytes).
590
+ */
591
+ async function resolveInitialTurnPrompt(
564
592
  admitted: AdmittedRoleInvocation,
565
593
  env: InstructionSeatRunEnv,
566
594
  ): Promise<string> {
567
595
  const record = roleRecord(admitted.role);
568
596
  if ("reaskPrompt" in record && record.reaskPrompt === true) {
569
597
  const sourceRunPath = admittedSourceRunPath(admitted);
570
- const reask = env.reviewReask ?? env.gateReviewInstruction;
571
598
  const body = await resolveReviewSeatDialogueBody({
572
599
  role: admitted.role,
573
- ...(reask === undefined ? {} : { reask }),
600
+ ...(env.reviewReask === undefined ? {} : { reviewReask: env.reviewReask }),
601
+ ...(env.gateReviewInstruction === undefined
602
+ ? {}
603
+ : { gateReviewInstruction: env.gateReviewInstruction }),
574
604
  callerInstruction: admitted.instruction,
575
605
  ...(sourceRunPath === undefined ? {} : { sourceRunPath }),
576
606
  projectRoot: admitted.projectRoot,
@@ -699,9 +729,10 @@ async function dispatchAdmitted(
699
729
  if (activeEnv.correlationId !== undefined && activeEnv.correlationId.trim() !== "") {
700
730
  await recordAdmittedCorrelation(admitted, activeEnv.correlationId);
701
731
  }
702
- const initialPromptText = await resolveInitialPrompt(admitted, activeEnv);
732
+ const initialPrompt = await resolveInitialTurnPrompt(admitted, activeEnv);
703
733
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
704
734
  if (auto) {
735
+ // #1208: auto-resume resends this turn's actual content, never a filler phrase.
705
736
  return await runPostAdmissionResumable({
706
737
  admitted,
707
738
  env: activeEnv,
@@ -710,14 +741,14 @@ async function dispatchAdmitted(
710
741
  admitted,
711
742
  roleTurnOptions(activeEnv, admitted, {
712
743
  kind: "initial",
713
- prompt: initialPromptText,
744
+ prompt: initialPrompt,
714
745
  }),
715
746
  ),
716
747
  buildResumeRequest: () => buildInstructionSeatTurnRequest(
717
748
  admitted,
718
749
  roleTurnOptions(activeEnv, admitted, {
719
750
  kind: "resume",
720
- prompt: buildAutoResumeContinuationPrompt(),
751
+ prompt: initialPrompt,
721
752
  }),
722
753
  ),
723
754
  adapters,
@@ -732,7 +763,7 @@ async function dispatchAdmitted(
732
763
  admitted,
733
764
  roleTurnOptions(activeEnv, admitted, {
734
765
  kind: "initial",
735
- prompt: initialPromptText,
766
+ prompt: initialPrompt,
736
767
  }),
737
768
  ),
738
769
  adapters,
@@ -851,21 +882,28 @@ function resumeSameParentInstructionSeat(input: {
851
882
  ...(input.ticketNumber === undefined ? {} : { ticketNumber: input.ticketNumber }),
852
883
  freshSummons: input.env.freshSummons,
853
884
  summons: input.summons,
854
- resume: (runId, materials, runDirectory) => runPublicInstructionSeatResume(
855
- {
856
- runId,
857
- runDirectory,
858
- ...(materials === undefined ? {} : { summons: materials }),
859
- },
860
- input.env,
861
- input.io,
862
- ),
885
+ resume: (runId, materials, runDirectory) => {
886
+ // Named operational reask on this summon: explicit message seam.
887
+ // Review dialogue stays on summons → shared resolver (notary: fixed).
888
+ // Do not stuff reask into summons.instruction for later passthrough.
889
+ const reask = input.env.reviewReask;
890
+ return runPublicInstructionSeatResume(
891
+ {
892
+ runId,
893
+ runDirectory,
894
+ ...(reask === undefined ? {} : { message: reask }),
895
+ ...(materials === undefined ? {} : { summons: materials }),
896
+ },
897
+ input.env,
898
+ input.io,
899
+ );
900
+ },
863
901
  });
864
902
  }
865
903
 
866
904
  /**
867
- * Same delivery rule as resolveInitialPrompt for same-parent resume summons.
868
- * Reask / explicit caller text / source-only ledger peer — one authority.
905
+ * Same delivery rule as resolveInitialTurnPrompt for same-parent resume summons.
906
+ * Shared resolver owns the input choice — callers only pass env fields.
869
907
  */
870
908
  async function sameParentDialogue(
871
909
  env: InstructionSeatRunEnv,
@@ -874,10 +912,12 @@ async function sameParentDialogue(
874
912
  projectRoot: string,
875
913
  role: string,
876
914
  ): Promise<{ readonly instruction: string; readonly instructionEmpty: boolean }> {
877
- const reask = env.reviewReask ?? env.gateReviewInstruction;
878
915
  const body = await resolveReviewSeatDialogueBody({
879
916
  role,
880
- ...(reask === undefined ? {} : { reask }),
917
+ ...(env.reviewReask === undefined ? {} : { reviewReask: env.reviewReask }),
918
+ ...(env.gateReviewInstruction === undefined
919
+ ? {}
920
+ : { gateReviewInstruction: env.gateReviewInstruction }),
881
921
  callerInstruction: fallback.instruction,
882
922
  sourceRunPath,
883
923
  projectRoot,
@@ -898,16 +938,27 @@ async function runCountersignBody(
898
938
  env: InstructionSeatRunEnv,
899
939
  io: CliIo,
900
940
  ): Promise<SeatRunResult> {
941
+ const projectRoot = resolve(parsed.project ?? env.cwd);
901
942
  const gateParentRunPath =
902
943
  typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== ""
903
944
  ? env.parentRunPath
904
945
  : undefined;
905
946
  if (gateParentRunPath !== undefined) {
906
- const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction ?? "";
947
+ // #1208 C1: same shared dialogue resolver as every other same-parent seat.
948
+ const dialogue = await sameParentDialogue(
949
+ env,
950
+ {
951
+ instruction: parsed.instruction ?? "",
952
+ instructionEmpty: (parsed.instruction ?? "").length === 0,
953
+ },
954
+ gateParentRunPath,
955
+ projectRoot,
956
+ "countersign",
957
+ );
907
958
  const resumed = await resumeSameParentInstructionSeat({
908
959
  env,
909
960
  io,
910
- projectRoot: resolve(parsed.project ?? env.cwd),
961
+ projectRoot,
911
962
  role: "countersign",
912
963
  parentRunPath: gateParentRunPath,
913
964
  ...(isSafePositiveTicketNumber(env.boundTicketNumber)
@@ -915,8 +966,8 @@ async function runCountersignBody(
915
966
  : {}),
916
967
  summons: {
917
968
  sourceRunPath: gateParentRunPath,
918
- instruction: resumeInstruction,
919
- instructionEmpty: resumeInstruction.length === 0,
969
+ instruction: dialogue.instruction,
970
+ instructionEmpty: dialogue.instructionEmpty,
920
971
  // #1166 J11: same-parent resume delivers this call's file-flag paths
921
972
  // through the shared summons → appendCallerFileFlagPaths rule.
922
973
  attachmentPaths: parsed.attachmentPaths ?? [],
@@ -954,14 +1005,10 @@ async function runCountersignBody(
954
1005
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
955
1006
  admitted = { ...admitted, sourceRunPath: gateParentRunPath };
956
1007
  }
957
- // #1166 J11: review-body branch must merge the same file-flag delivery as
958
- // resolveInitialPrompt / resumeTurnRequestProjectionOptions — never body alone.
959
- const reviewBody = env.reviewReask ?? env.gateReviewInstruction;
1008
+ // #1208 C1 / #1166 J11: one shared initial-prompt seam (incl. file flags).
960
1009
  const turnProjection = roleTurnOptions(env, admitted, {
961
1010
  kind: "initial",
962
- prompt: reviewBody !== undefined
963
- ? appendCallerFileFlagPaths(reviewBody, admitted.attachments)
964
- : buildInstructionTransportPrompt(admitted),
1011
+ prompt: await resolveInitialTurnPrompt(admitted, env),
965
1012
  });
966
1013
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
967
1014
  const result = await runPostAdmissionOneShot({
@@ -1187,6 +1234,10 @@ export async function runPublicInstructionSeat(
1187
1234
  await persistAdmittedSourceRunPath(admitted, auditorSource, auditorSubject);
1188
1235
  admitted = { ...admitted, sourceRunPath: auditorSource } as AdmittedRoleInvocation;
1189
1236
  }
1237
+ // #1208 C2: same helper as resume — known board ticket on the audited
1238
+ // source binds and relocates before the missing-ticket soft reask can
1239
+ // fire. True no-ticket sources stay unbound (#1171 asks once).
1240
+ admitted = await inheritKnownTicketFromSource(admitted, env.principalAuthority);
1190
1241
 
1191
1242
  const runAdmitted = async (): Promise<SeatRunResult> => {
1192
1243
  await markRunAdmitted(admitted, env.principalAuthority);
@@ -1235,6 +1286,25 @@ export async function runPublicInstructionSeat(
1235
1286
  return runAdmitted();
1236
1287
  }
1237
1288
 
1289
+ /**
1290
+ * #1208 C2: same as new source-locator admission — when this leg is still
1291
+ * unbound, inherit the board ticket already on the audited source and relocate.
1292
+ * True no-ticket sources stay unbound so #1171 still asks once.
1293
+ */
1294
+ async function inheritKnownTicketFromSource(
1295
+ admitted: AdmittedRoleInvocation,
1296
+ authority: DurablePrincipalAuthority,
1297
+ ): Promise<AdmittedRoleInvocation> {
1298
+ if (admitted.ticketNumber !== undefined) return admitted;
1299
+ const sourcePath = admittedSourceRunPath(admitted);
1300
+ if (sourcePath === undefined) return admitted;
1301
+ const sourceTicket = await readBoardTicketNumber(sourcePath);
1302
+ if (sourceTicket === undefined) return admitted;
1303
+ await bindAdmittedTicketNumber(admitted, sourceTicket);
1304
+ await relocateAdmittedRunToTicket(admitted, authority);
1305
+ return admitted;
1306
+ }
1307
+
1238
1308
  export async function runPublicInstructionSeatResume(
1239
1309
  request: PublicResumeRequest,
1240
1310
  env: InstructionSeatRunEnv,
@@ -1259,30 +1329,53 @@ export async function runPublicInstructionSeatResume(
1259
1329
  env.principalAuthority,
1260
1330
  effective.runDirectory === undefined ? undefined : { runDirectory: effective.runDirectory },
1261
1331
  );
1332
+ let admitted = loaded.admitted;
1262
1333
  if (
1263
- packagedRebindSourceOnResume(loaded.admitted.role)
1334
+ packagedRebindSourceOnResume(admitted.role)
1264
1335
  && effective.summons?.sourceRunPath !== undefined
1265
1336
  && effective.summons.sourceRun !== undefined
1266
1337
  ) {
1267
- return {
1268
- ...loaded,
1269
- admitted: {
1270
- ...loaded.admitted,
1271
- sourceRunPath: effective.summons.sourceRunPath,
1272
- sourceRun: effective.summons.sourceRun,
1273
- },
1274
- };
1338
+ // source-locator seats only (notary); sourceRun is on that admission face.
1339
+ admitted = {
1340
+ ...admitted,
1341
+ sourceRunPath: effective.summons.sourceRunPath,
1342
+ sourceRun: effective.summons.sourceRun,
1343
+ } as AdmittedRoleInvocation;
1275
1344
  }
1276
- return loaded;
1345
+ admitted = await inheritKnownTicketFromSource(admitted, env.principalAuthority);
1346
+ return { ...loaded, admitted };
1277
1347
  },
1278
1348
  buildTurnRequest: async (admitted, effective) => {
1279
1349
  const summonsPrepared = await prepareSummonsResumeMaterials(admitted.runDirectory, effective.summons);
1280
- return buildInstructionSeatTurnRequest(
1281
- admitted,
1282
- {
1283
- ...resumeTurnRequestProjectionOptions(admitted, effective, env, summonsPrepared),
1284
- },
1350
+ const projection = resumeTurnRequestProjectionOptions(
1351
+ admitted, effective, env, summonsPrepared,
1285
1352
  );
1353
+ // #1208: internal summons only — shared resolver owns review dialogue.
1354
+ // Public bare resume / explicit message keep projection (operational
1355
+ // reasks use the message seam). Do not reselect stale env dialogue over
1356
+ // this-round summons (#1208 R1).
1357
+ if (effective.summons === undefined || effective.message !== undefined) {
1358
+ return buildInstructionSeatTurnRequest(admitted, { ...projection });
1359
+ }
1360
+ const sourceRunPath = admittedSourceRunPath(admitted);
1361
+ const body = await resolveReviewSeatDialogueBody({
1362
+ role: admitted.role,
1363
+ callerInstruction: summonsPrepared?.instruction ?? "",
1364
+ ...(sourceRunPath === undefined ? {} : { sourceRunPath }),
1365
+ projectRoot: admitted.projectRoot,
1366
+ home: env.home,
1367
+ });
1368
+ if (body === undefined) {
1369
+ return buildInstructionSeatTurnRequest(admitted, { ...projection });
1370
+ }
1371
+ const fileFlags = summonsPrepared?.attachments ?? [];
1372
+ const prompt = fileFlags.length === 0
1373
+ ? body
1374
+ : appendCallerFileFlagPaths(body, fileFlags);
1375
+ return buildInstructionSeatTurnRequest(admitted, {
1376
+ ...projection,
1377
+ continuation: { kind: "resume", prompt },
1378
+ });
1286
1379
  },
1287
1380
  adapters: {
1288
1381
  trySettle: async () => undefined,
@@ -1361,9 +1454,12 @@ async function queueConclusionFromChild(
1361
1454
  return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
1362
1455
  }
1363
1456
  let current: AdmittedRoleInvocation = child;
1364
- // Notary, auditor, and inspector have no seat-local status reask. This loop
1365
- // is their one budget. The child resume keeps the caller's env so delivery,
1366
- // failure recovery, and other reask loops stay separate.
1457
+ // #1208: review-queue gate children also have seat-local
1458
+ // POST_SUBMISSION_ROUTING. This parent-side loop remains the
1459
+ // gate/correlation budget; nestedIncomplete from an exhausted seat-local
1460
+ // chain surfaces without a second spend. The child resume keeps the
1461
+ // caller's env so delivery, failure recovery, and other reask loops
1462
+ // stay separate.
1367
1463
  let budgetEnv: InstructionSeatRunEnv = envForCourtContinue(env);
1368
1464
  for (;;) {
1369
1465
  const terminal = await trySettlePublicSeat(
@@ -1548,7 +1644,8 @@ async function auditSubmittedRole(
1548
1644
  return runPublicInstructionSeatResume({
1549
1645
  runId: admitted.runId,
1550
1646
  runDirectory: admitted.runDirectory,
1551
- summons: { instruction: SECRETARIAT_STATUS_REASK },
1647
+ message: SECRETARIAT_STATUS_REASK,
1648
+ summons: { instructionEmpty: true },
1552
1649
  }, next, io);
1553
1650
  }
1554
1651
  if (admitted.role === "countersign" && (typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status))) {
@@ -1562,7 +1659,8 @@ async function auditSubmittedRole(
1562
1659
  return runPublicInstructionSeatResume({
1563
1660
  runId: admitted.runId,
1564
1661
  runDirectory: admitted.runDirectory,
1565
- summons: { instruction: officerConclusionReask(status) },
1662
+ message: officerConclusionReask(status),
1663
+ summons: { instructionEmpty: true },
1566
1664
  }, next, io);
1567
1665
  }
1568
1666
  // Self-escalation is a valid open routing state for these seats only.
@@ -23,12 +23,14 @@ import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
23
23
  import {
24
24
  ensureRoleRunDirectory,
25
25
  ensureRoleRunPlacement,
26
- formatRunLeaf,
27
26
  isUnboundRunDirectory,
28
27
  listBookRunDirectories,
29
28
  roleRunPlacement,
29
+ sessionDirectoryOf,
30
+ sessionFileIn,
30
31
  type RoleRunSubject,
31
32
  } from "../role-run-placement.ts";
33
+ import { relocateTicketProgressForLeg } from "../ticket-progress.ts";
32
34
  import type {
33
35
  DurablePrincipal,
34
36
  DurablePrincipalAuthority,
@@ -580,15 +582,101 @@ export async function recordAdmittedCorrelation(
580
582
  mutable.correlationIds = correlationIds;
581
583
  }
582
584
 
585
+ /**
586
+ * #1199 R2: registered 起居郎 legs that already belong on this ticket.
587
+ * Unbound source → whole-leg rename + publish; already under ticket → publish
588
+ * leftover staging only. No scan of unregistered siblings.
589
+ */
590
+ async function relocateRegisteredChildDiarists(input: {
591
+ readonly parentRunDirectory: string;
592
+ readonly bookKey: string;
593
+ readonly ticketNumber: number;
594
+ readonly projectRoot: string;
595
+ readonly ledgerHome: string;
596
+ readonly authority: DurablePrincipalAuthority;
597
+ }): Promise<void> {
598
+ const childRunIds = readPageSync(input.parentRunDirectory, "admitted")?.childDiaristRunIds;
599
+ for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
600
+ if (typeof childRunId !== "string") continue;
601
+ const childTarget = roleRunPlacement(input.ledgerHome, {
602
+ bookKey: input.bookKey,
603
+ subject: { ticketNumber: input.ticketNumber },
604
+ runId: childRunId,
605
+ role: "diarist",
606
+ });
607
+ const unboundChildDirectory = roleRunPlacement(input.ledgerHome, {
608
+ bookKey: input.bookKey,
609
+ subject: { unbound: true },
610
+ runId: childRunId,
611
+ role: "diarist",
612
+ }).runDirectory;
613
+
614
+ if (existsSync(unboundChildDirectory)) {
615
+ input.authority.seal(childTarget);
616
+ await bindTicketNumberOnRunDirectory(unboundChildDirectory, input.ticketNumber);
617
+ await rehomeUnboundTicketProvenance(
618
+ unboundChildDirectory,
619
+ input.ticketNumber,
620
+ input.projectRoot,
621
+ homeFromRunDirectory(input.parentRunDirectory),
622
+ );
623
+ ensureRoleRunDirectory(input.ledgerHome, dirname(childTarget.runDirectory));
624
+ // #1199: whole-leg rename first; originals travel with the directory.
625
+ await rename(unboundChildDirectory, childTarget.runDirectory);
626
+ relocateTicketProgressForLeg({
627
+ runDirectory: childTarget.runDirectory,
628
+ });
629
+ // Finished child legs will not settle again; refresh derived host.original
630
+ // after the rename commit. A derived render fault must not undo or block
631
+ // the committed placement (#1161 L1) — note and keep the true cause.
632
+ try {
633
+ renderCurrentSync(childTarget.runDirectory);
634
+ } catch (error) {
635
+ process.stderr.write(
636
+ `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
637
+ );
638
+ }
639
+ continue;
640
+ }
641
+
642
+ // Prior partial relocate left the child under the ticket with staging still held.
643
+ if (existsSync(childTarget.runDirectory)) {
644
+ relocateTicketProgressForLeg({
645
+ runDirectory: childTarget.runDirectory,
646
+ });
647
+ }
648
+ }
649
+ }
650
+
583
651
  /** File a role run under its latest typed ticket identity. */
584
652
  export async function relocateAdmittedRunToTicket(
585
653
  admitted: AdmittedRoleInvocation,
586
654
  authority: DurablePrincipalAuthority,
587
655
  heldLease?: { relocate(runDirectory: string): void },
588
656
  ): Promise<RunDirectoryRelocation | undefined> {
589
- if (admitted.ticketNumber === undefined || !isUnboundRunDirectory(admitted.runDirectory)) return undefined;
657
+ if (admitted.ticketNumber === undefined) return undefined;
658
+
659
+ const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(admitted.runDirectory));
660
+
661
+ // Already under the ticket: publish leftover progress.jsonl (self + registered children).
662
+ if (!isUnboundRunDirectory(admitted.runDirectory)) {
663
+ relocateTicketProgressForLeg({
664
+ runDirectory: admitted.runDirectory,
665
+ });
666
+ if (admitted.role !== "diarist") {
667
+ await relocateRegisteredChildDiarists({
668
+ parentRunDirectory: admitted.runDirectory,
669
+ bookKey: admitted.bookKey,
670
+ ticketNumber: admitted.ticketNumber,
671
+ projectRoot: admitted.projectRoot,
672
+ ledgerHome,
673
+ authority,
674
+ });
675
+ }
676
+ return undefined;
677
+ }
678
+
590
679
  const oldRunDirectory = admitted.runDirectory;
591
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
592
680
  const target = roleRunPlacement(ledgerHome, {
593
681
  bookKey: admitted.bookKey,
594
682
  subject: { ticketNumber: admitted.ticketNumber },
@@ -596,39 +684,18 @@ export async function relocateAdmittedRunToTicket(
596
684
  role: admitted.role,
597
685
  });
598
686
  ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
599
- // Host sealing is pure identity projection, but may reject the coordinates.
600
- // Keep that failure before the filesystem commit point.
601
- const principal = authority.seal(target);
687
+ // Pre-flight seal; final seal after the filesystem commit.
688
+ authority.seal(target);
602
689
 
603
690
  if (admitted.role !== "diarist") {
604
- const childRunIds = readPageSync(oldRunDirectory, "admitted")?.childDiaristRunIds;
605
- for (const childRunId of Array.isArray(childRunIds) ? childRunIds : []) {
606
- if (typeof childRunId !== "string") continue;
607
- const childDirectory = join(dirname(oldRunDirectory), formatRunLeaf(childRunId, "diarist"));
608
- // A child that already filed under a ticket keeps its own assertion.
609
- if (!existsSync(childDirectory)) continue;
610
- const childTarget = roleRunPlacement(ledgerHome, {
611
- bookKey: admitted.bookKey,
612
- subject: { ticketNumber: admitted.ticketNumber },
613
- runId: childRunId,
614
- role: "diarist",
615
- });
616
- authority.seal(childTarget);
617
- await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
618
- await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
619
- ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
620
- await rename(childDirectory, childTarget.runDirectory);
621
- // Finished child legs will not settle again; refresh derived host.original
622
- // after the rename commit. A derived render fault must not undo or block
623
- // the committed placement (#1161 L1) — note and keep the true cause.
624
- try {
625
- renderCurrentSync(childTarget.runDirectory);
626
- } catch (error) {
627
- process.stderr.write(
628
- `[invocation] current.json render refused after child relocate; placement kept: ${childTarget.runDirectory}: ${errorText(error)}\n`,
629
- );
630
- }
631
- }
691
+ await relocateRegisteredChildDiarists({
692
+ parentRunDirectory: oldRunDirectory,
693
+ bookKey: admitted.bookKey,
694
+ ticketNumber: admitted.ticketNumber,
695
+ projectRoot: admitted.projectRoot,
696
+ ledgerHome,
697
+ authority,
698
+ });
632
699
  }
633
700
 
634
701
  // The first ticket assignment moves only an unbound diarist's own diary.
@@ -637,21 +704,32 @@ export async function relocateAdmittedRunToTicket(
637
704
  }
638
705
 
639
706
  // Rename commits the run placement. Diary assignment above is a Sitian append
640
- // outside the atomic directory move.
641
- // Persisted paths are resolved from typed run identity on read.
707
+ // outside the atomic directory move. Session/receipts/progress travel with the leg.
642
708
  await rename(oldRunDirectory, target.runDirectory);
643
709
 
644
710
  // rename moved the open lock inode with the directory. Transfer cleanup and
645
- // admitted path ownership immediately after the commit — before any derived
646
- // render that may refuse (#1161 L1 / BASE order).
711
+ // live path ownership immediately after the commit — before final seal/render
712
+ // (#1161 L1 / BASE order). Live handles first so mid-turn setSessionFile sees
713
+ // the ticket leaf; final seal uses the post-move existence landing.
647
714
  heldLease?.relocate(target.runDirectory);
648
715
 
716
+ const actualSessionDirectory = sessionDirectoryOf(target.runDirectory);
717
+ const principal = authority.seal({
718
+ sessionDirectory: actualSessionDirectory,
719
+ sessionFile: sessionFileIn(actualSessionDirectory),
720
+ });
721
+
649
722
  rewriteAdmittedRoleRunPage(
650
723
  admitted as unknown as Record<string, unknown>,
651
724
  [{ oldRunDirectory, newRunDirectory: target.runDirectory }],
652
725
  );
653
726
  (admitted as { principal: DurablePrincipal }).principal = principal;
654
727
 
728
+ // After whole-leg rename + live-handle handoff: fill rounds and append ticket current.
729
+ relocateTicketProgressForLeg({
730
+ runDirectory: target.runDirectory,
731
+ });
732
+
655
733
  // current.json projects host.original from this run directory. Ownership is
656
734
  // already transferred; a refuse still carries the true cause to the caller
657
735
  // (#1161 L1) without leaving the leg under the unbound path.
@@ -1839,7 +1917,7 @@ type InstructionTransportSource = {
1839
1917
 
1840
1918
  function admittedTransportPromptKind(
1841
1919
  admitted: InstructionTransportSource,
1842
- ): "instruction" | "fixed-kickoff" | "baseline" | "skill-args" {
1920
+ ): "instruction" | "baseline" | "skill-args" {
1843
1921
  if (admitted.role === undefined) return "instruction";
1844
1922
  const record = packagedRoleMetadata(admitted.role);
1845
1923
  if (record !== undefined && "transportPrompt" in record) return record.transportPrompt;
@@ -1870,19 +1948,15 @@ export function appendCallerFileFlagPaths(
1870
1948
 
1871
1949
  /**
1872
1950
  * One initial prompt transport. The registry `transportPrompt` leaf selects
1873
- * a fixed kickoff, a bound baseline, or skill args. Absent means the caller
1874
- * instruction plus caller file-flag paths (ADR 0087).
1951
+ * a bound baseline or skill args. Absent means the caller instruction plus
1952
+ * caller file-flag paths (ADR 0087). Notary dialogue is owned by
1953
+ * resolveReviewSeatDialogueBody (review fixed dispatch), not an empty transport leaf.
1875
1954
  * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
1876
1955
  */
1877
1956
  export function buildInstructionTransportPrompt(
1878
1957
  admitted: InstructionTransportSource,
1879
1958
  ): string {
1880
1959
  const kind = admittedTransportPromptKind(admitted);
1881
- if (kind === "fixed-kickoff") {
1882
- // #1166 / ADR 0087: audited-run identity rides startup materials; dialogue
1883
- // prompt is peer body / reask only — never a package-computed directory path.
1884
- return "";
1885
- }
1886
1960
  if (kind === "baseline") {
1887
1961
  if (admitted.baseRevision === undefined) {
1888
1962
  throw new Error("baseline transport prompt is missing the bound revision");