@introspection-ai/recipes 0.13.0 → 0.14.1

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 (222) hide show
  1. package/README.md +46 -57
  2. package/dist/{agent-tool.d.ts → agents.d.ts} +4 -1
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/{agent-tool.js → agents.js} +1 -1
  5. package/dist/agents.js.map +1 -0
  6. package/dist/api/extensions.d.ts +3 -0
  7. package/dist/api/extensions.d.ts.map +1 -0
  8. package/dist/api/extensions.js +2 -0
  9. package/dist/api/extensions.js.map +1 -0
  10. package/dist/api/mcp.d.ts +4 -0
  11. package/dist/api/mcp.d.ts.map +1 -0
  12. package/dist/api/mcp.js +2 -0
  13. package/dist/api/mcp.js.map +1 -0
  14. package/dist/api/session.d.ts +3 -0
  15. package/dist/api/session.d.ts.map +1 -0
  16. package/dist/api/session.js +2 -0
  17. package/dist/api/session.js.map +1 -0
  18. package/dist/child-agent.d.ts +5 -4
  19. package/dist/child-agent.d.ts.map +1 -1
  20. package/dist/child-agent.js +26 -117
  21. package/dist/child-agent.js.map +1 -1
  22. package/dist/child-session.d.ts +23 -0
  23. package/dist/child-session.d.ts.map +1 -0
  24. package/dist/child-session.js +48 -0
  25. package/dist/child-session.js.map +1 -0
  26. package/dist/extensions.d.ts +29 -0
  27. package/dist/extensions.d.ts.map +1 -0
  28. package/dist/extensions.js +119 -0
  29. package/dist/extensions.js.map +1 -0
  30. package/dist/index.d.ts +4 -13
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -13
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.d.ts +38 -5
  35. package/dist/inspect.d.ts.map +1 -1
  36. package/dist/inspect.js +92 -41
  37. package/dist/inspect.js.map +1 -1
  38. package/dist/interactions.d.ts +18 -2
  39. package/dist/interactions.d.ts.map +1 -1
  40. package/dist/interactions.js +41 -10
  41. package/dist/interactions.js.map +1 -1
  42. package/dist/mcp-catalog.d.ts +1 -0
  43. package/dist/mcp-catalog.d.ts.map +1 -1
  44. package/dist/mcp-catalog.js +14 -6
  45. package/dist/mcp-catalog.js.map +1 -1
  46. package/dist/mcp-chunks/auth-command-ZLGYIEUY.js +273 -0
  47. package/dist/mcp-chunks/call-arguments-LU3W6D3K.js +13 -0
  48. package/dist/mcp-chunks/call-command-LKTKSYTH.js +612 -0
  49. package/dist/mcp-chunks/chunk-2U4BSDC4.js +3951 -0
  50. package/dist/mcp-chunks/chunk-2XL7MG7R.js +272 -0
  51. package/dist/mcp-chunks/chunk-3BJA27PY.js +2321 -0
  52. package/dist/mcp-chunks/chunk-4SQKWYFF.js +15 -0
  53. package/dist/mcp-chunks/chunk-5QOSCSQB.js +290 -0
  54. package/dist/mcp-chunks/chunk-6W5QFASN.js +77 -0
  55. package/dist/mcp-chunks/chunk-6XCZGU3G.js +1004 -0
  56. package/dist/mcp-chunks/chunk-7FHEBT5G.js +149 -0
  57. package/dist/mcp-chunks/chunk-7KA2VJVZ.js +114 -0
  58. package/dist/mcp-chunks/chunk-7UDVSOOF.js +138 -0
  59. package/dist/mcp-chunks/chunk-BNV3TPE4.js +66 -0
  60. package/dist/mcp-chunks/chunk-BTLAWHTA.js +129 -0
  61. package/dist/mcp-chunks/chunk-BV56DXPW.js +13 -0
  62. package/dist/mcp-chunks/chunk-BWHIA4PC.js +90 -0
  63. package/dist/mcp-chunks/chunk-BZIQOWE6.js +267 -0
  64. package/dist/mcp-chunks/chunk-D3MRAZ6H.js +16140 -0
  65. package/dist/mcp-chunks/chunk-DHBE7UBH.js +750 -0
  66. package/dist/mcp-chunks/chunk-E22DYOW5.js +251 -0
  67. package/dist/mcp-chunks/chunk-ECIVB7P4.js +139 -0
  68. package/dist/mcp-chunks/chunk-HKRJ6O36.js +1076 -0
  69. package/dist/mcp-chunks/chunk-IABRYROW.js +44 -0
  70. package/dist/mcp-chunks/chunk-J6LYIZXN.js +45 -0
  71. package/dist/mcp-chunks/chunk-L7NTQVYN.js +161 -0
  72. package/dist/mcp-chunks/chunk-LEZSACDE.js +141 -0
  73. package/dist/mcp-chunks/chunk-LKIBAYKU.js +208 -0
  74. package/dist/mcp-chunks/chunk-LNH5LAJD.js +152 -0
  75. package/dist/mcp-chunks/chunk-LQQEYD76.js +124 -0
  76. package/dist/mcp-chunks/chunk-LRKZP7DJ.js +47 -0
  77. package/dist/mcp-chunks/chunk-LS7PMWSR.js +109 -0
  78. package/dist/mcp-chunks/chunk-M3TPODXI.js +95 -0
  79. package/dist/mcp-chunks/chunk-MZ2NARG7.js +400 -0
  80. package/dist/mcp-chunks/chunk-N46NEFAF.js +45 -0
  81. package/dist/mcp-chunks/chunk-NNOEGEUZ.js +17273 -0
  82. package/dist/mcp-chunks/chunk-NRAJX5E4.js +69 -0
  83. package/dist/mcp-chunks/chunk-P3PSC54P.js +869 -0
  84. package/dist/mcp-chunks/chunk-PIIYJR44.js +6341 -0
  85. package/dist/mcp-chunks/chunk-PWYFKGKG.js +385 -0
  86. package/dist/mcp-chunks/chunk-SJCCBNZ4.js +45 -0
  87. package/dist/mcp-chunks/chunk-TATVT5IT.js +194 -0
  88. package/dist/mcp-chunks/chunk-TEUKFFQL.js +29 -0
  89. package/dist/mcp-chunks/chunk-UKYNQGNE.js +150 -0
  90. package/dist/mcp-chunks/chunk-UNKB4SBV.js +37 -0
  91. package/dist/mcp-chunks/chunk-WD6BOD24.js +138 -0
  92. package/dist/mcp-chunks/chunk-WQJZMCJN.js +73 -0
  93. package/dist/mcp-chunks/chunk-XANPGTLI.js +364 -0
  94. package/dist/mcp-chunks/chunk-XQK2JSW4.js +35 -0
  95. package/dist/mcp-chunks/chunk-YHQUTU6L.js +571 -0
  96. package/dist/mcp-chunks/chunk-YVZKMV5H.js +57 -0
  97. package/dist/mcp-chunks/cli-UV4QJZZM.js +929 -0
  98. package/dist/mcp-chunks/client-S7RQUG5U.js +13 -0
  99. package/dist/mcp-chunks/config-command-NLAGKID5.js +1101 -0
  100. package/dist/mcp-chunks/daemon-command-U242Z6VY.js +26 -0
  101. package/dist/mcp-chunks/dist-LK3SHAMX.js +6478 -0
  102. package/dist/mcp-chunks/emit-ts-command-VBXVXO6V.js +386 -0
  103. package/dist/mcp-chunks/generate-cli-runner-6CAZZA3P.js +1426 -0
  104. package/dist/mcp-chunks/inspect-cli-command-3ZORTOR7.js +111 -0
  105. package/dist/mcp-chunks/launch-YKRXHXJI.js +10 -0
  106. package/dist/mcp-chunks/lifecycle-XVKRHPH3.js +16 -0
  107. package/dist/mcp-chunks/list-command-2MEGGNFQ.js +4190 -0
  108. package/dist/mcp-chunks/output-utils-NKESLAJ6.js +12 -0
  109. package/dist/mcp-chunks/prompt-B1Yc1NPt-VGOTWJMN.js +918 -0
  110. package/dist/mcp-chunks/record-command-YXHTTTT5.js +17 -0
  111. package/dist/mcp-chunks/replay-command-25HJ6AQU.js +85 -0
  112. package/dist/mcp-chunks/resource-command-OO4NYF5C.js +97 -0
  113. package/dist/mcp-chunks/result-utils-MOHHLNM7.js +15 -0
  114. package/dist/mcp-chunks/runtime-5WJ3G25Y.js +33 -0
  115. package/dist/mcp-chunks/runtime-wrapper-RPWDIDBF.js +10 -0
  116. package/dist/mcp-chunks/serve-command-DHXJZMMN.js +3302 -0
  117. package/dist/mcp-chunks/timeouts-W2XWCCXR.js +18 -0
  118. package/dist/mcp-chunks/vault-command-PIYIUEZL.js +154 -0
  119. package/dist/mcp-daemon-client.d.ts +6 -1
  120. package/dist/mcp-daemon-client.d.ts.map +1 -1
  121. package/dist/mcp-daemon-client.js +119 -2
  122. package/dist/mcp-daemon-client.js.map +1 -1
  123. package/dist/mcp-daemon-protocol.d.ts +18 -1
  124. package/dist/mcp-daemon-protocol.d.ts.map +1 -1
  125. package/dist/mcp-daemon-protocol.js +31 -0
  126. package/dist/mcp-daemon-protocol.js.map +1 -1
  127. package/dist/mcp-daemon.js +169 -76602
  128. package/dist/mcp-daemon.js.map +1 -1
  129. package/dist/mcp-policy.d.ts +11 -0
  130. package/dist/mcp-policy.d.ts.map +1 -0
  131. package/dist/mcp-policy.js +27 -0
  132. package/dist/mcp-policy.js.map +1 -0
  133. package/dist/mcp-run-worker.js +38 -76540
  134. package/dist/mcp-tools.d.ts +39 -0
  135. package/dist/mcp-tools.d.ts.map +1 -0
  136. package/dist/mcp-tools.js +493 -0
  137. package/dist/mcp-tools.js.map +1 -0
  138. package/dist/mcp.d.ts +21 -9
  139. package/dist/mcp.d.ts.map +1 -1
  140. package/dist/mcp.js +101 -31
  141. package/dist/mcp.js.map +1 -1
  142. package/dist/model-binding.d.ts +39 -0
  143. package/dist/model-binding.d.ts.map +1 -0
  144. package/dist/model-binding.js +103 -0
  145. package/dist/model-binding.js.map +1 -0
  146. package/dist/pi-extension.d.ts +2 -22
  147. package/dist/pi-extension.d.ts.map +1 -1
  148. package/dist/pi-extension.js +296 -71
  149. package/dist/pi-extension.js.map +1 -1
  150. package/dist/recipe/resolve.d.ts +43 -20
  151. package/dist/recipe/resolve.d.ts.map +1 -1
  152. package/dist/recipe/resolve.js +215 -70
  153. package/dist/recipe/resolve.js.map +1 -1
  154. package/dist/recipe-agent.d.ts +24 -27
  155. package/dist/recipe-agent.d.ts.map +1 -1
  156. package/dist/recipe-agent.js +278 -230
  157. package/dist/recipe-agent.js.map +1 -1
  158. package/dist/recipe-check.d.ts +18 -0
  159. package/dist/recipe-check.d.ts.map +1 -0
  160. package/dist/recipe-check.js +261 -0
  161. package/dist/recipe-check.js.map +1 -0
  162. package/dist/recipe-extensions.d.ts.map +1 -1
  163. package/dist/recipe-extensions.js +0 -3
  164. package/dist/recipe-extensions.js.map +1 -1
  165. package/dist/recipe-model.d.ts +3 -1
  166. package/dist/recipe-model.d.ts.map +1 -1
  167. package/dist/recipe-model.js +15 -12
  168. package/dist/recipe-model.js.map +1 -1
  169. package/dist/recipe-package.d.ts +6 -28
  170. package/dist/recipe-package.d.ts.map +1 -1
  171. package/dist/recipe-package.js +248 -191
  172. package/dist/recipe-package.js.map +1 -1
  173. package/dist/recipe-skills.d.ts.map +1 -1
  174. package/dist/recipe-skills.js +42 -29
  175. package/dist/recipe-skills.js.map +1 -1
  176. package/dist/run-controller.d.ts +10 -9
  177. package/dist/run-controller.d.ts.map +1 -1
  178. package/dist/run-controller.js +28 -21
  179. package/dist/run-controller.js.map +1 -1
  180. package/dist/session.d.ts +40 -53
  181. package/dist/session.d.ts.map +1 -1
  182. package/dist/session.js +312 -173
  183. package/dist/session.js.map +1 -1
  184. package/dist/test-utils.d.ts +7 -7
  185. package/dist/test-utils.d.ts.map +1 -1
  186. package/dist/test-utils.js +32 -15
  187. package/dist/test-utils.js.map +1 -1
  188. package/docs/agent-composition.md +41 -35
  189. package/docs/host-api.md +201 -0
  190. package/docs/index.md +9 -14
  191. package/docs/interactions.md +12 -11
  192. package/docs/mcp-auth.md +6 -6
  193. package/docs/mcp-configuration.md +73 -25
  194. package/docs/pi-extension.md +39 -11
  195. package/docs/recipe-flow.md +9 -9
  196. package/docs/recipe-format.md +106 -41
  197. package/docs/recipe-judges.md +24 -36
  198. package/package.json +23 -17
  199. package/vendor/mcp-client/darwin-arm64/mcp-client +0 -0
  200. package/vendor/mcp-client/darwin-x64/mcp-client +0 -0
  201. package/vendor/mcp-client/linux-arm64/mcp-client +0 -0
  202. package/vendor/recipe-check/darwin-arm64/recipe-check +0 -0
  203. package/vendor/recipe-check/darwin-x64/recipe-check +0 -0
  204. package/vendor/recipe-check/linux-arm64/recipe-check +0 -0
  205. package/vendor/recipe-check/linux-x64/recipe-check +0 -0
  206. package/vendor/recipe-check/linux-x64-musl/recipe-check +0 -0
  207. package/vendor/recipe-check/win32-x64/recipe-check.exe +0 -0
  208. package/dist/agent-tool.d.ts.map +0 -1
  209. package/dist/agent-tool.js.map +0 -1
  210. package/dist/run.d.ts +0 -28
  211. package/dist/run.d.ts.map +0 -1
  212. package/dist/run.js +0 -84
  213. package/dist/run.js.map +0 -1
  214. package/dist/testing.d.ts +0 -25
  215. package/dist/testing.d.ts.map +0 -1
  216. package/dist/testing.js +0 -96
  217. package/dist/testing.js.map +0 -1
  218. package/docs/deployment-configuration.md +0 -65
  219. package/docs/migration.md +0 -70
  220. package/docs/python-bindings-release.md +0 -47
  221. package/docs/recipe-evals.md +0 -70
  222. package/docs/runtime-library.md +0 -171
