@akagilnc/pi-workflow-roles 0.1.3837 → 0.1.3873

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.
Files changed (42) hide show
  1. package/dist/acp-host/production-host.js +24606 -24532
  2. package/dist/engine-detour-tool.js +2 -2
  3. package/dist/engine-detour.js +23 -7
  4. package/dist/headless-host/production-host.js +4197 -4139
  5. package/dist/host-descriptions.js +5 -2
  6. package/dist/pi/role-turn-host.js +20 -92
  7. package/dist/public-cli/load-production-acp-host.js +1 -1
  8. package/dist/public-cli/load-production-external-host.js +10 -0
  9. package/dist/public-cli/load-production-headless-host.js +1 -1
  10. package/dist/public-cli/main.js +95 -102
  11. package/dist/public-role-summons.js +7 -49
  12. package/dist/reviewer-agent.js +1 -0
  13. package/dist/reviewer-child-executor.js +3 -2
  14. package/dist/role-activation-flags.js +50 -0
  15. package/dist/role-runtime.js +8 -3
  16. package/package.json +1 -1
  17. package/scripts/build-package.mjs +2 -2
  18. package/src/acp-host/production-host.ts +31 -94
  19. package/src/acp-host/role-turn-host.ts +178 -263
  20. package/src/diagnostic-tail.ts +18 -0
  21. package/src/engine-detour-tool.ts +5 -4
  22. package/src/engine-detour.ts +44 -9
  23. package/src/external-host-turn-loop.ts +172 -0
  24. package/src/headless-host/production-host.ts +10 -7
  25. package/src/headless-host/role-turn-host.ts +271 -263
  26. package/src/host-descriptions.ts +5 -2
  27. package/src/host-session-record.ts +41 -0
  28. package/src/pi/role-turn-host.ts +18 -93
  29. package/src/prepared-role-turn.ts +68 -0
  30. package/src/public-cli/cli.ts +11 -20
  31. package/src/public-cli/load-production-acp-host.ts +2 -1
  32. package/src/public-cli/load-production-external-host.ts +17 -0
  33. package/src/public-cli/load-production-headless-host.ts +2 -1
  34. package/src/public-role-summons.ts +12 -69
  35. package/src/reviewer-agent.ts +12 -1
  36. package/src/reviewer-child-executor.ts +10 -2
  37. package/src/role-activation-flags.ts +53 -0
  38. package/src/{acp-host/role-envelope.ts → role-envelope.ts} +28 -84
  39. package/src/role-runtime-dependencies.ts +91 -0
  40. package/src/role-runtime.ts +14 -4
  41. package/src/{acp-host/session-identity.ts → session-identity.ts} +6 -6
  42. /package/src/{acp-host/mcp-relay.mjs → mcp-relay.mjs} +0 -0
