@llblab/pi-actors 0.35.0 → 0.37.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 (42) hide show
  1. package/AGENTS.md +3 -1
  2. package/BACKLOG.md +4 -72
  3. package/CHANGELOG.md +17 -3
  4. package/README.md +2 -1
  5. package/dist/index.js +0 -1
  6. package/dist/lib/async-runs.js +4 -3
  7. package/dist/lib/command-templates.d.ts +2 -0
  8. package/dist/lib/command-templates.js +95 -5
  9. package/dist/lib/recipes-discovery.d.ts +2 -0
  10. package/dist/lib/recipes-discovery.js +52 -5
  11. package/dist/lib/recipes-references.d.ts +1 -0
  12. package/dist/lib/recipes-references.js +122 -35
  13. package/dist/lib/registry.d.ts +1 -1
  14. package/dist/lib/registry.js +6 -6
  15. package/dist/lib/runtime-notifier.js +8 -3
  16. package/dist/lib/runtime.d.ts +2 -6
  17. package/dist/lib/runtime.js +10 -11
  18. package/dist/lib/tools-inspect.js +172 -4
  19. package/dist/lib/tools-response.js +13 -2
  20. package/dist/lib/tools.d.ts +1 -1
  21. package/dist/lib/tools.js +1 -1
  22. package/dist/scripts/validate-recipe.mjs +164 -7
  23. package/dist/skills/actors/SKILL.md +45 -13
  24. package/dist/skills/swarm/SKILL.md +1 -1
  25. package/docs/recipe-library.md +2 -0
  26. package/docs/template-recipes.md +20 -1
  27. package/docs/tool-registry.md +15 -10
  28. package/index.ts +0 -1
  29. package/lib/async-runs.ts +4 -7
  30. package/lib/command-templates.ts +146 -7
  31. package/lib/recipes-discovery.ts +69 -6
  32. package/lib/recipes-references.ts +201 -34
  33. package/lib/registry.ts +5 -5
  34. package/lib/runtime-notifier.ts +9 -3
  35. package/lib/runtime.ts +10 -16
  36. package/lib/tools-inspect.ts +186 -4
  37. package/lib/tools-response.ts +16 -2
  38. package/lib/tools.ts +2 -2
  39. package/package.json +3 -2
  40. package/scripts/validate-recipe.mjs +164 -7
  41. package/skills/actors/SKILL.md +45 -13
  42. package/skills/swarm/SKILL.md +1 -1
@@ -248,6 +248,181 @@ function compactPiActorsRuntimeStatus(status: Record<string, unknown>): string {
248
248
  return `\npi-actors version=${String(status.version)} mode=${String(status.mode)} path=${String(status.package_root)} entrypoint=${String(status.entrypoint)}${status.git_commit ? ` git=${String(status.git_commit)}` : ""}`;
249
249
  }
250
250
 
