@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
package/lib/registry.ts CHANGED
@@ -10,9 +10,15 @@ import { dirname, join, relative, resolve } from "node:path";
10
10
  import * as CommandTemplates from "./command-templates.ts";
11
11
  import * as Config from "./config.ts";
12
12
  import * as ExecutionOutput from "./execution-output.ts";
13
- import { withFileMutationLock, writeJsonAtomic } from "./file-state.ts";
13
+ import {
14
+ withFileMutationLock,
15
+ writeJsonAtomic,
16
+ writeTextAtomic,
17
+ } from "./file-state.ts";
14
18
  import * as Identity from "./identity.ts";
15
19
  import * as Paths from "./paths.ts";
20
+ import * as RecipesContext from "./recipes-context.ts";
21
+ import * as RecipesDiscovery from "./recipes-discovery.ts";
16
22
  import * as RecipesReferences from "./recipes-references.ts";
17
23
  import * as Schema from "./schema.ts";
18
24
 
@@ -21,23 +27,33 @@ export interface RegisterToolInput {
21
27
  description?: string;
22
28
  async?: boolean;
23
29
  template?: CommandTemplates.CommandTemplateValue | null;
30
+ from?: string;
31
+ defaults?: Record<string, unknown>;
24
32
  draft?: string;
25
33
  args?: string;
26
34
  update?: boolean;
27
- values?: Record<string, unknown>;
28
35
  }
29
36
 
30
37
  export interface RegisterToolResultDetails {
38
+ active_tool?: boolean;
39
+ activation?: "current_session" | "unverified";
40
+ activation_boundary?: string;
31
41
  args?: string[];
32
42
  async?: boolean;
33
- config?: string;
43
+ callable_now?: boolean;
34
44
  defaults?: Record<string, string>;
35
- draft?: string;
45
+ host_registered?: boolean;
46
+ next_actions?: string[];
47
+ optional_args?: string[];
48
+ persisted?: boolean;
36
49
  promoted?: boolean;
37
- recipeName?: string;
38
- template?: CommandTemplates.CommandTemplateValue;
50
+ registry_active?: boolean;
51
+ required_args?: string[];
52
+ resolved?: boolean;
53
+ source?: string;
39
54
  templateWarnings?: string[];
40
55
  tool: string;
56
+ validated?: boolean;
41
57
  }
42
58
 
43
59
  export interface RegisterToolResult {
@@ -45,6 +61,13 @@ export interface RegisterToolResult {
45
61
  details: RegisterToolResultDetails;
46
62
  }
47
63
 
64
+ interface RuntimeActivation {
65
+ active_tool: boolean;
66
+ activation: "current_session" | "unverified";
67
+ callable_now: boolean;
68
+ host_registered: boolean;
69
+ }
70
+
48
71
  export interface RegisterToolRuntimeDeps<TContext> {
49
72
  configPath: string;
50
73
  recipeRoot?: string;
@@ -56,7 +79,7 @@ export interface RegisterToolRuntimeDeps<TContext> {
56
79
  message: string,
57
80
  type: "info" | "warning" | "error",
58
81
  ) => void;
59
- registerRuntimeTool: (cfg: Config.RegisteredTool) => void;
82
+ registerRuntimeTool: (cfg: Config.RegisteredTool) => RuntimeActivation | void;
60
83
  reservedToolNames: Set<string>;
61
84
  setActiveTools: (toolNames: string[]) => void;
62
85
  }
@@ -96,6 +119,54 @@ function getToolRecipePath<TContext>(
96
119
  return join(getRecipeRoot(deps), `${name}.json`);
97
120
  }
98
121
 
122
+ function activationBoundary(
123
+ activation: RuntimeActivation | undefined,
124
+ ): string {
125
+ if (activation?.callable_now) return "current_session";
126
+ if (!activation) return "session_activation_unverified";
127
+ if (!activation.host_registered) return "host_registration";
128
+ if (!activation.active_tool) return "active_tool_set";
129
+ return "callability_verification";
130
+ }
131
+
132
+ function recipeDoctorAction(source: string): string {
133
+ return /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(source)
134
+ ? `inspect target=recipes view=doctor identity=${source}`
135
+ : "inspect target=recipes view=doctor";
136
+ }
137
+
138
+ function registrationNextActions(
139
+ tool: string,
140
+ source: string,
141
+ callableNow: boolean,
142
+ ): string[] {
143
+ if (callableNow) {
144
+ return [
145
+ `call tool ${tool}`,
146
+ `inspect target=tool:${tool} view=status`,
147
+ ];
148
+ }
149
+ return [
150
+ `inspect target=tool:${tool} view=status`,
151
+ ...(source === "template" || source === "draft"
152
+ ? []
153
+ : [recipeDoctorAction(source)]),
154
+ ];
155
+ }
156
+
157
+ function activeSkillSummary(
158
+ resolutionContext: RecipesContext.RecipeResolutionContext,
159
+ ): string {
160
+ const names = Object.keys(
161
+ RecipesReferences.getActiveSkillRecipeNamespaces(
162
+ resolutionContext.activeSkills,
163
+ ),
164
+ )
165
+ .sort()
166
+ .slice(0, 20);
167
+ return names.length > 0 ? names.join(", ") : "<none>";
168
+ }
169
+
99
170
  function assertDraftPath<TContext>(
100
171
  deps: RegisterToolRuntimeDeps<TContext>,
101
172
  draft: string,
@@ -139,8 +210,8 @@ function promoteDraftRecipe<TContext>(
139
210
  ),
140
211
  );
141
212
  }
