@llblab/pi-actors 0.44.0 → 0.45.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 (51) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +5 -1
  4. package/dist/index.d.ts +1 -2
  5. package/dist/index.js +10 -106
  6. package/dist/lib/async-runs.d.ts +2 -0
  7. package/dist/lib/async-runs.js +33 -4
  8. package/dist/lib/config.js +1 -0
  9. package/dist/lib/extension-runtime.d.ts +21 -0
  10. package/dist/lib/extension-runtime.js +116 -0
  11. package/dist/lib/inspector.js +8 -2
  12. package/dist/lib/recipes-discovery.js +20 -3
  13. package/dist/lib/recipes-references.d.ts +10 -0
  14. package/dist/lib/recipes-references.js +168 -5
  15. package/dist/lib/schema.d.ts +3 -0
  16. package/dist/lib/schema.js +34 -1
  17. package/dist/lib/tools-inspect.js +10 -0
  18. package/dist/lib/tools-local.js +27 -10
  19. package/dist/lib/tools-spawn.js +2 -2
  20. package/dist/scripts/async-runner.mjs +0 -16
  21. package/dist/scripts/executable-block-style.d.mts +1 -0
  22. package/dist/scripts/executable-block-style.mjs +73 -0
  23. package/dist/scripts/locker.mjs +0 -21
  24. package/dist/scripts/recipe-utils.mjs +0 -1
  25. package/dist/scripts/release-gates.mjs +14 -11
  26. package/dist/scripts/validate-recipe.mjs +0 -1
  27. package/dist/skills/actors/SKILL.md +2 -2
  28. package/docs/command-templates.md +2 -2
  29. package/docs/recipe-library.md +2 -0
  30. package/docs/template-recipes.md +10 -4
  31. package/docs/tool-registry.md +1 -1
  32. package/index.ts +18 -107
  33. package/lib/async-runs.ts +43 -9
  34. package/lib/config.ts +4 -0
  35. package/lib/extension-runtime.ts +139 -0
  36. package/lib/inspector.ts +16 -2
  37. package/lib/recipes-discovery.ts +23 -3
  38. package/lib/recipes-references.ts +227 -7
  39. package/lib/schema.ts +53 -1
  40. package/lib/tools-inspect.ts +11 -0
  41. package/lib/tools-local.ts +49 -19
  42. package/lib/tools-spawn.ts +2 -2
  43. package/package.json +2 -2
  44. package/scripts/async-runner.mjs +0 -16
  45. package/scripts/executable-block-style.d.mts +1 -0
  46. package/scripts/executable-block-style.mjs +73 -0
  47. package/scripts/locker.mjs +0 -21
  48. package/scripts/recipe-utils.mjs +0 -1
  49. package/scripts/release-gates.mjs +14 -11
  50. package/scripts/validate-recipe.mjs +0 -1
  51. package/skills/actors/SKILL.md +2 -2
@@ -5,15 +5,106 @@
5
5
  */
6
6
  import { existsSync, readFileSync, statSync } from "node:fs";
7
7
  import { homedir } from "node:os";
8
- import { basename, dirname, extname, resolve } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
9
9
  import * as CommandTemplates from "./command-templates.js";
10
10
  import * as Paths from "./paths.js";
11
11
  import * as RecipeControl from "./recipe-control.js";
12
+ import * as Schema from "./schema.js";
12
13
  const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
13
14
  const MAX_RECIPE_IMPORT_DEPTH = 32;
