@llblab/pi-actors 0.46.1 → 0.48.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 (54) hide show
  1. package/AGENTS.md +7 -5
  2. package/BACKLOG.md +0 -582
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +8 -1
  5. package/banner.jpg +0 -0
  6. package/dist/lib/prompts.d.ts +6 -5
  7. package/dist/lib/prompts.js +22 -23
  8. package/dist/lib/recipes-discovery.d.ts +4 -0
  9. package/dist/lib/recipes-discovery.js +13 -1
  10. package/dist/lib/recipes-references.js +10 -6
  11. package/dist/lib/registry.d.ts +15 -11
  12. package/dist/lib/registry.js +195 -25
  13. package/dist/lib/runtime.js +37 -2
  14. package/dist/lib/tools-inspect.js +156 -12
  15. package/dist/lib/tools-register.js +2 -1
  16. package/dist/lib/tools-response.js +5 -1
  17. package/dist/scripts/conformance.mjs +1 -0
  18. package/dist/skills/actors/SKILL.md +87 -65
  19. package/dist/skills/actors/references/diagnostics.md +44 -0
  20. package/dist/skills/actors/references/persistent-tools.md +74 -0
  21. package/dist/skills/actors/references/recipes.md +51 -0
  22. package/dist/skills/actors/references/runs.md +39 -0
  23. package/dist/skills/artifacts/SKILL.md +24 -7
  24. package/dist/skills/media/SKILL.md +35 -7
  25. package/dist/skills/project-work/SKILL.md +28 -7
  26. package/dist/skills/recipe-memory/SKILL.md +27 -7
  27. package/dist/skills/swarm/SKILL.md +56 -437
  28. package/dist/skills/swarm/references/development-swarm.md +118 -525
  29. package/dist/skills/swarm/references/review-swarms.md +115 -0
  30. package/docs/README.md +5 -5
  31. package/docs/recipe-library.md +15 -10
  32. package/docs/tool-registry.md +10 -4
  33. package/lib/prompts.ts +24 -24
  34. package/lib/recipes-discovery.ts +22 -1
  35. package/lib/recipes-references.ts +14 -6
  36. package/lib/registry.ts +288 -51
  37. package/lib/runtime.ts +41 -2
  38. package/lib/tools-inspect.ts +202 -10
  39. package/lib/tools-register.ts +4 -3
  40. package/lib/tools-response.ts +5 -1
  41. package/package.json +1 -1
  42. package/scripts/conformance.mjs +1 -0
  43. package/skills/actors/SKILL.md +87 -65
  44. package/skills/actors/references/diagnostics.md +44 -0
  45. package/skills/actors/references/persistent-tools.md +74 -0
  46. package/skills/actors/references/recipes.md +51 -0
  47. package/skills/actors/references/runs.md +39 -0
  48. package/skills/artifacts/SKILL.md +24 -7
  49. package/skills/media/SKILL.md +35 -7
  50. package/skills/project-work/SKILL.md +28 -7
  51. package/skills/recipe-memory/SKILL.md +27 -7
  52. package/skills/swarm/SKILL.md +56 -437
  53. package/skills/swarm/references/development-swarm.md +118 -525
  54. package/skills/swarm/references/review-swarms.md +115 -0
@@ -4,20 +4,21 @@
4
4
  * Owns LLM-facing descriptions, prompt snippets, guidelines, and parameter descriptions
5
5
  */
6
6
  export declare const REGISTER_TOOL_DESCRIPTION: string;
7
- export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
7
+ export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent Recipes or 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 and shell-free: string leaves split into executable + argv, infer .js/.mjs through node\u2192bun\u2192deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: valid admitted recipes are reconciled as tools across sessions; register_tool writes there and reports current activation truth.\n- Recipes own template directly and may declare metadata/defaults/imports/control/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 internal transport vocabulary in public guidance.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.\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; active-Skill and explicit file Recipes remain components outside user tool discovery; offer to save successful recurring patterns only after confirmation.\n- Prefer maintained active-Skill Recipes with spawn recipe=<skill>/<recipe> before ad hoc scripts/wrappers; explicit file Recipes use exact .json/.md paths; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.\n- For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. 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 Skill routing:\n- Treat active bundled Skills as the operating authority.\n- For non-trivial pi-actors operation, diagnosis, or development, load and read the actors Skill before acting.\n- For work requiring multiple actors or subagents, additionally load and read the swarm Skill.\n- For capability-specific selection or constraints, load the owning capability Skill; actors owns generic mechanics and swarm owns multi-actor methodology.\n- Keep a Skill Recipe distinct from a registered tool and a Recipe spawn distinct from registered-tool invocation; actors owns the proof rules.\n- Treat persistence or registration as distinct from current callability; actors owns activation proof.\n- On failure or disagreement, preserve the logical Recipe identity, stop, and follow actors diagnosis; never bypass the owning Skills with copied contracts, helper paths, shell evaluation, or background-process workarounds.\n- If a capability Skill conflicts with actors about generic mechanics, follow actors and report the stale capability guidance.\n- README and docs are human-facing references, not the normal agent operating path.\n- AGENTS, source, and tests are implementation protocol and evidence; use them when changing or debugging the extension, not as substitutes for operating Skills.";
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.";
13
- readonly draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.";
13
+ readonly from: "Recipe to specialize by canonical <skill>/<recipe> identity or explicit .json/.md path. Inherits async, args/types, source defaults, artifacts, Control, and runtime origins.";
14
+ readonly defaults: "Optional caller-owned defaults. Keys and values must satisfy the effective source or command-template argument contract.";
15
+ readonly draft: "Promote a captured draft Recipe path from ~/.pi/agent/recipes/drafts. This is a source mode; do not combine it with from or template.";
14
16
  readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
