@akagilnc/pi-workflow-roles 0.1.3771 → 0.1.3789

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 (40) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +793 -393
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/collector-handbook.js +142 -0
  6. package/dist/collector-ledger.js +39 -47
  7. package/dist/collector-role.js +103 -14
  8. package/dist/collector-tool-schemas.js +25 -1
  9. package/dist/headless-host/description.js +77 -0
  10. package/dist/headless-host/mcp-relay.mjs +119 -0
  11. package/dist/headless-host/production-host.js +26841 -0
  12. package/dist/host-descriptions.js +44 -3
  13. package/dist/public-cli/load-production-external-host.js +19 -0
  14. package/dist/public-cli/load-production-headless-host.js +36 -0
  15. package/dist/public-cli/main.js +244 -234
  16. package/dist/public-cli/option-definitions.js +2 -2
  17. package/dist/public-role-summons.js +40 -5
  18. package/dist/role-runtime.js +3 -0
  19. package/extensions/role-runtime.ts +2 -0
  20. package/package.json +1 -1
  21. package/resources/collector-bot-handbook.md +33 -0
  22. package/scripts/build-package.mjs +28 -0
  23. package/src/acp-host/production-host.ts +4 -0
  24. package/src/acp-host/role-envelope.ts +141 -84
  25. package/src/acp-host/role-turn-host.ts +12 -0
  26. package/src/collector-handbook.ts +194 -0
  27. package/src/collector-ledger.ts +42 -62
  28. package/src/collector-role.ts +117 -11
  29. package/src/collector-tool-schemas.ts +25 -1
  30. package/src/headless-host/description.ts +123 -0
  31. package/src/headless-host/production-host.ts +75 -0
  32. package/src/headless-host/role-turn-host.ts +424 -0
  33. package/src/host-descriptions.ts +53 -6
  34. package/src/public-cli/cli.ts +2 -2
  35. package/src/public-cli/load-production-external-host.ts +29 -0
  36. package/src/public-cli/load-production-headless-host.ts +48 -0
  37. package/src/public-cli/main.ts +5 -0
  38. package/src/public-cli/option-definitions.ts +2 -2
  39. package/src/public-role-summons.ts +62 -9
  40. package/src/role-runtime.ts +5 -0
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Packaged host description table (#729 / #731).
3
- * Key = seat-table `host` value; the row is the generic ACP factory's input.
2
+ * Packaged host description tables (#729 / #731 / #645).
3
+ * Key = seat-table `host` value.
4
+ * - ACP family rows feed the generic ACP factory.
5
+ * - Headless CLI family rows feed the generic headless factory (#645; #646 codex next).
4
6
  * pi is the in-process default, not a row.
5
- * Unregistered names fail closed (#510); this table does not fallback.
7
+ * Unregistered names fail closed (#510); these tables do not fallback.
6
8
  */
7
9
  import type { AcpHostDescription } from "./acp-host/description.ts";
10
+ import type { HeadlessHostDescription } from "./headless-host/description.ts";
8
11
 
9
12
  /** Grok CLI reads vendor-private compat surfaces unless each is disabled by name. */
10
13
  const PRIVATE_COMPAT_ENV = Object.fromEntries(
@@ -15,6 +18,8 @@ const PRIVATE_COMPAT_ENV = Object.fromEntries(
15
18
 
16
19
  export const DEFAULT_ROLE_TURN_HOST = "pi" as const;
17
20
 
21
+ export type HostFamily = "acp" | "headless";
22
+
18
23
  export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = Object.freeze({
19
24
  /** Operator home `~/.grok`, native session/load resume, `agent [--model X] stdio`. */
20
25
  "grok-build": Object.freeze({
@@ -60,17 +65,59 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
60
65
  }),
61
66
  });
62
67
 
68
+ /**
69
+ * Headless CLI family (#645). Claude is the first row; codex (#646) adds another.
70
+ * fixedArgs: print mode, isolation without `--bare` (OAuth stays), full permissions.
71
+ * `--setting-sources` empty = load no user/project/local CLAUDE.md/hooks/skills
72
+ * (role envelope is delivered via `--system-prompt` wholesale replace).
73
+ * `--strict-mcp-config` with no `--mcp-config` drops operator MCP + claude.ai connectors.
74
+ */
75
+ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDescription>> = Object.freeze({
76
+ "claude": Object.freeze({
77
+ binaryFromHome: Object.freeze([".local", "bin", "claude"]),
78
+ sessionBindingFile: "claude-headless-session.json",
79
+ fixedArgs: Object.freeze([
80
+ // One result envelope (not stream-json): typed receipt only; no event-stream copy.
81
+ "--output-format", "json",
82
+ "--permission-mode", "bypassPermissions",
83
+ // Empty sources: no user/project/local operator surface (envelope owns materials).
84
+ "--setting-sources", "",
85
+ // With adapter-supplied --mcp-config only (AK relay); drops operator + claude.ai MCP.
86
+ "--strict-mcp-config",
87
+ ]),
88
+ promptFlag: "-p",
89
+ modelFlag: "--model",
90
+ effortFlag: "--effort",
91
+ // File path keeps large role envelopes off ARG_MAX.
92
+ systemPromptFlag: "--system-prompt-file",
93
+ jsonSchemaFlag: "--json-schema",
94
+ mcpConfigFlag: "--mcp-config",
95
+ sessionIdFlag: "--session-id",
96
+ resumeFlag: "--resume",
97
+ }),
98
+ });
99
+
63
100
  export function lookupHostDescription(host: string): AcpHostDescription | undefined {
64
101
  return Object.hasOwn(HOST_DESCRIPTIONS, host) ? HOST_DESCRIPTIONS[host] : undefined;
65
102
  }
66
103
 
104
+ export function lookupHeadlessHostDescription(host: string): HeadlessHostDescription | undefined {
105
+ return Object.hasOwn(HEADLESS_HOST_DESCRIPTIONS, host) ? HEADLESS_HOST_DESCRIPTIONS[host] : undefined;
106
+ }
107
+
108
+ export function lookupHostFamily(host: string): HostFamily | undefined {
109
+ if (lookupHostDescription(host) !== undefined) return "acp";
110
+ if (lookupHeadlessHostDescription(host) !== undefined) return "headless";
111
+ return undefined;
112
+ }
113
+
67
114
  export function packagedExternalHostNames(): readonly string[] {
68
- return Object.keys(HOST_DESCRIPTIONS);
115
+ return [...Object.keys(HOST_DESCRIPTIONS), ...Object.keys(HEADLESS_HOST_DESCRIPTIONS)];
69
116
  }
70
117
 
71
- /** Legal persistent/invocation host: default pi, or a table key. */
118
+ /** Legal persistent/invocation host: default pi, or a table key (any family). */
72
119
  export function assertRegisteredHostName(host: string): string {
73
- if (host === DEFAULT_ROLE_TURN_HOST || lookupHostDescription(host) !== undefined) {
120
+ if (host === DEFAULT_ROLE_TURN_HOST || lookupHostFamily(host) !== undefined) {
74
121
  return host;
75
122
  }
76
123
  throw new Error(`unregistered host: ${host}`);
@@ -45,7 +45,7 @@ import type { CliIo } from "./cli-io.ts";
45
45
  import type { PostAdmissionEnv } from "./post-admission.ts";
46
46
  import type { RoleTurnHost, RoleTurnRequest } from "../host-contracts.ts";
47
47
  import { packagedExternalHostNames } from "../host-descriptions.ts";
48
- import { loadProductionAcpHostFactory } from "./load-production-acp-host.ts";
48
+ import { loadProductionExternalHostFactory } from "./load-production-external-host.ts";
49
49
  import {
50
50
  createPiRoleTurnHost,
51
51
  appendPiSessionCustomEntry,
@@ -347,7 +347,7 @@ function resolveRoleTurnHost(
347
347
  ok: true as const,
348
348
  host: {
349
349
  executeTurn: async (request: RoleTurnRequest) => {
350
- hostPromise ??= loadProductionAcpHostFactory(env.packageRoot, name).then((create) =>
350
+ hostPromise ??= loadProductionExternalHostFactory(env.packageRoot, name).then((create) =>
351
351
  create({
352
352
  packageRoot: env.packageRoot,
353
353
  principalAuthority: options.principalAuthority,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Family-dispatch loader for packaged external RoleTurnHost factories (#645).
3
+ * ACP rows → production ACP host; headless rows → production headless host.
4
+ * Unregistered names fail closed before any import.
5
+ */
6
+ import { lookupHostFamily } from "../host-descriptions.ts";
7
+ import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
8
+ import { loadProductionAcpHostFactory } from "./load-production-acp-host.ts";
9
+ import { loadProductionHeadlessHostFactory } from "./load-production-headless-host.ts";
10
+
11
+ export type ProductionExternalHostFactory = (options: {
12
+ packageRoot: string;
13
+ principalAuthority: DurablePrincipalAuthority;
14
+ }) => RoleTurnHost;
15
+
16
+ /** Resolve the production host factory for one registered external host key. */
17
+ export async function loadProductionExternalHostFactory(
18
+ packageRoot: string,
19
+ host: string,
20
+ ): Promise<ProductionExternalHostFactory> {
21
+ const family = lookupHostFamily(host);
22
+ if (family === "acp") {
23
+ return loadProductionAcpHostFactory(packageRoot, host);
24
+ }
25
+ if (family === "headless") {
26
+ return loadProductionHeadlessHostFactory(packageRoot, host);
27
+ }
28
+ throw new Error(`unregistered host: ${host}`);
29
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Deferred loader for the packaged generic headless RoleTurnHost factory (#645).
3
+ *
4
+ * Public ak-role bin must not statically value-import the production host (or its
5
+ * role-runtime edges). Specifier is runtime-constructed so esbuild leaves this
6
+ * import external (ADR 0052 discovery stays peer-free).
7
+ */
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { pathToFileURL } from "node:url";
11
+
12
+ import type { HeadlessHostDescription } from "../headless-host/description.ts";
13
+ import { lookupHeadlessHostDescription } from "../host-descriptions.ts";
14
+ import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
15
+
16
+ export type ProductionHeadlessHostFactory = (options: {
17
+ packageRoot: string;
18
+ principalAuthority: DurablePrincipalAuthority;
19
+ }) => RoleTurnHost;
20
+
21
+ type GenericHeadlessHostFactory = (options: {
22
+ packageRoot: string;
23
+ principalAuthority: DurablePrincipalAuthority;
24
+ description: HeadlessHostDescription;
25
+ }) => RoleTurnHost;
26
+
27
+ /**
28
+ * Resolve the production headless host factory for one registered headless host
29
+ * key without a static graph edge into the public CLI bundle.
30
+ */
31
+ export async function loadProductionHeadlessHostFactory(
32
+ packageRoot: string,
33
+ host: string,
34
+ ): Promise<ProductionHeadlessHostFactory> {
35
+ const description = lookupHeadlessHostDescription(host);
36
+ if (description === undefined) {
37
+ throw new Error(`unregistered headless host: ${host}`);
38
+ }
39
+ const built = join(packageRoot, "dist/headless-host/production-host.js");
40
+ const source = join(packageRoot, "src/headless-host/production-host.ts");
41
+ const target = existsSync(built) ? built : source;
42
+ const href = pathToFileURL(target).href;
43
+ const mod = (await import(href)) as {
44
+ createProductionHeadlessRoleTurnHost: GenericHeadlessHostFactory;
45
+ };
46
+ const create = mod.createProductionHeadlessRoleTurnHost;
47
+ return (options) => create({ ...options, description });
48
+ }
@@ -26,6 +26,11 @@ function resolvePackageRoot(binDir: string): string {
26
26
 
27
27
  const packageRoot = resolvePackageRoot(here);
28
28
 
29
+ // Nested institutional summons resolve package root from this env (#675 / #645).
30
+ // Pin to the running bin's package so a stale outer install pin cannot load a
31
+ // host family this tree owns but the older install lacks.
32
+ process.env.AK_ROLE_PACKAGE_ROOT = packageRoot;
33
+
29
34
  // The host-provided runtime must be resolvable before the CLI module graph loads it.
30
35
  ensureHostPiRuntimeResolvable(packageRoot);
31
36
 
@@ -598,8 +598,8 @@ const COLLECTOR_OPTIONS = [
598
598
  repeatable: false,
599
599
  form: "option",
600
600
  description: {
601
- en: "Optional request manifest JSON path ({requests:[{id,body}]}).",
602
- zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。",
601
+ en: "Optional request manifest JSON path ({requests:[{id,body}]}). Role may also decide request bodies from handbook/field activity without this file.",
602
+ zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。角色亦可依手册/现场自行判定请求正文,不必依赖本文件。",
603
603
  },
604
604
  },
605
605
  ] as const satisfies readonly PublicOptionDefinition[];
@@ -65,6 +65,12 @@ 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;
68
74
  /**
69
75
  * Offline test inject — same face as public CLI env.roleTurnHost. Production
70
76
  * summons leave this unset and use the seat-resolved host.
@@ -211,14 +217,13 @@ async function createSummonEnv(options: {
211
217
  const hostName = options.seat.host ?? "pi";
212
218
  let roleTurnHost = piHost;
213
219
  if (hostName !== "pi") {
214
- const { lookupHostDescription } = await import("./host-descriptions.ts");
215
- const { loadProductionAcpHostFactory } = await import(
216
- "./public-cli/load-production-acp-host.ts"
220
+ const { lookupHostFamily } = await import("./host-descriptions.ts");
221
+ const { loadProductionExternalHostFactory } = await import(
222
+ "./public-cli/load-production-external-host.ts"
217
223
  );
218
- // #729: the description table is the sole host authority — every registered
219
- // key loads the same generic ACP factory (no per-host fork). Unregistered
220
- // keys fail closed before any turn (#510), as the pre-merge fork did.
221
- if (lookupHostDescription(hostName) === undefined) {
224
+ // #729 / #645: description tables are the sole host authority — family
225
+ // dispatch (ACP vs headless) is data-driven. Unregistered keys fail closed.
226
+ if (lookupHostFamily(hostName) === undefined) {
222
227
  throw new Error(
223
228
  `public role summons host unregistered: host=${hostName} seat=${options.role}`,
224
229
  );
@@ -226,7 +231,7 @@ async function createSummonEnv(options: {
226
231
  let hostPromise: Promise<RoleTurnHost> | undefined;
227
232
  roleTurnHost = {
228
233
  executeTurn: async (request) => {
229
- hostPromise ??= loadProductionAcpHostFactory(options.packageRoot, hostName).then(
234
+ hostPromise ??= loadProductionExternalHostFactory(options.packageRoot, hostName).then(
230
235
  (create) =>
231
236
  create({
232
237
  packageRoot: options.packageRoot,
@@ -286,7 +291,10 @@ export async function summonPublicRole(
286
291
  const credentials =
287
292
  options.credentials ?? (await loadCredentialProviders(agentDir));
288
293
  const config = await loadPublicCliConfig(home);
289
- const seat = resolveEffectiveSeat(config, options.role, credentials);
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);
290
298
  const env = {
291
299
  ...(await createSummonEnv({
292
300
  role: options.role,
@@ -412,6 +420,47 @@ export async function summonPublicRole(
412
420
  }
413
421
 
414
422
  /** 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
+
415
464
  export async function summonGateOfficer(options: {
416
465
  readonly officer: "inspector" | "notary" | "auditor";
417
466
  readonly sourceRunDirectory: string;
@@ -452,6 +501,9 @@ export async function summonGateOfficer(options: {
452
501
  submission: options.submission,
453
502
  });
454
503
  }
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);
455
507
  const common = {
456
508
  cwd: options.cwd,
457
509
  ...(home === undefined ? {} : { home }),
@@ -462,6 +514,7 @@ export async function summonGateOfficer(options: {
462
514
  ...(gateReviewInstruction === undefined
463
515
  ? {}
464
516
  : { gateReviewInstruction }),
517
+ ...(parentHost === undefined ? {} : { host: parentHost }),
465
518
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
466
519
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
467
520
  } as const;
@@ -611,6 +611,8 @@ export type RoleRuntimeDependencies = {
611
611
  /** Shared-seam issue-fetch capability for Reviewer Spec self-fetch (#343). */
612
612
  createReviewerIssueFetcher?(): ReviewerIssueFetcher;
613
613
  loadCollectorSoul?(): Promise<string>;
614
+ /** #677: optional packaged seed for first-use general bot handbook. */
615
+ loadCollectorHandbookSeed?(): Promise<string>;
614
616
  createCollectorTransport?(): CollectorGitHubTransport;
615
617
  loadDoctorSoul?(): Promise<string>;
616
618
  loadNotarySoul?(): Promise<string>;
@@ -1717,6 +1719,9 @@ export function createRoleRuntimeExtension(
1717
1719
  ...(dependencies.createCollectorClock === undefined
1718
1720
  ? {}
1719
1721
  : { createClock: dependencies.createCollectorClock }),
1722
+ ...(dependencies.loadCollectorHandbookSeed === undefined
1723
+ ? {}
1724
+ : { loadHandbookSeed: dependencies.loadCollectorHandbookSeed }),
1720
1725
  },
1721
1726
  hostActions,
1722
1727
  );