15
+ const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
16
+ const activeSkillRecipeRoots = new Map();
17
+ export function setActiveSkillRecipeSources(skills) {
18
+ activeSkillRecipeRoots.clear();
19
+ for (const skill of skills) {
20
+ const name = skill.name.trim();
21
+ const skillDir = skill.baseDir
22
+ ? resolve(skill.baseDir)
23
+ : skill.filePath
24
+ ? dirname(resolve(skill.filePath))
25
+ : undefined;
26
+ if (!name || !skillDir)
27
+ continue;
28
+ const roots = activeSkillRecipeRoots.get(name) ?? new Set();
29
+ roots.add(resolve(skillDir, "recipes"));
30
+ activeSkillRecipeRoots.set(name, roots);
31
+ }
32
+ }
33
+ export function getActiveSkillRecipeNamespaces() {
34
+ return Object.fromEntries([...activeSkillRecipeRoots.entries()]
35
+ .sort(([left], [right]) => left.localeCompare(right))
36
+ .map(([name, roots]) => [name, [...roots].sort()]));
37
+ }
38
+ function qualifiedRecipeNameForFile(file) {
39
+ const path = resolve(file);
40
+ const stdRelation = relative(Paths.getPackagedRecipeRoot(), path);
41
+ if (stdRelation &&
42
+ !stdRelation.startsWith("..") &&
43
+ !isAbsolute(stdRelation)) {
44
+ return `std:${stdRelation.replace(/\.(?:json|md)$/u, "")}`;
45
+ }
46
+ for (const [skillName, roots] of activeSkillRecipeRoots) {
47
+ if (roots.size !== 1)
48
+ continue;
49
+ const root = [...roots][0];
50
+ const relation = relative(root, path);
51
+ if (relation && !relation.startsWith("..") && !isAbsolute(relation))
52
+ return `skill:${skillName}/${relation.replaceAll("\\", "/").replace(/\.(?:json|md)$/u, "")}`;
53
+ }
54
+ return undefined;
55
+ }
56
+ function qualifiedRecipeCandidates(value) {
57
+ if (value.startsWith("std:")) {
58
+ return recipeNameFiles(value.slice(4)).map((file) => resolve(Paths.getPackagedRecipeRoot(), file));
59
+ }
60
+ if (!value.startsWith("skill:"))
61
+ return undefined;
62
+ const qualified = value.slice(6);
63
+ const separator = qualified.indexOf("/");
64
+ if (separator <= 0 || separator === qualified.length - 1)
65
+ throw new Error(`Invalid Skill Recipe reference: ${value}`);
66
+ const skillName = qualified.slice(0, separator);
67
+ const recipeName = qualified.slice(separator + 1);
68
+ const roots = [...(activeSkillRecipeRoots.get(skillName) ?? [])];
69
+ if (roots.length === 0)
70
+ throw new Error(`Active Skill Recipe namespace not found: ${skillName}`);
71
+ if (roots.length > 1)
72
+ throw new Error(`Ambiguous active Skill Recipe namespace ${skillName}: ${roots.join(", ")}`);
73
+ const root = roots[0];
74
+ return recipeNameFiles(recipeName).map((file) => {
75
+ const candidate = resolve(root, file);
76
+ const relation = relative(root, candidate);
77
+ if (!relation || relation.startsWith("..") || isAbsolute(relation))
78
+ throw new Error(`Skill Recipe reference escapes its recipes root: ${value}`);
79
+ return candidate;
80
+ });
81
+ }
14
82
  function hasWhitespace(value) {
15
83
  return /\s/.test(value);
16
84
  }
85
+ function findOwningSkillDir(recipeFile) {
86
+ let current = dirname(recipeFile);
87
+ while (true) {
88
+ if (existsSync(resolve(current, "SKILL.md")))
89
+ return current;
90
+ const parent = dirname(current);
91
+ if (parent === current)
92
+ return undefined;
93
+ current = parent;
94
+ }
95
+ }
96
+ function assertRuntimeRecipeValuesNotDeclared(raw) {
97
+ const args = Array.isArray(raw.args)
98
+ ? Schema.getExplicitToolArgNames(raw.args.map(String))
99
+ : [];
100
+ for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
101
+ if (args.includes(name) ||
102
+ (isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
103
+ (isRecord(raw.values) && Object.hasOwn(raw.values, name))) {
104
+ throw new Error(`Recipe runtime value is reserved: ${name}`);
105
+ }
106
+ }
107
+ }
17
108
  export function resolveRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
18
109
  const trimmed = value.trim();
19
110
  const repoRoot = resolve(recipeRoot, "..");
@@ -42,6 +133,9 @@ function recipeNameFiles(value) {
42
133
  return [`${trimmed}.json`, `${trimmed}.md`];
43
134
  }
