@akagilnc/pi-workflow-roles 0.1.4239 → 0.1.4259

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.
@@ -6,7 +6,7 @@
6
6
  * Role runners supply only turn request projection and narrow settlement adapters.
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
- import { readFile, writeFile } from "node:fs/promises";
9
+ import { writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
11
  import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
@@ -39,6 +39,7 @@ function appendContinuationSection(continuation, section) {
39
39
  function isStationChildOfficerDialogue(role, env) {
40
40
  return env.stationChild === true && isOfficerReviewSeat(role);
41
41
  }
42
+ import { mintEngineDetourInvocationScope, readInvocationSelectedHost, withEngineDetourInvocationScope, } from "../engine-detour-usage.js";
42
43
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
43
44
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
44
45
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
@@ -106,18 +107,6 @@ async function recordBestEffortPostDispatchDiagnostic(admitted, env, diagnostic,
106
107
  }
107
108
  }
108
109
  }
109
- /** Previous main-session host recorded on invocation.json, if any. */
110
- async function readInvocationHost(runDirectory) {
111
- try {
112
- const raw = JSON.parse(await readFile(join(runDirectory, "invocation.json"), "utf8"));
113
- return typeof raw.host === "string" && raw.host.trim() !== "" ? raw.host : undefined;
114
- }
115
- catch (error) {
116
- if (error.code === "ENOENT")
117
- return undefined;
118
- throw error;
119
- }
120
- }
121
110
  /**
122
111
  * Factory-seat resume method-material face (#833): one authority for
123
112
  * load → adapters / controlled-failure short-circuit. Seats only declare
@@ -196,9 +185,15 @@ export async function presentControlledFailure(admitted, failureInput, adapters,
196
185
  if (persistRunState && !failureInput.skipRunStateWrite) {
197
186
  await persistReturnedRunState(admitted, authority);
198
187
  }
199
- const terminal = await attachRecordedSubmissions(admitted, await settleFailureTerminalResult(admitted, failure, authority, resumable
200
- ? { resume: { command: renderResumeCommand(admitted.runId) } }
201
- : {}));
188
+ const terminal = await attachRecordedSubmissions(admitted, await settleFailureTerminalResult(admitted, failure, authority, {
189
+ ...(resumable
190
+ ? { resume: { command: renderResumeCommand(admitted.runId) } }
191
+ : {}),
192
+ ...(failureInput.invocationScopeId === undefined ||
193
+ failureInput.invocationScopeId.length === 0
194
+ ? {}
195
+ : { invocationScopeId: failureInput.invocationScopeId }),
196
+ }));
202
197
  presentFailureTerminal(terminal, io);
203
198
  return {
204
199
  exitCode: exitCodeForTerminalOutcome(terminal.roleOutcome),
@@ -282,12 +277,12 @@ export async function dispatchPostAdmissionTurn(input) {
282
277
  return result;
283
278
  }
284
279
  return {
285
- ...(await presentControlledFailure(admitted, {
280
+ ...(await presentControlledFailure(admitted, withEngineDetourInvocationScope({
286
281
  timedOut: false,
287
282
  code: null,
288
283
  stderr: "",
289
284
  thrown: error,
290
- }, adapters, env.principalAuthority, io, persistRunState)),
285
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
291
286
  ...(result.turnDispatched === true ? { turnDispatched: true } : {}),
292
287
  ...(result.skipAutoResume === true ? { skipAutoResume: true } : {}),
293
288
  ...deferredPersist,
@@ -298,7 +293,7 @@ export async function dispatchPostAdmissionTurn(input) {
298
293
  const missingCredential = missingCredentialPreDispatchFailure(env.model, env.credentials);
299
294
  if (missingCredential !== undefined) {
300
295
  return {
301
- ...(await presentControlledFailure(admitted, missingCredential, adapters, env.principalAuthority, io, persistRunState)),
296
+ ...(await presentControlledFailure(admitted, withEngineDetourInvocationScope(missingCredential, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
302
297
  ...deferredPersist,
303
298
  };
304
299
  }
@@ -312,7 +307,7 @@ export async function dispatchPostAdmissionTurn(input) {
312
307
  : env.principalAuthority.decode(admitted.principal);
313
308
  let hostTransition;
314
309
  try {
315
- previousHost = await readInvocationHost(admitted.runDirectory);
310
+ previousHost = readInvocationSelectedHost(admitted.runDirectory);
316
311
  hostTransition =
317
312
  previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
318
313
  ? await projectHostTransitionPriorNative({
@@ -325,12 +320,12 @@ export async function dispatchPostAdmissionTurn(input) {
325
320
  catch (error) {
326
321
  // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
327
322
  return {
328
- ...(await presentControlledFailure(admitted, {
323
+ ...(await presentControlledFailure(admitted, withEngineDetourInvocationScope({
329
324
  timedOut: false,
330
325
  code: null,
331
326
  stderr: "",
332
327
  thrown: error,
333
- }, adapters, env.principalAuthority, io, persistRunState)),
328
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState)),
334
329
  ...deferredPersist,
335
330
  };
336
331
  }
@@ -342,7 +337,7 @@ export async function dispatchPostAdmissionTurn(input) {
342
337
  // executeTurn — not merely past beforeDispatch (#840 r9 判词 class 2). Any
343
338
  // pre-turn retry (beforeDispatch, dossier projection, continuation
344
339
  // assembly) must still see the prior invocation host on its next attempt;
345
- // committing env.host earlier would make readInvocationHost read back the
340
+ // committing env.host earlier would make readInvocationSelectedHost read back the
346
341
  // new host on the retry and silently drop hostTransition.
347
342
  // Failures settle the run (presentControlledFailure); they must not leave it
348
343
  // permanently running. Call-local auto-resume retries this hook until it
@@ -353,12 +348,12 @@ export async function dispatchPostAdmissionTurn(input) {
353
348
  await adapters.beforeDispatch(admitted, lease);
354
349
  }
355
350
  catch (error) {
356
- const settled = (await presentControlledFailure(admitted, {
351
+ const settled = (await presentControlledFailure(admitted, withEngineDetourInvocationScope({
357
352
  timedOut: false,
358
353
  code: null,
359
354
  stderr: "",
360
355
  thrown: error,
361
- }, adapters, env.principalAuthority, io, persistRunState));
356
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState));
362
357
  if (error instanceof StationChildExhaustedError) {
363
358
  return { ...settled, skipAutoResume: true, ...deferredPersist };
364
359
  }
@@ -378,6 +373,11 @@ export async function dispatchPostAdmissionTurn(input) {
378
373
  if (hostTransition !== undefined) {
379
374
  turnRequest = { ...turnRequest, hostTransition };
380
375
  }
376
+ // Selected host axis rides the shared Host envelope for in-turn tools
377
+ // (detour usage ledger) — never a pre-spawn invocation.json reread.
378
+ if (typeof liveHost === "string" && liveHost.trim() !== "") {
379
+ turnRequest = { ...turnRequest, host: liveHost.trim() };
380
+ }
381
381
  if (isStationChildOfficerDialogue(admitted.role, env)) {
382
382
  // 0081 non-body face: freeze pointer section under run/attachments/.
383
383
  // Peer dialogue continuation.prompt stays parent payload only — the seat
@@ -408,17 +408,20 @@ export async function dispatchPostAdmissionTurn(input) {
408
408
  // immediately before the turn actually starts, after every retryable
409
409
  // pre-turn step above has succeeded on this attempt (#840 r9 判词 class 2).
410
410
  await markRunRunning(admitted.runDirectory, env.model, effectiveEngine, env.host, env.engineModel);
411
+ // #537 invocation scope is bound once at the public-entry boundary
412
+ // (runPostAdmissionResumable / ManualResume / station-child), not here:
413
+ // auto-resume re-enters this dispatch and must reuse the same scope.
411
414
  let result;
412
415
  try {
413
416
  result = await env.roleTurnHost.executeTurn(turnRequest);
414
417
  }
415
418
  catch (error) {
416
- const settled = await settleAfterTurnStarted(admitted, {
419
+ const settled = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
417
420
  timedOut: false,
418
421
  code: null,
419
422
  stderr: "",
420
423
  thrown: error,
421
- }, adapters, env.principalAuthority, io, persistRunState);
424
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
422
425
  return await finishAfterTurn({ ...settled, turnDispatched: true, ...deferredPersist });
423
426
  }
424
427
  // `result.stderr` stays live in memory for the real classification below
@@ -462,9 +465,18 @@ export async function dispatchPostAdmissionTurn(input) {
462
465
  // wrong, becomes the reported failure itself (#836).
463
466
  await recordBestEffortPostDispatchDiagnostic(admitted, env, `stderr.log write failed (best-effort continue): ${describeErrorIdentity(error)}`, io);
464
467
  }
465
- const courtScope = request.courtAttemptId === undefined || request.courtAttemptId.length === 0
468
+ const courtScope = (request.courtAttemptId === undefined || request.courtAttemptId.length === 0) &&
469
+ (request.invocationScopeId === undefined || request.invocationScopeId.length === 0)
466
470
  ? undefined
467
- : { courtAttemptId: request.courtAttemptId };
471
+ : {
472
+ ...(request.courtAttemptId === undefined || request.courtAttemptId.length === 0
473
+ ? {}
474
+ : { courtAttemptId: request.courtAttemptId }),
475
+ ...(request.invocationScopeId === undefined ||
476
+ request.invocationScopeId.length === 0
477
+ ? {}
478
+ : { invocationScopeId: request.invocationScopeId }),
479
+ };
468
480
  // Single complete boundary (#840 r9 判词 class 1): resolving the
469
481
  // host/runner failure facts, trySettle, its shouldPresent gate, and the
470
482
  // accepted-settlement cleanup all settle through this one catch — a
@@ -580,12 +592,12 @@ export async function dispatchPostAdmissionTurn(input) {
580
592
  catch (error) {
581
593
  // Settle (or its shouldPresent gate, or the failure-fact resolution
582
594
  // above) throw is a real failure fact — never swallow into undefined.
583
- const settledFailure = await settleAfterTurnStarted(admitted, {
595
+ const settledFailure = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
584
596
  timedOut: false,
585
597
  code: result.code,
586
598
  stderr: result.stderr,
587
599
  thrown: error,
588
- }, adapters, env.principalAuthority, io, persistRunState);
600
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
589
601
  return await finishAfterTurn({ ...settledFailure, turnDispatched: true, ...deferredPersist });
590
602
  }
591
603
  if (settledOutcome !== undefined) {
@@ -604,13 +616,13 @@ export async function dispatchPostAdmissionTurn(input) {
604
616
  // at all. skipRunStateWrite: the write that just threw is the same
605
617
  // write presentControlledFailure would otherwise retry — don't
606
618
  // call a known-failing operation twice.
607
- const failed = await settleAfterTurnStarted(admitted, {
619
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
608
620
  timedOut: false,
609
621
  code: null,
610
622
  stderr: "",
611
623
  thrown: error,
612
624
  skipRunStateWrite: true,
613
- }, adapters, env.principalAuthority, io, persistRunState);
625
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
614
626
  return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
615
627
  }
616
628
  }
@@ -624,7 +636,7 @@ export async function dispatchPostAdmissionTurn(input) {
624
636
  const stderrLogWriteDetails = stderrLogWriteFailure === undefined
625
637
  ? undefined
626
638
  : { stderrLogWriteFailure: describeCaughtError(stderrLogWriteFailure) };
627
- const failed = await settleAfterTurnStarted(admitted, {
639
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
628
640
  timedOut: result.timedOut,
629
641
  code: result.code,
630
642
  stderr: result.stderr,
@@ -642,22 +654,22 @@ export async function dispatchPostAdmissionTurn(input) {
642
654
  },
643
655
  }
644
656
  : { knownDetails: stderrLogWriteDetails }),
645
- }, adapters, env.principalAuthority, io, persistRunState);
657
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
646
658
  return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
647
659
  }
648
660
  // Nothing else was wrong, but the durable stderr mirror itself failed to
649
661
  // write — that is the real (infrastructure) problem in this case, not a
650
662
  // lawful absence of a receipt. Honest and loud, not silently no_receipt.
651
663
  if (stderrLogWriteFailure !== undefined) {
652
- const failed = await settleAfterTurnStarted(admitted, {
664
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
653
665
  timedOut: false,
654
666
  code: result.code,
655
667
  stderr: result.stderr,
656
668
  thrown: stderrLogWriteFailure,
657
- }, adapters, env.principalAuthority, io, persistRunState);
669
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
658
670
  return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
659
671
  }
660
- const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority), courtScope);
672
+ const noReceipt = await attachRecordedSubmissions(admitted, await settleHostEndedNoReceipt(admitted, env.principalAuthority, courtScope), courtScope);
661
673
  if (persistRunState) {
662
674
  try {
663
675
  await persistReturnedRunState(admitted, env.principalAuthority, { lawful: true });
@@ -668,13 +680,13 @@ export async function dispatchPostAdmissionTurn(input) {
668
680
  // the real failure surfaces loudly instead of escaping uncaught to
669
681
  // auto-resume's dispatch-retry path with no recorded submissions.
670
682
  // skipRunStateWrite: don't retry the write that just threw.
671
- const failed = await settleAfterTurnStarted(admitted, {
683
+ const failed = await settleAfterTurnStarted(admitted, withEngineDetourInvocationScope({
672
684
  timedOut: false,
673
685
  code: null,
674
686
  stderr: "",
675
687
  thrown: error,
676
688
  skipRunStateWrite: true,
677
- }, adapters, env.principalAuthority, io, persistRunState);
689
+ }, request.invocationScopeId), adapters, env.principalAuthority, io, persistRunState);
678
690
  return await finishAfterTurn({ ...failed, turnDispatched: true, ...deferredPersist });
679
691
  }
680
692
  }
@@ -941,6 +953,12 @@ export async function runPostAdmissionSeatResume(input) {
941
953
  if (input.env.stationChild === true) {
942
954
  let firstTurn;
943
955
  const stationAdapters = withOnceSuccessfulBeforeDispatch(adapters);
956
+ // One public call → one detour scope across in-place auto-resume dispatches.
957
+ const invocationScopeId = mintEngineDetourInvocationScope({
958
+ ...(input.effectiveEngine === undefined
959
+ ? {}
960
+ : { effectiveEngine: input.effectiveEngine }),
961
+ });
944
962
  return await runWithAutoResumeLoop({
945
963
  admitted: loaded.admitted,
946
964
  principalAuthority: input.env.principalAuthority,
@@ -974,7 +992,7 @@ export async function runPostAdmissionSeatResume(input) {
974
992
  },
975
993
  };
976
994
  }
977
- const turnRequest = await buildRequestAfterLease();
995
+ const turnRequest = withEngineDetourInvocationScope(await buildRequestAfterLease(), invocationScopeId);
978
996
  firstTurn = turnRequest;
979
997
  return turnRequest;
980
998
  },
@@ -1058,6 +1076,12 @@ export async function runPostAdmissionOneShot(input) {
1058
1076
  export async function runPostAdmissionResumable(input) {
1059
1077
  const { admitted, env, io, buildInitialRequest, buildResumeRequest, effectiveEngine } = input;
1060
1078
  const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
1079
+ // One public call → one detour scope across in-place auto-resume dispatches.
1080
+ const invocationScopeId = mintEngineDetourInvocationScope({
1081
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1082
+ });
1083
+ const buildScopedInitial = () => withEngineDetourInvocationScope(buildInitialRequest(), invocationScopeId);
1084
+ const buildScopedResume = () => withEngineDetourInvocationScope(buildResumeRequest(), invocationScopeId);
1061
1085
  return runWithAutoResumeLoop({
1062
1086
  admitted,
1063
1087
  principalAuthority: env.principalAuthority,
@@ -1065,8 +1089,8 @@ export async function runPostAdmissionResumable(input) {
1065
1089
  io,
1066
1090
  sessionAppender: env.sessionAppender,
1067
1091
  autoResumeLimit: env.autoResumeLimit,
1068
- buildInitialPayload: buildInitialRequest,
1069
- buildResumePayload: buildResumeRequest,
1092
+ buildInitialPayload: buildScopedInitial,
1093
+ buildResumePayload: buildScopedResume,
1070
1094
  dispatch: async (request, lease, _isFirst, attemptIo) => {
1071
1095
  const result = await dispatchPostAdmissionTurn({
1072
1096
  admitted,
@@ -1124,6 +1148,10 @@ export async function runPostAdmissionManualResume(input) {
1124
1148
  }
1125
1149
  throw error;
1126
1150
  }
1151
+ // Explicit public resume is a new counting unit — mint once for this call.
1152
+ const invocationScopeId = mintEngineDetourInvocationScope({
1153
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1154
+ });
1127
1155
  const result = await dispatchAfterWriterLease({
1128
1156
  lease,
1129
1157
  build: async () => {
@@ -1133,6 +1161,7 @@ export async function runPostAdmissionManualResume(input) {
1133
1161
  }
1134
1162
  request = await buildRequestAfterLease();
1135
1163
  }
1164
+ request = withEngineDetourInvocationScope(request, invocationScopeId);
1136
1165
  return request;
1137
1166
  },
1138
1167
  dispatch: (turnRequest) => dispatchPostAdmissionTurn({
@@ -21,6 +21,7 @@ import { AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE, AUDITOR_PARENT_ATTEMPT_BINDING_E
21
21
  // interval binding on historical session bytes). Provider-stop retain authority is Sitian.
22
22
  import { COLLECTOR_BIND_TARGET_TOOL, COLLECTOR_OBSERVE_TOOL, COLLECTOR_READ_TOOL, COLLECTOR_REQUEST_TOOL, COLLECTOR_WAIT_TOOL, } from "../collector-ledger.js";
23
23
  import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.js";
24
+ import { projectEngineDetourToolUsageForPublicTerminal, readEngineDetourToolUsage, readInvocationEngineMounted, runDirectoryFromSessionDirectory, sessionFileFromSessionDirectory, withEngineDetourToolUsageFact, } from "../engine-detour-usage.js";
24
25
  import { JUDGE_OUTPUT_TOOL_NAME, } from "../package-contracts/judge-output.js";
25
26
  import { COLLECTOR_OUTPUT_TOOL, } from "../package-contracts/collector-output.js";
26
27
  import { CODER_OUTPUT_TOOL_NAME, FIXER_OUTPUT_TOOL_NAME, } from "../package-contracts/worker-output.js";
@@ -127,7 +128,7 @@ export async function attachRecordedSubmissions(admitted, terminal, scope) {
127
128
  * Host ended with no recorded submission — lawful no_receipt (exit 0).
128
129
  * Does not invent an output failure for an empty ledger.
129
130
  */
