@llblab/pi-actors 0.46.0 → 0.46.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 (50) hide show
  1. package/BACKLOG.md +582 -0
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +1 -1
  4. package/dist/lib/async-runs.d.ts +1 -0
  5. package/dist/lib/async-runs.js +4 -1
  6. package/dist/lib/execution.d.ts +1 -0
  7. package/dist/lib/execution.js +1 -0
  8. package/dist/lib/extension-runtime.js +25 -9
  9. package/dist/lib/inspector.js +1 -0
  10. package/dist/lib/prompts.d.ts +1 -1
  11. package/dist/lib/prompts.js +2 -2
  12. package/dist/lib/recipes-context.d.ts +12 -3
  13. package/dist/lib/recipes-context.js +28 -4
  14. package/dist/lib/recipes-discovery.d.ts +18 -0
  15. package/dist/lib/recipes-discovery.js +95 -22
  16. package/dist/lib/recipes-references.d.ts +18 -0
  17. package/dist/lib/recipes-references.js +120 -33
  18. package/dist/lib/registry.d.ts +14 -1
  19. package/dist/lib/registry.js +110 -87
  20. package/dist/lib/runtime.d.ts +30 -3
  21. package/dist/lib/runtime.js +73 -9
  22. package/dist/lib/tools-inspect.d.ts +3 -0
  23. package/dist/lib/tools-inspect.js +40 -20
  24. package/dist/lib/tools-local.d.ts +2 -2
  25. package/dist/lib/tools-local.js +5 -2
  26. package/dist/lib/tools-response.js +2 -0
  27. package/dist/lib/tools-spawn.d.ts +2 -2
  28. package/dist/lib/tools-spawn.js +1 -1
  29. package/dist/lib/tools.d.ts +4 -1
  30. package/dist/lib/tools.js +2 -0
  31. package/dist/skills/actors/SKILL.md +3 -3
  32. package/docs/template-recipes.md +3 -1
  33. package/docs/tool-registry.md +9 -3
  34. package/lib/async-runs.ts +5 -1
  35. package/lib/execution.ts +2 -0
  36. package/lib/extension-runtime.ts +33 -14
  37. package/lib/inspector.ts +1 -0
  38. package/lib/prompts.ts +2 -2
  39. package/lib/recipes-context.ts +49 -4
  40. package/lib/recipes-discovery.ts +164 -24
  41. package/lib/recipes-references.ts +163 -39
  42. package/lib/registry.ts +185 -94
  43. package/lib/runtime.ts +108 -12
  44. package/lib/tools-inspect.ts +57 -23
  45. package/lib/tools-local.ts +9 -3
  46. package/lib/tools-response.ts +2 -0
  47. package/lib/tools-spawn.ts +3 -2
  48. package/lib/tools.ts +4 -1
  49. package/package.json +1 -1
  50. package/skills/actors/SKILL.md +3 -3
@@ -1,15 +1,60 @@
1
1
  /**
2
- * Recipe context prompt assembly.
3
- * Zones: async runner prompt context, recipe provenance, LLM child launches
4
- * Owns compact actor recipe context records appended to child-agent prompts.
2
+ * Recipe context contracts and prompt assembly.
3
+ * Zones: live session resolution, async runner prompt context, recipe provenance, LLM child launches
4
+ * Owns the immutable live Recipe environment and compact actor context appended to child-agent prompts.
5
5
  */
6
6
 
7
+ import { createHash } from "node:crypto";
7
8
  import { writeFileSync } from "node:fs";
8
- import { basename } from "node:path";
9
+ import { basename, resolve } from "node:path";
9
10
 
10
11
  import type { CommandTemplateActorRecipeContext } from "./command-templates.ts";
12
+ import * as RecipesReferences from "./recipes-references.ts";
11
13
  import type { TemplateRecipeContextRecord } from "./recipes-references.ts";
12
14
 
