@llblab/pi-actors 0.45.1 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +582 -0
  3. package/CHANGELOG.md +17 -1
  4. package/README.md +27 -13
  5. package/dist/fixtures/protocol/recipe-summary.json +1 -1
  6. package/dist/index.js +1 -1
  7. package/dist/lib/async-runs.d.ts +5 -1
  8. package/dist/lib/async-runs.js +26 -24
  9. package/dist/lib/automatic-review-runtime.d.ts +2 -0
  10. package/dist/lib/automatic-review-runtime.js +12 -5
  11. package/dist/lib/draft-sleep.js +0 -1
  12. package/dist/lib/execution.d.ts +1 -0
  13. package/dist/lib/execution.js +1 -0
  14. package/dist/lib/extension-runtime.d.ts +1 -1
  15. package/dist/lib/extension-runtime.js +26 -4
  16. package/dist/lib/inspector.js +46 -7
  17. package/dist/lib/paths.d.ts +0 -1
  18. package/dist/lib/paths.js +0 -7
  19. package/dist/lib/prompts.d.ts +1 -1
  20. package/dist/lib/prompts.js +4 -4
  21. package/dist/lib/recipes-context.d.ts +12 -3
  22. package/dist/lib/recipes-context.js +30 -6
  23. package/dist/lib/recipes-discovery.d.ts +18 -0
  24. package/dist/lib/recipes-discovery.js +95 -22
  25. package/dist/lib/recipes-references.d.ts +42 -9
  26. package/dist/lib/recipes-references.js +248 -135
  27. package/dist/lib/registry.d.ts +14 -1
  28. package/dist/lib/registry.js +110 -87
  29. package/dist/lib/runtime.d.ts +30 -4
  30. package/dist/lib/runtime.js +74 -12
  31. package/dist/lib/tools-inspect.d.ts +3 -1
  32. package/dist/lib/tools-inspect.js +56 -9
  33. package/dist/lib/tools-local.d.ts +2 -0
  34. package/dist/lib/tools-local.js +5 -2
  35. package/dist/lib/tools-response.js +2 -0
  36. package/dist/lib/tools-spawn.d.ts +2 -0
  37. package/dist/lib/tools-spawn.js +24 -52
  38. package/dist/lib/tools.d.ts +4 -1
  39. package/dist/lib/tools.js +2 -0
  40. package/dist/scripts/build-dist.mjs +13 -4
  41. package/dist/skills/actors/SKILL.md +8 -8
  42. package/dist/skills/actors/recipes/recipe-validate.json +11 -0
  43. package/{recipes/utility-coordinator-lock-snapshot.json → dist/skills/actors/recipes/resource-locker-snapshot.json} +1 -3
  44. package/dist/{recipes → skills/actors/recipes}/resource-locker.json +1 -3
  45. package/dist/{recipes/utility-run-ops-snapshot.json → skills/actors/recipes/run-ops-snapshot.json} +1 -3
  46. package/dist/skills/actors/recipes/run-summary.json +9 -0
  47. package/dist/{scripts/locker.mjs → skills/actors/scripts/resource-locker.mjs} +2 -2
  48. package/dist/skills/actors/scripts/run-utils.mjs +164 -0
  49. package/{scripts → dist/skills/actors/scripts}/validate-recipe.mjs +134 -17
  50. package/dist/skills/artifacts/SKILL.md +16 -0
  51. package/{recipes/pipeline-artifact-bundle.json → dist/skills/artifacts/recipes/bundle.json} +4 -4
  52. package/dist/{recipes/utility-artifact-write.json → skills/artifacts/recipes/file-write.json} +1 -3
  53. package/dist/{recipes/utility-artifact-manifest.json → skills/artifacts/recipes/manifest.json} +1 -3
  54. package/{recipes/pipeline-artifact-report.json → dist/skills/artifacts/recipes/report.json} +2 -2
  55. package/dist/{recipes/pipeline-artifact-write.json → skills/artifacts/recipes/write.json} +3 -3
  56. package/dist/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  57. package/dist/skills/media/SKILL.md +16 -0
  58. package/dist/{recipes/pipeline-media-library.json → skills/media/recipes/library.json} +2 -2
  59. package/{recipes/music-player.json → dist/skills/media/recipes/player.json} +1 -3
  60. package/dist/{recipes/utility-playlist-build.json → skills/media/recipes/playlist-build.json} +1 -3
  61. package/dist/skills/media/scripts/media-utils.mjs +47 -0
  62. package/dist/{scripts → skills/media/scripts}/music-player.mjs +9 -3
  63. package/dist/skills/project-work/SKILL.md +16 -0
  64. package/dist/skills/project-work/recipes/changelog-section.json +11 -0
  65. package/{recipes/pipeline-docs-maintenance.json → dist/skills/project-work/recipes/docs-maintenance.json} +4 -4
  66. package/dist/skills/project-work/recipes/package-summary.json +9 -0
  67. package/{recipes/pipeline-release-readiness.json → dist/skills/project-work/recipes/release-readiness.json} +6 -6
  68. package/dist/{recipes/pipeline-release-summary.json → skills/project-work/recipes/release-summary.json} +5 -5
  69. package/{recipes/pipeline-repo-health.json → dist/skills/project-work/recipes/repo-health.json} +6 -6
  70. package/{recipes/pipeline-async-run-ops.json → dist/skills/project-work/recipes/run-ops.json} +3 -3
  71. package/dist/skills/project-work/recipes/skill-summary.json +11 -0
  72. package/dist/skills/project-work/scripts/project-utils.mjs +89 -0
  73. package/dist/skills/recipe-memory/SKILL.md +16 -0
  74. package/dist/skills/swarm/SKILL.md +4 -4
  75. package/{recipes/pipeline-architect-coordinator.json → dist/skills/swarm/recipes/architect.json} +5 -5
  76. package/{recipes/pipeline-checkpoint-continuation.json → dist/skills/swarm/recipes/checkpoint-continuation.json} +3 -3
  77. package/dist/{recipes/pipeline-development-tasking.json → skills/swarm/recipes/development-tasking.json} +4 -4
  78. package/dist/{recipes/lens-swarm.json → skills/swarm/recipes/lens-review.json} +1 -1
  79. package/dist/{recipes/pipeline-quorum-review.json → skills/swarm/recipes/quorum-review.json} +4 -4
  80. package/{recipes/pipeline-research-synthesis.json → dist/skills/swarm/recipes/research-synthesis.json} +6 -6
  81. package/{recipes/pipeline-review-readiness.json → dist/skills/swarm/recipes/review-readiness.json} +1 -1
  82. package/{recipes/subagents-prompts.json → dist/skills/swarm/recipes/subagent-prompts.json} +1 -1
  83. package/{recipes → dist/skills/swarm/recipes}/subagent-quorum.json +1 -1
  84. package/dist/{recipes → skills/swarm/recipes}/subagent-review-coordinator.json +6 -6
  85. package/docs/actor-inspector.md +4 -3
  86. package/docs/async-runs.md +2 -2
  87. package/docs/recipe-library.md +23 -23
  88. package/docs/releasing.md +2 -2
  89. package/docs/template-recipes.md +32 -14
  90. package/docs/tool-registry.md +10 -4
  91. package/fixtures/protocol/recipe-summary.json +1 -1
  92. package/index.ts +2 -1
  93. package/lib/async-runs.ts +52 -25
  94. package/lib/automatic-review-runtime.ts +16 -3
  95. package/lib/draft-sleep.ts +0 -1
  96. package/lib/execution.ts +2 -0
  97. package/lib/extension-runtime.ts +42 -4
  98. package/lib/inspector.ts +57 -11
  99. package/lib/paths.ts +0 -7
  100. package/lib/prompts.ts +4 -4
  101. package/lib/recipes-context.ts +51 -6
  102. package/lib/recipes-discovery.ts +164 -24
  103. package/lib/recipes-references.ts +359 -146
  104. package/lib/registry.ts +185 -94
  105. package/lib/runtime.ts +109 -17
  106. package/lib/tools-inspect.ts +77 -10
  107. package/lib/tools-local.ts +9 -1
  108. package/lib/tools-response.ts +2 -0
  109. package/lib/tools-spawn.ts +38 -74
  110. package/lib/tools.ts +4 -1
  111. package/package.json +2 -4
  112. package/scripts/build-dist.mjs +13 -4
  113. package/skills/actors/SKILL.md +8 -8
  114. package/skills/actors/recipes/recipe-validate.json +11 -0
  115. package/{dist/recipes/utility-coordinator-lock-snapshot.json → skills/actors/recipes/resource-locker-snapshot.json} +1 -3
  116. package/{recipes → skills/actors/recipes}/resource-locker.json +1 -3
  117. package/{recipes/utility-run-ops-snapshot.json → skills/actors/recipes/run-ops-snapshot.json} +1 -3
  118. package/skills/actors/recipes/run-summary.json +9 -0
  119. package/{scripts/locker.mjs → skills/actors/scripts/resource-locker.mjs} +2 -2
  120. package/skills/actors/scripts/run-utils.mjs +164 -0
  121. package/{dist → skills/actors}/scripts/validate-recipe.mjs +134 -17
  122. package/skills/artifacts/SKILL.md +16 -0
  123. package/{dist/recipes/pipeline-artifact-bundle.json → skills/artifacts/recipes/bundle.json} +4 -4
  124. package/{recipes/utility-artifact-write.json → skills/artifacts/recipes/file-write.json} +1 -3
  125. package/{recipes/utility-artifact-manifest.json → skills/artifacts/recipes/manifest.json} +1 -3
  126. package/{dist/recipes/pipeline-artifact-report.json → skills/artifacts/recipes/report.json} +2 -2
  127. package/{recipes/pipeline-artifact-write.json → skills/artifacts/recipes/write.json} +3 -3
  128. package/skills/artifacts/scripts/artifact-utils.mjs +63 -0
  129. package/skills/media/SKILL.md +16 -0
  130. package/{recipes/pipeline-media-library.json → skills/media/recipes/library.json} +2 -2
  131. package/{dist/recipes/music-player.json → skills/media/recipes/player.json} +1 -3
  132. package/{recipes/utility-playlist-build.json → skills/media/recipes/playlist-build.json} +1 -3
  133. package/skills/media/scripts/media-utils.mjs +47 -0
  134. package/{scripts → skills/media/scripts}/music-player.mjs +9 -3
  135. package/skills/project-work/SKILL.md +16 -0
  136. package/skills/project-work/recipes/changelog-section.json +11 -0
  137. package/{dist/recipes/pipeline-docs-maintenance.json → skills/project-work/recipes/docs-maintenance.json} +4 -4
  138. package/skills/project-work/recipes/package-summary.json +9 -0
  139. package/{dist/recipes/pipeline-release-readiness.json → skills/project-work/recipes/release-readiness.json} +6 -6
  140. package/{recipes/pipeline-release-summary.json → skills/project-work/recipes/release-summary.json} +5 -5
  141. package/{dist/recipes/pipeline-repo-health.json → skills/project-work/recipes/repo-health.json} +6 -6
  142. package/{dist/recipes/pipeline-async-run-ops.json → skills/project-work/recipes/run-ops.json} +3 -3
  143. package/skills/project-work/recipes/skill-summary.json +11 -0
  144. package/skills/project-work/scripts/project-utils.mjs +89 -0
  145. package/skills/recipe-memory/SKILL.md +16 -0
  146. package/skills/swarm/SKILL.md +4 -4
  147. package/{dist/recipes/pipeline-architect-coordinator.json → skills/swarm/recipes/architect.json} +5 -5
  148. package/{dist/recipes/pipeline-checkpoint-continuation.json → skills/swarm/recipes/checkpoint-continuation.json} +3 -3
  149. package/{recipes/pipeline-development-tasking.json → skills/swarm/recipes/development-tasking.json} +4 -4
  150. package/{recipes/lens-swarm.json → skills/swarm/recipes/lens-review.json} +1 -1
  151. package/{recipes/pipeline-quorum-review.json → skills/swarm/recipes/quorum-review.json} +4 -4
  152. package/{dist/recipes/pipeline-research-synthesis.json → skills/swarm/recipes/research-synthesis.json} +6 -6
  153. package/{dist/recipes/pipeline-review-readiness.json → skills/swarm/recipes/review-readiness.json} +1 -1
  154. package/{dist/recipes/subagents-prompts.json → skills/swarm/recipes/subagent-prompts.json} +1 -1
  155. package/{dist → skills/swarm}/recipes/subagent-quorum.json +1 -1
  156. package/{recipes → skills/swarm/recipes}/subagent-review-coordinator.json +6 -6
  157. package/dist/recipes/utility-changelog-section.json +0 -13
  158. package/dist/recipes/utility-package-summary.json +0 -11
  159. package/dist/recipes/utility-run-summary.json +0 -11
  160. package/dist/recipes/utility-skill-summary.json +0 -13
  161. package/dist/recipes/utility-validate-recipe.json +0 -13
  162. package/dist/scripts/executable-block-style.d.mts +0 -1
  163. package/dist/scripts/executable-block-style.mjs +0 -73
  164. package/dist/scripts/recipe-utils.mjs +0 -404
  165. package/dist/scripts/release-gates.mjs +0 -281
  166. package/recipes/utility-changelog-section.json +0 -13
  167. package/recipes/utility-package-summary.json +0 -11
  168. package/recipes/utility-run-summary.json +0 -11
  169. package/recipes/utility-skill-summary.json +0 -13
  170. package/recipes/utility-validate-recipe.json +0 -13
  171. package/scripts/executable-block-style.d.mts +0 -1
  172. package/scripts/executable-block-style.mjs +0 -73
  173. package/scripts/recipe-utils.mjs +0 -404
  174. package/scripts/release-gates.mjs +0 -281
  175. /package/dist/{recipes/utility-validation-wrapper.json → skills/actors/recipes/command-validate.json} +0 -0
  176. /package/dist/{recipes/utility-jsonl-tail.json → skills/actors/recipes/jsonl-tail.json} +0 -0
  177. /package/dist/{recipes/utility-run-state-files.json → skills/actors/recipes/run-state-files.json} +0 -0
  178. /package/dist/{recipes/utility-playlist-scan.json → skills/media/recipes/playlist-scan.json} +0 -0
  179. /package/dist/{recipes/utility-changelog-head.json → skills/project-work/recipes/changelog-head.json} +0 -0
  180. /package/dist/{recipes/utility-git-log.json → skills/project-work/recipes/git-log.json} +0 -0
  181. /package/dist/{recipes/utility-git-status.json → skills/project-work/recipes/git-status.json} +0 -0
  182. /package/dist/{recipes/utility-markdown-index.json → skills/project-work/recipes/markdown-index.json} +0 -0
  183. /package/dist/{recipes → skills/recipe-memory/recipes}/draft-review.json +0 -0
  184. /package/dist/{recipes → skills/recipe-memory/recipes}/tool-review.json +0 -0
  185. /package/dist/{recipes → skills/swarm/recipes}/subagent-artifact.json +0 -0
  186. /package/dist/{recipes → skills/swarm/recipes}/subagent-checkpoint.json +0 -0
  187. /package/dist/{recipes → skills/swarm/recipes}/subagent-conflict-report.json +0 -0
  188. /package/dist/{recipes → skills/swarm/recipes}/subagent-contradiction-map.json +0 -0
  189. /package/dist/{recipes → skills/swarm/recipes}/subagent-critic.json +0 -0
  190. /package/dist/{recipes → skills/swarm/recipes}/subagent-evidence-map.json +0 -0
  191. /package/dist/{recipes → skills/swarm/recipes}/subagent-followup.json +0 -0
  192. /package/dist/{recipes → skills/swarm/recipes}/subagent-judge.json +0 -0
  193. /package/dist/{recipes → skills/swarm/recipes}/subagent-merge.json +0 -0
  194. /package/dist/{recipes → skills/swarm/recipes}/subagent-normalize.json +0 -0
  195. /package/dist/{recipes → skills/swarm/recipes}/subagent-plan.json +0 -0
  196. /package/dist/{recipes → skills/swarm/recipes}/subagent-preflight.json +0 -0
  197. /package/dist/{recipes → skills/swarm/recipes}/subagent-prompt.json +0 -0
  198. /package/dist/{recipes → skills/swarm/recipes}/subagent-review.json +0 -0
  199. /package/dist/{recipes → skills/swarm/recipes}/subagent-task-card.json +0 -0
  200. /package/dist/{recipes → skills/swarm/recipes}/subagent-tools.json +0 -0
  201. /package/dist/{recipes → skills/swarm/recipes}/subagent-verify.json +0 -0
  202. /package/{recipes/utility-validation-wrapper.json → skills/actors/recipes/command-validate.json} +0 -0
  203. /package/{recipes/utility-jsonl-tail.json → skills/actors/recipes/jsonl-tail.json} +0 -0
  204. /package/{recipes/utility-run-state-files.json → skills/actors/recipes/run-state-files.json} +0 -0
  205. /package/{recipes/utility-playlist-scan.json → skills/media/recipes/playlist-scan.json} +0 -0
  206. /package/{recipes/utility-changelog-head.json → skills/project-work/recipes/changelog-head.json} +0 -0
  207. /package/{recipes/utility-git-log.json → skills/project-work/recipes/git-log.json} +0 -0
  208. /package/{recipes/utility-git-status.json → skills/project-work/recipes/git-status.json} +0 -0
  209. /package/{recipes/utility-markdown-index.json → skills/project-work/recipes/markdown-index.json} +0 -0
  210. /package/{recipes → skills/recipe-memory/recipes}/draft-review.json +0 -0
  211. /package/{recipes → skills/recipe-memory/recipes}/tool-review.json +0 -0
  212. /package/{recipes → skills/swarm/recipes}/subagent-artifact.json +0 -0
  213. /package/{recipes → skills/swarm/recipes}/subagent-checkpoint.json +0 -0
  214. /package/{recipes → skills/swarm/recipes}/subagent-conflict-report.json +0 -0
  215. /package/{recipes → skills/swarm/recipes}/subagent-contradiction-map.json +0 -0
  216. /package/{recipes → skills/swarm/recipes}/subagent-critic.json +0 -0
  217. /package/{recipes → skills/swarm/recipes}/subagent-evidence-map.json +0 -0
  218. /package/{recipes → skills/swarm/recipes}/subagent-followup.json +0 -0
  219. /package/{recipes → skills/swarm/recipes}/subagent-judge.json +0 -0
  220. /package/{recipes → skills/swarm/recipes}/subagent-merge.json +0 -0
  221. /package/{recipes → skills/swarm/recipes}/subagent-normalize.json +0 -0
  222. /package/{recipes → skills/swarm/recipes}/subagent-plan.json +0 -0
  223. /package/{recipes → skills/swarm/recipes}/subagent-preflight.json +0 -0
  224. /package/{recipes → skills/swarm/recipes}/subagent-prompt.json +0 -0
  225. /package/{recipes → skills/swarm/recipes}/subagent-review.json +0 -0
  226. /package/{recipes → skills/swarm/recipes}/subagent-task-card.json +0 -0
  227. /package/{recipes → skills/swarm/recipes}/subagent-tools.json +0 -0
  228. /package/{recipes → skills/swarm/recipes}/subagent-verify.json +0 -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
 