130
- export async function settleHostEndedNoReceipt(admitted, authority) {
131
+ export async function settleHostEndedNoReceipt(admitted, authority, scope) {
131
132
  const facts = noReceiptLifecycleFacts({
132
133
  terminalToolCalled: false,
133
134
  rejectedReceipts: [],
@@ -147,7 +148,7 @@ export async function settleHostEndedNoReceipt(admitted, authority) {
147
148
  navigator: await extractNavigatorFactFromAdmittedSession(coordinates.sessionFile),
148
149
  artifacts: [],
149
150
  runId: admitted.runId,
150
- }, coordinates.sessionDirectory);
151
+ }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
151
152
  }
152
153
  async function closedLedgerOutcome(admitted, role, scope) {
153
154
  return sealedLedgerOutcome(admitted, role, scope);
@@ -1589,6 +1590,57 @@ export async function extractGateFactFromSessionDirectory(sessionDirectory, opti
1589
1590
  * or swallowed); callers that already hold a controlled failure still surface the
1590
1591
  * JSONL/session cause rather than pretend the gate was absent.
1591
1592
  */
1593
+ /**
1594
+ * #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
1595
+ * Absent when engine is not mounted; callCount 0 when mounted with zero calls.
1596
+ * Never mutates role payloads (ADR 0003 / 0042 / 0052).
1597
+ * Scope is the public-invocation id bound once per ak-role call — never
1598
+ * courtAttemptId and never Pi session.jsonl toolResult join. Session damage
1599
+ * therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
1600
+ */
1601
+ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext = {}) {
1602
+ const runDirectory = typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
1603
+ ? gateContext.runDirectory
1604
+ : runDirectoryFromSessionDirectory(sessionDirectory);
1605
+ const engineMounted = await readInvocationEngineMounted(runDirectory);
1606
+ if (!engineMounted)
1607
+ return base;
1608
+ // Public-invocation scope from the shared Host envelope (settlement scope), never
1609
+ // courtAttemptId and never a detour sidecar file.
1610
+ const invocationScopeId = typeof gateContext.invocationScopeId === "string" &&
1611
+ gateContext.invocationScopeId.length > 0
1612
+ ? gateContext.invocationScopeId
1613
+ : undefined;
1614
+ const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
1615
+ const usage = await readEngineDetourToolUsage({
1616
+ sessionParent: sessionFile,
1617
+ engineMounted: true,
1618
+ ...(invocationScopeId === undefined ? {} : { invocationScopeId }),
1619
+ cwd: runDirectory,
1620
+ });
1621
+ const projected = usage === undefined
1622
+ ? undefined
1623
+ : projectEngineDetourToolUsageForPublicTerminal(usage, {
1624
+ // Resumable Terminal: run ID only in resume.command — relative openable path.
1625
+ discloseRecordFile: base.resume === undefined,
1626
+ });
1627
+ return {
1628
+ ...base,
1629
+ roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, projected),
1630
+ };
1631
+ }
1632
+ /** Gate + detour projection context from admitted run + settlement scope. */
1633
+ function detourGateContext(admitted, scope) {
1634
+ return {
1635
+ runDirectory: admitted.runDirectory,
1636
+ ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
1637
+ ? {}
1638
+ : { courtAttemptId: scope.courtAttemptId }),
1639
+ ...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
1640
+ ? {}
1641
+ : { invocationScopeId: scope.invocationScopeId }),
1642
+ };
1643
+ }
1592
1644
  async function withOptionalGateProjection(base, sessionDirectory, gateContext = {}) {
1593
1645
  // A gate transport failure is already represented by typed evidence and has no
1594
1646
  // accepted gate cycle to project. Re-reading that rejected receipt as an
@@ -1596,15 +1648,19 @@ async function withOptionalGateProjection(base, sessionDirectory, gateContext =
1596
1648
  const secondaryEvidence = base.roleOutcome.kind === "failure"
1597
1649
  ? base.roleOutcome.decisiveFacts.secondaryEvidence
1598
1650
  : undefined;
1599
- if (isRecord(secondaryEvidence)
1651
+ const skipGate = isRecord(secondaryEvidence)
1600
1652
  && secondaryEvidence.kind === "role_infrastructure_failure"
1601
1653
  && (secondaryEvidence.stage === "gatekeeper"
1602
1654
  || secondaryEvidence.stage === "inspector"
1603
- || secondaryEvidence.stage === "notary"))
1604
- return base;
1605
- // Defaults live solely in extractGateFactFromSessionDirectory — do not re-derive.
1606
- const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
1607
- return gate === undefined ? base : { ...base, gate };
1655
+ || secondaryEvidence.stage === "notary");
1656
+ let next = base;
1657
+ if (!skipGate) {
1658
+ // Defaults live solely in extractGateFactFromSessionDirectory — do not re-derive.
1659
+ const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
1660
+ if (gate !== undefined)
1661
+ next = { ...base, gate };
1662
+ }
1663
+ return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
1608
1664
  }
