@llblab/pi-actors 0.36.0 → 0.37.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.37.1: Subagent Recipe Prompt Injection Hotfix
6
+
7
+ - `[Recipes]` Fixed actor recipe context injection for child `pi -p` launches with options after the print flag, so packaged subagent recipes such as `subagent-review` and `pipeline-review-readiness` append context to the actual prompt instead of corrupting `--model` or other option values.
8
+ - `[Guidance]` Tightened onboarding, actor-skill, and recipe-library guidance so agents prefer maintained packaged review recipes via `spawn file=<recipe>` before rebuilding script commands or creating unnecessary wrappers.
9
+
10
+ ## 0.37.0: Direct Recipe Delegation And Quiet Overrides
11
+
12
+ - `[Recipes]` Added direct recipe delegation from `template` strings so thin wrappers can point at ready recipe names/paths while preserving priority resolution, inherited metadata, and import-based composition for richer graphs.
13
+ - `[Command Templates]` Fixed inherited/default placeholder resolution for arrays and repeat-indexed values, unblocking lens-swarm recipes that pass `lenses` through wrapper defaults into `{lenses.length}` and `{lenses[index]}`.
14
+ - `[Registry]` Stopped treating same-name recipe/tool registration as a startup warning: higher-priority user recipes now quietly override lower-priority recipe/tool definitions as normal composition behavior, while reserved core tool names remain protected.
15
+
5
16
  ## 0.36.0: Recipe Diagnostics And Runtime Triage
6
17
 
7
18
  - `[Recipe Doctor]` Added deterministic advisory risk labels for discovered recipes, including shell, eval, filesystem mutation, network, external side effect, long-running, platform-specific, and secret-touching signals in verbose recipe inspection plus compact doctor risk counts.