@@ -2,15 +2,9 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { basename, join } from "node:path";
3
3
  import { parse } from "yaml";
4
4
  import { mergeRecipeAgentModelConfig, parseRecipeAgentModelConfig, RecipeModelConfigError, } from "./recipe-model.js";
5
- import { isValidRecipeMcpToolSelection, packageResourcePaths, parseRecipeMcpToolSelection, readPiPackageManifest, RecipePackageError, } from "./recipe-package.js";
6
- import { mcpSelectionAllowsTool, normalizeMcpServerId, } from "./mcp.js";
5
+ import { assertRecipePathContained, isValidRecipeMcpToolSelection, packageResourcePaths, parseRecipeMcpToolSelection, readPiPackageManifest, RecipePackageError, } from "./recipe-package.js";
6
+ import { mcpSelectionAllowsTool, normalizeMcpServerId, } from "./mcp-policy.js";
7
7
  const INVALID_AGENT_MCP = Symbol("invalidAgentMcp");
8
- export const REQUIRED_RECIPE_AGENT_FIELDS = [
9
- "model.name",
10
- "model.thinkingLevel",
11
- "tools",
12
- "systemInstructions",
13
- ];
14
8
  const AGENT_YAML_KEYS = new Set([
15
9
  "name",
16
10
  "from",
@@ -20,11 +14,10 @@ const AGENT_YAML_KEYS = new Set([
20
14
  "mcp",
21
15
  "skills",
22
16
  "subagents",
23
- "extensions",
24
17
  "system_instructions",
25
- "systemInstructions",
26
- "prompt",
27
18
  ]);
19
+ const PORTABLE_AGENT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
20
+ const MAX_INHERITANCE_DEPTH = 128;
28
21
  function asRecord(value) {
29
22
  return value && typeof value === "object" && !Array.isArray(value)
30
23
  ? value
@@ -32,9 +25,70 @@ function asRecord(value) {
32
25
  }
33
26
  function stringArray(value) {
34
27
  return Array.isArray(value)
35
- ? value.filter((item) => typeof item === "string")
28
+ ? value
29
+ .filter((item) => typeof item === "string")
30
+ .map((item) => item.trim())
36
31
  : [];
37
32
  }
33
+ function isNonEmptyStringArray(value) {
34
+ return (Array.isArray(value) &&
35
+ value.every((item) => typeof item === "string" && Boolean(item.trim())));
36
+ }
37
+ function invalidAgentField(data) {
38
+ if (!Object.hasOwn(data, "name")) {
39
+ return "name is required";
40
+ }
41
+ for (const key of ["name", "from"]) {
42
+ if (Object.hasOwn(data, key) &&
43
+ (typeof data[key] !== "string" || !data[key].trim())) {
44
+ return `${key} must be a non-empty string`;
45
+ }
46
+ if (Object.hasOwn(data, key) &&
47
+ typeof data[key] === "string" &&
48
+ !PORTABLE_AGENT_NAME.test(data[key])) {
49
+ return `${key} must use lowercase kebab-case`;
50
+ }
51
+ }
52
+ if (Object.hasOwn(data, "description") &&
53
+ typeof data.description !== "string") {
54
+ return "description must be a string";
55
+ }
56
+ for (const key of ["tools", "skills", "subagents"]) {
57
+ if (Object.hasOwn(data, key) && !isNonEmptyStringArray(data[key])) {
58
+ return `${key} must be an array of non-empty strings`;
59
+ }
60
+ if (Object.hasOwn(data, key) &&
61
+ new Set(stringArray(data[key])).size !== stringArray(data[key]).length) {
62
+ return `${key} must not contain duplicate entries`;
63
+ }
64
+ }
65
+ if (stringArray(data.tools).includes("agent")) {
66
+ return "tools must not declare the session-generated agent tool";
67
+ }
68
+ if (Object.hasOwn(data, "system_instructions")) {
69
+ const value = data.system_instructions;
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
71
+ return "system_instructions must be an object";
72
+ }
73
+ const instructions = value;
74
+ const unknown = Object.keys(instructions).filter((key) => key !== "mode" && key !== "content");
75
+ if (unknown.length > 0) {
76
+ return `system_instructions has unsupported key(s): ${unknown.join(", ")}`;
77
+ }
78
+ if (typeof instructions.content !== "string") {
79
+ return "system_instructions.content must be a string";
80
+ }
81
+ if (!instructions.content.trim()) {
82
+ return "system_instructions.content must be non-empty";
83
+ }
84
+ if (Object.hasOwn(instructions, "mode") &&
85
+ instructions.mode !== "append" &&
86
+ instructions.mode !== "replace") {
87
+ return "system_instructions.mode must be append or replace";
88
+ }
89
+ }
90
+ return undefined;
91
+ }
38
92
  function modelProjection(config) {
39
93
  if (!config)
40
94
  return undefined;
@@ -46,22 +100,48 @@ function modelProjection(config) {
46
100
  };
47
101
  }
48
102
  function parseSystemInstructions(data) {
49
- const raw = asRecord(data.system_instructions ?? data.systemInstructions);
103
+ const raw = asRecord(data.system_instructions);
50
104
  if (Object.hasOwn(raw, "content") && typeof raw.content === "string") {
51
105
  const mode = raw.mode === "replace" ? "replace" : "append";
52
106
  return { mode, content: raw.content.trim() };
53
107
  }
54
- const prompt = typeof data.prompt === "string" ? data.prompt.trim() : "";
55
- return prompt ? { mode: "append", content: prompt } : undefined;
108
+ return undefined;
109
+ }
110
+ function mergeSystemInstructions(base, child) {
111
+ if (!base)
112
+ return child;
113
+ if (!child)
114
+ return base;
115
+ if (child.mode === "replace")
116
+ return child;
117
+ return {
118
+ mode: base.mode,
119
+ content: `${base.content}\n\n${child.content}`,
120
+ };
121
+ }
122
+ function normalizedMcpServerId(value) {
123
+ return (value
124
+ .trim()
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9_-]+/g, "-")
127
+ .replace(/^-+|-+$/g, "") || "mcp");
128
+ }
129
+ function hasNormalizedMcpServerCollision(values) {
130
+ const seen = new Set();
131
+ for (const value of values) {
132
+ const normalized = normalizedMcpServerId(value);
133
+ if (seen.has(normalized))
134
+ return true;
135
+ seen.add(normalized);
136
+ }
137
+ return false;
56
138
  }