1609
1665
  export function extractNavigatorFact(entries) {
1610
1666
  // Affirmative attendance only. Missing / uncorrelated / unparseable is never no-advice.
@@ -1814,7 +1870,7 @@ async function settleLawfulJudgeTerminalResult(admitted, authority, scope) {
1814
1870
  navigator,
1815
1871
  artifacts,
1816
1872
  runId: admitted.runId,
1817
- }, coordinates.sessionDirectory);
1873
+ }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
1818
1874
  }
1819
1875
  /**
1820
1876
  * Settle a lawful typed terminal result from the admitted session.
@@ -1854,7 +1910,7 @@ async function settleLawfulCoderTerminalResult(admitted, authority, options = {}
1854
1910
  navigator,
1855
1911
  artifacts,
1856
1912
  runId: admitted.runId,
1857
- }, coordinates.sessionDirectory);
1913
+ }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
1858
1914
  }
1859
1915
  /** Settle a lawful Coder Terminal from the admitted session (shared #106 success interface). */
1860
1916
  export async function settleCoderTerminalResult(admitted, authority, options = {}, scope) {
@@ -1974,7 +2030,7 @@ async function settleLawfulFixerTerminalResult(admitted, authority, options, sco
1974
2030
  navigator,
1975
2031
  artifacts,
1976
2032
  runId: admitted.runId,
1977
- }, sessionDirectory);
2033
+ }, sessionDirectory, detourGateContext(admitted, scope));
1978
2034
  }