251
+ function isStaleClaim(message: AsyncRuns.RunInboxMessage, now: number): boolean {
252
+ if (message.status !== "claimed") return false;
253
+ const claimedAt = Date.parse(String(message.claimed_at ?? ""));
254
+ return Number.isFinite(claimedAt) && now - claimedAt > 5 * 60 * 1000;
255
+ }
256
+
257
+ function getRunTriageSignals(
258
+ runs: Array<Record<string, unknown>>,
259
+ ): Record<string, unknown> {
260
+ const now = Date.now();
261
+ const staleClaims: Array<Record<string, unknown>> = [];
262
+ const attentionMessages: Array<Record<string, unknown>> = [];
263
+ for (const run of runs) {
264
+ const stateDir = String(run.state_dir ?? "");
265
+ const runId = String(run.run ?? "");
266
+ if (!stateDir) continue;
267
+ try {
268
+ for (const message of AsyncRuns.readRunInboxMessages(stateDir, 200)) {
269
+ if (!isStaleClaim(message, now)) continue;
270
+ staleClaims.push({
271
+ run: runId,
272
+ id: message.id,
273
+ claimed_at: message.claimed_at,
274
+ claimed_by: message.claimed_by,
275
+ type: message.type,
276
+ });
277
+ }
278
+ } catch {}
279
+ try {
280
+ for (const event of AsyncRuns.readRunEvents(stateDir, 80)) {
281
+ if (event.metadata?.requires_response !== true) continue;
282
+ attentionMessages.push({
283
+ run: runId,
284
+ id: event.id,
285
+ summary: event.summary,
286
+ type: event.type ?? event.event,
287
+ });
288
+ }
289
+ } catch {}
290
+ }
291
+ return { attention_messages: attentionMessages, stale_claims: staleClaims };
292
+ }
293
+
294
+ function isTriageHighRiskRecipe(recipe: Record<string, unknown>): boolean {
295
+ if (recipe.tool !== true) return false;
296
+ const labels = Array.isArray(recipe.risk_labels)
297
+ ? recipe.risk_labels.map((label) => String(label))
298
+ : [];
299
+ return labels.some((label) => label !== "risk.long_running");
300
+ }
301
+
302
+ function getPiActorsTriage(
303
+ ctx: unknown,
304
+ deps: InspectToolDeps,
305
+ ): Record<string, unknown> {
306
+ const runtime = getPiActorsRuntimeStatus();
307
+ const currentSession = ToolsAccess.getContextSessionId(ctx);
308
+ const allRuns = AsyncRuns.listRuns().map((run) =>
309
+ AsyncRuns.getRunStatus(String(run.state_dir)),
310
+ );
311
+ const visibleRuns = currentSession
312
+ ? allRuns.filter(
313
+ (run) => !run.ownerId || run.ownerId === currentSession,
314
+ )
315
+ : allRuns;
316
+ const activeRuns = visibleRuns.filter((run) => run.status === "running");
317
+ const failedRuns = visibleRuns.filter((run) => run.status === "failed");
318
+ const otherRuns = currentSession
319
+ ? allRuns.filter((run) => run.ownerId && run.ownerId !== currentSession)
320
+ : [];
321
+ const recipeRoot = deps.recipeRoot ?? Paths.getRecipeRoot();
322
+ const discovered = RecipesDiscovery.discoverRecipeSources([
323
+ { root: recipeRoot, defaultTool: true, mutableUsage: true },
324
+ { root: deps.packagedRecipeRoot ?? Paths.getPackagedRecipeRoot() },
325
+ ]);
326
+ const recipeSummary: Record<string, unknown> = {
327
+ ...RecipesDiscovery.summarizeDiscovery(discovered),
328
+ drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
329
+ };
330
+ const activeRecipes = Array.isArray(recipeSummary.active)
331
+ ? (recipeSummary.active as Array<Record<string, unknown>>)
332
+ : [];
333
+ const highRiskRecipes = activeRecipes.filter(isTriageHighRiskRecipe);
334
+ const signals = getRunTriageSignals(visibleRuns);
335
+ const attentionMessages = signals.attention_messages as Array<
336
+ Record<string, unknown>
337
+ >;
338
+ const staleClaims = signals.stale_claims as Array<Record<string, unknown>>;
339
+ const invalidRecipes = Array.isArray(recipeSummary.invalid)
340
+ ? (recipeSummary.invalid as Array<Record<string, unknown>>)
341
+ : [];
342
+ const remediations = Array.isArray(recipeSummary.remediations)
343
+ ? (recipeSummary.remediations as Array<Record<string, unknown>>)
344
+ : [];
345
+ const drafts = Array.isArray(recipeSummary.drafts)
346
+ ? (recipeSummary.drafts as Array<Record<string, unknown>>)
347
+ : [];
348
+ const nextActions = [
349
+ invalidRecipes.length || remediations.length
350
+ ? "inspect target=recipes view=doctor"
351
+ : "",
352
+ drafts.length ? "inspect target=recipes view=summary verbose=true" : "",
353
+ failedRuns[0]?.run
354
+ ? `inspect target=run:${String(failedRuns[0].run)} view=tail lines=80`
355
+ : "",
356
+ attentionMessages[0]?.run
357
+ ? `inspect target=run:${String(attentionMessages[0].run)} view=messages`
358
+ : "",
359
+ activeRuns.length
360
+ ? "inspect target=session:all view=runs status=active"
361
+ : "",
362
+ ].filter(Boolean);
363
+ return {
364
+ runtime,
365
+ current_session: currentSession ?? null,
366
+ active_runs: activeRuns.map((run) => ({
367
+ run: run.run,
368
+ ownerId: run.ownerId,
369
+ recipe: run.recipe,
370
+ status: run.status,
371
+ })),
372
+ other_session_runs: otherRuns.length,
373
+ invalid_recipes: invalidRecipes,
374
+ blocking_recipes: remediations.filter((item) =>
375
+ String(item.kind ?? "").startsWith("blocking_"),
376
+ ),
377
+ high_risk_recipes: highRiskRecipes.map((recipe) => ({
378
+ id: recipe.id,
379
+ path: recipe.path,
380
+ risk_labels: recipe.risk_labels,
381
+ })),
382
+ draft_recipes: drafts,
383
+ stale_claims: staleClaims,
384
+ recent_failed_runs: failedRuns.slice(0, 5).map((run) => ({
385
+ run: run.run,
386
+ recipe: run.recipe,
387
+ status: run.status,
388
+ })),
389
+ attention_messages: attentionMessages.slice(-10),
390
+ next_actions: [...new Set(nextActions)].slice(0, 5),
391
+ };
392
+ }
393
+
394
+ function compactPiActorsTriage(summary: Record<string, unknown>): string {
395
+ const runtime = asRecord(summary.runtime);
396
+ const activeRuns = Array.isArray(summary.active_runs)
397
+ ? summary.active_runs.length
398
+ : 0;
399
+ const invalidRecipes = Array.isArray(summary.invalid_recipes)
400
+ ? summary.invalid_recipes.length
401
+ : 0;
402
+ const blockingRecipes = Array.isArray(summary.blocking_recipes)
403
+ ? summary.blocking_recipes.length
404
+ : 0;
405
+ const highRiskRecipes = Array.isArray(summary.high_risk_recipes)
406
+ ? summary.high_risk_recipes.length
407
+ : 0;
408
+ const drafts = Array.isArray(summary.draft_recipes)
409
+ ? summary.draft_recipes.length
410
+ : 0;
411
+ const staleClaims = Array.isArray(summary.stale_claims)
412
+ ? summary.stale_claims.length
413
+ : 0;
414
+ const failedRuns = Array.isArray(summary.recent_failed_runs)
415
+ ? summary.recent_failed_runs.length
416
+ : 0;
417
+ const attention = Array.isArray(summary.attention_messages)
418
+ ? summary.attention_messages.length
419
+ : 0;
420
+ const nextActions = Array.isArray(summary.next_actions)
421
+ ? (summary.next_actions as string[])
422
+ : [];
423
+ return `\ntriage version=${String(runtime.version ?? "unknown")} mode=${String(runtime.mode ?? "unknown")} active_runs=${activeRuns} other_runs=${String(summary.other_session_runs ?? 0)} invalid_recipes=${invalidRecipes} blocking_recipes=${blockingRecipes} high_risk_recipes=${highRiskRecipes} drafts=${drafts} stale_claims=${staleClaims} failed_runs=${failedRuns} attention=${attention}${ToolsResponse.compactNextActions(nextActions)}`;
424
+ }
425
+
251
426
  function compactToolActor(name: string, tool: Record<string, unknown>): string {
252
427
  const parameters = asRecord(tool.parameters);
253
428
  const required = Array.isArray(parameters.required)
@@ -414,10 +589,15 @@ export function createInspectToolDefinition<TContext = unknown>(
414
589
  }
415
590
  if (address.kind === "tool" && address.value) {
416
591
  if (address.value === "pi-actors") {
417
- if (view !== "status") {
418
- throw new Error("inspect tool:pi-actors supports view=status.");
592
+ if (view !== "status" && view !== "triage") {
593
+ throw new Error(
594
+ "inspect tool:pi-actors supports view=status or view=triage.",
595
+ );
419
596
  }
420
- const details = getPiActorsRuntimeStatus();
597
+ const details =
598
+ view === "triage"
599
+ ? getPiActorsTriage(ctx, deps)
600
+ : getPiActorsRuntimeStatus();
421
601
  return {
422
602
  content: [
423
603
  {
@@ -425,7 +605,9 @@ export function createInspectToolDefinition<TContext = unknown>(
425
605
  text: maybeJsonText(
426
606
  details,
427
607
  input.verbose === true,
428
- compactPiActorsRuntimeStatus(details),
608
+ view === "triage"
609
+ ? compactPiActorsTriage(details)
610
+ : compactPiActorsRuntimeStatus(details),
429
611
  ),
430
612
  },
431
613
  ],
@@ -173,15 +173,26 @@ export function compactRecipeDoctor(summary: Record<string, unknown>): string {
173
173
  const recommendations = Array.isArray(summary.recommendations)
174
174
  ? (summary.recommendations as Array<Record<string, unknown>>)
175
175
  : [];
176
+ const riskSummary = Array.isArray(summary.risk_summary)
177
+ ? (summary.risk_summary as Array<Record<string, unknown>>)
178
+ : [];
176
179
  const counts = { error: 0, info: 0, warning: 0 };
177
180
  for (const detail of details) {
178
181
  const severity = String(detail.severity ?? "info");
179
182
  if (severity === "error" || severity === "warning" || severity === "info")
180
183
  counts[severity] += 1;
181
184
  }
185
+ const riskCount = riskSummary.reduce(
186
+ (total, item) => total + Number(item.count ?? 0),
187
+ 0,
188
+ );
189
+ const topRisks = riskSummary
190
+ .slice(0, 4)
191
+ .map((item) => `${String(item.label)}:${String(item.count ?? 0)}`)
192
+ .join(",");
182
193
  const topAction = asRecord(summary.top_action);
183
194
  const lines = [
184
- `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
195
+ `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length} risks=${riskCount}${topRisks ? ` top_risks=${topRisks}` : ""}`,
185
196
  ];
186
197
  if (Object.keys(topAction).length > 0) {
187
198
  const action = compactPreview(
@@ -200,8 +211,11 @@ export function compactRecipeDoctor(summary: Record<string, unknown>): string {
200
211
  const blocked = item.blocked_fallback
201
212
  ? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
202
213
  : "";
214
+ const labels = Array.isArray(item.risk_labels)
215
+ ? ` labels=${(item.risk_labels as unknown[]).map(String).slice(0, 4).join(",")}`
216
+ : "";
203
217
  lines.push(
204
- `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
218
+ `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked}${labels} action=${action ?? "inspect"}`,
205
219
  );
206
220
  }
207
221
  const nextActions = Array.isArray(summary.next_actions)
package/lib/tools.ts CHANGED
@@ -27,7 +27,7 @@ export interface CoreActorToolDefinitionDeps<
27
27
  getRuntimeTool: (name: string) => unknown;
28
28
  registryRuntime: Pick<
29
29
  RegisterToolRuntimeDeps<TContext>,
30
- "getExternalToolConflict" | "getTools" | "notify" | "registerRuntimeTool"
30
+ "getToolNameBlocker" | "getTools" | "notify" | "registerRuntimeTool"
31
31
  >;
32
32
  setActiveTools: (toolNames: string[]) => void;
33
33
  }
@@ -53,7 +53,7 @@ export function createCoreActorToolDefinitions<
53
53
  ToolsRegister.createRegisterToolDefinition<TContext>({
54
54
  configPath: deps.configPath,
55
55
  getActiveTools: deps.getActiveTools,
56
- getExternalToolConflict: deps.registryRuntime.getExternalToolConflict,
56
+ getToolNameBlocker: deps.registryRuntime.getToolNameBlocker,
57
57
  getTools: deps.registryRuntime.getTools,
58
58
  notify: deps.registryRuntime.notify,
59
59
  registerRuntimeTool: deps.registryRuntime.registerRuntimeTool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -29,7 +29,8 @@
29
29
  "conformance": "node scripts/conformance.mjs",
30
30
  "test": "node --experimental-strip-types --test tests/*.test.ts",
31
31
  "pack:dry": "npm pack --dry-run",
32
- "validate": "npx tsc --noEmit && npm run build && npm run check && npm test && npm run pack:dry",
32
+ "recipes:qa": "node scripts/validate-recipe.mjs recipes --all --qa --summary",
33
+ "validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run pack:dry",
33
34
  "build": "node scripts/build-dist.mjs",
34
35
  "prepack": "npm run build"
35
36
  },
@@ -27,9 +27,9 @@ const { readResolvedRecipeConfig } = await importRuntimeModule("recipes-referenc
27
27
 
28
28
  export function validateRecipeUsage() {
29
29
  return `Usage:
30
- validate-recipe.mjs <recipe-file-or-dir> [--all]
30
+ validate-recipe.mjs <recipe-file-or-dir> [--all] [--qa] [--summary]
31
31
 
32
- Validates one template recipe file, or all *.json/*.md files in a directory when --all is set.`;
32
+ Validates one template recipe file, or all *.json/*.md files in a directory when --all is set. Add --qa for packaged-recipe quality checks. Add --summary for compact CLI output.`;
33
33
  }
34
34
 
35
35
  function expandPath(value) {
@@ -63,14 +63,133 @@ function recipeFiles(target, all) {
63
63
  .map((file) => resolve(target, file));
64
64
  }
65
65
 
66
- function validateFile(file) {
66
+ function mailboxType(value) {
67
+ if (typeof value === "string") return value;
68
+ if (value && typeof value === "object" && typeof value.type === "string")
69
+ return value.type;
70
+ return undefined;
71
+ }
72
+
73
+ function collectTemplateStrings(value, out = []) {
74
+ if (typeof value === "string") out.push(value);
75
+ else if (Array.isArray(value)) value.forEach((item) => collectTemplateStrings(item, out));
76
+ else if (value && typeof value === "object") {
77
+ collectTemplateStrings(value.template, out);
78
+ collectTemplateStrings(value.recover, out);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function hasPlatformNote(config) {
84
+ const text = [
85
+ config.description,
86
+ config.platforms,
87
+ config.platform_notes,
88
+ config.requirements,
89
+ ]
90
+ .filter(Boolean)
91
+ .join(" ");
92
+ return /linux|macos|darwin|windows|win32|unix|wsl|cross-platform|portable/i.test(text);
93
+ }
94
+
95
+ function validateArtifactDeclarations(config) {
96
+ const diagnostics = [];
97
+ const artifacts = config.artifacts;
98
+ if (artifacts === undefined) return diagnostics;
99
+ if (!artifacts || typeof artifacts !== "object" || Array.isArray(artifacts)) {
100
+ diagnostics.push("artifacts: must be an object of named artifact paths");
101
+ return diagnostics;
102
+ }
103
+ for (const [name, value] of Object.entries(artifacts)) {
104
+ const path = typeof value === "string" ? value : value?.path;
105
+ if (typeof path !== "string" || !path.trim())
106
+ diagnostics.push(`artifacts.${name}: must declare a non-empty path`);
107
+ if (typeof path === "string" && /^\/home\//.test(path))
108
+ diagnostics.push(`artifacts.${name}: must not use a machine-local absolute path`);
109
+ }
110
+ return diagnostics;
111
+ }
112
+
113
+ function validateHelperPaths(file, config) {
114
+ const diagnostics = [];
115
+ for (const template of collectTemplateStrings(config.template)) {
116
+ if (/(^|\s)(?:node\s+)?scripts\/[\w.-]+\.mjs/.test(template))
117
+ diagnostics.push("template: helper scripts must be referenced through {repo}/scripts for installed packages");
118
+ for (const match of template.matchAll(/\{repo\}\/scripts\/([^\s"']+\.mjs)/g)) {
119
+ const scriptPath = join(packageRoot(), "scripts", match[1]);
120
+ if (!existsSync(scriptPath))
121
+ diagnostics.push(`template: referenced helper script not found: scripts/${match[1]}`);
122
+ }
123
+ }
124
+ return diagnostics;
125
+ }
126
+
127
+ function validateMailboxContract(file, config) {
128
+ const diagnostics = [];
129
+ const mailbox = config.mailbox;
130
+ const accepts = Array.isArray(mailbox?.accepts) ? mailbox.accepts : [];
131
+ const emits = Array.isArray(mailbox?.emits) ? mailbox.emits : [];
132
+ if (config.async === true && !mailbox)
133
+ diagnostics.push("mailbox: async recipes must declare mailbox metadata");
134
+ if (config.async === true && !accepts.map(mailboxType).includes("control.kill"))
135
+ diagnostics.push("mailbox.accepts: async recipes must include control.kill");
136
+ const allowedLegacyTypes = new Set(["awaiting_assignment"]);
137
+ for (const [key, entries] of Object.entries({ accepts, emits })) {
138
+ entries.forEach((entry, index) => {
139
+ const type = mailboxType(entry);
140
+ if (!type) diagnostics.push(`mailbox.${key}[${index}]: must be a string or object with type`);
141
+ else if (
142
+ !allowedLegacyTypes.has(type) &&
143
+ !/^[a-z][a-z0-9_-]*\.(?:[a-z][a-z0-9_-]*|\*)$/.test(type)
144
+ )
145
+ diagnostics.push(`mailbox.${key}[${index}]: message type must use channel.action form`);
146
+ });
147
+ }
148
+ const allowedDomainTermination = new Set([
149
+ "coordinator-locker.json",
150
+ "locker.json",
151
+ "music-player.json",
152
+ ]);
153
+ const hasDomainTermination = accepts
154
+ .map(mailboxType)
155
+ .some((type) => type === "control.stop" || type === "control.cancel");
156
+ if (hasDomainTermination && !allowedDomainTermination.has(file.split(/[\\/]/).pop()))
157
+ diagnostics.push("mailbox.accepts: control.stop/control.cancel are reserved for actor-domain handlers");
158
+ return diagnostics;
159
+ }
160
+
161
+ function qaDiagnostics(file, config) {
162
+ const diagnostics = [];
163
+ const warnings = [];
164
+ if (typeof config.description !== "string" || !config.description.trim())
165
+ warnings.push("description: missing or empty");
166
+ diagnostics.push(...validateMailboxContract(file, config));
167
+ diagnostics.push(...validateArtifactDeclarations(config));
168
+ diagnostics.push(...validateHelperPaths(file, config));
169
+ const platformSpecificTemplate = collectTemplateStrings(config.template).some(
170
+ (template) =>
171
+ /(^|\s)(?:systemctl|launchctl|osascript|powershell|pwsh|cmd\.exe|apt|apt-get|dnf|yum|brew|pacman|apk)(\s|$)/i.test(
172
+ template,
173
+ ),
174
+ );
175
+ if (platformSpecificTemplate && !hasPlatformNote(config))
176
+ diagnostics.push("platform: platform-specific templates must document platform scope");
177
+ return { diagnostics, warnings };
178
+ }
179
+
180
+ function qaOk(qaReport) {
181
+ return qaReport.diagnostics.length === 0;
182
+ }
183
+
184
+ function validateFile(file, qa = false) {
67
185
  try {
68
186
  const config = readResolvedRecipeConfig(file);
69
187
  if (!config?.template)
70
188
  throw new Error("Recipe must define a non-empty template.");
189
+ const qaReport = qa ? qaDiagnostics(file, config) : { diagnostics: [], warnings: [] };
71
190
  return {
72
191
  file,
73
- ok: true,
192
+ ok: qaOk(qaReport),
74
193
  name: config.name ?? "",
75
194
  async: Boolean(config.async),
76
195
  args: Array.isArray(config.args) ? config.args : [],
@@ -93,6 +212,15 @@ function validateFile(file) {
93
212
  : [],
94
213
  }
95
214
  : undefined,
215
+ ...(qa
216
+ ? {
217
+ qa: {
218
+ ok: qaOk(qaReport),
219
+ diagnostics: qaReport.diagnostics,
220
+ warnings: qaReport.warnings,
221
+ },
222
+ }
223
+ : {}),
96
224
  template: templateKind(config.template),
97
225
  };
98
226
  } catch (error) {
@@ -107,12 +235,13 @@ function validateFile(file) {
107
235
  export function validateRecipes(argv) {
108
236
  const targetArg = argv.find((arg) => !arg.startsWith("-"));
109
237
  const all = argv.includes("--all");
238
+ const qa = argv.includes("--qa");
110
239
  if (!targetArg || argv.includes("--help") || argv.includes("-h")) {
111
240
  return { help: true, ok: Boolean(targetArg), usage: validateRecipeUsage() };
112
241
  }
113
242
 
114
243
  const files = recipeFiles(expandPath(targetArg), all);
115
- const results = files.map(validateFile);
244
+ const results = files.map((file) => validateFile(file, qa));
116
245
  const failed = results.filter((result) => !result.ok).length;
117
246
  return {
118
247
  ok: failed === 0,
@@ -123,10 +252,38 @@ export function validateRecipes(argv) {
123
252
  };
124
253
  }
125
254
 
255
+ function summarizeReport(report) {
256
+ const results = Array.isArray(report.results) ? report.results : [];
257
+ return {
258
+ ok: report.ok,
259
+ total: report.total,
260
+ passed: report.passed,
261
+ failed: report.failed,
262
+ diagnostics: results.reduce(
263
+ (total, result) => total + (result.qa?.diagnostics?.length ?? 0),
264
+ 0,
265
+ ),
266
+ warnings: results.reduce(
267
+ (total, result) => total + (result.qa?.warnings?.length ?? 0),
268
+ 0,
269
+ ),
270
+ failed_files: results
271
+ .filter((result) => !result.ok)
272
+ .map((result) => ({
273
+ file: result.file,
274
+ ...(result.error ? { error: result.error } : {}),
275
+ ...(result.qa?.diagnostics?.length
276
+ ? { diagnostics: result.qa.diagnostics }
277
+ : {}),
278
+ })),
279
+ };
280
+ }
281
+
126
282
  try {
127
- const report = validateRecipes(process.argv.slice(2));
283
+ const argv = process.argv.slice(2);
284
+ const report = validateRecipes(argv);
128
285
  if (report.help) console.error(report.usage);
129
- else console.log(JSON.stringify(report, null, 2));
286
+ else console.log(JSON.stringify(argv.includes("--summary") ? summarizeReport(report) : report, null, 2));
130
287
  process.exit(report.ok ? 0 : 1);
131
288
  } catch (error) {
132
289
  console.error(error instanceof Error ? error.message : String(error));
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.35.0
5
+ version: 0.37.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -194,12 +194,14 @@ Rules:
194
194
  2. `async: true` makes spawned work a detached actor run.
195
195
  3. Public knobs belong in `args`/`defaults`; hidden launch mechanics stay inside `template`.
196
196
  4. Use `imports` to compose recipes; imported recipes are definitions, not nested async runs.
197
- 5. Declare `mailbox` for actors that accept or emit meaningful messages.
198
- 6. Declare `artifacts` for durable outputs the coordinator should inspect.
199
- 7. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
200
- 8. File-backed async recipes pass child `pi -p` actors a bounded JSONL recipe context bundle by default: raw entry/import recipe records, derived `name`, import path/alias, and `"you_are_here": true` on the launching recipe node. Set `"actor_context": false` or `"off"` to suppress it for minimal prompts.
201
- 9. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
202
- 10. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
197
+ 5. Direct recipe delegation is the thin-wrapper case: when a `template` value is just a ready recipe name/path, the intended behavior is to delegate to that recipe rather than execute the recipe file as a program. Use this for simple handoffs and wrapper tools; use `imports` + `{ "name": "alias" }` when you need rich composition, multiple nodes, or import-specific values/defaults.
198
+ 6. When exposing an already-authored recipe as a user tool before direct delegation is available or when composition is needed, make a small wrapper recipe in `~/.pi/agent/recipes` that imports the source recipe and uses a `{ "name": "alias" }` node. Do not copy the ready recipe's script command, defaults, mailbox, or artifacts into a second template.
199
+ 7. Declare `mailbox` for actors that accept or emit meaningful messages.
200
+ 8. Declare `artifacts` for durable outputs the coordinator should inspect.
201
+ 9. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
202
+ 10. File-backed async recipes pass child `pi -p` actors a bounded JSONL recipe context bundle by default: raw entry/import recipe records, derived `name`, import path/alias, and `"you_are_here": true` on the launching recipe node. Set `"actor_context": false` or `"off"` to suppress it for minimal prompts.
203
+ 11. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
204
+ 12. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
203
205
 
204
206
  Priority for same-id recipes:
205
207
 
@@ -208,7 +210,7 @@ Priority for same-id recipes:
208
210
  3. Explicit ad hoc user recipe file outside `~/.pi/agent/recipes`.
209
211
  4. User recipe in `~/.pi/agent/recipes/*.json` or `*.md`: highest-priority operator tool surface.
210
212
 
211
- Only matching filename ids compete. Higher priority shadows lower priority; within one priority layer, same-id JSON shadows Markdown. An invalid or `disabled: true` higher-priority recipe blocks fallback so the agent does not silently run standard-library behavior when a user override is broken or intentionally disabled.
213
+ Only matching filename ids compete. Higher priority shadows lower priority; within one priority layer, same-id JSON shadows Markdown. Same-id overrides are normal composition/delegation behavior, not startup-warning material. An invalid or `disabled: true` higher-priority recipe blocks fallback so the agent does not silently run standard-library behavior when a user override is broken or intentionally disabled.
212
214
 
213
215
  Muscle-memory lens: pi-actors has two durable executable-memory layers.
214
216
 
@@ -227,23 +229,53 @@ Cleanup rule: periodically inspect `~/.pi/agent/recipes` as the live muscle-memo
227
229
 
228
230
  `register_tool` persists trusted local capabilities as recipe files in `~/.pi/agent/recipes/*.json`; hand-authored Markdown recipes in the same directory are also discovered as tools.
229
231
 
230
- Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion. `register_tool` should create/update/delete recipe files in the user recipe root; direct file editing is allowed but is the lower-level path.
232
+ Use it when a command/template/recipe should become durable agent muscle memory. Prefer typed args or placeholder-derived args; use `update=true` for replacement and `template=null` or `template=""` for deletion. `register_tool` should create/update/delete simple recipe files in the user recipe root; direct recipe-file editing is the right path when the wrapper needs `imports` or other top-level recipe metadata not exposed by the interactive mutation API.
233
+
234
+ Ready-recipe registration patterns:
235
+
236
+ Thin delegation target shape:
237
+
238
+ ```json
239
+ {
240
+ "description": "Run a ready recipe through a local tool name.",
241
+ "args": ["source:path", "volume:int=70"],
242
+ "template": "/path/to/ready-recipe.json"
243
+ }
244
+ ```
245
+
246
+ Delegation is for one-to-one handoff: expose or call a maintained recipe directly, preserving that recipe as the source of truth. If the runtime does not yet support direct recipe references in `template`, or if you need composition, use the import-node wrapper below.
247
+
248
+ Composition/import wrapper:
249
+
250
+ ```json
251
+ {
252
+ "description": "Run the ABCd context validator through its skill recipe.",
253
+ "imports": {
254
+ "validate_context": "{agent}/skills/abcd-context/recipes/validate-context.json"
255
+ },
256
+ "args": ["path:path=."],
257
+ "template": { "name": "validate_context" }
258
+ }
259
+ ```
260
+
261
+ Use delegation or this import pattern whenever a reusable recipe already exists: packaged pi-actors components, project-local recipes, ad hoc reviewed recipe files, and especially skill-owned recipes that wrap skill scripts. The wrapper owns only the public tool name, description, optional narrowed args/defaults, and local usage metadata. The delegated/imported recipe remains the source of truth for the script path, default values, mailbox contract, artifacts, and future fixes.
231
262
 
232
263
  Tool-registration lenses are open-ended prompts for deciding what deserves durable tool status:
233
264
 
234
265
  1. **Reliability lens**: register wrappers for operations where agents commonly omit checks, run steps out of order, pass ambiguous inputs, or recover poorly from partial failure.
235
266
  2. **Safety lens**: prefer read-only diagnostics, dry-runs, preflights, confirmations, or bounded adapters around high-impact operations before registering direct action tools.
236
267
  3. **Context-affordance lens**: register tools whose mere presence in the injected capability list should steer agents toward the right operational habit.
237
- 4. **Existing-recipe lens**: scan already-authored recipes before inventing a new tool. Packaged recipes, ad hoc project recipes, and recipes co-located under skill directories are often the first candidates to copy/register into the user recipe root when they match a recurring local workflow.
238
- 5. **Composition lens**: register small semantic entrypoints over reusable recipe components instead of baking one large scenario-specific shell command into a tool.
239
- 6. **Portability lens**: keep recipe files transportable; make tool exposure a consequence of placement in `~/.pi/agent/recipes`, not recipe-owned markers or machine-local assumptions.
268
+ 4. **Existing-recipe lens**: scan already-authored recipes before inventing a new tool. Packaged recipes, ad hoc project recipes, and recipes co-located under skill directories are the first candidates to delegate to or import from a user-root wrapper when they match a recurring local workflow.
269
+ 5. **Skill-recipe lens**: when a skill ships a recipe for its script, local tools must delegate to or import that recipe instead of calling the skill script directly. This preserves the skill's maintained interface and keeps future script/default changes centralized.
270
+ 6. **Composition lens**: register small semantic entrypoints over reusable recipe components instead of baking one large scenario-specific shell command into a tool; prefer direct delegation for one recipe, imports for composed graphs.
271
+ 7. **Portability lens**: keep recipe files transportable; make tool exposure a consequence of placement in `~/.pi/agent/recipes`, not recipe-owned markers or machine-local assumptions.
240
272
 
241
273
  Default bias: register diagnostic/preflight tools before action tools, and promote existing recipes before writing new orchestration. A good persistent tool shrinks the chance of a subtle operational mistake, not just the number of keystrokes.
242
274
 
243
275
  Tool templates may be:
244
276
 
245
277
  - A foreground command template.
246
- - A file-backed recipe name/path.
278
+ - A file-backed recipe name/path for thin delegation.
247
279
  - A complete recipe body, optionally `async: true`.
248
280
 
249
281
  The user recipe root is the default tool set by location. It accepts canonical JSON recipes and literate Markdown recipes with frontmatter plus fenced `template`/`json recipe` blocks; same-id JSON shadows Markdown in the same priority layer. Packaged recipes are lower-priority standard-library components and are not tools unless copied or registered into the agent recipe root. Ideal runtime behavior is reactive: create/edit/delete recipe files, validate them, then connect valid tools or surface diagnostics without requiring agents to hand-maintain a separate registry.
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.35.0
5
+ version: 0.37.0
6
6
  ---
7
7
 
8
8
  # Swarm