@akagilnc/pi-workflow-roles 0.1.3891 → 0.1.3898

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.
@@ -177,6 +177,63 @@ export type ControlledFailureInput = {
177
177
  typedHttpObservation?: TypedProviderHttpObservation;
178
178
  };
179
179
 
180
+ /** Result of seat prep after the single pre-lease admitted load. */
181
+ export type AfterAdmittedLoadResult<
182
+ A extends AdmittedRoleInvocation,
183
+ T extends TerminalResult = TerminalResult,
184
+ > =
185
+ | { kind: "continue"; adapters: PostAdmissionAdapters<A, T> }
186
+ | {
187
+ kind: "terminal";
188
+ exitCode: number;
189
+ admitted: A;
190
+ terminal: TerminalResult;
191
+ };
192
+
193
+ /**
194
+ * Factory-seat resume method-material face (#833): one authority for
195
+ * load → adapters / controlled-failure short-circuit. Seats only declare
196
+ * irreducible differences (loader, adapter factories, optional gate, knownCause).
197
+ */
198
+ export async function resolveResumeMethodMaterialAdapters<
199
+ A extends AdmittedRoleInvocation,
200
+ T extends TerminalResult = TerminalResult,
201
+ M = unknown,
202
+ >(input: {
203
+ admitted: A;
204
+ authority: DurablePrincipalAuthority;
205
+ io: CliIo;
206
+ /** When false, skip material and continue with emptyAdapters (coder plan). */
207
+ shouldLoad?: boolean;
208
+ loadMaterial: () => Promise<M>;
209
+ adaptersWith: (material: M) => PostAdmissionAdapters<A, T>;
210
+ emptyAdapters: PostAdmissionAdapters<A, T>;
211
+ knownCause?: ControlledFailureCause;
212
+ }): Promise<AfterAdmittedLoadResult<A, T>> {
213
+ if (input.shouldLoad === false) {
214
+ return { kind: "continue", adapters: input.emptyAdapters };
215
+ }
216
+ try {
217
+ const material = await input.loadMaterial();
218
+ return { kind: "continue", adapters: input.adaptersWith(material) };
219
+ } catch (error) {
220
+ const terminal = await presentControlledFailure(
221
+ input.admitted,
222
+ {
223
+ timedOut: false,
224
+ code: null,
225
+ stderr: "",
226
+ thrown: error,
227
+ ...(input.knownCause === undefined ? {} : { knownCause: input.knownCause }),
228
+ },
229
+ input.emptyAdapters,
230
+ input.authority,
231
+ input.io,
232
+ );
233
+ return { kind: "terminal", ...terminal };
234
+ }
235
+ }
236
+
180
237
  export async function presentControlledFailure<
181
238
  A extends AdmittedRoleInvocation,
182
239
  T extends TerminalResult = TerminalResult,
@@ -439,7 +496,7 @@ export async function dispatchPostAdmissionTurn<
439
496
  )) as { exitCode: number; admitted: A; terminal: T };
440
497
  }
441
498
  if (settled !== undefined && shouldPresent(settled)) {
442
- // This court sealed — drop open-court pointer so bare resume is run-scoped idempotent.
499
+ // This court sealed — drop open-court pointer (bare resume no longer continues it).
443
500
  if (
444
501
  settled.roleOutcome.kind === "accepted" &&
445
502
  request.courtAttemptId !== undefined &&
@@ -565,6 +622,7 @@ export function resumeTurnRequestProjectionOptions(
565
622
  kind: "resume",
566
623
  prompt,
567
624
  },
625
+ ...(request.message === undefined ? {} : { courtAttemptId: randomUUID() }),
568
626
  };
569
627
  }
570
628
 