142
- const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
143
- if (!config) {
213
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath);
214
+ if (!raw) {
144
215
  const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
145
216
  throw new Error(
146
217
  ExecutionOutput.formatToolText(
@@ -148,55 +219,79 @@ function promoteDraftRecipe<TContext>(
148
219
  ),
149
220
  );
150
221
  }
151
- const promoted = buildConfig(
152
- name,
222
+ const authoredRecipe = buildAuthoredRecipe(
153
223
  {
154
224
  ...input,
155
- description: input.description ?? config.description,
156
- template: draftPath,
225
+ description:
226
+ input.description ??
227
+ (typeof raw.description === "string" ? raw.description : undefined),
157
228
  },
158
229
  existing,
230
+ raw,
231
+ );
232
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
233
+ const candidate = RecipesDiscovery.admitUserRecipe(
234
+ targetPath,
235
+ resolutionContext,
236
+ authoredRecipe,
159
237
  );
160
- const raw = RecipesReferences.readRawRecipeConfig(draftPath)!;
238
+ if (!candidate.validated || !candidate.tool) {
239
+ throw new Error(
240
+ ExecutionOutput.formatToolText(
241
+ `Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`,
242
+ ),
243
+ );
244
+ }
161
245
  mkdirSync(dirname(targetPath), { recursive: true });
162
- writeJsonAtomic(targetPath, raw);
163
- promoted.template = targetPath;
164
- promoted.sourcePath = targetPath;
246
+ writeJsonAtomic(targetPath, authoredRecipe);
247
+ const promoted = RecipesDiscovery.admitUserRecipe(
248
+ targetPath,
249
+ resolutionContext,
250
+ ).tool!;
165
251
  tools.set(name, promoted);
166
- deps.registerRuntimeTool(promoted);
252
+ const activation = deps.registerRuntimeTool(promoted) ?? undefined;
253
+ const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(promoted);
254
+ const callableNow = activation?.callable_now ?? false;
255
+ const nextActions = registrationNextActions(name, "draft", callableNow);
167
256
  deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
168
257
  return {
169
258
  content: [
170
259
  textContent(
171
260
  ExecutionOutput.formatToolText(
172
- `${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`,
261
+ `${existing ? "Updated" : "Registered"} tool "${name}" from draft; callable_now=${callableNow}; next=${nextActions[0]}.`,
173
262
  ),
174
263
  ),
175
264
  ],
176
265
  details: {
266
+ active_tool: activation?.active_tool ?? false,
267
+ activation: activation?.activation ?? "unverified",
268
+ activation_boundary: activationBoundary(activation),
177
269
  args: promoted.args,
178
- config: targetPath,
179
- draft: draftPath,
270
+ callable_now: callableNow,
271
+ defaults: promoted.defaults,
272
+ host_registered: activation?.host_registered ?? false,
273
+ next_actions: nextActions,
274
+ optional_args: argSummary.optional,
275
+ persisted: true,
180
276
  promoted: true,
277
+ registry_active: tools.get(name) === promoted,
278
+ required_args: argSummary.required,
279
+ resolved: true,
280
+ source: "draft",
181
281
  tool: name,
182
- } as RegisterToolResultDetails & Record<string, unknown>,
282
+ validated: true,
283
+ },
183
284
  };
184
285
  }