15
- readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
17
+ readonly template: "Trusted command template with {arg} or {arg=default} placeholders. To specialize a Recipe, use from instead. Omitted updates keep the old template; empty string deletes the tool.";
16
18
  readonly templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.";
17
19
  readonly templateNull: "Delete the tool when template is null.";
18
20
  readonly args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast";
19
21
  readonly update: "Set to true to overwrite an existing tool registration.";
20
- readonly values: "Optional default runtime placeholder values for a co-located template recipe.";
21
22
  };
22
23
  export declare function formatRegisteredToolPromptSnippet(template: unknown): string;
23
24
  export declare function formatRecipeToolPromptSnippet(recipe: string, asyncRecipe: boolean): string;
@@ -3,41 +3,40 @@
3
3
  * Zones: prompts, onboarding, tool schema copy
4
4
  * Owns LLM-facing descriptions, prompt snippets, guidelines, and parameter descriptions
5
5
  */
6
- export const REGISTER_TOOL_DESCRIPTION = "Register a persistent custom tool from a command template, template recipe path, or co-located template recipe. " +
7
- "Definitions are stored as recipe files under ~/.pi/agent/recipes across reloads. " +
8
- "Use update=true to overwrite an existing tool, template=null/empty to delete.";
9
- export const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
6
+ export const REGISTER_TOOL_DESCRIPTION = "Register a persistent custom tool from one maintained Recipe, command template, or captured draft. " +
7
+ "Use from for Recipe specialization, template for trusted commands, or draft for promotion. " +
8
+ "Definitions persist under ~/.pi/agent/recipes; activation is reported separately.";
9
+ export const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent Recipes or command templates as agent-callable tools";
10
10
  export const REGISTER_TOOL_GUIDELINES = [
11
- "Use register_tool to wrap trusted local commands, scripts, programs, libraries, or template recipes as persistent pi tools.",
12
- "After register_tool succeeds, trust its callable_now and activation result; persisted definitions remain available for admission after reload.",
11
+ "Use register_tool from=<skill>/<recipe> with defaults={...} to specialize a maintained Recipe without copying its contract.",
12
+ "Use register_tool template only for trusted command templates, and use register_tool draft only for captured draft promotion.",
13
+ "After register_tool succeeds, trust its callable_now and activation result; persistence alone is not callability.",
13
14
  'Set template=null or template="" in register_tool to delete a persisted tool.',
14
15
  "Set update=true in register_tool to overwrite an existing tool registration.",
15
16
  ];