@@ -617,9 +675,10 @@ export async function prepareSummonsResumeMaterials(
617
675
 
618
676
  /**
619
677
  * Shared manual-resume orchestration for seats whose continuation is the
620
- * package resume envelope (#599 / #633): load → structural rejection → seat
621
- * turn projection → runPostAdmissionManualResume. Seat-owned loader validation,
622
- * turn builder, and adapters stay on the seat.
678
+ * package resume envelope (#599 / #633): load once → structural rejection →
679
+ * optional seat afterAdmittedLoad (method material / controlled failure) →
680
+ * seat turn projection → runPostAdmissionManualResume. Seat-owned loader
681
+ * validation, turn builder, and adapters stay on the seat.
623
682
  *
624
683
  * Court open/recovery transaction (#637): under the existing writer lease,
625
684
  * read currentCourt, judge seal, clear (bound to the judged court id), freeze,
@@ -640,12 +699,22 @@ export async function runPostAdmissionSeatResume<
640
699
  request: PublicResumeRequest,
641
700
  ) => RoleTurnRequest | Promise<RoleTurnRequest>;
642
701
  adapters: PostAdmissionAdapters<A, T>;
702
+ /**
703
+ * After the single pre-lease load. Factory seats resolve method-material
704
+ * adapters here via resolveResumeMethodMaterialAdapters (or short-circuit
705
+ * with the same controlled-failure face as initial). Must not re-load the
706
+ * same admitted; under-lease summons rehydrate remains the only second load,
707
+ * and only when materials change.
708
+ */
709
+ afterAdmittedLoad?: (
710
+ admitted: A,
711
+ ) => Promise<AfterAdmittedLoadResult<A, T>>;
643
712
  effectiveEngine?: string;
644
713
  }): Promise<{ exitCode: number; admitted?: A; terminal?: T }> {
645
714
  let request = input.request;
646
715
 
647
- // Load once for runDirectory / structural rejection; court identity is judged
648
- // only after the writer lease is held (below).
716
+ // Load once for runDirectory / structural rejection / afterAdmittedLoad;
717
+ // court identity is judged only after the writer lease is held (below).
649
718
  let loaded;
650
719
  try {
651
720
  loaded = await input.load(request);
@@ -657,20 +726,29 @@ export async function runPostAdmissionSeatResume<
657
726
  throw error;
658
727
  }
659
728
 
660
- // Entire court recovery / open path runs after lease. forceContinuation skips
661
- // the pre-lease sealed short-circuit; when the after-lease builder leaves no
662
- // courtAttemptId, manual resume still presents run-scoped sealed idempotence
663
- // under the same held lease.
729
+ let adapters = input.adapters;
730
+ if (input.afterAdmittedLoad !== undefined) {
731
+ const prepared = await input.afterAdmittedLoad(loaded.admitted);
732
+ if (prepared.kind === "terminal") {
733
+ return {
734
+ exitCode: prepared.exitCode,
735
+ admitted: prepared.admitted,
736
+ terminal: prepared.terminal as T,
737
+ };
738
+ }
739
+ adapters = prepared.adapters;
740
+ }
741
+
742
+ // Court recovery / open under lease, then always dispatch (resume is pass-through).
664
743
  try {
665
744
  return await runPostAdmissionManualResume({
666
745
  admitted: loaded.admitted,
667
746
  env: input.env,
668
747
  io: input.io,
669
- adapters: input.adapters,
748
+ adapters,
670
749
  ...(input.effectiveEngine === undefined
671
750
  ? {}
672
751
  : { effectiveEngine: input.effectiveEngine }),
673
- forceContinuation: true,
674
752
  buildRequestAfterLease: async () => {
675
753
  let openCourtAttemptId: string | undefined;
676
754
  // Build uses the admitted judged under this lease (rehydrated when open
@@ -741,10 +819,13 @@ export async function runPostAdmissionSeatResume<
741
819
 
742
820
  let turnRequest = await input.buildTurnRequest(admittedForBuild, request);
743
821
 
744
- // Court path only when continuing an open court or opening a new summons court.
745
- // Bare resume with no open court (or cleared sealed pointer) keeps no
746
- // courtAttemptId so run-scoped sealed idempotence can present under lease.
747
- if (openCourtAttemptId !== undefined || request.summons !== undefined) {
822
+ // Open court continue, or new court for summons / message re-review
823
+ // (clause 0 新庭可再交卷; #833). Bare resume without open court omits id.
824
+ if (
825
+ openCourtAttemptId !== undefined ||
826
+ request.summons !== undefined ||
827
+ request.message !== undefined
828
+ ) {
748
829
  const courtAttemptId =
749
830
  openCourtAttemptId ??
750
831
  (turnRequest.courtAttemptId !== undefined &&
@@ -872,73 +953,9 @@ export async function runPostAdmissionResumable<
872
953
  }
873
954
 
874
955
  /**
875
- * Single authority for manual-resume run-scoped sealed-accepted presentation
876
- * (#599 / #648 / #672 / #637). Used both before lease (eager request path) and
877
- * after court-recovery builder under lease when no courtAttemptId remains.
878
- * Returns undefined to fall through to dispatch; never rebuilds the gate.
879
- */
880
- async function presentSealedAcceptedManualResumeIfAny<
881
- A extends AdmittedRoleInvocation,
882
- T extends TerminalResult,
883
- >(input: {
884
- admitted: A;
885
- env: PostAdmissionEnv;
886
- io: CliIo;
887
- adapters: PostAdmissionAdapters<A, T>;
888
- shouldPresent: (terminal: T) => boolean;
889
- }): Promise<{ exitCode: number; admitted: A; terminal?: T } | undefined> {
890
- const { admitted, env, io, adapters, shouldPresent } = input;
891
- try {
892
- const existing = await adapters.trySettle(admitted, env.principalAuthority);
893
- if (
894
- existing !== undefined &&
895
- existing.roleOutcome.kind === "accepted" &&
896
- shouldPresent(existing)
897
- ) {
898
- (existing as { autoResumeCount?: number }).autoResumeCount = 0;
899
- io.stdout(formatTerminalResult(existing));
900
- return {
901
- exitCode: exitCodeForTerminalOutcome(existing.roleOutcome),
902
- admitted,
903
- terminal: existing,
904
- };
905
- }
906
- } catch (error) {
907
- // Settlement-owned sealed disposition: sealed accepted + publication/settle
908
- // throw fail closed without redispatch; authority failure preserves cause.
909
- const disposition = await sealedAcceptanceRedispatchDisposition(admitted);
910
- if (disposition.kind === "block") {
911
- return (await presentControlledFailure(
912
- admitted,
913
- {
914
- timedOut: false,
915
- code: null,
916
- stderr: "",
917
- thrown:
918
- disposition.reason === "authority-failed"
919
- ? disposition.cause
920
- : error,
921
- },
922
- adapters,
923
- env.principalAuthority,
924
- io,
925
- )) as { exitCode: number; admitted: A; terminal: T };
926
- }
927
- // Pre-dispatch settle failure without a sealed accepted projection is not
928
- // proof of seal; fall through to dispatch so the attempt path can settle
929
- // or fail honestly.
930
- }
931
- return undefined;
932
- }
933
-
934
- /**
935
- * Shared post-admission manual resume path: acquire writer lease and dispatch turn.
936
- * When the submission ledger is already sealed, project that accepted terminal
937
- * idempotently — do not dispatch a doomed turn that would append
938
- * post-seal-anomaly and erase the sealed read (#599; keep #416 open load).
939
- * Same-ticket re-summons (#637) pass forceContinuation + courtAttemptId so a new
940
- * court turn still runs with this summons' materials despite a prior sealed
941
- * acceptance, while submission-ledger sole-final stays per-attempt.
956
+ * Manual resume: lease + dispatch. Pass-through to the host — no sealed-accepted
957
+ * short-circuit (#833 / #416). Court open (summons / message / open court) is
958
+ * built under lease when using buildRequestAfterLease; sole-final stays per-attempt.
942
959
  */
943
960
  export async function runPostAdmissionManualResume<
944
961
  A extends AdmittedRoleInvocation,
@@ -949,17 +966,11 @@ export async function runPostAdmissionManualResume<
949
966
  io: CliIo;
950
967
  /** Eager turn request (non-court path / seats that build before lease). */
951
968
  request?: RoleTurnRequest;
952
- /**
953
- * Court-opening path (#637): build turn + freeze + record currentCourt only
954
- * after the writer lease is held. Mutually exclusive with a prebuilt request
955
- * when forceContinuation is set.
956
- */
969
+ /** After-lease builder (#637). Mutually exclusive with a prebuilt request. */
957
970
  buildRequestAfterLease?: () => Promise<RoleTurnRequest>;
958
971
  adapters: PostAdmissionAdapters<A, T>;
959
- /** Seat-table engine axis on resume (#600); written onto invocation.json when present. */
972
+ /** Seat-table engine axis on resume (#600). */
960
973
  effectiveEngine?: string;
961
- /** Same-ticket re-summons: skip sealed-accepted short-circuit and dispatch. */
962
- forceContinuation?: boolean;
963
974
  }): Promise<{
964
975
  exitCode: number;
965
976
  admitted?: A;
@@ -972,33 +983,11 @@ export async function runPostAdmissionManualResume<
972
983
  io,
973
984
  adapters,
974
985
  effectiveEngine,
975
- forceContinuation,
976
986
  buildRequestAfterLease,
977
987
  } = input;
978
988
  let request = input.request;
979
989
  // #617 DK-3: manual resume writes the live seat/env model (same as new legs).
980
990
  const effectiveModel = env.model;
981
- const shouldPresent =
982
- adapters.shouldPresentSettled ??
983
- ((terminal: T) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
984
- const sealedIdempotenceInput = {
985
- admitted,
986
- env,
987
- io,
988
- adapters,
989
- shouldPresent,
990
- } as const;
991
-
992
- // Sealed accepted receipt only — audit_escalation / residual failure must not
993
- // short-circuit; those still need a real continuation turn.
994
- // Same-ticket re-summons / court recovery forceContinuation skips the pre-lease
995
- // face; post-builder reuses the same presenter when no courtAttemptId remains.
996
- if (forceContinuation !== true && request !== undefined) {
997
- const presented = await presentSealedAcceptedManualResumeIfAny(
998
- sealedIdempotenceInput,
999
- );
1000
- if (presented !== undefined) return presented;
1001
- }
1002
991
 
1003
992
  let lease: RunWriterLease;
1004
993
  let staleWriterLeaseReclaimed: true | undefined;
@@ -1027,8 +1016,8 @@ export async function runPostAdmissionManualResume<
1027
1016
  }
1028
1017
 
1029
1018
  // Court open/recovery under held lease until dispatch owns release (finally
1030
- // below). Builder, sealed presenter, and any throw on this seam must release
1031
- // here — dispatch's finally only runs after handoff.
1019
+ // below). Builder and any throw on this seam must release here — dispatch's
1020
+ // finally only runs after handoff.
1032
1021
  let handedOffToDispatch = false;
1033
1022
  try {
1034
1023
  if (request === undefined) {
@@ -1038,23 +1027,6 @@ export async function runPostAdmissionManualResume<
1038
1027
  );
1039
1028
  }
1040
1029
  request = await buildRequestAfterLease();
1041
-
1042
- if (
1043
- request.courtAttemptId === undefined ||
1044
- request.courtAttemptId.length === 0
1045
- ) {
1046
- const presented = await presentSealedAcceptedManualResumeIfAny(
1047
- sealedIdempotenceInput,
1048
- );
1049
- if (presented !== undefined) {
1050
- return {
1051
- ...presented,
1052
- ...(staleWriterLeaseReclaimed === true
1053
- ? { staleWriterLeaseReclaimed: true as const }
1054
- : {}),
1055
- };
1056
- }
1057
- }
1058
1030
  }
1059
1031
 
1060
1032
  handedOffToDispatch = true;
@@ -43,8 +43,9 @@ import {
43
43
  } from "./turn-request.ts";
44
44
  import {
45
45
  presentControlledFailure,
46
- runPostAdmissionManualResume,
46
+ resolveResumeMethodMaterialAdapters,
47
47
  runPostAdmissionResumable,
48
+ runPostAdmissionSeatResume,
48
49
  type PostAdmissionAdapters,
49
50
  type PostAdmissionEnv,
50
51
  resumeTurnRequestProjectionOptions,
@@ -84,17 +85,22 @@ function reviewerAdapters(
84
85
  ): PostAdmissionAdapters<AdmittedReviewerInvocation> {
85
86
  return {
86
87
  beforeDispatch: (admitted) => clearReviewerDispatchRejection(admitted.runDirectory),
87
- trySettle: (admitted, authority) =>
88
+ trySettle: (admitted, authority, scope) =>
88
89
  methodMaterial === undefined
89
90
  ? Promise.resolve(undefined)
90
- : trySettleReviewerTerminalResult(admitted, authority, {
91
- methodProvenance: methodMaterial.provenance,
92
- methodSkillPath: methodMaterial.skillPath,
93
- methodSkillConfiguredPath: resolvePackagedMethodSkillPath(
94
- packageRoot,
95
- "code-review",
96
- ),
97
- }),
91
+ : trySettleReviewerTerminalResult(
92
+ admitted,
93
+ authority,
94
+ {
95
+ methodProvenance: methodMaterial.provenance,
96
+ methodSkillPath: methodMaterial.skillPath,
97
+ methodSkillConfiguredPath: resolvePackagedMethodSkillPath(
98
+ packageRoot,
99
+ "code-review",
100
+ ),
101
+ },
102
+ scope,
103
+ ),
98
104
  resolveRunnerKnownFailure: async ({ result, sessionFile }) => {
99
105
  const infrastructureFailure = await readEngineDetourInfrastructureFailure(sessionFile);
100
106
  return infrastructureFailure === undefined
@@ -238,49 +244,28 @@ export async function runPublicReviewerResume(
238
244
  admitted?: AdmittedReviewerInvocation;
239
245
  terminal?: TerminalResult;
240
246
  }> {
241
- let loaded;
242
- try {
243
- loaded = await loadResumableReviewerRun(env.home, request.runId, env.principalAuthority);
244
- } catch (error) {
245
- if (error instanceof CliUsageError) {
246
- presentStructuralRejection(error, io);
247
- return { exitCode: 2 };
248
- }
249
- throw error;
250
- }
251
-
252
- const { admitted } = loaded;
253
-
254
- let methodMaterial: PackagedMethodSkillMaterial;
255
- try {
256
- methodMaterial = await loadReviewerMethodMaterial(env.packageRoot);
257
- } catch (error) {
258
- return (await presentControlledFailure(
259
- admitted,
260
- {
261
- timedOut: false,
262
- code: null,
263
- stderr: "",
264
- thrown: error,
265
- },
266
- reviewerAdapters(env.packageRoot),
267
- env.principalAuthority,
268
- io,
269
- )) as { exitCode: number; admitted: AdmittedReviewerInvocation; terminal: TerminalResult };
270
- }
271
-
272
- const turnRequest = buildReviewerTurnRequest(
273
- admitted,
274
- resumeTurnRequestProjectionOptions(admitted, request, env),
275
- );
276
-
277
- return await runPostAdmissionManualResume({
278
- admitted,
247
+ return await runPostAdmissionSeatResume({
248
+ request,
279
249
  env,
280
250
  io,
281
- request: turnRequest,
251
+ load: (effective) =>
252
+ loadResumableReviewerRun(env.home, effective.runId, env.principalAuthority),
253
+ buildTurnRequest: (admitted, effective) =>
254
+ buildReviewerTurnRequest(
255
+ admitted,
256
+ resumeTurnRequestProjectionOptions(admitted, effective, env),
257
+ ),
258
+ adapters: reviewerAdapters(env.packageRoot),
259
+ afterAdmittedLoad: (admitted) =>
260
+ resolveResumeMethodMaterialAdapters({
261
+ admitted,
262
+ authority: env.principalAuthority,
263
+ io,
264
+ loadMaterial: () => loadReviewerMethodMaterial(env.packageRoot),
265
+ adaptersWith: (material) => reviewerAdapters(env.packageRoot, material),
266
+ emptyAdapters: reviewerAdapters(env.packageRoot),
267
+ }),
282
268
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
283
- adapters: reviewerAdapters(env.packageRoot, methodMaterial),
284
269
  });
285
270
  }
286
271
 
@@ -675,9 +675,9 @@ export async function recordCurrentCourt(
675
675
 
676
676
  /**
677
677
  * Clear open court after this courtAttemptId seals, or when the open court is
678
- * already sealed and bare resume returns to run-scoped idempotence (#637).
679
- * When expectedCourtAttemptId is set, clear only if it still matches — never
680
- * drop a different court recorded under the writer lease after our judgment.
678
+ * already sealed (#637). When expectedCourtAttemptId is set, clear only if it
679
+ * still matches — never drop a different court recorded under the writer lease
680
+ * after our judgment.
681
681
  */
682
682
  export async function clearCurrentCourt(
683
683
  runDirectory: string,