@akagilnc/pi-workflow-roles 0.1.4444 → 0.1.4489

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 (44) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/description.js +2 -3
  4. package/dist/acp-host/production-host.js +140 -107
  5. package/dist/auditor-soul.js +8 -1
  6. package/dist/headless-host/description.js +3 -3
  7. package/dist/headless-host/production-host.js +147 -76
  8. package/dist/host-descriptions.js +3 -18
  9. package/dist/method-host-plugin/.claude-plugin/plugin.json +5 -0
  10. package/dist/method-host-plugin/skills/ak-cross-m-review/CONTEXT.md +48 -0
  11. package/dist/method-host-plugin/skills/ak-cross-m-review/LICENSE +21 -0
  12. package/dist/method-host-plugin/skills/ak-cross-m-review/SKILL.md +170 -0
  13. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-completeness.md +118 -0
  14. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-reviewer.md +128 -0
  15. package/dist/method-host-plugin/skills/ak-cross-m-review/provenance.json +41 -0
  16. package/dist/method-host-plugin/skills/diagnosing-bugs/SKILL.md +134 -0
  17. package/dist/method-host-plugin/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  18. package/dist/method-host-plugin/skills/diagnosing-bugs/provenance.json +31 -0
  19. package/dist/method-host-plugin/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  20. package/dist/method-host-plugin/skills/resolving-merge-conflicts/SKILL.md +14 -0
  21. package/dist/method-host-plugin/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  22. package/dist/method-host-plugin/skills/resolving-merge-conflicts/provenance.json +26 -0
  23. package/dist/method-host-plugin/skills/tdd/SKILL.md +38 -0
  24. package/dist/method-host-plugin/skills/tdd/agents/openai.yaml +3 -0
  25. package/dist/method-host-plugin/skills/tdd/mocking.md +59 -0
  26. package/dist/method-host-plugin/skills/tdd/provenance.json +36 -0
  27. package/dist/method-host-plugin/skills/tdd/tests.md +77 -0
  28. package/dist/public-cli/main.js +14 -21
  29. package/dist/session-opening-materials.js +17 -5
  30. package/extensions/role-runtime.ts +16 -6
  31. package/package.json +1 -1
  32. package/resources/method-host-plugin/.claude-plugin/plugin.json +5 -0
  33. package/scripts/build-package.mjs +6 -1
  34. package/src/acp-host/description.ts +2 -4
  35. package/src/acp-host/production-host.ts +0 -1
  36. package/src/auditor-soul.ts +10 -1
  37. package/src/headless-host/description.ts +4 -3
  38. package/src/headless-host/role-turn-host.ts +27 -4
  39. package/src/host-descriptions.ts +3 -23
  40. package/src/host-native-method.ts +67 -0
  41. package/src/role-envelope.ts +17 -47
  42. package/src/role-runtime-dependencies.ts +17 -2
  43. package/src/role-runtime.ts +12 -7
  44. package/src/session-opening-materials.ts +27 -11
@@ -23,6 +23,12 @@ import {
23
23
  } from "../prepared-role-turn.ts";
24
24
 
25
25
  import { reportHostSessionEvent } from "../host-session-record.ts";