16
- export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
17
- - Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
18
- - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
19
- - Command templates stay sync and shell-free: string leaves split into executable + argv, infer .js/.mjs through node→bun→deno run and .sh through bash, and treat operators such as && as literal arguments; use template arrays for sequencing or an explicit trusted shell/script when shell semantics are required. Flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, accept_output, output.
20
- - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
21
- - ~/.pi/agent/recipes/*.json is actor muscle memory: valid admitted recipes are reconciled as tools across sessions; register_tool writes there and reports current activation truth.
22
- - Recipes own template directly and may declare metadata/defaults/imports/control/artifacts; files >1 MiB or import depth >32 fail closed.
23
- - Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
24
- - 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.
25
- - Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid internal transport vocabulary in public guidance.
26
- - Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.
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; active-Skill and explicit file Recipes remain components outside user tool discovery; offer to save successful recurring patterns only after confirmation.
28
- - Prefer maintained active-Skill Recipes with spawn recipe=<skill>/<recipe> before ad hoc scripts/wrappers; explicit file Recipes use exact .json/.md paths; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.
29
- - For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
17
+ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors Skill routing:
18
+ - Treat active bundled Skills as the operating authority.
19
+ - For non-trivial pi-actors operation, diagnosis, or development, load and read the actors Skill before acting.
20
+ - For work requiring multiple actors or subagents, additionally load and read the swarm Skill.
21
+ - For capability-specific selection or constraints, load the owning capability Skill; actors owns generic mechanics and swarm owns multi-actor methodology.
22
+ - Keep a Skill Recipe distinct from a registered tool and a Recipe spawn distinct from registered-tool invocation; actors owns the proof rules.
23
+ - Treat persistence or registration as distinct from current callability; actors owns activation proof.
24
+ - On failure or disagreement, preserve the logical Recipe identity, stop, and follow actors diagnosis; never bypass the owning Skills with copied contracts, helper paths, shell evaluation, or background-process workarounds.
25
+ - If a capability Skill conflicts with actors about generic mechanics, follow actors and report the stale capability guidance.
26
+ - README and docs are human-facing references, not the normal agent operating path.
27
+ - AGENTS, source, and tests are implementation protocol and evidence; use them when changing or debugging the extension, not as substitutes for operating Skills.`;
30
28
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
31
29
  name: "Tool name in snake_case (e.g., 'transcribe')",
32
30
  description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
33
- draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
31
+ from: "Recipe to specialize by canonical <skill>/<recipe> identity or explicit .json/.md path. Inherits async, args/types, source defaults, artifacts, Control, and runtime origins.",
32
+ defaults: "Optional caller-owned defaults. Keys and values must satisfy the effective source or command-template argument contract.",
33
+ draft: "Promote a captured draft Recipe path from ~/.pi/agent/recipes/drafts. This is a source mode; do not combine it with from or template.",
34
34
  async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
35
- template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.",
35
+ template: "Trusted command template with {arg} or {arg=default} placeholders. To specialize a Recipe, use from instead. Omitted updates keep the old template; empty string deletes the tool.",
36
36
  templateArray: "Sequential command-template composition array. Leaves may be strings or objects with template/defaults/timeout/retry/failure/recover.",
37
37
  templateNull: "Delete the tool when template is null.",
38
38
  args: "Optional comma-separated placeholder declarations. Usually omit because args are derived from template placeholders. Interactive shorthand defaults are accepted and normalized. Example: file,lang,mode=fast",
39
39
  update: "Set to true to overwrite an existing tool registration.",
40
- values: "Optional default runtime placeholder values for a co-located template recipe.",
41
40
  };
42
41
  export function formatRegisteredToolPromptSnippet(template) {
43
42
  const rendered = typeof template === "string" ? template : JSON.stringify(template);
@@ -69,5 +69,9 @@ export declare function createRecipeIntegrityManifest(result: RecipesDiscoveryRe
69
69
  export declare function getShadowedLaunchDiagnostic(result: RecipesDiscoveryResult, id: string): Record<string, unknown> | undefined;
70
70
  export declare function listDraftRecipes(root: string): Array<Record<string, unknown>>;
71
71
  export declare function summarizeDiscovery(result: RecipesDiscoveryResult): Record<string, unknown>;
72
+ export declare function summarizeRegisteredToolArgs(tool: RegisteredTool): {
73
+ optional: string[];
74
+ required: string[];
75
+ };
72
76
  export declare function admitUserRecipe(sourcePath: string, resolutionContext: RecipeResolutionContext, authoredRecipe?: Record<string, unknown>, mutableUsage?: boolean): UserRecipeAdmission;
73
77
  export declare function toRegisteredTool(entry: DiscoveredRecipe): RegisteredTool | undefined;
@@ -134,7 +134,7 @@ function readDiscoveredRecipe(root, file, priority, defaultTool = false, mutable
134
134
  mutableUsage,
135
135
  resolutionContext,
136
136
  diagnostics: [
137
- `Failed to load recipe ${file}: ${error instanceof Error ? error.message : String(error)}`,
137
+ `Recipe ${id} rejected: ${error instanceof Error ? error.message : String(error)}`,
138
138
  ],
139
139
  riskLabels: [],
140
140
  shadows: [],
@@ -699,6 +699,18 @@ function projectRegisteredTool(identity, path, cfg, mutableUsage) {
699
699
  : {}),
700
700
  };
701
701
  }
702
+ export function summarizeRegisteredToolArgs(tool) {
703
+ const publicArgs = tool.args.filter((arg) => !RecipesReferences.isRuntimeOwnedRecipeInput(arg));
704
+ const inlineDefaults = Schema.getExplicitToolArgDefaults(tool.recipe?.args);
705
+ const required = publicArgs.filter((arg) => !Object.hasOwn(tool.defaults, arg) &&
706
+ !Object.hasOwn(inlineDefaults, arg));
707
+ const requiredSet = new Set(required);
708
+ const optional = publicArgs.filter((arg) => !requiredSet.has(arg));
709
+ if (tool.recipe?.async === true) {
710
+ optional.push("run_id", "transport_context");
711
+ }
712
+ return { optional, required };
713
+ }
702
714
  function boundedAdmissionDiagnostic(error) {
703
715
  const message = error instanceof Error ? error.message : String(error);
704
716
  return message.length <= 4096 ? message : `${message.slice(0, 4095)}…`;
@@ -72,7 +72,7 @@ export function inventoryActiveSkillRecipeComponents(context) {
72
72
  for (const [skill, roots] of Object.entries(context.namespaces)) {
73
73
  if (roots.length > 1) {
74
74
  rejected.push({
75
- reason: `Duplicate active Skill identity ${skill}: ${roots.join(", ")}`,
75
+ reason: `Duplicate active Skill identity ${skill} (${roots.length} active definitions); exact Recipe resolution is ambiguous. Next: inspect target=recipes view=doctor identity=${skill}/<recipe>`,
76
76
  skill,
77
77
  });
78
78
  continue;
@@ -175,10 +175,11 @@ function qualifiedRecipeNameForFile(file, context) {
175
175
  }
176
176
  function assertRecipeReferencePrefixWasNotRemoved(value) {
177
177
  if (value.startsWith("std:")) {
178
- throw new Error("std: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> or an explicit .json/.md file path.");
178
+ throw new Error("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.");
179
179
  }
180
180
  if (value.startsWith("skill:")) {
181
- throw new Error("skill: Recipe references were removed in pi-actors 0.46.\nUse <skill>/<recipe> without a prefix.");
181
+ const identity = value.slice("skill:".length);
182
+ throw new Error(`skill: Recipe references were removed in pi-actors 0.46. Use ${identity} without a prefix. Next: inspect target=recipes view=doctor identity=${identity}`);
182
183
  }
183
184
  }
184
185
  function skillRecipeCandidates(value, context) {
@@ -188,12 +189,15 @@ function skillRecipeCandidates(value, context) {
188
189
  const [, skillName, recipeName] = match;
189
190
  const roots = [...(context.namespaces[skillName] ?? [])];
190
191
  if (roots.length === 0)
191
- throw new Error(`Active Skill Recipe not found: ${skillName}/${recipeName}`);
192
+ throw new Error(`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}`);
192
193
  if (roots.length > 1)
193
- throw new Error(`Duplicate active Skill identity ${skillName}: ${roots.join(", ")}`);
194
+ throw new Error(`Duplicate active Skill identity ${skillName} (${roots.length} active definitions); ${skillName}/${recipeName} cannot resolve unambiguously. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`);
194
195
  const root = roots[0];
195
196
  const candidates = ["json", "md"].map((extension) => resolve(root, `${recipeName}.${extension}`));
196
197
  const existing = candidates.filter((candidate) => existsSync(candidate));
198
+ if (existing.length === 0) {
199
+ throw new Error(`Skill Recipe component not found: ${skillName}/${recipeName}; owning Skill "${skillName}" is active. Next: inspect target=recipes view=doctor identity=${skillName}/${recipeName}`);
200
+ }
197
201
  if (existing.length > 1) {
198
202
  throw new Error(`Skill Recipe stem collision: ${skillName}/${recipeName} has both .json and .md files`);
199
203
  }
@@ -223,7 +227,7 @@ function findOwningSkillDir(recipeFile) {
223
227
  function assertRecipeHasNoDeclaredName(raw) {
224
228
  if (!Object.hasOwn(raw, "name"))
225
229
  return;
226
- throw new Error("Recipe.name was removed in pi-actors 0.46.\nFile-backed Recipe identity is derived from its file name.");
230
+ throw new Error("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.");
227
231
  }
228
232
  function assertRuntimeRecipeValuesNotDeclared(raw) {
229
233
  const args = Array.isArray(raw.args)
@@ -10,27 +10,29 @@ export interface RegisterToolInput {
10
10
  description?: string;
11
11
  async?: boolean;
12
12
  template?: CommandTemplates.CommandTemplateValue | null;
13
+ from?: string;
14
+ defaults?: Record<string, unknown>;
13
15
  draft?: string;
14
16
  args?: string;
15
17
  update?: boolean;
16
- values?: Record<string, unknown>;
17
18
  }
18
19
  export interface RegisterToolResultDetails {
19
20
  active_tool?: boolean;
20
21
  activation?: "current_session" | "unverified";
22
+ activation_boundary?: string;
21
23
  args?: string[];
22
24
  async?: boolean;
23
25
  callable_now?: boolean;
24
- config?: string;
25
26
  defaults?: Record<string, string>;
26
- draft?: string;
27
27
  host_registered?: boolean;
28
+ next_actions?: string[];
29
+ optional_args?: string[];
28
30
  persisted?: boolean;
29
31
  promoted?: boolean;
30
32
  registry_active?: boolean;
33
+ required_args?: string[];
31
34
  resolved?: boolean;
32
- recipeName?: string;
33
- template?: CommandTemplates.CommandTemplateValue;
35
+ source?: string;
34
36
  templateWarnings?: string[];
35
37
  tool: string;
36
38
  validated?: boolean;
@@ -42,6 +44,12 @@ export interface RegisterToolResult {
42
44
  }>;
43
45
  details: RegisterToolResultDetails;
44
46
  }
47
+ interface RuntimeActivation {
48
+ active_tool: boolean;
49
+ activation: "current_session" | "unverified";
50
+ callable_now: boolean;
51
+ host_registered: boolean;
52
+ }
45
53
  export interface RegisterToolRuntimeDeps<TContext> {
46
54
  configPath: string;
47
55
  recipeRoot?: string;
@@ -49,13 +57,9 @@ export interface RegisterToolRuntimeDeps<TContext> {
49
57
  getTools: () => Map<string, Config.RegisteredTool>;
50
58
  getActiveTools: () => string[];
51
59
  notify: (ctx: TContext, message: string, type: "info" | "warning" | "error") => void;
52
- registerRuntimeTool: (cfg: Config.RegisteredTool) => {
53
- active_tool: boolean;
54
- activation: "current_session" | "unverified";
55
- callable_now: boolean;
56
- host_registered: boolean;
57
- } | void;
60
+ registerRuntimeTool: (cfg: Config.RegisteredTool) => RuntimeActivation | void;
58
61
  reservedToolNames: Set<string>;
59
62
  setActiveTools: (toolNames: string[]) => void;
60
63
  }
61
64
  export declare function executeRegisterTool<TContext>(params: unknown, ctx: TContext, deps: RegisterToolRuntimeDeps<TContext>): Promise<RegisterToolResult>;
65
+ export {};
@@ -35,6 +35,42 @@ function getRecipeRoot(deps) {
35
35
  function getToolRecipePath(deps, name) {
36
36
  return join(getRecipeRoot(deps), `${name}.json`);
37
37
  }
38
+ function activationBoundary(activation) {
39
+ if (activation?.callable_now)
40
+ return "current_session";
41
+ if (!activation)
42
+ return "session_activation_unverified";
43
+ if (!activation.host_registered)
44
+ return "host_registration";
45
+ if (!activation.active_tool)
46
+ return "active_tool_set";
47
+ return "callability_verification";
48
+ }
49
+ function recipeDoctorAction(source) {
50
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(source)
51
+ ? `inspect target=recipes view=doctor identity=${source}`
52
+ : "inspect target=recipes view=doctor";
53
+ }
54
+ function registrationNextActions(tool, source, callableNow) {
55
+ if (callableNow) {
56
+ return [
57
+ `call tool ${tool}`,
58
+ `inspect target=tool:${tool} view=status`,
59
+ ];
60
+ }
61
+ return [
62
+ `inspect target=tool:${tool} view=status`,
63
+ ...(source === "template" || source === "draft"
64
+ ? []
65
+ : [recipeDoctorAction(source)]),
66
+ ];
67
+ }
68
+ function activeSkillSummary(resolutionContext) {
69
+ const names = Object.keys(RecipesReferences.getActiveSkillRecipeNamespaces(resolutionContext.activeSkills))
70
+ .sort()
71
+ .slice(0, 20);
72
+ return names.length > 0 ? names.join(", ") : "<none>";
73
+ }
38
74
  function assertDraftPath(deps, draft) {
39
75
  const draftRoot = resolve(getRecipeRoot(deps), "drafts");
40
76
  const path = resolve(draft);
@@ -79,18 +115,33 @@ function promoteDraftRecipe(name, input, ctx, deps) {
79
115
  writeJsonAtomic(targetPath, authoredRecipe);
80
116
  const promoted = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext).tool;
81
117
  tools.set(name, promoted);
82
- deps.registerRuntimeTool(promoted);
118
+ const activation = deps.registerRuntimeTool(promoted) ?? undefined;
119
+ const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(promoted);
120
+ const callableNow = activation?.callable_now ?? false;
121
+ const nextActions = registrationNextActions(name, "draft", callableNow);
83
122
  deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
84
123
  return {
85
124
  content: [
86
- textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`)),
125
+ textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft; callable_now=${callableNow}; next=${nextActions[0]}.`)),
87
126
  ],
88
127
  details: {
128
+ active_tool: activation?.active_tool ?? false,
129
+ activation: activation?.activation ?? "unverified",
130
+ activation_boundary: activationBoundary(activation),
89
131
  args: promoted.args,
90
- config: targetPath,
91
- draft: draftPath,
132
+ callable_now: callableNow,
133
+ defaults: promoted.defaults,
134
+ host_registered: activation?.host_registered ?? false,
135
+ next_actions: nextActions,
136
+ optional_args: argSummary.optional,
137
+ persisted: true,
92
138
  promoted: true,
139
+ registry_active: tools.get(name) === promoted,
140
+ required_args: argSummary.required,
141
+ resolved: true,
142
+ source: "draft",
93
143
  tool: name,
144
+ validated: true,
94
145
  },
95
146
  };
96
147
  }
@@ -120,7 +171,7 @@ function deleteTool(name, ctx, deps) {
120
171
  content: [
121
172
  textContent(ExecutionOutput.formatToolText(`Deleted tool "${name}". Reload to remove it from the complete registry.`)),
122
173
  ],
123
- details: { config: recipePath, tool: name },
174
+ details: { tool: name },
124
175
  };
125
176
  }
126
177
  function readAuthoritativeStoredTool(deps, name) {
@@ -144,7 +195,25 @@ function readAuthoritativeStoredTool(deps, name) {
144
195
  return { exists: true };
145
196
  }
146
197
  }
198
+ function looksLikeRecipeReference(value) {
199
+ const trimmed = value.trim();
200
+ return (trimmed.endsWith(".json") ||
201
+ trimmed.endsWith(".md") ||
202
+ /^[^/:\\]+\/[^/:\\]+$/.test(trimmed));
203
+ }
204
+ function assertTemplateIsNotNestedRecipe(value) {
205
+ if (!value || typeof value !== "object" || Array.isArray(value))
206
+ return;
207
+ const record = value;
208
+ if (typeof record.template === "string" &&
209
+ looksLikeRecipeReference(record.template) &&
210
+ (Object.keys(record).length === 1 ||
211
+ ["artifacts", "async", "control", "description", "imports", "values"].some((key) => Object.hasOwn(record, key)))) {
212
+ throw new Error(ExecutionOutput.formatToolText("Nested Recipe-shaped templates are not a register_tool source mode. Next: retry with from=<skill>/<recipe> and defaults={...}."));
213
+ }
214
+ }
147
215
  function getInputTemplate(value) {
216
+ assertTemplateIsNotNestedRecipe(value);
148
217
  if (typeof value === "string")
149
218
  return value.trim();
150
219
  if (value === null || value === undefined)
@@ -173,15 +242,61 @@ function getRegistrationResolutionContext(ctx, deps) {
173
242
  }
174
243
  return RecipesContext.createEmptyRecipeResolutionContext("offline-register-tool", getRecipeRoot(deps));
175
244
  }
245
+ function normalizeDefaults(value) {
246
+ if (value === undefined)
247
+ return undefined;
248
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
249
+ throw new Error(ExecutionOutput.formatToolText("Tool defaults must be an object."));
250
+ }
251
+ return { ...value };
252
+ }
253
+ function normalizeFromReference(value, resolutionContext) {
254
+ const trimmed = value.trim();
255
+ if (!trimmed) {
256
+ throw new Error(ExecutionOutput.formatToolText("Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path."));
257
+ }
258
+ if (trimmed.startsWith("std:")) {
259
+ throw new Error(ExecutionOutput.formatToolText("std: Recipe references were removed; use <skill>/<recipe> or an explicit .json/.md file path."));
260
+ }
261
+ if (trimmed.startsWith("skill:")) {
262
+ throw new Error(ExecutionOutput.formatToolText(`skill: Recipe references were removed; use ${trimmed.slice("skill:".length)} without a prefix.`));
263
+ }
264
+ if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) {
265
+ return resolve(resolutionContext.cwd, trimmed);
266
+ }
267
+ if (!looksLikeRecipeReference(trimmed)) {
268
+ throw new Error(ExecutionOutput.formatToolText("Tool from must use <skill>/<recipe> or an explicit .json/.md file path."));
269
+ }
270
+ return trimmed;
271
+ }
176
272
  function buildAuthoredRecipe(input, existing, existingRecipe) {
177
273
  const explicitArgs = input.args === undefined
178
274
  ? undefined
179
275
  : Schema.parseToolArgDeclarations(input.args);
180
276
  if (explicitArgs?.error)
181
277
  throw new Error(ExecutionOutput.formatToolText(explicitArgs.error));
278
+ const from = typeof input.from === "string" ? input.from.trim() : "";
182
279
  const description = (input.description ?? existing?.description ?? "").trim();
183
- if (!description) {
184
- throw new Error(ExecutionOutput.formatToolText("Tool description is required unless deleting."));
280
+ if (!description && !from) {
281
+ throw new Error(ExecutionOutput.formatToolText("Tool description is required for command-template registration."));
282
+ }
283
+ const inputDefaults = normalizeDefaults(input.defaults);
284
+ if (from) {
285
+ const runtimeOwnedDefault = Object.keys(inputDefaults ?? {}).find((key) => RecipesReferences.isRuntimeOwnedRecipeInput(key));
286
+ if (runtimeOwnedDefault) {
287
+ throw new Error(ExecutionOutput.formatToolText(`Tool defaults cannot set runtime-owned input: ${runtimeOwnedDefault}. Next: remove that default and retry register_tool; the runtime supplies it.`));
288
+ }
289
+ const authored = {
290
+ ...(description ? { description } : {}),
291
+ template: from,
292
+ };
293
+ const retainedDefaults = inputDefaults ??
294
+ (existingRecipe?.defaults && typeof existingRecipe.defaults === "object"
295
+ ? existingRecipe.defaults
296
+ : undefined);
297
+ if (retainedDefaults && Object.keys(retainedDefaults).length > 0)
298
+ authored.defaults = retainedDefaults;
299
+ return authored;
185
300
  }
186
301
  const template = getInputTemplate(input.template);
187
302
  if (template === null) {
@@ -202,17 +317,47 @@ function buildAuthoredRecipe(input, existing, existingRecipe) {
202
317
  authored.async = input.async;
203
318
  if (explicitArgs) {
204
319
  authored.args = explicitArgs.declarations;
205
- if (Object.keys(explicitArgs.defaults).length > 0)
206
- authored.defaults = explicitArgs.defaults;
320
+ const defaults = { ...explicitArgs.defaults, ...(inputDefaults ?? {}) };
321
+ if (Object.keys(defaults).length > 0)
322
+ authored.defaults = defaults;
323
+ else
324
+ delete authored.defaults;
325
+ }
326
+ else if (inputDefaults !== undefined) {
327
+ if (Object.keys(inputDefaults).length > 0)
328
+ authored.defaults = inputDefaults;
207
329
  else
208
330
  delete authored.defaults;
209
331
  }
210
- if (input.values && typeof input.values === "object")
211
- authored.values = input.values;
212
332
  return authored;
213
333
  }
334
+ function assertRegisterToolInputModes(input) {
335
+ const record = input;
336
+ if (Object.hasOwn(record, "values")) {
337
+ throw new Error(ExecutionOutput.formatToolText("register_tool values was removed. Next: retry register_tool with defaults for caller inputs, or use from=<skill>/<recipe> for maintained composition."));
338
+ }
339
+ const fromProvided = Object.hasOwn(record, "from");
340
+ if (fromProvided &&
341
+ (typeof input.from !== "string" || input.from.trim().length === 0)) {
342
+ throw new Error(ExecutionOutput.formatToolText("Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path."));
343
+ }
344
+ const draftProvided = typeof input.draft === "string" && input.draft.trim().length > 0;
345
+ const templateProvided = Object.hasOwn(record, "template");
346
+ const modeCount = Number(fromProvided) + Number(draftProvided) + Number(templateProvided);
347
+ if (modeCount > 1) {
348
+ throw new Error(ExecutionOutput.formatToolText("Use exactly one register_tool source mode: from, template, or draft. Next: remove the extra source fields and retry register_tool."));
349
+ }
350
+ if (fromProvided &&
351
+ (Object.hasOwn(record, "args") || Object.hasOwn(record, "async"))) {
352
+ throw new Error(ExecutionOutput.formatToolText("register_tool from inherits args and async behavior. Next: retry register_tool with from and optional defaults only; omit args and async."));
353
+ }
354
+ }
214
355
  function executeRegisterToolUnlocked(params, ctx, deps) {
215
- const input = params;
356
+ const rawInput = params && typeof params === "object"
357
+ ? params
358
+ : {};
359
+ assertRegisterToolInputModes(rawInput);
360
+ let input = rawInput;
216
361
  if (!input.name)
217
362
  return listTools(deps);
218
363
  const name = Identity.normalizeToolName(input.name);
@@ -224,6 +369,23 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
224
369
  if (typeof input.draft === "string" && input.draft.trim()) {
225
370
  return promoteDraftRecipe(name, input, ctx, deps);
226
371
  }
372
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
373
+ const requestedSource = typeof rawInput.from === "string" ? rawInput.from.trim() : undefined;
374
+ if (typeof input.from === "string") {
375
+ try {
376
+ input = {
377
+ ...input,
378
+ from: normalizeFromReference(input.from, resolutionContext),
379
+ };
380
+ }
381
+ catch (error) {
382
+ const reason = error instanceof Error ? error.message.trim() : String(error);
383
+ const diagnosticSource = requestedSource?.startsWith("skill:")
384
+ ? requestedSource.slice("skill:".length)
385
+ : requestedSource ?? "";
386
+ throw new Error(ExecutionOutput.formatToolText(`Recipe source "${requestedSource}" is invalid: ${reason}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(diagnosticSource)}`));
387
+ }
388
+ }
227
389
  const templateProvided = Object.hasOwn(input, "template");
228
390
  const template = getInputTemplate(input.template);
229
391
  if (templateProvided && (template === null || template === ""))
@@ -237,15 +399,17 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
237
399
  if ((authoritative.exists || existing) && !input.update) {
238
400
  throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
239
401
  }
240
- if (template === undefined && !existing) {
241
- throw new Error(ExecutionOutput.formatToolText("Tool template is required for new registrations."));
402
+ if (template === undefined && !input.from && !existing) {
403
+ throw new Error(ExecutionOutput.formatToolText("New registration requires exactly one source mode: from, template, or draft. Next: retry register_tool with one source field."));
242
404
  }
243
405
  const recipePath = getToolRecipePath(deps, name);
244
406
  const authoredRecipe = buildAuthoredRecipe(input, existing, authoritative.recipe);
245
- const resolutionContext = getRegistrationResolutionContext(ctx, deps);
246
407
  const candidate = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext, authoredRecipe);
247
408
  if (!candidate.validated || !candidate.tool) {
248
- throw new Error(ExecutionOutput.formatToolText(`Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`));
409
+ const sourceDiagnostic = requestedSource
410
+ ? `Recipe source "${requestedSource}" validation failed: ${candidate.diagnostics.join("; ")}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(requestedSource)}`
411
+ : `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`;
412
+ throw new Error(ExecutionOutput.formatToolText(sourceDiagnostic));
249
413
  }
250
414
  const activeBefore = deps.getActiveTools();
251
415
  let persisted = false;
@@ -265,7 +429,7 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
265
429
  (!activation.host_registered ||
266
430
  !activation.active_tool ||
267
431
  !activation.callable_now)) {
268
- throw new Error(`Host activation verification failed: ${JSON.stringify(activation)}`);
432
+ throw new Error(`Tool "${name}" activation failed at ${activationBoundary(activation)}; host_registered=${activation.host_registered}, active_tool=${activation.active_tool}, callable_now=${activation.callable_now}. Registration was rolled back. Next: inspect target=runtime view=status; do not use spawn as proof of tool invocation.`);
269
433
  }
270
434
  }
271
435
  catch (error) {
@@ -292,27 +456,33 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
292
456
  const warningText = templateWarnings.length > 0
293
457
  ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
294
458
  : "";
295
- const outcomeText = activation?.callable_now
296
- ? `${existing ? "Updated" : "Registered"} and activated tool "${name}"`
297
- : `Persisted tool "${name}"; current-session activation is unverified`;
459
+ const callableNow = activation?.callable_now ?? false;
460
+ const source = requestedSource ?? "template";
461
+ const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
462
+ const nextActions = registrationNextActions(name, source, callableNow);
463
+ const outcomeText = callableNow
464
+ ? `${existing ? "Updated" : "Registered"} tool "${name}" from ${source}; callable_now=true`
465
+ : `Persisted tool "${name}" from ${source}; callable_now=false; activation_boundary=${activationBoundary(activation)}`;
298
466
  return {
299
467
  content: [
300
- textContent(ExecutionOutput.formatToolText(`${outcomeText} (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`)),
468
+ textContent(ExecutionOutput.formatToolText(`${outcomeText}; required=${Schema.formatToolArgs(argSummary.required)}; optional=${Schema.formatToolArgs(argSummary.optional)}; next=${nextActions[0]}.${warningText}`)),
301
469
  ],
302
470
  details: {
303
471
  active_tool: activation?.active_tool ?? false,
304
472
  activation: activation?.activation ?? "unverified",
473
+ activation_boundary: activationBoundary(activation),
305
474
  args: cfg.args,
306
- callable_now: activation?.callable_now ?? false,
307
- config: recipePath,
475
+ callable_now: callableNow,
308
476
  defaults: cfg.defaults,
309
477
  host_registered: activation?.host_registered ?? false,
478
+ next_actions: nextActions,
479
+ optional_args: argSummary.optional,
310
480
  persisted: true,
311
481
  registry_active: tools.get(name) === cfg,
482
+ required_args: argSummary.required,
312
483
  resolved: true,
484
+ source,
313
485
  ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
314
- ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
315
- ...(cfg.template ? { template: cfg.template } : {}),
316
486
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
317
487
  tool: name,
318
488
  validated: true,