1979
2035
  /** Settle a lawful Fixer Terminal from the admitted session (shared #106 success interface). */
1980
2036
  export async function settleFixerTerminalResult(admitted, authority, options, scope) {
@@ -2037,7 +2093,7 @@ async function settleLawfulCollectorTerminalResult(admitted, authority, scope) {
2037
2093
  cause: "output",
2038
2094
  diagnostic: residual.diagnostic,
2039
2095
  details,
2040
- }, authority);
2096
+ }, authority, scope ?? {});
2041
2097
  return attachRecordedSubmissions(admitted, failed, scope);
2042
2098
  }
2043
2099
  return undefined;
@@ -2050,7 +2106,7 @@ async function settleLawfulCollectorTerminalResult(admitted, authority, scope) {
2050
2106
  navigator,
2051
2107
  artifacts,
2052
2108
  runId: admitted.runId,
2053
- }, sessionDirectory), scope);
2109
+ }, sessionDirectory, detourGateContext(admitted, scope)), scope);
2054
2110
  }
2055
2111
  /** Settle a lawful Collector Terminal from the admitted session. */
2056
2112
  export async function settleCollectorTerminalResult(admitted, authority, scope) {
@@ -2144,7 +2200,7 @@ async function settleLawfulDoctorTerminalResult(admitted, authority, scope) {
2144
2200
  navigator: extractNavigatorFact(entries),
2145
2201
  artifacts,
2146
2202
  runId: admitted.runId,
2147
- }, sessionDirectory);
2203
+ }, sessionDirectory, detourGateContext(admitted, scope));
2148
2204
  }