@@ -28,16 +34,24 @@ export interface RegisterToolInput {
28
34
  }
29
35
 
30
36
  export interface RegisterToolResultDetails {
37
+ active_tool?: boolean;
38
+ activation?: "current_session" | "unverified";
31
39
  args?: string[];
32
40
  async?: boolean;
41
+ callable_now?: boolean;
33
42
  config?: string;
34
43
  defaults?: Record<string, string>;
35
44
  draft?: string;
45
+ host_registered?: boolean;
46
+ persisted?: boolean;
36
47
  promoted?: boolean;
48
+ registry_active?: boolean;
49
+ resolved?: boolean;
37
50
  recipeName?: string;
38
51
  template?: CommandTemplates.CommandTemplateValue;
39
52
  templateWarnings?: string[];
40
53
  tool: string;
54
+ validated?: boolean;
41
55
  }
42
56
 
43
57
  export interface RegisterToolResult {
@@ -56,7 +70,14 @@ export interface RegisterToolRuntimeDeps<TContext> {
56
70
  message: string,
57
71
  type: "info" | "warning" | "error",
58
72
  ) => void;
59
- registerRuntimeTool: (cfg: Config.RegisteredTool) => void;
73
+ registerRuntimeTool: (cfg: Config.RegisteredTool) =>
74
+ | {
75
+ active_tool: boolean;
76
+ activation: "current_session" | "unverified";
77
+ callable_now: boolean;
78
+ host_registered: boolean;
79
+ }
80
+ | void;
60
81
  reservedToolNames: Set<string>;
61
82
  setActiveTools: (toolNames: string[]) => void;
62
83
  }
