@akagilnc/pi-workflow-roles 0.1.5096 → 0.1.5124

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.
@@ -8,13 +8,13 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildAutoResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildAutoResumeContinuationPrompt, findRunDirectoryById, readRoleRunState, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { bindAdmittedTicketNumber, buildInstructionTransportPrompt, freezeAttachmentsIntoRun, relocateAdmittedRunToTicket, } from "./invocation.js";
14
14
  import { readRecordedSubmissionRows } from "../submission-ledger.js";
15
15
  import { pathContainedIn } from "../activation-ledger-topology.js";
16
16
  import { pickEngineAxis } from "../package-resources/engine-material.js";
17
- import { readStoredHostSessionId, resolveHostAwareSessionAvailability } from "../session-identity.js";
17
+ import { readStoredHostSessionId, resolveHostAwareSessionAvailability, } from "../session-identity.js";
18
18
  import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
19
19
  import { isOfficerReviewSeat } from "../packaged-role-registry.js";
20
20
  import { deliverCaseDossierAsAttachment } from "./case-dossier-delivery.js";
@@ -39,7 +39,7 @@ import { projectHostTransitionPriorNative } from "../host-transition-prior-nativ
39
39
  import { clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, } from "./run-lifecycle.js";
40
40
  import { processCancelDiagnostic, processCancelSignalName, } from "./process-cancel.js";
41
41
  import { homeFromRunDirectory } from "../activation-ledger-topology.js";
42
- import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, } from "./settlement.js";
42
+ import { attemptProducedFreshSubmission, classifyPostAdmissionFailure, controlledFailureInputFromResolution, exitCodeForTerminalOutcome, explicitInternalKnownFailureClassificationInput, formatCliDiagnostic, formatErrorCauseDetail, formatTerminalResult, inspectJudgeSession, isLawfulTypedTerminalOutcome, presentFailureTerminal, publishedFailureErrorPath, presentStructuralRejection, resolveAuditedRunnerFailureResolution, resolveControlledFailureResumeObservation, settleFailureTerminalResult, settleHostEndedNoReceipt, attachRecordedSubmissions, rewritePublishedFailureErrorPath, } from "./settlement.js";
43
43
  import {} from "./terminal.js";
44
44
  import { ensureRealArtifactsDirectory, persistReturnedRunState, runWithAutoResumeLoop, TurnDispatchedFailure, } from "./auto-resume.js";
45
45
  function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
@@ -56,6 +56,7 @@ function projectRelocatedTurnIdentity(request, result, admitted, relocation) {
56
56
  }
57
57
  }
58
58
  if (result.terminal !== undefined) {
59
+ rewritePublishedFailureErrorPath(result.terminal, relocation.oldRunDirectory, relocation.newRunDirectory);
59
60
  for (const artifact of result.terminal.artifacts) {
60
61
  artifact.path = rewrite(artifact.path);
61
62
  }
@@ -175,7 +176,7 @@ export async function resolveResumeMethodMaterialAdapters(input) {
175
176
  stderr: "",
176
177
  thrown: error,
177
178
  ...(input.knownCause === undefined ? {} : { knownCause: input.knownCause }),
178
- }, input.emptyAdapters, input.authority, input.io);
179
+ }, input.emptyAdapters, input.authority, { ...input.io, omitFailureStderrDiagnostic: true });
179
180
  return { kind: "terminal", ...terminal };
180
181
  }
181
182
  }
@@ -233,15 +234,27 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
233
234
  if (persistRunState && !failureInput.skipRunStateWrite) {
234
235
  await persistReturnedRunState(admitted, authority);
235
236
  }
