@llblab/pi-actors 0.46.0 → 0.47.0

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 (78) hide show
  1. package/AGENTS.md +7 -5
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +2 -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 +6 -5
  11. package/dist/lib/prompts.js +22 -23
  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 +22 -0
  15. package/dist/lib/recipes-discovery.js +108 -23
  16. package/dist/lib/recipes-references.d.ts +18 -0
  17. package/dist/lib/recipes-references.js +129 -38
  18. package/dist/lib/registry.d.ts +23 -6
  19. package/dist/lib/registry.js +294 -101
  20. package/dist/lib/runtime.d.ts +30 -3
  21. package/dist/lib/runtime.js +108 -9
  22. package/dist/lib/tools-inspect.d.ts +3 -0
  23. package/dist/lib/tools-inspect.js +190 -26
  24. package/dist/lib/tools-local.d.ts +2 -2
  25. package/dist/lib/tools-local.js +5 -2
  26. package/dist/lib/tools-register.js +2 -1
  27. package/dist/lib/tools-response.js +7 -1
  28. package/dist/lib/tools-spawn.d.ts +2 -2
  29. package/dist/lib/tools-spawn.js +1 -1
  30. package/dist/lib/tools.d.ts +4 -1
  31. package/dist/lib/tools.js +2 -0
  32. package/dist/scripts/conformance.mjs +1 -0
  33. package/dist/skills/actors/SKILL.md +76 -65
  34. package/dist/skills/actors/references/diagnostics.md +44 -0
  35. package/dist/skills/actors/references/persistent-tools.md +74 -0
  36. package/dist/skills/actors/references/recipes.md +51 -0
  37. package/dist/skills/actors/references/runs.md +39 -0
  38. package/dist/skills/artifacts/SKILL.md +24 -7
  39. package/dist/skills/media/SKILL.md +35 -7
  40. package/dist/skills/project-work/SKILL.md +28 -7
  41. package/dist/skills/recipe-memory/SKILL.md +27 -7
  42. package/dist/skills/swarm/SKILL.md +41 -445
  43. package/dist/skills/swarm/references/development-swarm.md +87 -539
  44. package/dist/skills/swarm/references/review-swarms.md +115 -0
  45. package/docs/README.md +5 -5
  46. package/docs/recipe-library.md +15 -10
  47. package/docs/template-recipes.md +3 -1
  48. package/docs/tool-registry.md +18 -6
  49. package/lib/async-runs.ts +5 -1
  50. package/lib/execution.ts +2 -0
  51. package/lib/extension-runtime.ts +33 -14
  52. package/lib/inspector.ts +1 -0
  53. package/lib/prompts.ts +24 -24
  54. package/lib/recipes-context.ts +49 -4
  55. package/lib/recipes-discovery.ts +186 -25
  56. package/lib/recipes-references.ts +176 -44
  57. package/lib/registry.ts +441 -113
  58. package/lib/runtime.ts +147 -12
  59. package/lib/tools-inspect.ts +254 -28
  60. package/lib/tools-local.ts +9 -3
  61. package/lib/tools-register.ts +4 -3
  62. package/lib/tools-response.ts +7 -1
  63. package/lib/tools-spawn.ts +3 -2
  64. package/lib/tools.ts +4 -1
  65. package/package.json +1 -1
  66. package/scripts/conformance.mjs +1 -0
  67. package/skills/actors/SKILL.md +76 -65
  68. package/skills/actors/references/diagnostics.md +44 -0
  69. package/skills/actors/references/persistent-tools.md +74 -0
  70. package/skills/actors/references/recipes.md +51 -0
  71. package/skills/actors/references/runs.md +39 -0
  72. package/skills/artifacts/SKILL.md +24 -7
  73. package/skills/media/SKILL.md +35 -7
  74. package/skills/project-work/SKILL.md +28 -7
  75. package/skills/recipe-memory/SKILL.md +27 -7
  76. package/skills/swarm/SKILL.md +41 -445
  77. package/skills/swarm/references/development-swarm.md +87 -539
  78. package/skills/swarm/references/review-swarms.md +115 -0
