@akagilnc/pi-workflow-roles 0.1.2521 → 0.1.2653-next.71811ca

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 (109) hide show
  1. package/README.md +129 -39
  2. package/README.zh-CN.md +128 -39
  3. package/dist/activation-ledger-topology.js +1 -1
  4. package/dist/archivist-record-entry.js +47 -26
  5. package/dist/auditor-dossier-tool.js +3 -1
  6. package/dist/compliance-transport.js +17 -26
  7. package/dist/doctor-contracts.js +1 -2
  8. package/dist/dossier-resolution.js +4 -8
  9. package/dist/engine-detour-tool.js +33 -8
  10. package/dist/engine-detour.js +1 -0
  11. package/dist/evidence-child-executor.js +277 -424
  12. package/dist/host-contracts.js +22 -0
  13. package/dist/institutional-resolution.js +88 -0
  14. package/dist/merger-contracts.js +1 -2
  15. package/dist/navigator-attendance.js +16 -0
  16. package/dist/notary-contracts.js +2 -3
  17. package/dist/package-contracts/fixer-output.js +1 -2
  18. package/dist/packaged-role-registry.js +102 -9
  19. package/dist/pi/in-process-session.js +671 -0
  20. package/dist/public-cli/config.js +7 -5
  21. package/dist/public-cli/main.js +3767 -4113
  22. package/dist/reviewer-child-executor.js +6 -1
  23. package/dist/session-opening-materials.js +10 -38
  24. package/dist/sitian-appender.js +146 -0
  25. package/dist/sitian-contracts.js +21 -0
  26. package/dist/sitian-facade.js +15 -0
  27. package/dist/sitian-reader.js +52 -0
  28. package/extensions/role-runtime.ts +9 -7
  29. package/package.json +5 -5
  30. package/resources/engines/cursor.md +12 -26
  31. package/resources/engines/hermes.md +2 -23
  32. package/resources/engines/opus.md +1 -16
  33. package/resources/navigator-route-playbook.md +2 -2
  34. package/scripts/build-package.mjs +0 -1
  35. package/souls/quality-law.md +1 -1
  36. package/src/activation-ledger-topology.ts +1 -1
  37. package/src/archivist-record-entry.ts +57 -45
  38. package/src/auditor-dossier-tool.ts +4 -2
  39. package/src/canonical-skill-binding.ts +37 -27
  40. package/src/collector-role.ts +11 -20
  41. package/src/collector-tool-schemas.ts +1 -4
  42. package/src/compliance-transport.ts +19 -44
  43. package/src/doctor-auditor.ts +1 -5
  44. package/src/doctor-contracts.ts +1 -4
  45. package/src/doctor-role.ts +31 -8
  46. package/src/dossier-resolution.ts +8 -8
  47. package/src/engine-detour-tool.ts +48 -19
  48. package/src/engine-detour.ts +3 -0
  49. package/src/evidence-child-executor.ts +277 -485
  50. package/src/factory-board.ts +3 -3
  51. package/src/gatekeeper-role.ts +21 -10
  52. package/src/host-contracts.ts +386 -0
  53. package/src/in-process-session.ts +1 -115
  54. package/src/institutional-resolution.ts +135 -0
  55. package/src/judge-auditor.ts +1 -5
  56. package/src/judge-role.ts +37 -51
  57. package/src/merger-contracts.ts +1 -4
  58. package/src/merger-role.ts +7 -11
  59. package/src/navigator-attendance.ts +20 -1
  60. package/src/notary-contracts.ts +8 -11
  61. package/src/notary-role.ts +7 -23
  62. package/src/notary-source-run.ts +4 -3
  63. package/src/package-contracts/fixer-output.ts +1 -4
  64. package/src/package-resources/method-skill-binding.ts +10 -34
  65. package/src/packaged-role-registry.ts +122 -12
  66. package/src/pi/adapter.ts +192 -0
  67. package/src/pi/durable-principal.ts +98 -0
  68. package/src/pi/in-process-session.ts +819 -0
  69. package/src/pi/known-failure.ts +77 -0
  70. package/src/pi/pi-normalization.ts +284 -0
  71. package/src/pi/role-turn-host.ts +459 -0
  72. package/src/public-cli/auto-resume.ts +42 -44
  73. package/src/public-cli/cli.ts +171 -257
  74. package/src/public-cli/coder-run.ts +115 -433
  75. package/src/public-cli/collector-run.ts +62 -314
  76. package/src/public-cli/config.ts +7 -5
  77. package/src/public-cli/doctor-run.ts +41 -52
  78. package/src/public-cli/fixer-run.ts +122 -427
  79. package/src/public-cli/invocation.ts +287 -96
  80. package/src/public-cli/judge-run.ts +92 -422
  81. package/src/public-cli/merger-run.ts +167 -429
  82. package/src/public-cli/notary-run.ts +41 -47
  83. package/src/public-cli/option-definitions.ts +136 -3
  84. package/src/public-cli/post-admission.ts +497 -0
  85. package/src/public-cli/public-run-credentials.ts +4 -4
  86. package/src/public-cli/reviewer-dispatch-rejection.ts +106 -0
  87. package/src/public-cli/reviewer-run.ts +132 -449
  88. package/src/public-cli/run-lifecycle.ts +339 -327
  89. package/src/public-cli/settlement.ts +251 -134
  90. package/src/public-cli/terminal.ts +3 -9
  91. package/src/public-cli/turn-request.ts +87 -0
  92. package/src/reviewer-child-executor.ts +9 -1
  93. package/src/reviewer-role.ts +11 -20
  94. package/src/role-runtime.ts +129 -78
  95. package/src/session-opening-materials.ts +25 -39
  96. package/src/sitian-appender.ts +171 -0
  97. package/src/sitian-contracts.ts +106 -0
  98. package/src/sitian-facade.ts +18 -0
  99. package/src/sitian-reader.ts +62 -0
  100. package/src/worker-role.ts +42 -40
  101. package/src/worker-submission-gates.ts +33 -0
  102. package/dist/archivist-role-run-coordinates.js +0 -28
  103. package/dist/package-contracts/terminating-infrastructure.js +0 -90
  104. package/resources/engines/sonnet.md +0 -27
  105. package/resources/engines/zcode.md +0 -63
  106. package/src/archivist-role-run-coordinates.ts +0 -45
  107. package/src/package-contracts/terminating-infrastructure.ts +0 -132
  108. package/src/public-cli/explicit-internal.ts +0 -427
  109. package/src/public-cli/one-shot-dispatch.ts +0 -300
