@llblab/pi-actors 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AGENTS.md +7 -5
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +2 -1
  4. package/dist/lib/async-runs.d.ts +1 -0
  5. package/dist/lib/async-runs.js +4 -1
  6. package/dist/lib/execution.d.ts +1 -0
  7. package/dist/lib/execution.js +1 -0
  8. package/dist/lib/extension-runtime.js +25 -9
  9. package/dist/lib/inspector.js +1 -0
  10. package/dist/lib/prompts.d.ts +6 -5
  11. package/dist/lib/prompts.js +22 -23
  12. package/dist/lib/recipes-context.d.ts +12 -3
  13. package/dist/lib/recipes-context.js +28 -4
  14. package/dist/lib/recipes-discovery.d.ts +22 -0
  15. package/dist/lib/recipes-discovery.js +108 -23
  16. package/dist/lib/recipes-references.d.ts +18 -0
  17. package/dist/lib/recipes-references.js +129 -38
  18. package/dist/lib/registry.d.ts +23 -6
  19. package/dist/lib/registry.js +294 -101
  20. package/dist/lib/runtime.d.ts +30 -3
  21. package/dist/lib/runtime.js +108 -9
  22. package/dist/lib/tools-inspect.d.ts +3 -0
  23. package/dist/lib/tools-inspect.js +190 -26
  24. package/dist/lib/tools-local.d.ts +2 -2
  25. package/dist/lib/tools-local.js +5 -2
  26. package/dist/lib/tools-register.js +2 -1
  27. package/dist/lib/tools-response.js +7 -1
  28. package/dist/lib/tools-spawn.d.ts +2 -2
  29. package/dist/lib/tools-spawn.js +1 -1
  30. package/dist/lib/tools.d.ts +4 -1
  31. package/dist/lib/tools.js +2 -0
  32. package/dist/scripts/conformance.mjs +1 -0
  33. package/dist/skills/actors/SKILL.md +76 -65
  34. package/dist/skills/actors/references/diagnostics.md +44 -0
  35. package/dist/skills/actors/references/persistent-tools.md +74 -0
  36. package/dist/skills/actors/references/recipes.md +51 -0
  37. package/dist/skills/actors/references/runs.md +39 -0
  38. package/dist/skills/artifacts/SKILL.md +24 -7
  39. package/dist/skills/media/SKILL.md +35 -7
  40. package/dist/skills/project-work/SKILL.md +28 -7
  41. package/dist/skills/recipe-memory/SKILL.md +27 -7
  42. package/dist/skills/swarm/SKILL.md +41 -445
  43. package/dist/skills/swarm/references/development-swarm.md +87 -539
  44. package/dist/skills/swarm/references/review-swarms.md +115 -0
  45. package/docs/README.md +5 -5
  46. package/docs/recipe-library.md +15 -10
  47. package/docs/template-recipes.md +3 -1
  48. package/docs/tool-registry.md +18 -6
  49. package/lib/async-runs.ts +5 -1
  50. package/lib/execution.ts +2 -0
  51. package/lib/extension-runtime.ts +33 -14
  52. package/lib/inspector.ts +1 -0
  53. package/lib/prompts.ts +24 -24
  54. package/lib/recipes-context.ts +49 -4
  55. package/lib/recipes-discovery.ts +186 -25
  56. package/lib/recipes-references.ts +176 -44
  57. package/lib/registry.ts +441 -113
  58. package/lib/runtime.ts +147 -12
  59. package/lib/tools-inspect.ts +254 -28
  60. package/lib/tools-local.ts +9 -3
  61. package/lib/tools-register.ts +4 -3
  62. package/lib/tools-response.ts +7 -1
  63. package/lib/tools-spawn.ts +3 -2
  64. package/lib/tools.ts +4 -1
  65. package/package.json +1 -1
  66. package/scripts/conformance.mjs +1 -0
  67. package/skills/actors/SKILL.md +76 -65
  68. package/skills/actors/references/diagnostics.md +44 -0
  69. package/skills/actors/references/persistent-tools.md +74 -0
  70. package/skills/actors/references/recipes.md +51 -0
  71. package/skills/actors/references/runs.md +39 -0
  72. package/skills/artifacts/SKILL.md +24 -7
  73. package/skills/media/SKILL.md +35 -7
  74. package/skills/project-work/SKILL.md +28 -7
  75. package/skills/recipe-memory/SKILL.md +27 -7
  76. package/skills/swarm/SKILL.md +41 -445
  77. package/skills/swarm/references/development-swarm.md +87 -539
  78. package/skills/swarm/references/review-swarms.md +115 -0