@@ -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,8 +241,9 @@ function readDiscoveredRecipe(
219
241
  disabled: false,
220
242
  tool: false,
221
243
  mutableUsage,
244
+ resolutionContext,
222
245
  diagnostics: [
223
- `Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
246
+ `Recipe ${id} rejected: ${error instanceof Error ? error.message : String(error)}`,
224
247
  ],
225
248
  riskLabels: [],
226
249
  shadows: [],
@@ -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,120 @@ export function toRegisteredTool(
896
940
  : {}),
897
941
  };
898
942
  }
943
+
944
+ export function summarizeRegisteredToolArgs(tool: RegisteredTool): {
945
+ optional: string[];
946
+ required: string[];
947
+ } {
948
+ const publicArgs = tool.args.filter(
949
+ (arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg),
950
+ );
951
+ const inlineDefaults = Schema.getExplicitToolArgDefaults(tool.recipe?.args);
952
+ const required = publicArgs.filter(
953
+ (arg) =>
954
+ !Object.hasOwn(tool.defaults, arg) &&
955
+ !Object.hasOwn(inlineDefaults, arg),
956
+ );
957
+ const requiredSet = new Set(required);
958
+ const optional = publicArgs.filter((arg) => !requiredSet.has(arg));
959
+ if (tool.recipe?.async === true) {
960
+ optional.push("run_id", "transport_context");
961
+ }
962
+ return { optional, required };
963
+ }
964
+
965
+ function boundedAdmissionDiagnostic(error: unknown): string {
966
+ const message = error instanceof Error ? error.message : String(error);
967
+ return message.length <= 4096 ? message : `${message.slice(0, 4095)}…`;
968
+ }
969
+
970
+ export function admitUserRecipe(
971
+ sourcePath: string,
972
+ resolutionContext: RecipeResolutionContext,
973
+ authoredRecipe?: Record<string, unknown>,
974
+ mutableUsage = true,
975
+ ): UserRecipeAdmission {
976
+ const identity = RecipesReferences.getRecipeIdFromPath(sourcePath);
977
+ try {
978
+ const effectiveRecipe = RecipesReferences.readResolvedRecipeConfig(
979
+ sourcePath,
980
+ [],
981
+ {
982
+ ...(authoredRecipe
983
+ ? { authoredEntry: { path: sourcePath, recipe: authoredRecipe } }
984
+ : {}),
985
+ skillContext: resolutionContext.activeSkills,
986
+ },
987
+ );
988
+ if (!effectiveRecipe) {
989
+ return {
990
+ diagnostics: [
991
+ authoredRecipe
992
+ ? "Recipe must define a valid template."
993
+ : RecipesReferences.diagnoseRawRecipeConfigFailure(sourcePath) ??
994
+ "Recipe must define a valid template.",
995
+ ],
996
+ identity,
997
+ sourcePath,
998
+ validated: false,
999
+ };
1000
+ }
1001
+ if (effectiveRecipe.disabled === true) {
1002
+ return {
1003
+ diagnostics: ["Recipe is disabled."],
1004
+ effectiveRecipe,
1005
+ identity,
1006
+ sourcePath,
1007
+ validated: false,
1008
+ };
1009
+ }
1010
+ const tool = projectRegisteredTool(
1011
+ identity,
1012
+ sourcePath,
1013
+ effectiveRecipe,
1014
+ mutableUsage,
1015
+ );
1016
+ return {
1017
+ args: tool.args,
1018
+ argTypes: tool.argTypes,
1019
+ artifacts: effectiveRecipe.artifacts,
1020
+ async: effectiveRecipe.async === true,
1021
+ control: effectiveRecipe.control,
1022
+ defaults: effectiveRecipe.defaults,
1023
+ diagnostics: [],
1024
+ effectiveRecipe,
1025
+ identity,
1026
+ sourcePath,
1027
+ tool,
1028
+ validated: true,
1029
+ };
1030
+ } catch (error) {
1031
+ return {
1032
+ diagnostics: [boundedAdmissionDiagnostic(error)],
1033
+ identity,
1034
+ sourcePath,
1035
+ validated: false,
1036
+ };
1037
+ }
1038
+ }
1039
+
1040
+ export function toRegisteredTool(
1041
+ entry: DiscoveredRecipe,
1042
+ ): RegisteredTool | undefined {
1043
+ if (!entry.tool || entry.invalid || entry.disabled || !entry.config)
1044
+ return undefined;
1045
+ if (entry.resolutionContext) {
1046
+ return admitUserRecipe(
1047
+ entry.path,
1048
+ entry.resolutionContext,
1049
+ undefined,
1050
+ entry.mutableUsage,
1051
+ ).tool;
1052
+ }
1053
+ return projectRegisteredTool(
1054
+ entry.id,
1055
+ entry.path,
1056
+ entry.config,
1057
+ entry.mutableUsage,
1058
+ );
1059
+ }
@@ -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.length} active definitions); exact Recipe resolution is ambiguous. Next: inspect target=recipes view=doctor identity=${skill}/<recipe>`,
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 =
@@ -239,12 +319,13 @@ function qualifiedRecipeNameForFile(
239
319
  function assertRecipeReferencePrefixWasNotRemoved(value: string): void {
240
320
  if (value.startsWith("std:")) {
241
321
  throw new Error(
242
- "std: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> or an explicit .json/.md file path.",
322
+ "std: Recipe references were removed in pi-actors 0.46. Use <skill>/<recipe> or an explicit .json/.md file path. Next: inspect target=recipes view=status to identify the active owning Skill.",
243
323
  );
244
324
  }
245
325
  if (value.startsWith("skill:")) {
326
+ const identity = value.slice("skill:".length);
246
327
  throw new Error(
247
- "skill: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> without a prefix.",
328
+ `skill: Recipe references were removed in pi-actors 0.46. Use ${identity} without a prefix. Next: inspect target=recipes view=doctor identity=${identity}`,
248
329
  );
249
330
  }
250
331
  }
@@ -258,16 +339,23 @@ function skillRecipeCandidates(
258
339
  const [, skillName, recipeName] = match;
259
340
  const roots = [...(context.namespaces[skillName] ?? [])];
260
341
  if (roots.length === 0)
261
- throw new Error(`Active Skill Recipe not found: ${skillName}/${recipeName}`);
342
+ throw new Error(
343
+ `Active Skill Recipe not found: ${skillName}/${recipeName}. Owning Skill "${skillName}" is not active. Next: activate Skill ${skillName}, then inspect target=recipes view=doctor identity=${skillName}/${recipeName}`,
344
+ );
262
345
  if (roots.length > 1)
263
346
  throw new Error(
264
- `Duplicate active Skill identity ${skillName}: ${roots.join(", ")}`,
347
+ `Duplicate active Skill identity ${skillName} (${roots.length} active definitions); ${skillName}/${recipeName} cannot resolve unambiguously. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`,
265
348
  );
266
349
  const root = roots[0];
267
350
  const candidates = ["json", "md"].map((extension) =>
268
351
  resolve(root, `${recipeName}.${extension}`),
269
352
  );
270
353
  const existing = candidates.filter((candidate) => existsSync(candidate));
354
+ if (existing.length === 0) {
355
+ throw new Error(
356
+ `Skill Recipe component not found: ${skillName}/${recipeName}; owning Skill "${skillName}" is active. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`,
357
+ );
358
+ }
271
359
  if (existing.length > 1) {
272
360
  throw new Error(
273
361
  `Skill Recipe stem collision: ${skillName}/${recipeName} has both .json and .md files`,
@@ -303,7 +391,7 @@ function findOwningSkillDir(recipeFile: string): string | undefined {
303
391
  function assertRecipeHasNoDeclaredName(raw: Record<string, unknown>): void {
304
392
  if (!Object.hasOwn(raw, "name")) return;
305
393
  throw new Error(
306
- "Recipe.name was removed in pi-actors 0.46.\nFile-backed Recipe identity is derived from its file name.",
394
+ "Recipe.name was removed in pi-actors 0.46. File-backed Recipe identity is derived from its file name. Next: remove the name field, keep the intended filename stem, then inspect target=recipes view=doctor.",
307
395
  );
308
396
  }
309
397
 
@@ -311,7 +399,7 @@ function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): voi
311
399
  const args = Array.isArray(raw.args)
312
400
  ? Schema.getExplicitToolArgNames(raw.args.map(String))
313
401
  : [];
314
- for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
402
+ for (const name of RESERVED_AUTHORED_RECIPE_VALUE_NAMES) {
315
403
  if (
316
404
  args.includes(name) ||
317
405
  (isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
@@ -1068,6 +1156,46 @@ function getDirectDelegatedRecipe(
1068
1156
  return undefined;
1069
1157
  }
1070
1158
 
1159
+ function resolveDirectDelegationArgs(
1160
+ delegated: TemplateRecipeConfig,
1161
+ wrapperArgs: unknown,
1162
+ wrapperDefaults: Record<string, unknown> | undefined,
1163
+ ): string[] | undefined {
1164
+ if (!Array.isArray(delegated.args))
1165
+ return Array.isArray(wrapperArgs) ? wrapperArgs.map(String) : undefined;
1166
+ const delegatedSpec = Schema.parseToolArgDeclarationList(delegated.args);
1167
+ if (delegatedSpec.error) throw new Error(delegatedSpec.error);
1168
+ const wrapperDeclarations = Array.isArray(wrapperArgs)
1169
+ ? wrapperArgs.map(String)
1170
+ : [];
1171
+ const wrapperSpec = Schema.parseToolArgDeclarationList(wrapperDeclarations);
1172
+ if (wrapperSpec.error) throw new Error(wrapperSpec.error);
1173
+ const declared = new Set(delegatedSpec.args);
1174
+ for (const arg of wrapperSpec.args) {
1175
+ if (!declared.has(arg))
1176
+ throw new Error(`Unknown delegated Recipe argument: ${arg}`);
1177
+ }
1178
+ Schema.assertCompatibleToolArgTypes(
1179
+ delegatedSpec.argTypes,
1180
+ wrapperSpec.argTypes,
1181
+ );
1182
+ assertRecipeDefaultsDeclared(
1183
+ delegated.args,
1184
+ wrapperDefaults,
1185
+ "delegated Recipe default",
1186
+ );
1187
+ Schema.normalizeRuntimeValues(
1188
+ Object.fromEntries(
1189
+ Object.entries(wrapperDefaults ?? {}).filter(
1190
+ ([, value]) =>
1191
+ typeof value !== "string" || !/\{[^{}]+\}/.test(value),
1192
+ ),
1193
+ ),
1194
+ delegatedSpec.argTypes,
1195
+ );
1196
+ return delegated.args;
1197
+ }
1198
+
1071
1199
  function expandImportNodes(
1072
1200
  value: CommandTemplateValue,
1073
1201
  imports: Record<string, ImportedRecipe>,
@@ -1157,7 +1285,10 @@ export function readResolvedRecipeConfig(
1157
1285
  `Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`,
1158
1286
  );
1159
1287
  }
1160
- const raw = readRawRecipeConfig(path);
1288
+ const raw =
1289
+ options.authoredEntry?.path === path
1290
+ ? options.authoredEntry.recipe
1291
+ : readRawRecipeConfig(path);
1161
1292
  if (!raw || !Object.hasOwn(raw, "template")) return undefined;
1162
1293
  assertRecipeHasNoDeclaredName(raw);
1163
1294
  RecipeControl.assertRecipeHasNoMailbox(raw);
@@ -1244,10 +1375,15 @@ export function readResolvedRecipeConfig(
1244
1375
  role: stack.length > 0 ? "import" : "entry",
1245
1376
  })
1246
1377
  : expandedTemplate;
1247
- const mergedDefaults = mergeDefaults(
1248
- delegated?.defaults,
1249
- isRecord(substituted.defaults) ? substituted.defaults : undefined,
1250
- );
1378
+ const wrapperDefaults = isRecord(substituted.defaults)
1379
+ ? substituted.defaults
1380
+ : undefined;
1381
+ const mergedDefaults = mergeDefaults(delegated?.defaults, wrapperDefaults);
1382
+ const effectiveArgs = delegated
1383
+ ? resolveDirectDelegationArgs(delegated, substituted.args, wrapperDefaults)
1384
+ : Array.isArray(substituted.args)
1385
+ ? (substituted.args as string[])
1386
+ : undefined;
1251
1387
  const artifactSource = isRecord(substituted.artifacts)
1252
1388
  ? substituted.artifacts
1253
1389
  : delegated?.artifacts;
@@ -1284,11 +1420,7 @@ export function readResolvedRecipeConfig(
1284
1420
  ? { imports: getRecipeImports(raw) }
1285
1421
  : {}),
1286
1422
  template: templateWithContext,
1287
- ...(Array.isArray(substituted.args)
1288
- ? { args: substituted.args as string[] }
1289
- : Array.isArray(delegated?.args)
1290
- ? { args: delegated.args }
1291
- : {}),
1423
+ ...(effectiveArgs ? { args: effectiveArgs } : {}),
1292
1424
  ...(mergedDefaults ? { defaults: mergedDefaults } : {}),
1293
1425
  ...(typeof substituted.parallel === "boolean"
1294
1426
  ? { parallel: substituted.parallel }