@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621

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 (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -0,0 +1,667 @@
1
+ /**
2
+ * Unified post-admission Role lifecycle coordinator (stages ③–⑤; ADR 0018 / #505 / #517 / #526).
3
+ * Owns writer lease → running → ③ dispatch → ④ tool loop / gates → ⑤ settle / fail →
4
+ * terminal → release. The durable admitted mark (markRunAdmitted) is owned by the
5
+ * initial role facades before entering; manual resume never re-admits.
6
+ * Role runners supply only turn request projection and narrow settlement adapters.
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import { readFile, writeFile } from "node:fs/promises";
10
+ import { isAbsolute, join, resolve } from "node:path";
11
+ import { buildResumeContinuationPrompt, } from "./run-lifecycle.js";
12
+ import { CliUsageError } from "./cli-errors.js";
13
+ import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
+ import { pathContainedIn } from "../activation-ledger-topology.js";
15
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
16
+ import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
17
+ import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
18
+ import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, markRunResumable, markRunRunning, markRunTerminal, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
19
+ import { homeFromRunDirectory } from "../activation-ledger-topology.js";
20
+ import { readSealedSubmission } from "../submission-ledger.js";
21
+ import { classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, sealedAcceptanceRedispatchDisposition, } from "./settlement.js";
22
+ import {} from "./terminal.js";
23
+ import { runWithAutoResumeLoop } from "./auto-resume.js";
24
+ /** Previous main-session host recorded on invocation.json, if any. */
25
+ async function readInvocationHost(runDirectory) {
26
+ try {
27
+ const raw = JSON.parse(await readFile(join(runDirectory, "invocation.json"), "utf8"));
28
+ return typeof raw.host === "string" && raw.host.trim() !== "" ? raw.host : undefined;
29
+ }
30
+ catch (error) {
31
+ if (error.code === "ENOENT")
32
+ return undefined;
33
+ throw error;
34
+ }
35
+ }
36
+ export async function presentControlledFailure(admitted, failureInput, adapters, authority, io) {
37
+ const hasThrown = Object.hasOwn(failureInput, "thrown");
38
+ const resumeObservation = await resolveControlledFailureResumeObservation({
39
+ runDirectory: admitted.runDirectory,
40
+ ...(failureInput.typedHttpObservationSettled === true
41
+ ? {
42
+ typedHttpObservationSettled: true,
43
+ ...(failureInput.typedHttpObservation === undefined
44
+ ? {}
45
+ : { typedHttpObservation: failureInput.typedHttpObservation }),
46
+ }
47
+ : {}),
48
+ });
49
+ const knownFailure = failureInput.knownFailure ?? resumeObservation.observationReadFailure;
50
+ const session = !hasThrown &&
51
+ !failureInput.timedOut &&
52
+ knownFailure === undefined &&
53
+ failureInput.knownCause === undefined &&
54
+ admitted.principal !== undefined
55
+ ? await inspectJudgeSession(authority.decode(admitted.principal).sessionFile)
56
+ : undefined;
57
+ // knownFailure channel owns details when present; otherwise caller knownDetails.
58
+ const fromKnownFailure = explicitInternalKnownFailureClassificationInput(knownFailure);
59
+ const failure = classifyPostAdmissionFailure({
60
+ timedOut: failureInput.timedOut,
61
+ code: failureInput.code,
62
+ stderr: failureInput.stderr,
63
+ ...(hasThrown ? { thrown: failureInput.thrown } : {}),
64
+ ...(failureInput.knownDetails === undefined
65
+ ? {}
66
+ : { knownDetails: failureInput.knownDetails }),
67
+ ...fromKnownFailure,
68
+ ...(failureInput.knownCause === undefined
69
+ ? {}
70
+ : { knownCause: failureInput.knownCause }),
71
+ ...(failureInput.knownIdentity === undefined
72
+ ? {}
73
+ : { knownIdentity: failureInput.knownIdentity }),
74
+ ...(failureInput.knownDiagnostic === undefined
75
+ ? {}
76
+ : { knownDiagnostic: failureInput.knownDiagnostic }),
77
+ ...(session === undefined ? {} : { session }),
78
+ });
79
+ // #665 / #416: resume hint is seat-uniform — principal available && typed 429 → show.
80
+ // Do not fork the public terminal face on per-seat hasLawful / isResumableRole (ADR 0040).
81
+ let resumable = false;
82
+ const typedHttp429 = resumeObservation.typedHttp429;
83
+ if (admitted.principal !== undefined) {
84
+ const sessionPrincipalAvailable = await authority.isAvailable(admitted.principal);
85
+ resumable = sessionPrincipalAvailable && typedHttp429 !== undefined;
86
+ }
87
+ if (resumable && typedHttp429 !== undefined) {
88
+ await markRunResumable(admitted.runDirectory, typedHttp429);
89
+ }
90
+ else {
91
+ await markRunTerminal(admitted.runDirectory);
92
+ }
93
+ const terminal = await settleFailureTerminalResult(admitted, failure, authority, resumable
94
+ ? { resume: { command: renderResumeCommand(admitted.runId) } }
95
+ : {});
96
+ presentFailureTerminal(terminal, io);
97
+ return {
98
+ exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
99
+ admitted,
100
+ terminal,
101
+ };
102
+ }
103
+ export async function dispatchPostAdmissionTurn(input) {
104
+ const { admitted, env, io, request, lease, adapters, effectiveEngine } = input;
105
+ const shouldPresent = adapters.shouldPresentSettled ?? ((terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
106
+ try {
107
+ const missingCredential = missingCredentialPreDispatchFailure(env.model, env.credentials);
108
+ if (missingCredential !== undefined) {
109
+ return (await presentControlledFailure(admitted, missingCredential, adapters, env.principalAuthority, io));
110
+ }
111
+ // #617 DK-4: capture previous invocation host before markRunRunning overwrites it.
112
+ // Single authority projectHostTransitionPriorNative classifies the prior native volume.
113
+ // Same-run resume (#637) keeps host identity on the run's invocation page.
114
+ let previousHost;
115
+ const liveHost = env.host;
116
+ const principalCoordinates = admitted.principal === undefined
117
+ ? undefined
118
+ : env.principalAuthority.decode(admitted.principal);
119
+ let turnRequest;
120
+ try {
121
+ previousHost = await readInvocationHost(admitted.runDirectory);
122
+ const hostTransition = previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
123
+ ? await projectHostTransitionPriorNative({
124
+ previousHost,
125
+ liveHost,
126
+ piSessionFile: principalCoordinates.sessionFile,
127
+ })
128
+ : undefined;
129
+ turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
130
+ if (hostTransition !== undefined) {
131
+ turnRequest = { ...turnRequest, hostTransition };
132
+ }
133
+ }
134
+ catch (error) {
135
+ // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
136
+ return (await presentControlledFailure(admitted, {
137
+ timedOut: false,
138
+ code: null,
139
+ stderr: "",
140
+ thrown: error,
141
+ }, adapters, env.principalAuthority, io));
142
+ }
143
+ await markRunRunning(admitted.runDirectory, env.model, effectiveEngine, env.host);
144
+ await clearTypedProviderHttpObservation(admitted.runDirectory);
145
+ // beforeDispatch (seat-owned pre-turn work) runs after running is marked —
146
+ // its failures must settle the run, not leave it permanently running.
147
+ if (adapters.beforeDispatch !== undefined) {
148
+ try {
149
+ await adapters.beforeDispatch(admitted);
150
+ }
151
+ catch (error) {
152
+ return (await presentControlledFailure(admitted, {
153
+ timedOut: false,
154
+ code: null,
155
+ stderr: "",
156
+ thrown: error,
157
+ }, adapters, env.principalAuthority, io));
158
+ }
159
+ }
160
+ let result;
161
+ try {
162
+ result = await env.roleTurnHost.executeTurn(turnRequest);
163
+ }
164
+ catch (error) {
165
+ return (await presentControlledFailure(admitted, {
166
+ timedOut: false,
167
+ code: null,
168
+ stderr: "",
169
+ thrown: error,
170
+ }, adapters, env.principalAuthority, io));
171
+ }
172
+ try {
173
+ await writeFile(join(admitted.runDirectory, "stderr.log"), result.stderr, "utf8");
174
+ }
175
+ catch {
176
+ // continue to lawful / controlled-failure settlement
177
+ }
178
+ let settled;
179
+ // Same-ticket re-summons carry courtAttemptId — settle only that attempt so a
180
+ // prior sealed pass cannot wash this turn's missing/escalated/failed result.
181
+ const courtScope = request.courtAttemptId === undefined || request.courtAttemptId.length === 0
182
+ ? undefined
183
+ : { courtAttemptId: request.courtAttemptId };
184
+ try {
185
+ settled = await adapters.trySettle(admitted, env.principalAuthority, courtScope);
186
+ }
187
+ catch (error) {
188
+ // Settle throw is a real failure fact — never swallow into undefined.
189
+ return (await presentControlledFailure(admitted, {
190
+ timedOut: false,
191
+ code: result.code,
192
+ stderr: result.stderr,
193
+ thrown: error,
194
+ }, adapters, env.principalAuthority, io));
195
+ }
196
+ if (settled !== undefined && shouldPresent(settled)) {
197
+ // This court sealed — drop open-court pointer so bare resume is run-scoped idempotent.
198
+ if (settled.roleOutcome.kind === "accepted" &&
199
+ request.courtAttemptId !== undefined &&
200
+ request.courtAttemptId.length > 0) {
201
+ await clearCurrentCourt(admitted.runDirectory, request.courtAttemptId);
202
+ }
203
+ await markRunTerminal(admitted.runDirectory);
204
+ io.stdout(formatTerminalResult(settled));
205
+ return {
206
+ exitCode: exitCodeForTerminalOutcome(settled.roleOutcome),
207
+ admitted,
208
+ terminal: settled,
209
+ };
210
+ }
211
+ const sessionFile = admitted.principal !== undefined
212
+ ? env.principalAuthority.decode(admitted.principal).sessionFile
213
+ : "";
214
+ const runnerKnownFailure = adapters.resolveRunnerKnownFailure !== undefined && sessionFile !== ""
215
+ ? await adapters.resolveRunnerKnownFailure({ result, sessionFile })
216
+ : result.knownFailure;
217
+ const credentialFailure = postRunMissingCredentialFailure(result, env.model, env.credentials);
218
+ const resolution = await resolveAuditedRunnerFailureResolution({
219
+ runner: runnerKnownFailure,
220
+ sessionFile,
221
+ credential: credentialFailure,
222
+ runDirectory: admitted.runDirectory,
223
+ });
224
+ return (await presentControlledFailure(admitted, {
225
+ timedOut: result.timedOut,
226
+ code: result.code,
227
+ stderr: result.stderr,
228
+ ...controlledFailureInputFromResolution(resolution),
229
+ }, adapters, env.principalAuthority, io));
230
+ }
231
+ finally {
232
+ await lease.release();
233
+ }
234
+ }
235
+ /**
236
+ * Shared resume continuation projection (#471 / #600 / #633 / #637): seat-table
237
+ * model/engine/timeout axes, restored correlation, and either
238
+ * - manual resume: package envelope / optional caller message (unchanged), or
239
+ * - same-ticket summons: this turn's instruction + frozen attachment paths.
240
+ * Caller message and summons materials each keep their place: a resume message
241
+ * keeps manual-resume prompt semantics while open-court frozen attachments still
242
+ * ride; summons instruction is only the prompt base when no caller message.
243
+ * Seats add only their activation projection. Call prepareSummonsResumeMaterials
244
+ * first when request.summons carries attachment paths.
245
+ */
246
+ export function resumeTurnRequestProjectionOptions(admitted, request, env, summonsPrepared) {
247
+ const engineMaterial = engineSessionMaterialFromOptions({
248
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
249
+ packageRoot: env.packageRoot,
250
+ });
251
+ let prompt;
252
+ if (request.message !== undefined) {
253
+ // Manual resume caller-message semantics stay authoritative for the prompt:
254
+ // message present → base bytes unchanged (including blank/whitespace).
255
+ // Open-court frozen attachments still continue when present; attachment
256
+ // projection must not re-interpret the caller message as instructionEmpty.
257
+ if (summonsPrepared !== undefined &&
258
+ summonsPrepared.attachments.length > 0) {
259
+ prompt = buildInstructionTransportPrompt({
260
+ instruction: request.message,
261
+ instructionEmpty: false,
262
+ attachments: summonsPrepared.attachments,
263
+ }, engineMaterial);
264
+ }
265
+ else {
266
+ prompt = buildResumeContinuationPrompt({
267
+ packageRoot: env.packageRoot,
268
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
269
+ message: request.message,
270
+ });
271
+ }
272
+ }
273
+ else if (summonsPrepared !== undefined) {
274
+ prompt = buildInstructionTransportPrompt(summonsPrepared, engineMaterial);
275
+ }
276
+ else {
277
+ prompt = buildResumeContinuationPrompt({
278
+ packageRoot: env.packageRoot,
279
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
280
+ });
281
+ }
282
+ return {
283
+ packageRoot: env.packageRoot,
284
+ home: env.home,
285
+ agentDir: env.agentDir,
286
+ ...(env.model === undefined ? {} : { model: env.model }),
287
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
288
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
289
+ ...(admitted.correlationId === undefined && env.correlationId === undefined
290
+ ? {}
291
+ : { correlationId: admitted.correlationId ?? env.correlationId }),
292
+ continuation: {
293
+ kind: "resume",
294
+ prompt,
295
+ },
296
+ };
297
+ }
298
+ function isAlreadyFrozenSummonsAttachment(runDirectory, attachmentPath) {
299
+ const absolute = isAbsolute(attachmentPath)
300
+ ? attachmentPath
301
+ : resolve(attachmentPath);
302
+ return pathContainedIn(join(runDirectory, "attachments"), absolute);
303
+ }
304
+ /**
305
+ * Freeze same-ticket summons attachments into the retained run directory (#637).
306
+ * No-op materials (no paths / instruction-only) skip the freeze.
307
+ * Paths already under this run's attachments/ are the accepted freeze identity —
308
+ * reuse them; do not re-freeze from external originals on bare resume.
309
+ * Manual resume never calls this — old attachment semantics stay intact.
310
+ */
311
+ export async function prepareSummonsResumeMaterials(runDirectory, summons) {
312
+ if (summons === undefined)
313
+ return undefined;
314
+ if (summons.instruction === undefined && (summons.attachmentPaths?.length ?? 0) === 0) {
315
+ return undefined;
316
+ }
317
+ const instruction = summons.instruction ?? "";
318
+ const instructionEmpty = summons.instructionEmpty ?? instruction.trim() === "";
319
+ let attachments = [];
320
+ if (summons.attachmentPaths !== undefined && summons.attachmentPaths.length > 0) {
321
+ const alreadyFrozen = summons.attachmentPaths.every((path) => isAlreadyFrozenSummonsAttachment(runDirectory, path));
322
+ attachments = alreadyFrozen
323
+ ? summons.attachmentPaths.map((frozenPath) => ({ frozenPath }))
324
+ : await freezeAttachmentsIntoRun(summons.attachmentPaths, runDirectory);
325
+ }
326
+ return { instruction, instructionEmpty, attachments };
327
+ }
328
+ /**
329
+ * Shared manual-resume orchestration for seats whose continuation is the
330
+ * package resume envelope (#599 / #633): load → structural rejection → seat
331
+ * turn projection → runPostAdmissionManualResume. Seat-owned loader validation,
332
+ * turn builder, and adapters stay on the seat.
333
+ *
334
+ * Court open/recovery transaction (#637): under the existing writer lease,
335
+ * read currentCourt, judge seal, clear (bound to the judged court id), freeze,
336
+ * and record. No pre-lease clear or stale court-snapshot consumption.
337
+ */
338
+ export async function runPostAdmissionSeatResume(input) {
339
+ let request = input.request;
340
+ // Load once for runDirectory / structural rejection; court identity is judged
341
+ // only after the writer lease is held (below).
342
+ let loaded;
343
+ try {
344
+ loaded = await input.load(request);
345
+ }
346
+ catch (error) {
347
+ if (error instanceof CliUsageError) {
348
+ presentStructuralRejection(error, input.io);
349
+ return { exitCode: 2 };
350
+ }
351
+ throw error;
352
+ }
353
+ // Entire court recovery / open path runs after lease. forceContinuation skips
354
+ // the pre-lease sealed short-circuit; when the after-lease builder leaves no
355
+ // courtAttemptId, manual resume still presents run-scoped sealed idempotence
356
+ // under the same held lease.
357
+ try {
358
+ return await runPostAdmissionManualResume({
359
+ admitted: loaded.admitted,
360
+ env: input.env,
361
+ io: input.io,
362
+ adapters: input.adapters,
363
+ ...(input.effectiveEngine === undefined
364
+ ? {}
365
+ : { effectiveEngine: input.effectiveEngine }),
366
+ forceContinuation: true,
367
+ buildRequestAfterLease: async () => {
368
+ let openCourtAttemptId;
369
+ // Build uses the admitted judged under this lease (rehydrated when open
370
+ // court materials ride). Settlement identity stays on the outer admitted.
371
+ let admittedForBuild = loaded.admitted;
372
+ // Bare resume: read + seal-judge + bound clear only under the held lease.
373
+ if (request.summons === undefined) {
374
+ const openCourt = await readCurrentCourt(admittedForBuild.runDirectory);
375
+ if (openCourt !== undefined) {
376
+ const sealedForOpen = await readSealedSubmission(admittedForBuild.projectRoot, admittedForBuild.runId, {
377
+ home: homeFromRunDirectory(admittedForBuild.runDirectory),
378
+ attemptId: openCourt.courtAttemptId,
379
+ });
380
+ if (sealedForOpen === undefined) {
381
+ // Continue open court: same summons materials + existing courtAttemptId.
382
+ // Caller message (if any) stays on the request — projection keeps it.
383
+ openCourtAttemptId = openCourt.courtAttemptId;
384
+ request = {
385
+ runId: request.runId,
386
+ ...(request.message === undefined
387
+ ? {}
388
+ : { message: request.message }),
389
+ ...(openCourt.summons === undefined
390
+ ? {}
391
+ : { summons: openCourt.summons }),
392
+ };
393
+ if (openCourt.summons !== undefined) {
394
+ const reloaded = await input.load(request);
395
+ admittedForBuild = reloaded.admitted;
396
+ }
397
+ }
398
+ else {
399
+ // Open court already sealed — clear only the court id just judged.
400
+ await clearCurrentCourt(admittedForBuild.runDirectory, openCourt.courtAttemptId);
401
+ }
402
+ }
403
+ }
404
+ // Freeze external paths once; rewrite summons to the frozen identity so
405
+ // currentCourt + later bare resume reuse the accepted snapshot.
406
+ if (request.summons !== undefined) {
407
+ const prepared = await prepareSummonsResumeMaterials(admittedForBuild.runDirectory, request.summons);
408
+ if (prepared !== undefined &&
409
+ (request.summons.attachmentPaths?.length ?? 0) > 0) {
410
+ request = {
411
+ ...request,
412
+ summons: {
413
+ ...request.summons,
414
+ attachmentPaths: prepared.attachments.map((attachment) => attachment.frozenPath),
415
+ },
416
+ };
417
+ }
418
+ }
419
+ let turnRequest = await input.buildTurnRequest(admittedForBuild, request);
420
+ // Court path only when continuing an open court or opening a new summons court.
421
+ // Bare resume with no open court (or cleared sealed pointer) keeps no
422
+ // courtAttemptId so run-scoped sealed idempotence can present under lease.
423
+ if (openCourtAttemptId !== undefined || request.summons !== undefined) {
424
+ const courtAttemptId = openCourtAttemptId ??
425
+ (turnRequest.courtAttemptId !== undefined &&
426
+ turnRequest.courtAttemptId.length > 0
427
+ ? turnRequest.courtAttemptId
428
+ : randomUUID());
429
+ turnRequest = { ...turnRequest, courtAttemptId };
430
+ if (openCourtAttemptId === undefined) {
431
+ const court = {
432
+ courtAttemptId,
433
+ ...(request.summons === undefined
434
+ ? {}
435
+ : { summons: request.summons }),
436
+ };
437
+ await recordCurrentCourt(admittedForBuild.runDirectory, court);
438
+ }
439
+ }
440
+ return turnRequest;
441
+ },
442
+ });
443
+ }
444
+ catch (error) {
445
+ // Open-court rehydrate load under lease may still surface seat structural
446
+ // rejection (e.g. notary rejects caller message) — same exit face as pre-lease.
447
+ if (error instanceof CliUsageError) {
448
+ presentStructuralRejection(error, input.io);
449
+ return { exitCode: 2 };
450
+ }
451
+ throw error;
452
+ }
453
+ }
454
+ /**
455
+ * Shared post-admission one-shot path: writer lease, then turn dispatch.
456
+ * Initial facades own the durable admitted mark (markRunAdmitted) before
457
+ * entering; manual resume never re-admits.
458
+ */
459
+ export async function runPostAdmissionOneShot(input) {
460
+ const { admitted, env, io, request, adapters, effectiveEngine } = input;
461
+ let lease;
462
+ try {
463
+ lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic) => io.stderr(diagnostic));
464
+ }
465
+ catch (error) {
466
+ if (error instanceof RunWriterLeaseHeldError) {
467
+ presentStructuralRejection(error, io);
468
+ return { exitCode: 2, admitted };
469
+ }
470
+ throw error;
471
+ }
472
+ return await dispatchPostAdmissionTurn({
473
+ admitted,
474
+ env,
475
+ io,
476
+ request,
477
+ lease,
478
+ adapters,
479
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
480
+ });
481
+ }
482
+ /**
483
+ * Shared post-admission resumable path with auto-resume retry loop.
484
+ * Initial facades own the durable admitted mark before entering.
485
+ */
486
+ export async function runPostAdmissionResumable(input) {
487
+ const { admitted, env, io, buildInitialRequest, buildResumeRequest, adapters, effectiveEngine } = input;
488
+ return runWithAutoResumeLoop({
489
+ admitted,
490
+ principalAuthority: env.principalAuthority,
491
+ io,
492
+ sessionAppender: env.sessionAppender,
493
+ autoResumeLimit: env.autoResumeLimit,
494
+ buildInitialPayload: buildInitialRequest,
495
+ buildResumePayload: buildResumeRequest,
496
+ sealedAcceptanceDisposition: () => sealedAcceptanceRedispatchDisposition(admitted),
497
+ dispatch: (request, lease, _isFirst, attemptIo) => dispatchPostAdmissionTurn({
498
+ admitted,
499
+ env: {
500
+ ...env,
501
+ ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
502
+ },
503
+ io: attemptIo,
504
+ request,
505
+ lease,
506
+ adapters,
507
+ // #600: every attempt (initial + auto-resume) writes seat engine when present.
508
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
509
+ }),
510
+ });
511
+ }
512
+ /**
513
+ * Single authority for manual-resume run-scoped sealed-accepted presentation
514
+ * (#599 / #648 / #672 / #637). Used both before lease (eager request path) and
515
+ * after court-recovery builder under lease when no courtAttemptId remains.
516
+ * Returns undefined to fall through to dispatch; never rebuilds the gate.
517
+ */
518
+ async function presentSealedAcceptedManualResumeIfAny(input) {
519
+ const { admitted, env, io, adapters, shouldPresent } = input;
520
+ try {
521
+ const existing = await adapters.trySettle(admitted, env.principalAuthority);
522
+ if (existing !== undefined &&
523
+ existing.roleOutcome.kind === "accepted" &&
524
+ shouldPresent(existing)) {
525
+ existing.autoResumeCount = 0;
526
+ io.stdout(formatTerminalResult(existing));
527
+ return {
528
+ exitCode: exitCodeForTerminalOutcome(existing.roleOutcome),
529
+ admitted,
530
+ terminal: existing,
531
+ };
532
+ }
533
+ }
534
+ catch (error) {
535
+ // Settlement-owned sealed disposition: sealed accepted + publication/settle
536
+ // throw fail closed without redispatch; authority failure preserves cause.
537
+ const disposition = await sealedAcceptanceRedispatchDisposition(admitted);
538
+ if (disposition.kind === "block") {
539
+ return (await presentControlledFailure(admitted, {
540
+ timedOut: false,
541
+ code: null,
542
+ stderr: "",
543
+ thrown: disposition.reason === "authority-failed"
544
+ ? disposition.cause
545
+ : error,
546
+ }, adapters, env.principalAuthority, io));
547
+ }
548
+ // Pre-dispatch settle failure without a sealed accepted projection is not
549
+ // proof of seal; fall through to dispatch so the attempt path can settle
550
+ // or fail honestly.
551
+ }
552
+ return undefined;
553
+ }
554
+ /**
555
+ * Shared post-admission manual resume path: acquire writer lease and dispatch turn.
556
+ * When the submission ledger is already sealed, project that accepted terminal
557
+ * idempotently — do not dispatch a doomed turn that would append
558
+ * post-seal-anomaly and erase the sealed read (#599; keep #416 open load).
559
+ * Same-ticket re-summons (#637) pass forceContinuation + courtAttemptId so a new
560
+ * court turn still runs with this summons' materials despite a prior sealed
561
+ * acceptance, while submission-ledger sole-final stays per-attempt.
562
+ */
563
+ export async function runPostAdmissionManualResume(input) {
564
+ const { admitted, env, io, adapters, effectiveEngine, forceContinuation, buildRequestAfterLease, } = input;
565
+ let request = input.request;
566
+ // #617 DK-3: manual resume writes the live seat/env model (same as new legs).
567
+ const effectiveModel = env.model;
568
+ const shouldPresent = adapters.shouldPresentSettled ??
569
+ ((terminal) => isLawfulTypedTerminalOutcome(terminal.roleOutcome));
570
+ const sealedIdempotenceInput = {
571
+ admitted,
572
+ env,
573
+ io,
574
+ adapters,
575
+ shouldPresent,
576
+ };
577
+ // Sealed accepted receipt only — audit_escalation / residual failure must not
578
+ // short-circuit; those still need a real continuation turn.
579
+ // Same-ticket re-summons / court recovery forceContinuation skips the pre-lease
580
+ // face; post-builder reuses the same presenter when no courtAttemptId remains.
581
+ if (forceContinuation !== true && request !== undefined) {
582
+ const presented = await presentSealedAcceptedManualResumeIfAny(sealedIdempotenceInput);
583
+ if (presented !== undefined)
584
+ return presented;
585
+ }
586
+ let lease;
587
+ let staleWriterLeaseReclaimed;
588
+ try {
589
+ lease = await acquireRunWriterLease(admitted.runDirectory, (diagnostic, kind) => {
590
+ // Record the typed fact before the fallible sink: if io.stderr throws
591
+ // (acquire deliberately swallows diagnostic-sink failures), the reclaim
592
+ // still happened and must stay observable.
593
+ if (kind === "stale-reclaimed")
594
+ staleWriterLeaseReclaimed = true;
595
+ io.stderr(diagnostic);
596
+ });
597
+ }
598
+ catch (error) {
599
+ if (error instanceof RunWriterLeaseHeldError) {
600
+ io.stderr(formatCliDiagnostic(error.message));
601
+ // A held rejection after our own reclaim must still carry the fact that
602
+ // this caller reclaimed the stale lock — e.g. another resumer re-locked
603
+ // before our retry create (#629).
604
+ return {
605
+ exitCode: 1,
606
+ ...(staleWriterLeaseReclaimed === true
607
+ ? { staleWriterLeaseReclaimed: true }
608
+ : {}),
609
+ };
610
+ }
611
+ throw error;
612
+ }
613
+ // Court open/recovery under held lease until dispatch owns release (finally
614
+ // below). Builder, sealed presenter, and any throw on this seam must release
615
+ // here — dispatch's finally only runs after handoff.
616
+ let handedOffToDispatch = false;
617
+ try {
618
+ if (request === undefined) {
619
+ if (buildRequestAfterLease === undefined) {
620
+ throw new Error("runPostAdmissionManualResume requires request or buildRequestAfterLease");
621
+ }
622
+ request = await buildRequestAfterLease();
623
+ if (request.courtAttemptId === undefined ||
624
+ request.courtAttemptId.length === 0) {
625
+ const presented = await presentSealedAcceptedManualResumeIfAny(sealedIdempotenceInput);
626
+ if (presented !== undefined) {
627
+ return {
628
+ ...presented,
629
+ ...(staleWriterLeaseReclaimed === true
630
+ ? { staleWriterLeaseReclaimed: true }
631
+ : {}),
632
+ };
633
+ }
634
+ }
635
+ }
636
+ handedOffToDispatch = true;
637
+ const result = await dispatchPostAdmissionTurn({
638
+ admitted,
639
+ env: {
640
+ ...env,
641
+ ...(effectiveModel === undefined ? {} : { model: effectiveModel }),
642
+ ...(admitted.correlationId === undefined
643
+ ? {}
644
+ : { correlationId: admitted.correlationId }),
645
+ },
646
+ io,
647
+ request,
648
+ lease,
649
+ adapters,
650
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
651
+ });
652
+ if (result.terminal !== undefined) {
653
+ result.terminal.autoResumeCount = 0;
654
+ }
655
+ return {
656
+ ...result,
657
+ ...(staleWriterLeaseReclaimed === true
658
+ ? { staleWriterLeaseReclaimed: true }
659
+ : {}),
660
+ };
661
+ }
662
+ finally {
663
+ if (!handedOffToDispatch) {
664
+ await lease.release();
665
+ }
666
+ }
667
+ }