@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/runtime.ts CHANGED
@@ -10,7 +10,10 @@ import { basename, dirname } from "node:path";
10
10
  import * as Config from "./config.ts";
11
11
  import type { RegisteredToolExec } from "./execution.ts";
12
12
  import * as Paths from "./paths.ts";
13
+ import type { RecipeResolutionContext } from "./recipes-context.ts";
13
14
  import * as RecipesDiscovery from "./recipes-discovery.ts";
15
+ import * as RecipesReferences from "./recipes-references.ts";
16
+ import * as RecipesUsage from "./recipes-usage.ts";
14
17
  import * as ToolsLocal from "./tools-local.ts";
15
18
 
16
19
  export interface RuntimeContext {
@@ -20,11 +23,19 @@ export interface RuntimeContext {
20
23
  };
21
24
  }
22
25
 
26
+ export interface RuntimeToolActivation {
27
+ active_tool: boolean;
28
+ activation: "current_session" | "unverified";
29
+ callable_now: boolean;
30
+ host_registered: boolean;
31
+ }
32
+
23
33
  export interface ToolRegistryRuntimeDeps {
24
34
  configPath: string;
25
35
  exec: RegisteredToolExec;
26
36
  recipeRoot?: string;
27
37
  getActiveTools?: () => string[];
38
+ getAllTools?: () => Array<{ name: string }>;
28
39
  registerTool: (
29
40
  definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
30
41
  ) => void;
@@ -32,16 +43,32 @@ export interface ToolRegistryRuntimeDeps {
32
43
  setActiveTools?: (toolNames: string[]) => void;
33
44
  }
34
45
 
46
+ export interface RecipeRegistryStatus {
47
+ active_tool_count: number;
48
+ last_scan_counts: { active: number; rejected: number; scanned: number };
49
+ last_scan_at?: string;
50
+ registry_generation: number;
51
+ resolution_generation?: string;
52
+ watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
53
+ watched_root: string;
54
+ }
55
+
35
56
  export interface ToolRegistryRuntime {
57
+ getStatus(): RecipeRegistryStatus;
58
+ getToolStatus(name: string): Record<string, unknown> | undefined;
36
59
  getToolNameBlocker(name: string): string | undefined;
37
60
  getTools(): Map<string, Config.RegisteredTool>;
38
- loadTools(ctx: RuntimeContext): void;
61
+ loadTools(
62
+ ctx: RuntimeContext,
63
+ resolutionContext?: RecipeResolutionContext,
64
+ ): void;
39
65
  notify(
40
66
  ctx: RuntimeContext,
41
67
  message: string,
42
68
  type: "info" | "warning" | "error",
43
69
  ): void;
44
- registerRuntimeTool(cfg: Config.RegisteredTool): void;
70
+ registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
71
+ setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
45
72
  }
46
73
 
47
74
  export interface RecipeToolReloadWatcher {
@@ -55,6 +82,14 @@ export function createAutoToolsRuntime(
55
82
  const tools = new Map<string, Config.RegisteredTool>();
56
83
  const runtimeToolFingerprints = new Map<string, string>();
57
84
  const runtimeTools = new Set<string>();
85
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
86
+ const status: RecipeRegistryStatus = {
87
+ active_tool_count: 0,
88
+ last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
89
+ registry_generation: 0,
90
+ watch_status: "closed",
91
+ watched_root: recipeRoot,
92
+ };
58
93
  function notify(
59
94
  ctx: RuntimeContext,
60
95
  message: string,
@@ -77,6 +112,25 @@ export function createAutoToolsRuntime(
77
112
  template: cfg.template,
78
113
  });
79
114
  }
115
+ function registeredToolSource(cfg: Config.RegisteredTool): string {
116
+ const raw = cfg.sourcePath
117
+ ? RecipesReferences.readRawRecipeConfig(cfg.sourcePath)
118
+ : undefined;
119
+ const template = raw?.template;
120
+ if (
121
+ typeof template === "string" &&
122
+ /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(template)
123
+ ) {
124
+ return template;
125
+ }
126
+ if (
127
+ typeof template === "string" &&
128
+ (template.endsWith(".json") || template.endsWith(".md"))
129
+ ) {
130
+ return `<explicit-file:${basename(template)}>`;
131
+ }
132
+ return "template";
133
+ }
80
134
  function deactivateMissingRuntimeTools(activeNames: Set<string>): void {
81
135
  const stale = [...runtimeTools].filter((name) => !activeNames.has(name));
82
136
  if (stale.length === 0) return;
@@ -90,12 +144,30 @@ export function createAutoToolsRuntime(
90
144
  deps.getActiveTools().filter((name) => !staleSet.has(name)),
91
145
  );
92
146
  }
93
- function registerRuntimeTool(cfg: Config.RegisteredTool) {
147
+ function activationFor(name: string): RuntimeToolActivation {
148
+ const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
149
+ const activeTool = deps.getActiveTools?.().includes(name) ?? false;
150
+ return {
151
+ active_tool: activeTool,
152
+ activation:
153
+ hostRegistered && activeTool ? "current_session" : "unverified",
154
+ callable_now: hostRegistered && activeTool,
155
+ host_registered: hostRegistered,
156
+ };
157
+ }
158
+ function registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation {
94
159
  const fingerprint = getToolFingerprint(cfg);
95
- if (runtimeToolFingerprints.get(cfg.name) === fingerprint) return;
96
- deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
97
- runtimeTools.add(cfg.name);
98
- runtimeToolFingerprints.set(cfg.name, fingerprint);
160
+ if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
161
+ deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
162
+ runtimeTools.add(cfg.name);
163
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
164
+ }
165
+ if (deps.getActiveTools && deps.setActiveTools) {
166
+ deps.setActiveTools([
167
+ ...new Set([...deps.getActiveTools(), cfg.name]),
168
+ ]);
169
+ }
170
+ return activationFor(cfg.name);
99
171
  }
100
172
  function isStartupActionableRegistryWarning(warning: string): boolean {
101
173
  if (warning.includes(" shadows ")) return false;
@@ -135,13 +207,21 @@ export function createAutoToolsRuntime(
135
207
  lines.push("Other registry diagnostics:");
136
208
  lines.push(...other.map((warning) => `• ${warning}`));
137
209
  }
210
+ lines.push("Next: inspect target=recipes view=doctor");
138
211
  return `${lines.join("\n")}\n`;
139
212
  }
140
- function loadTools(ctx: RuntimeContext) {
213
+ function loadTools(
214
+ ctx: RuntimeContext,
215
+ resolutionContext?: RecipeResolutionContext,
216
+ ) {
141
217
  const warnings: string[] = [];
142
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
143
218
  const discovered = RecipesDiscovery.discoverRecipeSources([
144
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
219
+ {
220
+ root: recipeRoot,
221
+ defaultTool: true,
222
+ mutableUsage: true,
223
+ resolutionContext,
224
+ },
145
225
  ]);
146
226
  warnings.push(...discovered.diagnostics);
147
227
  tools.clear();
@@ -164,28 +244,77 @@ export function createAutoToolsRuntime(
164
244
  }
165
245
  registerRuntimeTool(cfg);
166
246
  }
247
+ status.active_tool_count = tools.size;
248
+ status.last_scan_at = new Date().toISOString();
249
+ status.last_scan_counts = {
250
+ active: tools.size,
251
+ rejected: discovered.entries.filter((entry) => entry.invalid).length,
252
+ scanned: discovered.entries.length,
253
+ };
254
+ status.registry_generation += 1;
255
+ status.resolution_generation = resolutionContext?.generation;
167
256
  const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
168
257
  if (startupWarnings.length > 0) {
169
258
  notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
170
259
  }
171
260
  }
172
261
  return {
262
+ getStatus: () => ({
263
+ ...status,
264
+ last_scan_counts: { ...status.last_scan_counts },
265
+ }),
266
+ getToolStatus: (name) => {
267
+ const cfg = tools.get(name);
268
+ if (!cfg) return undefined;
269
+ const usage = cfg.sourcePath
270
+ ? RecipesUsage.readRecipeUsage(cfg.sourcePath)
271
+ : undefined;
272
+ const activation = activationFor(name);
273
+ const args = RecipesDiscovery.summarizeRegisteredToolArgs(cfg);
274
+ return {
275
+ ...activation,
276
+ activation_boundary: activation.callable_now
277
+ ? "current_session"
278
+ : !activation.host_registered
279
+ ? "host_registration"
280
+ : "active_tool_set",
281
+ persisted: Boolean(cfg.sourcePath),
282
+ registry_active: true,
283
+ source: registeredToolSource(cfg),
284
+ required_args: args.required,
285
+ optional_args: args.optional,
286
+ next_actions: activation.callable_now
287
+ ? [`call tool ${name}`]
288
+ : [
289
+ `register_tool name=${name} update=true`,
290
+ `inspect target=tool:${name} view=status`,
291
+ ],
292
+ ...(usage?.launch_kind ? { launch_kind: usage.launch_kind } : {}),
293
+ spawn_calls: Number(usage?.spawn_calls ?? 0),
294
+ tool_calls: Number(usage?.tool_calls ?? 0),
295
+ };
296
+ },
173
297
  getToolNameBlocker,
174
298
  getTools: () => tools,
175
299
  loadTools,
176
300
  notify,
177
301
  registerRuntimeTool,
302
+ setWatchStatus: (watchStatus) => {
303
+ status.watch_status = watchStatus;
304
+ },
178
305
  };
179
306
  }
180
307
 
181
308
  export interface RecipeToolReloadWatcherDeps {
182
309
  exists?: (path: string) => boolean;
310
+ getResolutionContext?: () => RecipeResolutionContext | undefined;
183
311
  recipeRoot?: string;
184
312
  watchPath?: typeof watch;
185
313
  }
186
314
 
187
315
  export function createRecipeToolReloadWatcher(
188
- runtime: Pick<ToolRegistryRuntime, "loadTools">,
316
+ runtime: Pick<ToolRegistryRuntime, "loadTools"> &
317
+ Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>,
189
318
  deps: RecipeToolReloadWatcherDeps = {},
190
319
  ): RecipeToolReloadWatcher {
191
320
  const pathExists = deps.exists ?? existsSync;
@@ -194,6 +323,8 @@ export function createRecipeToolReloadWatcher(
194
323
  let rootWatcher: FSWatcher | undefined;
195
324
  let parentWatcher: FSWatcher | undefined;
196
325
  let failureNotified = false;
326
+ const setWatchStatus = (watchStatus: RecipeRegistryStatus["watch_status"]): void =>
327
+ runtime.setWatchStatus?.(watchStatus);
197
328
  const close = (): void => {
198
329
  const closingRoot = rootWatcher;
199
330
  rootWatcher = undefined;
@@ -203,10 +334,12 @@ export function createRecipeToolReloadWatcher(
203
334
  closingParent?.close();
204
335
  if (reloadTimeout) clearTimeout(reloadTimeout);
205
336
  reloadTimeout = undefined;
337
+ setWatchStatus("closed");
206
338
  };
207
339
  const notifyFailure = (ctx: RuntimeContext): void => {
208
340
  if (failureNotified) return;
209
341
  failureNotified = true;
342
+ setWatchStatus("failed");
210
343
  ctx.ui.notify(
211
344
  "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
212
345
  "warning",
@@ -216,7 +349,7 @@ export function createRecipeToolReloadWatcher(
216
349
  failureNotified = false;
217
350
  if (reloadTimeout) clearTimeout(reloadTimeout);
218
351
  reloadTimeout = setTimeout(() => {
219
- runtime.loadTools(ctx);
352
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
220
353
  ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
221
354
  }, 150);
222
355
  reloadTimeout.unref?.();
@@ -245,6 +378,7 @@ export function createRecipeToolReloadWatcher(
245
378
  scheduleReload(ctx);
246
379
  });
247
380
  parentWatcher = watcher;
381
+ setWatchStatus("watching_parent");
248
382
  watcher.on("error", () => {
249
383
  if (parentWatcher !== watcher) return;
250
384
  parentWatcher = undefined;
@@ -274,6 +408,7 @@ export function createRecipeToolReloadWatcher(
274
408
  scheduleReload(ctx);
275
409
  });
276
410
  rootWatcher = watcher;
411
+ setWatchStatus("watching_root");
277
412
  watcher.on("error", () => {
278
413
  if (rootWatcher !== watcher) return;
279
414
  rootWatcher = undefined;
@@ -4,8 +4,8 @@
4
4
  * Owns exact inspect target/view dispatch; source projection stays in domain modules.
5
5
  */
6
6
 
7
- import { statSync } from "node:fs";
8
- import { join } from "node:path";
7
+ import { existsSync, statSync } from "node:fs";
8
+ import { isAbsolute, join, relative } from "node:path";
9
9
 
10
10
  import * as AsyncRuns from "./async-runs.ts";
11
11
  import * as ControlProjection from "./control-projection.ts";
@@ -13,12 +13,14 @@ import * as Inspector from "./inspector.ts";
13
13
  import * as Limits from "./limits.ts";
14
14
  import { computeRunControlCapacity } from "./run-evidence-policy.ts";
15
15
  import * as Paths from "./paths.ts";
16
+ import type * as RecipeResolution from "./recipes-context.ts";
16
17
  import * as RecipesDiscovery from "./recipes-discovery.ts";
17
18
  import * as RecipesReferences from "./recipes-references.ts";
18
19
  import * as ReviewDiagnostics from "./review-diagnostics.ts";
19
20
  import * as RunsControls from "./runs-controls.ts";
20
21
  import * as RunControlDelivery from "./runs-control-delivery.ts";
21
22
  import * as RunsTrace from "./runs-trace.ts";
23
+ import * as Runtime from "./runtime.ts";
22
24
  import * as RuntimeIdentity from "./runtime-identity.ts";
23
25
  import * as RuntimeTriage from "./runtime-triage.ts";
24
26
  import * as Schema from "./schema.ts";
@@ -32,8 +34,10 @@ const maybeJsonText = ToolsResponse.maybeJsonText;
32
34
  export interface InspectToolDeps<TContext = unknown> {
33
35
  getRunStatus?: (runOrDir: string) => Record<string, any>;
34
36
  getTool?: (name: string) => any | undefined;
37
+ getToolStatus?: (name: string) => Record<string, unknown> | undefined;
35
38
  listRuns?: () => Array<Record<string, any>>;
36
39
  recipeRoot?: string;
40
+ registryStatus?: () => Runtime.RecipeRegistryStatus;
37
41
  }
38
42
 
39
43
  function runtimeStatus(): Record<string, unknown> {
@@ -191,8 +195,148 @@ function inspectRuntime(
191
195
  throw new Error("inspect runtime supports view=status, view=runs, or view=triage.");
192
196
  }
193
197
 
198
+ function portableSkillRecipePath(
199
+ file: string,
200
+ skill: string,
201
+ namespaces: Record<string, string[]>,
202
+ ): string {
203
+ const root = namespaces[skill]?.[0];
204
+ if (!root) return `<active-skill:${skill}>`;
205
+ const relation = relative(root, file);
206
+ return !relation.startsWith("..") && !isAbsolute(relation)
207
+ ? `<active-skill:${skill}>/${relation.replaceAll("\\", "/")}`
208
+ : `<active-skill:${skill}>`;
209
+ }
210
+
211
+ function portableSkillRecipeDiagnosticReason(
212
+ diagnostic: RecipesReferences.SkillRecipeComponentDiagnostic,
213
+ namespaces: Record<string, string[]>,
214
+ ): string {
215
+ let reason = diagnostic.reason;
216
+ if (diagnostic.file) {
217
+ reason = reason.replaceAll(
218
+ diagnostic.file,
219
+ portableSkillRecipePath(
220
+ diagnostic.file,
221
+ diagnostic.skill,
222
+ namespaces,
223
+ ),
224
+ );
225
+ }
226
+ for (const root of namespaces[diagnostic.skill] ?? []) {
227
+ reason = reason.replaceAll(root, `<active-skill:${diagnostic.skill}>`);
228
+ }
229
+ return reason;
230
+ }
231
+
232
+ function inspectRecipeIdentity(
233
+ identity: string,
234
+ resolutionContext: RecipeResolution.RecipeResolutionContext | undefined,
235
+ namespaces: Record<string, string[]>,
236
+ inventory: RecipesReferences.SkillRecipeComponentInventory,
237
+ registryStatus: Runtime.RecipeRegistryStatus | undefined,
238
+ ): Record<string, unknown> {
239
+ const match = /^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u.exec(identity);
240
+ const skill = match?.[1] ?? "";
241
+ const roots = skill ? namespaces[skill] ?? [] : [];
242
+ const skillActive = roots.length === 1;
243
+ const matchingComponent = inventory.components.find(
244
+ (component) => component.identity === identity,
245
+ );
246
+ const matchingRejection = inventory.rejected.find(
247
+ (diagnostic) =>
248
+ diagnostic.skill === skill &&
249
+ (diagnostic.stem === undefined || diagnostic.stem === match?.[2]),
250
+ );
251
+ let sourceLocation: string | undefined;
252
+ let rejectedReason: string | undefined;
253
+ let resolvable = false;
254
+ if (!match) {
255
+ rejectedReason =
256
+ "Identity must use canonical <skill>/<recipe> syntax.";
257
+ } else if (roots.length === 0) {
258
+ rejectedReason = `Active Skill Recipe not found: ${identity}`;
259
+ } else if (roots.length > 1) {
260
+ rejectedReason = matchingRejection
261
+ ? portableSkillRecipeDiagnosticReason(matchingRejection, namespaces)
262
+ : `Duplicate active Skill identity ${skill}`;
263
+ } else if (matchingRejection) {
264
+ rejectedReason = portableSkillRecipeDiagnosticReason(
265
+ matchingRejection,
266
+ namespaces,
267
+ );
268
+ if (matchingRejection.file) {
269
+ sourceLocation = portableSkillRecipePath(
270
+ matchingRejection.file,
271
+ skill,
272
+ namespaces,
273
+ );
274
+ }
275
+ } else {
276
+ try {
277
+ const file = RecipesReferences.resolveRecipeReferencePath(
278
+ identity,
279
+ resolutionContext?.cwd,
280
+ resolutionContext?.activeSkills,
281
+ );
282
+ if (!file || !existsSync(file)) {
283
+ rejectedReason = `Skill Recipe component not found: ${identity}`;
284
+ } else {
285
+ sourceLocation = portableSkillRecipePath(file, skill, namespaces);
286
+ const recipe = RecipesReferences.readResolvedRecipeConfig(file, [], {
287
+ skillContext: resolutionContext?.activeSkills,
288
+ });
289
+ if (recipe) resolvable = true;
290
+ else {
291
+ rejectedReason =
292
+ RecipesReferences.diagnoseRawRecipeConfigFailure(file) ??
293
+ `Recipe could not be resolved: ${identity}`;
294
+ }
295
+ }
296
+ } catch (error) {
297
+ rejectedReason = error instanceof Error ? error.message : String(error);
298
+ }
299
+ }
300
+ const componentStatus = resolvable
301
+ ? "available"
302
+ : !match
303
+ ? "invalid_identity"
304
+ : roots.length === 0
305
+ ? "skill_inactive"
306
+ : roots.length > 1
307
+ ? "ambiguous_skill"
308
+ : matchingRejection
309
+ ? "rejected"
310
+ : matchingComponent
311
+ ? "unresolvable"
312
+ : "missing";
313
+ return {
314
+ identity,
315
+ skill_active: skillActive,
316
+ resolvable,
317
+ catalog_partial: inventory.partial,
318
+ component_status: componentStatus,
319
+ ...(sourceLocation ? { source_location: sourceLocation } : {}),
320
+ resolution_generation:
321
+ resolutionContext?.generation ?? registryStatus?.resolution_generation ?? "unavailable",
322
+ ...(rejectedReason ? { rejected_reason: rejectedReason } : {}),
323
+ next_actions: resolvable
324
+ ? [
325
+ `spawn recipe=${identity}`,
326
+ `register_tool name=<tool-name> from=${identity}`,
327
+ ]
328
+ : roots.length === 0 && skill
329
+ ? [
330
+ `activate Skill ${skill}`,
331
+ `inspect target=recipes view=doctor identity=${identity}`,
332
+ ]
333
+ : ["inspect target=recipes view=imports verbose=true"],
334
+ };
335
+ }
336
+
194
337
  function inspectRecipes(
195
338
  view: string,
339
+ input: Record<string, unknown>,
196
340
  deps: InspectToolDeps,
197
341
  context: unknown,
198
342
  ): Record<string, unknown> {
@@ -214,37 +358,70 @@ function inspectRecipes(
214
358
  const discovered = RecipesDiscovery.discoverRecipeSources([
215
359
  { root: recipeRoot, defaultTool: true, mutableUsage: true },
216
360
  ]);
217
- const skillContext =
218
- context && typeof context === "object" && "activeSkillRecipeContext" in context
361
+ const resolutionContext =
362
+ context && typeof context === "object" && "recipeResolutionContext" in context
219
363
  ? (context as {
220
- activeSkillRecipeContext?: RecipesReferences.ActiveSkillRecipeContext;
221
- }).activeSkillRecipeContext
364
+ recipeResolutionContext?: RecipeResolution.RecipeResolutionContext;
365
+ }).recipeResolutionContext
222
366
  : undefined;
223
367
  const activeSkillContext =
224
- skillContext ?? RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
368
+ resolutionContext?.activeSkills ??
369
+ RecipesReferences.EMPTY_ACTIVE_SKILL_RECIPE_CONTEXT;
225
370
  const skillRecipeNamespaces = RecipesReferences.getActiveSkillRecipeNamespaces(
226
371
  activeSkillContext,
227
372
  );
228
- let skillRecipeComponents: Array<Record<string, unknown>> = [];
229
- const skillRecipeComponentDiagnostics: Array<Record<string, unknown>> = [];
230
- try {
231
- skillRecipeComponents = RecipesReferences.listActiveSkillRecipeComponents(
232
- activeSkillContext,
233
- ).map((component) => ({
234
- identity: component.identity,
235
- source_kind: "active_skill_component",
236
- skill: component.skill,
237
- stem: component.stem,
238
- imports: component.imports,
239
- }));
240
- } catch (error) {
241
- skillRecipeComponentDiagnostics.push({
242
- error: error instanceof Error ? error.message : String(error),
243
- });
373
+ const skillInventory =
374
+ RecipesReferences.inventoryActiveSkillRecipeComponents(activeSkillContext);
375
+ const registryStatus = deps.registryStatus?.();
376
+ const identity =
377
+ typeof input.identity === "string" ? input.identity.trim() : "";
378
+ if (identity && view !== "doctor") {
379
+ throw new Error("inspect recipes identity is supported only with view=doctor.");
380
+ }
381
+ if (view === "doctor" && identity) {
382
+ return inspectRecipeIdentity(
383
+ identity,
384
+ resolutionContext,
385
+ skillRecipeNamespaces,
386
+ skillInventory,
387
+ registryStatus,
388
+ );
244
389
  }
390
+ const skillRecipeComponents = skillInventory.components.map((component) => ({
391
+ identity: component.identity,
392
+ source_kind: "active_skill_component",
393
+ skill: component.skill,
394
+ stem: component.stem,
395
+ imports: component.imports,
396
+ }));
397
+ const skillRecipeComponentDiagnostics = skillInventory.rejected.map(
398
+ (diagnostic) => ({
399
+ error: portableSkillRecipeDiagnosticReason(
400
+ diagnostic,
401
+ skillRecipeNamespaces,
402
+ ),
403
+ ...(diagnostic.file
404
+ ? {
405
+ file: portableSkillRecipePath(
406
+ diagnostic.file,
407
+ diagnostic.skill,
408
+ skillRecipeNamespaces,
409
+ ),
410
+ }
411
+ : {}),
412
+ skill: diagnostic.skill,
413
+ ...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
414
+ }),
415
+ );
245
416
  const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
246
417
  const summary = {
247
418
  ...discoverySummary,
419
+ ...(registryStatus
420
+ ? {
421
+ ...registryStatus,
422
+ watched_root: "~/.pi/agent/recipes",
423
+ }
424
+ : {}),
248
425
  active: Array.isArray(discoverySummary.active)
249
426
  ? discoverySummary.active.map((entry) => ({
250
427
  ...(entry as Record<string, unknown>),
@@ -253,6 +430,7 @@ function inspectRecipes(
253
430
  : discoverySummary.active,
254
431
  skill_recipe_components: skillRecipeComponents,
255
432
  skill_recipe_component_diagnostics: skillRecipeComponentDiagnostics,
433
+ skill_recipe_catalog_partial: skillInventory.partial,
256
434
  active_skill_recipe_identities: Object.keys(skillRecipeNamespaces).sort(),
257
435
  skill_recipe_namespace_diagnostics: Object.entries(skillRecipeNamespaces)
258
436
  .filter(([, roots]) => roots.length > 1)
@@ -366,18 +544,63 @@ function inspectTool(
366
544
  throw new Error("inspect tool:<name> supports view=status or view=schema.");
367
545
  }
368
546
  const tool = deps.getTool?.(name);
369
- if (!tool) throw new Error(`registered tool not found: ${name}`);
547
+ const status = deps.getToolStatus?.(name);
548
+ if (!tool && !status) {
549
+ throw new Error(
550
+ `Registered tool not found: ${name}. Next: inspect target=recipes view=doctor; do not substitute spawn for tool invocation.`,
551
+ );
552
+ }
553
+ if (view === "schema" && !tool) {
554
+ throw new Error(
555
+ `Tool "${name}" is registered but its callable schema is unavailable; callable_now=${String(status?.callable_now ?? false)}, activation_boundary=${String(status?.activation_boundary ?? "host_registration")}. Next: inspect target=tool:${name} view=status.`,
556
+ );
557
+ }
370
558
  return {
371
559
  name,
372
- description: tool.description,
373
- parameters: tool.parameters,
374
- promptSnippet: tool.promptSnippet,
560
+ ...(view === "status" ? status : {}),
561
+ ...(tool
562
+ ? {
563
+ description: tool.description,
564
+ parameters: tool.parameters,
565
+ promptSnippet: tool.promptSnippet,
566
+ }
567
+ : {}),
375
568
  };
376
569
  }
377
570
 
378
571
  function compactResult(target: string, view: string, details: Record<string, unknown>): string {
379
572
  if (target === "runtime") return compactRuntime(view, details);
573
+ if (target === "recipes" && typeof details.identity === "string") {
574
+ const lines = [
575
+ `recipes doctor identity=${details.identity} skill_active=${String(details.skill_active)} resolvable=${String(details.resolvable)} catalog_partial=${String(details.catalog_partial)} component_status=${String(details.component_status)}`,
576
+ ...(details.source_location
577
+ ? [`source=${String(details.source_location)}`]
578
+ : []),
579
+ `resolution_generation=${String(details.resolution_generation)}`,
580
+ ...(details.rejected_reason
581
+ ? [`rejected_reason=${String(details.rejected_reason)}`]
582
+ : []),
583
+ ...((details.next_actions as unknown[] | undefined) ?? []).map(
584
+ (action) => `next=${String(action)}`,
585
+ ),
586
+ ];
587
+ return `\n${lines.join("\n")}`;
588
+ }
380
589
  if (target === "recipes") return ToolsResponse.compactRecipeRegistry(details);
590
+ if (target.startsWith("tool:") && view === "status") {
591
+ const required = Array.isArray(details.required_args)
592
+ ? details.required_args.map(String).join(",") || "none"
593
+ : "unknown";
594
+ const optional = Array.isArray(details.optional_args)
595
+ ? details.optional_args.map(String).join(",") || "none"
596
+ : "unknown";
597
+ const next = Array.isArray(details.next_actions) && details.next_actions.length > 0
598
+ ? String(details.next_actions[0])
599
+ : details.callable_now === true
600
+ ? `call tool ${target.slice(5)}`
601
+ : `register_tool name=${target.slice(5)} update=true`;
602
+ return `\ntool=${target.slice(5)} source=${String(details.source ?? "unknown")} callable_now=${String(details.callable_now ?? false)} activation_boundary=${String(details.activation_boundary ?? "unknown")} required=${required} optional=${optional} launch_kind=${String(details.launch_kind ?? "none")} spawn_calls=${Number(details.spawn_calls ?? 0)} tool_calls=${Number(details.tool_calls ?? 0)} next=${next}`;
603
+ }
381
604
  if (target.startsWith("tool:")) return `\ntool=${target.slice(5)} view=${view}`;
382
605
  return `\nrun=${target.slice(4)} view=${view}`;
383
606
  }
@@ -392,6 +615,9 @@ export function createInspectToolDefinition<TContext = unknown>(
392
615
  "Inspect a Run's Recipe, Trace, or Control evidence, or runtime/recipe/tool diagnostics.",
393
616
  parameters: Schema.objectSchema(
394
617
  {
618
+ identity: Schema.stringSchema(
619
+ "Optional canonical <skill>/<recipe> identity for focused Recipe doctor diagnosis.",
620
+ ),
395
621
  lines: Schema.stringSchema("Bounded item count for Trace or recent Controls."),
396
622
  source: Schema.stringSchema(
397
623
  "Optional Trace source: all, lifecycle, control, process, agent, artifact, or runtime.",
@@ -419,7 +645,7 @@ export function createInspectToolDefinition<TContext = unknown>(
419
645
  if (target === "runtime") {
420
646
  details = inspectRuntime(view, input, ctx, deps);
421
647
  } else if (target === "recipes") {
422
- details = inspectRecipes(view, deps, ctx);
648
+ details = inspectRecipes(view, input, deps, ctx);
423
649
  } else {
424
650
  const run = parseRunTarget(target);
425
651
  if (run) details = inspectRun(run, view, input, ctx, deps);