@akagilnc/pi-workflow-roles 0.1.3652 → 0.1.3663

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.
@@ -24,9 +24,8 @@ import {
24
24
  type ParseDiaristArgvResult,
25
25
  } from "./invocation.ts";
26
26
  import {
27
- applyInstructionTicketProbe,
28
- probeInstructionTicket,
29
- ticketNumberFromProbe,
27
+ bindReusedTicketNumber,
28
+ resolveDiaristSummonsTicketNumber,
30
29
  tryResumeSameTicketSeatRun,
31
30
  } from "./seat-ticket-binding.ts";
32
31
  import {
@@ -170,16 +169,15 @@ export async function runPublicDiarist(
170
169
  }
171
170
 
172
171
  // #637: same ticket → resume this seat's prior run with this summons' materials.
173
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
174
- // controlled failure (bare pre-admit throw skips terminal settlement).
172
+ // #709 / ADR 0081: reuse book records, or take the caller's sole dispatch token
173
+ // on a first summons that has not yet minted a volume — no seat recognizer call.
175
174
  const projectRoot = parsed.project ?? env.cwd;
176
- const ticketProbe = await probeInstructionTicket(
177
- parsed.instruction,
175
+ const reusedTicketNumber = await resolveDiaristSummonsTicketNumber({
176
+ instruction: parsed.instruction,
178
177
  projectRoot,
179
- env,
180
- );
181
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
182
- if (probedTicketNumber !== undefined) {
178
+ home: env.home,
179
+ });
180
+ if (reusedTicketNumber !== undefined) {
183
181
  const summons: SameTicketSummonsMaterials = {
184
182
  instruction: parsed.instruction,
185
183
  instructionEmpty: parsed.instruction.trim() === "",
@@ -189,7 +187,7 @@ export async function runPublicDiarist(
189
187
  home: env.home,
190
188
  projectRoot,
191
189
  role: "diarist",
192
- ticketNumber: probedTicketNumber,
190
+ ticketNumber: reusedTicketNumber,
193
191
  freshSummons: env.freshSummons,
194
192
  summons,
195
193
  resume: (runId, materials) =>
@@ -256,7 +254,7 @@ export async function runPublicDiarist(
256
254
  request: turnRequest,
257
255
  adapters: diaristAdapters({
258
256
  beforeDispatch: async (admittedSeat) => {
259
- await applyInstructionTicketProbe(admittedSeat, ticketProbe);
257
+ await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
260
258
  const sourcesPath = await freezeDiaristSourceCatalog(admittedSeat, env);
261
259
  Object.assign(
262
260
  turnRequest,
@@ -9,8 +9,8 @@ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contrac
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
10
10
  import { CliUsageError } from "./cli-errors.ts";
11
11
  import {
12
- applyInstructionTicketProbe,
13
- probeInstructionTicket,
12
+ bindReusedTicketNumber,
13
+ resolveKnownTicketNumber,
14
14
  tryResumeSameTicketSeatRun,
15
15
  } from "./seat-ticket-binding.ts";
16
16
  import {
@@ -88,15 +88,14 @@ export async function runPublicInspector(
88
88
  }
89
89
 
90
90
  // #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials.
91
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
92
- // controlled failure (bare pre-admit throw skips terminal settlement).
91
+ // #709: ticket identity is reused from records this book already holds — no seat model call.
93
92
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
94
93
  const projectRoot = parsed.project ?? env.cwd;
95
- const ticketProbe = await probeInstructionTicket(
96
- parsed.instruction,
94
+ const reusedTicketNumber = await resolveKnownTicketNumber({
95
+ instruction: parsed.instruction,
97
96
  projectRoot,
98
- env,
99
- );
97
+ home: env.home,
98
+ });
100
99
  const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
101
100
  if (parentRunPath !== undefined) {
102
101
  const summons: SameTicketSummonsMaterials = {
@@ -171,8 +170,8 @@ export async function runPublicInspector(
171
170
  request: turnRequest,
172
171
  adapters: inspectorAdapters({
173
172
  beforeDispatch: async (admittedSeat) => {
174
- // #635/#637: apply pre-admit probe inside controlled-failure boundary.
175
- await applyInstructionTicketProbe(admittedSeat, ticketProbe);
173
+ // #635/#709: bind the reused identity inside the controlled-failure boundary.
174
+ await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
176
175
  },
177
176
  }),
178
177
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
@@ -36,11 +36,9 @@ import {
36
36
  type SameTicketSummonsMaterials,
37
37
  } from "./run-lifecycle.ts";
38
38
  import {
39
- applyInstructionTicketProbe,
40
- probeInstructionTicket,
41
- ticketNumberFromProbe,
39
+ bindReusedTicketNumber,
40
+ resolveKnownTicketNumber,
42
41
  tryResumeSameTicketSeatRun,
43
- type InstructionTicketProbe,
44
42
  } from "./seat-ticket-binding.ts";
45
43
  import {
46
44
  presentStructuralRejection,
@@ -248,11 +246,11 @@ export async function runPublicInstructionSeat(
248
246
  let auditorSubject: "judge" | "doctor" | undefined;
249
247
  let auditorSourceRun: string | undefined;
250
248
  let auditorSourceTicket: number | undefined;
251
- let ticketProbe: InstructionTicketProbe | undefined;
249
+ let reusedTicketNumber: number | undefined;
252
250
 
253
251
  // #637 / #747: resume prior seat run with this summons' materials.
254
252
  // Auditor (#747): parent --source-run path is the lookup key.
255
- // Other instruction seats: ticket probe (inspector face). No bare catch→fresh.
253
+ // Other instruction seats: reuse known ticket (#709). No bare catch→fresh.
256
254
  const projectRoot = parsed.project ?? env.cwd;
257
255
  if (role === "auditor") {
258
256
  if (parsed.subject !== "judge" && parsed.subject !== "doctor") {
@@ -288,12 +286,11 @@ export async function runPublicInstructionSeat(
288
286
  return { exitCode: 2 };
289
287
  }
290
288
  } else {
291
- // Inspector face: unconditional probe (same seam as inspector-run.ts).
292
- ticketProbe = await probeInstructionTicket(
293
- parsed.instruction,
289
+ reusedTicketNumber = await resolveKnownTicketNumber({
290
+ instruction: parsed.instruction,
294
291
  projectRoot,
295
- env,
296
- );
292
+ home: env.home,
293
+ });
297
294
  }
298
295
 
299
296
  const summons: SameTicketSummonsMaterials = {
@@ -322,26 +319,22 @@ export async function runPublicInstructionSeat(
322
319
  }),
323
320
  });
324
321
  if (resumed !== undefined) return resumed;
325
- } else {
326
- const probedTicketNumber =
327
- ticketProbe === undefined ? undefined : ticketNumberFromProbe(ticketProbe);
328
- if (probedTicketNumber !== undefined) {
329
- const resumed = await tryResumeSameTicketSeatRun({
330
- home: env.home,
331
- projectRoot,
332
- role,
333
- ticketNumber: probedTicketNumber,
334
- freshSummons: env.freshSummons,
335
- summons,
336
- resume: (runId, materials) =>
337
- runPublicInstructionSeatResume(
338
- { runId, ...(materials === undefined ? {} : { summons: materials }) },
339
- env,
340
- io,
341
- ),
342
- });
343
- if (resumed !== undefined) return resumed;
344
- }
322
+ } else if (reusedTicketNumber !== undefined) {
323
+ const resumed = await tryResumeSameTicketSeatRun({
324
+ home: env.home,
325
+ projectRoot,
326
+ role,
327
+ ticketNumber: reusedTicketNumber,
328
+ freshSummons: env.freshSummons,
329
+ summons,
330
+ resume: (runId, materials) =>
331
+ runPublicInstructionSeatResume(
332
+ { runId, ...(materials === undefined ? {} : { summons: materials }) },
333
+ env,
334
+ io,
335
+ ),
336
+ });
337
+ if (resumed !== undefined) return resumed;
345
338
  }
346
339
 
347
340
  let admitted: AdmittedInstructionSeatInvocation;
@@ -406,13 +399,13 @@ export async function runPublicInstructionSeat(
406
399
  request: turnRequest,
407
400
  adapters: instructionSeatAdapters({
408
401
  beforeDispatch: async (admittedSeat) => {
409
- // #635/#637: ticket bind inside controlled-failure boundary.
402
+ // #635/#709: ticket bind inside controlled-failure boundary.
410
403
  // Auditor inherits source-run ticket (notary face).
411
- // Other seats: same applyInstructionTicketProbe as inspector-run.
404
+ // Other seats: reuse known ticket identity.
412
405
  if (auditorSourceTicket !== undefined) {
413
406
  await bindAdmittedTicketNumber(admittedSeat, auditorSourceTicket);
414
- } else if (ticketProbe !== undefined) {
415
- await applyInstructionTicketProbe(admittedSeat, ticketProbe);
407
+ } else {
408
+ await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
416
409
  }
417
410
  },
418
411
  }),
@@ -105,17 +105,10 @@ export type AdmittedRoleInvocationBase = {
105
105
  */
106
106
  readonly correlationId?: string;
107
107
  /**
108
- * Typed ticketNumber after seat LLM bind or notary source-run inheritance (#635).
109
- * Admission does not bind from CLI flag or attachment frontmatter.
108
+ * Typed ticketNumber after known-identity reuse or notary source-run inheritance
109
+ * (#635 / #709). Admission does not bind from CLI flag or attachment frontmatter.
110
110
  */
111
111
  readonly ticketNumber?: number;
112
- /**
113
- * Seat LLM ticket resolution settled as true-unbound (#635).
114
- * Distinct from unbound-not-yet-resolved (field absent). Mutually exclusive
115
- * with ticketNumber; both are durable one-shot conclusions so auto-resume
116
- * and manual resume never re-enter hermes resolution.
117
- */
118
- readonly ticketResolution?: "true-unbound";
119
112
  /** Effective model from invocation identity; restored on resume when no CLI model is given. */
120
113
  readonly model?: InvocationEffectiveModel;
121
114
  };
@@ -464,8 +457,8 @@ export async function persistAdmittedSourceRunPath(
464
457
  /**
465
458
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
466
459
  * object and both durable pages (invocation.json + admitted-request.json).
467
- * Used by seat LLM ticket binding when admission was unbound
468
- * (#635 / #582 diarist-resolves-ticket-llm-layer). Never clears an existing binding.
460
+ * Used by known-ticket reuse when admission was unbound (#635 / #709).
461
+ * Never clears an existing binding.
469
462
  */
470
463
  export async function bindAdmittedTicketNumber(
471
464
  admitted: AdmittedRoleInvocation,
@@ -474,11 +467,6 @@ export async function bindAdmittedTicketNumber(
474
467
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
475
468
  throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
476
469
  }
477
- if (admitted.ticketResolution === "true-unbound") {
478
- throw new Error(
479
- "bindAdmittedTicketNumber refuses to replace durable true-unbound ticket resolution",
480
- );
481
- }
482
470
  if (admitted.ticketNumber !== undefined) {
483
471
  if (admitted.ticketNumber === ticketNumber) return;
484
472
  throw new Error(
@@ -499,36 +487,6 @@ export async function bindAdmittedTicketNumber(
499
487
  );
500
488
  }
501
489
 
502
- /**
503
- * Persist seat LLM true-unbound conclusion onto in-memory admitted + both durable
504
- * pages so later auto-resume attempts and manual resume never re-enter hermes (#635).
505
- */
506
- export async function recordTrueUnboundTicketResolution(
507
- admitted: AdmittedRoleInvocation,
508
- ): Promise<void> {
509
- if (admitted.ticketNumber !== undefined) {
510
- throw new Error(
511
- `recordTrueUnboundTicketResolution refuses to overwrite ticket #${admitted.ticketNumber}`,
512
- );
513
- }
514
- if (admitted.ticketResolution === "true-unbound") return;
515
- (admitted as { ticketResolution?: "true-unbound" }).ticketResolution =
516
- "true-unbound";
517
- await mergeInvocationIdentityPage(admitted.runDirectory, {
518
- ticketResolution: "true-unbound",
519
- });
520
- const admittedPath = admitted.admittedRequestPath;
521
- const current = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
522
- string,
523
- unknown
524
- >;
525
- await writeFile(
526
- admittedPath,
527
- `${JSON.stringify({ ...current, ticketResolution: "true-unbound" }, null, 2)}\n`,
528
- "utf8",
529
- );
530
- }
531
-
532
490
  /** Add the identity returned by the production Pi launch seam to its existing ledger page. */
533
491
  export async function recordLaunchedPiIdentity(
534
492
  runDirectory: string,
@@ -4,6 +4,9 @@
4
4
  * the shared post-admission seam; this module keeps only Notary adapters.
5
5
  * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run.
6
6
  */
7
+ import { existsSync } from "node:fs";
8
+
9
+ import { gateSubmissionCandidatePath } from "../auditor-dossier-tool.ts";
7
10
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
8
11
  import {
9
12
  NotarySourceRunError,
@@ -26,6 +29,7 @@ import {
26
29
  resumeTurnRequestProjectionOptions,
27
30
  } from "./post-admission.ts";
28
31
  import {
32
+ appendResumeMaterialReread,
29
33
  loadResumableNotaryRun,
30
34
  markRunAdmitted,
31
35
  type PublicResumeRequest,
@@ -234,13 +238,21 @@ export async function runPublicNotaryResume(
234
238
  }
235
239
  return loaded;
236
240
  },
237
- buildTurnRequest: (admitted, effective) =>
238
- // #755: review continuation is plain dialogue / resume envelope — no
239
- //「重新读」candidate line, no engine handbook packaging.
240
- buildNotaryTurnRequest(
241
+ buildTurnRequest: (admitted, effective) => {
242
+ const projection = resumeTurnRequestProjectionOptions(
241
243
  admitted,
242
- resumeTurnRequestProjectionOptions(admitted, effective, env),
243
- ),
244
+ effective,
245
+ env,
246
+ );
247
+ const candidatePath = gateSubmissionCandidatePath(admitted.sourceRunPath);
248
+ const prompt = existsSync(candidatePath)
249
+ ? appendResumeMaterialReread(projection.continuation.prompt, candidatePath)
250
+ : projection.continuation.prompt;
251
+ return buildNotaryTurnRequest(admitted, {
252
+ ...projection,
253
+ continuation: { kind: "resume", prompt },
254
+ });
255
+ },
244
256
  adapters: notaryAdapters(),
245
257
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
246
258
  });
@@ -11,7 +11,7 @@ import { isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import {
13
13
  buildResumeContinuationPrompt,
14
- RESUME_TRANSPORT_ENVELOPE,
14
+ instructResumeHandbookRead,
15
15
  type PublicResumeRequest,
16
16
  type SameTicketSummonsMaterials,
17
17
  } from "./run-lifecycle.ts";
@@ -22,17 +22,31 @@ import {
22
22
  freezeAttachmentsIntoRun,
23
23
  } from "./invocation.ts";
24
24
  import { pathContainedIn } from "../activation-ledger-topology.ts";
25
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
25
26
 
26
27
  import type {
27
28
  ControlledFailureCause,
28
29
  DurablePrincipal,
29
30
  DurablePrincipalAuthority,
31
+ RoleTurnContinuation,
30
32
  RoleTurnHost,
31
33
  RoleTurnKnownFailure,
32
34
  RoleTurnRequest,
33
35
  RoleTurnResult,
34
36
  SessionCustomEntryAppender,
35
37
  } from "../host-contracts.ts";
38
+ import { projectCaseDossierPointerSection } from "./case-dossier-delivery.ts";
39
+
40
+ /** Append one system section to a continuation prompt, keeping its kind. */
41
+ function appendContinuationSection(
42
+ continuation: RoleTurnContinuation,
43
+ section: string,
44
+ ): RoleTurnContinuation {
45
+ const prompt = `${continuation.prompt}\n\n${section}`;
46
+ return continuation.kind === "initial"
47
+ ? { kind: "initial", prompt }
48
+ : { kind: "resume", prompt };
49
+ }
36
50
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.ts";
37
51
  import type { CredentialProviders, SeatModelConfig } from "./config.ts";
38
52
  import {
@@ -297,10 +311,10 @@ export async function dispatchPostAdmissionTurn<
297
311
  admitted.principal === undefined
298
312
  ? undefined
299
313
  : env.principalAuthority.decode(admitted.principal);
300
- let turnRequest: RoleTurnRequest;
314
+ let hostTransition: RoleTurnRequest["hostTransition"];
301
315
  try {
302
316
  previousHost = await readInvocationHost(admitted.runDirectory);
303
- const hostTransition =
317
+ hostTransition =
304
318
  previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
305
319
  ? await projectHostTransitionPriorNative({
306
320
  previousHost,
@@ -308,10 +322,6 @@ export async function dispatchPostAdmissionTurn<
308
322
  piSessionFile: principalCoordinates.sessionFile,
309
323
  })
310
324
  : undefined;
311
- turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
312
- if (hostTransition !== undefined) {
313
- turnRequest = { ...turnRequest, hostTransition };
314
- }
315
325
  } catch (error) {
316
326
  // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
317
327
  return (await presentControlledFailure(
@@ -351,6 +361,32 @@ export async function dispatchPostAdmissionTurn<
351
361
  }
352
362
  }
353
363
 
364
+ // Turn request is assembled after beforeDispatch so this turn sees whatever it
365
+ // settled — the seat's ticket bind re-projection. Case dossier delivery
366
+ // (ADR 0081) rides here for every public entry: first call, same-ticket
367
+ // re-summons and manual resume alike. System refs append their own neutral
368
+ // section; caller frozen attachments and the seat's own prompt bytes are
369
+ // never rewritten.
370
+ let turnRequest: RoleTurnRequest =
371
+ env.signal === undefined ? request : { ...request, signal: env.signal };
372
+ if (hostTransition !== undefined) {
373
+ turnRequest = { ...turnRequest, hostTransition };
374
+ }
375
+ const dossierSection = await projectCaseDossierPointerSection({
376
+ ticketNumber: admitted.ticketNumber,
377
+ projectRoot: admitted.projectRoot,
378
+ home: env.home,
379
+ });
380
+ if (dossierSection !== undefined) {
381
+ turnRequest = {
382
+ ...turnRequest,
383
+ continuation: appendContinuationSection(
384
+ turnRequest.continuation,
385
+ dossierSection,
386
+ ),
387
+ };
388
+ }
389
+
354
390
  let result: RoleTurnResult;
355
391
  try {
356
392
  result = await env.roleTurnHost.executeTurn(turnRequest);
@@ -458,18 +494,15 @@ export async function dispatchPostAdmissionTurn<
458
494
  }
459
495
 
460
496
  /**
461
- * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755):
462
- * seat-table model/engine/timeout axes, restored correlation, and either
463
- * - manual resume (no same-ticket summons): package envelope / optional caller
464
- * message, with engine-axis handbook via buildResumeContinuationPrompt, or
465
- * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
466
- * attachment paths only — no「重新读」、no engine handbook packaging
467
- * (#750/#755), whether or not attachments are present.
468
- * Caller message wins as prompt base when supplied (bytes unchanged, including
469
- * blank/whitespace); else summons instruction. Attachment projection must not
470
- * re-interpret the caller message as instructionEmpty.
497
+ * Shared resume continuation projection (#471 / #600 / #633 / #637): seat-table
498
+ * model/engine/timeout axes, restored correlation, and either
499
+ * - manual resume: package envelope / optional caller message (unchanged), or
500
+ * - same-ticket summons: this turn's instruction + frozen attachment paths.
501
+ * Caller message and summons materials each keep their place: a resume message
502
+ * keeps manual-resume prompt semantics while open-court frozen attachments still
503
+ * ride; summons instruction is only the prompt base when no caller message.
471
504
  * Seats add only their activation projection. Call prepareSummonsResumeMaterials
472
- * first when request.summons carries instruction or attachment paths.
505
+ * first when request.summons carries attachment paths.
473
506
  */
474
507
  export function resumeTurnRequestProjectionOptions(
475
508
  admitted: AdmittedRoleInvocation,
@@ -481,21 +514,32 @@ export function resumeTurnRequestProjectionOptions(
481
514
  readonly attachments: readonly { frozenPath: string }[];
482
515
  },
483
516
  ): RoleTurnRequestProjectionOptions {
517
+ const engineMaterial = engineSessionMaterialFromOptions({
518
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
519
+ packageRoot: env.packageRoot,
520
+ });
484
521
  let prompt: string;
485
522
  if (request.message !== undefined) {
486
- if (summonsPrepared !== undefined) {
487
- // #755: same-ticket review / open-court — caller words + optional paths.
488
- // Attachments are not a gate: message-only summons must stay plain too.
489
- prompt = buildInstructionTransportPrompt({
490
- instruction: request.message,
491
- instructionEmpty: false,
492
- attachments: summonsPrepared.attachments,
493
- });
494
- } else if (request.summons !== undefined) {
495
- // #755: same-ticket summons without prepared materials — caller words only.
496
- prompt = request.message;
523
+ // Manual resume caller-message semantics stay authoritative for the prompt:
524
+ // message present → base bytes unchanged (including blank/whitespace).
525
+ // Open-court frozen attachments still continue when present; attachment
526
+ // projection must not re-interpret the caller message as instructionEmpty.
527
+ if (
528
+ summonsPrepared !== undefined &&
529
+ summonsPrepared.attachments.length > 0
530
+ ) {
531
+ prompt = instructResumeHandbookRead(
532
+ buildInstructionTransportPrompt(
533
+ {
534
+ instruction: request.message,
535
+ instructionEmpty: false,
536
+ attachments: summonsPrepared.attachments,
537
+ },
538
+ engineMaterial,
539
+ ),
540
+ engineMaterial,
541
+ );
497
542
  } else {
498
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
499
543
  prompt = buildResumeContinuationPrompt({
500
544
  packageRoot: env.packageRoot,
501
545
  ...(env.engine === undefined ? {} : { engine: env.engine }),
@@ -503,14 +547,11 @@ export function resumeTurnRequestProjectionOptions(
503
547
  });
504
548
  }
505
549
  } else if (summonsPrepared !== undefined) {
506
- // #755: same-ticket review summons — instruction/attachments only.
507
- prompt = buildInstructionTransportPrompt(summonsPrepared);
508
- } else if (request.summons !== undefined) {
509
- // #755: same-ticket summons with no instruction/attachments (e.g. notary
510
- // source-run pointer) — plain resume envelope, no handbook / 重新读.
511
- prompt = RESUME_TRANSPORT_ENVELOPE;
550
+ prompt = instructResumeHandbookRead(
551
+ buildInstructionTransportPrompt(summonsPrepared, engineMaterial),
552
+ engineMaterial,
553
+ );
512
554
  } else {
513
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
514
555
  prompt = buildResumeContinuationPrompt({
515
556
  packageRoot: env.packageRoot,
516
557
  ...(env.engine === undefined ? {} : { engine: env.engine }),