@llblab/pi-actors 0.44.0 → 0.45.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.
@@ -45,11 +45,16 @@ function shouldAddRuntimeToolUsageHint(error) {
45
45
  }
46
46
  function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
47
47
  const optional = cfg.args.filter((arg) => !required.includes(arg));
48
+ const exampleDefaults = {
49
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
50
+ ...cfg.defaults,
51
+ ...(cfg.recipe?.values ?? {}),
52
+ };
48
53
  const example = {};
49
54
  for (const arg of required)
50
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
55
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
51
56
  for (const arg of optional)
52
- example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], cfg.defaults);
57
+ example[arg] = sampleValueForArg(arg, cfg.argTypes?.[arg], exampleDefaults);
53
58
  if (includeRunId)
54
59
  example.run_id = `${cfg.name}-1`;
55
60
  const lines = [
@@ -62,6 +67,21 @@ function formatRuntimeToolUsageHint(cfg, required, includeRunId) {
62
67
  lines.push(`Optional: ${[...optional, ...(includeRunId ? ["run_id"] : [])].join(", ")}`);
63
68
  return lines.join("\n");
64
69
  }
70
+ function resolveRecipeToolValues(cfg, callerValues, ctx) {
71
+ const recipeRuntimeValues = {
72
+ ...(cfg.recipe?.recipe_dir
73
+ ? { recipe_dir: cfg.recipe.recipe_dir }
74
+ : {}),
75
+ ...(cfg.recipe?.skill_dir ? { skill_dir: cfg.recipe.skill_dir } : {}),
76
+ };
77
+ return Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({
78
+ ...Schema.getExplicitToolArgDefaults(cfg.recipe?.args),
79
+ ...cfg.defaults,
80
+ ...(cfg.recipe?.values ?? {}),
81
+ ...callerValues,
82
+ ...recipeRuntimeValues,
83
+ }, ctx), cfg.argTypes);
84
+ }
65
85
  function formatRuntimeToolArgumentError(cfg, error, required, includeRunId) {
66
86
  const message = error instanceof Error ? error.message : String(error);
67
87
  if (!shouldAddRuntimeToolUsageHint(error))
@@ -87,8 +107,10 @@ export function createRuntimeToolDefinition(cfg, exec) {
87
107
  defaults: cfg.defaults,
88
108
  template: requiredTemplate,
89
109
  };
110
+ const recipeInlineDefaults = Schema.getExplicitToolArgDefaults(cfg.recipe?.args);
90
111
  const requiredArgs = isRecipe && cfg.storedArgs !== undefined
91
- ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
112
+ ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg) &&
113
+ !Object.hasOwn(recipeInlineDefaults, arg)))
92
114
  : RecipesReferences.isRecipeReference(cfg.template) && !recipeTemplate
93
115
  ? new Set(cfg.args.filter((arg) => !Object.hasOwn(cfg.defaults, arg)))
94
116
  : Schema.getRequiredToolArgNames(requiredTemplateConfig);
@@ -137,7 +159,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
137
159
  run_id: runId,
138
160
  tool: cfg.name,
139
161
  policy_values: ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...values }, ctx),
140
- values: Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values }, ctx), cfg.argTypes),
162
+ values: resolveRecipeToolValues(cfg, values, ctx),
141
163
  }, ctx.cwd);
142
164
  return {
143
165
  content: [
@@ -150,12 +172,7 @@ export function createRuntimeToolDefinition(cfg, exec) {
150
172
  };
151
173
  }
152
174
  if (isRecipe && recipeTemplate) {
153
- const paramsWithDefaults = ModelContext.withCurrentModelValues({
154
- ...(cfg.recipe?.values ?? {}),
155
- ...cfg.defaults,
156
- ...params,
157
- }, ctx);
158
- return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes), exec, ctx.cwd, signal);
175
+ return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, resolveRecipeToolValues(cfg, params, ctx), exec, ctx.cwd, signal);
159
176
  }
160
177
  return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues(params, ctx), cfg.argTypes), exec, ctx.cwd, signal);
161
178
  }