@@ -68,6 +68,7 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
68
68
  /**
69
69
  * Headless CLI family (#645). Claude is the first row; codex (#646) adds another.
70
70
  * fixedArgs: print mode, isolation without `--bare` (OAuth stays), full permissions.
71
+ * stream-json + verbose: live host events for sitian records (#811); result is last line.
71
72
  * `--setting-sources` empty = load no user/project/local CLAUDE.md/hooks/skills
72
73
  * (role envelope is delivered via `--system-prompt` wholesale replace).
73
74
  * `--strict-mcp-config` with no `--mcp-config` drops operator MCP + claude.ai connectors.
@@ -77,8 +78,10 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
77
78
  binaryFromHome: Object.freeze([".local", "bin", "claude"]),
78
79
  sessionBindingFile: "claude-headless-session.json",
79
80
  fixedArgs: Object.freeze([
80
- // One result envelope (not stream-json): typed receipt only; no event-stream copy.
81
- "--output-format", "json",
81
+ // Live NDJSON events → sitian host-session records (#811); last line is the result receipt.
82
+ "--output-format", "stream-json",
83
+ // Intermediate assistant/tool/system events require verbose with stream-json.
84
+ "--verbose",
82
85
  "--permission-mode", "bypassPermissions",
83
86
  // Empty sources: no user/project/local operator surface (envelope owns materials).
84
87
  "--setting-sources", "",
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Non-pi host turn events → sitian sole entry (ADR 0065 / 0077 / #811).
3
+ * Adapters capture host-native structured events; this helper only packages
4
+ * the common sitianReport call. session.jsonl stays header-only (#617 DK-4).
5
+ * Write failures propagate (SitianInfrastructureError) — not best-effort.
6
+ */
7
+ import { sitianReport } from "./sitian-facade.ts";
8
+
9
+ /** Volume category under `<run>/session/<kind>/records.jsonl`. */
10
+ export const HOST_SESSION_RECORD_KIND = "host-session" as const;
11
+
12
+ function eventField(event: unknown, key: string): string | undefined {
13
+ if (typeof event !== "object" || event === null || Array.isArray(event)) return undefined;
14
+ const value = (event as Record<string, unknown>)[key];
15
+ return typeof value === "string" && value.length > 0 ? value : undefined;
16
+ }
17
+
18
+ /** Live write of one host-emitted structured event. Throws on sitian persistence failure. */
19
+ export function reportHostSessionEvent(input: {
20
+ readonly host: string;
21
+ readonly cwd: string;
22
+ readonly sessionParent: string;
23
+ readonly source: string;
24
+ readonly event: unknown;
25
+ readonly identity?: string;
26
+ readonly timestamp?: string;
27
+ }): void {
28
+ const identity = input.identity ?? eventField(input.event, "uuid");
29
+ const timestamp = input.timestamp ?? eventField(input.event, "timestamp");
30
+ sitianReport({
31
+ level: "event",
32
+ kind: HOST_SESSION_RECORD_KIND,
33
+ host: input.host,
34
+ cwd: input.cwd,
35
+ sessionParent: input.sessionParent,
36
+ source: input.source,
37
+ payload: input.event,
38
+ ...(identity === undefined ? {} : { identity }),
39
+ ...(timestamp === undefined ? {} : { timestamp }),
40
+ });
41
+ }
@@ -15,7 +15,6 @@ import type {
15
15
  DurablePrincipal,
16
16
  DurablePrincipalAuthority,
17
17
  MethodBinding,
18
- RoleTurnActivation,
19
18
  RoleTurnHost,
20
19
  RoleTurnKnownFailure,
21
20
  RoleTurnModelConfig,
@@ -24,6 +23,7 @@ import type {
24
23
  } from "../host-contracts.ts";
25
24
  import { ExplicitInternalActivationError } from "../host-contracts.ts";
26
25
  import { applyEngineChildEnv } from "../engine-detour.ts";
26
+ import { projectActivationFlags } from "../role-activation-flags.ts";
27
27
 
28
28
 
29
29
  /** Package-relative Internal role entrypoint (ADR 0052; same path as public-cli registry). */
@@ -66,95 +66,18 @@ function buildSeatModelCliArgs(model: RoleTurnModelConfig | undefined): string[]
66
66
  ];
67
67
  }
68
68
 
