@akagilnc/pi-workflow-roles 0.1.4489 → 0.1.4508

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.
@@ -13,27 +13,34 @@
13
13
  * effect is what this seat currently does. 起居录 path delivery is owned once by
14
14
  * post-admission (#709 / ADR 0081).
15
15
  *
16
- * Wiring (#771): admit first so the countersign run exists; resolve typed identity
17
- * from 起居郎 after admit; same-ticket resume uses that typed key (abandon the
18
- * unused mint when resuming). 起居郎 escalate (认不出) and typed failure terminals
16
+ * Wiring (#771 / #863): resolve typed identity before materializing the admitted
17
+ * run; same-ticket resume writes only to the retained run. 起居郎 escalate (认不出)
18
+ * and typed failure terminals
19
19
  * (incl. verification failure) settle as countersign controlled failure — never
20
20
  * wash into 真无票. Only a true missing lawful typed terminal stays unbound and
21
21
  * continues the body (r5 unbound-continue). Bound refresh hands the typed key to
22
22
  * 起居郎 so freeze loads issue face (ADR 0075: 每次过庭都跑是调用者用法 / typed handoff).
23
23
  */
24
- import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
25
- import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts";
26
- import { CliUsageError } from "./cli-errors.ts";
24
+ import type {
25
+ DurablePrincipalAuthority,
26
+ RoleTurnRequest,
27
+ } from "../host-contracts.ts";
27
28
  import {
28
- projectCourtTicketNumbers,
29
- } from "../diarist-contracts.ts";
29
+ engineSessionMaterialFromOptions,
30
+ pickEngineAxis,
31
+ } from "../package-resources/engine-material.ts";
32
+ import { CliUsageError } from "./cli-errors.ts";
33
+ import { projectCourtTicketNumbers } from "../diarist-contracts.ts";
30
34
  import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
31
35
  import {
32
36
  admitCountersignInvocation,
33
37
  bindAdmittedTicketNumber,
34
38
  bindCourtTicketNumbersOnAdmitted,
35
39
  buildCountersignTransportPrompt,
40
+ freezePreparedAttachmentsIntoRun,
41
+ materializeCountersignInvocation,
36
42
  relocateAdmittedRunToTicket,
43
+ withPreparedAttachments,
37
44
  type AdmittedCountersignInvocation,
38
45
  type ParseCountersignArgvResult,
39
46
  } from "./invocation.ts";
@@ -49,7 +56,6 @@ import {
49
56
  import {
50
57
  loadResumableCountersignRun,
51
58
  markRunAdmitted,
52
- markRunTerminal,
53
59
  type PublicResumeRequest,
54
60
  type RunWriterLease,
55
61
  type SameTicketSummonsMaterials,
@@ -155,11 +161,15 @@ function courtTicketNumbersFromOutcome(
155
161
  roleOutcome.kind === "accepted" ||
156
162
  roleOutcome.kind === "audit_escalation" ||
157
163
  roleOutcome.kind === "failure"
158
- ? roleOutcome.payloads ?? []
164
+ ? (roleOutcome.payloads ?? [])
159
165
  : [];
160
166
  let latest: readonly number[] | undefined;
161
167
  for (const payload of payloads) {
162
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
168
+ if (
169
+ typeof payload !== "object" ||
170
+ payload === null ||
171
+ Array.isArray(payload)
172
+ ) {
163
173
  continue;
164
174
  }
165
175
  const record = payload as Record<string, unknown>;
@@ -173,25 +183,34 @@ function courtTicketNumbersFromOutcome(
173
183
  if (members.length === 0) {
174
184
  // Literal [] is the intentional principal-only replace; non-empty garbage is not.
175
185
  if (raw.length === 0) {
176
- latest = projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
186
+ latest =
187
+ projectCourtTicketNumbers([], { principalTicket }) ?? undefined;
177
188
  }
178
189
  continue;
179
190
  }
180
- latest =
181
- projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
191
+ latest = projectCourtTicketNumbers(raw, { principalTicket }) ?? undefined;
182
192
  }
183
193
  return latest;
184
194
  }
185
195
 
186
196
  /** Routing boolean over the child's own typed sequence — does not pick or rewrite a sole row. */
187
- function courtDiaristEscalated(roleOutcome: TerminalRoleOutcome | undefined): boolean {
197
+ function courtDiaristEscalated(
198
+ roleOutcome: TerminalRoleOutcome | undefined,
199
+ ): boolean {
188
200
  if (roleOutcome === undefined) return false;
189
201
  if (roleOutcome.kind === "audit_escalation") return true;
190
202
  if (roleOutcome.kind !== "accepted") return false;
191
203
  return (roleOutcome.payloads ?? []).some((payload) => {
192
- if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return false;
204
+ if (
205
+ typeof payload !== "object" ||
206
+ payload === null ||
207
+ Array.isArray(payload)
208
+ )
209
+ return false;
193
210
  const record = payload as Record<string, unknown>;
194
- return record.status === "escalate" || record.countersignStatus === "escalate";
211
+ return (
212
+ record.status === "escalate" || record.countersignStatus === "escalate"
213
+ );
195
214
  });
196
215
  }
197
216
 
@@ -203,16 +222,20 @@ function courtDiaristEscalated(roleOutcome: TerminalRoleOutcome | undefined): bo
203
222
  * When `boundTicketNumber` is set (typed handoff from countersign), diarist
204
223
  * binds under that key before the turn — never mechanical recognition from prose.
205
224
  */
206
- export async function invokeCourtDiarist(input: {
207
- readonly instruction: string;
208
- readonly projectRoot: string;
209
- readonly failureLabel: string;
210
- readonly attachmentPaths?: readonly string[];
211
- /** Direct parent run id for durable child lineage (ADR 0010). */
212
- readonly correlationId?: string;
213
- /** Already-verified typed key from countersign (refresh / post-assert handoff). */
214
- readonly boundTicketNumber?: number;
215
- }, env: CourtDiaristSummonEnv, io: CliIo): Promise<CourtDiaristInvocationResult> {
225
+ export async function invokeCourtDiarist(
226
+ input: {
227
+ readonly instruction: string;
228
+ readonly projectRoot: string;
229
+ readonly failureLabel: string;
230
+ readonly attachmentPaths?: readonly string[];
231
+ /** Direct parent run id for durable child lineage (ADR 0010). */
232
+ readonly correlationId?: string;
233
+ /** Already-verified typed key from countersign (refresh / post-assert handoff). */
234
+ readonly boundTicketNumber?: number;
235
+ },
236
+ env: CourtDiaristSummonEnv,
237
+ io: CliIo,
238
+ ): Promise<CourtDiaristInvocationResult> {
216
239
  // Quiet face: the countersign caller must not see diarist CLI chatter.
217
240
  const quietIo: CliIo = {
218
241
  stdout() {},
@@ -239,13 +262,17 @@ export async function invokeCourtDiarist(input: {
239
262
  io: quietIo,
240
263
  ...(env.credentials === undefined ? {} : { credentials: env.credentials }),
241
264
  ...(env.signal === undefined ? {} : { signal: env.signal }),
242
- ...(input.correlationId === undefined ? {} : { correlationId: input.correlationId }),
265
+ ...(input.correlationId === undefined
266
+ ? {}
267
+ : { correlationId: input.correlationId }),
243
268
  ...(input.boundTicketNumber === undefined
244
269
  ? {}
245
270
  : { boundTicketNumber: input.boundTicketNumber }),
246
271
  // Child seat selects from the composition-root table. Do not pass the
247
272
  // already-selected parent adapter (#840 / ADR 0082: --host 旗标>席位配置>缺省 pi).
248
- ...(env.hostAdapters === undefined ? {} : { hostAdapters: env.hostAdapters }),
273
+ ...(env.hostAdapters === undefined
274
+ ? {}
275
+ : { hostAdapters: env.hostAdapters }),
249
276
  });
250
277
 
251
278
  const roleOutcome = result.terminal?.roleOutcome;
@@ -270,9 +297,13 @@ export async function invokeCourtDiarist(input: {
270
297
  };
271
298
  }
272
299
 
273
- const asserted = (result.admitted as { ticketNumber?: number } | undefined)?.ticketNumber;
300
+ const asserted = (result.admitted as { ticketNumber?: number } | undefined)
301
+ ?.ticketNumber;
274
302
  if (isSafePositiveTicketNumber(asserted)) {
275
- const courtTicketNumbers = courtTicketNumbersFromOutcome(roleOutcome, asserted);
303
+ const courtTicketNumbers = courtTicketNumbersFromOutcome(
304
+ roleOutcome,
305
+ asserted,
306
+ );
276
307
  return {
277
308
  identity: {
278
309
  kind: "ticket",
@@ -348,7 +379,9 @@ export async function runCountersignCourtDiaristStation(
348
379
  );
349
380
  }
350
381
  if (outcome.failedWithoutEscalate !== undefined) {
351
- throw new StationChildExhaustedError(outcome.failedWithoutEscalate.diagnostic);
382
+ throw new StationChildExhaustedError(
383
+ outcome.failedWithoutEscalate.diagnostic,
384
+ );
352
385
  }
353
386
  }
354
387
  }
@@ -383,9 +416,14 @@ export async function runPublicCountersign(
383
416
  instruction: parsed.instruction,
384
417
  attachmentPaths: parsed.attachmentPaths,
385
418
  ...(parsed.project === undefined ? {} : { project: parsed.project }),
386
- ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }),
419
+ ...(env.createRunId === undefined
420
+ ? {}
421
+ : { createRunId: env.createRunId }),
387
422
  ...(env.model === undefined ? {} : { model: env.model }),
388
- ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
423
+ ...(env.correlationId === undefined
424
+ ? {}
425
+ : { correlationId: env.correlationId }),
426
+ deferPersistence: true,
389
427
  });
390
428
  } catch (error) {
391
429
  if (error instanceof CliUsageError) {
@@ -395,199 +433,267 @@ export async function runPublicCountersign(
395
433
  throw error;
396
434
  }
397
435
 
398
- await markRunAdmitted(admitted, env.principalAuthority);
399
-
400
- // #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
401
- // (never mechanical matching of summons text). Resolve that typed key after
402
- // admit so the countersign run page exists first; same-ticket re-summons
403
- // resume the prior run on the typed key (unused mint is abandoned). The test
404
- // seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
405
- // hook failures stay on the parent call-local budget, exhausted nested
406
- // station children still skip parent auto-resume (#840 父子不层叠).
407
- let typedTicket: number | undefined;
408
- let typedCourtTicketNumbers: readonly number[] | undefined;
409
- let identityDiaristRan = false;
436
+ // One owner spans prepare, lookup/load/resume or new-run materialization.
437
+ // It preserves both causes when its final cleanup also fails.
438
+ try {
439
+ return await withPreparedAttachments(
440
+ parsed.attachmentPaths,
441
+ async (preparedAttachments) => {
442
+ const materializeAdmission = async (): Promise<void> => {
443
+ await materializeCountersignInvocation(admitted, {
444
+ home: env.home,
445
+ principalAuthority: env.principalAuthority,
446
+ preparedAttachments,
447
+ ...(env.model === undefined ? {} : { model: env.model }),
448
+ });
449
+ };
410
450
 
411
- if (env.runCourtDiaristStation === undefined) {
412
- const outcome = await invokeCourtDiarist(
413
- {
414
- instruction: parsed.instruction,
415
- projectRoot: admitted.projectRoot,
416
- failureLabel: "unbound summons",
417
- correlationId: admitted.runId,
418
- },
419
- env,
420
- io,
421
- );
422
- identityDiaristRan = true;
451
+ // #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
452
+ // (never mechanical matching of summons text). Resolve that typed key before
453
+ // materializing a run: same-ticket re-summons write only to the retained run.
454
+ // Controlled failures materialize below so they still have a durable page. The test
455
+ // seam `runCourtDiaristStation` defers identity to beforeDispatch; generic
456
+ // hook failures stay on the parent call-local budget, exhausted nested
457
+ // station children still skip parent auto-resume (#840 父子不层叠).
458
+ let typedTicket: number | undefined;
459
+ let typedCourtTicketNumbers: readonly number[] | undefined;
460
+ let identityDiaristRan = false;
423
461
 
424
- if (outcome.identity.kind === "escalate") {
425
- // 御批: 识别不了就上抛 — settle on the admitted countersign run.
426
- return await presentControlledFailure(
427
- admitted,
428
- {
429
- timedOut: false,
430
- code: null,
431
- stderr: "",
432
- thrown: new Error(
433
- "court diarist station escalated (cannot identify court target)",
434
- ),
435
- },
436
- countersignAdapters(),
437
- env.principalAuthority,
438
- io,
439
- );
440
- }
462
+ if (env.runCourtDiaristStation === undefined) {
463
+ let outcome: CourtDiaristInvocationResult;
464
+ try {
465
+ // The identity child cannot name the newly minted countersign id as parent:
466
+ // same-ticket lookup may intentionally never materialize that run. A selected
467
+ // retained run receives its normally correlated refresh child during resume.
468
+ outcome = await invokeCourtDiarist(
469
+ {
470
+ instruction: parsed.instruction,
471
+ projectRoot: admitted.projectRoot,
472
+ failureLabel: "unbound summons",
473
+ },
474
+ env,
475
+ io,
476
+ );
477
+ } catch (error) {
478
+ await materializeAdmission();
479
+ await markRunAdmitted(admitted, env.principalAuthority);
480
+ return await presentControlledFailure(
481
+ admitted,
482
+ { timedOut: false, code: null, stderr: "", thrown: error },
483
+ countersignAdapters(),
484
+ env.principalAuthority,
485
+ io,
486
+ );
487
+ }
488
+ identityDiaristRan = true;
441
489
 
442
- // Typed failure terminal (verification / infra / non-zero without escalate)
443
- // is not 真无票 — settle controlled failure on the admitted run (失败诚实).
444
- // Only a true missing lawful typed terminal keeps the r5 unbound-continue.
445
- if (outcome.failedWithoutEscalate !== undefined) {
446
- return await presentControlledFailure(
447
- admitted,
448
- {
449
- timedOut: false,
450
- code: null,
451
- stderr: "",
452
- thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
453
- },
454
- countersignAdapters(),
455
- env.principalAuthority,
456
- io,
457
- );
458
- }
490
+ if (outcome.identity.kind === "escalate") {
491
+ // 御批: 识别不了就上抛 — materialize and settle this countersign run.
492
+ await materializeAdmission();
493
+ await markRunAdmitted(admitted, env.principalAuthority);
494
+ return await presentControlledFailure(
495
+ admitted,
496
+ {
497
+ timedOut: false,
498
+ code: null,
499
+ stderr: "",
500
+ thrown: new Error(
501
+ "court diarist station escalated (cannot identify court target)",
502
+ ),
503
+ },
504
+ countersignAdapters(),
505
+ env.principalAuthority,
506
+ io,
507
+ );
508
+ }
459
509
 
460
- // Missing lawful 起居郎 terminal is not countersign body failure: leave
461
- // unbound and continue (true-unbound face). Escalate / typed failure above;
462
- // bound refresh still fails honest via runCountersignCourtDiaristStation.
463
- if (outcome.identity.kind === "ticket") {
464
- const assertedTicket = outcome.identity.ticketNumber;
465
- typedTicket = assertedTicket;
466
- // #871: identity may hand a co-review set; absent field defaults to [main].
467
- typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
468
- const summons: SameTicketSummonsMaterials = {
469
- instruction: parsed.instruction,
470
- instructionEmpty: parsed.instruction.trim() === "",
471
- attachmentPaths: parsed.attachmentPaths,
472
- };
473
- const resumed = await tryResumeSameTicketSeatRun({
474
- home: env.home,
475
- projectRoot: admitted.projectRoot,
476
- role: "countersign",
477
- ticketNumber: typedTicket,
478
- freshSummons: env.freshSummons,
479
- summons,
480
- resume: async (runId, materials) => {
481
- // Resume selected: file the provisional run under the asserted ticket
482
- // before abandoning it, so typed identity never leaves an unbound row.
483
- await bindAdmittedTicketNumber(admitted, assertedTicket);
484
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
485
- await markRunTerminal(admitted.runDirectory);
486
- // Identity 起居郎 asserted unbound (no issue face). Resume still runs
487
- // the bound refresh station under the typed key (ADR 0075: 每次过庭都跑是调用者用法).
488
- // #871: hand the identity set so resume can whole-replace the run fact
489
- // when this summons produced a new typed set (never union).
490
- return await runPublicCountersignResume(
491
- {
492
- runId,
493
- ...(materials === undefined ? {} : { summons: materials }),
494
- },
495
- {
496
- ...env,
497
- ...(typedCourtTicketNumbers === undefined
498
- ? {}
499
- : { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
500
- },
501
- io,
502
- );
503
- },
504
- });
505
- if (resumed !== undefined) {
506
- return resumed;
507
- }
508
- }
509
- }
510
+ // Typed failure terminal (verification / infra / non-zero without escalate)
511
+ // is not 真无票 — settle controlled failure on the admitted run (失败诚实).
512
+ // Only a true missing lawful typed terminal keeps the r5 unbound-continue.
513
+ if (outcome.failedWithoutEscalate !== undefined) {
514
+ await materializeAdmission();
515
+ await markRunAdmitted(admitted, env.principalAuthority);
516
+ return await presentControlledFailure(
517
+ admitted,
518
+ {
519
+ timedOut: false,
520
+ code: null,
521
+ stderr: "",
522
+ thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
523
+ },
524
+ countersignAdapters(),
525
+ env.principalAuthority,
526
+ io,
527
+ );
528
+ }
510
529
 
511
- if (identityDiaristRan && typedTicket !== undefined) {
512
- try {
513
- await bindAdmittedTicketNumber(admitted, typedTicket);
514
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
515
- // First court: explicit set wins; omitted field → single-ticket face [main].
516
- // Set persistence is outside beforeDispatch — route write failures into the
517
- // same controlled-failure settlement as station children (#871 B7).
518
- await bindCourtTicketNumbersOnAdmitted(
519
- admitted,
520
- typedCourtTicketNumbers ?? [typedTicket],
521
- );
522
- } catch (error) {
523
- return await presentControlledFailure(
524
- admitted,
525
- {
526
- timedOut: false,
527
- code: null,
528
- stderr: "",
529
- thrown: error,
530
- },
531
- countersignAdapters(),
532
- env.principalAuthority,
533
- io,
534
- );
535
- }
536
- }
530
+ // Missing lawful 起居郎 terminal is not countersign body failure: leave
531
+ // unbound and continue (true-unbound face). Escalate / typed failure above;
532
+ // bound refresh still fails honest via runCountersignCourtDiaristStation.
533
+ if (outcome.identity.kind === "ticket") {
534
+ const assertedTicket = outcome.identity.ticketNumber;
535
+ typedTicket = assertedTicket;
536
+ // #871: identity may hand a co-review set; absent field defaults to [main].
537
+ typedCourtTicketNumbers = outcome.identity.courtTicketNumbers;
538
+ const summons: SameTicketSummonsMaterials = {
539
+ instruction: parsed.instruction,
540
+ instructionEmpty: parsed.instruction.trim() === "",
541
+ };
542
+ const resumed = await tryResumeSameTicketSeatRun({
543
+ home: env.home,
544
+ projectRoot: admitted.projectRoot,
545
+ role: "countersign",
546
+ ticketNumber: typedTicket,
547
+ freshSummons: env.freshSummons,
548
+ summons,
549
+ resume: async (runId, materials) => {
550
+ // Consume the same pre-identity snapshot into the retained run; never
551
+ // reopen caller paths after identity has run.
552
+ const retained = await loadResumableCountersignRun(
553
+ env.home,
554
+ runId,
555
+ env.principalAuthority,
556
+ );
557
+ if (retained.admitted === undefined) {
558
+ throw new Error(
559
+ `retained countersign run disappeared before resume: ${runId}`,
560
+ );
561
+ }
562
+ const frozenPaths = (
563
+ await freezePreparedAttachmentsIntoRun(
564
+ preparedAttachments,
565
+ retained.admitted.runDirectory,
566
+ `s-${Date.now().toString(36)}`,
567
+ )
568
+ ).map((attachment) => attachment.frozenPath);
569
+ const preparedMaterials: SameTicketSummonsMaterials = {
570
+ ...(materials ?? {}),
571
+ ...(frozenPaths.length === 0
572
+ ? {}
573
+ : { attachmentPaths: frozenPaths }),
574
+ };
575
+ // Identity 起居郎 asserted unbound (no issue face). Resume still runs
576
+ // the bound refresh station under the typed key (ADR 0075: 每次过庭都跑是调用者用法).
577
+ // #871: hand the identity set so resume can whole-replace the run fact
578
+ // when this summons produced a new typed set (never union).
579
+ return await runPublicCountersignResume(
580
+ {
581
+ runId,
582
+ summons: preparedMaterials,
583
+ },
584
+ {
585
+ ...env,
586
+ ...(typedCourtTicketNumbers === undefined
587
+ ? {}
588
+ : { pendingCourtTicketNumbers: typedCourtTicketNumbers }),
589
+ },
590
+ io,
591
+ );
592
+ },
593
+ });
594
+ if (resumed !== undefined) {
595
+ return resumed;
596
+ }
597
+ }
598
+ }
537
599
 
538
- const turnProjection: RoleTurnRequestProjectionOptions = {
539
- packageRoot: env.packageRoot,
540
- home: env.home,
541
- agentDir: env.agentDir,
542
- ...(env.model === undefined ? {} : { model: env.model }),
543
- ...pickEngineAxis(env),
544
- ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
545
- ...(env.correlationId === undefined || env.correlationId.trim() === ""
546
- ? {}
547
- : { correlationId: env.correlationId }),
548
- continuation: {
549
- kind: "initial",
550
- prompt: buildCountersignTransportPrompt(
551
- admitted,
552
- engineSessionMaterialFromOptions({
553
- ...pickEngineAxis(env),
554
- packageRoot: env.packageRoot,
555
- }),
556
- ),
557
- },
558
- };
559
- // Mutable shell: ticket bind re-projects activation before executeTurn.
560
- const turnRequest = buildCountersignTurnRequest(admitted, turnProjection);
600
+ // No prior run was selected. Materialize this invocation now; true-unbound,
601
+ // first-ticket and deferred test-seam paths all retain their own durable page.
602
+ await materializeAdmission();
603
+ await markRunAdmitted(admitted, env.principalAuthority);
561
604
 
562
- const result = await runPostAdmissionOneShot({
563
- admitted,
564
- env,
565
- io,
566
- request: turnRequest,
567
- adapters: countersignAdapters({
568
- beforeDispatch: async (admittedSeat, lease) => {
569
- // Dossier pointer delivery rides post-admission after this hook (#709).
570
- if (!identityDiaristRan) {
571
- // Test seam (or any deferred identity): station owns assert + bind.
572
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
573
- } else if (typedTicket !== undefined) {
574
- await runCountersignCourtDiaristStation(admittedSeat, env, io);
605
+ if (identityDiaristRan && typedTicket !== undefined) {
606
+ try {
607
+ await bindAdmittedTicketNumber(admitted, typedTicket);
608
+ await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
609
+ // First court: explicit set wins; omitted field → single-ticket face [main].
610
+ // Set persistence is outside beforeDispatch — route write failures into the
611
+ // same controlled-failure settlement as station children (#871 B7).
612
+ await bindCourtTicketNumbersOnAdmitted(
613
+ admitted,
614
+ typedCourtTicketNumbers ?? [typedTicket],
615
+ );
616
+ } catch (error) {
617
+ return await presentControlledFailure(
618
+ admitted,
619
+ {
620
+ timedOut: false,
621
+ code: null,
622
+ stderr: "",
623
+ thrown: error,
624
+ },
625
+ countersignAdapters(),
626
+ env.principalAuthority,
627
+ io,
628
+ );
629
+ }
575
630
  }
576
- await relocateAdmittedRunToTicket(
577
- admittedSeat,
578
- env.principalAuthority,
579
- lease,
580
- );
581
- Object.assign(
582
- turnRequest,
583
- buildCountersignTurnRequest(admittedSeat, turnProjection),
631
+
632
+ const turnProjection: RoleTurnRequestProjectionOptions = {
633
+ packageRoot: env.packageRoot,
634
+ home: env.home,
635
+ agentDir: env.agentDir,
636
+ ...(env.model === undefined ? {} : { model: env.model }),
637
+ ...pickEngineAxis(env),
638
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
639
+ ...(env.correlationId === undefined || env.correlationId.trim() === ""
640
+ ? {}
641
+ : { correlationId: env.correlationId }),
642
+ continuation: {
643
+ kind: "initial",
644
+ prompt: buildCountersignTransportPrompt(
645
+ admitted,
646
+ engineSessionMaterialFromOptions({
647
+ ...pickEngineAxis(env),
648
+ packageRoot: env.packageRoot,
649
+ }),
650
+ ),
651
+ },
652
+ };
653
+ // Mutable shell: ticket bind re-projects activation before executeTurn.
654
+ const turnRequest = buildCountersignTurnRequest(
655
+ admitted,
656
+ turnProjection,
584
657
  );
658
+
659
+ const result = await runPostAdmissionOneShot({
660
+ admitted,
661
+ env,
662
+ io,
663
+ request: turnRequest,
664
+ adapters: countersignAdapters({
665
+ beforeDispatch: async (admittedSeat, lease) => {
666
+ // Dossier pointer delivery rides post-admission after this hook (#709).
667
+ if (!identityDiaristRan) {
668
+ // Test seam (or any deferred identity): station owns assert + bind.
669
+ await runCountersignCourtDiaristStation(admittedSeat, env, io);
670
+ } else if (typedTicket !== undefined) {
671
+ await runCountersignCourtDiaristStation(admittedSeat, env, io);
672
+ }
673
+ await relocateAdmittedRunToTicket(
674
+ admittedSeat,
675
+ env.principalAuthority,
676
+ lease,
677
+ );
678
+ Object.assign(
679
+ turnRequest,
680
+ buildCountersignTurnRequest(admittedSeat, turnProjection),
681
+ );
682
+ },
683
+ }),
684
+ ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
685
+ });
686
+ await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
687
+ return result;
585
688
  },
586
- }),
587
- ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
588
- });
589
- await relocateAdmittedRunToTicket(admitted, env.principalAuthority);
590
- return result;
689
+ );
690
+ } catch (error) {
691
+ if (error instanceof CliUsageError) {
692
+ presentStructuralRejection(error, io);
693
+ return { exitCode: 2 };
694
+ }
695
+ throw error;
696
+ }
591
697
  }
592
698
 
593
699
  function countersignAdapters(options?: {
@@ -658,7 +764,9 @@ export async function runPublicCountersignResume(
658
764
  // #871 B7: durable set damage already identified at load — settle as
659
765
  // station-child exhausted → presentControlledFailure (not structural exit 2).
660
766
  if (admitted.courtTicketNumbersDamage !== undefined) {
661
- throw new StationChildExhaustedError(admitted.courtTicketNumbersDamage);
767
+ throw new StationChildExhaustedError(
768
+ admitted.courtTicketNumbersDamage,
769
+ );
662
770
  }
663
771
  // #871: same-ticket re-summons may hand a fresh typed set from identity.
664
772
  // Whole-set replace onto this run fact; no new set → keep stored set.