236
- const terminal = await attachRecordedSubmissions(admitted, await settleFailureTerminalResult(admitted, failure, authority, {
237
- ...(resumable
238
- ? { resume: { command: renderResumeCommand(admitted.runId) } }
239
- : {}),
240
- ...(failureInput.invocationScopeId === undefined ||
241
- failureInput.invocationScopeId.length === 0
242
- ? {}
243
- : { invocationScopeId: failureInput.invocationScopeId }),
244
- }));
237
+ let publishedErrorPath;
238
+ let terminal;
239
+ try {
240
+ const settled = await settleFailureTerminalResult(admitted, failure, authority, {
241
+ ...(resumable
242
+ ? { resume: { command: renderResumeCommand(admitted.runId) } }
243
+ : {}),
244
+ ...(failureInput.invocationScopeId === undefined ||
245
+ failureInput.invocationScopeId.length === 0
246
+ ? {}
247
+ : { invocationScopeId: failureInput.invocationScopeId }),
248
+ onErrorPublished: (path) => { publishedErrorPath = path; },
249
+ });
250
+ terminal = await attachRecordedSubmissions(admitted, settled);
251
+ }
252
+ catch (error) {
253
+ if (io.omitFailureStderrDiagnostic && publishedErrorPath !== undefined) {
254
+ writeResumeFailurePointer(io, publishedErrorPath);
255
+ }
256
+ throw error;
257
+ }
245
258
  presentFailureTerminal(terminal, io);