2149
2205
  const roleOutcome = sealed;
2150
2206
  const navigator = extractNavigatorFact(entries);
@@ -2159,7 +2215,7 @@ async function settleLawfulDoctorTerminalResult(admitted, authority, scope) {
2159
2215
  navigator,
2160
2216
  artifacts,
2161
2217
  runId: admitted.runId,
2162
- }, sessionDirectory);
2218
+ }, sessionDirectory, detourGateContext(admitted, scope));
2163
2219
  }
2164
2220
  /** Settle a lawful Doctor Terminal from the admitted session. */
2165
2221
  export async function settleDoctorTerminalResult(admitted, authority, scope) {
@@ -2203,7 +2259,7 @@ async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec,
2203
2259
  cause: "output",
2204
2260
  diagnostic: residual.diagnostic,
2205
2261
  details,
2206
- }, authority);
2262
+ }, authority, scope ?? {});
2207
2263
  return withSubmissions(failed, submissions);
2208
2264
  }
2209
2265
  }
@@ -2215,7 +2271,7 @@ async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec,
2215
2271
  navigator,
2216
2272
  artifacts: [],
2217
2273
  runId: admitted.runId,
2218
- }, sessionDirectory), submissions);
2274
+ }, sessionDirectory, detourGateContext(admitted, scope)), submissions);
2219
2275
  }
