@akagilnc/pi-workflow-roles 0.1.4243 → 0.1.4274

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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * #537 typed ak_engine_detour tool usage ledger.
3
+ *
4
+ * Observes only the package detour tool path. Bash/CLI ordinary path
5
+ * (resources/engine-dispatch.md ordinary path) stays outside this ledger — a
6
+ * permanent blind spot that must never be read as "the seat did not use an engine".
7
+ *
8
+ * Runtime owns these facts beside role payloads (ADR 0042); they land in
9
+ * TerminalResult.decisiveFacts and are written live via sitian (ADR 0077).
10
+ * No gate, no required field, no index bytes (ADR 0049 / 0057).
11
+ *
12
+ * Invocation scope is one public ak-role call (#537), owned by the shared Host
13
+ * execution envelope (RoleTurnRequest / HostContext). Auto-resume attempts inside
14
+ * that call share the same scope; only an explicit new public call (including
15
+ * `ak-role resume`) mints a new one. Never courtAttemptId, never Pi session
16
+ * toolResult join keys, and never a detour-owned sidecar file.
17
+ */
18
+ import { randomUUID } from "node:crypto";
19
+ import { readFileSync } from "node:fs";
20
+ import { dirname, join } from "node:path";
21
+
22
+ import { ENGINE_DETOUR_TOOL_NAME } from "./engine-detour.ts";
23
+ import {
24
+ readSitianRecords,
25
+ resolveSitianRecordPath,
26
+ sitianReport,
27
+ type RecordPointer,
28
+ } from "./sitian-facade.ts";
29
+
30
+ /** Sitian event kind for one detour-tool call (volume under session/). */
31
+ export const ENGINE_DETOUR_CALL_KIND = "engine-detour-call" as const;
32
+
33
+ /** decisiveFacts key — name states detour-tool scope, not full engine usage. */
34
+ export const ENGINE_DETOUR_TOOL_USAGE_FACT_KEY = "engineDetourToolUsage" as const;
35
+
36
+ /**
37
+ * Run-relative sitian volume path for one detour call.
38
+ * Openable once the run directory is known (resume.command / top-level runId);
39
+ * contains no runId bytes itself (#108 + #537 AC8).
40
+ */
41
+ export const ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE =
42
+ `session/${ENGINE_DETOUR_CALL_KIND}/records.jsonl` as const;
43
+
44
+ /** stdout UTF-8 byte length (ticket-frozen metric; empty stdout is real 0). */
45
+ export function engineDetourStdoutByteLength(stdout: string): number {
46
+ return Buffer.byteLength(stdout, "utf8");
47
+ }
48
+
49
+ /** One observed ak_engine_detour call. code/stdoutByteLength absent on spawn failure. */
50
+ export type EngineDetourCallFact = {
51
+ readonly toolCallId: string;
52
+ readonly durationMs: number;
53
+ readonly code?: number;
54
+ readonly stdoutByteLength?: number;
55
+ readonly recordPointer: Readonly<RecordPointer>;
56
+ };
57
+
58
+ /**
59
+ * Per-invocation aggregate for the detour tool only.
60
+ * callCount 0 is lawful when engine is mounted (ADR 0071 soft rule).
61
+ */
62
+ export type EngineDetourToolUsageFact = {
63
+ readonly callCount: number;
64
+ readonly calls: readonly EngineDetourCallFact[];
65
+ };
66
+
67
+ export type ReportEngineDetourCallInput = {
68
+ readonly toolCallId: string;
69
+ readonly durationMs: number;
70
+ /** Present only when the child closed (not spawn failure). */
71
+ readonly code?: number;
72
+ /** Present only when the child closed; empty stdout → 0. */
73
+ readonly stdoutByteLength?: number;
74
+ readonly cwd: string;
75
+ readonly sessionParent: string;
76
+ readonly home?: string;
77
+ /** Real selected host (ADR 0077 / 0082). Never invent "pi". */
78
+ readonly host?: string;
79
+ readonly runId?: string;
80
+ /** Public-invocation scope id from the shared Host envelope. */
81
+ readonly invocationScopeId?: string;
82
+ };
83
+
84
+ /**
85
+ * Deterministic sitian identity: invocation scope + toolCallId.
86
+ * Must not embed runId — public Terminal decisiveFacts re-expose identity (#108).
87
+ */
88
+ export function engineDetourCallIdentity(input: {
89
+ readonly toolCallId: string;
90
+ readonly invocationScopeId?: string;
91
+ }): string {
92
+ const scope = input.invocationScopeId ?? "";
93
+ return `engine-detour-call:${scope}:${input.toolCallId}`;
94
+ }
95
+
96
+ /** Live sitian write for one detour call. Returns the call fact with pointer. */
97
+ export function reportEngineDetourCall(
98
+ input: ReportEngineDetourCallInput,
99
+ ): EngineDetourCallFact {
100
+ const payload: Record<string, unknown> = {
101
+ tool: ENGINE_DETOUR_TOOL_NAME,
102
+ toolCallId: input.toolCallId,
103
+ durationMs: input.durationMs,
104
+ };
105
+ if (input.code !== undefined) payload.code = input.code;
106
+ if (input.stdoutByteLength !== undefined) {
107
+ payload.stdoutByteLength = input.stdoutByteLength;
108
+ }
109
+ if (input.invocationScopeId !== undefined) {
110
+ payload.invocationScopeId = input.invocationScopeId;
111
+ }
112
+ if (input.runId !== undefined) payload.runId = input.runId;
113
+
114
+ // SitianSubject object form requires runId; invocation scope rides payload always.
115
+ const subject =
116
+ input.runId === undefined
117
+ ? undefined
118
+ : {
119
+ runId: input.runId,
120
+ ...(input.invocationScopeId === undefined
121
+ ? {}
122
+ : { invocationScopeId: input.invocationScopeId }),
123
+ };
124
+
125
+ const pointer = sitianReport({
126
+ level: "event",
127
+ kind: ENGINE_DETOUR_CALL_KIND,
128
+ identity: engineDetourCallIdentity({
129
+ toolCallId: input.toolCallId,
130
+ ...(input.invocationScopeId === undefined
131
+ ? {}
132
+ : { invocationScopeId: input.invocationScopeId }),
133
+ }),
134
+ cwd: input.cwd,
135
+ sessionParent: input.sessionParent,
136
+ source: "engine-detour-tool",
137
+ payload,
138
+ raw: {
139
+ sessionFile: input.sessionParent,
140
+ entryId: input.toolCallId,
141
+ },
142
+ ...(input.home === undefined ? {} : { home: input.home }),
143
+ ...(input.host === undefined ? {} : { host: input.host }),
144
+ ...(subject === undefined ? {} : { subject }),
145
+ });
146
+
147
+ return {
148
+ toolCallId: input.toolCallId,
149
+ durationMs: input.durationMs,
150
+ ...(input.code === undefined ? {} : { code: input.code }),
151
+ ...(input.stdoutByteLength === undefined
152
+ ? {}
153
+ : { stdoutByteLength: input.stdoutByteLength }),
154
+ recordPointer: pointer,
155
+ };
156
+ }
157
+
158
+ function isRecord(value: unknown): value is Record<string, unknown> {
159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
160
+ }
161
+
162
+ function callFactFromSitianPayload(
163
+ payload: unknown,
164
+ pointer: RecordPointer,
165
+ ): EngineDetourCallFact | undefined {
166
+ if (!isRecord(payload)) return undefined;
167
+ if (payload.tool !== ENGINE_DETOUR_TOOL_NAME) return undefined;
168
+ if (typeof payload.toolCallId !== "string" || payload.toolCallId.length === 0) {
169
+ return undefined;
170
+ }
171
+ if (typeof payload.durationMs !== "number" || !Number.isFinite(payload.durationMs)) {
172
+ return undefined;
173
+ }
174
+ return {
175
+ toolCallId: payload.toolCallId,
176
+ durationMs: payload.durationMs,
177
+ ...(typeof payload.code === "number" ? { code: payload.code } : {}),
178
+ ...(typeof payload.stdoutByteLength === "number"
179
+ ? { stdoutByteLength: payload.stdoutByteLength }
180
+ : {}),
181
+ recordPointer: pointer,
182
+ };
183
+ }
184
+
185
+ function invocationScopeIdOfRecord(record: {
186
+ readonly subject?: unknown;
187
+ readonly payload?: unknown;
188
+ }): string | undefined {
189
+ if (isRecord(record.subject)) {
190
+ const fromSubject = record.subject.invocationScopeId;
191
+ if (typeof fromSubject === "string" && fromSubject.length > 0) return fromSubject;
192
+ }
193
+ if (isRecord(record.payload)) {
194
+ const fromPayload = record.payload.invocationScopeId;
195
+ if (typeof fromPayload === "string" && fromPayload.length > 0) return fromPayload;
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ /**
201
+ * Read this-invocation detour-tool usage from sitian volume.
202
+ * When `invocationScopeId` is provided, only records bound to that public call count
203
+ * (host-neutral boundary — not session toolResult join keys, not courtAttemptId).
204
+ * When engineMounted is false, returns undefined (field absent).
205
+ * When engineMounted is true and no calls, returns callCount 0.
206
+ */
207
+ export async function readEngineDetourToolUsage(input: {
208
+ readonly sessionParent: string;
209
+ readonly engineMounted: boolean;
210
+ readonly invocationScopeId?: string;
211
+ readonly home?: string;
212
+ readonly cwd?: string;
213
+ }): Promise<EngineDetourToolUsageFact | undefined> {
214
+ if (!input.engineMounted) return undefined;
215
+
216
+ const { recordFile } = resolveSitianRecordPath({
217
+ level: "event",
218
+ kind: ENGINE_DETOUR_CALL_KIND,
219
+ sessionParent: input.sessionParent,
220
+ ...(input.home === undefined ? {} : { home: input.home }),
221
+ ...(input.cwd === undefined ? {} : { cwd: input.cwd }),
222
+ });
223
+
224
+ const { records } = await readSitianRecords(recordFile);
225
+ const calls: EngineDetourCallFact[] = [];
226
+ for (const record of records) {
227
+ if (record.kind !== ENGINE_DETOUR_CALL_KIND) continue;
228
+ const boundScope = invocationScopeIdOfRecord(record);
229
+ if (input.invocationScopeId !== undefined && input.invocationScopeId.length > 0) {
230
+ if (boundScope !== input.invocationScopeId) continue;
231
+ } else if (boundScope !== undefined) {
232
+ // Unscoped settlement must not pull invocation-bound rows across resume.
233
+ continue;
234
+ }
235
+ const pointer: RecordPointer = {
236
+ identity: record.identity,
237
+ recordFile,
238
+ kind: record.kind,
239
+ level: record.level,
240
+ };
241
+ const fact = callFactFromSitianPayload(record.payload, pointer);
242
+ if (fact === undefined) continue;
243
+ calls.push(fact);
244
+ }
245
+
246
+ return { callCount: calls.length, calls };
247
+ }
248
+
249
+ /**
250
+ * Project usage onto the public Terminal face.
251
+ * Non-resumable: keep absolute recordFile (runId already public via top-level runId).
252
+ * Resumable: keep an openable run-relative recordFile and identity with no runId
253
+ * bytes (#108 single disclosure + #537 AC8 reopen).
254
+ */
255
+ export function projectEngineDetourToolUsageForPublicTerminal(
256
+ usage: EngineDetourToolUsageFact,
257
+ options: { readonly discloseRecordFile: boolean },
258
+ ): EngineDetourToolUsageFact {
259
+ if (options.discloseRecordFile) return usage;
260
+ return {
261
+ callCount: usage.callCount,
262
+ calls: usage.calls.map((call) => ({
263
+ toolCallId: call.toolCallId,
264
+ durationMs: call.durationMs,
265
+ ...(call.code === undefined ? {} : { code: call.code }),
266
+ ...(call.stdoutByteLength === undefined
267
+ ? {}
268
+ : { stdoutByteLength: call.stdoutByteLength }),
269
+ recordPointer: {
270
+ identity: call.recordPointer.identity,
271
+ kind: call.recordPointer.kind,
272
+ level: call.recordPointer.level,
273
+ recordFile: ENGINE_DETOUR_CALL_RECORD_FILE_RELATIVE,
274
+ },
275
+ })),
276
+ };
277
+ }
278
+
279
+ /** One shared invocation.json reader (ENOENT → undefined; other errors propagate). */
280
+ function readInvocationRecord(
281
+ runDirectory: string,
282
+ ): Record<string, unknown> | undefined {
283
+ try {
284
+ const raw = JSON.parse(
285
+ readFileSync(join(runDirectory, "invocation.json"), "utf8"),
286
+ ) as unknown;
287
+ return isRecord(raw) ? raw : undefined;
288
+ } catch (error) {
289
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return undefined;
290
+ throw error;
291
+ }
292
+ }
293
+
294
+ /** True when invocation.json carries a non-empty engine axis. */
295
+ export async function readInvocationEngineMounted(
296
+ runDirectory: string,
297
+ ): Promise<boolean> {
298
+ const raw = readInvocationRecord(runDirectory);
299
+ if (raw === undefined) return false;
300
+ return typeof raw.engine === "string" && raw.engine.trim() !== "";
301
+ }
302
+
303
+ /**
304
+ * Selected host from the admission invocation page — for post-admission
305
+ * host-transition classification only. In-turn tools take host from the shared
306
+ * Host envelope (RoleTurnRequest / HostContext), never this reader.
307
+ * Undefined when absent — callers must not invent "pi".
308
+ */
309
+ export function readInvocationSelectedHost(runDirectory: string): string | undefined {
310
+ const raw = readInvocationRecord(runDirectory);
311
+ if (raw === undefined) return undefined;
312
+ return typeof raw.host === "string" && raw.host.trim() !== ""
313
+ ? raw.host.trim()
314
+ : undefined;
315
+ }
316
+
317
+ /** Merge usage into decisiveFacts without touching role payloads. */
318
+ export function withEngineDetourToolUsageFact<
319
+ T extends { readonly decisiveFacts?: Readonly<Record<string, unknown>> },
320
+ >(
321
+ outcome: T,
322
+ usage: EngineDetourToolUsageFact | undefined,
323
+ ): T {
324
+ if (usage === undefined) return outcome;
325
+ const prior = isRecord(outcome.decisiveFacts) ? outcome.decisiveFacts : {};
326
+ return {
327
+ ...outcome,
328
+ decisiveFacts: {
329
+ ...prior,
330
+ [ENGINE_DETOUR_TOOL_USAGE_FACT_KEY]: usage,
331
+ },
332
+ };
333
+ }
334
+
335
+ /** runDirectory owning a session directory (.../runs/<id>@role/session). */
336
+ export function runDirectoryFromSessionDirectory(sessionDirectory: string): string {
337
+ return dirname(sessionDirectory);
338
+ }
339
+
340
+ /** session.jsonl under a session directory. */
341
+ export function sessionFileFromSessionDirectory(sessionDirectory: string): string {
342
+ return join(sessionDirectory, "session.jsonl");
343
+ }
344
+
345
+ /**
346
+ * Mint one public-invocation scope id when an engine is mounted.
347
+ * Call once at the public-entry boundary — never inside the auto-resume loop.
348
+ * The id lives on RoleTurnRequest / HostContext only (no detour sidecar file).
349
+ */
350
+ export function mintEngineDetourInvocationScope(input: {
351
+ readonly effectiveEngine?: string;
352
+ }): string | undefined {
353
+ const engine = input.effectiveEngine?.trim();
354
+ if (engine === undefined || engine.length === 0) return undefined;
355
+ return randomUUID();
356
+ }
357
+
358
+ /** Attach a minted scope onto a turn request (shared Host envelope field). */
359
+ export function withEngineDetourInvocationScope<T extends object>(
360
+ request: T & { readonly invocationScopeId?: string },
361
+ invocationScopeId: string | undefined,
362
+ ): T & { readonly invocationScopeId?: string } {
363
+ if (invocationScopeId === undefined || invocationScopeId.length === 0) {
364
+ return request;
365
+ }
366
+ if (
367
+ typeof request.invocationScopeId === "string" &&
368
+ request.invocationScopeId.length > 0
369
+ ) {
370
+ return request;
371
+ }
372
+ return { ...request, invocationScopeId };
373
+ }
@@ -194,6 +194,17 @@ export type RoleTurnRequest = {
194
194
  * and resume-with-message set this; bare resume without an open court omits it.
195
195
  */
196
196
  readonly courtAttemptId?: string;
197
+ /**
198
+ * Public-invocation scope (#537): one ak-role call. Auto-resume reuses it;
199
+ * explicit resume mints a new one. Owned by this shared Host envelope — not
200
+ * courtAttemptId and not a detour sidecar file.
201
+ */
202
+ readonly invocationScopeId?: string;
203
+ /**
204
+ * Selected host axis for this turn (#537 / ADR 0082). Projected from the
205
+ * public-entry seat resolution — never re-read from invocation.json by tools.
206
+ */
207
+ readonly host?: string;
197
208
  /** Station child role run (#840): omit automatic navigator attendance. */
198
209
  readonly stationChild?: boolean;
199
210
  };
@@ -246,7 +257,7 @@ export interface DurablePrincipalAuthority {
246
257
  type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLeafId(): string | null | undefined; getEntries(): Iterable<HostSessionEntry>; getSessionDir(): string; getSessionFile(): string | undefined; getHeader?(): { readonly type: string; readonly id?: string } | null; setSessionFile?(path: string): void; appendCustomEntry?(customType: string, data?: unknown): unknown; };
247
258
 
248
259
  /** Context supplied by a host for one activation and its interceptable events. */
249
- export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
260
+ export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; /** Public-invocation scope (#537); never process-global env. */ invocationScopeId?: string; /** Selected host axis (#537 / ADR 0082); never process-global env invent. */ host?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
250
261
 
251
262
  /** Per-turn run directory; adapters must project any child-process identity. */
252
263
  export function runDirectoryFromHostContext(context: HostContext): string | undefined {
@@ -18,7 +18,6 @@ import {
18
18
  import type { HostContext } from "./host-contracts.ts";
19
19
  import { createNativeNavigatorSessionFactory } from "./navigator-public-session.ts";
20
20
  import {
21
- NAVIGATOR_DEFAULT_MODEL,
22
21
  NAVIGATOR_PREPARE_TOOL_NAME,
23
22
  NavigatorUnavailableError,
24
23
  navigatorModelSettingPath,
@@ -40,7 +39,6 @@ import {
40
39
  import { sitianReport } from "./sitian-facade.ts";
41
40
 
42
41
  export {
43
- NAVIGATOR_DEFAULT_MODEL,
44
42
  NAVIGATOR_PREPARE_TOOL_NAME,
45
43
  NavigatorUnavailableError,
46
44
  navigatorModelSettingPath,
@@ -12,7 +12,6 @@ import {
12
12
  navigatorProviderFailureFromError,
13
13
  navigatorUnavailableError,
14
14
  parseNavigatorModelSetting,
15
- resolveNavigatorSeatSelection,
16
15
  type NavigatorProviderFailureFact,
17
16
  type NavigatorSessionFactory,
18
17
  } from "./navigator-session-contracts.ts";
@@ -43,8 +42,9 @@ async function resolveNavigatorLedgerHome(context: HostContext): Promise<string
43
42
 
44
43
  export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
45
44
  return async ({ context, subject, tool }) => {
46
- const resolved = await resolveNavigatorSeatSelection(context);
47
- let thinkingLevel = resolved.thinkingLevel;
45
+ // Model is enforced at prepare (attendance seat resolve) and at prompt (summon).
46
+ // Factory open only books the archivist nest — no package default, no eager seat read.
47
+ let thinkingLevel: string | undefined;
48
48
 
49
49
  // Archivist nest for attendance route memory only (ADR 0018 / 0065) — not a session open.
50
50
  // #852: navigator/<work-subject> is the sole book-top exception; always pass subject so
@@ -15,7 +15,6 @@ import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
15
15
 
16
16
 
17
17
  export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
18
- export const NAVIGATOR_DEFAULT_MODEL = "openai-codex/gpt-5.6-luna:max" as const;
19
18
 
20
19
  export type NavigatorUnavailableKey =
21
20
  | "context" | "session" | "model" | "thinking" | "auth" | "quota" | "transport" | "unknown";
@@ -229,8 +228,9 @@ export async function readNavigatorModelSetting(path = navigatorModelSettingPath
229
228
  }
230
229
  return raw.model;
231
230
  } catch (error) {
231
+ // #178: missing file is absence (no package default).
232
232
  if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
233
- return NAVIGATOR_DEFAULT_MODEL;
233
+ throw new Error("Navigator model setting is missing");
234
234
  }
235
235
  throw error;
236
236
  }
@@ -294,19 +294,13 @@ export async function resolveNavigatorSeatSelection(
294
294
  : packageMachineHome();
295
295
  const config = await loadPublicCliConfig(home);
296
296
  const modelOnly = seatModelOnly(config.seats.navigator);
297
- // Seat table is the only model authority — no legacy navigator-model.json.
298
- // Bare missing seat uses the package default constant (not a file path).
297
+ // Seat table only — missing navigator seat → typed model-unavailable (#178).
299
298
  if (modelOnly === undefined) {
300
- const parsed = parseNavigatorModelSetting(NAVIGATOR_DEFAULT_MODEL);
301
- return {
302
- selection: {
303
- provider: parsed.provider,
304
- model: parsed.model,
305
- ...(parsed.thinkingLevel === undefined ? {} : { thinking: parsed.thinkingLevel }),
306
- },
307
- configuredLabel: NAVIGATOR_DEFAULT_MODEL,
308
- ...(parsed.thinkingLevel === undefined ? {} : { thinkingLevel: parsed.thinkingLevel }),
309
- };
299
+ const { missingResolvedSeatModelMessage } = await import("./public-cli/config.ts");
300
+ throw navigatorUnavailableError(
301
+ "model",
302
+ new Error(missingResolvedSeatModelMessage("navigator")),
303
+ );
310
304
  }
311
305
  // Host params passthrough only — no map/validate/default (#675 ⑥).
312
306
  const thinkingRaw = modelOnly.thinking;
package/src/pi/adapter.ts CHANGED
@@ -52,6 +52,12 @@ function projectPiContext(context: ExtensionContext, transcriptFromContext?: (co
52
52
  ...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
53
53
  ? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
54
54
  : {}),
55
+ ...(typeof process.env.AK_ROLE_INVOCATION_SCOPE === "string" && process.env.AK_ROLE_INVOCATION_SCOPE.trim() !== ""
56
+ ? { invocationScopeId: process.env.AK_ROLE_INVOCATION_SCOPE }
57
+ : {}),
58
+ ...(typeof process.env.AK_ROLE_HOST === "string" && process.env.AK_ROLE_HOST.trim() !== ""
59
+ ? { host: process.env.AK_ROLE_HOST.trim() }
60
+ : {}),
55
61
  sessionManager: {
56
62
  getLeafEntry: () => context.sessionManager.getLeafEntry(),
57
63
  getLeafId: () => context.sessionManager.getLeafId(),
@@ -447,6 +447,12 @@ export function createPiRoleTurnHost(config: PiRoleTurnHostConfig): RoleTurnHost
447
447
  };
448
448
  if (request.courtAttemptId === undefined) delete env.AK_ROLE_COURT_ATTEMPT;
449
449
  else env.AK_ROLE_COURT_ATTEMPT = request.courtAttemptId;
450
+ // Public-invocation scope (#537): omit must not inherit a parent env value.
451
+ if (request.invocationScopeId === undefined) delete env.AK_ROLE_INVOCATION_SCOPE;
452
+ else env.AK_ROLE_INVOCATION_SCOPE = request.invocationScopeId;
453
+ // Selected host axis (#537 / ADR 0082): omit must not inherit a parent env value.
454
+ if (request.host === undefined || request.host.trim() === "") delete env.AK_ROLE_HOST;
455
+ else env.AK_ROLE_HOST = request.host.trim();
450
456
  applyEngineChildEnv(env, request.engine);
451
457
  // Nested auditor dossier tool binds the parent run pointer when published.
452
458
  if (