@akagilnc/pi-workflow-roles 0.1.5013 → 0.1.5037

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.
@@ -258,9 +258,7 @@ async function runCountersignBody(parsed, env, io) {
258
258
  projectRoot: resolve(parsed.project ?? env.cwd),
259
259
  role: "countersign",
260
260
  parentRunPath: gateParentRunPath,
261
- ...(env.boundTicketNumber === undefined
262
- ? {}
263
- : { ticketNumber: env.boundTicketNumber }),
261
+ ...(isSafePositiveTicketNumber(env.boundTicketNumber) ? { ticketNumber: env.boundTicketNumber } : {}),
264
262
  freshSummons: env.freshSummons,
265
263
  summons: {
266
264
  sourceRunPath: gateParentRunPath,
@@ -296,14 +294,16 @@ async function runCountersignBody(parsed, env, io) {
296
294
  let typedTicket;
297
295
  let typedCourtTicketNumbers;
298
296
  let identityDiaristRan = false;
299
- let unboundDiaristRunId;
300
- if (env.runCourtDiaristStation === undefined) {
297
+ let childDiaristRunId;
298
+ let pausedDiaristTerminal;
299
+ if (gateParentRunPath === undefined && env.runCourtDiaristStation === undefined) {
301
300
  let outcome;
302
301
  try {
303
302
  outcome = await invokeCourtDiarist({
304
303
  instruction: parsed.instruction ?? "",
305
304
  projectRoot: admitted.projectRoot,
306
305
  failureLabel: "unbound summons",
306
+ correlationId: admitted.runId,
307
307
  ...(env.boundTicketNumber === undefined ? {} : { boundTicketNumber: env.boundTicketNumber }),
308
308
  }, env, io);
309
309
  }
@@ -318,16 +318,16 @@ async function runCountersignBody(parsed, env, io) {
318
318
  }, seatAdapters(admitted, env), env.principalAuthority, io);
319
319
  }
320
320
  identityDiaristRan = true;
321
+ if (outcome.identity.kind === "escalate")
322
+ pausedDiaristTerminal = outcome.terminal;
321
323
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
322
- unboundDiaristRunId = outcome.admitted.runId;
324
+ childDiaristRunId = outcome.admitted.runId;
323
325
  }
324
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
325
- const diagnostic = outcome.identity.kind === "escalate"
326
- ? outcome.identity.diagnostic
327
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
326
+ if (outcome.failedWithoutEscalate !== undefined) {
327
+ const diagnostic = outcome.failedWithoutEscalate.diagnostic;
328
328
  await materializeAdmission(isSafePositiveTicketNumber(env.boundTicketNumber) ? env.boundTicketNumber : undefined);
329
- if (unboundDiaristRunId !== undefined)
330
- await recordChildDiaristRun(admitted, unboundDiaristRunId);
329
+ if (childDiaristRunId !== undefined)
330
+ await recordChildDiaristRun(admitted, childDiaristRunId);
331
331
  await markRunAdmitted(admitted, env.principalAuthority);
332
332
  return await presentControlledFailure(admitted, {
333
333
  timedOut: false,
@@ -337,19 +337,19 @@ async function runCountersignBody(parsed, env, io) {
337
337
  }, seatAdapters(admitted, env), env.principalAuthority, io);
338
338
  }
339
339
  if (outcome.identity.kind === "ticket") {
340
- typedTicket = isSafePositiveTicketNumber(env.boundTicketNumber)
341
- ? env.boundTicketNumber
342
- : outcome.identity.ticketNumber;
340
+ typedTicket = outcome.identity.ticketNumber;
343
341
  typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
344
342
  }
345
- else if (isSafePositiveTicketNumber(env.boundTicketNumber)) {
346
- typedTicket = env.boundTicketNumber;
347
- }
348
343
  }
349
344
  await materializeAdmission(typedTicket);
350
345
  await markRunAdmitted(admitted, env.principalAuthority);
351
- if (unboundDiaristRunId !== undefined) {
352
- await recordChildDiaristRun(admitted, unboundDiaristRunId);
346
+ if (childDiaristRunId !== undefined) {
347
+ await recordChildDiaristRun(admitted, childDiaristRunId);
348
+ }
349
+ if (pausedDiaristTerminal !== undefined) {
350
+ // The child already submitted its own pause; no parent turn exists yet.
351
+ io.stdout(formatTerminalResult(pausedDiaristTerminal));
352
+ return { exitCode: 0, admitted, terminal: pausedDiaristTerminal };
353
353
  }
354
354
  if (gateParentRunPath !== undefined) {
355
355
  await persistAdmittedSourceRunPath(admitted, gateParentRunPath);
@@ -401,7 +401,11 @@ async function runCountersignBody(parsed, env, io) {
401
401
  if (!packagedAdmitsCountersign(admittedSeat.role))
402
402
  return;
403
403
  if (!identityDiaristRan || typedTicket !== undefined) {
404
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
404
+ const refresh = await runCountersignCourtDiaristStation(admittedSeat, env, io);
405
+ if (refresh !== undefined) {
406
+ io.stdout(formatTerminalResult(refresh.terminal));
407
+ return refresh.terminal;
408
+ }
405
409
  }
406
410
  await relocateAdmittedRunToTicket(admittedSeat, env.principalAuthority, lease);
407
411
  Object.assign(turnRequest, buildInstructionSeatTurnRequest(admittedSeat, turnProjection));
@@ -486,7 +490,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
486
490
  projectRoot,
487
491
  role,
488
492
  parentRunPath: sourceDirectory,
489
- ...(auditorTicket === undefined ? {} : { ticketNumber: auditorTicket }),
490
493
  freshSummons: env.freshSummons,
491
494
  summons,
492
495
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -511,7 +514,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
511
514
  projectRoot,
512
515
  role,
513
516
  parentRunPath,
514
- ...(env.boundTicketNumber === undefined ? {} : { ticketNumber: env.boundTicketNumber }),
515
517
  freshSummons: env.freshSummons,
516
518
  summons,
517
519
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -537,7 +539,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
537
539
  throw error;
538
540
  }
539
541
  const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
540
- const knownTicket = (await readBoardTicketNumber(source.runDirectory)) ?? env.boundTicketNumber;
541
542
  const summons = {
542
543
  sourceRunPath: source.runDirectory,
543
544
  sourceRun: source,
@@ -548,7 +549,6 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
548
549
  projectRoot,
549
550
  role,
550
551
  parentRunPath: source.runDirectory,
551
- ...(knownTicket === undefined ? {} : { ticketNumber: knownTicket }),
552
552
  freshSummons: env.freshSummons,
553
553
  summons,
554
554
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -579,54 +579,34 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
579
579
  }
580
580
  const runAdmitted = async () => {
581
581
  await markRunAdmitted(admitted, env.principalAuthority);
582
- if (record.sameParent === "court-diarist") {
583
- const { invokeCourtDiarist } = await import("./countersign-run.js");
584
- const outcome = await invokeCourtDiarist({
585
- instruction: parsed.instruction ?? "",
586
- projectRoot: admitted.projectRoot,
587
- failureLabel: "secretariat unbound summons",
588
- attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
589
- correlationId: admitted.runId,
590
- }, {
591
- cwd: env.cwd,
592
- home: env.home,
593
- agentDir: env.agentDir,
594
- packageRoot: env.packageRoot,
595
- ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
596
- ...(env.signal === undefined ? {} : { signal: env.signal }),
597
- ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
598
- }, io);
582
+ if (role === "secretariat") {
583
+ let outcome;
584
+ try {
585
+ outcome = await invokeCourtDiarist({
586
+ instruction: parsed.instruction ?? "",
587
+ projectRoot: admitted.projectRoot,
588
+ failureLabel: "secretariat unbound summons",
589
+ attachmentPaths: admitted.attachments.map((attachment) => attachment.frozenPath),
590
+ correlationId: admitted.runId,
591
+ }, env, io);
592
+ }
593
+ catch (error) {
594
+ return await presentControlledFailure(admitted, {
595
+ timedOut: false, code: null, stderr: "", thrown: error,
596
+ }, seatAdapters(admitted, env), env.principalAuthority, io);
597
+ }
599
598
  if (outcome.admitted?.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) {
600
599
  await recordChildDiaristRun(admitted, outcome.admitted.runId);
601
600
  }
602
- if (outcome.identity.kind === "escalate" || outcome.failedWithoutEscalate !== undefined) {
603
- const diagnostic = outcome.identity.kind === "escalate"
604
- ? outcome.identity.diagnostic
605
- : outcome.failedWithoutEscalate?.diagnostic ?? "";
601
+ if (outcome.failedWithoutEscalate !== undefined) {
606
602
  return await presentControlledFailure(admitted, {
607
- timedOut: false,
608
- code: null,
609
- stderr: "",
610
- thrown: new Error(diagnostic),
603
+ timedOut: false, code: null, stderr: "",
604
+ thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
611
605
  }, seatAdapters(admitted, env), env.principalAuthority, io);
612
606
  }
613
- if (outcome.identity.kind === "ticket") {
614
- try {
615
- if (outcome.admitted === undefined)
616
- throw new Error("court diarist ticket has no admitted run");
617
- await bindAdmittedTicketNumber(outcome.admitted, outcome.identity.ticketNumber);
618
- await relocateAdmittedRunToTicket(outcome.admitted, env.principalAuthority);
619
- await bindAdmittedTicketNumber(admitted, outcome.identity.ticketNumber);
620
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
621
- }
622
- catch (error) {
623
- return await presentControlledFailure(admitted, {
624
- timedOut: false,
625
- code: null,
626
- stderr: "",
627
- thrown: error,
628
- }, seatAdapters(admitted, env), env.principalAuthority, io);
629
- }
607
+ if (outcome.identity.kind === "escalate" && outcome.terminal !== undefined) {
608
+ io.stdout(formatTerminalResult(outcome.terminal));
609
+ return { exitCode: 0, admitted, terminal: outcome.terminal };
630
610
  }
631
611
  }
632
612
  return dispatchAdmitted(admitted, env, io);
@@ -689,3 +669,19 @@ export async function runPublicInstructionSeatResume(request, env, io) {
689
669
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
690
670
  });
691
671
  }
672
+ /** Continue the parent once any escalated child has submitted. */
673
+ export async function continueParentAfterChild(parentRunId, child, env, io) {
674
+ const loaded = await loadResumablePublicRole(env.home, parentRunId, env.principalAuthority, true);
675
+ if (loaded.run.state !== "admitted") {
676
+ return runPublicInstructionSeatResume({ runId: parentRunId }, env, io);
677
+ }
678
+ const admitted = loaded.admitted;
679
+ if (admitted.role === "countersign" && admitted.ticketNumber !== undefined) {
680
+ const refresh = await runCountersignCourtDiaristStation(admitted, env, io, child.ticketNumber);
681
+ if (refresh !== undefined) {
682
+ io.stdout(formatTerminalResult(refresh.terminal));
683
+ return { exitCode: 0, admitted, terminal: refresh.terminal };
684
+ }
685
+ }
686
+ return dispatchAdmitted(admitted, env, io);
687
+ }
@@ -9,7 +9,7 @@ import { tmpdir } from "node:os";
9
9
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
10
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
11
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
- import { ensureRoleRunDirectory, ensureRoleRunPlacement, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
12
+ import { ensureRoleRunDirectory, ensureRoleRunPlacement, listBookRunDirectories, roleRunArtifactsDirectory, roleRunPlacement, } from "../role-run-placement.js";
13
13
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
14
  import { isSafePositiveTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
15
  import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
@@ -164,15 +164,11 @@ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
164
164
  /**
165
165
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
166
166
  * object and both durable pages (invocation.json + admitted-request.json).
167
- * Used by known-ticket reuse when admission was unbound (#635 / #709).
168
- * Never clears an existing binding.
167
+ * A later typed role receipt replaces an earlier assertion (#1025).
169
168
  */
170
169
  export async function bindAdmittedTicketNumber(admitted, ticketNumber) {
171
- if (admitted.ticketNumber !== undefined) {
172
- if (admitted.ticketNumber === ticketNumber)
173
- return;
174
- throw new Error(`bindAdmittedTicketNumber refuses to replace existing ticket #${admitted.ticketNumber} with #${ticketNumber}`);
175
- }
170
+ if (admitted.ticketNumber === ticketNumber)
171
+ return;
176
172
  await bindTicketNumberOnRunDirectory(admitted.runDirectory, ticketNumber);
177
173
  admitted.ticketNumber = ticketNumber;
178
174
  }
@@ -229,7 +225,7 @@ export async function bindCourtTicketNumbersOnAdmitted(admitted, courtTicketNumb
229
225
  });
230
226
  admitted.courtTicketNumbers = frozen;
231
227
  }
232
- /** Move a settled first-entry run from unbound to its asserted ticket directory. */
228
+ /** File a role run under its latest typed ticket identity. */
233
229
  export async function relocateAdmittedRunToTicket(admitted, authority, heldLease) {
234
230
  if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`))
235
231
  return undefined;
@@ -252,6 +248,9 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
252
248
  if (typeof childRunId !== "string")
253
249
  continue;
254
250
  const childDirectory = join(dirname(oldRunDirectory), `${childRunId}@diarist`);
251
+ // A child that already filed under a ticket keeps its own assertion.
252
+ if (!existsSync(childDirectory))
253
+ continue;
255
254
  const childTarget = roleRunPlacement(ledgerHome, {
256
255
  bookKey: admitted.bookKey,
257
256
  subject: { ticketNumber: admitted.ticketNumber },
@@ -259,21 +258,18 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
259
258
  role: "diarist",
260
259
  });
261
260
  authority.seal(childTarget);
262
- if (!existsSync(childDirectory) && existsSync(childTarget.runDirectory))
263
- continue;
264
261
  await bindTicketNumberOnRunDirectory(childDirectory, admitted.ticketNumber);
265
262
  await rehomeUnboundTicketProvenance(childDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
266
263
  ensureRoleRunDirectory(ledgerHome, dirname(childTarget.runDirectory));
267
264
  await rename(childDirectory, childTarget.runDirectory);
268
265
  }
269
266
  }
270
- // An identity diarist can file before its caller obtains a ticket. Both its
271
- // own bind and the caller's later bind use this existing relocation seam.
267
+ // The first ticket assignment moves only an unbound diarist's own diary.
272
268
  if (admitted.role === "diarist") {
273
269
  await rehomeUnboundTicketProvenance(oldRunDirectory, admitted.ticketNumber, admitted.projectRoot, homeFromRunDirectory(oldRunDirectory));
274
270
  }
275
- // Rename commits the run placement. Diary assignment above is an idempotent
276
- // Sitian append, not part of an atomic transaction with this directory move.
271
+ // Rename commits the run placement. Diary assignment above is a Sitian append
272
+ // outside the atomic directory move.
277
273
  // Persisted paths are resolved from typed run identity on read.
278
274
  await rename(oldRunDirectory, target.runDirectory);
279
275
  // rename moved the open lock inode with the directory. Transfer cleanup
@@ -312,24 +308,9 @@ export async function recordChildDiaristRun(parent, childRunId) {
312
308
  export async function bindTicketNumberOnRunDirectory(runDirectory, ticketNumber) {
313
309
  requireSafePositiveTicketNumber(ticketNumber, "bindTicketNumberOnRunDirectory");
314
310
  const admittedPath = join(runDirectory, "admitted-request.json");
315
- const invocationPath = join(runDirectory, "invocation.json");
316
311
  const admitted = JSON.parse(await readFile(admittedPath, "utf8"));
317
- const existing = admitted.ticketNumber;
318
- if (typeof existing === "number") {
319
- if (existing === ticketNumber)
320
- return;
321
- throw new Error(`bindTicketNumberOnRunDirectory refuses to replace existing ticket #${existing} with #${ticketNumber}`);
322
- }
323
- // Conflict guard before any write: crash window of bindAdmittedTicketNumber
324
- // can leave invocation bound while admitted-request is still unbound — refuse
325
- // silent rebind. Read-before-merge; never check the page just overwritten.
326
- if (existsSync(invocationPath)) {
327
- const invocation = JSON.parse(await readFile(invocationPath, "utf8"));
328
- if (typeof invocation.ticketNumber === "number" &&
329
- invocation.ticketNumber !== ticketNumber) {
330
- throw new Error(`bindTicketNumberOnRunDirectory refuses to replace invocation ticket #${invocation.ticketNumber} with #${ticketNumber}`);
331
- }
332
- }
312
+ if (admitted.ticketNumber === ticketNumber)
313
+ return;
333
314
  await writeFile(admittedPath, `${JSON.stringify({ ...admitted, ticketNumber }, null, 2)}\n`, "utf8");
334
315
  await mergeInvocationIdentityPage(runDirectory, { ticketNumber });
335
316
  }