15
+ export interface RecipeResolutionContext {
16
+ readonly activeSkills: RecipesReferences.ActiveSkillRecipeContext;
17
+ readonly cwd: string;
18
+ readonly generation: string;
19
+ readonly sessionId: string;
20
+ }
21
+
22
+ export function createRecipeResolutionContext(
23
+ sessionId: string,
24
+ cwd: string,
25
+ activeSkills: RecipesReferences.ActiveSkillRecipeContext,
26
+ ): RecipeResolutionContext {
27
+ const normalizedSessionId = sessionId.trim();
28
+ if (!normalizedSessionId) throw new Error("Recipe resolution session id is required.");
29
+ const normalizedCwd = resolve(cwd);
30
+ const generation = createHash("sha256")
31
+ .update(
32
+ JSON.stringify({
33
+ activeSkills: RecipesReferences.getActiveSkillRecipeNamespaces(activeSkills),
34
+ cwd: normalizedCwd,
35
+ sessionId: normalizedSessionId,
36
+ }),
37
+ )
38
+ .digest("hex");
39
+ return Object.freeze({
40
+ activeSkills,
41
+ cwd: normalizedCwd,
42
+ generation,
43
+ sessionId: normalizedSessionId,
44
+ });
45
+ }
46
+
47
+ export function createEmptyRecipeResolutionContext(
48
+ sessionId: string,
49
+ cwd: string,
50
+ ): RecipeResolutionContext {
51
+ return createRecipeResolutionContext(
52
+ sessionId,
53
+ cwd,
54
+ RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT,
55
+ );
56
+ }
57
+
13
58
  export interface MarkedRecipeContextRecord extends TemplateRecipeContextRecord {
14
59
  you_are_here?: true;
15
60
  you_are_here_path?: string;
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import * as CommandTemplates from "./command-templates.ts";
13
13
  import type { RegisteredTool } from "./config.ts";
14
14
  import * as ModelContext from "./model-context.ts";
15
+ import type { RecipeResolutionContext } from "./recipes-context.ts";
15
16
  import type { TemplateRecipeConfig } from "./recipes-references.ts";
16
17
  import * as RecipesReferences from "./recipes-references.ts";
17
18
  import * as RecipesUsage from "./recipes-usage.ts";
@@ -29,6 +30,7 @@ export interface DiscoveredRecipe {
29
30
  disabled: boolean;
30
31
  tool: boolean;
31
32
  mutableUsage: boolean;
33
+ resolutionContext?: RecipeResolutionContext;
32
34
  diagnostics: string[];
33
35
  riskLabels: CommandTemplates.CommandTemplateRiskLabel[];
34
36
  shadows: string[];
@@ -58,6 +60,22 @@ export interface RecipesDiscoverySource {
58
60
  file?: string;
59
61
  defaultTool?: boolean;
60
62
  mutableUsage?: boolean;
63
+ resolutionContext?: RecipeResolutionContext;
64
+ }
65
+
66
+ export interface UserRecipeAdmission {
67
+ args?: string[];
68
+ argTypes?: RegisteredTool["argTypes"];
69
+ artifacts?: Record<string, string>;
70
+ async?: boolean;
71
+ control?: string[];
72
+ defaults?: Record<string, unknown>;
73
+ diagnostics: string[];
74
+ effectiveRecipe?: TemplateRecipeConfig;
75
+ identity: string;
76
+ sourcePath: string;
77
+ tool?: RegisteredTool;
78
+ validated: boolean;
61
79
  }
62
80
 
63
81
  function assertToolSafeRepeatConfig(
@@ -185,10 +203,13 @@ function readDiscoveredRecipe(
185
203
  priority: number,
186
204
  defaultTool = false,
187
205
  mutableUsage = false,
206
+ resolutionContext?: RecipeResolutionContext,
188
207
  ): DiscoveredRecipe {
189
208
  const id = RecipesReferences.getRecipeIdFromPath(file);
190
209
  try {
191
- const config = RecipesReferences.readResolvedRecipeConfig(file);
210
+ const config = RecipesReferences.readResolvedRecipeConfig(file, [], {
211
+ skillContext: resolutionContext?.activeSkills,
212
+ });
192
213
  const invalid = !config;
193
214
  const disabled = config?.disabled === true;
194
215
  return {
@@ -203,6 +224,7 @@ function readDiscoveredRecipe(
203
224
  disabled,
204
225
  tool: defaultTool && !disabled && !invalid,
205
226
  mutableUsage,
227
+ resolutionContext,
206
228
  diagnostics: getRecipeConfigDiagnostics(file, config),
207
229
  riskLabels: getRecipeRiskLabels(config),
208
230
  shadows: [],
@@ -219,6 +241,7 @@ function readDiscoveredRecipe(
219
241
  disabled: false,
220
242
  tool: false,
221
243
  mutableUsage,
244
+ resolutionContext,
222
245
  diagnostics: [
223
246
  `Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
224
247
  ],
@@ -233,6 +256,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
233
256
  file: string;
234
257
  defaultTool: boolean;
235
258
  mutableUsage: boolean;
259
+ resolutionContext?: RecipeResolutionContext;
236
260
  }> {
237
261
  const defaultTool = source.defaultTool === true;
238
262
  const mutableUsage = source.mutableUsage === true;
@@ -243,6 +267,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
243
267
  file: source.file,
244
268
  defaultTool,
245
269
  mutableUsage,
270
+ resolutionContext: source.resolutionContext,
246
271
  },
247
272
  ];
248
273
  return source.root
@@ -251,6 +276,7 @@ function filesForSource(source: RecipesDiscoverySource): Array<{
251
276
  file,
252
277
  defaultTool,
253
278
  mutableUsage,
279
+ resolutionContext: source.resolutionContext,
254
280
  }))
255
281
  : [];
256
282
  }
@@ -338,8 +364,16 @@ export function discoverRecipeSources(
338
364
  sources: RecipesDiscoverySource[],
339
365
  ): RecipesDiscoveryResult {
340
366
  const entries = sources.flatMap((source, priority) =>
341
- filesForSource(source).map(({ root, file, defaultTool, mutableUsage }) =>
342
- readDiscoveredRecipe(root, file, priority, defaultTool, mutableUsage),
367
+ filesForSource(source).map(
368
+ ({ root, file, defaultTool, mutableUsage, resolutionContext }) =>
369
+ readDiscoveredRecipe(
370
+ root,
371
+ file,
372
+ priority,
373
+ defaultTool,
374
+ mutableUsage,
375
+ resolutionContext,
376
+ ),
343
377
  ),
344
378
  );
345
379
  const byId = new Map<string, DiscoveredRecipe[]>();
@@ -836,14 +870,13 @@ export function summarizeDiscovery(
836
870
  };
837
871
  }
838
872
 
839
- export function toRegisteredTool(
840
- entry: DiscoveredRecipe,
841
- ): RegisteredTool | undefined {
842
- if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
843
- return undefined;
844
- const cfg = entry.config;
845
- const template = entry.path;
846
- const description = cfg.description ?? `Execute template recipe: ${entry.id}`;
873
+ function projectRegisteredTool(
874
+ identity: string,
875
+ path: string,
876
+ cfg: TemplateRecipeConfig,
877
+ mutableUsage: boolean,
878
+ ): RegisteredTool {
879
+ const description = cfg.description ?? `Execute template recipe: ${identity}`;
847
880
  const argTemplate = cfg.template;
848
881
  const argTemplateConfig =
849
882
  typeof argTemplate === "object" && !Array.isArray(argTemplate)
@@ -867,27 +900,38 @@ export function toRegisteredTool(
867
900
  explicitArgTypes,
868
901
  { ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) },
869
902
  );
870
- const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
871
- Schema.assertCompatibleToolArgTypes(explicitArgTypes, argTypes);
903
+ const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
904
+ Schema.assertCompatibleToolArgTypes(explicitArgTypes, inferredArgTypes);
905
+ const effectiveArgTypes = { ...inferredArgTypes, ...explicitArgTypes };
906
+ const args = Schema.getToolArgNames(argTemplateConfig).filter(
907
+ (arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg),
908
+ );
909
+ const callerArgSet = new Set(args);
910
+ const argTypes = Object.fromEntries(
911
+ Object.entries(effectiveArgTypes).filter(([arg]) => callerArgSet.has(arg)),
912
+ );
913
+ const defaults = Object.fromEntries(
914
+ Object.entries(cfg.defaults ?? {}).filter(([arg]) => callerArgSet.has(arg)),
915
+ );
916
+ const storedArgs = cfg.args?.filter((arg) =>
917
+ callerArgSet.has(Schema.parseToolArgToken(String(arg)).arg),
918
+ );
872
919
  return {
873
- name: entry.id,
920
+ name: identity,
874
921
  description,
875
- template,
922
+ template: path,
876
923
  recipe: cfg,
877
- args: Schema.getToolArgNames(argTemplateConfig),
924
+ args,
878
925
  defaults: Object.fromEntries(
879
- Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
880
- key,
881
- String(value),
882
- ]),
926
+ Object.entries(defaults).map(([key, value]) => [key, String(value)]),
883
927
  ),
884
928
  ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
885
- ...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
886
- ...(cfg.args ? { storedArgs: cfg.args } : {}),
887
- ...(cfg.defaults
929
+ ...(mutableUsage ? { sourcePath: path } : {}),
930
+ ...(storedArgs ? { storedArgs } : {}),
931
+ ...(Object.keys(defaults).length > 0
888
932
  ? {
889
933
  storedDefaults: Object.fromEntries(
890
- Object.entries(cfg.defaults).map(([key, value]) => [
934
+ Object.entries(defaults).map(([key, value]) => [
891
935
  key,
892
936
  String(value),
893
937
  ]),
@@ -896,3 +940,99 @@ export function toRegisteredTool(
896
940
  : {}),
897
941
  };
898
942
  }
943
+
944
+ function boundedAdmissionDiagnostic(error: unknown): string {
945
+ const message = error instanceof Error ? error.message : String(error);
946
+ return message.length <= 4096 ? message : `${message.slice(0, 4095)}…`;
947
+ }
948
+
949
+ export function admitUserRecipe(
950
+ sourcePath: string,
951
+ resolutionContext: RecipeResolutionContext,
952
+ authoredRecipe?: Record<string, unknown>,
953
+ mutableUsage = true,
954
+ ): UserRecipeAdmission {
955
+ const identity = RecipesReferences.getRecipeIdFromPath(sourcePath);
956
+ try {
957
+ const effectiveRecipe = RecipesReferences.readResolvedRecipeConfig(
958
+ sourcePath,
959
+ [],
960
+ {
961
+ ...(authoredRecipe
962
+ ? { authoredEntry: { path: sourcePath, recipe: authoredRecipe } }
963
+ : {}),
964
+ skillContext: resolutionContext.activeSkills,
965
+ },
966
+ );
967
+ if (!effectiveRecipe) {
968
+ return {
969
+ diagnostics: [
970
+ authoredRecipe
971
+ ? "Recipe must define a valid template."
972
+ : RecipesReferences.diagnoseRawRecipeConfigFailure(sourcePath) ??
973
+ "Recipe must define a valid template.",
974
+ ],
975
+ identity,
976
+ sourcePath,
977
+ validated: false,
978
+ };
979
+ }
980
+ if (effectiveRecipe.disabled === true) {
981
+ return {
982
+ diagnostics: ["Recipe is disabled."],
983
+ effectiveRecipe,
984
+ identity,
985
+ sourcePath,
986
+ validated: false,
987
+ };
988
+ }
989
+ const tool = projectRegisteredTool(
990
+ identity,
991
+ sourcePath,
992
+ effectiveRecipe,
993
+ mutableUsage,
994
+ );
995
+ return {
996
+ args: tool.args,
997
+ argTypes: tool.argTypes,
998
+ artifacts: effectiveRecipe.artifacts,
999
+ async: effectiveRecipe.async === true,
1000
+ control: effectiveRecipe.control,
1001
+ defaults: effectiveRecipe.defaults,
1002
+ diagnostics: [],
1003
+ effectiveRecipe,
1004
+ identity,
1005
+ sourcePath,
1006
+ tool,
1007
+ validated: true,
1008
+ };
1009
+ } catch (error) {
1010
+ return {
1011
+ diagnostics: [boundedAdmissionDiagnostic(error)],
1012
+ identity,
1013
+ sourcePath,
1014
+ validated: false,
1015
+ };
1016
+ }
1017
+ }
1018
+
1019
+ export function toRegisteredTool(
1020
+ entry: DiscoveredRecipe,
1021
+ ): RegisteredTool | undefined {
1022
+ if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
1023
+ return undefined;
1024
+ if (entry.resolutionContext) {
1025
+ return admitUserRecipe(
1026
+ entry.path,
1027
+ entry.resolutionContext,
1028
+ undefined,
1029
+ entry.mutableUsage,
1030
+ ).tool;
1031
+ }
1032
+ return projectRegisteredTool(
1033
+ entry.id,
1034
+ entry.path,
1035
+ entry.config,
1036
+ entry.mutableUsage,
1037
+ );
1038
+ }
@@ -87,11 +87,34 @@ export interface TemplateRecipeContextRecord {
87
87
  }
88
88
 
89
89
  export interface ReadResolvedRecipeConfigOptions {
90
+ authoredEntry?: { path: string; recipe: Record<string, unknown> };
90
91
  includeActorRecipeContext?: boolean;
91
92
  skillContext?: ActiveSkillRecipeContext;
92
93
  }
93
94
 
94
- const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
95
+ const RESERVED_AUTHORED_RECIPE_VALUE_NAMES = new Set([
96
+ "recipe_dir",
97
+ "skill_dir",
98
+ ]);
99
+
100
+ export const RUNTIME_OWNED_RECIPE_INPUT_NAMES = Object.freeze([
101
+ "owner_id",
102
+ "recipe_dir",
103
+ "run_instance_id",
104
+ "runtime_state_root",
105
+ "session_id",
106
+ "skill_dir",
107
+ "state_dir",
108
+ "trace_file",
109
+ ]);
110
+
111
+ const RUNTIME_OWNED_RECIPE_INPUT_SET = new Set(
112
+ RUNTIME_OWNED_RECIPE_INPUT_NAMES,
113
+ );
114
+
115
+ export function isRuntimeOwnedRecipeInput(name: string): boolean {
116
+ return RUNTIME_OWNED_RECIPE_INPUT_SET.has(name);
117
+ }
95
118
 
96
119
  export interface ActiveSkillRecipeSource {
97
120
  name: string;
@@ -149,6 +172,19 @@ export interface ActiveSkillRecipeComponent {
149
172
  stem: string;
150
173
  }
151
174
 
175
+ export interface SkillRecipeComponentDiagnostic {
176
+ file?: string;
177
+ reason: string;
178
+ skill: string;
179
+ stem?: string;
180
+ }
181
+
182
+ export interface SkillRecipeComponentInventory {
183
+ components: ActiveSkillRecipeComponent[];
184
+ partial: boolean;
185
+ rejected: SkillRecipeComponentDiagnostic[];
186
+ }
187
+
152
188
  function nestedRecipeFiles(root: string): string[] {
153
189
  if (!existsSync(root)) return [];
154
190
  return readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
@@ -158,20 +194,32 @@ function nestedRecipeFiles(root: string): string[] {
158
194
  });
159
195
  }
160
196
 
161
- export function listActiveSkillRecipeComponents(
197
+ export function inventoryActiveSkillRecipeComponents(
162
198
  context: ActiveSkillRecipeContext,
163
- ): ActiveSkillRecipeComponent[] {
199
+ ): SkillRecipeComponentInventory {
164
200
  const components: ActiveSkillRecipeComponent[] = [];
201
+ const rejected: SkillRecipeComponentDiagnostic[] = [];
165
202
  for (const [skill, roots] of Object.entries(context.namespaces)) {
166
- if (roots.length > 1)
167
- throw new Error(`Duplicate active Skill identity ${skill}: ${roots.join(", ")}`);
203
+ if (roots.length > 1) {
204
+ rejected.push({
205
+ reason: `Duplicate active Skill identity ${skill}: ${roots.join(", ")}`,
206
+ skill,
207
+ });
208
+ continue;
209
+ }
168
210
  const root = roots[0];
169
211
  if (!root || !existsSync(root)) continue;
170
212
  const direct = readdirSync(root, { withFileTypes: true });
171
213
  for (const entry of direct.filter((candidate) => candidate.isDirectory())) {
172
214
  const nested = nestedRecipeFiles(join(root, entry.name));
173
- if (nested.length > 0)
174
- throw new Error(`Nested Skill Recipe file is not addressable: ${nested[0]}`);
215
+ for (const file of nested) {
216
+ rejected.push({
217
+ file,
218
+ reason: `Nested Skill Recipe file is not addressable: ${file}`,
219
+ skill,
220
+ stem: getRecipeIdFromPath(file),
221
+ });
222
+ }
175
223
  }
176
224
  const files = direct
177
225
  .filter(
@@ -186,30 +234,62 @@ export function listActiveSkillRecipeComponents(
186
234
  byStem.set(stem, matches);
187
235
  }
188
236
  for (const [stem, matches] of [...byStem].sort(([a], [b]) => a.localeCompare(b))) {
189
- if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem))
190
- throw new Error(`Invalid Skill Recipe filename stem: ${stem}`);
191
- if (matches.length > 1)
192
- throw new Error(`Skill Recipe stem collision: ${skill}/${stem} has both .json and .md files`);
237
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(stem)) {
238
+ rejected.push({
239
+ file: matches[0],
240
+ reason: `Invalid Skill Recipe filename stem: ${stem}`,
241
+ skill,
242
+ stem,
243
+ });
244
+ continue;
245
+ }
246
+ if (matches.length > 1) {
247
+ rejected.push({
248
+ file: matches[0],
249
+ reason: `Skill Recipe stem collision: ${skill}/${stem} has both .json and .md files`,
250
+ skill,
251
+ stem,
252
+ });
253
+ continue;
254
+ }
193
255
  const file = matches[0];
194
- const raw = readRawRecipeConfig(file);
195
- if (!raw || !Object.hasOwn(raw, "template"))
196
- throw new Error(`Invalid Skill Recipe component: ${skill}/${stem}`);
197
- assertRecipeHasNoDeclaredName(raw);
198
- components.push({
199
- file,
200
- identity: `${skill}/${stem}`,
201
- imports: Object.fromEntries(
202
- Object.entries(getRecipeImports(raw)).map(([alias, value]) => [
203
- alias,
204
- getImportFrom(value),
205
- ]),
206
- ),
207
- skill,
208
- stem,
209
- });
256
+ try {
257
+ const raw = readRawRecipeConfig(file);
258
+ if (!raw || !Object.hasOwn(raw, "template"))
259
+ throw new Error(`Invalid Skill Recipe component: ${skill}/${stem}`);
260
+ assertRecipeHasNoDeclaredName(raw);
261
+ components.push({
262
+ file,
263
+ identity: `${skill}/${stem}`,
264
+ imports: Object.fromEntries(
265
+ Object.entries(getRecipeImports(raw)).map(([alias, value]) => [
266
+ alias,
267
+ getImportFrom(value),
268
+ ]),
269
+ ),
270
+ skill,
271
+ stem,
272
+ });
273
+ } catch (error) {
274
+ rejected.push({
275
+ file,
276
+ reason: error instanceof Error ? error.message : String(error),
277
+ skill,
278
+ stem,
279
+ });
280
+ }
210
281
  }
211
282
  }
212
- return components;
283
+ return { components, partial: rejected.length > 0, rejected };
284
+ }
285
+
286
+ export function listActiveSkillRecipeComponents(
287
+ context: ActiveSkillRecipeContext,
288
+ ): ActiveSkillRecipeComponent[] {
289
+ const inventory = inventoryActiveSkillRecipeComponents(context);
290
+ if (inventory.rejected.length > 0)
291
+ throw new Error(inventory.rejected[0].reason);
292
+ return inventory.components;
213
293
  }
214
294
 
215
295
  const SKILL_RECIPE_REFERENCE =
@@ -311,7 +391,7 @@ function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): voi
311
391
  const args = Array.isArray(raw.args)
312
392
  ? Schema.getExplicitToolArgNames(raw.args.map(String))
313
393
  : [];
314
- for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
394
+ for (const name of RESERVED_AUTHORED_RECIPE_VALUE_NAMES) {
315
395
  if (
316
396
  args.includes(name) ||
317
397
  (isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
@@ -1068,6 +1148,46 @@ function getDirectDelegatedRecipe(
1068
1148
  return undefined;
1069
1149
  }
1070
1150
 
1151
+ function resolveDirectDelegationArgs(
1152
+ delegated: TemplateRecipeConfig,
1153
+ wrapperArgs: unknown,
1154
+ wrapperDefaults: Record<string, unknown> | undefined,
1155
+ ): string[] | undefined {
1156
+ if (!Array.isArray(delegated.args))
1157
+ return Array.isArray(wrapperArgs) ? wrapperArgs.map(String) : undefined;
1158
+ const delegatedSpec = Schema.parseToolArgDeclarationList(delegated.args);
1159
+ if (delegatedSpec.error) throw new Error(delegatedSpec.error);
1160
+ const wrapperDeclarations = Array.isArray(wrapperArgs)
1161
+ ? wrapperArgs.map(String)
1162
+ : [];
1163
+ const wrapperSpec = Schema.parseToolArgDeclarationList(wrapperDeclarations);
1164
+ if (wrapperSpec.error) throw new Error(wrapperSpec.error);
1165
+ const declared = new Set(delegatedSpec.args);
1166
+ for (const arg of wrapperSpec.args) {
1167
+ if (!declared.has(arg))
1168
+ throw new Error(`Unknown delegated Recipe argument: ${arg}`);
1169
+ }
1170
+ Schema.assertCompatibleToolArgTypes(
1171
+ delegatedSpec.argTypes,
1172
+ wrapperSpec.argTypes,
1173
+ );
1174
+ assertRecipeDefaultsDeclared(
1175
+ delegated.args,
1176
+ wrapperDefaults,
1177
+ "delegated Recipe default",
1178
+ );
1179
+ Schema.normalizeRuntimeValues(
1180
+ Object.fromEntries(
1181
+ Object.entries(wrapperDefaults ?? {}).filter(
1182
+ ([, value]) =>
1183
+ typeof value !== "string" || !/\{[^{}]+\}/.test(value),
1184
+ ),
1185
+ ),
1186
+ delegatedSpec.argTypes,
1187
+ );
1188
+ return delegated.args;
1189
+ }
1190
+
1071
1191
  function expandImportNodes(
1072
1192
  value: CommandTemplateValue,
1073
1193
  imports: Record<string, ImportedRecipe>,
@@ -1157,7 +1277,10 @@ export function readResolvedRecipeConfig(
1157
1277
  `Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`,
1158
1278
  );
1159
1279
  }
1160
- const raw = readRawRecipeConfig(path);
1280
+ const raw =
1281
+ options.authoredEntry?.path === path
1282
+ ? options.authoredEntry.recipe
1283
+ : readRawRecipeConfig(path);
1161
1284
  if (!raw || !Object.hasOwn(raw, "template")) return undefined;
1162
1285
  assertRecipeHasNoDeclaredName(raw);
1163
1286
  RecipeControl.assertRecipeHasNoMailbox(raw);
@@ -1244,10 +1367,15 @@ export function readResolvedRecipeConfig(
1244
1367
  role: stack.length > 0 ? "import" : "entry",
1245
1368
  })
1246
1369
  : expandedTemplate;
1247
- const mergedDefaults = mergeDefaults(
1248
- delegated?.defaults,
1249
- isRecord(substituted.defaults) ? substituted.defaults : undefined,
1250
- );
1370
+ const wrapperDefaults = isRecord(substituted.defaults)
1371
+ ? substituted.defaults
1372
+ : undefined;
1373
+ const mergedDefaults = mergeDefaults(delegated?.defaults, wrapperDefaults);
1374
+ const effectiveArgs = delegated
1375
+ ? resolveDirectDelegationArgs(delegated, substituted.args, wrapperDefaults)
1376
+ : Array.isArray(substituted.args)
1377
+ ? (substituted.args as string[])
1378
+ : undefined;
1251
1379
  const artifactSource = isRecord(substituted.artifacts)
1252
1380
  ? substituted.artifacts
1253
1381
  : delegated?.artifacts;
@@ -1284,11 +1412,7 @@ export function readResolvedRecipeConfig(
1284
1412
  ? { imports: getRecipeImports(raw) }
1285
1413
  : {}),
1286
1414
  template: templateWithContext,
1287
- ...(Array.isArray(substituted.args)
1288
- ? { args: substituted.args as string[] }
1289
- : Array.isArray(delegated?.args)
1290
- ? { args: delegated.args }
1291
- : {}),
1415
+ ...(effectiveArgs ? { args: effectiveArgs } : {}),
1292
1416
  ...(mergedDefaults ? { defaults: mergedDefaults } : {}),
1293
1417
  ...(typeof substituted.parallel === "boolean"
1294
1418
  ? { parallel: substituted.parallel }