@akagilnc/pi-workflow-roles 0.1.3826 → 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 (52) hide show
  1. package/dist/acp-host/production-host.js +22757 -22676
  2. package/dist/canonical-skill-binding.js +0 -3
  3. package/dist/engine-detour-tool.js +2 -2
  4. package/dist/engine-detour.js +23 -7
  5. package/dist/headless-host/production-host.js +4658 -4593
  6. package/dist/host-descriptions.js +5 -2
  7. package/dist/pi/adapter.js +11 -0
  8. package/dist/pi/role-turn-host.js +44 -93
  9. package/dist/public-cli/invocation.js +4 -5
  10. package/dist/public-cli/load-production-acp-host.js +1 -1
  11. package/dist/public-cli/load-production-external-host.js +10 -0
  12. package/dist/public-cli/load-production-headless-host.js +1 -1
  13. package/dist/public-cli/main.js +145 -140
  14. package/dist/public-role-summons.js +7 -49
  15. package/dist/reviewer-agent.js +1 -0
  16. package/dist/reviewer-child-executor.js +3 -2
  17. package/dist/role-activation-flags.js +50 -0
  18. package/dist/role-runtime.js +12 -10
  19. package/dist/worker-role.js +14 -12
  20. package/package.json +1 -1
  21. package/scripts/build-package.mjs +2 -2
  22. package/src/acp-host/production-host.ts +31 -94
  23. package/src/acp-host/role-turn-host.ts +178 -263
  24. package/src/canonical-skill-binding.ts +4 -4
  25. package/src/diagnostic-tail.ts +18 -0
  26. package/src/engine-detour-tool.ts +5 -4
  27. package/src/engine-detour.ts +44 -9
  28. package/src/external-host-turn-loop.ts +172 -0
  29. package/src/headless-host/production-host.ts +10 -7
  30. package/src/headless-host/role-turn-host.ts +271 -263
  31. package/src/host-contracts.ts +6 -0
  32. package/src/host-descriptions.ts +5 -2
  33. package/src/host-session-record.ts +41 -0
  34. package/src/package-resources/method-skill-binding.ts +0 -3
  35. package/src/pi/adapter.ts +13 -0
  36. package/src/pi/role-turn-host.ts +46 -94
  37. package/src/prepared-role-turn.ts +68 -0
  38. package/src/public-cli/cli.ts +11 -20
  39. package/src/public-cli/invocation.ts +4 -5
  40. package/src/public-cli/load-production-acp-host.ts +2 -1
  41. package/src/public-cli/load-production-external-host.ts +17 -0
  42. package/src/public-cli/load-production-headless-host.ts +2 -1
  43. package/src/public-role-summons.ts +12 -69
  44. package/src/reviewer-agent.ts +12 -1
  45. package/src/reviewer-child-executor.ts +10 -2
  46. package/src/role-activation-flags.ts +53 -0
  47. package/src/{acp-host/role-envelope.ts → role-envelope.ts} +41 -98
  48. package/src/role-runtime-dependencies.ts +91 -0
  49. package/src/role-runtime.ts +21 -11
  50. package/src/{acp-host/session-identity.ts → session-identity.ts} +6 -6
  51. package/src/worker-role.ts +14 -13
  52. /package/src/{acp-host/mcp-relay.mjs → mcp-relay.mjs} +0 -0
@@ -321,6 +321,12 @@ export type HostSkillExpansionEvidence = Readonly<{
321
321
  /** Host capability declaration (contract verb ④). */
322
322
  export type HostCapabilityDeclaration = Readonly<{
323
323
  skillExpansion(prompt: string): HostSkillExpansionEvidence | undefined;
324
+ /**
325
+ * Pi-only: recover the plain original request from a native skill turn text
326
+ * (argv may already carry `/skill:<name>` from the Pi adapter). Absent on
327
+ * non-pi hosts so role body never parses Pi slash syntax (ADR 0082).
328
+ */
329
+ skillOriginalRequest?(name: string, text: string): string;
324
330
  }>;
325
331
 
326
332
  /** Host-owned effects used by the shared activation envelope. */
@@ -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
+ }
@@ -46,9 +46,6 @@ export async function loadPackagedCanonicalSkillBinding<
46
46
  const binding: CanonicalSkillBinding<Name> = {
47
47
  name,
48
48
  snapshot,
49
- invocation(originalRequest) {
50
- return `/skill:${name} ${originalRequest}`;
51
- },
52
49
  captureExpansion(evidence, originalRequest) {
53
50
  return captureCanonicalSkillExpansion(
54
51
  name,
package/src/pi/adapter.ts CHANGED
@@ -157,6 +157,19 @@ export function createPiRoleHostAdapter(
157
157
  userMessage,
158
158
  });
159
159
  },
160
+ /** Recover plain original request from Pi-native `/skill:` turn text (ADR 0082). */
161
+ skillOriginalRequest(name, text) {
162
+ const token = `/skill:${name}`;
163
+ const trimmed = text.trimStart();
164
+ if (
165
+ trimmed === token
166
+ || trimmed.startsWith(`${token} `)
167
+ || trimmed.startsWith(`${token}\n`)
168
+ ) {
169
+ return text.slice(text.indexOf(token) + token.length).trim();
170
+ }
171
+ return text.trim();
172
+ },
160
173
  },