2220
2276
  if (roleOutcome?.role === spec.role) {
2221
2277
  const navigator = extractNavigatorFact(entries);
@@ -2224,7 +2280,7 @@ async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec,
2224
2280
  navigator,
2225
2281
  artifacts: [],
2226
2282
  runId: admitted.runId,
2227
- }, sessionDirectory), submissions);
2283
+ }, sessionDirectory, detourGateContext(admitted, scope)), submissions);
2228
2284
  }
2229
2285
  return undefined;
2230
2286
  }
@@ -2429,7 +2485,7 @@ async function settleLawfulReviewerTerminalResult(admitted, authority, options,
2429
2485
  navigator,
2430
2486
  artifacts,
2431
2487
  runId: admitted.runId,
2432
- }, sessionDirectory);
2488
+ }, sessionDirectory, detourGateContext(admitted, scope));
2433
2489
  }
2434
2490
  /** Settle a lawful Reviewer Terminal from the admitted session (shared #106 success interface). */
2435
2491
  export async function settleReviewerTerminalResult(admitted, authority, options, scope) {
@@ -2525,7 +2581,7 @@ async function settleLawfulMergerTerminalResult(admitted, authority, options, sc
2525
2581
  cause: "output",
2526
2582
  diagnostic: residual.diagnostic,
2527
2583
  details,
2528
- }, authority);
2584
+ }, authority, scope ?? {});
2529
2585
  return attachRecordedSubmissions(admitted, failed, scope);