26
+ import {
27
+ applyClaudeSkillInvocation,
28
+ applyCodexSkillInvocation,
29
+ hostMethodSkills,
30
+ packagedMethodPluginDir,
31
+ } from "../host-native-method.ts";
26
32
  import {
27
33
  closeJsonSchemaForCodex,
28
34
  codexTurnArgs,
@@ -410,6 +416,7 @@ function buildTurnArgs(options: {
410
416
  readonly sessionKind: "new" | "resume";
411
417
  readonly cwd: string;
412
418
  readonly writableRoots?: readonly string[];
419
+ readonly pluginDir?: string;
413
420
  }): readonly string[] {
414
421
  if (isCodexExecDescription(options.description)) {
415
422
  if (options.sessionKind === "resume" && !options.sessionId) {
@@ -441,6 +448,7 @@ function buildTurnArgs(options: {
441
448
  ...(options.model === undefined ? {} : { model: options.model }),
442
449
  ...(options.effort === undefined ? {} : { effort: options.effort }),
443
450
  session: { kind: options.sessionKind, id: options.sessionId },
451
+ ...(options.pluginDir === undefined ? {} : { pluginDir: options.pluginDir }),
444
452
  });
445
453
  }
446
454
 
@@ -469,11 +477,17 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
469
477
  await writeFile(systemPromptPath, systemPrompt, "utf8");
470
478
  let mcpConfigPath: string | undefined;
471
479
  let outputSchemaPath: string | undefined;
480
+ let pluginDir: string | undefined;
481
+ let applyMethodPrompt: (prompt: string) => string = codex
482
+ ? (prompt) => applyCodexSkillInvocation(request.methods, prompt)
483
+ : (prompt) => prompt;
472
484
  if (codex) {
473
- // Codex --output-schema needs a closed transport projection on disk.
474
485
  outputSchemaPath = join(request.runDirectory, "headless-output-schema.json");
475
- const closed = closeJsonSchemaForCodex(prepared.jsonSchema);
476
- await writeFile(outputSchemaPath, `${JSON.stringify(closed, null, 2)}\n`, "utf8");
486
+ await writeFile(
487
+ outputSchemaPath,
488
+ `${JSON.stringify(closeJsonSchemaForCodex(prepared.jsonSchema), null, 2)}\n`,
489
+ "utf8",
490
+ );
477
491
  } else {
478
492
  mcpConfigPath = join(request.runDirectory, "headless-mcp-config.json");
479
493
  await writeFile(
@@ -481,6 +495,14 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
481
495
  `${JSON.stringify(headlessMcpConfigDocument(prepared.mcpServers), null, 2)}\n`,
482
496
  "utf8",
483
497
  );
498
+ if (hostMethodSkills(request.methods).length > 0) {
499
+ const packageRoot = env.AK_PACKAGE_ROOT ?? process.env.AK_PACKAGE_ROOT;
500
+ if (typeof packageRoot !== "string" || packageRoot === "") {
501
+ throw new Error("claude method plugin-dir requires AK_PACKAGE_ROOT");
502
+ }
503
+ pluginDir = packagedMethodPluginDir(packageRoot);
504
+ applyMethodPrompt = (prompt) => applyClaudeSkillInvocation(request.methods, prompt);
505
+ }
484
506
  }
485
507
 
486
508
  const sessionParent = config.sessionIdentity.resolveSessionFile(request.principal);
@@ -505,6 +527,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
505
527
  sessionKind,
506
528
  cwd: request.cwd,
507
529
  ...(gitCommonDir === undefined ? {} : { writableRoots: [gitCommonDir] }),
530
+ ...(pluginDir === undefined ? {} : { pluginDir }),
508
531
  });
509
532
  } catch (error) {
510
533
  const message = error instanceof Error ? error.message : String(error);
@@ -522,7 +545,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
522
545
  args,
523
546
  cwd: request.cwd,
524
547
  env,
525
- stdin: prompt,
548
+ stdin: applyMethodPrompt(prompt),
526
549
  ...(abortSignal === undefined ? {} : { signal: abortSignal }),
527
550
  ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
528
551
  onStdoutLine(line) {
@@ -9,13 +9,6 @@
9
9
  import type { AcpHostDescription } from "./acp-host/description.ts";
10
10
  import type { HeadlessHostDescription } from "./headless-host/description.ts";
11
11
 
12
- /** Grok CLI reads vendor-private compat surfaces unless each is disabled by name. */
13
- const PRIVATE_COMPAT_ENV = Object.fromEntries(
14
- ["CLAUDE", "CURSOR", "CODEX"].flatMap((vendor) =>
15
- ["SKILLS", "RULES", "AGENTS", "MCPS", "HOOKS", "SESSIONS"].map((kind) =>
16
- [`GROK_${vendor}_${kind}_ENABLED`, "false"] as const)),
17
- );
18
-
19
12
  export const DEFAULT_ROLE_TURN_HOST = "pi" as const;
20
13
 
21
14
  export type HostFamily = "acp" | "headless";
@@ -32,11 +25,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
32
25
  modelPassing: "argv",
33
26
  boundResume: "session/load",
34
27
  sessionBindingFile: "grok-acp-session.json",
35
- childEnv: Object.freeze({
36
- ...PRIVATE_COMPAT_ENV,
37
- GROK_MEMORY: "0",
38
- GROK_SUBAGENTS: "0",
39
- }),
40
28
  }),