@@ -139,8 +160,8 @@ function promoteDraftRecipe<TContext>(
139
160
  ),
140
161
  );
141
162
  }
142
- const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
143
- if (!config) {
163
+ const raw = RecipesReferences.readRawRecipeConfig(draftPath);
164
+ if (!raw) {
144
165
  const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
145
166
  throw new Error(
146
167
  ExecutionOutput.formatToolText(
@@ -148,20 +169,35 @@ function promoteDraftRecipe<TContext>(
148
169
  ),
149
170
  );
150
171
  }
151
- const promoted = buildConfig(
152
- name,
172
+ const authoredRecipe = buildAuthoredRecipe(
153
173
  {
154
174
  ...input,
155
- description: input.description ?? config.description,
156
- template: draftPath,
175
+ description:
176
+ input.description ??
177
+ (typeof raw.description === "string" ? raw.description : undefined),
157
178
  },
158
179
  existing,
180
+ raw,
181
+ );
182
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
183
+ const candidate = RecipesDiscovery.admitUserRecipe(
184
+ targetPath,
185
+ resolutionContext,
186
+ authoredRecipe,
159
187
  );
160
- const raw = RecipesReferences.readRawRecipeConfig(draftPath)!;
188
+ if (!candidate.validated || !candidate.tool) {
189
+ throw new Error(
190
+ ExecutionOutput.formatToolText(
191
+ `Draft recipe is invalid: ${candidate.diagnostics.join("; ")}`,
192
+ ),
193
+ );
194
+ }
161
195
  mkdirSync(dirname(targetPath), { recursive: true });
162
- writeJsonAtomic(targetPath, raw);
163
- promoted.template = targetPath;
164
- promoted.sourcePath = targetPath;
196
+ writeJsonAtomic(targetPath, authoredRecipe);
197
+ const promoted = RecipesDiscovery.admitUserRecipe(
198
+ targetPath,
199
+ resolutionContext,
200
+ ).tool!;
165
201
  tools.set(name, promoted);
166
202
  deps.registerRuntimeTool(promoted);
167
203
  deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
@@ -185,18 +221,12 @@ function promoteDraftRecipe<TContext>(
185
221
 
186
222
  function persistToolRecipe<TContext>(
187
223
  deps: RegisterToolRuntimeDeps<TContext>,
188
- cfg: Config.RegisteredTool,
224
+ name: string,
225
+ recipe: Record<string, unknown>,
189
226
  ): string {
190
- const path = getToolRecipePath(deps, cfg.name);
227
+ const path = getToolRecipePath(deps, name);
191
228
  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
- });
229
+ writeJsonAtomic(path, recipe);
200
230
  return path;
201
231
  }
202
232
 
@@ -238,18 +268,27 @@ function deleteTool<TContext>(
238
268
  function readAuthoritativeStoredTool<TContext>(
239
269
  deps: RegisterToolRuntimeDeps<TContext>,
240
270
  name: string,
241
- ): { exists: boolean; tool?: Config.RegisteredTool } {
271
+ ): {
272
+ bytes?: string;
273
+ exists: boolean;
274
+ recipe?: Record<string, unknown>;
275
+ tool?: Config.RegisteredTool;
276
+ } {
242
277
  const path = getToolRecipePath(deps, name);
243
278
  if (!existsSync(path)) return { exists: false };
244
279
  try {
280
+ const bytes = readFileSync(path, "utf8");
281
+ const recipe = JSON.parse(bytes);
245
282
  const normalized = Config.normalizeStoredTool(
246
283
  name,
247
- JSON.parse(readFileSync(path, "utf8")),
284
+ recipe,
248
285
  deps.reservedToolNames,
249
286
  );
250
- if (!normalized.cfg) return { exists: true };
287
+ if (!normalized.cfg) return { bytes, exists: true, recipe };
251
288
  return {
289
+ bytes,
252
290
  exists: true,
291
+ recipe,
253
292
  tool: { ...normalized.cfg, sourcePath: path },
254
293
  };
255
294
  } catch {
@@ -285,11 +324,31 @@ function getInputTemplate(
285
324
  );
286
325
  }
287
326
 
288
- function buildConfig(
289
- name: string,
327
+ function getRegistrationResolutionContext<TContext>(
328
+ ctx: TContext,
329
+ deps: RegisterToolRuntimeDeps<TContext>,
330
+ ): RecipesContext.RecipeResolutionContext {
331
+ if (
332
+ ctx &&
333
+ typeof ctx === "object" &&
334
+ "recipeResolutionContext" in ctx
335
+ ) {
336
+ const resolutionContext = (ctx as {
337
+ recipeResolutionContext?: RecipesContext.RecipeResolutionContext;
338
+ }).recipeResolutionContext;
339
+ if (resolutionContext) return resolutionContext;
340
+ }
341
+ return RecipesContext.createEmptyRecipeResolutionContext(
342
+ "offline-register-tool",
343
+ getRecipeRoot(deps),
344
+ );
345
+ }
346
+
347
+ function buildAuthoredRecipe(
290
348
  input: RegisterToolInput,
291
349
  existing: Config.RegisteredTool | undefined,
292
- ): Config.RegisteredTool {
350
+ existingRecipe: Record<string, unknown> | undefined,
351
+ ): Record<string, unknown> {
293
352
  const explicitArgs =
294
353
  input.args === undefined
295
354
  ? undefined
@@ -311,68 +370,29 @@ function buildConfig(
311
370
  );
312
371
  }
313
372
  const finalTemplate =
314
- template === undefined || template === "" ? existing?.template : template;
373
+ template === undefined || template === ""
374
+ ? existingRecipe?.template ?? existing?.template
375
+ : template;
315
376
  if (!finalTemplate) {
316
377
  throw new Error(
317
378
  ExecutionOutput.formatToolText("Tool template is required."),
318
379
  );
319
380
  }
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,
381
+ const authored: Record<string, unknown> = {
382
+ ...(existingRecipe ?? {}),
361
383
  description,
362
384
  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
385
  };
386
+ if (typeof input.async === "boolean") authored.async = input.async;
387
+ if (explicitArgs) {
388
+ authored.args = explicitArgs.declarations;
389
+ if (Object.keys(explicitArgs.defaults).length > 0)
390
+ authored.defaults = explicitArgs.defaults;
391
+ else delete authored.defaults;
392
+ }
393
+ if (input.values && typeof input.values === "object")
394
+ authored.values = input.values;
395
+ return authored;
376
396
  }
377
397
 
378
398
  function executeRegisterToolUnlocked<TContext>(
@@ -416,47 +436,118 @@ function executeRegisterToolUnlocked<TContext>(
416
436
  ),
417
437
  );
418
438
  }
419
- const cfg = buildConfig(name, input, existing);
420
- let recipePath: string;
439
+ const recipePath = getToolRecipePath(deps, name);
440
+ const authoredRecipe = buildAuthoredRecipe(
441
+ input,
442
+ existing,
443
+ authoritative.recipe,
444
+ );
445
+ const resolutionContext = getRegistrationResolutionContext(ctx, deps);
446
+ const candidate = RecipesDiscovery.admitUserRecipe(
447
+ recipePath,
448
+ resolutionContext,
449
+ authoredRecipe,
450
+ );
451
+ if (!candidate.validated || !candidate.tool) {
452
+ throw new Error(
453
+ ExecutionOutput.formatToolText(
454
+ `Tool recipe validation failed: ${candidate.diagnostics.join("; ")}`,
455
+ ),
456
+ );
457
+ }
458
+ const activeBefore = deps.getActiveTools();
459
+ let persisted = false;
460
+ let activation:
461
+ | {
462
+ active_tool: boolean;
463
+ activation: "current_session" | "unverified";
464
+ callable_now: boolean;
465
+ host_registered: boolean;
466
+ }
467
+ | undefined;
468
+ let cfg: Config.RegisteredTool;
421
469
  try {
422
- recipePath = persistToolRecipe(deps, cfg);
470
+ persistToolRecipe(deps, name, authoredRecipe);
471
+ persisted = true;
472
+ const admitted = RecipesDiscovery.admitUserRecipe(
473
+ recipePath,
474
+ resolutionContext,
475
+ );
476
+ if (!admitted.validated || !admitted.tool) {
477
+ throw new Error(
478
+ `Persisted tool recipe admission failed: ${admitted.diagnostics.join("; ")}`,
479
+ );
480
+ }
481
+ cfg = admitted.tool;
482
+ tools.set(name, cfg);
483
+ activation = deps.registerRuntimeTool(cfg) ?? undefined;
484
+ if (
485
+ activation &&
486
+ (!activation.host_registered ||
487
+ !activation.active_tool ||
488
+ !activation.callable_now)
489
+ ) {
490
+ throw new Error(
491
+ `Host activation verification failed: ${JSON.stringify(activation)}`,
492
+ );
493
+ }
423
494
  } catch (error) {
495
+ if (persisted) {
496
+ if (authoritative.bytes !== undefined)
497
+ writeTextAtomic(recipePath, authoritative.bytes);
498
+ else if (existsSync(recipePath)) unlinkSync(recipePath);
499
+ }
500
+ if (existing) {
501
+ tools.set(name, existing);
502
+ deps.registerRuntimeTool(existing);
503
+ } else {
504
+ tools.delete(name);
505
+ }
506
+ deps.setActiveTools(activeBefore);
424
507
  throw new Error(
425
508
  ExecutionOutput.formatToolText(
426
- `Failed to persist tool recipe: ${error instanceof Error ? error.message : String(error)}`,
509
+ `Tool registration transaction failed: ${error instanceof Error ? error.message : String(error)}`,
427
510
  ),
428
511
  );
429
512
  }
430
- cfg.sourcePath = recipePath;
431
- tools.set(name, cfg);
432
- deps.registerRuntimeTool(cfg);
433
- deps.notify(ctx, `Tool persisted: ${name}`, "info");
513
+ deps.notify(ctx, `Tool activated: ${name}`, "info");
434
514
  const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
435
- typeof cfg.template === "object" && !Array.isArray(cfg.template)
436
- ? cfg.template
437
- : { template: cfg.template! },
515
+ typeof cfg.recipe?.template === "object" && !Array.isArray(cfg.recipe.template)
516
+ ? cfg.recipe.template
517
+ : { template: cfg.recipe?.template ?? cfg.template! },
438
518
  );
439
519
  const warningText =
440
520
  templateWarnings.length > 0
441
521
  ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
442
522
  : "";
523
+ const outcomeText = activation?.callable_now
524
+ ? `${existing ? "Updated" : "Registered"} and activated tool "${name}"`
525
+ : `Persisted tool "${name}"; current-session activation is unverified`;
443
526
  return {
444
527
  content: [
445
528
  textContent(
446
529
  ExecutionOutput.formatToolText(
447
- `${existing ? "Updated" : "Registered"} tool "${name}" (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
530
+ `${outcomeText} (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
448
531
  ),
449
532
  ),
450
533
  ],
451
534
  details: {
535
+ active_tool: activation?.active_tool ?? false,
536
+ activation: activation?.activation ?? "unverified",
452
537
  args: cfg.args,
538
+ callable_now: activation?.callable_now ?? false,
453
539
  config: recipePath,
454
540
  defaults: cfg.defaults,
541
+ host_registered: activation?.host_registered ?? false,
542
+ persisted: true,
543
+ registry_active: tools.get(name) === cfg,
544
+ resolved: true,
455
545
  ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
456
546
  ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
457
547
  ...(cfg.template ? { template: cfg.template } : {}),
458
548
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
459
549
  tool: name,
550
+ validated: true,
460
551
  },
461
552
  };
462
553
  }
package/lib/runtime.ts CHANGED
@@ -10,7 +10,9 @@ 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 RecipesUsage from "./recipes-usage.ts";
14
16
  import * as ToolsLocal from "./tools-local.ts";
15
17
 
16
18
  export interface RuntimeContext {
@@ -20,12 +22,19 @@ export interface RuntimeContext {
20
22
  };
21
23
  }
22
24
 
25
+ export interface RuntimeToolActivation {
26
+ active_tool: boolean;
27
+ activation: "current_session" | "unverified";
28
+ callable_now: boolean;
29
+ host_registered: boolean;
30
+ }
31
+
23
32
  export interface ToolRegistryRuntimeDeps {
24
33
  configPath: string;
25
34
  exec: RegisteredToolExec;
26
- packagedRecipeRoot?: string;
27
35
  recipeRoot?: string;
28
36
  getActiveTools?: () => string[];
37
+ getAllTools?: () => Array<{ name: string }>;
29
38
  registerTool: (
30
39
  definition: ReturnType<typeof ToolsLocal.createRuntimeToolDefinition>,
31
40
  ) => void;
@@ -33,16 +42,32 @@ export interface ToolRegistryRuntimeDeps {
33
42
  setActiveTools?: (toolNames: string[]) => void;
34
43
  }
35
44
 
45
+ export interface RecipeRegistryStatus {
46
+ active_tool_count: number;
47
+ last_scan_counts: { active: number; rejected: number; scanned: number };
48
+ last_scan_at?: string;
49
+ registry_generation: number;
50
+ resolution_generation?: string;
51
+ watch_status: "closed" | "failed" | "watching_parent" | "watching_root";
52
+ watched_root: string;
53
+ }
54
+
36
55
  export interface ToolRegistryRuntime {
56
+ getStatus(): RecipeRegistryStatus;
57
+ getToolStatus(name: string): Record<string, unknown> | undefined;
37
58
  getToolNameBlocker(name: string): string | undefined;
38
59
  getTools(): Map<string, Config.RegisteredTool>;
39
- loadTools(ctx: RuntimeContext): void;
60
+ loadTools(
61
+ ctx: RuntimeContext,
62
+ resolutionContext?: RecipeResolutionContext,
63
+ ): void;
40
64
  notify(
41
65
  ctx: RuntimeContext,
42
66
  message: string,
43
67
  type: "info" | "warning" | "error",
44
68
  ): void;
45
- registerRuntimeTool(cfg: Config.RegisteredTool): void;
69
+ registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation;
70
+ setWatchStatus(status: RecipeRegistryStatus["watch_status"]): void;
46
71
  }
47
72
 
48
73
  export interface RecipeToolReloadWatcher {
@@ -56,6 +81,14 @@ export function createAutoToolsRuntime(
56
81
  const tools = new Map<string, Config.RegisteredTool>();
57
82
  const runtimeToolFingerprints = new Map<string, string>();
58
83
  const runtimeTools = new Set<string>();
84
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
85
+ const status: RecipeRegistryStatus = {
86
+ active_tool_count: 0,
87
+ last_scan_counts: { active: 0, rejected: 0, scanned: 0 },
88
+ registry_generation: 0,
89
+ watch_status: "closed",
90
+ watched_root: recipeRoot,
91
+ };
59
92
  function notify(
60
93
  ctx: RuntimeContext,
61
94
  message: string,
@@ -91,12 +124,30 @@ export function createAutoToolsRuntime(
91
124
  deps.getActiveTools().filter((name) => !staleSet.has(name)),
92
125
  );
93
126
  }
94
- function registerRuntimeTool(cfg: Config.RegisteredTool) {
127
+ function activationFor(name: string): RuntimeToolActivation {
128
+ const hostRegistered = deps.getAllTools?.().some((tool) => tool.name === name) ?? false;
129
+ const activeTool = deps.getActiveTools?.().includes(name) ?? false;
130
+ return {
131
+ active_tool: activeTool,
132
+ activation:
133
+ hostRegistered && activeTool ? "current_session" : "unverified",
134
+ callable_now: hostRegistered && activeTool,
135
+ host_registered: hostRegistered,
136
+ };
137
+ }
138
+ function registerRuntimeTool(cfg: Config.RegisteredTool): RuntimeToolActivation {
95
139
  const fingerprint = getToolFingerprint(cfg);
96
- if (runtimeToolFingerprints.get(cfg.name) === fingerprint) return;
97
- deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
98
- runtimeTools.add(cfg.name);
99
- runtimeToolFingerprints.set(cfg.name, fingerprint);
140
+ if (runtimeToolFingerprints.get(cfg.name) !== fingerprint) {
141
+ deps.registerTool(ToolsLocal.createRuntimeToolDefinition(cfg, deps.exec));
142
+ runtimeTools.add(cfg.name);
143
+ runtimeToolFingerprints.set(cfg.name, fingerprint);
144
+ }
145
+ if (deps.getActiveTools && deps.setActiveTools) {
146
+ deps.setActiveTools([
147
+ ...new Set([...deps.getActiveTools(), cfg.name]),
148
+ ]);
149
+ }
150
+ return activationFor(cfg.name);
100
151
  }
101
152
  function isStartupActionableRegistryWarning(warning: string): boolean {
102
153
  if (warning.includes(" shadows ")) return false;
@@ -125,7 +176,7 @@ export function createAutoToolsRuntime(
125
176
  );
126
177
  const lines = ["pi-actors recipe registry warning"];
127
178
  if (shadowed.length > 0) {
128
- lines.push("User recipes override packaged recipes:");
179
+ lines.push("User recipes shadow lower-priority user sources:");
129
180
  lines.push(...shadowed.map((warning) => `• ${warning}`));
130
181
  }
131
182
  if (skipped.length > 0) {
@@ -138,14 +189,18 @@ export function createAutoToolsRuntime(
138
189
  }
139
190
  return `${lines.join("\n")}\n`;
140
191
  }
141
- function loadTools(ctx: RuntimeContext) {
192
+ function loadTools(
193
+ ctx: RuntimeContext,
194
+ resolutionContext?: RecipeResolutionContext,
195
+ ) {
142
196
  const warnings: string[] = [];
143
- const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
144
- const packagedRecipeRoot =
145
- deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot();
146
197
  const discovered = RecipesDiscovery.discoverRecipeSources([
147
- { root: recipeRoot, defaultTool: true, mutableUsage: true },
148
- { root: packagedRecipeRoot },
198
+ {
199
+ root: recipeRoot,
200
+ defaultTool: true,
201
+ mutableUsage: true,
202
+ resolutionContext,
203
+ },
149
204
  ]);
150
205
  warnings.push(...discovered.diagnostics);
151
206
  tools.clear();
@@ -168,28 +223,59 @@ export function createAutoToolsRuntime(
168
223
  }
169
224
  registerRuntimeTool(cfg);
170
225
  }
226
+ status.active_tool_count = tools.size;
227
+ status.last_scan_at = new Date().toISOString();
228
+ status.last_scan_counts = {
229
+ active: tools.size,
230
+ rejected: discovered.entries.filter((entry) => entry.invalid).length,
231
+ scanned: discovered.entries.length,
232
+ };
233
+ status.registry_generation += 1;
234
+ status.resolution_generation = resolutionContext?.generation;
171
235
  const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
172
236
  if (startupWarnings.length > 0) {
173
237
  notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
174
238
  }
175
239
  }
176
240
  return {
241
+ getStatus: () => ({
242
+ ...status,
243
+ last_scan_counts: { ...status.last_scan_counts },
244
+ }),
245
+ getToolStatus: (name) => {
246
+ const cfg = tools.get(name);
247
+ if (!cfg) return undefined;
248
+ const usage = cfg.sourcePath
249
+ ? RecipesUsage.readRecipeUsage(cfg.sourcePath)
250
+ : undefined;
251
+ return {
252
+ ...activationFor(name),
253
+ launch_kind: usage?.launch_kind,
254
+ spawn_calls: Number(usage?.spawn_calls ?? 0),
255
+ tool_calls: Number(usage?.tool_calls ?? 0),
256
+ };
257
+ },
177
258
  getToolNameBlocker,
178
259
  getTools: () => tools,
179
260
  loadTools,
180
261
  notify,
181
262
  registerRuntimeTool,
263
+ setWatchStatus: (watchStatus) => {
264
+ status.watch_status = watchStatus;
265
+ },
182
266
  };
183
267
  }
184
268
 
185
269
  export interface RecipeToolReloadWatcherDeps {
186
270
  exists?: (path: string) => boolean;
271
+ getResolutionContext?: () => RecipeResolutionContext | undefined;
187
272
  recipeRoot?: string;
188
273
  watchPath?: typeof watch;
189
274
  }
190
275
 
191
276
  export function createRecipeToolReloadWatcher(
192
- runtime: Pick<ToolRegistryRuntime, "loadTools">,
277
+ runtime: Pick<ToolRegistryRuntime, "loadTools"> &
278
+ Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>,
193
279
  deps: RecipeToolReloadWatcherDeps = {},
194
280
  ): RecipeToolReloadWatcher {
195
281
  const pathExists = deps.exists ?? existsSync;
@@ -198,6 +284,8 @@ export function createRecipeToolReloadWatcher(
198
284
  let rootWatcher: FSWatcher | undefined;
199
285
  let parentWatcher: FSWatcher | undefined;
200
286
  let failureNotified = false;
287
+ const setWatchStatus = (watchStatus: RecipeRegistryStatus["watch_status"]): void =>
288
+ runtime.setWatchStatus?.(watchStatus);
201
289
  const close = (): void => {
202
290
  const closingRoot = rootWatcher;
203
291
  rootWatcher = undefined;
@@ -207,10 +295,12 @@ export function createRecipeToolReloadWatcher(
207
295
  closingParent?.close();
208
296
  if (reloadTimeout) clearTimeout(reloadTimeout);
209
297
  reloadTimeout = undefined;
298
+ setWatchStatus("closed");
210
299
  };
211
300
  const notifyFailure = (ctx: RuntimeContext): void => {
212
301
  if (failureNotified) return;
213
302
  failureNotified = true;
303
+ setWatchStatus("failed");
214
304
  ctx.ui.notify(
215
305
  "Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
216
306
  "warning",
@@ -220,7 +310,7 @@ export function createRecipeToolReloadWatcher(
220
310
  failureNotified = false;
221
311
  if (reloadTimeout) clearTimeout(reloadTimeout);
222
312
  reloadTimeout = setTimeout(() => {
223
- runtime.loadTools(ctx);
313
+ runtime.loadTools(ctx, deps.getResolutionContext?.());
224
314
  ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
225
315
  }, 150);
226
316
  reloadTimeout.unref?.();
@@ -249,6 +339,7 @@ export function createRecipeToolReloadWatcher(
249
339
  scheduleReload(ctx);
250
340
  });
251
341
  parentWatcher = watcher;
342
+ setWatchStatus("watching_parent");
252
343
  watcher.on("error", () => {
253
344
  if (parentWatcher !== watcher) return;
254
345
  parentWatcher = undefined;
@@ -278,6 +369,7 @@ export function createRecipeToolReloadWatcher(
278
369
  scheduleReload(ctx);
279
370
  });
280
371
  rootWatcher = watcher;
372
+ setWatchStatus("watching_root");
281
373
  watcher.on("error", () => {
282
374
  if (rootWatcher !== watcher) return;
283
375
  rootWatcher = undefined;