185
286
 
186
287
  function persistToolRecipe<TContext>(
187
288
  deps: RegisterToolRuntimeDeps<TContext>,
188
- cfg: Config.RegisteredTool,
289
+ name: string,
290
+ recipe: Record<string, unknown>,
189
291
  ): string {
190
- const path = getToolRecipePath(deps, cfg.name);
292
+ const path = getToolRecipePath(deps, name);
191
293
  mkdirSync(dirname(path), { recursive: true });
192
- writeJsonAtomic(path, {
193
- description: cfg.description,
194
- ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
195
- ...(cfg.storedArgs ? { args: cfg.storedArgs } : {}),
196
- ...(cfg.storedDefaults ? { defaults: cfg.storedDefaults } : {}),
197
- ...(cfg.recipe?.values ? { values: cfg.recipe.values } : {}),
198
- template: cfg.template,
199
- });
294
+ writeJsonAtomic(path, recipe);
200
295
  return path;
201
296
  }
202
297
 
@@ -231,25 +326,34 @@ function deleteTool<TContext>(
231
326
  ),
232
327
  ),
233
328
  ],
234
- details: { config: recipePath, tool: name },
329
+ details: { tool: name },
235
330
  };
236
331
  }
237
332
 
238
333
  function readAuthoritativeStoredTool<TContext>(
239
334
  deps: RegisterToolRuntimeDeps<TContext>,
240
335
  name: string,
241
- ): { exists: boolean; tool?: Config.RegisteredTool } {
336
+ ): {
337
+ bytes?: string;
338
+ exists: boolean;
339
+ recipe?: Record<string, unknown>;
340
+ tool?: Config.RegisteredTool;
341
+ } {
242
342
  const path = getToolRecipePath(deps, name);
243
343
  if (!existsSync(path)) return { exists: false };
244
344
  try {
345
+ const bytes = readFileSync(path, "utf8");
346
+ const recipe = JSON.parse(bytes);
245
347
  const normalized = Config.normalizeStoredTool(
246
348
  name,
247
- JSON.parse(readFileSync(path, "utf8")),
349
+ recipe,
248
350
  deps.reservedToolNames,
249
351
  );
250
- if (!normalized.cfg) return { exists: true };
352
+ if (!normalized.cfg) return { bytes, exists: true, recipe };
251
353
  return {
354
+ bytes,
252
355
  exists: true,
356
+ recipe,
253
357
  tool: { ...normalized.cfg, sourcePath: path },
254
358
  };
255
359
  } catch {
@@ -257,9 +361,38 @@ function readAuthoritativeStoredTool<TContext>(
257
361
  }
258
362
  }
259
363
 
364
+ function looksLikeRecipeReference(value: string): boolean {
365
+ const trimmed = value.trim();
366
+ return (
367
+ trimmed.endsWith(".json") ||
368
+ trimmed.endsWith(".md") ||
369
+ /^[^/:\\]+\/[^/:\\]+$/.test(trimmed)
370
+ );
371
+ }
372
+
373
+ function assertTemplateIsNotNestedRecipe(value: unknown): void {
374
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
375
+ const record = value as Record<string, unknown>;
376
+ if (
377
+ typeof record.template === "string" &&
378
+ looksLikeRecipeReference(record.template) &&
379
+ (Object.keys(record).length === 1 ||
380
+ ["artifacts", "async", "control", "description", "imports", "values"].some(
381
+ (key) => Object.hasOwn(record, key),
382
+ ))
383
+ ) {
384
+ throw new Error(
385
+ ExecutionOutput.formatToolText(
386
+ "Nested Recipe-shaped templates are not a register_tool source mode. Next: retry with from=<skill>/<recipe> and defaults={...}.",
387
+ ),
388
+ );
389
+ }
390
+ }
391
+
260
392
  function getInputTemplate(
261
393
  value: CommandTemplates.CommandTemplateValue | null | undefined,
262
394
  ): CommandTemplates.CommandTemplateValue | null | undefined {
395
+ assertTemplateIsNotNestedRecipe(value);
263
396
  if (typeof value === "string") return value.trim();
264
397
  if (value === null || value === undefined) return value;
265
398
  if (Array.isArray(value)) {
@@ -285,25 +418,120 @@ function getInputTemplate(
285
418
  );
286
419
  }
287
420
 
288
- function buildConfig(
289
- name: string,
421
+ function getRegistrationResolutionContext<TContext>(
422
+ ctx: TContext,
423
+ deps: RegisterToolRuntimeDeps<TContext>,
424
+ ): RecipesContext.RecipeResolutionContext {
425
+ if (
426
+ ctx &&
427
+ typeof ctx === "object" &&
428
+ "recipeResolutionContext" in ctx
429
+ ) {
430
+ const resolutionContext = (ctx as {
431
+ recipeResolutionContext?: RecipesContext.RecipeResolutionContext;
432
+ }).recipeResolutionContext;
433
+ if (resolutionContext) return resolutionContext;
434
+ }
435
+ return RecipesContext.createEmptyRecipeResolutionContext(
436
+ "offline-register-tool",
437
+ getRecipeRoot(deps),
438
+ );
439
+ }
440
+
441
+ function normalizeDefaults(value: unknown): Record<string, unknown> | undefined {
442
+ if (value === undefined) return undefined;
443
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
444
+ throw new Error(
445
+ ExecutionOutput.formatToolText("Tool defaults must be an object."),
446
+ );
447
+ }
448
+ return { ...(value as Record<string, unknown>) };
449
+ }
450
+
451
+ function normalizeFromReference(
452
+ value: string,
453
+ resolutionContext: RecipesContext.RecipeResolutionContext,
454
+ ): string {
455
+ const trimmed = value.trim();
456
+ if (!trimmed) {
457
+ throw new Error(
458
+ ExecutionOutput.formatToolText(
459
+ "Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path.",
460
+ ),
461
+ );
462
+ }
463
+ if (trimmed.startsWith("std:")) {
464
+ throw new Error(
465
+ ExecutionOutput.formatToolText(
466
+ "std: Recipe references were removed; use <skill>/<recipe> or an explicit .json/.md file path.",
467
+ ),
468
+ );
469
+ }
470
+ if (trimmed.startsWith("skill:")) {
471
+ throw new Error(
472
+ ExecutionOutput.formatToolText(
473
+ `skill: Recipe references were removed; use ${trimmed.slice("skill:".length)} without a prefix.`,
474
+ ),
475
+ );
476
+ }
477
+ if (trimmed.endsWith(".json") || trimmed.endsWith(".md")) {
478
+ return resolve(resolutionContext.cwd, trimmed);
479
+ }
480
+ if (!looksLikeRecipeReference(trimmed)) {
481
+ throw new Error(
482
+ ExecutionOutput.formatToolText(
483
+ "Tool from must use <skill>/<recipe> or an explicit .json/.md file path.",
484
+ ),
485
+ );
486
+ }
487
+ return trimmed;
488
+ }
489
+
490
+ function buildAuthoredRecipe(
290
491
  input: RegisterToolInput,
291
492
  existing: Config.RegisteredTool | undefined,
292
- ): Config.RegisteredTool {
493
+ existingRecipe: Record<string, unknown> | undefined,
494
+ ): Record<string, unknown> {
293
495
  const explicitArgs =
294
496
  input.args === undefined
295
497
  ? undefined
296
498
  : Schema.parseToolArgDeclarations(input.args);
297
499
  if (explicitArgs?.error)
298
500
  throw new Error(ExecutionOutput.formatToolText(explicitArgs.error));
501
+ const from = typeof input.from === "string" ? input.from.trim() : "";
299
502
  const description = (input.description ?? existing?.description ?? "").trim();
300
- if (!description) {
503
+ if (!description && !from) {
301
504
  throw new Error(
302
505
  ExecutionOutput.formatToolText(
303
- "Tool description is required unless deleting.",
506
+ "Tool description is required for command-template registration.",
304
507
  ),
305
508
  );
306
509
  }
510
+ const inputDefaults = normalizeDefaults(input.defaults);
511
+ if (from) {
512
+ const runtimeOwnedDefault = Object.keys(inputDefaults ?? {}).find((key) =>
513
+ RecipesReferences.isRuntimeOwnedRecipeInput(key),
514
+ );
515
+ if (runtimeOwnedDefault) {
516
+ throw new Error(
517
+ ExecutionOutput.formatToolText(
518
+ `Tool defaults cannot set runtime-owned input: ${runtimeOwnedDefault}. Next: remove that default and retry register_tool; the runtime supplies it.`,
519
+ ),
520
+ );
521
+ }
522
+ const authored: Record<string, unknown> = {
523
+ ...(description ? { description } : {}),
524
+ template: from,
525
+ };
526
+ const retainedDefaults =
527
+ inputDefaults ??
528
+ (existingRecipe?.defaults && typeof existingRecipe.defaults === "object"
529
+ ? (existingRecipe.defaults as Record<string, unknown>)
530
+ : undefined);
531
+ if (retainedDefaults && Object.keys(retainedDefaults).length > 0)
532
+ authored.defaults = retainedDefaults;
533
+ return authored;
534
+ }
307
535
  const template = getInputTemplate(input.template);
308
536
  if (template === null) {
309
537
  throw new Error(
@@ -311,68 +539,74 @@ function buildConfig(
311
539
  );
312
540
  }
313
541
  const finalTemplate =
314
- template === undefined || template === "" ? existing?.template : template;
542
+ template === undefined || template === ""
543
+ ? existingRecipe?.template ?? existing?.template
544
+ : template;
315
545
  if (!finalTemplate) {
316
546
  throw new Error(
317
547
  ExecutionOutput.formatToolText("Tool template is required."),
318
548
  );
319
549
  }
320
- const inputRecipe = typeof input.async === "boolean" ? name : undefined;
321
- const recipe: RecipesReferences.TemplateRecipeConfig | undefined = inputRecipe
322
- ? {
323
- name: inputRecipe,
324
- ...(typeof input.async === "boolean" ? { async: input.async } : {}),
325
- template: finalTemplate,
326
- ...(input.values && typeof input.values === "object"
327
- ? { values: input.values }
328
- : {}),
329
- }
330
- : template === undefined
331
- ? existing?.recipe
332
- : undefined;
333
- const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
334
- const storedArgs = explicitArgs
335
- ? explicitArgs.declarations
336
- : existing?.storedArgs;
337
- const storedDefaults =
338
- Object.keys(defaults).length > 0 ? defaults : undefined;
339
- const recipeTemplate = RecipesReferences.getRecipeTemplate(finalTemplate);
340
- const argTemplate = recipeTemplate ?? finalTemplate;
341
- const argTemplateConfig: CommandTemplates.CommandTemplateConfig =
342
- typeof argTemplate === "object" && !Array.isArray(argTemplate)
343
- ? {
344
- ...argTemplate,
345
- ...(storedArgs !== undefined ? { args: storedArgs } : {}),
346
- defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
347
- }
348
- : {
349
- args: storedArgs,
350
- defaults,
351
- template: argTemplate,
352
- };
353
- const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
354
- const argTypes = {
355
- ...inferredArgTypes,
356
- ...(existing?.argTypes ?? {}),
357
- ...(explicitArgs?.argTypes ?? {}),
358
- };
359
- return {
360
- name,
550
+ const authored: Record<string, unknown> = {
551
+ ...(existingRecipe ?? {}),
361
552
  description,
362
553
  template: finalTemplate,
363
- ...(recipe ? { recipe } : {}),
364
- args:
365
- RecipesReferences.isRecipeTool(finalTemplate, recipe) &&
366
- storedArgs !== undefined
367
- ? Schema.getExplicitToolArgNames(storedArgs)
368
- : RecipesReferences.isRecipeReference(finalTemplate) && !recipeTemplate
369
- ? Schema.getExplicitToolArgNames(storedArgs)
370
- : Schema.getToolArgNames(argTemplateConfig),
371
- defaults,
372
- ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
373
- ...(storedArgs !== undefined ? { storedArgs } : {}),
374
- ...(storedDefaults !== undefined ? { storedDefaults } : {}),
375
554
  };
555
+ if (typeof input.async === "boolean") authored.async = input.async;
556
+ if (explicitArgs) {
557
+ authored.args = explicitArgs.declarations;
558
+ const defaults = { ...explicitArgs.defaults, ...(inputDefaults ?? {}) };
559
+ if (Object.keys(defaults).length > 0) authored.defaults = defaults;
560
+ else delete authored.defaults;
561
+ } else if (inputDefaults !== undefined) {
562
+ if (Object.keys(inputDefaults).length > 0) authored.defaults = inputDefaults;
563
+ else delete authored.defaults;
564
+ }
565
+ return authored;
566
+ }
567
+
568
+ function assertRegisterToolInputModes(input: RegisterToolInput): void {
569
+ const record = input as RegisterToolInput & Record<string, unknown>;
570
+ if (Object.hasOwn(record, "values")) {
571
+ throw new Error(
572
+ ExecutionOutput.formatToolText(
573
+ "register_tool values was removed. Next: retry register_tool with defaults for caller inputs, or use from=<skill>/<recipe> for maintained composition.",
574
+ ),
575
+ );
576
+ }
577
+ const fromProvided = Object.hasOwn(record, "from");
578
+ if (
579
+ fromProvided &&
580
+ (typeof input.from !== "string" || input.from.trim().length === 0)
581
+ ) {
582
+ throw new Error(
583
+ ExecutionOutput.formatToolText(
584
+ "Tool from must be a non-empty Recipe reference. Next: retry register_tool with from=<skill>/<recipe> or an explicit .json/.md path.",
585
+ ),
586
+ );
587
+ }
588
+ const draftProvided =
589
+ typeof input.draft === "string" && input.draft.trim().length > 0;
590
+ const templateProvided = Object.hasOwn(record, "template");
591
+ const modeCount =
592
+ Number(fromProvided) + Number(draftProvided) + Number(templateProvided);
593
+ if (modeCount > 1) {
594
+ throw new Error(
595
+ ExecutionOutput.formatToolText(
596
+ "Use exactly one register_tool source mode: from, template, or draft. Next: remove the extra source fields and retry register_tool.",
597
+ ),
598
+ );
599
+ }
600
+ if (
601
+ fromProvided &&
602
+ (Object.hasOwn(record, "args") || Object.hasOwn(record, "async"))
603
+ ) {
604
+ throw new Error(
605
+ ExecutionOutput.formatToolText(
606
+ "register_tool from inherits args and async behavior. Next: retry register_tool with from and optional defaults only; omit args and async.",
607
+ ),
608
+ );
609
+ }
376
610
  }
377
611
 
378
612
  function executeRegisterToolUnlocked<TContext>(
@@ -380,7 +614,12 @@ function executeRegisterToolUnlocked<TContext>(
380
614
  ctx: TContext,
381
615
  deps: RegisterToolRuntimeDeps<TContext>,
382
616
  ): RegisterToolResult {
383
- const input = params as RegisterToolInput;
617
+ const rawInput =
618
+ params && typeof params === "object"
619
+ ? (params as RegisterToolInput)
620
+ : ({} as RegisterToolInput);
621
+ assertRegisterToolInputModes(rawInput);
622
+ let input = rawInput;
384
623
  if (!input.name) return listTools(deps);
385
624
  const name = Identity.normalizeToolName(input.name);
386
625
  if (!name)
@@ -393,6 +632,27 @@ function executeRegisterToolUnlocked<TContext>(
393
632
  if (typeof input.draft === "string" && input.draft.trim()) {
394
633
  return promoteDraftRecipe(name, input, ctx, deps);
395
634
  }
635
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
636
+ const requestedSource =
637
+ typeof rawInput.from === "string" ? rawInput.from.trim() : undefined;
638
+ if (typeof input.from === "string") {
639
+ try {
640
+ input = {
641
+ ...input,
642
+ from: normalizeFromReference(input.from, resolutionContext),
643
+ };
644
+ } catch (error) {
645
+ const reason = error instanceof Error ? error.message.trim() : String(error);
646
+ const diagnosticSource = requestedSource?.startsWith("skill:")
647
+ ? requestedSource.slice("skill:".length)
648
+ : requestedSource ?? "";
649
+ throw new Error(
650
+ ExecutionOutput.formatToolText(
651
+ `Recipe source "${requestedSource}" is invalid: ${reason}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(diagnosticSource)}`,
652
+ ),
653
+ );
654
+ }
655
+ }
396
656
  const templateProvided = Object.hasOwn(input, "template");
397
657
  const template = getInputTemplate(input.template);
398
658
  if (templateProvided && (template === null || template === ""))
@@ -409,54 +669,122 @@ function executeRegisterToolUnlocked<TContext>(
409
669
  ),
410
670
  );
411
671
  }
412
- if (template === undefined && !existing) {
672
+ if (template === undefined && !input.from && !existing) {
413
673
  throw new Error(
414
674
  ExecutionOutput.formatToolText(
415
- "Tool template is required for new registrations.",
675
+ "New registration requires exactly one source mode: from, template, or draft. Next: retry register_tool with one source field.",
416
676
  ),
417
677
  );
418
678
  }
419
- const cfg = buildConfig(name, input, existing);
420
- let recipePath: string;
679
+ const recipePath = getToolRecipePath(deps, name);
680
+ const authoredRecipe = buildAuthoredRecipe(
681
+ input,
682
+ existing,
683
+ authoritative.recipe,
684
+ );
685
+ const candidate = RecipesDiscovery.admitUserRecipe(
686
+ recipePath,
687
+ resolutionContext,
688
+ authoredRecipe,
689
+ );
690
+ if (!candidate.validated || !candidate.tool) {
691
+ const sourceDiagnostic = requestedSource
692
+ ? `Recipe source "${requestedSource}" validation failed: ${candidate.diagnostics.join("; ")}. Active Skills: ${activeSkillSummary(resolutionContext)}. Next: ${recipeDoctorAction(requestedSource)}`
693
+ : `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`;
694
+ throw new Error(ExecutionOutput.formatToolText(sourceDiagnostic));
695
+ }
696
+ const activeBefore = deps.getActiveTools();
697
+ let persisted = false;
698
+ let activation: RuntimeActivation | undefined;
699
+ let cfg: Config.RegisteredTool;
421
700
  try {
422
- recipePath = persistToolRecipe(deps, cfg);
701
+ persistToolRecipe(deps, name, authoredRecipe);
702
+ persisted = true;
703
+ const admitted = RecipesDiscovery.admitUserRecipe(
704
+ recipePath,
705
+ resolutionContext,
706
+ );
707
+ if (!admitted.validated || !admitted.tool) {
708
+ throw new Error(
709
+ `Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`,
710
+ );
711
+ }
712
+ cfg = admitted.tool;
713
+ tools.set(name, cfg);
714
+ activation = deps.registerRuntimeTool(cfg) ?? undefined;
715
+ if (
716
+ activation &&
717
+ (!activation.host_registered ||
718
+ !activation.active_tool ||
719
+ !activation.callable_now)
720
+ ) {
721
+ throw new Error(
722
+ `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.`,
723
+ );
724
+ }
423
725
  } catch (error) {
726
+ if (persisted) {
727
+ if (authoritative.bytes !== undefined)
728
+ writeTextAtomic(recipePath, authoritative.bytes);
729
+ else if (existsSync(recipePath)) unlinkSync(recipePath);
730
+ }
731
+ if (existing) {
732
+ tools.set(name, existing);
733
+ deps.registerRuntimeTool(existing);
734
+ } else {
735
+ tools.delete(name);
736
+ }
737
+ deps.setActiveTools(activeBefore);
424
738
  throw new Error(
425
739
  ExecutionOutput.formatToolText(
426
- `Failed to persist tool recipe: ${error instanceof Error ? error.message : String(error)}`,
740
+ `Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`,
427
741
  ),
428
742
  );
429
743
  }
430
- cfg.sourcePath = recipePath;
431
- tools.set(name, cfg);
432
- deps.registerRuntimeTool(cfg);
433
- deps.notify(ctx, `Tool persisted: ${name}`, "info");
744
+ deps.notify(ctx, `Tool activated: ${name}`, "info");
434
745
  const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
435
- typeof cfg.template === "object" && !Array.isArray(cfg.template)
436
- ? cfg.template
437
- : { template: cfg.template! },
746
+ typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
747
+ ? cfg.recipe.template
748
+ : { template: cfg.recipe?.template ?? cfg.template! },
438
749
  );
439
750
  const warningText =
440
751
  templateWarnings.length > 0
441
752
  ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
442
753
  : "";
754
+ const callableNow = activation?.callable_now ?? false;
755
+ const source = requestedSource ?? "template";
756
+ const argSummary = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
757
+ const nextActions = registrationNextActions(name, source, callableNow);
758
+ const outcomeText = callableNow
759
+ ? `${existing ? "Updated" : "Registered"} tool "${name}" from ${source}; callable_now=true`
760
+ : `Persisted tool "${name}" from ${source}; callable_now=false; activation_boundary=${activationBoundary(activation)}`;
443
761
  return {
444
762
  content: [
445
763
  textContent(
446
764
  ExecutionOutput.formatToolText(
447
- `${existing ? "Updated" : "Registered"} tool "${name}" (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
765
+ `${outcomeText}; required=${Schema.formatToolArgs(argSummary.required)}; optional=${Schema.formatToolArgs(argSummary.optional)}; next=${nextActions[0]}.${warningText}`,
448
766
  ),
449
767
  ),
450
768
  ],
451
769
  details: {
770
+ active_tool: activation?.active_tool ?? false,
771
+ activation: activation?.activation ?? "unverified",
772
+ activation_boundary: activationBoundary(activation),
452
773
  args: cfg.args,
453
- config: recipePath,
774
+ callable_now: callableNow,
454
775
  defaults: cfg.defaults,
776
+ host_registered: activation?.host_registered ?? false,
777
+ next_actions: nextActions,
778
+ optional_args: argSummary.optional,
779
+ persisted: true,
780
+ registry_active: tools.get(name) === cfg,
781
+ required_args: argSummary.required,
782
+ resolved: true,
783
+ source,
455
784
  ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
456
- ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
457
- ...(cfg.template ? { template: cfg.template } : {}),
458
785
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
459
786
  tool: name,
787
+ validated: true,
460
788
  },
461
789
  };
462
790
  }