41
29
  /**
42
30
  * Operator home `~/.hermes`, native session/load resume, `acp` subcommand.
@@ -55,7 +43,6 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
55
43
  modelPassing: "set_model",
56
44
  boundResume: "session/load",
57
45
  sessionBindingFile: "hermes-acp-session.json",
58
- childEnv: Object.freeze({}),
59
46
  seatProfileSoul: Object.freeze({
60
47
  flag: "-p",
61
48
  namePrefix: "ak-",
@@ -69,12 +56,9 @@ export const HOST_DESCRIPTIONS: Readonly<Record<string, AcpHostDescription>> = O
69
56
  * Headless CLI family (#645 / #646). Claude print-mode is the first row;
70
57
  * codex exec (#646) adds another. Protocol-specific argv/parse live in
71
58
  * headless-host helpers (#752 per-host impl).
72
- * Claude fixedArgs: print mode, isolation without `--bare` (OAuth stays), full
73
- * permissions. stream-json + verbose: live host events for sitian records
74
- * (#811); result is last line. `--setting-sources` empty = load no
75
- * user/project/local CLAUDE.md/hooks/skills (role envelope is delivered via
76
- * `--system-prompt` wholesale replace). `--strict-mcp-config` with no
77
- * `--mcp-config` drops operator MCP + claude.ai connectors.
59
+ * Claude fixedArgs: print mode, full permissions. stream-json + verbose: live
60
+ * host events for sitian records (#811); result is last line. Forced methods
61
+ * ride `--plugin-dir` (#922); operator skill/setting surfaces stay open.
78
62
  */