69
- function buildActivationFlagArgs(activation: RoleTurnActivation): string[] {
70
- switch (activation.role) {
71
- case "judge":
72
- return ["--ak-role", "judge"];
73
- case "coder":
74
- return [
75
- "--ak-role",
76
- "coder",
77
- "--ak-coder-phase",
78
- activation.phase,
79
- "--ak-coder-task",
80
- activation.taskPath,
81
- ];
82
- case "fixer":
83
- return [
84
- "--ak-role",
85
- "fixer",
86
- "--ak-fixer-phase",
87
- activation.phase,
88
- "--ak-fix-packet",
89
- activation.packetPath,
90
- ...(activation.prerequisitesPath === undefined
91
- ? []
92
- : ["--ak-fixer-prerequisites", activation.prerequisitesPath]),
93
- ];
94
- case "reviewer":
95
- return [
96
- "--ak-role",
97
- "reviewer",
98
- "--ak-review-base",
99
- activation.baseRevision,
100
- ...(activation.authorityRefs.length === 0
101
- ? []
102
- : ["--ak-review-authority-refs", JSON.stringify([...activation.authorityRefs])]),
103
- ...(activation.ticketNumber === undefined
104
- ? []
105
- : ["--ak-review-ticket-number", String(activation.ticketNumber)]),
106
- ];
107
- case "merger":
108
- return ["--ak-role", "merger", "--ak-merger-input", activation.inputPath];
109
- case "collector":
110
- return [
111
- "--ak-role",
112
- "collector",
113
- "--ak-collector-repo",
114
- activation.repo,
115
- ...(activation.pr === undefined ? [] : ["--ak-collector-pr", activation.pr]),
116
- ...(activation.requestManifestPath === undefined
117
- ? []
118
- : ["--ak-collector-request-manifest", activation.requestManifestPath]),
119
- ...(activation.waitMs === undefined ? [] : ["--ak-collector-wait-ms", activation.waitMs]),
120
- ];
121
- case "doctor":
122
- return ["--ak-role", "doctor", "--ak-doctor-case", activation.casePath];
123
- case "notary":
124
- return [
125
- "--ak-role",
126
- "notary",
127
- "--ak-notary-source-run",
128
- activation.sourceRun,
129
- ...(activation.ticketNumber === undefined
130
- ? []
131
- : ["--ak-notary-ticket-number", String(activation.ticketNumber)]),
132
- ];
133
- case "countersign":
134
- // ticketNumber rides activation/admission/invocation only (#632: no private flag).
135
- return ["--ak-role", "countersign"];
136
- case "gleaner-left":
137
- return [
138
- "--ak-role",
139
- "gleaner-left",
140
- "--ak-gleaner-left-base",
141
- activation.baseRevision,
142
- ];
143
- case "inspector":
144
- return ["--ak-role", "inspector"];
145
- case "gatekeeper":
146
- return ["--ak-role", "gatekeeper"];
147
- case "navigator":
148
- return ["--ak-role", "navigator"];
149
- case "auditor":
150
- return ["--ak-role", "auditor"];
151
- case "diarist":
152
- return ["--ak-role", "diarist"];
153
- default: {
154
- const _exhaustive: never = activation;
155
- return _exhaustive;
156
- }
69
+ /**
70
+ * Pi last hop only: shared envelope flags → controlled-session argv pairs (#819).
71
+ * Flag membership/values stay in projectActivationFlags (middle layer).
72
+ */
73
+ function activationFlagsToPiArgv(flags: ReadonlyMap<string, boolean | string>): string[] {
74
+ const args: string[] = [];
75
+ for (const [name, value] of flags) {
76
+ if (value === false) continue;
77
+ args.push(`--${name}`);
78
+ if (value !== true) args.push(String(value));
157
79
  }
80
+ return args;
158
81
  }
159
82
 
160
83
  function buildMethodArgs(methods: readonly MethodBinding[]): string[] {
@@ -193,9 +116,10 @@ export function applyPiNativeSkillInvocation(
193
116
  }
194
117
 
195
118
  /**
196
- * Translate a closed RoleTurnRequest into Pi argv after `--no-extensions -e entry`.
197
- * Controlled session constants and session coordinates are adapter-internal.
198
- * Single forced method → Pi-native `/skill:` on the argv prompt (coder/reviewer/merger).
119
+ * Pi last-hop argv after `--no-extensions -e entry` (#819).
120
+ * Activation flag membership comes from middle-layer projectActivationFlags;
121
+ * this function only renders session coords, controlled constants, and pairs.
122
+ * Single forced method → Pi-native `/skill:` on the argv prompt (#822).
199
123
  */
200
124
  export function buildPiTurnExtraArgs(
201
125
  request: RoleTurnRequest,
@@ -222,7 +146,8 @@ export function buildPiTurnExtraArgs(
222
146
  "--session-dir",
223
147
  sessionDirectory,
224
148
  ...extraPiArgs,
225
- ...buildActivationFlagArgs(request.activation),
149
+ // Envelope assembly = projectActivationFlags; pi only renders argv pairs.
150
+ ...activationFlagsToPiArgv(projectActivationFlags(request)),
226
151
  "--mode",
227
152
  "json",
228
153
  ...buildSeatModelCliArgs(request.model),
@@ -0,0 +1,68 @@
1
+ import type { RoleTurnKnownFailure, RoleTurnRequest } from "./host-contracts.ts";
2
+ import { renderAgentStartMaterials } from "./agent-start-materials.ts";
3
+
4
+ /** The shared envelope, prepared before session/new (systemPrompt delivery) and
5
+ * able to observe the host's real builtin tool surface once it arrives post-session. */
6
+ export type PreparedRoleTurn = Readonly<{
7
+ mcpServers: readonly Readonly<Record<string, unknown>>[];
8
+ /**
9
+ * Structured system-prompt authority. `body` is the provider-facing prompt
10
+ * bytes; `materials` are typed agent-start reading materials (e.g. Notary
11
+ * session bound) that the adapter folds into the override at the provider
12
+ * boundary. This structure is the authoritative production input of the
13
+ * send path — never a test-only parallel face.
14
+ */
15
+ systemPrompt: { readonly body: string; readonly materials: readonly unknown[] };
16
+ /** Effective user prompt after host-side input transform (canonical Skill invocation). */
17
+ prompt: string;
18
+ /**
19
+ * Host abort signal armed only by typed infrastructure failure (envelope
20
+ * rememberInfrastructureFailure / non-correctable MCP catch). Lawful
21
+ * context.abort() (seal / non-sole) does not arm it. executeTurn races
22
+ * session/prompt against this so infra declarations terminate even when
23
+ * ACP never resolves (#593).
24
+ */
25
+ abortSignal?: AbortSignal;
26
+ /** Shared ledger consumes the complete ACP round after session/prompt resolves. */
27
+ closeRound(): Promise<
28
+ | { readonly accepted: true }
29
+ | {
30
+ readonly accepted: false;
31
+ /** Shared envelope provides officer/correctable text; adapters only deliver it (#813). */
32
+ readonly retry: {
33
+ readonly code: string;
34
+ readonly toolCallIds: readonly string[];
35
+ readonly message: string;
36
+ };
37
+ }
38
+ | { readonly accepted: false; readonly failure: RoleTurnKnownFailure }
39
+ >;
40
+ dispose?(): Promise<void>;
41
+ /**
42
+ * Headless CLI family (#645): role terminating-tool schema for host-native
43
+ * `--json-schema`. Present for every prepared turn; ACP ignores it.
44
+ */
45
+ jsonSchema: Readonly<Record<string, unknown>>;
46
+ /** Terminating tool name whose schema is `jsonSchema`. */
47
+ terminatingToolName: string;
48
+ /**
49
+ * Headless CLI family: feed host-native `structured_output` through the same
50
+ * terminating-tool path the MCP relay uses (ledger + gates). ACP ignores it.
51
+ */
52
+ ingestStructuredOutput(params: unknown): Promise<void>;
53
+ }>;
54
+
55
+ /** Fold structured system-prompt authority into the provider-visible ACP override. */
56
+ export function renderSystemPromptOverride(authority: {
57
+ readonly body: string;
58
+ readonly materials: readonly unknown[];
59
+ }): string {
60
+ return renderAgentStartMaterials(authority.body, authority.materials);
61
+ }
62
+
63
+ export type SessionIdentityAuthority = Readonly<{
64
+ load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
65
+ bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
66
+ /** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
67
+ resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
68
+ }>;
@@ -43,9 +43,9 @@ import { seatModelOnly } from "./registry.ts";
43
43
  import { CliUsageError } from "./cli-errors.ts";
44
44
  import type { CliIo } from "./cli-io.ts";
45
45
  import type { PostAdmissionEnv } from "./post-admission.ts";
46
- import type { RoleTurnHost, RoleTurnRequest } from "../host-contracts.ts";
46
+ import type { RoleTurnHost } from "../host-contracts.ts";
47
47
  import { packagedExternalHostNames } from "../host-descriptions.ts";
48
- import { loadProductionExternalHostFactory } from "./load-production-external-host.ts";
48
+ import { createLazyProductionExternalHost } from "./load-production-external-host.ts";
49
49
  import {
50
50
  createPiRoleTurnHost,
51
51
  appendPiSessionCustomEntry,
@@ -335,29 +335,20 @@ function resolveRoleTurnHost(
335
335
  recordLaunchedRolePackageIdentity,
336
336
  observeLaunchedRolePackageIdentity,
337
337
  });
338
- // Composition-root adapter table: pi (in-process default) + one ACP adapter per description-table key.
338
+ // Composition-root adapter table: pi (in-process default) + one lazy external host per description-table key (#820).
339
339
  const adapters: readonly NamedRoleTurnHostAdapter[] = env.hostAdapters ?? [
340
340
  { name: "pi", create: () => ({ ok: true as const, host: piHost }) },
341
341
  ...packagedExternalHostNames().map((name) => ({
342
342
  name,
343
- create: () => {
343
+ create: () => ({
344
+ ok: true as const,
344
345
  // Factory loads outside the public bin static graph (ADR 0052 peer-free discovery).
345
- let hostPromise: Promise<RoleTurnHost> | undefined;
346
- return {
347
- ok: true as const,
348
- host: {
349
- executeTurn: async (request: RoleTurnRequest) => {
350
- hostPromise ??= loadProductionExternalHostFactory(env.packageRoot, name).then((create) =>
351
- create({
352
- packageRoot: env.packageRoot,
353
- principalAuthority: options.principalAuthority,
354
- }),
355
- );
356
- return (await hostPromise).executeTurn(request);
357
- },
358
- },
359
- };
360
- },
346
+ host: createLazyProductionExternalHost({
347
+ packageRoot: env.packageRoot,
348
+ hostName: name,
349
+ principalAuthority: options.principalAuthority,
350
+ }),
351
+ }),
361
352
  })),
362
353
  ];
363
354
  const hostName = options.seat.host;
@@ -25,6 +25,7 @@ type GenericAcpHostFactory = (options: {
25
25
  packageRoot: string;
26
26
  principalAuthority: DurablePrincipalAuthority;
27
27
  description: AcpHostDescription;
28
+ hostName: string;
28
29
  }) => RoleTurnHost;
29
30
 
30
31
  /**
@@ -48,5 +49,5 @@ export async function loadProductionAcpHostFactory(
48
49
  createProductionAcpRoleTurnHost: GenericAcpHostFactory;
49
50
  };
50
51
  const create = mod.createProductionAcpRoleTurnHost;
51
- return (options) => create({ ...options, description });
52
+ return (options) => create({ ...options, description, hostName: host });
52
53
  }
@@ -27,3 +27,20 @@ export async function loadProductionExternalHostFactory(
27
27
  }
28
28
  throw new Error(`unregistered host: ${host}`);
29
29
  }
30
+
31
+ /** Lazy production external host — sole builder for CLI adapter table + public summons (#820). */
32
+ export function createLazyProductionExternalHost(options: {
33
+ readonly packageRoot: string;
34
+ readonly hostName: string;
35
+ readonly principalAuthority: DurablePrincipalAuthority;
36
+ }): RoleTurnHost {
37
+ let hostPromise: Promise<RoleTurnHost> | undefined;
38
+ return {
39
+ executeTurn: async (request) => {
40
+ hostPromise ??= loadProductionExternalHostFactory(options.packageRoot, options.hostName).then((create) =>
41
+ create({ packageRoot: options.packageRoot, principalAuthority: options.principalAuthority }),
42
+ );
43
+ return (await hostPromise).executeTurn(request);
44
+ },
45
+ };
46
+ }
@@ -22,6 +22,7 @@ type GenericHeadlessHostFactory = (options: {
22
22
  packageRoot: string;
23
23
  principalAuthority: DurablePrincipalAuthority;
24
24
  description: HeadlessHostDescription;
25
+ hostName: string;
25
26
  }) => RoleTurnHost;
26
27
 
27
28
  /**
@@ -44,5 +45,5 @@ export async function loadProductionHeadlessHostFactory(
44
45
  createProductionHeadlessRoleTurnHost: GenericHeadlessHostFactory;
45
46
  };
46
47
  const create = mod.createProductionHeadlessRoleTurnHost;
47
- return (options) => create({ ...options, description });
48
+ return (options) => create({ ...options, description, hostName: host });
48
49
  }
@@ -65,12 +65,6 @@ export type PublicSummonRequest = {
65
65
  * Never folded into argv / parent-run lookup keys (ADR 0079 卷宗指针 stays pure).
66
66
  */
67
67
  readonly gateReviewInstruction?: string;
68
- /**
69
- * Parent-invocation host axis (#617 / #645). Nested institutional legs inherit
70
- * the live parent `--host` so a single public entry does not require mutating
71
- * persistent seat host. Seat model/thinking stay on the officer seat table.
72
- */
73
- readonly host?: string;
74
68
  /**
75
69
  * Offline test inject — same face as public CLI env.roleTurnHost. Production
76
70
  * summons leave this unset and use the seat-resolved host.
@@ -218,7 +212,7 @@ async function createSummonEnv(options: {
218
212
  let roleTurnHost = piHost;
219
213
  if (hostName !== "pi") {
220
214
  const { lookupHostFamily } = await import("./host-descriptions.ts");
221
- const { loadProductionExternalHostFactory } = await import(
215
+ const { createLazyProductionExternalHost } = await import(
222
216
  "./public-cli/load-production-external-host.ts"
223
217
  );
224
218
  // #729 / #645: description tables are the sole host authority — family
@@ -228,19 +222,12 @@ async function createSummonEnv(options: {
228
222
  `public role summons host unregistered: host=${hostName} seat=${options.role}`,
229
223
  );
230
224
  }
231
- let hostPromise: Promise<RoleTurnHost> | undefined;
232
- roleTurnHost = {
233
- executeTurn: async (request) => {
234
- hostPromise ??= loadProductionExternalHostFactory(options.packageRoot, hostName).then(
235
- (create) =>
236
- create({
237
- packageRoot: options.packageRoot,
238
- principalAuthority,
239
- }),
240
- );
241
- return (await hostPromise).executeTurn(request);
242
- },
243
- };
225
+ // Same lazy external host face as public CLI (#820) — one builder, two callers.
226
+ roleTurnHost = createLazyProductionExternalHost({
227
+ packageRoot: options.packageRoot,
228
+ hostName,
229
+ principalAuthority,
230
+ });
244
231
  }
245
232
  // #788: host is registered above; only then project host-facing provider.
246
233
  const { loadHostProvidersTable, projectHostFacingProvider } = await import(
@@ -291,10 +278,9 @@ export async function summonPublicRole(
291
278
  const credentials =
292
279
  options.credentials ?? (await loadCredentialProviders(agentDir));
293
280
  const config = await loadPublicCliConfig(home);
294
- // Nested legs inherit parent invocation host when provided (#645 review path).
295
- // Model/thinking stay on the officer seat — host-only inherit (seat-table law).
296
- const invocation = options.host === undefined ? undefined : { host: options.host };
297
- const seat = resolveEffectiveSeat(config, options.role, credentials, invocation);
281
+ // Nested summons resolve host on the officer seat only (flag>seat>default pi).
282
+ // Parent run host is not an override channel (#821 / ADR 0082 host-flag-two-channels).
283
+ const seat = resolveEffectiveSeat(config, options.role, credentials);
298
284
  const env = {
299
285
  ...(await createSummonEnv({
300
286
  role: options.role,
@@ -420,47 +406,6 @@ export async function summonPublicRole(
420
406
  }
421
407
 
422
408
  /** Gate officer summons: notary/auditor via --source-run; inspector via pointer instruction. */
423
- /**
424
- * Read parent invocation host so nested officers inherit live --host only (#645).
425
- * Gate source runs necessarily own invocation.json — missing page, bad JSON, or
426
- * bad shape stay loud (failure-honesty). Absent host field is lawful (seat default).
427
- */
428
- async function parentInvocationHost(sourceRunDirectory: string): Promise<string | undefined> {
429
- const { readFile } = await import("node:fs/promises");
430
- const path = join(sourceRunDirectory, "invocation.json");
431
- let text: string;
432
- try {
433
- text = await readFile(path, "utf8");
434
- } catch (error) {
435
- const code =
436
- error instanceof Error && "code" in error
437
- ? (error as NodeJS.ErrnoException).code
438
- : undefined;
439
- throw new Error(
440
- `parent invocation.json required for gate source run at ${path}: ${code ?? (error instanceof Error ? error.message : String(error))}`,
441
- { cause: error },
442
- );
443
- }
444
- let raw: unknown;
445
- try {
446
- raw = JSON.parse(text) as unknown;
447
- } catch (error) {
448
- throw new Error(
449
- `parent invocation.json unreadable at ${path}: ${error instanceof Error ? error.message : String(error)}`,
450
- { cause: error },
451
- );
452
- }
453
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
454
- throw new Error(`parent invocation.json has non-object shape at ${path}`);
455
- }
456
- const host = (raw as Record<string, unknown>).host;
457
- if (host === undefined) return undefined;
458
- if (typeof host !== "string" || host.trim() === "") {
459
- throw new Error(`parent invocation.json host must be a non-empty string at ${path}`);
460
- }
461
- return host;
462
- }
463
-
464
409
  export async function summonGateOfficer(options: {
465
410
  readonly officer: "inspector" | "notary" | "auditor";
466
411
  readonly sourceRunDirectory: string;
@@ -501,9 +446,8 @@ export async function summonGateOfficer(options: {
501
446
  submission: options.submission,
502
447
  });
503
448
  }
504
- // Inherit parent live --host only so review roundtrip stays on one host without
505
- // mutating persistent seat tables (#645). Officer model/thinking stay seat-owned.
506
- const parentHost = await parentInvocationHost(options.sourceRunDirectory);
449
+ // Officer host is seat-owned only (#821). Parent invocation.json.host stays a
450
+ // hostTransition consumer in post-admission — not an officer override channel.
507
451
  const common = {
508
452
  cwd: options.cwd,
509
453
  ...(home === undefined ? {} : { home }),
@@ -514,7 +458,6 @@ export async function summonGateOfficer(options: {
514
458
  ...(gateReviewInstruction === undefined
515
459
  ? {}
516
460
  : { gateReviewInstruction }),
517
- ...(parentHost === undefined ? {} : { host: parentHost }),
518
461
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
519
462
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
520
463
  } as const;
@@ -29,7 +29,17 @@ export class ReviewerDispatchExecutionError extends Error {
29
29
  this.name = "ReviewerDispatchExecutionError";
30
30
  }
31
31
  }
32
- export type ReviewerAgentRunner = { run(dispatch: AcceptedReviewerExecution, options: { context: HostContext; signal?: AbortSignal }): Promise<ReviewerSuccessfulDispatchRunResult>; shutdown(): Promise<void> };
32
+ export type ReviewerDispatchRunOptions = Readonly<{
33
+ context: HostContext;
34
+ signal?: AbortSignal;
35
+ /**
36
+ * Request-scoped RoleHost flag reader (#818).
37
+ * Forwards to axis sub-session so envelope legs arm engine from request.engine,
38
+ * not ambient process.env (same resolveEngineName gate as parent registration).
39
+ */
40
+ getFlag?: (name: string) => boolean | string | undefined;
41
+ }>;
42
+ export type ReviewerAgentRunner = { run(dispatch: AcceptedReviewerExecution, options: ReviewerDispatchRunOptions): Promise<ReviewerSuccessfulDispatchRunResult>; shutdown(): Promise<void> };
33
43
  export type ReviewerAgentFaultPoint = ReviewerWorkspaceFaultPoint;
34
44
  type Dependencies = Readonly<{
35
45
  fault?(operation: ReviewerAgentFaultPoint): void;
@@ -81,6 +91,7 @@ export function createReviewerAgentRunner(dependencies: Dependencies = {}): Revi
81
91
  ...(dependencies.packageRoot === undefined
82
92
  ? {}
83
93
  : { packageRoot: dependencies.packageRoot }),
94
+ ...(options.getFlag === undefined ? {} : { getFlag: options.getFlag }),
84
95
  });
85
96
  const disposition = await workspaceOwner.dispose(workspace);
86
97
  return [leg.axis, Object.freeze({ status: "successful" as const, report: child.report, usage: child.usage, target: batch.target, prompt: child.prompt, workspaceDisposition: disposition })] as const;
@@ -9,7 +9,7 @@ import type { Usage } from "@earendil-works/pi-ai";
9
9
 
10
10
  import type { HostContext, RoleTurnModelConfig } from "./host-contracts.ts";
11
11
  import { createEngineDetourToolDefinition } from "./engine-detour-tool.ts";
12
- import { engineNameFromEnv } from "./engine-detour.ts";
12
+ import { resolveEngineName } from "./engine-detour.ts";
13
13
  import {
14
14
  appendEngineSessionMaterial,
15
15
  engineSessionMaterialFromOptions,
@@ -101,6 +101,13 @@ export type ReviewerChildExecuteOptions = Readonly<{
101
101
  credentialScratchParent?: string;
102
102
  /** Package root for optional engine method-material on legs (#378). */
103
103
  packageRoot?: string;
104
+ /**
105
+ * Request-scoped RoleHost flag reader (#818).
106
+ * Same single gate as registerEngineDetourTool / resolveEngineName:
107
+ * envelope projects request.engine here; absent → pi child-env fallback.
108
+ * Must not read ambient process.env as the primary signal on envelope legs.
109
+ */
110
+ getFlag?: (name: string) => boolean | string | undefined;
104
111
  }>;
105
112
 
106
113
  /**
@@ -145,7 +152,8 @@ export async function executeReviewerChild(
145
152
  const selection = await parentSelection(context);
146
153
  const { openPiInProcessSession } = await import("./pi/in-process-session.ts");
147
154
  const { createRecordSession } = await import("./archivist-record-entry.ts");
148
- const engineName = engineNameFromEnv();
155
+ // One gate with parent registration (#818): request flag, else pi child env.
156
+ const engineName = resolveEngineName(options.getFlag);
149
157
  const engineMaterial =
150
158
  engineName === undefined
151
159
  ? undefined
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Middle-layer role activation → host flag map (#819 / ADR 0082).
3
+ * Sole envelope-assembly definition for activation flags across host families.
4
+ * Host adapters only deliver these flags (ACP/headless getFlag map; pi argv pairs).
5
+ */
6
+ import type { RoleTurnRequest } from "./host-contracts.ts";
7
+ import { packagedRoleInputFlag, packagedRolePhaseFlag } from "./packaged-role-registry.ts";
8
+
9
+ /** Project closed RoleTurnActivation onto the shared host flag map. */
10
+ export function projectActivationFlags(request: RoleTurnRequest): Map<string, boolean | string> {
11
+ const activation = request.activation;
12
+ const flags = new Map<string, boolean | string>([["ak-role", activation.role]]);
13
+ const inputFlag = packagedRoleInputFlag(activation.role);
14
+ const phaseFlag = packagedRolePhaseFlag(activation.role);
15
+ if ("phase" in activation && phaseFlag !== undefined) flags.set(phaseFlag, activation.phase);
16
+ if (inputFlag !== undefined) {
17
+ const path = "taskPath" in activation ? activation.taskPath
18
+ : "packetPath" in activation ? activation.packetPath
19
+ : "casePath" in activation ? activation.casePath
20
+ : "inputPath" in activation ? activation.inputPath
21
+ : "sourceRun" in activation ? activation.sourceRun
22
+ : undefined;
23
+ if (path !== undefined) flags.set(inputFlag, path);
24
+ }
25
+ if (activation.role === "fixer" && activation.prerequisitesPath !== undefined) {
26
+ flags.set("ak-fixer-prerequisites", activation.prerequisitesPath);
27
+ }
28
+ if (activation.role === "reviewer") {
29
+ flags.set("ak-review-base", activation.baseRevision);
30
+ flags.set("ak-review-authority-refs", JSON.stringify(activation.authorityRefs));
31
+ if (activation.ticketNumber !== undefined) {
32
+ flags.set("ak-review-ticket-number", String(activation.ticketNumber));
33
+ }
34
+ }
35
+ // countersign ticketNumber stays on activation/admission/invocation only —
36
+ // no private transport flag (inner-gate material path deleted in #632).
37
+ if (activation.role === "notary" && activation.ticketNumber !== undefined) {
38
+ flags.set("ak-notary-ticket-number", String(activation.ticketNumber));
39
+ }
40
+ if (activation.role === "gleaner-left") {
41
+ flags.set("ak-gleaner-left-base", activation.baseRevision);
42
+ }
43
+ if (activation.role === "collector") {
44
+ flags.set("ak-collector-repo", activation.repo);
45
+ // #676 D1: pr optional at admission; omit flag when role binds from materials.
46
+ if (activation.pr !== undefined) flags.set("ak-collector-pr", activation.pr);
47
+ if (activation.requestManifestPath !== undefined) {
48
+ flags.set("ak-collector-request-manifest", activation.requestManifestPath);
49
+ }
50
+ if (activation.waitMs !== undefined) flags.set("ak-collector-wait-ms", activation.waitMs);
51
+ }
52
+ return flags;
53
+ }