package/dist/index.js CHANGED
@@ -88,7 +88,6 @@ export default function toolRegistryExtension(pi) {
88
88
  configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
89
89
  exec: CommandTemplates.execCommandTemplate,
90
90
  getActiveTools: () => pi.getActiveTools(),
91
- getAllTools: () => pi.getAllTools(),
92
91
  registerTool: (definition) => {
93
92
  actorToolDefinitions.set(definition.name, definition);
94
93
  pi.registerTool(definition);
@@ -20,7 +20,7 @@ import * as RunsStart from "./runs-start.js";
20
20
  import * as RunsIndex from "./runs-index.js";
21
21
  import { claimRunInboxMessageInStateDir, parseRunInboxLine, processRunInboxMessagesInStateDir, runInboxFile, updateRunInboxMessageStatusInStateDir, } from "./runs-mailbox.js";
22
22
  import { deliverRunMessage, } from "./runs-messages.js";
23
- import { buildRunStatus, tailFile, tailLines, } from "./runs-status.js";
23
+ import { buildRunStatus, tailFile, tailLines } from "./runs-status.js";
24
24
  import { readJsonFileResilient } from "./state-readers.js";
25
25
  const RUNNER_IDENTITY_GRACE_MS = 5000;
26
26
  const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
@@ -77,8 +77,9 @@ function assertNoActiveRunState(stateDir) {
77
77
  RunsStart.assertNoActiveRunState(stateDir, readJson, RUNNER_PATH);
78
78
  }
79
79
  function resolveRecipeFile(file) {
80
- return (RecipesReferences.getRecipePath(file, DEFAULT_RECIPE_ROOT) ??
81
- RecipesReferences.resolveRecipePath(file, DEFAULT_RECIPE_ROOT));
80
+ return (RecipesReferences.resolveRecipeReferencePath(file, Paths.getRecipeRoot()) ??
81
+ RecipesReferences.getRecipePath(file, Paths.getRecipeRoot()) ??
82
+ RecipesReferences.resolveRecipePath(file, Paths.getRecipeRoot()));
82
83
  }
83
84
  function isMutableUsageRecipeFile(file) {
84
85
  const userRoot = resolve(DEFAULT_RECIPE_ROOT);
@@ -44,18 +44,32 @@ function normalizeCommandTemplateDefaults(defaults) {
44
44
  return normalized;
45
45
  }
46
46
  export function resolveInheritedDefaultReferences(ownDefaults, inheritedDefaults, runtimeValues = {}) {
47
- if (!ownDefaults || !inheritedDefaults)
47
+ if (!ownDefaults)
48
48
  return ownDefaults;
49
49
  const resolved = { ...ownDefaults };
50
+ const values = { ...(inheritedDefaults ?? {}), ...runtimeValues };
50
51
  for (const [key, value] of Object.entries(ownDefaults)) {
51
52
  if (typeof value !== "string")
52
53
  continue;
53
54
  const exact = /^\{([A-Za-z_][A-Za-z0-9_-]*)\}$/.exec(value);
54
- if (!exact ||
55
- Object.hasOwn(runtimeValues, exact[1]) ||
56
- !Object.hasOwn(inheritedDefaults, exact[1]))
55
+ if (exact && Object.hasOwn(values, exact[1])) {
56
+ resolved[key] = values[exact[1]];
57
57
  continue;
58
- resolved[key] = inheritedDefaults[exact[1]];
58
+ }
59
+ const indexed = value.match(/^\{([A-Za-z_][A-Za-z0-9_-]*)\[([A-Za-z_][A-Za-z0-9_-]*|\d+)\]\}$/);
60
+ if (!indexed)
61
+ continue;
62
+ const source = values[indexed[1]];
63
+ const indexValue = /^\d+$/.test(indexed[2])
64
+ ? indexed[2]
65
+ : values[indexed[2]];
66
+ const index = Number(indexValue);
67
+ if (Array.isArray(source) &&
68
+ Number.isInteger(index) &&
69
+ index >= 0 &&
70
+ index < source.length) {
71
+ resolved[key] = source[index] ?? "";
72
+ }
59
73
  }
60
74
  return resolved;
61
75
  }
@@ -6,7 +6,7 @@
6
6
  export declare const REGISTER_TOOL_DESCRIPTION: string;
7
7
  export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
8
8
  export declare const REGISTER_TOOL_GUIDELINES: string[];
9
- export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
9
+ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.\n- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; long fanout = parent async recipe wrapping template(parallel:true) and imports.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.";
10
10
  export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
11
11
  readonly name: "Tool name in snake_case (e.g., 'transcribe')";
12
12
  readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
@@ -25,7 +25,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
25
25
  - Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.
26
26
  - Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
27
27
  - Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
28
- - Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.
28
+ - Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; long fanout = parent async recipe wrapping template(parallel:true) and imports.
29
29
  - For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
30
30
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
31
31
  name: "Tool name in snake_case (e.g., 'transcribe')",
@@ -10,14 +10,66 @@ function commandName(command) {
10
10
  function isPiCommand(command) {
11
11
  return commandName(command) === "pi";
12
12
  }
13
+ const PI_PRINT_FLAGS = new Set(["-p", "--print"]);
14
+ const PI_VALUE_OPTIONS = new Set([
15
+ "--api-key",
16
+ "--append-system-prompt",
17
+ "--exclude-tools",
18
+ "--extension",
19
+ "--fork",
20
+ "--mode",
21
+ "--model",
22
+ "--models",
23
+ "--name",
24
+ "--prompt-template",
25
+ "--provider",
26
+ "--session",
27
+ "--session-dir",
28
+ "--skill",
29
+ "--system-prompt",
30
+ "--theme",
31
+ "--thinking",
32
+ "--tools",
33
+ ]);
34
+ const PI_SHORT_VALUE_OPTIONS = new Set(["-e", "-n", "-t", "-xt"]);
35
+ function isPiPrintFlag(arg) {
36
+ return PI_PRINT_FLAGS.has(arg);
37
+ }
38
+ function isPiOption(arg) {
39
+ return arg.startsWith("-") && arg !== "-";
40
+ }
41
+ function piOptionConsumesNextArg(arg) {
42
+ if (arg.includes("="))
43
+ return false;
44
+ return PI_VALUE_OPTIONS.has(arg) || PI_SHORT_VALUE_OPTIONS.has(arg);
45
+ }
46
+ function isPiFileArgument(arg) {
47
+ return arg.startsWith("@") && arg.length > 1;
48
+ }
13
49
  function findPrintPromptIndex(args) {
50
+ let printMode = false;
51
+ let positionalOnly = false;
52
+ let promptIndex;
14
53
  for (let index = 0; index < args.length; index += 1) {
15
54
  const arg = args[index];
16
- if ((arg === "-p" || arg === "--print") && index + 1 < args.length) {
17
- return index + 1;
55
+ if (!positionalOnly && arg === "--") {
56
+ positionalOnly = true;
57
+ continue;
58
+ }
59
+ if (!positionalOnly && isPiPrintFlag(arg)) {
60
+ printMode = true;
61
+ continue;
62
+ }
63
+ if (!positionalOnly && isPiOption(arg)) {
64
+ if (piOptionConsumesNextArg(arg))
65
+ index += 1;
66
+ continue;
18
67
  }
68
+ if (!printMode || isPiFileArgument(arg))
69
+ continue;
70
+ promptIndex = index;
19
71
  }
20
- return undefined;
72
+ return promptIndex;
21
73
  }
22
74
  function matchesActorContext(record, context) {
23
75
  if (!context)
@@ -65,6 +65,7 @@ export interface ReadResolvedRecipeConfigOptions {
65
65
  includeActorRecipeContext?: boolean;
66
66
  }
67
67
  export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
68
+ export declare function resolveRecipeReferencePath(value: unknown, currentRecipeRoot?: string): string | undefined;
68
69
  export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
69
70
  export declare function diagnoseRawRecipeConfigFailure(path: string): string | undefined;
70
71
  export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
@@ -40,19 +40,43 @@ function recipeNameFiles(value) {
40
40
  return [trimmed];
41
41
  return [`${trimmed}.json`, `${trimmed}.md`];
42
42
  }
43
- function resolveRecipeImportPath(value, currentRecipeRoot) {
43
+ function recipeCandidatePaths(value, currentRecipeRoot) {
44
44
  if (!isBareRecipeName(value))
45
- return resolveRecipePath(value, currentRecipeRoot);
45
+ return [resolveRecipePath(value, currentRecipeRoot)];
46
46
  const roots = [
47
47
  Paths.getRecipeRoot(),
48
48
  currentRecipeRoot,
49
49
  Paths.getPackagedRecipeRoot(),
50
50
  ];
51
- const candidates = [
51
+ return [
52
52
  ...new Set(roots.flatMap((root) => recipeNameFiles(value).map((file) => resolve(root, file)))),
53
53
  ];
54
+ }
55
+ function resolveRecipeImportPath(value, currentRecipeRoot) {
56
+ const candidates = recipeCandidatePaths(value, currentRecipeRoot);
54
57
  return candidates.find((candidate) => existsSync(candidate)) ?? candidates[0];
55
58
  }
59
+ export function resolveRecipeReferencePath(value, currentRecipeRoot = Paths.getRecipeRoot()) {
60
+ if (typeof value !== "string")
61
+ return undefined;
62
+ const trimmed = value.trim();
63
+ if (!trimmed || hasWhitespace(trimmed))
64
+ return undefined;
65
+ for (const path of recipeCandidatePaths(trimmed, currentRecipeRoot)) {
66
+ if (!existsSync(path))
67
+ continue;
68
+ try {
69
+ const raw = readRawRecipeConfig(path);
70
+ if (raw && typeof raw === "object" && Object.hasOwn(raw, "template"))
71
+ return path;
72
+ return path;
73
+ }
74
+ catch {
75
+ return path;
76
+ }
77
+ }
78
+ return undefined;
79
+ }
56
80
  export function getRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
57
81
  if (typeof value !== "string")
58
82
  return undefined;
@@ -61,20 +85,7 @@ export function getRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
61
85
  return undefined;
62
86
  if (trimmed.endsWith(".json") || trimmed.endsWith(".md"))
63
87
  return resolveRecipePath(trimmed, recipeRoot);
64
- const jsonPath = resolveRecipePath(trimmed, recipeRoot);
65
- const mdPath = resolveRecipePath(`${trimmed}.md`, recipeRoot);
66
- const path = existsSync(jsonPath) ? jsonPath : mdPath;
67
- if (!existsSync(path))
68
- return undefined;
69
- try {
70
- const raw = readRawRecipeConfig(path);
71
- return raw && typeof raw === "object" && Object.hasOwn(raw, "template")
72
- ? path
73
- : undefined;
74
- }
75
- catch {
76
- return undefined;
77
- }
88
+ return resolveRecipeReferencePath(trimmed, recipeRoot);
78
89
  }
79
90
  function isImportNode(value) {
80
91
  if (!isRecord(value) || Object.hasOwn(value, "template"))
@@ -344,7 +355,7 @@ export function getRecipeIdFromPath(file) {
344
355
  return basename(file, extname(file));
345
356
  }
346
357
  function readRecipeConfig(value) {
347
- const path = getRecipePath(value);
358
+ const path = resolveRecipeReferencePath(value);
348
359
  return path ? readResolvedRecipeConfig(path) : undefined;
349
360
  }
350
361
  function isRecord(value) {
@@ -483,6 +494,7 @@ function mergeDefaults(...items) {
483
494
  function applyDefaultsToTemplate(template, defaults, overrides) {
484
495
  const cleanOverrides = { ...overrides };
485
496
  delete cleanOverrides.name;
497
+ delete cleanOverrides.template;
486
498
  delete cleanOverrides.values;
487
499
  if (typeof template === "object" && !Array.isArray(template)) {
488
500
  return {
@@ -509,6 +521,56 @@ function withActorRecipeContext(value, context) {
509
521
  }
510
522
  return { actorRecipeContext: context, template: value };
511
523
  }
524
+ function loadDelegatedRecipe(value, currentRecipeFile, stack, options) {
525
+ const path = resolveRecipeReferencePath(value, dirname(currentRecipeFile));
526
+ if (!path)
527
+ return undefined;
528
+ const config = readResolvedRecipeConfig(path, [...stack, currentRecipeFile], options);
529
+ if (!config)
530
+ throw new Error(`Template recipe must define template: ${path}`);
531
+ if (config.disabled === true)
532
+ throw new Error(`Template recipe is disabled: ${path}`);
533
+ return config;
534
+ }
535
+ function applyDelegatedRecipeToNode(delegated, overrides = {}) {
536
+ return applyDefaultsToTemplate(delegated.template, delegated.values, overrides);
537
+ }
538
+ function expandRecipeDelegations(value, currentRecipeFile, stack, options = {}) {
539
+ if (typeof value === "string") {
540
+ const delegated = loadDelegatedRecipe(value, currentRecipeFile, stack, options);
541
+ return delegated ? applyDelegatedRecipeToNode(delegated) : value;
542
+ }
543
+ if (Array.isArray(value)) {
544
+ return value.map((item) => expandRecipeDelegations(item, currentRecipeFile, stack, options));
545
+ }
546
+ const record = value;
547
+ if (typeof record.template === "string") {
548
+ const delegated = loadDelegatedRecipe(record.template, currentRecipeFile, stack, options);
549
+ if (delegated)
550
+ return applyDelegatedRecipeToNode(delegated, record);
551
+ }
552
+ if (Array.isArray(record.template)) {
553
+ return {
554
+ ...record,
555
+ template: record.template.map((item) => expandRecipeDelegations(item, currentRecipeFile, stack, options)),
556
+ };
557
+ }
558
+ if (record.template && typeof record.template === "object") {
559
+ return {
560
+ ...record,
561
+ template: expandRecipeDelegations(record.template, currentRecipeFile, stack, options),
562
+ };
563
+ }
564
+ return value;
565
+ }
566
+ function getDirectDelegatedRecipe(value, currentRecipeFile, stack, options = {}) {
567
+ if (typeof value === "string")
568
+ return loadDelegatedRecipe(value, currentRecipeFile, stack, options);
569
+ if (!Array.isArray(value) && typeof value.template === "string") {
570
+ return loadDelegatedRecipe(value.template, currentRecipeFile, stack, options);
571
+ }
572
+ return undefined;
573
+ }
512
574
  function expandImportNodes(value, imports, options = {}) {
513
575
  if (typeof value === "string")
514
576
  return value;
@@ -585,7 +647,14 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
585
647
  const template = getRecipeCommandTemplate(substituted);
586
648
  if (!template)
587
649
  return undefined;
588
- const expandedTemplate = expandImportNodes(template, imports, options);
650
+ const expandedImportsTemplate = expandImportNodes(template, imports, options);
651
+ const delegated = getDirectDelegatedRecipe(expandedImportsTemplate, path, stack, options);
652
+ const expandedTemplate = delegated
653
+ ? applyDelegatedRecipeToNode(delegated, typeof expandedImportsTemplate === "object" &&
654
+ !Array.isArray(expandedImportsTemplate)
655
+ ? expandedImportsTemplate
656
+ : {})
657
+ : expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
589
658
  const recipeName = getRecipeIdFromPath(path);
590
659
  const templateWithContext = options.includeActorRecipeContext
591
660
  ? withActorRecipeContext(expandedTemplate, {
@@ -595,33 +664,50 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
595
664
  role: stack.length > 0 ? "import" : "entry",
596
665
  })
597
666
  : expandedTemplate;
667
+ const mergedDefaults = mergeDefaults(delegated?.defaults, isRecord(substituted.defaults) ? substituted.defaults : undefined);
668
+ const artifactSource = isRecord(substituted.artifacts)
669
+ ? substituted.artifacts
670
+ : delegated?.artifacts;
671
+ const mailboxSource = isRecord(substituted.mailbox)
672
+ ? substituted.mailbox
673
+ : delegated?.mailbox;
598
674
  return {
599
675
  name: recipeName,
600
676
  ...(typeof substituted.description === "string" &&
601
677
  substituted.description.trim()
602
678
  ? { description: substituted.description.trim() }
603
- : {}),
679
+ : typeof delegated?.description === "string"
680
+ ? { description: delegated.description }
681
+ : {}),
604
682
  ...(typeof substituted.disabled === "boolean"
605
683
  ? { disabled: substituted.disabled }
606
- : {}),
684
+ : typeof delegated?.disabled === "boolean"
685
+ ? { disabled: delegated.disabled }
686
+ : {}),
607
687
  ...(substituted.async === true
608
688
  ? { async: true }
609
689
  : substituted.async === false
610
690
  ? { async: false }
611
- : {}),
691
+ : delegated?.async === true
692
+ ? { async: true }
693
+ : delegated?.async === false
694
+ ? { async: false }
695
+ : {}),
612
696
  ...(typeof substituted.state_dir === "string"
613
697
  ? { state_dir: substituted.state_dir }
614
- : {}),
698
+ : typeof delegated?.state_dir === "string"
699
+ ? { state_dir: delegated.state_dir }
700
+ : {}),
615
701
  ...(Object.keys(imports).length > 0
616
702
  ? { imports: getRecipeImports(raw) }
617
703
  : {}),
618
704
  template: templateWithContext,
619
705
  ...(Array.isArray(substituted.args)
620
706
  ? { args: substituted.args }
621
- : {}),
622
- ...(isRecord(substituted.defaults)
623
- ? { defaults: substituted.defaults }
624
- : {}),
707
+ : Array.isArray(delegated?.args)
708
+ ? { args: delegated.args }
709
+ : {}),
710
+ ...(mergedDefaults ? { defaults: mergedDefaults } : {}),
625
711
  ...(typeof substituted.parallel === "boolean"
626
712
  ? { parallel: substituted.parallel }
627
713
  : {}),
@@ -643,30 +729,31 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
643
729
  ...(typeof substituted.output === "string"
644
730
  ? { output: substituted.output }
645
731
  : {}),
646
- ...(isRecord(substituted.artifacts)
732
+ ...(isRecord(artifactSource)
647
733
  ? {
648
- artifacts: Object.fromEntries(Object.entries(substituted.artifacts).filter((entry) => typeof entry[1] === "string")),
734
+ artifacts: Object.fromEntries(Object.entries(artifactSource).filter((entry) => typeof entry[1] === "string")),
649
735
  }
650
736
  : {}),
651
- ...(isRecord(substituted.mailbox)
737
+ ...(isRecord(mailboxSource)
652
738
  ? {
653
739
  mailbox: {
654
- ...(Array.isArray(substituted.mailbox.accepts)
740
+ ...(Array.isArray(mailboxSource.accepts)
655
741
  ? {
656
- accepts: substituted.mailbox.accepts.filter((value) => typeof value === "string" ||
742
+ accepts: mailboxSource.accepts.filter((value) => typeof value === "string" ||
657
743
  (isRecord(value) && typeof value.type === "string")),
658
744
  }
659
745
  : {}),
660
- ...(Array.isArray(substituted.mailbox.emits)
746
+ ...(Array.isArray(mailboxSource.emits)
661
747
  ? {
662
- emits: substituted.mailbox.emits.filter((value) => typeof value === "string" ||
748
+ emits: mailboxSource.emits.filter((value) => typeof value === "string" ||
663
749
  (isRecord(value) && typeof value.type === "string")),
664
750
  }
665
751
  : {}),
666
752
  },
667
753
  }
668
754
  : {}),
669
- ...(substituted.retire_when === "children_terminal"
755
+ ...(substituted.retire_when === "children_terminal" ||
756
+ delegated?.retire_when === "children_terminal"
670
757
  ? { retire_when: "children_terminal" }
671
758
  : {}),
672
759
  ...(typeof substituted.retry === "number" ||
@@ -39,7 +39,7 @@ export interface RegisterToolResult {
39
39
  export interface RegisterToolRuntimeDeps<TContext> {
40
40
  configPath: string;
41
41
  recipeRoot?: string;
42
- getExternalToolConflict: (name: string) => string | undefined;
42
+ getToolNameBlocker: (name: string) => string | undefined;
43
43
  getTools: () => Map<string, Config.RegisteredTool>;
44
44
  getActiveTools: () => string[];
45
45
  notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
@@ -51,9 +51,9 @@ function promoteDraftRecipe(name, input, ctx, deps) {
51
51
  const targetPath = getToolRecipePath(deps, name);
52
52
  const tools = deps.getTools();
53
53
  const existing = tools.get(name);
54
- const conflict = deps.getExternalToolConflict(name);
55
- if (conflict)
56
- throw new Error(ExecutionOutput.formatToolText(conflict));
54
+ const blocker = deps.getToolNameBlocker(name);
55
+ if (blocker)
56
+ throw new Error(ExecutionOutput.formatToolText(blocker));
57
57
  if ((existing || existsSync(targetPath)) && !input.update) {
58
58
  throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
59
59
  }
@@ -238,9 +238,9 @@ export async function executeRegisterTool(params, ctx, deps) {
238
238
  return deleteTool(name, ctx, deps);
239
239
  const tools = deps.getTools();
240
240
  const existing = tools.get(name);
241
- const conflict = deps.getExternalToolConflict(name);
242
- if (conflict)
243
- throw new Error(ExecutionOutput.formatToolText(conflict));
241
+ const blocker = deps.getToolNameBlocker(name);
242
+ if (blocker)
243
+ throw new Error(ExecutionOutput.formatToolText(blocker));
244
244
  if (existing && !input.update) {
245
245
  throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
246
246
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool registry runtime coordinator
3
3
  * Zones: runtime coordination, registry loading, pi tools
4
- * Owns persisted tool loading, conflict detection, runtime registration, and warning notification
4
+ * Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
5
5
  */
6
6
  import * as Config from "./config.ts";
7
7
  import type { RegisteredToolExec } from "./execution.ts";
@@ -12,22 +12,18 @@ export interface RuntimeContext {
12
12
  notify(message: string, type?: "info" | "warning" | "error"): void;
13
13
  };
14
14
  }
15
- export interface ToolInfoLike {
16
- name: string;
17
- }
18
15
  export interface ToolRegistryRuntimeDeps {
19
16
  configPath: string;
20
17
  exec: RegisteredToolExec;
21
18
  packagedRecipeRoot?: string;
22
19
  recipeRoot?: string;
23
20
  getActiveTools?: () => string[];
24
- getAllTools: () => ToolInfoLike[];
25
21
  registerTool: (definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>) => void;
26
22
  reservedToolNames: Set<string>;
27
23
  setActiveTools?: (toolNames: string[]) => void;
28
24
  }
29
25
  export interface ToolRegistryRuntime {
30
- getExternalToolConflict(name: string): string | undefined;
26
+ getToolNameBlocker(name: string): string | undefined;
31
27
  getTools(): Map<string, Config.RegisteredTool>;
32
28
  loadTools(ctx: RuntimeContext): void;
33
29
  notify(ctx: RuntimeContext, message: string, type: "info" | "warning" | "error"): void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Tool registry runtime coordinator
3
3
  * Zones: runtime coordination, registry loading, pi tools
4
- * Owns persisted tool loading, conflict detection, runtime registration, and warning notification
4
+ * Owns persisted tool loading, reserved-name guards, runtime registration, and warning notification
5
5
  */
6
6
  import { existsSync, watch } from "node:fs";
7
7
  import * as Paths from "./paths.js";
@@ -15,12 +15,9 @@ export function createAutoToolsRuntime(deps) {
15
15
  if (ctx.hasUI)
16
16
  ctx.ui.notify(message, type);
17
17
  }
18
- function getExternalToolConflict(name) {
19
- if (runtimeTools.has(name))
20
- return undefined;
21
- const existing = deps.getAllTools().find((tool) => tool.name === name);
22
- return existing
23
- ? `Tool "${name}" is already registered outside pi-actors.`
18
+ function getToolNameBlocker(name) {
19
+ return deps.reservedToolNames.has(name)
20
+ ? `Reserved tool name: ${name}`
24
21
  : undefined;
25
22
  }
26
23
  function getToolFingerprint(cfg) {
@@ -55,6 +52,8 @@ export function createAutoToolsRuntime(deps) {
55
52
  runtimeToolFingerprints.set(cfg.name, fingerprint);
56
53
  }
57
54
  function isStartupActionableRegistryWarning(warning) {
55
+ if (warning.includes(" shadows "))
56
+ return false;
58
57
  if (warning.includes("invokes bash;") &&
59
58
  warning.includes("trusted executable content"))
60
59
  return false;
@@ -104,9 +103,9 @@ export function createAutoToolsRuntime(deps) {
104
103
  }
105
104
  deactivateMissingRuntimeTools(new Set(tools.keys()));
106
105
  for (const cfg of tools.values()) {
107
- const conflict = getExternalToolConflict(cfg.name);
108
- if (conflict) {
109
- warnings.push(conflict);
106
+ const blocker = getToolNameBlocker(cfg.name);
107
+ if (blocker) {
108
+ warnings.push(blocker);
110
109
  continue;
111
110
  }
112
111
  registerRuntimeTool(cfg);
@@ -117,7 +116,7 @@ export function createAutoToolsRuntime(deps) {
117
116
  }
118
117
  }
119
118
  return {
120
- getExternalToolConflict,
119
+ getToolNameBlocker,
121
120
  getTools: () => tools,
122
121
  loadTools,
123
122
  notify,
@@ -15,7 +15,7 @@ export interface CoreActorToolDefinitionDeps<TContext extends RuntimeToolContext
15
15
  configPath: string;
16
16
  getActiveTools: () => string[];
17
17
  getRuntimeTool: (name: string) => unknown;
18
- registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getExternalToolConflict" | "getTools" | "notify" | "registerRuntimeTool">;
18
+ registryRuntime: Pick<RegisterToolRuntimeDeps<TContext>, "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool">;
19
19
  setActiveTools: (toolNames: string[]) => void;
20
20
  }
21
21
  export declare const RESERVED_TOOL_NAMES: Set<string>;
package/dist/lib/tools.js CHANGED
@@ -25,7 +25,7 @@ export function createCoreActorToolDefinitions(deps) {
25
25
  ToolsRegister.createRegisterToolDefinition({
26
26
  configPath: deps.configPath,
27
27
  getActiveTools: deps.getActiveTools,
28
- getExternalToolConflict: deps.registryRuntime.getExternalToolConflict,
28
+ getToolNameBlocker: deps.registryRuntime.getToolNameBlocker,
29
29
  getTools: deps.registryRuntime.getTools,
30
30
  notify: deps.registryRuntime.notify,
31
31
  registerRuntimeTool: deps.registryRuntime.registerRuntimeTool,