79
63
  export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDescription>> = Object.freeze({
80
64
  "claude": Object.freeze({
@@ -87,10 +71,6 @@ export const HEADLESS_HOST_DESCRIPTIONS: Readonly<Record<string, HeadlessHostDes
87
71
  // Intermediate assistant/tool/system events require verbose with stream-json.
88
72
  "--verbose",
89
73
  "--permission-mode", "bypassPermissions",
90
- // Empty sources: no user/project/local operator surface (envelope owns materials).
91
- "--setting-sources", "",
92
- // With adapter-supplied --mcp-config only (AK relay); drops operator + claude.ai MCP.
93
- "--strict-mcp-config",
94
74
  ]),
95
75
  promptFlag: "-p",
96
76
  modelFlag: "--model",
@@ -0,0 +1,67 @@
1
+ /** #922 host-native packaged method delivery. */
2
+ import { lstat, mkdir, readlink, realpath, symlink } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import type { MethodBinding } from "./host-contracts.ts";
5
+
6
+ type HostMethodSkill = Readonly<{ name: string }>;
7
+ export const HOST_METHOD_PLUGIN_NAME = "ak-methods" as const;
8
+ export const packagedMethodsDir = (root: string) => join(root, "resources", "methods");
9
+ export const packagedMethodPluginDir = (root: string) => join(root, "dist", "method-host-plugin");
10
+
11
+ export function hostMethodSkills(methods: readonly MethodBinding[]): readonly HostMethodSkill[] {
12
+ return Object.freeze(methods.flatMap((method) => {
13
+ if (method.kind !== "skill") return [];
14
+ const name = basename(dirname(method.path));
15
+ return name ? [Object.freeze({ name })] : [];
16
+ }));
17
+ }
18
+
19
+ const alreadyPrefixed = (prompt: string, token: string) => {
20
+ const text = prompt.trimStart();
21
+ return text === token || text.startsWith(`${token} `) || text.startsWith(`${token}\n`);
22
+ };
23
+
24
+ function prefixMethodInvocation(token: string, prompt: string): string {
25
+ if (alreadyPrefixed(prompt, token)) return prompt;
26
+ return prompt ? `${token} ${prompt}` : token;
27
+ }
28
+
29
+ /** Claude's documented plugin Skill invocation syntax. */
30
+ export function applyClaudeSkillInvocation(methods: readonly MethodBinding[], prompt: string): string {
31
+ const skills = hostMethodSkills(methods);
32
+ if (skills.length !== 1) return prompt;
33
+ return prefixMethodInvocation(`/${HOST_METHOD_PLUGIN_NAME}:${skills[0]!.name}`, prompt);
34
+ }
35
+
36
+ /** Codex's documented explicit Skill invocation syntax. */
37
+ export function applyCodexSkillInvocation(methods: readonly MethodBinding[], prompt: string): string {
38
+ return hostMethodSkills(methods).reduceRight(
39
+ (invocation, skill) => prefixMethodInvocation(`$${skill.name}`, invocation),
40
+ prompt,
41
+ );
42
+ }
43
+
44
+ /** Install the documented project Skill catalog once and leave it in place. */
45
+ export async function installWorkspaceMethodSkills(cwd: string, packageRoot: string): Promise<void> {
46
+ const target = await realpath(packagedMethodsDir(packageRoot));
47
+ const link = join(cwd, ".agents", "skills");
48
+ try {
49
+ const stat = await lstat(link);
50
+ if (!stat.isSymbolicLink() || await realpath(link) !== target) {
51
+ const detail = stat.isSymbolicLink() ? `symlink to ${await readlink(link)}` : "non-symlink entry";
52
+ throw new Error(`workspace method catalog conflict at ${link}: ${detail}`);
53
+ }
54
+ return;
55
+ } catch (error) {
56
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
57
+ }
58
+ await mkdir(dirname(link), { recursive: true });
59
+ try {
60
+ await symlink(target, link);
61
+ } catch (error) {
62
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
63
+ if ((await realpath(link).catch(() => "")) !== target) {
64
+ throw new Error(`workspace method catalog conflict at ${link}`);
65
+ }
66
+ }
67
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { createServer, type Server, type Socket } from "node:net";
4
- import { basename, dirname, join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  import {
@@ -13,15 +13,14 @@ import { requireGatekeeperPass } from "./gatekeeper-pass-envelope.ts";
13
13
  import type {
14
14
  HostContext,
15
15
  HostEventRegistration,
16
- HostSkillExpansionEvidence,
17
16
  HostToolDefinition,
18
17
  RoleEnvelopeHost,
19
18
  RoleHost,
20
19
  RoleTurnKnownFailure,
21
20
  RoleTurnRequest,
22
21
  } from "./host-contracts.ts";
22
+ import { hostMethodSkills, installWorkspaceMethodSkills } from "./host-native-method.ts";
23
23
  import { packagedRoleOutputTool } from "./packaged-role-registry.ts";
24
- import { stripSkillFrontmatter } from "./package-resources/method-skill.ts";
25
24
  import {
26
25
  createRoleRuntimeExtension,
27
26
  type RoleRuntimeDependencies,
@@ -44,30 +43,6 @@ type RpcRequest = { readonly id: number; readonly token: string; readonly method
44
43
  type ToolCallParams = { readonly name?: unknown; readonly arguments?: unknown };
45
44
  type ContentPart = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
46
45
 
47
- /**
48
- * Build host-side Skill expansion evidence from pre-read RoleTurnRequest.methods.
49
- * Non-pi hosts never parse Pi `/skill:` syntax (ADR 0082: pi is one adapter with
50
- * no privilege; Pi-only seams stay inside the pi adapter);
51
- * a single bound method treats the plain prompt as the preserved user message.
52
- * Pi-native slash forms stay inside `src/pi/` only.
53
- */
54
- export function buildSkillExpansion(
55
- methodSkills: ReadonlyMap<string, { readonly path: string; readonly body: string }>,
56
- prompt: string,
57
- ): HostSkillExpansionEvidence | undefined {
58
- if (methodSkills.size !== 1) return undefined;
59
- const entry = methodSkills.entries().next().value;
60
- if (entry === undefined) return undefined;
61
- const [name, method] = entry;
62
- return Object.freeze({
63
- name,
64
- location: method.path,
65
- content: method.body,
66
- // Non-pi typed chain keeps original task bytes (ticket #822 r3); no consumer trim.
67
- userMessage: prompt,
68
- });
69
- }
70
-
71
46
  async function listen(server: Server, path: string): Promise<void> {
72
47
  await new Promise<void>((resolve, reject) => {
73
48
  server.once("error", reject);
@@ -129,7 +104,6 @@ export async function prepareRoleEnvelope(options: {
129
104
  const customEntries: Array<{ customType: string; data: unknown }> = [];
130
105
  /** In-memory turn books for role lifecycle and audit subjects. */
131
106
  const sessionEntries: Array<Record<string, unknown>> = [];
132
- const methodSkills = new Map<string, { path: string; body: string }>();
133
107
  let preferredTools: string[] = [];
134
108
  let rejection:
135
109
  | { readonly code: string; readonly toolCallIds: readonly string[]; readonly message: string }
@@ -140,14 +114,6 @@ export async function prepareRoleEnvelope(options: {
140
114
  const runId = request.runDirectory.split("/").filter(Boolean).at(-1) ?? randomUUID();
141
115
  await mkdir(request.runDirectory, { recursive: true });
142
116
 
143
- // Canonical Skill expansion consumes RoleTurnRequest.methods (typed true source).
144
- for (const method of request.methods) {
145
- if (method.kind !== "skill") continue;
146
- const name = basename(dirname(method.path));
147
- const raw = await readFile(method.path, "utf8");
148
- methodSkills.set(name, { path: method.path, body: stripSkillFrontmatter(raw).trim() });
149
- }
150
-
151
117
  // Durable principal file for isAvailable / resumable settlement (public-cli).
152
118
  // #617 DK-4: header layout only — never host conversation/tool writeback into Pi JSONL.
153
119
  let sessionFile = options.sessionFile ?? join(request.runDirectory, "session", "session.jsonl");
@@ -219,9 +185,8 @@ export async function prepareRoleEnvelope(options: {
219
185
  // #836 删 1/A4.5: do not arm closeRound retry with「终局交卷并非本轮唯一工具调用」.
220
186
  },
221
187
  capabilities: {
222
- skillExpansion(prompt): HostSkillExpansionEvidence | undefined {
223
- return buildSkillExpansion(methodSkills, prompt);
224
- },
188
+ // #922: native loaders own expansion; package does not pre-read bodies (ADR 0032).
189
+ skillExpansion() { return undefined; },
225
190
  },
226
191
  registerFlag(name, definition) { if (!flags.has(name) && definition.default !== undefined) flags.set(name, definition.default); },
227
192
  getFlag(name) { return flags.get(name); },
@@ -625,14 +590,10 @@ export async function prepareRoleEnvelope(options: {
625
590
  message: { role: "user", content: prompt },
626
591
  });
627
592
  }
628
- // Method notes only here — role before_agent_start injects soul once.
629
- // Preloading session materials duplicated soul under the role tag (#632).
630
- // #879: case-dossier owner reads runDirectory from this turn's HostContext.
631
-
632
- const methodPrompt = (await Promise.all(request.methods.map(({ path }) => readFile(path, "utf8")))).join("\n\n");
593
+ // Soul/materials only — forced methods use host-native loaders (#922/#632/#879).
633
594
  const promptResults = await emit("before_agent_start", {
634
595
  prompt,
635
- systemPrompt: methodPrompt,
596
+ systemPrompt: "",
636
597
  systemPromptOptions: {},
637
598
  });
638
599
  const systemPromptParts = promptResults.flatMap((value) => {
@@ -640,7 +601,7 @@ export async function prepareRoleEnvelope(options: {
640
601
  if (!("systemPrompt" in value) || typeof value.systemPrompt !== "string") return [];
641
602
  return [value.systemPrompt];
642
603
  });
643
- const systemPromptBody = systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : methodPrompt;
604
+ const systemPromptBody = systemPromptParts.join("\n\n");
644
605
  // Typed reading materials from agent-start handlers (incl. single shared
645
606
  // case-dossier owner). Folded into provider-visible systemPrompt at send.
646
607
  const readingMaterials: unknown[] = [];
@@ -656,6 +617,15 @@ export async function prepareRoleEnvelope(options: {
656
617
  throw new Error(`terminating tool not registered after activation: ${terminatingToolName}`);
657
618
  }
658
619
  const jsonSchema = terminatingToolJsonSchema(terminating.parameters);
620
+
621
+ if (request.host === "codex" && hostMethodSkills(request.methods).length > 0) {
622
+ const packageRoot = options.dependencies.packageRoot;
623
+ if (typeof packageRoot !== "string" || packageRoot === "") {
624
+ throw new Error("codex project Skill catalog requires packageRoot");
625
+ }
626
+ await installWorkspaceMethodSkills(request.cwd, packageRoot);
627
+ }
628
+
659
629
  return {
660
630
  mcpServers: [{
661
631
  name: `ak-${request.activation.role}`,
@@ -10,8 +10,15 @@ import { loadNavigatorWorkContext } from "./navigator-work-context.ts";
10
10
  import { loadNotarySourceRunLocator } from "./notary-source-run.ts";
11
11
  import { loadPackagedCanonicalSkillBinding } from "./package-resources/method-skill-binding.ts";
12
12
  import { formatNavigatorRoleHelp, type RoleRuntimeDependencies } from "./role-runtime.ts";
13
- import { loadAuditorSoulFromSubjectInput } from "./auditor-soul.ts";
14
- import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "./session-opening-materials.ts";
13
+ import {
14
+ loadAuditorReferenceMaterialsFromSubjectInput,
15
+ loadAuditorSoulFromSubjectInput,
16
+ } from "./auditor-soul.ts";
17
+ import {
18
+ loadGatekeeperSessionMaterials,
19
+ loadMainRoleReferenceMaterials,
20
+ loadMainRoleSessionMaterials,
21
+ } from "./session-opening-materials.ts";
15
22
 
16
23
  const navigatorRoutePlaybookPath = fileURLToPath(
17
24
  new URL("../resources/navigator-route-playbook.md", import.meta.url),
@@ -20,12 +27,20 @@ const collectorHandbookSeedPath = fileURLToPath(
20
27
  new URL("../resources/collector-bot-handbook.md", import.meta.url),
21
28
  );
22
29
 
30
+ /** Single packaged source for reference materials across production composition roots. */
31
+ export function loadPackagedRoleReferenceMaterials(role: Parameters<NonNullable<RoleRuntimeDependencies["loadRoleReferenceMaterials"]>>[0]): Promise<string> {
32
+ return role === "auditor"
33
+ ? loadAuditorReferenceMaterialsFromSubjectInput()
34
+ : loadMainRoleReferenceMaterials(role);
35
+ }
36
+
23
37
  /** Host-neutral packaged role runtime deps for the parent-process envelope. */
24
38
  export function createRoleRuntimeDependencies(packageRoot: string): RoleRuntimeDependencies {
25
39
  const doctorAuditor = createPiDoctorAuditor();
26
40
  const navigatorSessionFactory = createNativeNavigatorSessionFactory();
27
41
  return {
28
42
  packageRoot,
43
+ loadRoleReferenceMaterials: loadPackagedRoleReferenceMaterials,
29
44
  loadJudgeSoul: () => loadMainRoleSessionMaterials("judge"),
30
45
  loadFixerSoul: () => loadMainRoleSessionMaterials("fixer"),
31
46
  loadFixPacket: (path) => readFile(path, "utf8"),
@@ -580,6 +580,8 @@ type NavigatorAttendanceDependency = Omit<NavigatorAttendance, "knownRoutePlaybo
580
580
  export type RoleRuntimeDependencies = {
581
581
  /** Package root for packaged engine-note resolution (#879). */
582
582
  packageRoot?: string;
583
+ /** Non-identity opening materials delivered in the ordinary role brief. */
584
+ loadRoleReferenceMaterials?(role: PackagedRole): Promise<string>;
583
585
  loadJudgeSoul(): Promise<string>;
584
586
  loadFixerSoul?(): Promise<string>;
585
587
  loadFixPacket?(path: string): Promise<string>;
@@ -1266,7 +1268,8 @@ export function createRoleRuntimeExtension(
1266
1268
  });
1267
1269
 
1268
1270
  let admitted = false;
1269
- let selectedRole: string | undefined;
1271
+ let selectedRole: PackagedRole | undefined;
1272
+ let roleReferenceMaterials = "";
1270
1273
  /** Live Reviewer parent activation for envelope agent_start prompt assembly. */
1271
1274
  let activeReviewerParent: ReviewerActivation | undefined;
1272
1275
  /** Envelope-owned Reviewer Skill expansion state (ADR 0018 — not a role-module facade). */
@@ -1379,14 +1382,14 @@ export function createRoleRuntimeExtension(
1379
1382
  activeReviewerParent.skillBinding.name,
1380
1383
  text,
1381
1384
  ) ?? text;
1382
- return text === event.text
1383
- ? { action: "continue" as const }
1384
- : { action: "transform" as const, text };
1385
1385
  }
1386
- return text === event.text
1387
- ? { action: "continue" as const }
1388
- : { action: "transform" as const, text };
1386
+ return { action: "continue" as const };
1389
1387
  });
1388
+ // Reference law/guides use the existing typed reading-material channel;
1389
+ // they are not rewritten into operator dialogue or the identity Soul.
1390
+ roleHost.on("before_agent_start", () => roleReferenceMaterials === ""
1391
+ ? undefined
1392
+ : { readingMaterial: { kind: "role-reference-materials", content: roleReferenceMaterials } });
1390
1393
  roleHost.on("before_agent_start", async (event, ctx) => {
1391
1394
  const role = roleHost.getFlag(ROLE_FLAG.name);
1392
1395
  const prompt = event.prompt;
@@ -2070,6 +2073,7 @@ export function createRoleRuntimeExtension(
2070
2073
  }
2071
2074
  admitted = false;
2072
2075
  selectedRole = undefined;
2076
+ roleReferenceMaterials = "";
2073
2077
  activeReviewerParent = undefined;
2074
2078
  reviewerOriginalRequest = undefined;
2075
2079
  reviewerExpansionCaptured = false;
@@ -2230,6 +2234,7 @@ export function createRoleRuntimeExtension(
2230
2234
  }
2231
2235
 
2232
2236
  await executeActivationStage(entry.role, activationStage(entry.role, runtime), { clock, writeTrace });
2237
+ roleReferenceMaterials = await dependencies.loadRoleReferenceMaterials?.(entry.role) ?? "";
2233
2238
  // #357 T2 / #378 / #380 / #391 / #818: any role+engine activation registers the package detour tool once.
2234
2239
  // Gate is resolveEngineName (RoleHost flag → env fallback) — no per-engine execute branch; no role-module spawn.
2235
2240
  if (!engineDetourRegistered) {
@@ -50,12 +50,28 @@ export async function joinPackageMaterials(
50
50
  relativePaths: readonly string[],
51
51
  ): Promise<string> {
52
52
  const chunks: string[] = [];
53
- for (const relativePath of relativePaths) {
54
- chunks.push(await readPackageMaterial(relativePath));
55
- }
53
+ for (const relativePath of relativePaths) chunks.push(await readPackageMaterial(relativePath));
56
54
  return chunks.join("\n\n");
57
55
  }
58
56
 
57
+ function roleSoulPath(role: string, materials: readonly string[]): string {
58
+ const suffix = `/souls/${role}.md`;
59
+ const path = materials.find((candidate) => `/${candidate}`.endsWith(suffix));
60
+ if (path === undefined) throw new Error(`session materials omit the ${role} Soul`);
61
+ return path;
62
+ }
63
+
64
+ async function loadSeparatedSessionPart(
65
+ role: string,
66
+ materials: readonly string[],
67
+ part: "soul" | "references",
68
+ ): Promise<string> {
69
+ const soul = roleSoulPath(role, materials);
70
+ return part === "soul"
71
+ ? readPackageMaterial(soul)
72
+ : joinPackageMaterials(materials.filter((path) => path !== soul));
73
+ }
74
+
59
75
  type PublicRoleMaterials = {
60
76
  readonly [Role in PackagedRole]: Extract<
61
77
  (typeof PUBLIC_ROLE_RECORDS)[number],
@@ -72,10 +88,12 @@ export const MAIN_ROLE_SESSION_MATERIALS = {
72
88
 
73
89
  export type MainRoleSession = keyof typeof MAIN_ROLE_SESSION_MATERIALS;
74
90
 
75
- export function loadMainRoleSessionMaterials(
76
- role: MainRoleSession,
77
- ): Promise<string> {
78
- return joinPackageMaterials(MAIN_ROLE_SESSION_MATERIALS[role]);
91
+ export function loadMainRoleSessionMaterials(role: MainRoleSession): Promise<string> {
92
+ return loadSeparatedSessionPart(role, MAIN_ROLE_SESSION_MATERIALS[role], "soul");
93
+ }
94
+
95
+ export function loadMainRoleReferenceMaterials(role: MainRoleSession): Promise<string> {
96
+ return loadSeparatedSessionPart(role, MAIN_ROLE_SESSION_MATERIALS[role], "references");
79
97
  }
80
98
 
81
99
  /** Gatekeeper province; notary reuses the public notary materials definition. */
@@ -88,8 +106,6 @@ export const GATEKEEPER_SESSION_MATERIALS = {
88
106
 
89
107
  export type GatekeeperSessionRole = keyof typeof GATEKEEPER_SESSION_MATERIALS;
90
108
 
91
- export function loadGatekeeperSessionMaterials(
92
- role: GatekeeperSessionRole,
93
- ): Promise<string> {
94
- return joinPackageMaterials(GATEKEEPER_SESSION_MATERIALS[role]);
109
+ export function loadGatekeeperSessionMaterials(role: GatekeeperSessionRole): Promise<string> {
110
+ return loadSeparatedSessionPart(role, GATEKEEPER_SESSION_MATERIALS[role], "soul");
95
111
  }