@akagilnc/pi-workflow-roles 0.1.5756 → 0.1.5775

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.
@@ -29,7 +29,7 @@ import { CliUsageError } from "./cli-errors.js";
29
29
  import { admitPublicRole, admittedSourceRunPath, bindAdmittedTicketNumber, appendCallerFileFlagPaths, buildInstructionTransportPrompt, materializeCountersignInvocation, persistAdmittedAuditedSubmissionToolCallId, persistAdmittedSourceRunPath, readAdmittedAuditedSubmissionToolCallId, recordAdmittedCorrelation, recordChildDiaristRun, relocateAdmittedRunToTicket, } from "./invocation.js";
30
30
  import { bindDiaristAssertedTicketNumber, presentControlledFailure, packageFaultNoteFor, prepareSummonsResumeMaterials, roleTurnOptions, runPostAdmissionOneShot, runPostAdmissionResumable, runPostAdmissionSeatResume, showResumeErrorPointer, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
31
31
  import { presentTerminal } from "./auto-resume.js";
32
- import { buildAutoResumeContinuationPrompt, loadResumablePublicRole, markRunAdmitted, readCurrentCourt, } from "./run-lifecycle.js";
32
+ import { loadResumablePublicRole, markRunAdmitted, readCurrentCourt, } from "./run-lifecycle.js";
33
33
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
34
34
  import { presentStructuralRejection, readBoundSessionEntries, trySettlePublicSeat, } from "./settlement.js";
35
35
  import { warnMissingMethodSkills } from "./machine-method-skills.js";
@@ -49,6 +49,20 @@ const POST_SUBMISSION_ROUTING = {
49
49
  judge: {
50
50
  statuses: REVIEW_QUEUE_STATUSES,
51
51
  },
52
+ // #1208 C1 / ADR 0055: every review-queue seat uses the shared
53
+ // unreadable-status seam (officerConclusionReask). Independent public
54
+ // entry for notary/auditor/inspector was the same miss — route here, do
55
+ // not invent a parallel loop. Gate summons still own the outer budget
56
+ // via nestedIncomplete when this seat exhausts unsettled.
57
+ notary: {
58
+ statuses: REVIEW_QUEUE_STATUSES,
59
+ },
60
+ auditor: {
61
+ statuses: REVIEW_QUEUE_STATUSES,
62
+ },
63
+ inspector: {
64
+ statuses: REVIEW_QUEUE_STATUSES,
65
+ },
52
66
  coder: {
53
67
  statuses: WORKER_ROUTING_STATUSES,
54
68
  reask: WORKER_STATUS_REASK,
@@ -160,10 +174,13 @@ async function reaskUnreadablePostSubmissionStatus(admitted, terminal, env, io,
160
174
  const next = withUnreadableReask(env);
161
175
  if (next === undefined)
162
176
  return UNREADABLE_REASK_EXHAUSTED;
177
+ // Named status-reask: summons opens a new court; message carries the diagnostic
178
+ // (not arbitrary summons.instruction passthrough through the review resolver).
163
179
  return runPublicInstructionSeatResume({
164
180
  runId: admitted.runId,
165
181
  runDirectory: admitted.runDirectory,
166
- summons: { instruction: message },
182
+ message,
183
+ summons: { instructionEmpty: true },
167
184
  }, next, io);
168
185
  }
169
186
  /**
@@ -201,10 +218,13 @@ async function reaskMissingTicketOnce(admitted, terminal, env, io) {
201
218
  softTicketReaskTurn: true,
202
219
  softTicketReaskOwnership,
203
220
  };
221
+ // Named #1171 missing-ticket: summons opens a new court; message carries the
222
+ // package reask copy (not arbitrary summons.instruction passthrough).
204
223
  const result = await runPublicInstructionSeatResume({
205
224
  runId: admitted.runId,
206
225
  runDirectory: admitted.runDirectory,
207
- summons: { instruction },
226
+ message: instruction,
227
+ summons: { instructionEmpty: true },
208
228
  }, softReaskEnv, io);
209
229
  if (softTicketReaskOwnership.sealedSubstitute
210
230
  || terminalCarriesSealedSubmission(result.terminal)) {
@@ -323,8 +343,8 @@ export function buildInstructionSeatTurnRequest(admitted, options) {
323
343
  }
324
344
  /**
325
345
  * Direct review-seat call: reconnect ledger peer-body delivery (ADR 0085 / #1166).
326
- * Gate summons already ride gateReviewInstruction; direct --source-run must not
327
- * leave a blank fixed-kickoff.
346
+ * Gate summons already ride gateReviewInstruction; direct --source-run falls
347
+ * through the shared dialogue resolver (notary: fixed dispatch).
328
348
  */
329
349
  async function loadLedgerPeerBody(sourceRunDirectory, projectRoot, home) {
330
350
  const runId = runIdFromRunDirectory(sourceRunDirectory);
@@ -344,40 +364,47 @@ async function loadLedgerPeerBody(sourceRunDirectory, projectRoot, home) {
344
364
  return undefined;
345
365
  return readableGateItem(latest.accepted);
346
366
  }
347
- /** Owner-approved fixed Notary dispatch (2026-10-09); never the parent verdict. */
367
+ /** Owner-approved fixed Notary review dispatch (2026-10-09 #1208); never the parent verdict. */
348
368
  export const NOTARY_FIXED_DISPATCH = "请审本轮受审对象,按符宝郎职掌交卷。";
349
369
  /**
350
370
  * One delivery rule for review-seat dialogue on new turns and same-parent resume
351
- * (#1166 / ADR 0085 / ADR 0087): explicit reask → non-empty caller dispatch →
352
- * source-only ledger peer body. Identity stays in startup materials.
353
- * #1195: ticket-court notary skips peer-body preload (countersign source only) and
354
- * receives the fixed dispatch instead (codex rejects an empty prompt).
371
+ * (#1166 / ADR 0085 / ADR 0087 / #1208): notary review uses fixed dispatch;
372
+ * named operational reasks ride reviewReask (first-mint / gate) or the resume
373
+ * request message seam — never arbitrary non-empty caller/summons passthrough.
374
+ * Other seats prefer format reask, then gate body, then caller dispatch, then
375
+ * ledger peer. Identity stays in startup materials. Notary never takes
376
+ * gate/parent verdict as dialogue (gateReviewInstruction stays unread for notary).
355
377
  */
356
- async function resolveReviewSeatDialogueBody(input) {
357
- const reask = input.reask;
358
- if (reask !== undefined && reask.length > 0)
359
- return reask;
378
+ export async function resolveReviewSeatDialogueBody(input) {
379
+ // #1208: fixed sentence is the review function only (owner 37bfe80d /
380
+ // 393daf98). reviewReask is the named operational override; caller/summons
381
+ // body is not a free passthrough (countersign rejected that prescription).
382
+ if (input.role === "notary") {
383
+ if (input.reviewReask !== undefined)
384
+ return input.reviewReask;
385
+ return NOTARY_FIXED_DISPATCH;
386
+ }
387
+ const preferred = input.reviewReask ?? input.gateReviewInstruction;
388
+ if (preferred !== undefined && preferred.length > 0)
389
+ return preferred;
360
390
  const caller = input.callerInstruction ?? "";
361
391
  if (caller.length > 0)
362
392
  return caller;
363
393
  const sourceRunPath = admittedSourceRunPath(input)?.trim();
364
394
  if (sourceRunPath === undefined)
365
395
  return undefined;
366
- if (input.role === "notary") {
367
- const { isTicketCourtCountersignSource } = await import("../run-terminal-artifacts.js");
368
- if (isTicketCourtCountersignSource(sourceRunPath))
369
- return NOTARY_FIXED_DISPATCH;
370
- }
371
396
  return await loadLedgerPeerBody(sourceRunPath, input.projectRoot, input.home);
372
397
  }
373
398
  async function resolveInitialPrompt(admitted, env) {
374
399
  const record = roleRecord(admitted.role);
375
400
  if ("reaskPrompt" in record && record.reaskPrompt === true) {
376
401
  const sourceRunPath = admittedSourceRunPath(admitted);
377
- const reask = env.reviewReask ?? env.gateReviewInstruction;
378
402
  const body = await resolveReviewSeatDialogueBody({
379
403
  role: admitted.role,
380
- ...(reask === undefined ? {} : { reask }),
404
+ ...(env.reviewReask === undefined ? {} : { reviewReask: env.reviewReask }),
405
+ ...(env.gateReviewInstruction === undefined
406
+ ? {}
407
+ : { gateReviewInstruction: env.gateReviewInstruction }),
381
408
  callerInstruction: admitted.instruction,
382
409
  ...(sourceRunPath === undefined ? {} : { sourceRunPath }),
383
410
  projectRoot: admitted.projectRoot,
@@ -489,6 +516,7 @@ async function dispatchAdmitted(admitted, env, io) {
489
516
  const initialPromptText = await resolveInitialPrompt(admitted, activeEnv);
490
517
  const auto = "inCallAutoResume" in record && record.inCallAutoResume === true;
491
518
  if (auto) {
519
+ // #1208: auto-resume resends this turn's actual content, never a filler phrase.
492
520
  return await runPostAdmissionResumable({
493
521
  admitted,
494
522
  env: activeEnv,
@@ -499,7 +527,7 @@ async function dispatchAdmitted(admitted, env, io) {
499
527
  })),
500
528
  buildResumeRequest: () => buildInstructionSeatTurnRequest(admitted, roleTurnOptions(activeEnv, admitted, {
501
529
  kind: "resume",
502
- prompt: buildAutoResumeContinuationPrompt(),
530
+ prompt: initialPromptText,
503
531
  })),
504
532
  adapters,
505
533
  ...(activeEnv.engine === undefined ? {} : { effectiveEngine: activeEnv.engine }),
@@ -607,22 +635,31 @@ function resumeSameParentInstructionSeat(input) {
607
635
  ...(input.ticketNumber === undefined ? {} : { ticketNumber: input.ticketNumber }),
608
636
  freshSummons: input.env.freshSummons,
609
637
  summons: input.summons,
610
- resume: (runId, materials, runDirectory) => runPublicInstructionSeatResume({
611
- runId,
612
- runDirectory,
613
- ...(materials === undefined ? {} : { summons: materials }),
614
- }, input.env, input.io),
638
+ resume: (runId, materials, runDirectory) => {
639
+ // Named operational reask on this summon: explicit message seam.
640
+ // Review dialogue stays on summons → shared resolver (notary: fixed).
641
+ // Do not stuff reask into summons.instruction for later passthrough.
642
+ const reask = input.env.reviewReask;
643
+ return runPublicInstructionSeatResume({
644
+ runId,
645
+ runDirectory,
646
+ ...(reask === undefined ? {} : { message: reask }),
647
+ ...(materials === undefined ? {} : { summons: materials }),
648
+ }, input.env, input.io);
649
+ },
615
650
  });
616
651
  }
617
652
  /**
618
653
  * Same delivery rule as resolveInitialPrompt for same-parent resume summons.
619
- * Reask / explicit caller text / source-only ledger peer — one authority.
654
+ * Shared resolver owns the input choice — callers only pass env fields.
620
655
  */
621
656
  async function sameParentDialogue(env, fallback, sourceRunPath, projectRoot, role) {
622
- const reask = env.reviewReask ?? env.gateReviewInstruction;
623
657
  const body = await resolveReviewSeatDialogueBody({
624
658
  role,
625
- ...(reask === undefined ? {} : { reask }),
659
+ ...(env.reviewReask === undefined ? {} : { reviewReask: env.reviewReask }),
660
+ ...(env.gateReviewInstruction === undefined
661
+ ? {}
662
+ : { gateReviewInstruction: env.gateReviewInstruction }),
626
663
  callerInstruction: fallback.instruction,
627
664
  sourceRunPath,
628
665
  projectRoot,
@@ -638,15 +675,20 @@ async function sameParentDialogue(env, fallback, sourceRunPath, projectRoot, rol
638
675
  * then the same one-shot settlement as every other seat.
639
676
  */
640
677
  async function runCountersignBody(parsed, env, io) {
678
+ const projectRoot = resolve(parsed.project ?? env.cwd);
641
679
  const gateParentRunPath = typeof env.parentRunPath === "string" && env.parentRunPath.trim() !== ""
642
680
  ? env.parentRunPath
643
681
  : undefined;
644
682
  if (gateParentRunPath !== undefined) {
645
- const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction ?? parsed.instruction ?? "";
683
+ // #1208 C1: same shared dialogue resolver as every other same-parent seat.
684
+ const dialogue = await sameParentDialogue(env, {
685
+ instruction: parsed.instruction ?? "",
686
+ instructionEmpty: (parsed.instruction ?? "").length === 0,
687
+ }, gateParentRunPath, projectRoot, "countersign");
646
688
  const resumed = await resumeSameParentInstructionSeat({
647
689
  env,
648
690
  io,
649
- projectRoot: resolve(parsed.project ?? env.cwd),
691
+ projectRoot,
650
692
  role: "countersign",
651
693
  parentRunPath: gateParentRunPath,
652
694
  ...(isSafePositiveTicketNumber(env.boundTicketNumber)
@@ -654,8 +696,8 @@ async function runCountersignBody(parsed, env, io) {
654
696
  : {}),
655
697
  summons: {
656
698
  sourceRunPath: gateParentRunPath,
657
- instruction: resumeInstruction,
658
- instructionEmpty: resumeInstruction.length === 0,
699
+ instruction: dialogue.instruction,
700
+ instructionEmpty: dialogue.instructionEmpty,
659
701
  // #1166 J11: same-parent resume delivers this call's file-flag paths
660
702
  // through the shared summons → appendCallerFileFlagPaths rule.
661
703
  attachmentPaths: parsed.attachmentPaths ?? [],
@@ -691,14 +733,10 @@ async function runCountersignBody(parsed, env, io) {
691
733
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
692
734
  admitted = { ...admitted, sourceRunPath: gateParentRunPath };
693
735
  }
694
- // #1166 J11: review-body branch must merge the same file-flag delivery as
695
- // resolveInitialPrompt / resumeTurnRequestProjectionOptions — never body alone.
696
- const reviewBody = env.reviewReask ?? env.gateReviewInstruction;
736
+ // #1208 C1 / #1166 J11: one shared initial-prompt seam (incl. file flags).
697
737
  const turnProjection = roleTurnOptions(env, admitted, {
698
738
  kind: "initial",
699
- prompt: reviewBody !== undefined
700
- ? appendCallerFileFlagPaths(reviewBody, admitted.attachments)
701
- : buildInstructionTransportPrompt(admitted),
739
+ prompt: await resolveInitialPrompt(admitted, env),
702
740
  });
703
741
  const turnRequest = buildInstructionSeatTurnRequest(admitted, turnProjection);
704
742
  const result = await runPostAdmissionOneShot({
@@ -902,6 +940,10 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
902
940
  await persistAdmittedSourceRunPath(admitted, auditorSource, auditorSubject);
903
941
  admitted = { ...admitted, sourceRunPath: auditorSource };
904
942
  }
943
+ // #1208 C2: same helper as resume — known board ticket on the audited
944
+ // source binds and relocates before the missing-ticket soft reask can
945
+ // fire. True no-ticket sources stay unbound (#1171 asks once).
946
+ admitted = await inheritKnownTicketFromSource(admitted, env.principalAuthority);
905
947
  const runAdmitted = async () => {
906
948
  await markRunAdmitted(admitted, env.principalAuthority);
907
949
  if (role === "secretariat") {
@@ -946,6 +988,24 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
946
988
  }
947
989
  return runAdmitted();
948
990
  }
991
+ /**
992
+ * #1208 C2: same as new source-locator admission — when this leg is still
993
+ * unbound, inherit the board ticket already on the audited source and relocate.
994
+ * True no-ticket sources stay unbound so #1171 still asks once.
995
+ */
996
+ async function inheritKnownTicketFromSource(admitted, authority) {
997
+ if (admitted.ticketNumber !== undefined)
998
+ return admitted;
999
+ const sourcePath = admittedSourceRunPath(admitted);
1000
+ if (sourcePath === undefined)
1001
+ return admitted;
1002
+ const sourceTicket = await readBoardTicketNumber(sourcePath);
1003
+ if (sourceTicket === undefined)
1004
+ return admitted;
1005
+ await bindAdmittedTicketNumber(admitted, sourceTicket);
1006
+ await relocateAdmittedRunToTicket(admitted, authority);
1007
+ return admitted;
1008
+ }
949
1009
  export async function runPublicInstructionSeatResume(request, env, io) {
950
1010
  let selectedRequest = request;
951
1011
  const resume = async () => {
@@ -960,24 +1020,48 @@ export async function runPublicInstructionSeatResume(request, env, io) {
960
1020
  io: turnIo,
961
1021
  load: async (effective) => {
962
1022
  const loaded = await loadResumablePublicRole(env.home, effective.runId, env.principalAuthority, effective.runDirectory === undefined ? undefined : { runDirectory: effective.runDirectory });
963
- if (packagedRebindSourceOnResume(loaded.admitted.role)
1023
+ let admitted = loaded.admitted;
1024
+ if (packagedRebindSourceOnResume(admitted.role)
964
1025
  && effective.summons?.sourceRunPath !== undefined
965
1026
  && effective.summons.sourceRun !== undefined) {
966
- return {
967
- ...loaded,
968
- admitted: {
969
- ...loaded.admitted,
970
- sourceRunPath: effective.summons.sourceRunPath,
971
- sourceRun: effective.summons.sourceRun,
972
- },
1027
+ // source-locator seats only (notary); sourceRun is on that admission face.
1028
+ admitted = {
1029
+ ...admitted,
1030
+ sourceRunPath: effective.summons.sourceRunPath,
1031
+ sourceRun: effective.summons.sourceRun,
973
1032
  };
974
1033
  }
975
- return loaded;
1034
+ admitted = await inheritKnownTicketFromSource(admitted, env.principalAuthority);
1035
+ return { ...loaded, admitted };
976
1036
  },
977
1037
  buildTurnRequest: async (admitted, effective) => {
978
1038
  const summonsPrepared = await prepareSummonsResumeMaterials(admitted.runDirectory, effective.summons);
1039
+ const projection = resumeTurnRequestProjectionOptions(admitted, effective, env, summonsPrepared);
1040
+ // #1208: internal summons only — shared resolver owns review dialogue.
1041
+ // Public bare resume / explicit message keep projection (operational
1042
+ // reasks use the message seam). Do not reselect stale env dialogue over
1043
+ // this-round summons (#1208 R1).
1044
+ if (effective.summons === undefined || effective.message !== undefined) {
1045
+ return buildInstructionSeatTurnRequest(admitted, { ...projection });
1046
+ }
1047
+ const sourceRunPath = admittedSourceRunPath(admitted);
1048
+ const body = await resolveReviewSeatDialogueBody({
1049
+ role: admitted.role,
1050
+ callerInstruction: summonsPrepared?.instruction ?? "",
1051
+ ...(sourceRunPath === undefined ? {} : { sourceRunPath }),
1052
+ projectRoot: admitted.projectRoot,
1053
+ home: env.home,
1054
+ });
1055
+ if (body === undefined) {
1056
+ return buildInstructionSeatTurnRequest(admitted, { ...projection });
1057
+ }
1058
+ const fileFlags = summonsPrepared?.attachments ?? [];
1059
+ const prompt = fileFlags.length === 0
1060
+ ? body
1061
+ : appendCallerFileFlagPaths(body, fileFlags);
979
1062
  return buildInstructionSeatTurnRequest(admitted, {
980
- ...resumeTurnRequestProjectionOptions(admitted, effective, env, summonsPrepared),
1063
+ ...projection,
1064
+ continuation: { kind: "resume", prompt },
981
1065
  });
982
1066
  },
983
1067
  adapters: {
@@ -1037,9 +1121,12 @@ async function queueConclusionFromChild(child, env, io) {
1037
1121
  return { stop: { exitCode: 0, ...(terminal === undefined ? {} : { terminal }) } };
1038
1122
  }
1039
1123
  let current = child;
1040
- // Notary, auditor, and inspector have no seat-local status reask. This loop
1041
- // is their one budget. The child resume keeps the caller's env so delivery,
1042
- // failure recovery, and other reask loops stay separate.
1124
+ // #1208: review-queue gate children also have seat-local
1125
+ // POST_SUBMISSION_ROUTING. This parent-side loop remains the
1126
+ // gate/correlation budget; nestedIncomplete from an exhausted seat-local
1127
+ // chain surfaces without a second spend. The child resume keeps the
1128
+ // caller's env so delivery, failure recovery, and other reask loops
1129
+ // stay separate.
1043
1130
  let budgetEnv = envForCourtContinue(env);
1044
1131
  for (;;) {
1045
1132
  const terminal = await trySettlePublicSeat(current, env.principalAuthority, { ...await readCurrentCourt(current.runDirectory), ...packageFaultScope(current, env, io) });
@@ -1177,7 +1264,8 @@ async function auditSubmittedRole(turn, env, io, resumedOfficer, passedReceipt)
1177
1264
  return runPublicInstructionSeatResume({
1178
1265
  runId: admitted.runId,
1179
1266
  runDirectory: admitted.runDirectory,
1180
- summons: { instruction: SECRETARIAT_STATUS_REASK },
1267
+ message: SECRETARIAT_STATUS_REASK,
1268
+ summons: { instructionEmpty: true },
1181
1269
  }, next, io);
1182
1270
  }
1183
1271
  if (admitted.role === "countersign" && (typeof status !== "string" || !REVIEW_QUEUE_STATUSES.has(status))) {
@@ -1192,7 +1280,8 @@ async function auditSubmittedRole(turn, env, io, resumedOfficer, passedReceipt)
1192
1280
  return runPublicInstructionSeatResume({
1193
1281
  runId: admitted.runId,
1194
1282
  runDirectory: admitted.runDirectory,
1195
- summons: { instruction: officerConclusionReask(status) },
1283
+ message: officerConclusionReask(status),
1284
+ summons: { instructionEmpty: true },
1196
1285
  }, next, io);
1197
1286
  }
1198
1287
  // Self-escalation is a valid open routing state for these seats only.
@@ -1131,17 +1131,13 @@ export function appendCallerFileFlagPaths(body, attachments, requestManifestPath
1131
1131
  }
1132
1132
  /**
1133
1133
  * One initial prompt transport. The registry `transportPrompt` leaf selects
1134
- * a fixed kickoff, a bound baseline, or skill args. Absent means the caller
1135
- * instruction plus caller file-flag paths (ADR 0087).
1134
+ * a bound baseline or skill args. Absent means the caller instruction plus
1135
+ * caller file-flag paths (ADR 0087). Notary dialogue is owned by
1136
+ * resolveReviewSeatDialogueBody (review fixed dispatch), not an empty transport leaf.
1136
1137
  * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
1137
1138
  */
1138
1139
  export function buildInstructionTransportPrompt(admitted) {
1139
1140
  const kind = admittedTransportPromptKind(admitted);
1140
- if (kind === "fixed-kickoff") {
1141
- // #1166 / ADR 0087: audited-run identity rides startup materials; dialogue
1142
- // prompt is peer body / reask only — never a package-computed directory path.
1143
- return "";
1144
- }
1145
1141
  if (kind === "baseline") {
1146
1142
  if (admitted.baseRevision === undefined) {
1147
1143
  throw new Error("baseline transport prompt is missing the bound revision");