44
135
  function recipeCandidatePaths(value, currentRecipeRoot) {
136
+ const qualified = qualifiedRecipeCandidates(value.trim());
137
+ if (qualified)
138
+ return qualified;
45
139
  if (!isBareRecipeName(value))
46
140
  return [resolveRecipePath(value, currentRecipeRoot)];
47
141
  const roots = [
@@ -495,11 +589,46 @@ function mergeDefaults(...items) {
495
589
  const merged = Object.assign({}, ...items.filter(Boolean));
496
590
  return Object.keys(merged).length > 0 ? merged : undefined;
497
591
  }
592
+ function assertRecipeDefaultsDeclared(args, defaults, label) {
593
+ const spec = Schema.parseToolArgDeclarationList(args ?? []);
594
+ if (spec.error)
595
+ throw new Error(spec.error);
596
+ if (spec.args.length === 0)
597
+ return;
598
+ const declared = new Set(spec.args);
599
+ for (const key of Object.keys(defaults ?? {})) {
600
+ if (!declared.has(key))
601
+ throw new Error(`Unknown ${label} argument: ${key}`);
602
+ }
603
+ }
604
+ function assertRecipeArgumentContract(config, label = "Recipe default") {
605
+ assertRecipeDefaultsDeclared(config.args, config.defaults, label);
606
+ const spec = Schema.parseToolArgDeclarationList(config.args ?? []);
607
+ if (spec.error)
608
+ throw new Error(spec.error);
609
+ Schema.assertCompatibleToolArgTypes(spec.argTypes, Schema.getTemplateArgTypes({ template: config.template }));
610
+ const staticValues = Object.fromEntries(Object.entries({ ...config.defaults, ...config.values }).filter(([, value]) => typeof value !== "string" || !/\{[^{}]+\}/.test(value)));
611
+ Schema.normalizeRuntimeValues(staticValues, spec.argTypes);
612
+ }
613
+ function resolveRecipeValueLayers(args, ...layers) {
614
+ const spec = Schema.parseToolArgDeclarationList(args ?? []);
615
+ if (spec.error)
616
+ throw new Error(spec.error);
617
+ const merged = mergeDefaults(spec.defaults, ...layers);
618
+ if (!merged)
619
+ return undefined;
620
+ const staticValues = Object.fromEntries(Object.entries(merged).filter(([, value]) => typeof value !== "string" || !/\{[^{}]+\}/.test(value)));
621
+ return {
622
+ ...merged,
623
+ ...Schema.normalizeRuntimeValues(staticValues, spec.argTypes),
624
+ };
625
+ }
498
626
  function applyDefaultsToTemplate(template, defaults, overrides) {
499
627
  const cleanOverrides = { ...overrides };
500
628
  delete cleanOverrides.name;
501
629
  delete cleanOverrides.template;
502
630
  delete cleanOverrides.values;
631
+ delete cleanOverrides.defaults;
503
632
  if (typeof template === "object" && !Array.isArray(template)) {
504
633
  return {
505
634
  ...template,
@@ -537,7 +666,15 @@ function loadDelegatedRecipe(value, currentRecipeFile, stack, options) {
537
666
  return config;
538
667
  }
539
668
  function applyDelegatedRecipeToNode(delegated, overrides = {}) {
540
- return applyDefaultsToTemplate(delegated.template, delegated.values, overrides);
669
+ const nodeDefaults = isRecord(overrides.defaults)
670
+ ? overrides.defaults
671
+ : undefined;
672
+ const nodeValues = isRecord(overrides.values) ? overrides.values : undefined;
673
+ const values = resolveRecipeValueLayers(delegated.args, delegated.defaults, delegated.values, nodeDefaults, nodeValues, {
674
+ ...(delegated.recipe_dir ? { recipe_dir: delegated.recipe_dir } : {}),
675
+ ...(delegated.skill_dir ? { skill_dir: delegated.skill_dir } : {}),
676
+ });
677
+ return applyDefaultsToTemplate(delegated.template, values, overrides);
541
678
  }
542
679
  function expandRecipeDelegations(value, currentRecipeFile, stack, options = {}) {
543
680
  if (typeof value === "string") {
@@ -593,7 +730,7 @@ function expandImportNodes(value, imports, options = {}) {
593
730
  ? record.defaults
594
731
  : undefined;
595
732
  const nodeValues = isRecord(record.values) ? record.values : undefined;
596
- const defaults = mergeDefaults(imported.defaults, imported.values, nodeDefaults, nodeValues);
733
+ const defaults = resolveRecipeValueLayers(imported.config.args, imported.defaults, imported.values, nodeDefaults, nodeValues);
597
734
  const expanded = applyDefaultsToTemplate(imported.config.template, defaults, record);
598
735
  return options.includeActorRecipeContext
599
736
  ? withActorRecipeContext(expanded, {
@@ -631,6 +768,8 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
631
768
  if (!raw || !Object.hasOwn(raw, "template"))
632
769
  return undefined;
633
770
  RecipeControl.assertRecipeHasNoMailbox(raw);
771
+ assertRuntimeRecipeValuesNotDeclared(raw);
772
+ assertRecipeArgumentContract(raw);
634
773
  const imports = {};
635
774
  for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
636
775
  const importPath = resolveRecipeImportPath(getImportFrom(binding), dirname(path));
@@ -639,13 +778,18 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
639
778
  throw new Error(`Recipe import not found: ${alias}`);
640
779
  const bindingDefaults = typeof binding === "string" ? undefined : binding.defaults;
641
780
  const bindingValues = typeof binding === "string" ? undefined : binding.values;
781
+ assertRecipeDefaultsDeclared(config.args, bindingDefaults, `Recipe import ${alias} default`);
642
782
  imports[alias] = {
643
783
  alias,
644
784
  file: importPath,
645
785
  name: config.name ?? alias,
646
786
  config,
647
787
  defaults: { ...(config.defaults ?? {}), ...(bindingDefaults ?? {}) },
648
- values: { ...(bindingValues ?? {}) },
788
+ values: {
789
+ ...(bindingValues ?? {}),
790
+ ...(config.recipe_dir ? { recipe_dir: config.recipe_dir } : {}),
791
+ ...(config.skill_dir ? { skill_dir: config.skill_dir } : {}),
792
+ },
649
793
  };
650
794
  }
651
795
  const substituted = substituteImportRefs(raw, imports);
@@ -661,6 +805,17 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
661
805
  : {})
662
806
  : expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
663
807
  const recipeName = getRecipeIdFromPath(path);
808
+ const recipeDir = dirname(path);
809
+ const skillDir = findOwningSkillDir(path);
810
+ if (!skillDir &&
811
+ JSON.stringify({
812
+ artifacts: substituted.artifacts,
813
+ defaults: substituted.defaults,
814
+ template: substituted.template,
815
+ values: substituted.values,
816
+ }).includes("{skill_dir}")) {
817
+ throw new Error(`Recipe uses {skill_dir} outside an owning Skill directory: ${path}`);
818
+ }
664
819
  const templateWithContext = options.includeActorRecipeContext
665
820
  ? withActorRecipeContext(expandedTemplate, {
666
821
  file: path,
@@ -678,6 +833,8 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
678
833
  : delegated?.control);
679
834
  return {
680
835
  name: recipeName,
836
+ recipe_dir: recipeDir,
837
+ ...(skillDir ? { skill_dir: skillDir } : {}),
681
838
  ...(typeof substituted.description === "string" &&
682
839
  substituted.description.trim()
683
840
  ? { description: substituted.description.trim() }
@@ -765,7 +922,11 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
765
922
  ...(typeof substituted.repeat === "number"
766
923
  ? { repeat: substituted.repeat }
767
924
  : {}),
768
- ...(isRecord(substituted.values) ? { values: substituted.values } : {}),
925
+ values: {
926
+ ...(isRecord(substituted.values) ? substituted.values : {}),
927
+ recipe_dir: recipeDir,
928
+ ...(skillDir ? { skill_dir: skillDir } : {}),
929
+ },
769
930
  ...(isRecord(substituted.usage) ? { usage: substituted.usage } : {}),
770
931
  };
771
932
  }
@@ -780,12 +941,14 @@ function collectRecipeContextRecords(file, stack, importPath, alias) {
780
941
  const raw = readRawRecipeConfig(path);
781
942
  if (!raw || !Object.hasOwn(raw, "template"))
782
943
  return [];
944
+ const qualifiedName = qualifiedRecipeNameForFile(path);
783
945
  const record = {
784
946
  ...(alias ? { alias } : {}),
785
947
  depth: stack.length,
786
948
  file: path,
787
949
  import_path: importPath,
788
950
  name: getRecipeIdFromPath(path),
951
+ ...(qualifiedName ? { qualified_name: qualifiedName } : {}),
789
952
  recipe: raw,
790
953
  role: stack.length === 0 ? "entry" : "import",
791
954
  };
@@ -44,14 +44,17 @@ export declare function objectSchema(properties: Record<string, JsonSchema>, req
44
44
  export declare function looseObjectSchema(description: string): JsonSchema;
45
45
  export declare function parseToolArgToken(value: string): ParsedToolArgToken;
46
46
  export declare function parseToolArgDeclarations(value: string): ToolArgSpec;
47
+ export declare function parseToolArgDeclarationList(value: unknown[]): ToolArgSpec;
47
48
  export declare function normalizeStoredToolArgDeclarations(argsValue: unknown, defaultsValue: unknown): ToolArgSpec & {
48
49
  changed: boolean;
49
50
  provided: boolean;
50
51
  };
51
52
  export declare function formatToolArgs(args: string[]): string;
52
53
  export declare function getTemplatePlaceholderNames(config: CommandTemplates.CommandTemplateConfig): string[];
54
+ export declare function assertCompatibleToolArgTypes(...sources: Array<Record<string, ToolArgType> | undefined>): void;
53
55
  export declare function getTemplateArgTypes(config: CommandTemplates.CommandTemplateConfig): Record<string, ToolArgType>;
54
56
  export declare function getExplicitToolArgNames(args: string[] | undefined): string[];
57
+ export declare function getExplicitToolArgDefaults(args: string[] | string | undefined): Record<string, string>;
55
58
  export declare function getToolArgNames(config: CommandTemplates.CommandTemplateConfig): string[];
56
59
  export declare function getRequiredToolArgNames(config: CommandTemplates.CommandTemplateConfig): Set<string>;
57
60
  export declare function normalizeRuntimeValues(values: Record<string, unknown>, argTypes: Record<string, ToolArgType> | undefined): Record<string, unknown>;
@@ -147,7 +147,10 @@ function isValidDefault(type, value) {
147
147
  }
148
148
  }
149
149
  export function parseToolArgDeclarations(value) {
150
- const source = splitArgDeclarations(value);
150
+ return parseToolArgDeclarationList(splitArgDeclarations(value));
151
+ }
152
+ export function parseToolArgDeclarationList(value) {
153
+ const source = value.map(String);
151
154
  const seen = new Set();
152
155
  const duplicates = new Set();
153
156
  const args = [];
@@ -272,6 +275,8 @@ function collectCommandTemplateConfigDeclarations(config, declarations) {
272
275
  collectTemplatePlaceholderDeclarations(config, declarations);
273
276
  return;
274
277
  }
278
+ if (!config || typeof config !== "object")
279
+ return;
275
280
  if (Array.isArray(config)) {
276
281
  for (const step of config)
277
282
  collectCommandTemplateConfigDeclarations(step, declarations);
@@ -296,9 +301,30 @@ function getTemplatePlaceholderDeclarations(config) {
296
301
  export function getTemplatePlaceholderNames(config) {
297
302
  return mergeUnique(getTemplatePlaceholderDeclarations(config).map((item) => item.arg));
298
303
  }
304
+ function toolArgTypesEqual(left, right) {
305
+ return JSON.stringify(left) === JSON.stringify(right);
306
+ }
307
+ export function assertCompatibleToolArgTypes(...sources) {
308
+ const seen = {};
309
+ for (const source of sources) {
310
+ for (const [name, type] of Object.entries(source ?? {})) {
311
+ const previous = seen[name];
312
+ if (previous && !toolArgTypesEqual(previous, type)) {
313
+ throw new Error(`Conflicting argument type for ${name}: ${canonicalArgDeclaration(name, previous)} vs ${canonicalArgDeclaration(name, type)}`);
314
+ }
315
+ seen[name] = type;
316
+ }
317
+ }
318
+ }
299
319
  export function getTemplateArgTypes(config) {
300
320
  const argTypes = {};
301
321
  for (const declaration of getTemplatePlaceholderDeclarations(config)) {
322
+ const previous = argTypes[declaration.arg];
323
+ if (previous &&
324
+ declaration.type.kind !== "string" &&
325
+ !toolArgTypesEqual(previous, declaration.type)) {
326
+ throw new Error(`Conflicting argument type for ${declaration.arg}: ${canonicalArgDeclaration(declaration.arg, previous)} vs ${declaration.declaration}`);
327
+ }
302
328
  if (declaration.type.kind !== "string")
303
329
  argTypes[declaration.arg] = declaration.type;
304
330
  }
@@ -307,6 +333,13 @@ export function getTemplateArgTypes(config) {
307
333
  export function getExplicitToolArgNames(args) {
308
334
  return mergeUnique((args ?? []).map((item) => parseToolArgToken(String(item)).arg));
309
335
  }
336
+ export function getExplicitToolArgDefaults(args) {
337
+ const declarations = typeof args === "string" ? splitArgDeclarations(args) : (args ?? []);
338
+ return Object.fromEntries(declarations
339
+ .map((item) => parseToolArgToken(String(item)))
340
+ .filter((item) => item.defaultValue !== undefined)
341
+ .map((item) => [item.arg, item.defaultValue]));
342
+ }
310
343
  export function getToolArgNames(config) {
311
344
  const normalizedConfig = CommandTemplates.normalizeCommandTemplateConfig(config);
312
345
  const declaredArgs = Array.isArray(normalizedConfig.args)
@@ -12,6 +12,7 @@ import * as Limits from "./limits.js";
12
12
  import { computeRunControlCapacity } from "./run-evidence-policy.js";
13
13
  import * as Paths from "./paths.js";
14
14
  import * as RecipesDiscovery from "./recipes-discovery.js";
15
+ import * as RecipesReferences from "./recipes-references.js";
15
16
  import * as ReviewDiagnostics from "./review-diagnostics.js";
16
17
  import * as RunsControls from "./runs-controls.js";
17
18
  import * as RunControlDelivery from "./runs-control-delivery.js";
@@ -172,8 +173,17 @@ function inspectRecipes(view, deps) {
172
173
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
173
174
  { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
174
175
  ]);
176
+ const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces();
175
177
  const summary = {
176
178
  ...RecipesDiscovery.summarizeDiscovery(discovered),
179
+ skill_recipe_namespaces: skillRecipeNamespaces,
180
+ skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
181
+ .filter(([, roots]) => roots.length > 1)
182
+ .map(([name, roots]) => ({
183
+ name,
184
+ roots,
185
+ error: `Ambiguous active Skill Recipe namespace ${name}`,
186
+ })),
177
187
  drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
178
188
  };
179
189
  return {
@@ -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"),
@@ -74,7 +74,6 @@ function summarizeCommandDetail(commandDetail) {
74
74
 
75
75
  export async function runAsyncRunner(stateDir = process.argv[2]) {
76
76
  if (!stateDir) throw new Error("missing state dir");
77
-
78
77
  const runPath = join(stateDir, "run.json");
79
78
  const progressPath = join(stateDir, "progress.json");
80
79
  const resultPath = join(stateDir, "result.json");
@@ -83,11 +82,9 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
83
82
  const stderrPath = join(stateDir, "stderr.log");
84
83
  const executionPath = join(stateDir, "execution.json");
85
84
  const meta = JSON.parse(readFileSync(runPath, "utf8"));
86
-
87
85
  function event(kind, data = {}) {
88
86
  appendRunTraceEvent(stateDir, { kind, data });
89
87
  }
90
-
91
88
  function observation(kind, summary, data = {}, delivery = "log", level = "info") {
92
89
  appendRunTraceEvent(stateDir, {
93
90
  kind,
@@ -97,7 +94,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
97
94
  ...(level === "error" ? { level: "error" } : {}),
98
95
  });
99
96
  }
100
-
101
97
  function progress(phase, extra = {}) {
102
98
  writeJsonAtomic(progressPath, {
103
99
  ...(meta.model_policy ? { model_policy: meta.model_policy } : {}),
@@ -106,7 +102,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
106
102
  ...extra,
107
103
  });
108
104
  }
109
-
110
105
  let activeSubagents = 0;
111
106
  let completedSubagents = 0;
112
107
  let promptCounter = 0;
@@ -115,7 +110,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
115
110
  const evidenceRecords = [];
116
111
  const stageOccurrences = new Map();
117
112
  let reportEvidence;
118
-
119
113
  function writeExecutionManifest(status) {
120
114
  writeJsonAtomic(executionPath, {
121
115
  version: 1,
@@ -129,7 +123,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
129
123
  updated_at: new Date().toISOString(),
130
124
  });
131
125
  }
132
-
133
126
  function commandSessionFiles(sessionDir) {
134
127
  if (!sessionDir) return [];
135
128
  try {
@@ -141,7 +134,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
141
134
  return [];
142
135
  }
143
136
  }
144
-
145
137
  function commandEvidenceStartRecord({
146
138
  commandDetail,
147
139
  commandId,
@@ -181,7 +173,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
181
173
  : "not_required",
182
174
  };
183
175
  }
184
-
185
176
  function commandEvidenceRecord({
186
177
  captureDir,
187
178
  commandDetail,
@@ -273,7 +264,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
273
264
  : "rejected",
274
265
  };
275
266
  }
276
-
277
267
  function auditReviewReport(text) {
278
268
  const required = evidenceRecords
279
269
  .filter((record) =>
@@ -294,11 +284,9 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
294
284
  complete_allowed: missing.length === 0,
295
285
  };
296
286
  }
297
-
298
287
  function getCommandDoneDelivery(result) {
299
288
  return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
300
289
  }
301
-
302
290
  function progressRunning() {
303
291
  progress("running", {
304
292
  activeSubagents,
@@ -306,14 +294,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
306
294
  failures: subagentFailures,
307
295
  });
308
296
  }
309
-
310
297
  function promptFilePath() {
311
298
  promptCounter += 1;
312
299
  const dir = join(stateDir, "prompts");
313
300
  mkdirSync(dir, { recursive: true });
314
301
  return join(dir, `command-${String(promptCounter).padStart(3, "0")}.md`);
315
302
  }
316
-
317
303
  function readPromptText(promptFile) {
318
304
  if (!promptFile) return undefined;
319
305
  try {
@@ -322,7 +308,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
322
308
  return undefined;
323
309
  }
324
310
  }
325
-
326
311
  async function observedExec(command, args, options) {
327
312
  captureCounter += 1;
328
313
  const commandId = `command-${String(captureCounter).padStart(3, "0")}`;
@@ -485,7 +470,6 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
485
470
  progressRunning();
486
471
  return result;
487
472
  }
488
-
489
473
  try {
490
474
  event("run.runner.start", { pid: process.pid });
491
475
  progressRunning();
@@ -0,0 +1 @@
1
+ export function executableBlockBlankLines(source: string): number[];
@@ -0,0 +1,73 @@
1
+ /** Detect blank lines inside executable JavaScript and TypeScript blocks. */
2
+
3
+ export function executableBlockBlankLines(source) {
4
+ const lines = source.split(/\r?\n/u);
5
+ const blanks = [];
6
+ const stack = [];
7
+ let blockComment = false;
8
+ let currentExecutableDepth = 0;
9
+ let previousCode = "";
10
+ let quote;
11
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
12
+ const line = lines[lineIndex];
13
+ if (
14
+ /^\s*$/u.test(line) &&
15
+ currentExecutableDepth > 0 &&
16
+ quote !== "`"
17
+ )
18
+ blanks.push(lineIndex + 1);
19
+ let code = "";
20
+ let regularExpression = false;
21
+ for (let index = 0; index < line.length; index += 1) {
22
+ const char = line[index];
23
+ const next = line[index + 1];
24
+ if (blockComment) {
25
+ if (char === "*" && next === "/") {
26
+ blockComment = false;
27
+ index += 1;
28
+ }
29
+ continue;
30
+ }
31
+ if (quote) {
32
+ if (char === "\\") index += 1;
33
+ else if (char === quote) quote = undefined;
34
+ continue;
35
+ }
36
+ if (regularExpression) {
37
+ if (char === "\\") index += 1;
38
+ else if (char === "/") regularExpression = false;
39
+ continue;
40
+ }
41
+ if (char === "/" && next === "*") {
42
+ blockComment = true;
43
+ index += 1;
44
+ continue;
45
+ }
46
+ if (char === "/" && next === "/") break;
47
+ if (
48
+ char === "/" &&
49
+ /^(?:|.*(?:[=(,:;!&|?{}]|\b(?:return|case|throw)))\s*$/u.test(code)
50
+ ) {
51
+ regularExpression = true;
52
+ continue;
53
+ }
54
+ if (char === '"' || char === "'" || char === "`") {
55
+ quote = char;
56
+ continue;
57
+ }
58
+ if (char === "{") {
59
+ const prefix = code.trimEnd() || previousCode;
60
+ const executable =
61
+ /\)\s*$|=>\s*$|\b(?:else|try|finally|do)\s*$/u.test(prefix);
62
+ stack.push(executable);
63
+ if (executable) currentExecutableDepth += 1;
64
+ } else if (char === "}") {
65
+ if (stack.pop()) currentExecutableDepth -= 1;
66
+ }
67
+ code += char;
68
+ }
69
+ if (quote !== "`") quote = undefined;
70
+ if (code.trim()) previousCode = code.trim();
71
+ }
72
+ return blanks;
73
+ }