@@ -90,8 +90,8 @@ export function createSpawnToolDefinition() {
90
90
  parameters: Schema.objectSchema({
91
91
  artifacts: Schema.looseObjectSchema("Optional named artifact paths for the spawned Run."),
92
92
  as: Schema.stringSchema("Optional Run identity in exact run:<id> form."),
93
- file: Schema.stringSchema("Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes."),
94
- recipe: Schema.stringSchema("Alias for file; template recipe JSON file/name to spawn."),
93
+ file: Schema.stringSchema("Optional template Recipe reference. Supports paths, bare user/adjacent/std names, std:<name>, and skill:<skill>/<recipe>."),
94
+ recipe: Schema.stringSchema("Alias for file; qualified Skill/std or ordinary Recipe reference to spawn."),
95
95
  template: Schema.unionSchema([
96
96
  Schema.stringSchema("Inline command template string"),
97
97
  Schema.arraySchema("Inline command-template sequence or parallel tree"),
@@ -198,7 +198,7 @@ try {
198
198
  );
199
199
  console.log("[release] zero-warning Recipe QA gate checked");
200
200
 
201
- const maximumShippedLines = 29_598;
201
+ const maximumShippedLines = 29_767;
202
202
  const shippedPath = /^(?:lib\/|scripts\/|recipes\/|docs\/|skills\/)/u;
203
203
  const shippedLines = files
204
204
  .filter((path) => shippedPath.test(path))
@@ -25,11 +25,11 @@ A Run target exposes exactly three inspect views: `recipe`, `trace`, and `contro
25
25
 
26
26
  ## Recipe
27
27
 
28
- A Recipe defines execution. It may declare args, defaults, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
28
+ A Recipe defines execution. It may declare named typed args, inline fallbacks, configuration `defaults`, composition `values`, imports, artifacts, command-template flags, and `control: ["action"]` when a long-lived service actually consumes actor-local inputs.
29
29
 
30
30
  Do not declare Control for ordinary one-shot work. Runtime lifecycle actions such as `kill` stay runtime-owned and must not appear in Recipe Control declarations. Imported Recipes act as local definitions inside one Run; they do not create nested Runs unless execution explicitly spawns them.
31
31
 
32
- Prefer maintained packaged Recipes over ad hoc wrappers. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
32
+ Prefer maintained packaged Recipes over ad hoc wrappers. Use `std:<recipe>` for exact packaged lookup and `skill:<skill>/<recipe-path>` for a component bundled with a Pi-active Skill. File-backed Recipes own `{recipe_dir}` and Skill Recipes own `{skill_dir}`; callers never pass or override these origins. Skill Recipes are components, not automatic tools. Keep model, thinking, mission, concurrency, quorum, and timeout choices caller-owned unless a Recipe documents a stable policy.
33
33
 
34
34
  ## Trace
35
35
 
@@ -53,7 +53,7 @@ Common object fields:
53
53
  - `concurrency`: Optional positive integer cap for a `parallel: true` node. Omit it to launch all children at once.
54
54
  - `min_successful`: Optional non-negative integer evidence threshold for a `parallel: true` node. Usable branches are successful branches with non-empty stdout; joins include a `parallel_status` header when this is set.
55
55
  - `when`: Optional node guard. A false guard skips the node; strings may be `flag`, `!flag`, or `{flag?yes:no}` style expressions.
56
- - `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `request_timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `request_timeout:int=60000` before persistence.
56
+ - `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `request_timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. `name:type=value` is an optional argument with an inline fallback. Hosts reject duplicate names and conflicting declared/placeholder types rather than selecting one silently.
57
57
  - `defaults`: Placeholder default values by name.
58
58
  - `timeout`: Optional execution timeout in milliseconds. Omit it, or set `0`, to leave the command unbounded. Set an explicit positive timeout when a tool must fail closed instead of waiting indefinitely. Numeric control fields may be literal numbers or placeholders such as `"{timeout_ms}"`.
59
59
  - `delay`: Optional wait in milliseconds before starting this node. Default is no delay. It may be a literal number or placeholder.
@@ -104,7 +104,7 @@ With runtime values `{ "text": "hello" }`, argv is:
104
104
  ["--text", "hello", "--lang", "ru", "--rate", "+30%"]
105
105
  ```
106
106
 
107
- Use `defaults` for visible configuration data; use inline defaults for compact local literals. Prefer flag-style examples such as `/path/to/tool --file {file} --lang {lang=ru}` for readability, but positional forms such as `/path/to/tool {file} {lang=ru}` are valid when the invoked script defines that CLI contract.
107
+ Use `defaults` for visible fallback configuration, `values` for composition binding, and inline defaults for compact local literals. Named resolution uses caller values before composition values, explicit defaults, and inline defaults; the final selected value is type/enum validated. Prefer flag-style examples such as `/path/to/tool --file {file} --lang {lang=ru}` for readability, but positional forms such as `/path/to/tool {file} {lang=ru}` are valid when the invoked script defines that CLI contract.
108
108
 
109
109
  Use `{env??dev}` for fallback values and `{all?--all:}` to map boolean args to optional text.
110
110
 
@@ -45,6 +45,8 @@ Subagent components provide reusable command-template cells for normalization, p
45
45
 
46
46
  Imports compose these definitions inside one parent Run. They are not independently addressable peers. Parent template flags control sequencing, parallelism, retries, failure scope, recovery, and repeated execution.
47
47
 
48
+ Packaged components can be selected exactly as `std:<recipe-name>`. Recipes bundled under a Pi-active Skill are selected as `skill:<skill-name>/<recipe-path>` and receive runtime-owned `{skill_dir}` plus `{recipe_dir}`. Active Skill components are namespaced library entries, never automatic tools; expose one intentionally through a user Recipe wrapper when a direct tool is desired.
49
+
48
50
  ## Utility Recipes
49
51
 
50
52
  Utilities wrap deterministic local capabilities such as:
@@ -44,7 +44,7 @@ Fences marked `template`, `command-template`, `json`, or `recipe` can define exe
44
44
  Common Recipe fields:
45
45
 
46
46
  - `name`, `description`, `disabled`;
47
- - `args`, typed arg declarations, and `defaults`;
47
+ - `args`, typed arg declarations, inline defaults, `defaults`, and composition `values`;
48
48
  - `imports` with optional binding defaults/values;
49
49
  - `template`;
50
50
  - `async`;
@@ -71,7 +71,9 @@ Common Recipe fields:
71
71
  }
72
72
  ```
73
73
 
74
- Imports are local definitions. Named nodes call imported templates inside the same execution graph and Run. Resolution enforces Recipe-root priority, file-size/depth bounds, and cycle rejection.
74
+ Imports are local definitions. Named nodes call imported templates inside the same execution graph and Run. Effective values follow `caller > node/import/Recipe values > defaults > inline arg default > missing-value error`, then the selected value is checked against its declared type or enum.
75
+
76
+ Bare references preserve user → adjacent → packaged resolution. `std:<name>` selects a packaged Recipe exactly. `skill:<skill-name>/<recipe-path>` selects a component under the `recipes/` tree of a Skill currently active through Pi resource discovery. Skill Recipes never become tools merely by existing, and duplicate active Skill names fail as ambiguous rather than shadowing silently.
75
77
 
76
78
  Direct delegation can use another Recipe as the entire template. The delegated Recipe remains the source of truth while the wrapper may narrow args/defaults or override selected lifecycle metadata.
77
79
 
@@ -102,9 +104,13 @@ Actions must be lowercase ASCII, unique, non-reserved, and at most 64 characters
102
104
 
103
105
  Artifact paths resolve under containment policy and appear in Run inspection. Recipes should write declared artifacts deterministically and fail when the requested write policy cannot be honored.
104
106
 
107
+ ## File Origins
108
+
109
+ Every file-backed Recipe receives immutable `{recipe_dir}`. A Recipe under an active Skill also receives `{skill_dir}`, resolved to the directory containing that Skill's `SKILL.md`; using `{skill_dir}` elsewhere fails clearly. These runtime values cannot be declared in `args`, `defaults`, or `values`, and caller input cannot override them. They expand in templates, recursive defaults/values, imports, and artifacts while existing `./` executable behavior remains relative to invocation `cwd`.
110
+
105
111
  ## Context and Provenance
106
112
 
107
- File-backed Runs capture Recipe context records for the entry and imports. The captured bundle explains composition identity and remains generation-local evidence. It does not override the authored task prompt.
113
+ File-backed Runs capture Recipe context records for the entry and imports, including qualified `std:` or `skill:` identity when applicable. The captured bundle explains composition identity and remains generation-local evidence. Runtime origin paths remain in local Run provenance but are omitted from model-facing launch values. It does not override the authored task prompt.
108
114
 
109
115
  Recipes that need a minimal child prompt may opt out of injected Recipe context through the documented `actor_context` launch option.
110
116
 
@@ -125,7 +131,7 @@ Resolution fails before launch when required current policy is unavailable. The
125
131
 
126
132
  ## Resolution and Shadowing
127
133
 
128
- User Recipes under `~/.pi/agent/recipes` take priority over packaged Recipes. An invalid active file blocks fallback and reports both paths. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
134
+ User Recipes under `~/.pi/agent/recipes` take priority over adjacent and packaged Recipes for compatible bare lookup. Exact `std:` and `skill:` references bypass that ambiguous space. The active Skill namespace converges from Pi's loaded Skill metadata on startup/reload; pi-actors does not scan ambient Skill roots independently. An invalid active file blocks fallback and reports both paths. Disabled Recipes cannot launch. Registry watchers converge after atomic changes without executing partial definitions.
129
135
 
130
136
  ## Validation
131
137
 
@@ -61,7 +61,7 @@ Usage and lineage live in locked metadata ledgers rather than authored Recipe fi
61
61
 
62
62
  ## Wrapping Existing Recipes
63
63
 
64
- Prefer a small user-root wrapper that imports a maintained packaged or skill-owned Recipe by path and delegates by alias. Do not duplicate its executable template, defaults, Control declaration, or artifacts. Install only specific capabilities; internal automatic-review Recipes must not become user-callable tools.
64
+ Prefer a small user-root wrapper that imports a maintained Recipe by exact `std:<recipe>` or `skill:<skill>/<recipe-path>` identity and delegates by alias. Skill Recipes remain components and are never exposed merely because their Skill is active. Do not duplicate executable templates, defaults, Control declarations, artifacts, or runtime-owned `{recipe_dir}`/`{skill_dir}`. Install only specific capabilities; internal automatic-review Recipes must not become user-callable tools.
65
65
 
66
66
  ## Safety
67
67
 
package/index.ts CHANGED
@@ -11,6 +11,7 @@ import * as InspectorCommand from "./lib/inspector-command.ts";
11
11
  import * as Paths from "./lib/paths.ts";
12
12
  import * as Pi from "./lib/pi.ts";
13
13
  import * as Prompts from "./lib/prompts.ts";
14
+ import * as RecipesReferences from "./lib/recipes-references.ts";
14
15
  import * as RunUiRuntime from "./lib/run-ui-runtime.ts";
15
16
  import * as Runtime from "./lib/runtime.ts";
16
17
  import * as Temp from "./lib/temp.ts";
@@ -98,9 +99,14 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
98
99
  runUiRuntime.shutdown(event.reason, ctx);
99
100
  });
100
101
  InspectorCommand.registerActorInspectorCommand(pi, getRunOwnerId);
101
- pi.on("before_agent_start", async (event) => ({
102
- systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
103
- }));
102
+ pi.on("before_agent_start", async (event) => {
103
+ RecipesReferences.setActiveSkillRecipeSources(
104
+ event.systemPromptOptions.skills ?? [],
105
+ );
106
+ return {
107
+ systemPrompt: `${event.systemPrompt}\n\n${Prompts.ONBOARDING_SYSTEM_PROMPT}`,
108
+ };
109
+ });
104
110
  Pi.registerToolDefinitions(
105
111
  pi,
106
112
  Tools.createCoreActorToolDefinitions<Pi.ExtensionContext>({
package/lib/async-runs.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  import * as Paths from "./paths.ts";
34
34
  import * as RecipesReferences from "./recipes-references.ts";
35
35
  import * as RecipesUsage from "./recipes-usage.ts";
36
+ import * as Schema from "./schema.ts";
36
37
  import {
37
38
  resolveArtifactManifest,
38
39
  resolveArtifactPaths,
@@ -128,6 +129,8 @@ export interface AsyncRunStartParams {
128
129
  template?: CommandTemplateValue;
129
130
  args?: string[];
130
131
  defaults?: Record<string, unknown>;
132
+ recipe_dir?: string;
133
+ skill_dir?: string;
131
134
  parallel?: boolean;
132
135
  concurrency?: number | string;
133
136
  min_successful?: number | string;
@@ -309,7 +312,14 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
309
312
  fileParams.run_id ||
310
313
  fileParams.name ||
311
314
  getRunIdFromFile(fileParams.file),
312
- values: { ...(fileParams.values ?? {}), ...(params.values ?? {}) },
315
+ values: {
316
+ ...(fileParams.values ?? {}),
317
+ ...(params.values ?? {}),
318
+ ...(fileParams.recipe_dir
319
+ ? { recipe_dir: fileParams.recipe_dir }
320
+ : {}),
321
+ ...(fileParams.skill_dir ? { skill_dir: fileParams.skill_dir } : {}),
322
+ },
313
323
  };
314
324
  }
315
325
 
@@ -477,13 +487,37 @@ export function startRun(
477
487
  !isAbsolute(recipeRelation)
478
488
  ? dirname(packagedRecipeRoot)
479
489
  : undefined;
480
- const values = {
481
- ...(packagedRepo ? { repo: packagedRepo } : {}),
482
- ...(startParams.values || {}),
483
- run_id: run,
484
- state_dir: stateDir,
485
- trace_file: join(stateDir, "trace.jsonl"),
486
- };
490
+ const argSpec = Schema.parseToolArgDeclarationList(startParams.args ?? []);
491
+ if (argSpec.error) throw new Error(argSpec.error);
492
+ const declaredArgs = new Set(argSpec.args);
493
+ for (const key of Object.keys(startParams.defaults ?? {})) {
494
+ if (declaredArgs.size > 0 && !declaredArgs.has(key))
495
+ throw new Error(`Unknown Recipe default argument: ${key}`);
496
+ }
497
+ const values = Schema.normalizeRuntimeValues(
498
+ {
499
+ ...argSpec.defaults,
500
+ ...(startParams.defaults || {}),
501
+ ...(packagedRepo ? { repo: packagedRepo } : {}),
502
+ ...(startParams.values || {}),
503
+ run_id: run,
504
+ state_dir: stateDir,
505
+ trace_file: join(stateDir, "trace.jsonl"),
506
+ },
507
+ argSpec.argTypes,
508
+ );
509
+ const runtimeTemplate =
510
+ resolved.template &&
511
+ typeof resolved.template === "object" &&
512
+ !Array.isArray(resolved.template)
513
+ ? {
514
+ ...resolved.template,
515
+ defaults: {
516
+ ...(resolved.template.defaults ?? {}),
517
+ ...values,
518
+ },
519
+ }
520
+ : resolved.template;
487
521
  const modelPolicy = describeCurrentPolicyProvenance({
488
522
  defaults: startParams.defaults,
489
523
  template: resolved.template,
@@ -557,7 +591,7 @@ export function startRun(
557
591
  state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
558
592
  status: "running",
559
593
  ...(startParams.tool ? { tool: startParams.tool } : {}),
560
- template: resolved.template,
594
+ template: runtimeTemplate,
561
595
  values,
562
596
  model_policy: modelPolicy,
563
597
  ...(artifacts ? { artifacts } : {}),
package/lib/config.ts CHANGED
@@ -188,6 +188,10 @@ export function normalizeStoredTool(
188
188
  template: argTemplate,
189
189
  };
190
190
  const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
191
+ Schema.assertCompatibleToolArgTypes(
192
+ inferredArgTypes,
193
+ declarations.argTypes,
194
+ );
191
195
  const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
192
196
  const cfg = {
193
197
  name,
package/lib/inspector.ts CHANGED
@@ -105,9 +105,22 @@ export function readActorInspectorRecipe(
105
105
  const redactedRecord = (value: Record<string, unknown>): Record<string, unknown> =>
106
106
  SessionEvidence.redactSessionEvidenceValue(
107
107
  Object.fromEntries(
108
- Object.entries(value).filter(([, entry]) => entry !== undefined),
108
+ Object.entries(value).filter(
109
+ ([key, entry]) =>
110
+ entry !== undefined &&
111
+ key !== "recipe_dir" &&
112
+ key !== "skill_dir",
113
+ ),
109
114
  ),
110
115
  ) as Record<string, unknown>;
116
+ const launchValues =
117
+ meta.values && typeof meta.values === "object" && !Array.isArray(meta.values)
118
+ ? Object.fromEntries(
119
+ Object.entries(meta.values as Record<string, unknown>).filter(
120
+ ([key]) => key !== "recipe_dir" && key !== "skill_dir",
121
+ ),
122
+ )
123
+ : meta.values;
111
124
  return {
112
125
  composition: contexts.map((entry) =>
113
126
  redactedRecord({
@@ -116,6 +129,7 @@ export function readActorInspectorRecipe(
116
129
  file: entry.file,
117
130
  import_path: entry.import_path,
118
131
  name: entry.name,
132
+ qualified_name: entry.qualified_name,
119
133
  }),
120
134
  ),
121
135
  ...(definition ? { definition: redactedRecord(definition) } : {}),
@@ -129,7 +143,7 @@ export function readActorInspectorRecipe(
129
143
  launch: redactedRecord({
130
144
  cwd: meta.cwd,
131
145
  template: meta.template,
132
- values: meta.values,
146
+ values: launchValues,
133
147
  model_policy: meta.model_policy,
134
148
  control: meta.control,
135
149
  artifacts: meta.artifacts,
@@ -122,6 +122,25 @@ function getRecipeCommandTemplateConfig(
122
122
  : config) as CommandTemplates.CommandTemplateConfig;
123
123
  }
124
124
 
125
+ function getRecipeDiagnosticTemplateConfig(
126
+ config: TemplateRecipeConfig,
127
+ ): CommandTemplates.CommandTemplateObjectConfig {
128
+ const templateConfig = getRecipeCommandTemplateConfig(config);
129
+ return typeof templateConfig === "object" && !Array.isArray(templateConfig)
130
+ ? {
131
+ ...templateConfig,
132
+ defaults: {
133
+ ...(templateConfig.defaults ?? {}),
134
+ ...(config.defaults ?? {}),
135
+ ...(config.values ?? {}),
136
+ },
137
+ }
138
+ : {
139
+ defaults: { ...(config.defaults ?? {}), ...(config.values ?? {}) },
140
+ template: templateConfig,
141
+ };
142
+ }
143
+
125
144
  function getRecipeConfigDiagnostics(
126
145
  file: string,
127
146
  config: TemplateRecipeConfig | undefined,
@@ -131,7 +150,7 @@ function getRecipeConfigDiagnostics(
131
150
  return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
132
151
  }
133
152
  return CommandTemplates.getCommandTemplateWarnings(
134
- getRecipeCommandTemplateConfig(config),
153
+ getRecipeDiagnosticTemplateConfig(config),
135
154
  ).map((warning) => `Recipe ${file}: ${warning}`);
136
155
  }
137
156
 
@@ -141,7 +160,7 @@ function getRecipeRiskLabels(
141
160
  if (!config) return [];
142
161
  const labels = new Set(
143
162
  CommandTemplates.getCommandTemplateRiskLabels(
144
- getRecipeCommandTemplateConfig(config),
163
+ getRecipeDiagnosticTemplateConfig(config),
145
164
  ),
146
165
  );
147
166
  if (config.async === true) labels.add("risk.long_running");
@@ -846,9 +865,10 @@ export function toRegisteredTool(
846
865
  assertToolSafeRepeatConfig(
847
866
  argTemplateConfig,
848
867
  explicitArgTypes,
849
- cfg.defaults ?? {},
868
+ { ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) },
850
869
  );
851
870
  const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
871
+ Schema.assertCompatibleToolArgTypes(explicitArgTypes, argTypes);
852
872
  return {
853
873
  name: entry.id,
854
874
  description,