@@ -4,15 +4,11 @@
4
4
  * 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
- import { existsSync, realpathSync, writeFileSync } from "node:fs";
8
- import { dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
7
+ import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ import { dirname, resolve, join } from "node:path";
9
9
  import { SessionManager } from "@earendil-works/pi-coding-agent";
10
10
 
11
11
  import { resolveBookKeyFromGit } from "./activation-ledger-git.ts";
12
- export {
13
- roleRunSessionCoordinates,
14
- type RoleRunSessionCoordinates,
15
- } from "./archivist-role-run-coordinates.ts";
16
12
  import {
17
13
  ActivationLedgerError,
18
14
  activationBookDirectory,
@@ -23,12 +19,42 @@ import {
23
19
  resolveActivationLedgerHome,
24
20
  } from "./activation-ledger-topology.ts";
25
21
 
26
- // continueRecent always allocates a deferred sessionFile; this is the discovery result.
27
- const { findMostRecentSession } = await import(
28
- new URL("./core/session-manager.js", import.meta.resolve("@earendil-works/pi-coding-agent")).href,
29
- ) as {
30
- findMostRecentSession: (sessionDir: string, cwd?: string) => string | null;
31
- };
22
+ const CURRENT_SESSION_LEDGER = "current-session.json";
23
+
24
+ type CurrentSessionRecord = { readonly sessionFile: string };
25
+
26
+ function readCurrentSession(sessionDir: string): string {
27
+ const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
28
+ try {
29
+ const value: unknown = JSON.parse(readFileSync(ledger, "utf8"));
30
+ if (
31
+ typeof value !== "object"
32
+ || value === null
33
+ || typeof (value as CurrentSessionRecord).sessionFile !== "string"
34
+ || (value as CurrentSessionRecord).sessionFile.length === 0
35
+ ) {
36
+ throw new Error("sessionFile is missing");
37
+ }
38
+ return (value as CurrentSessionRecord).sessionFile;
39
+ } catch (error) {
40
+ throw new ActivationLedgerError(
41
+ `archivist current-session ledger is unavailable or invalid (${ledger}): ${errorText(error)}`,
42
+ { cause: error },
43
+ );
44
+ }
45
+ }
46
+
47
+ function writeCurrentSession(sessionDir: string, sessionFile: string): void {
48
+ const ledger = join(sessionDir, CURRENT_SESSION_LEDGER);
49
+ try {
50
+ writeFileSync(ledger, `${JSON.stringify({ sessionFile })}\n`, { flag: "wx" });
51
+ } catch (error) {
52
+ throw new ActivationLedgerError(
53
+ `archivist current-session ledger cannot be created (${ledger}): ${errorText(error)}`,
54
+ { cause: error },
55
+ );
56
+ }
57
+ }
32
58
 
33
59
  /** Parent session surface needed to link and (when already under home) nest the record. */
34
60
  export type RecordSessionParent = {
@@ -53,44 +79,28 @@ export const WORKER_SUBMISSION_GATE_KIND = "worker-submission-gate";
53
79
  * Sole file-level placement lock for a resumed same-nest principal (ADR 0065 / #221).
54
80
  * ensureRealDirectoryTree already owns the sessionDir chain; a final .jsonl symlink is
55
81
  * invisible to that directory walk, so this runs once before SessionManager.open.
56
- * Circle is the book physical root that owns sessionDir — not merely ledger home — so a
57
- * books/A nest cannot resume a final file whose realpath lands in books/B.
82
+ * Circle is the authorized nest (sessionDir) itself — lexical path and realpath must both
83
+ * stay inside it. Same-book cross-nest pointers and cross-book symlinks are refused alike.
58
84
  * realpath/stat failures stay typed ActivationLedgerError with original cause — never
59
85
  * wash through physicalPathIdentity's non-ENOENT lexical fallback.
60
86
  */
61
- function assertRecentFinalFileUnderLedgerHome(
62
- ledgerHome: string,
87
+ function assertRecentFinalFileUnderSessionDir(
63
88
  sessionDir: string,
64
89
  recentFile: string,
65
90
  ): void {
66
- const absoluteHome = resolve(ledgerHome);
67
91
  const absoluteSessionDir = resolve(sessionDir);
68
92
  const absoluteFile = resolve(recentFile);
69
- // sessionDir just passed ensureRealDirectoryTree: derive the owning book lexically.
70
- const relToHome = relative(absoluteHome, absoluteSessionDir);
71
- const segments = relToHome.split(sep);
72
- if (
73
- relToHome === ""
74
- || isAbsolute(relToHome)
75
- || relToHome === ".."
76
- || relToHome.startsWith(`..${sep}`)
77
- || segments[0] !== "books"
78
- || segments[1] === undefined
79
- || segments[1] === ""
80
- || segments[1] === "."
81
- || segments[1] === ".."
82
- ) {
93
+ if (absoluteFile !== absoluteSessionDir && !pathContainedIn(absoluteSessionDir, absoluteFile)) {
83
94
  throw new ActivationLedgerError(
84
- `archivist record sessionDir must be under a ledger book (${ledgerHome}): ${sessionDir}`,
95
+ `archivist record session must be under the authorized nest (${sessionDir}): ${recentFile}`,
85
96
  );
86
97
  }
87
- const bookRoot = join(absoluteHome, "books", segments[1]);
88
- let realBookRoot: string;
98
+ let realSessionDir: string;
89
99
  try {
90
- realBookRoot = realpathSync(bookRoot);
100
+ realSessionDir = realpathSync(absoluteSessionDir);
91
101
  } catch (error) {
92
102
  throw new ActivationLedgerError(
93
- `activation ledger book is not resolvable (${bookRoot}): ${errorText(error)}`,
103
+ `archivist record sessionDir is not resolvable (${absoluteSessionDir}): ${errorText(error)}`,
94
104
  { cause: error },
95
105
  );
96
106
  }
@@ -103,9 +113,9 @@ function assertRecentFinalFileUnderLedgerHome(
103
113
  { cause: error },
104
114
  );
105
115
  }
106
- if (realFile !== realBookRoot && !pathContainedIn(realBookRoot, realFile)) {
116
+ if (realFile !== realSessionDir && !pathContainedIn(realSessionDir, realFile)) {
107
117
  throw new ActivationLedgerError(
108
- `archivist record session must be under the ledger book (${bookRoot}): ${recentFile}`,
118
+ `archivist record session must be under the authorized nest (${sessionDir}): ${recentFile}`,
109
119
  );
110
120
  }
111
121
  }
@@ -117,7 +127,7 @@ function assertRecentFinalFileUnderLedgerHome(
117
127
  * identity is checked once before SessionManager.open (directory walk cannot see a
118
128
  * trailing .jsonl symlink). New principals mint under the already-validated sessionDir
119
129
  * via destination-free SessionManager.create — no derived postcondition.
120
- * Resume via Pi findMostRecentSession is limited to subject-keyed identity and the
130
+ * Resume via the AK-owned current-session ledger is limited to subject-keyed identity and the
121
131
  * authorized worker-submission-gate durable path. Ordinary no-subject children
122
132
  * (evidence-children, auditor-roles, …) always mint a fresh session — never reopen
123
133
  * a sibling volume selected only by kind/cwd/mtime.
@@ -156,18 +166,17 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
156
166
  parentSession = parentFile;
157
167
  }
158
168
 
169
+ const nestAlreadyExists = existsSync(sessionDir);
159
170
  // Directory-chain ownership: containment + physical components (no parallel assert).
160
171
  ensureRealDirectoryTree(ledgerHome, sessionDir);
161
172
  // Subject-keyed nests continue by subject digest; gate durable resume is the only
162
173
  // authorized no-subject same-nest continuation. All other kinds mint fresh.
163
174
  const mayResumeSameNest =
164
175
  options.subject !== undefined || options.kind === WORKER_SUBMISSION_GATE_KIND;
165
- if (mayResumeSameNest) {
166
- const recentFile = findMostRecentSession(sessionDir, cwd);
167
- if (recentFile !== null) {
168
- assertRecentFinalFileUnderLedgerHome(ledgerHome, sessionDir, recentFile);
169
- return SessionManager.open(recentFile, sessionDir, cwd);
170
- }
176
+ if (mayResumeSameNest && nestAlreadyExists) {
177
+ const recentFile = readCurrentSession(sessionDir);
178
+ assertRecentFinalFileUnderSessionDir(sessionDir, recentFile);
179
+ return SessionManager.open(recentFile, sessionDir, cwd);
171
180
  }
172
181
 
173
182
  const session = SessionManager.create(
@@ -188,6 +197,9 @@ export function createRecordSession(options: CreateRecordSessionOptions): Sessio
188
197
  session.setSessionFile(file);
189
198
  }
190
199
  }
200
+ if (mayResumeSameNest && file !== undefined) {
201
+ writeCurrentSession(sessionDir, file);
202
+ }
191
203
  }
192
204
  return session;
193
205
  }
@@ -1,6 +1,7 @@
1
1
  import { dirname, join, resolve } from "node:path";
2
2
 
3
3
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { HostContext } from "./host-contracts.ts";
4
5
  import { Type } from "typebox";
5
6
 
6
7
  export const AUDITOR_DOSSIER_TOOL_NAME = "ak_get_run_dossier" as const;
@@ -14,9 +15,10 @@ export type AuditorDossierLocation = {
14
15
  };
15
16
 
16
17
  /** Resolve the exact run binding already carried by the parent record session. */
17
- export function auditorRunDirectory(context: ExtensionContext): string | undefined {
18
+ export function auditorRunDirectory(context: ExtensionContext | HostContext): string | undefined {
18
19
  const sessionFile = context.sessionManager?.getSessionFile?.();
19
- return sessionFile === undefined ? undefined : resolve(dirname(dirname(sessionFile)));
20
+ if (sessionFile !== undefined) return resolve(dirname(dirname(sessionFile)));
21
+ return undefined;
20
22
  }
21
23
 
22
24
  /** The one shared, run-bound dossier locator exposed to every auditor seat. */
@@ -2,7 +2,8 @@ import { readFile, realpath } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, resolve } from "node:path";
4
4
 
5
- import { parseSkillBlock, stripFrontmatter } from "@earendil-works/pi-coding-agent";
5
+ import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
6
+ import type { HostSkillExpansionEvidence } from "./host-contracts.ts";
6
7
  export type CanonicalSkillName = "tdd" | "code-review";
7
8
 
8
9
  export type CanonicalSkillSnapshot = Readonly<{
@@ -26,7 +27,7 @@ export type CanonicalSkillBinding<Name extends CanonicalSkillName = CanonicalSki
26
27
  snapshot: CanonicalSkillSnapshot;
27
28
  invocation(originalRequest: string): string;
28
29
  captureExpansion(
29
- prompt: string,
30
+ evidence: HostSkillExpansionEvidence | undefined,
30
31
  originalRequest: string,
31
32
  ): CanonicalSkillEvidence<Name> | undefined;
32
33
  }>;
@@ -35,6 +36,33 @@ export type AnyCanonicalSkillBinding =
35
36
  | CanonicalSkillBinding<"tdd">
36
37
  | CanonicalSkillBinding<"code-review">;
37
38
 
39
+ export function captureCanonicalSkillExpansion<Name extends CanonicalSkillName>(
40
+ name: Name,
41
+ snapshot: CanonicalSkillSnapshot,
42
+ configuredPath: string,
43
+ evidence: HostSkillExpansionEvidence | undefined,
44
+ originalRequest: string,
45
+ ): CanonicalSkillEvidence<Name> | undefined {
46
+ const matchedPath =
47
+ evidence?.location === configuredPath
48
+ ? configuredPath
49
+ : evidence?.location === snapshot.path
50
+ ? snapshot.path
51
+ : undefined;
52
+ const expectedContent = matchedPath === undefined
53
+ ? undefined
54
+ : `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`;
55
+ if (
56
+ evidence?.name !== name
57
+ || matchedPath === undefined
58
+ || evidence.content !== expectedContent
59
+ || evidence.userMessage !== originalRequest
60
+ ) {
61
+ return undefined;
62
+ }
63
+ return Object.freeze({ ...evidence, name });
64
+ }
65
+
38
66
  export class CanonicalSkillUnavailableError extends Error {
39
67
  readonly code = "canonical-skill-unavailable" as const;
40
68
  constructor(readonly skillName: CanonicalSkillName, path: string, cause: unknown) {
@@ -75,32 +103,14 @@ export async function loadCanonicalSkillBinding(
75
103
  invocation(originalRequest) {
76
104
  return `/skill:${name} ${originalRequest}`;
77
105
  },
78
- captureExpansion(prompt, originalRequest) {
79
- const parsed = parseSkillBlock(prompt);
80
- const matchedPath =
81
- parsed?.location === configuredPath
82
- ? configuredPath
83
- : parsed?.location === snapshot.path
84
- ? snapshot.path
85
- : undefined;
86
- const expectedContent = matchedPath === undefined
87
- ? undefined
88
- : `References are relative to ${dirname(matchedPath)}.\n\n${snapshot.body}`;
89
- const userMessage = parsed?.userMessage ?? "";
90
- if (
91
- parsed?.name !== name ||
92
- matchedPath === undefined ||
93
- parsed.content !== expectedContent ||
94
- userMessage !== originalRequest
95
- ) {
96
- return undefined;
97
- }
98
- return Object.freeze({
106
+ captureExpansion(evidence, originalRequest) {
107
+ return captureCanonicalSkillExpansion(
99
108
  name,
100
- location: parsed.location,
101
- content: parsed.content,
102
- userMessage,
103
- });
109
+ snapshot,
110
+ configuredPath,
111
+ evidence,
112
+ originalRequest,
113
+ );
104
114
  },
105
115
  };
106
116
  return Object.freeze(binding) as AnyCanonicalSkillBinding;
@@ -1,7 +1,4 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionContext,
4
- } from "@earendil-works/pi-coding-agent";
1
+ import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
5
2
  import type { Static } from "typebox";
6
3
 
7
4
  import {
@@ -30,9 +27,6 @@ import {
30
27
  buildCollectorReceipt,
31
28
  type CollectorReceipt,
32
29
  } from "./collector-receipt.ts";
33
- import {
34
- failOnInfrastructureFailureDeclaration,
35
- } from "./package-contracts/terminating-infrastructure.ts";
36
30
  import {
37
31
  collectorObserveArgsSchema,
38
32
  collectorOutputArgsSchema,
@@ -73,7 +67,7 @@ export type CollectorRoleDependencies = {
73
67
  };
74
68
 
75
69
  export type CollectorRoleHostActions = {
76
- failInfrastructure(error: unknown, ctx: ExtensionContext, toolCallId?: string): never;
70
+ failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never;
77
71
  };
78
72
 
79
73
  type CollectorActivation = {
@@ -89,13 +83,13 @@ type CollectorActivation = {
89
83
  /** Sole-final at the output execution seam (same shape as other terminating roles). */
90
84
  function assertSoleFinalCollectorOutput(
91
85
  toolCallId: string,
92
- ctx: ExtensionContext,
86
+ ctx: HostContext,
93
87
  ): void {
94
88
  const leaf = ctx.sessionManager.getLeafEntry();
95
- if (leaf?.type !== "message" || leaf.message.role !== "assistant") {
89
+ if (leaf?.type !== "message" || leaf.message === undefined || leaf.message.role !== "assistant" || !Array.isArray(leaf.message.content)) {
96
90
  throw new Error("通进司回执非唯一终局工具调用");
97
91
  }
98
- const calls = leaf.message.content.filter((part) => part.type === "toolCall");
92
+ const calls = leaf.message.content.filter((part): part is Extract<typeof part, { type: "toolCall" }> => part.type === "toolCall");
99
93
  const call = calls[0];
100
94
  if (
101
95
  calls.length !== 1 ||
@@ -119,12 +113,12 @@ function buildMethodContext(activation: CollectorActivation): string {
119
113
  }
120
114
 
121
115
  export function createCollectorRoleRuntime(
122
- pi: ExtensionAPI,
116
+ pi: RoleHost,
123
117
  dependencies: CollectorRoleDependencies,
124
118
  hostActions: CollectorRoleHostActions,
125
119
  ): {
126
120
  activate(
127
- ctx: ExtensionContext,
121
+ ctx: HostContext,
128
122
  event: { reason: string },
129
123
  ): Promise<void>;
130
124
  } {
@@ -288,7 +282,7 @@ export function createCollectorRoleRuntime(
288
282
  description: "抓取配置目标的完整 GitHub PR 证据,存不可变快照入卷。",
289
283
  promptSnippet: "抓取配置目标 PR 证据",
290
284
  parameters: observeSchema,
291
- async execute(toolCallId, _params, signal, _onUpdate, ctx) {
285
+ async execute(toolCallId: string, _params: unknown, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
292
286
  if (activation === undefined) {
293
287
  throw new Error("通进司未激活");
294
288
  }
@@ -323,7 +317,7 @@ export function createCollectorRoleRuntime(
323
317
  description: "按配置请求体与关联标记,在所引最新快照 HEAD 发一次请求。",
324
318
  promptSnippet: "按配置发一次请求",
325
319
  parameters: requestSchema,
326
- async execute(toolCallId, params: RequestParams, signal, _onUpdate, ctx) {
320
+ async execute(toolCallId: string, params: RequestParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
327
321
  if (activation === undefined) {
328
322
  throw new Error("通进司未激活");
329
323
  }
@@ -355,7 +349,7 @@ export function createCollectorRoleRuntime(
355
349
  description: "再观察前等待;单次上限五分钟且不超剩余资格。",
356
350
  promptSnippet: "资格截止前等待",
357
351
  parameters: waitSchema,
358
- async execute(toolCallId, params: WaitParams, signal, _onUpdate, ctx) {
352
+ async execute(toolCallId: string, params: WaitParams, signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
359
353
  if (activation === undefined) {
360
354
  throw new Error("通进司未激活");
361
355
  }
@@ -386,14 +380,11 @@ export function createCollectorRoleRuntime(
386
380
  description: "观察完成后提交;回执由 runtime 组装。",
387
381
  promptSnippet: "提交通进司回执",
388
382
  parameters: outputSchema,
389
- async execute(toolCallId, params: OutputParams, _signal, _onUpdate, ctx) {
383
+ async execute(toolCallId: string, params: OutputParams, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: HostContext) {
390
384
  if (activation === undefined) {
391
385
  throw new Error("通进司未激活");
392
386
  }
393
387
  try {
394
- // #541: infra declaration fails via the shared host seam before the
395
- // operational ledger begins / a receipt is built.
396
- failOnInfrastructureFailureDeclaration(params, hostActions, ctx, toolCallId);
397
388
  assertSoleFinalCollectorOutput(toolCallId, ctx);
398
389
  activation.ledger.beginOperational(COLLECTOR_OUTPUT_TOOL, toolCallId);
399
390
  const receipt: CollectorReceipt = buildCollectorReceipt(
@@ -1,6 +1,5 @@
1
1
  import { Type, type Static } from "typebox";
2
2
  import { COLLECTOR_ELIGIBILITY_MS } from "./collector-evidence.ts";
3
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
4
3
 
5
4
  export const collectorObserveArgsSchema = Type.Object({}, { additionalProperties: false });
6
5
  export const collectorRequestArgsSchema = Type.Object({
@@ -11,9 +10,7 @@ export const collectorWaitArgsSchema = Type.Object({
11
10
  durationMs: Type.Integer({ minimum: 1, maximum: COLLECTOR_ELIGIBILITY_MS, description: "等待毫秒;单次上限五分钟且不超剩余资格" }),
12
11
  }, { additionalProperties: false });
13
12
  /** Runtime owns the observed groups; the model only signals sole-final submission. */
14
- export const collectorOutputArgsSchema = withInfrastructureFailureDeclaration(
15
- Type.Object({}, { additionalProperties: true }),
16
- );
13
+ export const collectorOutputArgsSchema = Type.Object({}, { additionalProperties: true });
17
14
  (collectorOutputArgsSchema as unknown as { required: string[] }).required = [];
18
15
 
19
16
  export type CollectorObserveArgs = Static<typeof collectorObserveArgsSchema>;
@@ -1,15 +1,13 @@
1
- import type { Api, AssistantMessage, Model, Usage } from "@earendil-works/pi-ai";
1
+ import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
2
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
4
  import {
5
5
  executeAuditorChild,
6
- type AuditorCompletion,
7
6
  } from "./evidence-child-executor.ts";
8
7
  import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
9
8
  import type { DossierObservation } from "./dossier-resolution.ts";
10
9
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
11
10
 
12
- export type ComplianceCompletion = AuditorCompletion;
13
11
  export type ComplianceArgumentRootType = "null" | "array" | "undefined" | "string" | "number" | "boolean" | "bigint" | "symbol" | "function";
14
12
  export type ComplianceAuditObservation =
15
13
  | { kind: "non-object-arguments"; type: ComplianceArgumentRootType }
@@ -39,8 +37,6 @@ export class ComplianceCandidateUnreadableError extends Error {
39
37
  if (usage !== undefined) this.usage = usage;
40
38
  }
41
39
  }
42
- export type ComplianceDispatch = { model: Model<Api>; auth: { apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> } };
43
-
44
40
  /** Zero-projection kickoff — soul already carries dossier-fetch duty; no hand-delivered materials. */
45
41
  export const AUDITOR_DOSSIER_PROMPT = "本 run 卷宗已就绪。" as const;
46
42
 
@@ -55,15 +51,6 @@ export function createComplianceDecisionTool(name: string, description: string)
55
51
  return { name, description, parameters: complianceDecisionSchema, async execute(_id: string, params: unknown): Promise<AgentToolResult<unknown>> { return { content: [{ type: "text", text: "审计决议已收" }], details: params, terminate: true }; } };
56
52
  }
57
53
 
58
- export async function prepareComplianceDispatch(model: Model<Api>, context: ExtensionContext, label: string): Promise<ComplianceDispatch> {
59
- const resolution = await context.modelRegistry.getProviderAuth(model.provider).catch((error: unknown) => { throw new Error(`${label} authentication failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); });
60
- if (resolution === undefined) throw new Error(`${label} authentication failed: provider is not configured: ${model.provider}`);
61
- const auth = await context.modelRegistry.getApiKeyAndHeaders(model);
62
- if (!auth.ok) throw new Error(`${label} authentication failed: ${auth.error}`);
63
- const env = auth.env ?? resolution.env;
64
- return { model: resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model, auth: { ...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }), ...(auth.headers === undefined ? {} : { headers: auth.headers }), ...(env === undefined ? {} : { env }) } };
65
- }
66
-
67
54
  export const COMPLIANCE_RESPONSE_ENTRY_TYPE = "ak_compliance_response" as const;
68
55
  export const AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE = "ak_auditor_parent_attempt_binding" as const;
69
56
  export const AUDITOR_COMPLIANCE_FAILURE_ENTRY_TYPE = "ak_auditor_compliance_failure" as const;
@@ -76,43 +63,32 @@ export type AuditorParentAttemptBinding = {
76
63
  readonly attemptEntryId?: string;
77
64
  };
78
65
  };
66
+ import { attachDirectErrnoCode, sitianReport } from "./sitian-facade.ts";
67
+
79
68
  export class ComplianceResponseRetentionError extends Error {
80
- constructor(message: string, options?: ErrorOptions) { super(message, options); this.name = "ComplianceResponseRetentionError"; }
81
- }
82
- export type ActiveSessionResponseAppender = { appendCustomEntry(customType: string, data?: unknown): string };
83
- /** Unique owner for session custom-entry append with availability check and typed failure. */
84
- export function appendActiveSessionCustomEntry(
85
- context: ExtensionContext,
86
- customType: string,
87
- data?: unknown,
88
- labels: { unavailable?: string; failed?: string } = {},
89
- ): string {
90
- const manager = context.sessionManager as unknown as Partial<ActiveSessionResponseAppender> | undefined;
91
- if (typeof manager?.appendCustomEntry !== "function") {
92
- throw new ComplianceResponseRetentionError(
93
- labels.unavailable ?? "session custom entry append is unavailable",
94
- );
69
+ constructor(message: string, options?: ErrorOptions) {
70
+ super(message, options);
71
+ this.name = "ComplianceResponseRetentionError";
72
+ attachDirectErrnoCode(this, options?.cause);
95
73
  }
74
+ }
75
+ function retainComplianceResponse(context: ExtensionContext, response: AssistantMessage): void {
96
76
  try {
97
- return manager.appendCustomEntry(customType, data);
77
+ sitianReport({
78
+ level: "event",
79
+ kind: "auditor",
80
+ cwd: context.cwd,
81
+ sessionParent: context.sessionManager.getSessionFile(),
82
+ payload: { version: 1, response },
83
+ source: "compliance-transport",
84
+ });
98
85
  } catch (error) {
99
86
  throw new ComplianceResponseRetentionError(
100
- labels.failed ?? "session custom entry append failed",
87
+ `compliance response retention failed: ${error instanceof Error ? error.message : String(error)}`,
101
88
  { cause: error },
102
89
  );
103
90
  }
104
91
  }
105
- function retainComplianceResponse(context: ExtensionContext, response: AssistantMessage): void {
106
- appendActiveSessionCustomEntry(
107
- context,
108
- COMPLIANCE_RESPONSE_ENTRY_TYPE,
109
- { version: 1, response },
110
- {
111
- unavailable: "compliance response retention is unavailable",
112
- failed: "compliance response retention failed",
113
- },
114
- );
115
- }
116
92
  function readListField(value: unknown): readonly unknown[] { return Array.isArray(value) ? value : value === undefined ? [] : [value]; }
117
93
  export function readComplianceCandidate(arguments_: unknown, usage?: Usage): ComplianceDecision {
118
94
  if (typeof arguments_ !== "object" || arguments_ === null || Array.isArray(arguments_)) {
@@ -140,7 +116,6 @@ export type RunComplianceAuditOptions = {
140
116
  serializedInput?: string;
141
117
  roleLabel: string;
142
118
  invalidDecisionLabel: string;
143
- runCompletion?: ComplianceCompletion;
144
119
  context: ExtensionContext;
145
120
  /** Exact machine-owned run binding; never sourced from AK_ROLE_RUN_DIR. */
146
121
  runDirectory?: string | undefined;
@@ -157,7 +132,7 @@ export async function runComplianceAudit(options: RunComplianceAuditOptions): Pr
157
132
  roleLabel: options.roleLabel,
158
133
  context: options.context,
159
134
  retainResponse: (response) => retainComplianceResponse(options.context, response),
160
- ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
135
+ ...(options.runDirectory === undefined ? {} : { runDirectory: options.runDirectory }),
161
136
  ...(options.signal === undefined ? {} : { signal: options.signal }),
162
137
  });
163
138
  if (receipt.noReceiptLifecycle !== undefined) {
@@ -5,7 +5,6 @@ import { loadAuditorSoul } from "./auditor-soul.ts";
5
5
  import {
6
6
  createComplianceDecisionTool,
7
7
  runComplianceAudit,
8
- type ComplianceCompletion,
9
8
  type ComplianceDecision,
10
9
  } from "./compliance-transport.ts";
11
10
  import {
@@ -30,9 +29,7 @@ const tool = createComplianceDecisionTool(
30
29
  * Doctor auditor: zero hand-delivered materials.
31
30
  * Candidate testimony must already be on the parent-session books.
32
31
  */
33
- export function createPiDoctorAuditor(
34
- runCompletion?: ComplianceCompletion,
35
- ): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
32
+ export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
36
33
  return async (options) => {
37
34
  const dossier = resolveAuditDossier();
38
35
  requireAuditMaterials(dossier);
@@ -47,7 +44,6 @@ export function createPiDoctorAuditor(
47
44
  context: options.context,
48
45
  ...(auditorRunDirectory(options.context) === undefined ? {} : { runDirectory: auditorRunDirectory(options.context) }),
49
46
  ...(options.signal === undefined ? {} : { signal: options.signal }),
50
- ...(runCompletion === undefined ? {} : { runCompletion }),
51
47
  });
52
48
  };
53
49
  }
@@ -1,7 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { canonicalJson } from "./canonical-json.ts";
3
3
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
4
- import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
5
4
 
6
5
  export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
7
6
  export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
@@ -88,9 +87,7 @@ const doctorSubmissionVariants = Type.Union([
88
87
  missingEvidence: Type.Array(Type.Object({ need: nonblank, targetKeys: Type.Array(nonblank, { minItems: 1 }) }, { additionalProperties: false }), { minItems: 1, description: "如实证词所需而尚缺的证据" }),
89
88
  }, { additionalProperties: false, description: "证据不足以支撑如实案证词" }),
90
89
  ]);
91
- export const doctorSubmissionSchema = withInfrastructureFailureDeclaration(
92
- openToolObjectFromUnion(doctorSubmissionVariants),
93
- );
90
+ export const doctorSubmissionSchema = openToolObjectFromUnion(doctorSubmissionVariants);
94
91
  export const doctorOutputSchema = Type.Union([
95
92
  Type.Object({ status: Type.Literal("completed"), case: caseIdentity, findings: Type.Array(finding), cost }, { additionalProperties: false }),
96
93
  doctorSubmissionVariants.anyOf[1]!,
@@ -1,19 +1,42 @@
1
- import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts";
2
2
  import { disposeComplianceDecision } from "./audit-escalation.ts";
3
- import { appendActiveSessionCustomEntry, type ComplianceDecision } from "./compliance-transport.ts";
3
+ import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
4
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
- import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
6
5
  import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
7
6
 
7
+ import { sitianReport } from "./sitian-facade.ts";
8
+
8
9
  export { DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME };
9
10
  export const DOCTOR_CASE_FLAG = { name: "ak-doctor-case", definition: { description: "Retained .ak-roles/books/<book>/issues/<n>/runs directory", type: "string" as const } } as const;
10
- export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context: ExtensionContext; signal?: AbortSignal }): Promise<ComplianceDecision> };
11
- function singleton(toolCallId: string, ctx: ExtensionContext) { const leaf = ctx.sessionManager.getLeafEntry(); if (leaf?.type !== "message" || leaf.message.role !== "assistant") throw new Error("太医署回执非唯一终局工具调用"); const calls = leaf.message.content.filter((part) => part.type === "toolCall"); if (calls.length !== 1 || calls[0]?.id !== toolCallId || calls[0]?.name !== DOCTOR_OUTPUT_TOOL_NAME) throw new Error("太医署回执非唯一终局工具调用"); }
12
- export function createDoctorRoleRuntime(pi: ExtensionAPI, dependencies: DoctorRoleDependencies, host: { failInfrastructure(error: unknown, ctx: ExtensionContext, toolCallId?: string): never }) {
11
+ export type DoctorRoleDependencies = { loadSoul(): Promise<string>; loadCase(path: string): Promise<DoctorCase>; auditCompliance(options: { context: HostContext; signal?: AbortSignal }): Promise<ComplianceDecision> };
12
+ function appendCandidate(ctx: HostContext, data: unknown): void {
13
+ try {
14
+ sitianReport({
15
+ level: "event",
16
+ kind: "candidate",
17
+ cwd: ctx.cwd,
18
+ sessionParent: ctx.sessionManager.getSessionFile(),
19
+ payload: data,
20
+ source: "doctor-role",
21
+ });
22
+ } catch (error) {
23
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
24
+ }
25
+ const append = ctx.sessionManager.appendCustomEntry;
26
+ if (typeof append === "function") {
27
+ try {
28
+ append.call(ctx.sessionManager, DOCTOR_CANDIDATE_ENTRY_TYPE, data);
29
+ } catch (error) {
30
+ throw new ComplianceResponseRetentionError(`太医署候选留存失败: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
31
+ }
32
+ }
33
+ }
34
+ function singleton(toolCallId: string, ctx: HostContext) { const leaf = ctx.sessionManager.getLeafEntry(); if (leaf?.type !== "message" || leaf.message === undefined || leaf.message.role !== "assistant" || !Array.isArray(leaf.message.content)) throw new Error("太医署回执非唯一终局工具调用"); const calls = leaf.message.content.filter((part): part is Extract<typeof part, { type: "toolCall" }> => part.type === "toolCall"); if (calls.length !== 1 || calls[0]?.id !== toolCallId || calls[0]?.name !== DOCTOR_OUTPUT_TOOL_NAME) throw new Error("太医署回执非唯一终局工具调用"); }
35
+ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDependencies, host: { failInfrastructure(error: unknown, ctx: HostContext, toolCallId?: string): never }) {
13
36
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
14
37
  return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
15
- if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
16
- pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id, params, signal, _update, ctx): Promise<AgentToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); failOnInfrastructureFailureDeclaration(params, host, ctx, id); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendActiveSessionCustomEntry(ctx, DOCTOR_CANDIDATE_ENTRY_TYPE, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }, { unavailable: "太医署候选留存不可用", failed: "太医署候选留存失败" }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<AgentToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
38
+ if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
39
+ pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); singleton(id, ctx); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
17
40
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "由留存 session 字节推导,封入受理回执。", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
18
41
  const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
19
42
  }