246
259
  return {
247
260
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
@@ -359,8 +372,12 @@ export async function dispatchPostAdmissionTurn(input) {
359
372
  if (relocation !== undefined) {
360
373
  projectRelocatedTurnIdentity(request, result, admitted, relocation);
361
374
  }
362
- if (adapters.afterDispatch !== undefined)
363
- await adapters.afterDispatch(admitted, lease);
375
+ const afterDispatchRelocation = adapters.afterDispatch === undefined
376
+ ? undefined
377
+ : await adapters.afterDispatch(admitted, lease);
378
+ if (afterDispatchRelocation !== undefined) {
379
+ projectRelocatedTurnIdentity(request, result, admitted, afterDispatchRelocation);
380
+ }
364
381
  return result;
365
382
  }
366
383
  catch (error) {
@@ -968,16 +985,24 @@ export async function runPostAdmissionSeatResume(input) {
968
985
  loaded = await input.load(request);
969
986
  }
970
987
  catch (error) {
971
- if (error instanceof CliUsageError) {
972
- presentStructuralRejection(error, input.io);
973
- return { exitCode: 2 };
988
+ const usageError = error instanceof CliUsageError;
989
+ // Unknown usage failures retain the structural fallback. Other load
990
+ // failures keep the CLI's outer handling unless an existing run is found.
991
+ const recorded = await pointExistingRunFailure(input.env.home, request.runId, input.env.principalAuthority, input.io, error);
992
+ if (!recorded) {
993
+ if (usageError) {
994
+ presentStructuralRejection(error, input.io);
995
+ return { exitCode: 2 };
996
+ }
997
+ throw error;
974
998
  }
975
- throw error;
999
+ return { exitCode: usageError ? 2 : 1 };
976
1000
  }
977
1001
  let adapters = input.adapters;
978
1002
  if (input.afterAdmittedLoad !== undefined) {
979
1003
  const prepared = await input.afterAdmittedLoad(loaded.admitted);
980
1004
  if (prepared.kind === "terminal") {
1005
+ showResumeErrorPointer(input.io, prepared.exitCode, prepared.terminal);
981
1006
  return {
982
1007
  exitCode: prepared.exitCode,
983
1008
  admitted: prepared.admitted,
@@ -1116,26 +1141,100 @@ export async function runPostAdmissionSeatResume(input) {
1116
1141
  }),
1117
1142
  });
1118
1143
  }
1119
- return await runPostAdmissionManualResume({
1144
+ const resumed = await runPostAdmissionManualResume({
1120
1145
  admitted: loaded.admitted,
1121
1146
  env,
1122
- io: input.io,
1147
+ io: { ...input.io, omitFailureStderrDiagnostic: true },
1123
1148
  adapters,
1124
1149
  ...(input.effectiveEngine === undefined
1125
1150
  ? {}
1126
1151
  : { effectiveEngine: input.effectiveEngine }),
1127
1152
  buildRequestAfterLease,
1128
1153
  });
1154
+ showResumeErrorPointer(input.io, resumed.exitCode, resumed.terminal);
1155
+ return resumed;
1129
1156
  }
1130
1157
  catch (error) {
1131
- // Turn construction may still surface structural rejection.
1132
- if (error instanceof CliUsageError) {
1133
- presentStructuralRejection(error, input.io);
1134
- return { exitCode: 2 };
1158
+ if (error instanceof TurnDispatchedFailure)
1159
+ throw error;
1160
+ await presentResumeFailurePointer(input.io, error, (failure) => writeResumeDiagnosticFile(loaded.admitted.runDirectory, loaded.admitted.runId, failure, loaded.admitted.role));
1161
+ return { exitCode: error instanceof CliUsageError ? 2 : 1 };
1162
+ }
1163
+ }
1164
+ async function pointExistingRunFailure(home, runId, authority, io, thrown) {
1165
+ let foundRunDirectory;
1166
+ try {
1167
+ foundRunDirectory = await findRunDirectoryById(home, runId);
1168
+ }
1169
+ catch {
1170
+ return false;
1171
+ }
1172
+ if (foundRunDirectory === undefined)
1173
+ return false;
1174
+ const runDirectory = foundRunDirectory;
1175
+ let record;
1176
+ try {
1177
+ record = await readRoleRunState(runDirectory, authority);
1178
+ }
1179
+ catch {
1180
+ await presentResumeFailurePointer(io, thrown, (failure) => writeResumeDiagnosticFile(runDirectory, runId, failure));
1181
+ return true;
1182
+ }
1183
+ if (record === undefined)
1184
+ return false;
1185
+ await presentResumeFailurePointer(io, thrown, (failure) => writeResumeDiagnosticFile(runDirectory, runId, failure, record.role));
1186
+ return true;
1187
+ }
1188
+ export async function presentLocatedResumeFailure(home, runId, authority, io, thrown) {
1189
+ return await pointExistingRunFailure(home, runId, authority, io, thrown);
1190
+ }
1191
+ function writeResumeFailurePointer(io, errorPath) {
1192
+ io.stderr(formatCliDiagnostic(`续跑失败,当次错误记录:${errorPath}`));
1193
+ }
1194
+ export function showResumeErrorPointer(io, exitCode, terminal) {
1195
+ if (exitCode === 0 || terminal === undefined)
1196
+ return;
1197
+ const publishedError = terminal.artifacts.find((artifact) => artifact.kind === "error")?.path
1198
+ ?? publishedFailureErrorPath(terminal);
1199
+ if (publishedError !== undefined)
1200
+ writeResumeFailurePointer(io, publishedError);
1201
+ }
1202
+ async function presentResumeFailurePointer(io, thrown, publish) {
1203
+ const failure = classifyPostAdmissionFailure({
1204
+ timedOut: false,
1205
+ code: null,
1206
+ stderr: "",
1207
+ thrown,
1208
+ });
1209
+ try {
1210
+ writeResumeFailurePointer(io, await publish(failure));
1211
+ }
1212
+ catch (publishError) {
1213
+ const cause = {
1214
+ publishError: formatErrorCauseDetail(publishError),
1215
+ };
1216
+ if (thrown instanceof Error && thrown.cause !== undefined) {
1217
+ cause.thrownCause = formatErrorCauseDetail(thrown.cause);
1135
1218
  }
1136
- throw error;
1219
+ presentStructuralRejection({
1220
+ message: thrown instanceof Error ? thrown.message : String(thrown),
1221
+ cause,
1222
+ }, io);
1137
1223
  }
1138
1224
  }
1225
+ async function writeResumeDiagnosticFile(runDirectory, runId, failure, role) {
1226
+ const dir = await ensureRealArtifactsDirectory(runDirectory);
1227
+ const path = join(dir, `resume-diagnostic-${randomUUID()}.json`);
1228
+ await writeFile(path, `${JSON.stringify({
1229
+ runId,
1230
+ ...(role === undefined ? {} : { role }),
1231
+ diagnostic: failure.diagnostic,
1232
+ ...(failure.cause === undefined ? {} : { cause: failure.cause }),
1233
+ ...(failure.identity === undefined ? {} : { identity: failure.identity }),
1234
+ ...(failure.details === undefined ? {} : { details: failure.details }),
1235
+ }, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
1236
+ return path;
1237
+ }
1139
1238
  /**
1140
1239
  * Shared post-admission one-shot path: folds into runPostAdmissionResumable (#840 / #416).
1141
1240
  * All callable roles share the single auto-resume loop.
@@ -31,6 +31,7 @@ import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENT
31
31
  import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isReceiptSettlementBindingClear, NAVIGATOR_INVOCATION_ENTRY, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, parseInvocationMarkerIdentity, } from "../navigator-invocation-identity.js";
32
32
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_TURN_LIMIT, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, } from "../receipt-delivery-policy.js";
33
33
  import { roleRunArtifactsDirectory } from "../role-run-placement.js";
34
+ import { rewriteRunDirectoryPathValue } from "../role-run-relocation.js";
34
35
  import { listSeamOwnedUniqueErrorFacePaths, RUN_TERMINAL_ARTIFACT_FILES, RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS, } from "../run-terminal-artifacts.js";
35
36
  import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
36
37
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
@@ -127,7 +128,11 @@ export async function attachRecordedSubmissions(admitted, terminal, scope) {
127
128
  // pass courtAttemptId here — roleOutcome already carries this-court payloads
128
129
  // from sealedLedgerOutcome when scoped; withSubmissions keeps them.
129
130
  void scope;
130
- return withSubmissions(terminal, await recordedSubmissionPayloads(admitted, undefined));
131
+ const result = withSubmissions(terminal, await recordedSubmissionPayloads(admitted, undefined));
132
+ const errorPath = privateFailureErrorPaths.get(terminal);
133
+ if (errorPath !== undefined)
134
+ privateFailureErrorPaths.set(result, errorPath);
135
+ return result;
131
136
  }
132
137
  /**
133
138
  * Host ended with no recorded submission — lawful no_receipt (exit 0).
@@ -163,6 +168,18 @@ function coordinatesFromAdmitted(authority, admitted) {
163
168
  }
164
169
  import { coalesceSubmissionRows, exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, adviceNavigatorFact, } from "./terminal.js";
165
170
  export { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, };
171
+ // A resumable public Terminal omits artifact paths (#108); its actual error
172
+ // publication remains available only to the CLI's resume presentation seam.
173
+ const privateFailureErrorPaths = new WeakMap();
174
+ export function publishedFailureErrorPath(terminal) {
175
+ return privateFailureErrorPaths.get(terminal);
176
+ }
177
+ export function rewritePublishedFailureErrorPath(terminal, oldRunDirectory, newRunDirectory) {
178
+ const errorPath = privateFailureErrorPaths.get(terminal);
179
+ if (errorPath !== undefined) {
180
+ privateFailureErrorPaths.set(terminal, rewriteRunDirectoryPathValue(errorPath, oldRunDirectory, newRunDirectory));
181
+ }
182
+ }
166
183
  /**
167
184
  * Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
168
185
  */
@@ -2282,7 +2299,7 @@ async function writeFailureJsonRetainingCause(preferredCandidates, uniqueFallbac
2282
2299
  error.publicationAttempts = issues;
2283
2300
  throw error;
2284
2301
  }
2285
- export async function publishFailureArtifacts(admitted, failure, authority) {
2302
+ export async function publishFailureArtifacts(admitted, failure, authority, onErrorPublished) {
2286
2303
  const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
2287
2304
  const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
2288
2305
  const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
@@ -2346,6 +2363,7 @@ export async function publishFailureArtifacts(admitted, failure, authority) {
2346
2363
  ...(failure.details === undefined ? {} : { details: failure.details }),
2347
2364
  };
2348
2365
  const errorWrite = await writeFailureJsonRetainingCause(errorCandidates, uniqueFallbackDirs, "error", errorPayloadBase, priorIssues);
2366
+ onErrorPublished?.(errorWrite.path);
2349
2367
  const evidencePayload = {
2350
2368
  runId: admitted.runId,
2351
2369
  sessionDirectory: sessionDirectory,
@@ -2437,7 +2455,8 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2437
2455
  // Exact-session attendance only — never infer no-advice from caller omission.
2438
2456
  const navigator = await extractNavigatorFactFromAdmittedSession(sessionFile);
2439
2457
  // Private durable artifacts retain the original diagnostic identity (including run ID).
2440
- const artifacts = await publishFailureArtifacts(admitted, failure, authority);
2458
+ const artifacts = await publishFailureArtifacts(admitted, failure, authority, options.onErrorPublished);
2459
+ const errorPath = artifacts.find((artifact) => artifact.kind === "error")?.path;
2441
2460
  const decisiveFacts = {
2442
2461
  ...(failure.cause === undefined ? {} : { cause: failure.cause }),
2443
2462
  diagnostic: failure.diagnostic,
@@ -2463,12 +2482,15 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2463
2482
  diagnostic: failure.diagnostic,
2464
2483
  decisiveFacts,
2465
2484
  };
2466
- return withOptionalGateProjection({
2485
+ const terminal = await withOptionalGateProjection({
2467
2486
  roleOutcome,
2468
2487
  navigator,
2469
2488
  artifacts: [],
2470
2489
  resume: options.resume,
2471
2490
  }, sessionDirectory, detourGateContext(admitted, options));
2491
+ if (errorPath !== undefined)
2492
+ privateFailureErrorPaths.set(terminal, errorPath);
2493
+ return terminal;
2472
2494
  }
2473
2495
  const roleOutcome = {
2474
2496
  kind: "failure",
@@ -2499,6 +2521,8 @@ export function presentFailureTerminal(terminal, io) {
2499
2521
  }
2500
2522
  io.stdout(formatTerminalResult(terminal));
2501
2523
  if (terminal.roleOutcome.kind === "failure") {
2524
+ if (io.omitFailureStderrDiagnostic)
2525
+ return;
2502
2526
  io.stderr(formatFailureStderrDiagnostic({
2503
2527
  ...(terminal.roleOutcome.cause === undefined ? {} : { cause: terminal.roleOutcome.cause }),
2504
2528
  diagnostic: terminal.roleOutcome.diagnostic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5096",
3
+ "version": "0.1.5124",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -10,12 +10,18 @@ parameters.
10
10
  ## Invocation examples (local OpenCode CLI)
11
11
 
12
12
  The machine entrypoint is `opencode`. Run from the role project root.
13
- Non-interactive labor uses `run` with an explicit model:
13
+ Non-interactive labor uses `run` with `--auto` and an explicit model:
14
14
 
15
15
  ```bash
16
- opencode run -m <MODEL_ID> "YOUR_LABOR_PROMPT"
16
+ opencode run --auto -m <MODEL_ID> "YOUR_LABOR_PROMPT"
17
17
  ```
18
18
 
19
+ - `--auto` is required: it auto-approves permissions that are not explicitly
20
+ denied. Without it, any `ask` permission (e.g. `external_directory` when the
21
+ labor reads the case dossier outside the project root) is auto-rejected in a
22
+ headless `run`, and the engine exits after its opening sentence with no labor
23
+ done (upstream anomalyco/opencode#36413).
24
+
19
25
  - Model id is not pinned by this note; it comes from the dispatch order
20
26
  (owner pool directive), passed verbatim as `provider/model`. **Always
21
27
  confirm the current id with `opencode models` first** — provider prefixes
@@ -23,7 +29,6 @@ opencode run -m <MODEL_ID> "YOUR_LABOR_PROMPT"
23
29
  GLM lives under `opencode-go/`, e.g. `opencode-go/glm-5.2`; the older
24
30
  `zai/glm-5.2` id errors with "Unexpected server error").
25
31
  - Plain text output only (never `--format json` for labor — the returned body
26
- goes back into the seat's context); `--auto` auto-approves non-denied
27
- permissions for unattended runs (per official docs).
32
+ goes back into the seat's context).
28
33
  - Output goes to stdout; long prompts may be passed via shell heredoc or a
29
34
  file read into the argument — follow the installed CLI's actual interface.
@@ -1,4 +1,6 @@
1
1
  export type CliIo = {
2
2
  stdout: (text: string) => void;
3
3
  stderr: (text: string) => void;
4
+ /** Public resume presents its failure diagnostic via a durable-file pointer instead. */
5
+ omitFailureStderrDiagnostic?: boolean;
4
6
  };
@@ -1,4 +1,4 @@
1
- import type { DurablePrincipalAuthority } from "../host-contracts.ts";
1
+ import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
2
2
  import { piDurablePrincipalAuthority } from "../pi/durable-principal.ts";
3
3
  /**
4
4
  * Public ak-role CLI dispatcher (roles / config / layered help / Judge run).
@@ -49,8 +49,10 @@ import { seatModelOnly } from "./registry.ts";
49
49
  import { CliUsageError } from "./cli-errors.ts";
50
50
  import type { CliIo } from "./cli-io.ts";
51
51
  import { runMachineSkillSetup } from "./machine-method-skills.ts";
52
- import type { PostAdmissionEnv } from "./post-admission.ts";
53
- import type { RoleTurnHost } from "../host-contracts.ts";
52
+ import {
53
+ presentLocatedResumeFailure,
54
+ type PostAdmissionEnv,
55
+ } from "./post-admission.ts";
54
56
  import { appendPiSessionCustomEntry } from "../pi/role-turn-host.ts";
55
57
  import {
56
58
  composeRoleTurnHostAdapters,
@@ -78,6 +80,7 @@ import {
78
80
  type PublicRoleOptionOwner,
79
81
  type TypedOptionConsumer,
80
82
  } from "./option-definitions.ts";
83
+ import { TurnDispatchedFailure } from "./auto-resume.ts";
81
84
  import { continueParentAfterChild, runPublicInstructionSeat, runPublicInstructionSeatResume } from "./instruction-seat-run.ts";
82
85
  import { courtDiaristEscalated } from "./countersign-run.ts";
83
86
  import { runPublicAnalyst } from "./analyst-run.ts";
@@ -1093,6 +1096,9 @@ export async function runAkRole(
1093
1096
  env: CliEnv,
1094
1097
  ): Promise<CliResult> {
1095
1098
  const io = env.io ?? defaultIo();
1099
+ let resumeFailureContext:
1100
+ | { readonly home: string; readonly runId: string; readonly authority: DurablePrincipalAuthority }
1101
+ | undefined;
1096
1102
 
1097
1103
  try {
1098
1104
  // Select the installed package identity once, before any role-owned Skill,
@@ -1207,12 +1213,17 @@ export async function runAkRole(
1207
1213
  // must still exist. Seat and dispatch follow the durable admitted role.
1208
1214
  // #471: unique parser owns {runId, message?}; five role paths only consume it.
1209
1215
  if (parsed.command === "resume") {
1216
+ const resumeRequest = parseResumeRequest(parsed.args);
1217
+ resumeFailureContext = {
1218
+ home,
1219
+ runId: resumeRequest.runId,
1220
+ authority: env.principalAuthority ?? piDurablePrincipalAuthority,
1221
+ };
1210
1222
  const agentDir = resolveAgentDir(env, home);
1211
1223
  const cwd = env.cwd ?? process.cwd();
1212
1224
  const config = await loadAndValidateConfig(home, env.packageRoot);
1213
1225
  const credentials =
1214
1226
  env.credentials ?? (await loadCredentialProviders(agentDir));
1215
- const resumeRequest = parseResumeRequest(parsed.args);
1216
1227
  const resumeRole = await peekRoleRunRole(home, resumeRequest.runId);
1217
1228
  // Missing durable role keeps the judge seat table. The resume entry
1218
1229
  // itself is one function; it reads the stored run.
@@ -1243,6 +1254,11 @@ export async function runAkRole(
1243
1254
  && !courtDiaristEscalated(current.terminal.roleOutcome)
1244
1255
  ) {
1245
1256
  const parentRunId = current.admitted.correlationId;
1257
+ resumeFailureContext = {
1258
+ home,
1259
+ runId: parentRunId,
1260
+ authority: env.principalAuthority ?? piDurablePrincipalAuthority,
1261
+ };
1246
1262
  const parentRole = await peekRoleRunRole(home, parentRunId);
1247
1263
  if (parentRole === undefined) break;
1248
1264
  const parentSeat = resolveEffectiveSeat(config, parentRole, credentials, invocationFromParsed(parsed));
@@ -1287,6 +1303,24 @@ export async function runAkRole(
1287
1303
  // tokens (including misspelled role names) are structural rejects.
1288
1304
  throw new CliUsageError(`unknown command: ${parsed.command}`);
1289
1305
  } catch (error) {
1306
+ // A dispatched turn owns its settlement; only pre-dispatch resume failures
1307
+ // fall through to the located-run diagnostic presenter here.
1308
+ if (
1309
+ resumeFailureContext !== undefined
1310
+ && !(error instanceof TurnDispatchedFailure)
1311
+ && await presentLocatedResumeFailure(
1312
+ resumeFailureContext.home,
1313
+ resumeFailureContext.runId,
1314
+ resumeFailureContext.authority,
1315
+ io,
1316
+ error,
1317
+ )
1318
+ ) {
1319
+ return {
1320
+ exitCode: error instanceof CliUsageError ? 2 : 1,
1321
+ ...(error instanceof HostSelectionError ? { hostFailure: error.failure } : {}),
1322
+ };
1323
+ }
1290
1324
  if (error instanceof HostSelectionError) {
1291
1325
  io.stderr(formatCliDiagnostic(formatHostSelectionFailure(error.failure)));
1292
1326
  return { exitCode: 1, hostFailure: error.failure };
@@ -33,6 +33,7 @@ import {
33
33
  type AdmittedCountersignInvocation,
34
34
  type AdmittedRoleInvocation,
35
35
  type PublicSeatParse,
36
+ type RunDirectoryRelocation,
36
37
  } from "./invocation.ts";
37
38
  import {
38
39
  presentControlledFailure,
@@ -41,6 +42,7 @@ import {
41
42
  runPostAdmissionOneShot,
42
43
  runPostAdmissionResumable,
43
44
  runPostAdmissionSeatResume,
45
+ showResumeErrorPointer,
44
46
  resumeTurnRequestProjectionOptions,
45
47
  type PostAdmissionAdapters,
46
48
  type PostAdmissionEnv,
@@ -143,13 +145,13 @@ async function bindAndRelocateDiarist(
143
145
  admitted: AdmittedRoleInvocation & { role: "diarist" },
144
146
  authority: DurablePrincipalAuthority,
145
147
  lease?: RunWriterLease,
146
- ): Promise<void> {
148
+ ): Promise<RunDirectoryRelocation | undefined> {
147
149
  const boardTicket = await readBoardTicketNumber(admitted.runDirectory);
148
- if (boardTicket === undefined) return;
150
+ if (boardTicket === undefined) return undefined;
149
151
  if (admitted.ticketNumber === undefined) {
150
152
  await bindAdmittedTicketNumber(admitted, boardTicket);
151
153
  }
152
- await relocateAdmittedRunToTicket(admitted, authority, lease);
154
+ return await relocateAdmittedRunToTicket(admitted, authority, lease);
153
155
  }
154
156
 
155
157
  function seatAdapters(
@@ -176,8 +178,8 @@ function seatAdapters(
176
178
  await bindAndRelocateDiarist(seat, env.principalAuthority, lease);
177
179
  },
178
180
  afterDispatch: async (seat: AdmittedRoleInvocation, lease?: RunWriterLease) => {
179
- if (!isBoardTicketSeat(seat)) return;
180
- await bindAndRelocateDiarist(seat, env.principalAuthority, lease);
181
+ if (!isBoardTicketSeat(seat)) return undefined;
182
+ return await bindAndRelocateDiarist(seat, env.principalAuthority, lease);
181
183
  },
182
184
  }
183
185
  : {}),
@@ -824,5 +826,7 @@ export async function continueParentAfterChild(
824
826
  return { exitCode: 0, admitted, terminal: refresh.terminal };
825
827
  }
826
828
  }
827
- return dispatchAdmitted(admitted, env, io);
829
+ const result = await dispatchAdmitted(admitted, env, { ...io, omitFailureStderrDiagnostic: true });
830
+ showResumeErrorPointer(io, result.exitCode, result.terminal);
831
+ return result;
828
832
  }
@@ -308,6 +308,11 @@ export type AdmittedRoleInvocation =
308
308
  | AdmittedReviewerInvocation
309
309
  | AdmittedMergerInvocation;
310
310
 
311
+ export type RunDirectoryRelocation = {
312
+ readonly oldRunDirectory: string;
313
+ readonly newRunDirectory: string;
314
+ };
315
+
311
316
  /** Persistence projection only — not carried on Admitted (opaque principal owns identity). */
312
317
  type RoleInvocationLedgerSource = Pick<
313
318
  AdmittedRoleInvocationBase,
@@ -641,7 +646,7 @@ export async function relocateAdmittedRunToTicket(
641
646
  admitted: AdmittedRoleInvocation,
642
647
  authority: DurablePrincipalAuthority,
643
648
  heldLease?: { relocate(runDirectory: string): void },
644
- ): Promise<{ oldRunDirectory: string; newRunDirectory: string } | undefined> {
649
+ ): Promise<RunDirectoryRelocation | undefined> {
645
650
  if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return undefined;
646
651
  const oldRunDirectory = admitted.runDirectory;
647
652
  const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));