@@ -8,9 +8,11 @@ import { dirname, join, relative, resolve } from "node:path";
8
8
  import * as CommandTemplates from "./command-templates.js";
9
9
  import * as Config from "./config.js";
10
10
  import * as ExecutionOutput from "./execution-output.js";
11
- import { withFileMutationLock, writeJsonAtomic } from "./file-state.js";
11
+ import { withFileMutationLock, writeJsonAtomic, writeTextAtomic, } from "./file-state.js";
12
12
  import * as Identity from "./identity.js";
13
13
  import * as Paths from "./paths.js";
14
+ import * as RecipesContext from "./recipes-context.js";
15
+ import * as RecipesDiscovery from "./recipes-discovery.js";
14
16
  import * as RecipesReferences from "./recipes-references.js";
15
17
  import * as Schema from "./schema.js";
16
18
  function textContent(text) {
@@ -33,6 +35,42 @@ function getRecipeRoot(deps) {
33
35
  function getToolRecipePath(deps, name) {
34
36
  return join(getRecipeRoot(deps), `${name}.json`);
35
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
+ }
36
74
  function assertDraftPath(deps, draft) {
37
75
  const draftRoot = resolve(getRecipeRoot(deps), "drafts");
38
76
  const path = resolve(draft);
@@ -58,48 +96,59 @@ function promoteDraftRecipe(name, input, ctx, deps) {
58
96
  if ((existing || existsSync(targetPath)) && !input.update) {
59
97
  throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
60
98
  }
61
- const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
62
- if (!config) {
99
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath);
100
+ if (!raw) {
63
101
  const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
64
102
  throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid${reason ? `: ${reason}` : "."}`));
65
103
  }
66
- const promoted = buildConfig(name, {
104
+ const authoredRecipe = buildAuthoredRecipe({
67
105
  ...input,
68
- description: input.description ?? config.description,
69
- template: draftPath,
70
- }, existing);
71
- const raw = RecipesReferences.readRawRecipeConfig(draftPath);
106
+ description: input.description ??
107
+ (typeof raw.description === "string" ? raw.description : undefined),
108
+ }, existing, raw);
109
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
110
+ const candidate = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext, authoredRecipe);
111
+ if (!candidate.validated || !candidate.tool) {
112
+ throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`));
113
+ }
72
114
  mkdirSync(dirname(targetPath), { recursive: true });
73
- writeJsonAtomic(targetPath, raw);
74
- promoted.template = targetPath;
75
- promoted.sourcePath = targetPath;
115
+ writeJsonAtomic(targetPath, authoredRecipe);
116
+ const promoted = RecipesDiscovery.admitUserRecipe(targetPath, resolutionContext).tool;
76
117
  tools.set(name, promoted);
77
- 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);
78
122
  deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
79
123
  return {
80
124
  content: [
81
- 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]}.`)),
82
126
  ],
83
127
  details: {
128
+ active_tool: activation?.active_tool ?? false,
129
+ activation: activation?.activation ?? "unverified",
130
+ activation_boundary: activationBoundary(activation),
84
131
  args: promoted.args,
85
- config: targetPath,
86
- 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,
87
138
  promoted: true,
139
+ registry_active: tools.get(name) === promoted,
140
+ required_args: argSummary.required,
141
+ resolved: true,
142
+ source: "draft",
88
143
  tool: name,
144
+ validated: true,
89
145
  },
90
146
  };
91
147
  }
92
- function persistToolRecipe(deps, cfg) {
93
- const path = getToolRecipePath(deps, cfg.name);
148
+ function persistToolRecipe(deps, name, recipe) {
149
+ const path = getToolRecipePath(deps, name);
94
150
  mkdirSync(dirname(path), { recursive: true });
95
- writeJsonAtomic(path, {
96
- description: cfg.description,
97
- ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
98
- ...(cfg.storedArgs ? { args: cfg.storedArgs } : {}),
99
- ...(cfg.storedDefaults ? { defaults: cfg.storedDefaults } : {}),
100
- ...(cfg.recipe?.values ? { values: cfg.recipe.values } : {}),
101
- template: cfg.template,
102
- });
151
+ writeJsonAtomic(path, recipe);
103
152
  return path;
104
153
  }
105
154
  function deleteTool(name, ctx, deps) {
@@ -122,7 +171,7 @@ function deleteTool(name, ctx, deps) {
122
171
  content: [
123
172
  textContent(ExecutionOutput.formatToolText(`Deleted tool "${name}". Reload to remove it from the complete registry.`)),
124
173
  ],
125
- details: { config: recipePath, tool: name },
174
+ details: { tool: name },
126
175
  };
127
176
  }
128
177
  function readAuthoritativeStoredTool(deps, name) {
@@ -130,11 +179,15 @@ function readAuthoritativeStoredTool(deps, name) {
130
179
  if (!existsSync(path))
131
180
  return { exists: false };
132
181
  try {
133
- const normalized = Config.normalizeStoredTool(name, JSON.parse(readFileSync(path, "utf8")), deps.reservedToolNames);
182
+ const bytes = readFileSync(path, "utf8");
183
+ const recipe = JSON.parse(bytes);
184
+ const normalized = Config.normalizeStoredTool(name, recipe, deps.reservedToolNames);
134
185
  if (!normalized.cfg)
135
- return { exists: true };
186
+ return { bytes, exists: true, recipe };
136
187
  return {
188
+ bytes,
137
189
  exists: true,
190
+ recipe,
138
191
  tool: { ...normalized.cfg, sourcePath: path },
139
192
  };
140
193
  }
@@ -142,7 +195,25 @@ function readAuthoritativeStoredTool(deps, name) {
142
195
  return { exists: true };
143
196
  }
144
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
+ }
145
215
  function getInputTemplate(value) {
216
+ assertTemplateIsNotNestedRecipe(value);
146
217
  if (typeof value === "string")
147
218
  return value.trim();
148
219
  if (value === null || value === undefined)
@@ -161,80 +232,132 @@ function getInputTemplate(value) {
161
232
  }
162
233
  throw new Error(ExecutionOutput.formatToolText("Tool template must be a string, object, or sequence."));
163
234
  }
164
- function buildConfig(name, input, existing) {
235
+ function getRegistrationResolutionContext(ctx, deps) {
236
+ if (ctx &&
237
+ typeof ctx === "object" &&
238
+ "recipeResolutionContext" in ctx) {
239
+ const resolutionContext = ctx.recipeResolutionContext;
240
+ if (resolutionContext)
241
+ return resolutionContext;
242
+ }
243
+ return RecipesContext.createEmptyRecipeResolutionContext("offline-register-tool", getRecipeRoot(deps));
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
+ }
272
+ function buildAuthoredRecipe(input, existing, existingRecipe) {
165
273
  const explicitArgs = input.args === undefined
166
274
  ? undefined
167
275
  : Schema.parseToolArgDeclarations(input.args);
168
276
  if (explicitArgs?.error)
169
277
  throw new Error(ExecutionOutput.formatToolText(explicitArgs.error));
278
+ const from = typeof input.from === "string" ? input.from.trim() : "";
170
279
  const description = (input.description ?? existing?.description ?? "").trim();
171
- if (!description) {
172
- 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;
173
300
  }
174
301
  const template = getInputTemplate(input.template);
175
302
  if (template === null) {
176
303
  throw new Error(ExecutionOutput.formatToolText("Tool template cannot be null here."));
177
304
  }
178
- const finalTemplate = template === undefined || template === "" ? existing?.template : template;
305
+ const finalTemplate = template === undefined || template === ""
306
+ ? existingRecipe?.template ?? existing?.template
307
+ : template;
179
308
  if (!finalTemplate) {
180
309
  throw new Error(ExecutionOutput.formatToolText("Tool template is required."));
181
310
  }
182
- const inputRecipe = typeof input.async === "boolean" ? name : undefined;
183
- const recipe = inputRecipe
184
- ? {
185
- name: inputRecipe,
186
- ...(typeof input.async === "boolean" ? { async: input.async } : {}),
187
- template: finalTemplate,
188
- ...(input.values && typeof input.values === "object"
189
- ? { values: input.values }
190
- : {}),
191
- }
192
- : template === undefined
193
- ? existing?.recipe
194
- : undefined;
195
- const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
196
- const storedArgs = explicitArgs
197
- ? explicitArgs.declarations
198
- : existing?.storedArgs;
199
- const storedDefaults = Object.keys(defaults).length > 0 ? defaults : undefined;
200
- const recipeTemplate = RecipesReferences.getRecipeTemplate(finalTemplate);
201
- const argTemplate = recipeTemplate ?? finalTemplate;
202
- const argTemplateConfig = typeof argTemplate === "object" && !Array.isArray(argTemplate)
203
- ? {
204
- ...argTemplate,
205
- ...(storedArgs !== undefined ? { args: storedArgs } : {}),
206
- defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
207
- }
208
- : {
209
- args: storedArgs,
210
- defaults,
211
- template: argTemplate,
212
- };
213
- const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
214
- const argTypes = {
215
- ...inferredArgTypes,
216
- ...(existing?.argTypes ?? {}),
217
- ...(explicitArgs?.argTypes ?? {}),
218
- };
219
- return {
220
- name,
311
+ const authored = {
312
+ ...(existingRecipe ?? {}),
221
313
  description,
222
314
  template: finalTemplate,
223
- ...(recipe ? { recipe } : {}),
224
- args: RecipesReferences.isRecipeTool(finalTemplate, recipe) &&
225
- storedArgs !== undefined
226
- ? Schema.getExplicitToolArgNames(storedArgs)
227
- : RecipesReferences.isRecipeReference(finalTemplate) && !recipeTemplate
228
- ? Schema.getExplicitToolArgNames(storedArgs)
229
- : Schema.getToolArgNames(argTemplateConfig),
230
- defaults,
231
- ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
232
- ...(storedArgs !== undefined ? { storedArgs } : {}),
233
- ...(storedDefaults !== undefined ? { storedDefaults } : {}),
234
315
  };
316
+ if (typeof input.async === "boolean")
317
+ authored.async = input.async;
318
+ if (explicitArgs) {
319
+ authored.args = explicitArgs.declarations;
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;
329
+ else
330
+ delete authored.defaults;
331
+ }
332
+ return authored;
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
+ }
235
354
  }
236
355
  function executeRegisterToolUnlocked(params, ctx, deps) {
237
- const input = params;
356
+ const rawInput = params && typeof params === "object"
357
+ ? params
358
+ : {};
359
+ assertRegisterToolInputModes(rawInput);
360
+ let input = rawInput;
238
361
  if (!input.name)
239
362
  return listTools(deps);
240
363
  const name = Identity.normalizeToolName(input.name);
@@ -246,6 +369,23 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
246
369
  if (typeof input.draft === "string" && input.draft.trim()) {
247
370
  return promoteDraftRecipe(name, input, ctx, deps);
248
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
+ }
249
389
  const templateProvided = Object.hasOwn(input, "template");
250
390
  const template = getInputTemplate(input.template);
251
391
  if (templateProvided && (template === null || template === ""))
@@ -259,40 +399,93 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
259
399
  if ((authoritative.exists || existing) && !input.update) {
260
400
  throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
261
401
  }
262
- if (template === undefined && !existing) {
263
- 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."));
404
+ }
405
+ const recipePath = getToolRecipePath(deps, name);
406
+ const authoredRecipe = buildAuthoredRecipe(input, existing, authoritative.recipe);
407
+ const candidate = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext, authoredRecipe);
408
+ if (!candidate.validated || !candidate.tool) {
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));
264
413
  }
265
- const cfg = buildConfig(name, input, existing);
266
- let recipePath;
414
+ const activeBefore = deps.getActiveTools();
415
+ let persisted = false;
416
+ let activation;
417
+ let cfg;
267
418
  try {
268
- recipePath = persistToolRecipe(deps, cfg);
419
+ persistToolRecipe(deps, name, authoredRecipe);
420
+ persisted = true;
421
+ const admitted = RecipesDiscovery.admitUserRecipe(recipePath, resolutionContext);
422
+ if (!admitted.validated || !admitted.tool) {
423
+ throw new Error(`Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`);
424
+ }
425
+ cfg = admitted.tool;
426
+ tools.set(name, cfg);
427
+ activation = deps.registerRuntimeTool(cfg) ?? undefined;
428
+ if (activation &&
429
+ (!activation.host_registered ||
430
+ !activation.active_tool ||
431
+ !activation.callable_now)) {
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.`);
433
+ }
269
434
  }