2530
2586
  }
2531
2587
  return undefined;
@@ -2544,7 +2600,7 @@ async function settleLawfulMergerTerminalResult(admitted, authority, options, sc
2544
2600
  navigator,
2545
2601
  artifacts,
2546
2602
  runId: admitted.runId,
2547
- }, sessionDirectory), scope);
2603
+ }, sessionDirectory, detourGateContext(admitted, scope)), scope);
2548
2604
  }
2549
2605
  /** Settle a lawful Merger Terminal from the admitted session (shared #106 success interface). */
2550
2606
  export async function settleMergerTerminalResult(admitted, authority, options, scope) {
@@ -2760,7 +2816,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2760
2816
  navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
2761
2817
  artifacts: [],
2762
2818
  runId: admitted.runId,
2763
- }, sessionDirectory);
2819
+ }, sessionDirectory, detourGateContext(admitted, options));
2764
2820
  }
2765
2821
  }
2766
2822
  catch { /* malformed lifecycle bytes remain the existing nonzero output failure */ }
@@ -2801,7 +2857,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2801
2857
  navigator,
2802
2858
  artifacts: [],
2803
2859
  resume: options.resume,
2804
- }, sessionDirectory);
2860
+ }, sessionDirectory, detourGateContext(admitted, options));
2805
2861
  }
2806
2862
  const roleOutcome = {
2807
2863
  kind: "failure",
@@ -2816,7 +2872,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2816
2872
  navigator,
2817
2873
  artifacts,
2818
2874
  runId: admitted.runId,
2819
- }, sessionDirectory);
2875
+ }, sessionDirectory, detourGateContext(admitted, options));
2820
2876
  }
2821
2877
  /** Judge-named alias retained for #107 call sites. */
2822
2878
  export async function settleJudgeFailureTerminalResult(admitted, failure, authority, options = {}) {
@@ -25,6 +25,9 @@ export function projectRoleTurnRequest(admitted, roleDetails, options) {
25
25
  ...(options.courtAttemptId === undefined || options.courtAttemptId.length === 0
26
26
  ? {}
27
27
  : { courtAttemptId: options.courtAttemptId }),
28
+ ...(options.invocationScopeId === undefined || options.invocationScopeId.length === 0
29
+ ? {}
30
+ : { invocationScopeId: options.invocationScopeId }),
28
31
  ...(options.stationChild === undefined ? {} : { stationChild: options.stationChild }),
29
32
  };
30
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4239",
3
+ "version": "0.1.4259",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",