57
- function parseExtensions(data) {
58
- const raw = asRecord(data.extensions);
59
- const extensions = {};
60
- if (Object.hasOwn(raw, "include"))
61
- extensions.include = stringArray(raw.include);
62
- if (Object.hasOwn(raw, "exclude"))
63
- extensions.exclude = stringArray(raw.exclude);
64
- return extensions.include || extensions.exclude ? extensions : undefined;
139
+ function validActivationSelectors(selectors) {
140
+ if (selectors === undefined)
141
+ return true;
142
+ return (selectors.every((selector) => Boolean(selector.trim()) &&
143
+ (!selector.includes("*") || selector === "*")) &&
144
+ (!selectors.includes("*") || selectors.length === 1));
65
145
  }
66
146
  function parseMcp(data) {
67
147
  if (!Object.hasOwn(data, "mcp"))
@@ -69,11 +149,67 @@ function parseMcp(data) {
69
149
  const value = data.mcp;
70
150
  const validObject = Boolean(value) && typeof value === "object" && !Array.isArray(value);
71
151
  const raw = asRecord(value);
72
- const mcp = {};
73
- for (const [serverId, value] of Object.entries(raw)) {
74
- mcp[serverId] = parseRecipeMcpToolSelection(value);
152
+ const rawServers = asRecord(raw.servers);
153
+ const servers = {};
154
+ for (const [serverId, serverValue] of Object.entries(rawServers)) {
155
+ const serverRaw = asRecord(serverValue);
156
+ servers[normalizedMcpServerId(serverId)] = {
157
+ ...parseRecipeMcpToolSelection(serverValue),
158
+ ...(Object.hasOwn(serverRaw, "defer")
159
+ ? {
160
+ defer: stringArray(serverRaw.defer).map((selector) => selector.trim()),
161
+ }
162
+ : {}),
163
+ ...(Object.hasOwn(serverRaw, "eager")
164
+ ? {
165
+ eager: stringArray(serverRaw.eager).map((selector) => selector.trim()),
166
+ }
167
+ : {}),
168
+ };
75
169
  }
76
- if (!validObject) {
170
+ const mode = raw.mode === "cli" || raw.mode === "tools"
171
+ ? raw.mode
172
+ : undefined;
173
+ const mcp = {
174
+ ...(mode ? { mode } : {}),
175
+ servers,
176
+ };
177
+ const malformedServers = Object.entries(rawServers).some(([, serverValue]) => {
178
+ const server = asRecord(serverValue);
179
+ return (!serverValue ||
180
+ typeof serverValue !== "object" ||
181
+ Array.isArray(serverValue) ||
182
+ Object.keys(server).some((key) => !["include", "exclude", "defer", "eager"].includes(key)) ||
183
+ ["include", "exclude", "defer", "eager"].some((key) => {
184
+ if (!Object.hasOwn(server, key))
185
+ return false;
186
+ const values = stringArray(server[key]);
187
+ return new Set(values).size !== values.length;
188
+ }) ||
189
+ ["defer", "eager"].some((key) => Object.hasOwn(server, key) &&
190
+ (!Array.isArray(server[key]) ||
191
+ server[key].some((selector) => typeof selector !== "string"))) ||
192
+ !validActivationSelectors(Object.hasOwn(server, "defer")
193
+ ? stringArray(server.defer).map((selector) => selector.trim())
194
+ : undefined) ||
195
+ !validActivationSelectors(Object.hasOwn(server, "eager")
196
+ ? stringArray(server.eager).map((selector) => selector.trim())
197
+ : undefined) ||
198
+ (mode === "cli" &&
199
+ (Object.hasOwn(server, "defer") || Object.hasOwn(server, "eager"))));
200
+ });
201
+ const malformedStructured = malformedServers ||
202
+ !Object.hasOwn(raw, "servers") ||
203
+ (Object.hasOwn(raw, "mode") &&
204
+ raw.mode !== "cli" &&
205
+ raw.mode !== "tools") ||
206
+ Object.keys(rawServers).some((serverId) => !serverId.trim()) ||
207
+ hasNormalizedMcpServerCollision(Object.keys(rawServers)) ||
208
+ !raw.servers ||
209
+ typeof raw.servers !== "object" ||
210
+ Array.isArray(raw.servers) ||
211
+ Object.keys(raw).some((key) => !["mode", "servers"].includes(key));
212
+ if (!validObject || malformedStructured) {
77
213
  Object.defineProperty(mcp, INVALID_AGENT_MCP, {
78
214
  value: true,
79
215
  enumerable: false,
@@ -86,17 +222,9 @@ function mergeMcp(base, child) {
86
222
  return child;
87
223
  if (!child)
88
224
  return base;
89
- const merged = { ...base };
90
- for (const [serverId, childServer] of Object.entries(child)) {
91
- const baseServer = base[serverId];
92
- merged[serverId] = {
93
- ...(baseServer?.include !== undefined ? { include: baseServer.include } : {}),
94
- ...(baseServer?.exclude !== undefined ? { exclude: baseServer.exclude } : {}),
95
- ...(childServer.include !== undefined ? { include: childServer.include } : {}),
96
- ...(childServer.exclude !== undefined ? { exclude: childServer.exclude } : {}),
97
- };
98
- }
99
- return merged;
225
+ // Capability policy is a review boundary: once a child declares `mcp`, the
226
+ // whole block replaces its base instead of silently retaining parent servers.
227
+ return child;
100
228
  }
101
229
  function readYaml(path) {
102
230
  return asRecord(parse(readFileSync(path, "utf8")));
@@ -141,14 +269,17 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
141
269
  const sources = [];
142
270
  for (const path of recipeAgentFiles(recipeDir)) {
143
271
  const data = readYaml(path);
144
- const fallbackName = basename(path).replace(/\.ya?ml$/i, "");
145
- const explicitName = typeof data.name === "string" && Boolean(data.name.trim());
146
- const name = explicitName ? data.name.trim() : fallbackName;
147
272
  const unknownKeys = Object.keys(data).filter((key) => !AGENT_YAML_KEYS.has(key));
148
273
  if (unknownKeys.length > 0) {
149
274
  opts.onInvalidFile?.(path, new Error(`Agent YAML at ${path} has unsupported key(s): ${unknownKeys.join(", ")}`));
150
275
  continue;
151
276
  }
277
+ const fieldError = invalidAgentField(data);
278
+ if (fieldError) {
279
+ opts.onInvalidFile?.(path, new Error(`Agent YAML at ${path}: ${fieldError}`));
280
+ continue;
281
+ }
282
+ const name = data.name;
152
283
  let modelConfig;
153
284
  try {
154
285
  modelConfig = parseRecipeAgentModelConfig(`Agent YAML at ${path}`, data.model);
@@ -160,11 +291,9 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
160
291
  continue;
161
292
  }
162
293
  sources.push({
163
- fallbackName,
164
- explicitName,
165
294
  definition: {
166
295
  name,
167
- from: typeof data.from === "string" && data.from.trim() ? data.from.trim() : undefined,
296
+ from: typeof data.from === "string" ? data.from : undefined,
168
297
  description: typeof data.description === "string" ? data.description : undefined,
169
298
  model: modelProjection(modelConfig),
170
299
  modelConfig,
@@ -172,53 +301,40 @@ function readRecipeAgentSources(recipeDir, opts = {}) {
172
301
  mcp: parseMcp(data),
173
302
  skills: Object.hasOwn(data, "skills") ? stringArray(data.skills) : undefined,
174
303
  subagents: Object.hasOwn(data, "subagents") ? stringArray(data.subagents) : undefined,
175
- extensions: parseExtensions(data),
176
304
  systemInstructions: parseSystemInstructions(data),
305
+ declaredFields: [
306
+ "description",
307
+ "model",
308
+ "tools",
309
+ "mcp",
310
+ "skills",
311
+ "subagents",
312
+ "system_instructions",
313
+ ].filter((key) => Object.hasOwn(data, key)),
177
314
  },
178
315
  });
179
316
  }
180
317
  return sources;
181
318
  }
182
- export function loadRecipeAgentDefinitions(recipeDir) {
319
+ function definitionsFromSources(sources) {
183
320
  const rawDefinitions = new Map();
184
- const aliases = new Map();
185
321
  const resolvedDefinitions = new Map();
186
322
  const definitions = new Map();
187
- const sources = readRecipeAgentSources(recipeDir, {
188
- onInvalidFile: (path, error) => {
189
- console.warn(`[recipes] skipping ${path}: ${error.message}`);
190
- },
191
- });
192
323
  for (const source of sources) {
193
324
  rawDefinitions.set(source.definition.name, source.definition);
194
- aliases.set(source.fallbackName, source.definition.name);
195
- }
196
- function resolveName(name) {
197
- return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
198
- }
199
- function mergeExtensions(base, child) {
200
- if (!base)
201
- return child;
202
- if (!child)
203
- return base;
204
- return {
205
- ...(base.include ? { include: base.include } : {}),
206
- ...(base.exclude ? { exclude: base.exclude } : {}),
207
- ...(child.include ? { include: child.include } : {}),
208
- ...(child.exclude ? { exclude: child.exclude } : {}),
209
- };
210
325
  }
211
326
  function resolveDefinition(name, stack = []) {
212
- const resolvedName = resolveName(name);
213
- if (resolvedDefinitions.has(resolvedName))
214
- return resolvedDefinitions.get(resolvedName);
215
- if (stack.includes(resolvedName))
327
+ if (resolvedDefinitions.has(name))
328
+ return resolvedDefinitions.get(name);
329
+ if (stack.length >= MAX_INHERITANCE_DEPTH)
216
330
  return undefined;
217
- const raw = rawDefinitions.get(resolvedName);
331
+ if (stack.includes(name))
332
+ return undefined;
333
+ const raw = rawDefinitions.get(name);
218
334
  if (!raw)
219
335
  return undefined;
220
336
  const base = raw.from
221
- ? resolveDefinition(raw.from, [...stack, resolvedName])
337
+ ? resolveDefinition(raw.from, [...stack, name])
222
338
  : undefined;
223
339
  if (raw.from && !base)
224
340
  return undefined;
@@ -233,183 +349,128 @@ export function loadRecipeAgentDefinitions(recipeDir) {
233
349
  mcp: mergeMcp(base?.mcp, raw.mcp),
234
350
  skills: raw.skills ?? base?.skills ?? [],
235
351
  subagents: raw.subagents ?? base?.subagents ?? [],
236
- skillsDeclared: raw.skills !== undefined || base?.skillsDeclared === true,
237
- subagentsDeclared: raw.subagents !== undefined || base?.subagentsDeclared === true,
238
- extensions: mergeExtensions(base?.extensions, raw.extensions),
239
- systemInstructions: raw.systemInstructions ?? base?.systemInstructions,
352
+ systemInstructions: mergeSystemInstructions(base?.systemInstructions, raw.systemInstructions),
353
+ declaredFields: [...(raw.declaredFields ?? [])],
240
354
  };
241
- resolvedDefinitions.set(resolvedName, definition);
355
+ resolvedDefinitions.set(name, definition);
242
356
  return definition;
243
357
  }
244
358
  for (const name of rawDefinitions.keys()) {
245
359
  const definition = resolveDefinition(name);
246
- if (!definition) {
247
- console.warn(`[recipes] skipping agent "${name}": ${fromChainSkipReason(name, rawDefinitions, resolveName)}`);
360
+ if (!definition)
248
361
  continue;
249
- }
250
362
  definitions.set(name, definition);
251
363
  }
252
- for (const [alias, name] of aliases) {
253
- if (definitions.has(alias))
254
- continue;
255
- const definition = definitions.get(name);
256
- if (definition)
257
- definitions.set(alias, definition);
258
- }
259
364
  return definitions;
260
365
  }
261
- /** Explain why an agent's from: chain failed to resolve (cycle or missing base). */
262
- function fromChainSkipReason(name, rawDefinitions, resolveName) {
263
- const stack = [];
264
- let current = resolveName(name);
265
- for (;;) {
266
- const raw = rawDefinitions.get(current);
267
- if (!raw?.from)
268
- return "from: chain failed to resolve";
269
- const next = resolveName(raw.from);
270
- if (!rawDefinitions.has(next)) {
271
- return `from: "${raw.from}" does not exist in the recipe`;
272
- }
273
- if (stack.includes(next) || next === current) {
274
- return `from: cycle detected (${[...stack, current, next].join(" -> ")})`;
275
- }
276
- stack.push(current);
277
- current = next;
278
- }
279
- }
280
- function recipeAgentFieldProvided(definition, field) {
281
- if (field === "model.name")
282
- return Boolean(definition.model?.name);
283
- if (field === "model.thinkingLevel")
284
- return Boolean(definition.model?.thinkingLevel);
285
- if (field === "tools")
286
- return definition.tools !== undefined;
287
- if (field === "skills")
288
- return definition.skills !== undefined;
289
- if (field === "subagents")
290
- return definition.subagents !== undefined;
291
- return definition.systemInstructions !== undefined;
292
- }
293
- export function validateResolvedRecipeAgentDefinition(opts) {
366
+ function validateResolvedRecipeAgentSource(opts, sources) {
294
367
  const rawDefinitions = new Map();
295
- const aliases = new Map();
296
- const explicitNames = new Map();
297
- for (const source of readRecipeAgentSources(opts.recipeDir)) {
368
+ for (const source of sources) {
298
369
  rawDefinitions.set(source.definition.name, source.definition);
299
- aliases.set(source.fallbackName, source.definition.name);
300
- explicitNames.set(source.definition.name, source.explicitName);
301
- }
302
- function resolveName(name) {
303
- return rawDefinitions.has(name) ? name : aliases.get(name) ?? name;
304
370
  }
305
371
  function inheritanceFinding(name, stack = []) {
306
- const resolvedName = resolveName(name);
307
- const definition = rawDefinitions.get(resolvedName);
372
+ if (stack.length >= MAX_INHERITANCE_DEPTH) {
373
+ return {
374
+ agentName: name,
375
+ field: "from",
376
+ message: `Recipe agent "${name}" exceeds the maximum from depth of ${MAX_INHERITANCE_DEPTH}`,
377
+ };
378
+ }
379
+ const definition = rawDefinitions.get(name);
308
380
  if (!definition) {
309
381
  return {
310
- agentName: resolvedName,
382
+ agentName: name,
311
383
  field: "from",
312
- message: `Recipe agent "${resolvedName}" was not found`,
384
+ message: `Recipe agent "${name}" was not found`,
313
385
  };
314
386
  }
315
387
  if (!definition.from)
316
388
  return undefined;
317
- const resolvedFrom = resolveName(definition.from);
389
+ const resolvedFrom = definition.from;
318
390
  if (stack.includes(resolvedFrom)) {
319
391
  return {
320
- agentName: resolvedName,
392
+ agentName: name,
321
393
  field: "from",
322
- message: `Recipe agent "${resolvedName}" has cyclic from chain: ${[
394
+ message: `Recipe agent "${name}" has cyclic from chain: ${[
323
395
  ...stack,
324
- resolvedName,
396
+ name,
325
397
  resolvedFrom,
326
398
  ].join(" -> ")}`,
327
399
  };
328
400
  }
329
401
  if (!rawDefinitions.has(resolvedFrom)) {
330
402
  return {
331
- agentName: resolvedName,
403
+ agentName: name,
332
404
  field: "from",
333
- message: `Recipe agent "${resolvedName}" inherits from missing agent "${definition.from}"`,
405
+ message: `Recipe agent "${name}" inherits from missing agent "${definition.from}"`,
334
406
  };
335
407
  }
336
- return inheritanceFinding(definition.from, [...stack, resolvedName]);
408
+ return inheritanceFinding(definition.from, [...stack, name]);
337
409
  }
338
- function resolvedFieldProvided(name, field, stack = []) {
339
- const resolvedName = resolveName(name);
340
- if (stack.includes(resolvedName))
410
+ function resolvedModelProvided(name, stack = []) {
411
+ if (stack.includes(name) ||
412
+ stack.length >= MAX_INHERITANCE_DEPTH)
341
413
  return false;
342
- const definition = rawDefinitions.get(resolvedName);
414
+ const definition = rawDefinitions.get(name);
343
415
  if (!definition)
344
416
  return false;
345
- if (recipeAgentFieldProvided(definition, field))
417
+ if (definition.model?.name)
346
418
  return true;
347
419
  return definition.from
348
- ? resolvedFieldProvided(definition.from, field, [...stack, resolvedName])
420
+ ? resolvedModelProvided(definition.from, [...stack, name])
349
421
  : false;
350
422
  }
351
423
  function resolvedTools(name, stack = []) {
352
- const resolvedName = resolveName(name);
353
- if (stack.includes(resolvedName))
424
+ if (stack.includes(name) ||
425
+ stack.length >= MAX_INHERITANCE_DEPTH)
354
426
  return undefined;
355
- const definition = rawDefinitions.get(resolvedName);
427
+ const definition = rawDefinitions.get(name);
356
428
  if (!definition)
357
429
  return undefined;
358
430
  if (definition.tools !== undefined)
359
431
  return definition.tools;
360
432
  return definition.from
361
- ? resolvedTools(definition.from, [...stack, resolvedName])
433
+ ? resolvedTools(definition.from, [...stack, name])
362
434
  : undefined;
363
435
  }
364
436
  function resolvedMcp(name, stack = []) {
365
- const resolvedName = resolveName(name);
366
- if (stack.includes(resolvedName))
437
+ if (stack.includes(name) ||
438
+ stack.length >= MAX_INHERITANCE_DEPTH)
367
439
  return undefined;
368
- const definition = rawDefinitions.get(resolvedName);
440
+ const definition = rawDefinitions.get(name);
369
441
  if (!definition)
370
442
  return undefined;
371
443
  const base = definition.from
372
- ? resolvedMcp(definition.from, [...stack, resolvedName])
444
+ ? resolvedMcp(definition.from, [...stack, name])
373
445
  : undefined;
374
446
  return mergeMcp(base, definition.mcp);
375
447
  }
376
448
  function rawMcpChainInvalid(name, stack = []) {
377
- const resolvedName = resolveName(name);
378
- if (stack.includes(resolvedName))
449
+ if (stack.includes(name) ||
450
+ stack.length >= MAX_INHERITANCE_DEPTH)
379
451
  return false;
380
- const definition = rawDefinitions.get(resolvedName);
452
+ const definition = rawDefinitions.get(name);
381
453
  if (!definition)
382
454
  return false;
383
455
  const rawMcp = definition.mcp;
384
456
  if (rawMcp?.[INVALID_AGENT_MCP] ||
385
- Object.values(rawMcp ?? {}).some((selection) => !isValidRecipeMcpToolSelection(selection))) {
457
+ Object.values(rawMcp?.servers ?? {}).some((selection) => !isValidRecipeMcpToolSelection(selection))) {
386
458
  return true;
387
459
  }
388
460
  return definition.from
389
- ? rawMcpChainInvalid(definition.from, [...stack, resolvedName])
461
+ ? rawMcpChainInvalid(definition.from, [...stack, name])
390
462
  : false;
391
463
  }
392
- const agentName = resolveName(opts.agentName);
464
+ const agentName = opts.agentName;
393
465
  const findings = [];
394
- if (opts.requireExplicitName && explicitNames.get(agentName) !== true) {
395
- findings.push({
396
- agentName,
397
- field: "name",
398
- severity: "warning",
399
- message: `Recipe agent "${agentName}" must declare name`,
400
- });
401
- }
402
466
  const inheritance = inheritanceFinding(agentName);
403
467
  if (inheritance)
404
468
  findings.push(inheritance);
405
- for (const field of opts.requiredFields ?? []) {
406
- if (resolvedFieldProvided(agentName, field))
407
- continue;
469
+ if (!resolvedModelProvided(agentName)) {
408
470
  findings.push({
409
471
  agentName,
410
- field,
411
- ...(field === "model.name" ? {} : { severity: "warning" }),
412
- message: `Recipe agent "${agentName}" must declare ${field} directly or inherit it with from`,
472
+ field: "model.name",
473
+ message: `Recipe agent "${agentName}" must declare model.name directly or inherit it with from`,
413
474
  });
414
475
  }
415
476
  const mcp = resolvedMcp(agentName);
@@ -420,10 +481,10 @@ export function validateResolvedRecipeAgentDefinition(opts) {
420
481
  server.tools,
421
482
  ]))
422
483
  : undefined;
423
- // Rust recipe-check owns detailed authoring diagnostics. This runtime guard
484
+ // The Recipe checker owns detailed authoring diagnostics. This session guard
424
485
  // only ensures malformed raw policies fail closed before agent startup.
425
486
  const invalidMcpPolicy = rawMcpChainInvalid(agentName) ||
426
- Object.entries(mcp ?? {}).some(([serverId, selection]) => {
487
+ Object.entries(mcp?.servers ?? {}).some(([serverId, selection]) => {
427
488
  if (!serverId.trim() || !isValidRecipeMcpToolSelection(selection)) {
428
489
  return true;
429
490
  }
@@ -436,17 +497,25 @@ export function validateResolvedRecipeAgentDefinition(opts) {
436
497
  const value = toolName.trim();
437
498
  return value !== "*" && !mcpSelectionAllowsTool(packageSelection, value);
438
499
  });
439
- });
500
+ }) ||
501
+ Object.values(mcp?.servers ?? {}).some((selection) => (mcp?.mode !== "tools" &&
502
+ (selection.defer !== undefined || selection.eager !== undefined)) ||
503
+ !validActivationSelectors(selection.defer) ||
504
+ !validActivationSelectors(selection.eager) ||
505
+ [...(selection.defer ?? []), ...(selection.eager ?? [])].some((selector) => selector !== "*" && !mcpSelectionAllowsTool(selection, selector)));
440
506
  if (invalidMcpPolicy) {
441
507
  findings.push({
442
508
  agentName,
443
509
  field: "mcp",
444
510
  code: "mcp_invalid",
445
- message: `Recipe agent "${agentName}" has an invalid MCP policy; run introspection check for details`,
511
+ message: `Recipe agent "${agentName}" has an invalid MCP policy`,
446
512
  });
447
513
  }
448
514
  return findings;
449
515
  }
516
+ export function validateResolvedRecipeAgentDefinition(opts) {
517
+ return validateResolvedRecipeAgentSource(opts, readRecipeAgentSources(opts.recipeDir));
518
+ }
450
519
  function isValidRecipeModelSpec(spec) {
451
520
  const slash = spec.indexOf("/");
452
521
  if (slash <= 0 || slash === spec.length - 1)
@@ -470,15 +539,11 @@ function validateRecipeAgentModelSpecs(sources) {
470
539
  }
471
540
  function validateRecipeAgentNames(sources) {
472
541
  const findings = [];
473
- const explicitNameCounts = new Map();
474
- const explicitNames = new Set();
542
+ const nameCounts = new Map();
475
543
  for (const source of sources) {
476
- if (!source.explicitName)
477
- continue;
478
- explicitNames.add(source.definition.name);
479
- explicitNameCounts.set(source.definition.name, (explicitNameCounts.get(source.definition.name) ?? 0) + 1);
544
+ nameCounts.set(source.definition.name, (nameCounts.get(source.definition.name) ?? 0) + 1);
480
545
  }
481
- for (const [name, count] of explicitNameCounts) {
546
+ for (const [name, count] of nameCounts) {
482
547
  if (count <= 1)
483
548
  continue;
484
549
  findings.push({
@@ -487,20 +552,27 @@ function validateRecipeAgentNames(sources) {
487
552
  message: `Recipe agent name "${name}" is declared by multiple files`,
488
553
  });
489
554
  }
490
- for (const source of sources) {
491
- if (source.fallbackName === source.definition.name ||
492
- !explicitNames.has(source.fallbackName)) {
493
- continue;
494
- }
495
- findings.push({
496
- agentName: source.definition.name,
497
- field: "name",
498
- message: `Recipe agent file alias "${source.fallbackName}" conflicts with an explicit agent name`,
499
- });
500
- }
501
555
  return findings;
502
556
  }
503
- export function validateRecipeAgentDefinitions(recipeDir) {
557
+ function validateRecipeAgentSources(recipeDir, sources, invalidFileFindings) {
558
+ const agentNames = [
559
+ ...new Set(sources.map((source) => source.definition.name)),
560
+ ].sort();
561
+ return [
562
+ ...invalidFileFindings,
563
+ ...validateRecipeAgentNames(sources),
564
+ ...validateRecipeAgentModelSpecs(sources),
565
+ ...agentNames.flatMap((agentName) => validateResolvedRecipeAgentSource({
566
+ recipeDir,
567
+ agentName,
568
+ }, sources)),
569
+ ];
570
+ }
571
+ /**
572
+ * Parse agent YAML once, then validate and resolve the complete inheritance
573
+ * Recipe snapshot from that exact source.
574
+ */
575
+ export function loadValidatedRecipeAgentDefinitions(recipeDir) {
504
576
  const invalidFileFindings = [];
505
577
  const sources = readRecipeAgentSources(recipeDir, {
506
578
  onInvalidFile: (path, error) => {
@@ -511,44 +583,20 @@ export function validateRecipeAgentDefinitions(recipeDir) {
511
583
  });
512
584
  },
513
585
  });
514
- const agentNames = [...new Set(sources.map((source) => source.definition.name))].sort();
515
- return [
516
- ...invalidFileFindings,
517
- ...validateRecipeAgentNames(sources),
518
- ...validateRecipeAgentModelSpecs(sources),
519
- ...agentNames.flatMap((agentName) => validateResolvedRecipeAgentDefinition({
520
- recipeDir,
521
- agentName,
522
- requireExplicitName: true,
523
- requiredFields: REQUIRED_RECIPE_AGENT_FIELDS,
524
- })),
525
- ];
526
- }
527
- export function resolveRecipeAgentName(opts) {
528
- if (opts.agentName?.trim())
529
- return opts.agentName.trim();
530
- const definitions = loadRecipeAgentDefinitions(opts.recipeDir);
531
- const defaultAgent = definitions.get("agent");
532
- if (defaultAgent)
533
- return defaultAgent.name;
534
- const uniqueNames = [
535
- ...new Set([...definitions.values()].map((definition) => definition.name)),
536
- ];
537
- if (uniqueNames.length === 1)
538
- return uniqueNames[0];
539
- if (uniqueNames.length === 0)
540
- return "agent";
541
- throw new Error("Recipe has multiple agents and no default entrypoint; add agents/agent.yaml or set PI_AGENT_NAME");
586
+ const definitions = definitionsFromSources(sources);
587
+ return {
588
+ definitions,
589
+ findings: validateRecipeAgentSources(recipeDir, sources, invalidFileFindings),
590
+ };
542
591
  }
543
- export function resolveRecipeAgentDefinition(opts) {
544
- const agentName = resolveRecipeAgentName(opts);
545
- const agent = loadRecipeAgentDefinitions(opts.recipeDir).get(agentName) ?? null;
546
- return { agentName, agent };
592
+ export function validateRecipeAgentDefinitions(recipeDir) {
593
+ return loadValidatedRecipeAgentDefinitions(recipeDir).findings;
547
594
  }
548
595
  export function loadRecipeSystemPrompt(recipeDir) {
549
596
  const path = join(recipeDir, "SYSTEM.md");
550
597
  if (!existsSync(path))
551
598
  return undefined;
599
+ assertRecipePathContained(recipeDir, path, "SYSTEM.md");
552
600
  const content = readFileSync(path, "utf8").trim();
553
601
  return content || undefined;
554
602
  }