270
435
  catch (error) {
271
- throw new Error(ExecutionOutput.formatToolText(`Failed to persist tool recipe: ${error instanceof Error ? error.message : String(error)}`));
272
- }
273
- cfg.sourcePath = recipePath;
274
- tools.set(name, cfg);
275
- deps.registerRuntimeTool(cfg);
276
- deps.notify(ctx, `Tool persisted: ${name}`, "info");
277
- const templateWarnings = CommandTemplates.getCommandTemplateWarnings(typeof cfg.template === "object" && !Array.isArray(cfg.template)
278
- ? cfg.template
279
- : { template: cfg.template });
436
+ if (persisted) {
437
+ if (authoritative.bytes !== undefined)
438
+ writeTextAtomic(recipePath, authoritative.bytes);
439
+ else if (existsSync(recipePath))
440
+ unlinkSync(recipePath);
441
+ }
442
+ if (existing) {
443
+ tools.set(name, existing);
444
+ deps.registerRuntimeTool(existing);
445
+ }
446
+ else {
447
+ tools.delete(name);
448
+ }
449
+ deps.setActiveTools(activeBefore);
450
+ throw new Error(ExecutionOutput.formatToolText(`Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`));
451
+ }
452
+ deps.notify(ctx, `Tool activated: ${name}`, "info");
453
+ const templateWarnings = CommandTemplates.getCommandTemplateWarnings(typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
454
+ ? cfg.recipe.template
455
+ : { template: cfg.recipe?.template ?? cfg.template });
280
456
  const warningText = templateWarnings.length > 0