161
174
  registerFlag: (name, definition) => pi.registerFlag(name, definition),
162
175
  getFlag: (name) => pi.getFlag(name),
@@ -6,7 +6,7 @@
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { constants } from "node:fs";
8
8
  import { access, appendFile, readFile, realpath } from "node:fs/promises";
9
- import { delimiter, isAbsolute, join, resolve } from "node:path";
9
+ import { basename, delimiter, dirname, isAbsolute, join, resolve } from "node:path";
10
10
  import { platform } from "node:process";
11
11
  import { promisify } from "node:util";
12
12
  import { randomUUID } from "node:crypto";
@@ -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[] {
@@ -168,8 +91,35 @@ function buildMethodArgs(methods: readonly MethodBinding[]): string[] {
168
91
  }
169
92
 
170
93
  /**
171
- * Translate a closed RoleTurnRequest into Pi argv after `--no-extensions -e entry`.
172
- * Controlled session constants and session coordinates are adapter-internal.
94
+ * Pi-native `/skill:<name>` for a single forced method (ADR 0082 `pi-no-privilege`).
95
+ * Driven by typed RoleTurnRequest.methods — middle-layer decision, Pi-only syntax.
96
+ * Zero/many methods leave the prompt alone (Fixer optional pair stays `--skill` only).
97
+ */
98
+ export function applyPiNativeSkillInvocation(
99
+ methods: readonly MethodBinding[],
100
+ prompt: string,
101
+ ): string {
102
+ const skills = methods.filter((method) => method.kind === "skill");
103
+ if (skills.length !== 1) return prompt;
104
+ const name = basename(dirname(skills[0]!.path));
105
+ if (name.length === 0) return prompt;
106
+ const token = `/skill:${name}`;
107
+ const trimmed = prompt.trimStart();
108
+ if (
109
+ trimmed === token
110
+ || trimmed.startsWith(`${token} `)
111
+ || trimmed.startsWith(`${token}\n`)
112
+ ) {
113
+ return prompt;
114
+ }
115
+ return prompt.length === 0 ? token : `${token} ${prompt}`;
116
+ }
117
+
118
+ /**
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).
173
123
  */
174
124
  export function buildPiTurnExtraArgs(
175
125
  request: RoleTurnRequest,
@@ -177,13 +127,14 @@ export function buildPiTurnExtraArgs(
177
127
  extraPiArgs: readonly string[] = [],
178
128
  ): string[] {
179
129
  const { sessionFile, sessionDirectory } = authority.decode(request.principal);
180
- const prompt =
130
+ const rawPrompt =
181
131
  request.continuation.kind === "initial" || request.continuation.kind === "resume"
182
132
  ? request.continuation.prompt
183
133
  : (() => {
184
134
  const _exhaustive: never = request.continuation;
185
135
  return _exhaustive;
186
136
  })();
137
+ const prompt = applyPiNativeSkillInvocation(request.methods, rawPrompt);
187
138
  return [
188
139
  "--no-skills",
189
140
  ...buildMethodArgs(request.methods),
@@ -195,7 +146,8 @@ export function buildPiTurnExtraArgs(
195
146
  "--session-dir",
196
147
  sessionDirectory,
197
148
  ...extraPiArgs,
198
- ...buildActivationFlagArgs(request.activation),
149
+ // Envelope assembly = projectActivationFlags; pi only renders argv pairs.
150
+ ...activationFlagsToPiArgv(projectActivationFlags(request)),
199
151
  "--mode",
200
152
  "json",
201
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;
@@ -3071,16 +3071,15 @@ export async function admitMergerInvocation(
3071
3071
  }
3072
3072
 
3073
3073
  /**
3074
- * Build the Pi prompt transport for an admitted Merger request.
3075
- * Every invocation forces package merge-only method expansion before conflict work.
3074
+ * Build the host-neutral prompt transport for an admitted Merger request.
3075
+ * Method material binds on RoleTurnRequest.methods; Pi-native `/skill:` form
3076
+ * is adapter-internal only (ADR 0082).
3076
3077
  */
3077
3078
  export function buildMergerTransportPrompt(
3078
3079
  admitted: AdmittedMergerInvocation,
3079
3080
  engineMaterial?: EngineSessionMaterial,
3080
3081
  ): string {
3081
- const lines: string[] = [
3082
- `/skill:resolving-merge-conflicts ${admitted.instruction}`,
3083
- ];
3082
+ const lines: string[] = [admitted.instruction];
3084
3083
  if (admitted.attachments.length > 0) {
3085
3084
  lines.push("");
3086
3085
  lines.push("已受理附件(冻结快照路径):");
@@ -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;