281
457
  ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
282
458
  : "";
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)}`;
283
466
  return {
284
467
  content: [
285
- textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" (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}`)),
286
469
  ],
287
470
  details: {
471
+ active_tool: activation?.active_tool ?? false,
472
+ activation: activation?.activation ?? "unverified",
473
+ activation_boundary: activationBoundary(activation),
288
474
  args: cfg.args,
289
- config: recipePath,
475
+ callable_now: callableNow,
290
476
  defaults: cfg.defaults,
477
+ host_registered: activation?.host_registered ?? false,
478
+ next_actions: nextActions,
479
+ optional_args: argSummary.optional,
480
+ persisted: true,
481
+ registry_active: tools.get(name) === cfg,
482
+ required_args: argSummary.required,
483
+ resolved: true,
484
+ source,
291
485
  ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
292
- ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
293
- ...(cfg.template ? { template: cfg.template } : {}),
294
486
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
295
487
  tool: name,
488
+ validated: true,
296
489
  },
297
490
  };
298
491
  }
@@ -6,6 +6,7 @@
6
6
  import { watch } from "node:fs";
7
7
  import * as Config from "./config.ts";
8
8
  import type { RegisteredToolExec } from "./execution.ts";
9
+ import type { RecipeResolutionContext } from "./recipes-context.ts";
9
10
  import * as ToolsLocal from "./tools-local.ts";
10
11
  export interface RuntimeContext {
11
12
  hasUI: boolean;
@@ -13,21 +14,46 @@ export interface RuntimeContext {
13
14
  notify(message: string, type?: "info" | "warning" | "error"): void;
14
15
  };
15
16
  }
17
+ export interface RuntimeToolActivation {
18
+ active_tool: boolean;
19
+ activation: "current_session" | "unverified";
20
+ callable_now: boolean;
21
+ host_registered: boolean;
22
+ }
16
23
  export interface ToolRegistryRuntimeDeps {
17
24
  configPath: string;
18
25
  exec: RegisteredToolExec;
19
26
  recipeRoot?: string;
20
27
  getActiveTools?: () => string[];
28
+ getAllTools?: () => Array<{
29
+ name: string;
30
+ }>;
21
31
  registerTool: (definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>) => void;
22
32
  reservedToolNames: Set<string>;
23
33
  setActiveTools?: (toolNames: string[]) => void;
24
34
  }
35
+ export interface RecipeRegistryStatus {
36
+ active_tool_count: number;
37
+ last_scan_counts: {
38
+ active: number;
39
+ rejected: number;
40
+ scanned: number;
41
+ };
42
+ last_scan_at?: string;
43
+ registry_generation: number;
44
+ resolution_generation?: string;
45
+ watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
46
+ watched_root: string;
47
+ }
25
48
  export interface ToolRegistryRuntime {
49
+ getStatus(): RecipeRegistryStatus;
50
+ getToolStatus(name: string): Record<string, unknown> | undefined;
26
51
  getToolNameBlocker(name: string): string | undefined;
27
52
  getTools(): Map<string, Config.RegisteredTool>;
28
- loadTools(ctx: RuntimeContext): void;
53
+ loadTools(ctx: RuntimeContext, resolutionContext?: RecipeResolutionContext): void;
29
54
  notify(ctx: RuntimeContext, message: string, type: "info" | "warning" | "error"): void;
30
- registerRuntimeTool(cfg: Config.RegisteredTool): void;
55
+ registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
56
+ setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
31
57
  }
32
58
  export interface RecipeToolReloadWatcher {
33
59
  close(): void;
@@ -36,7 +62,8 @@ export interface RecipeToolReloadWatcher {
36
62
  export declare function createAutoToolsRuntime(deps: ToolRegistryRuntimeDeps): ToolRegistryRuntime;
37
63
  export interface RecipeToolReloadWatcherDeps {
38
64
  exists?: (path: string) => boolean;
65
+ getResolutionContext?: () => RecipeResolutionContext | undefined;
39
66
  recipeRoot?: string;
40
67
  watchPath?: typeof watch;
41
68
  }
42
- export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools">, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;
69
+ export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"> & Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;