@llblab/pi-actors 0.14.2 → 0.15.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 (67) hide show
  1. package/AGENTS.md +5 -1
  2. package/BACKLOG.md +18 -32
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -20
  5. package/docs/actor-messages.md +1 -1
  6. package/docs/async-runs.md +4 -4
  7. package/docs/command-templates.md +11 -11
  8. package/docs/recipe-library.md +7 -3
  9. package/docs/task-first-recipes.md +44 -43
  10. package/docs/template-recipes.md +7 -2
  11. package/docs/tool-registry.md +7 -5
  12. package/lib/actor-messages.ts +20 -7
  13. package/lib/async-runs.ts +25 -12
  14. package/lib/command-templates.ts +6 -1
  15. package/lib/config.ts +2 -2
  16. package/lib/execution.ts +9 -5
  17. package/lib/observability.ts +20 -10
  18. package/lib/prompts.ts +13 -20
  19. package/lib/tools.ts +196 -64
  20. package/package.json +17 -9
  21. package/recipes/coordinator-locker.json +46 -0
  22. package/recipes/music-player.json +16 -2
  23. package/recipes/pipeline-architect-coordinator.json +11 -3
  24. package/recipes/pipeline-artifact-bundle.json +12 -3
  25. package/recipes/pipeline-artifact-report.json +9 -3
  26. package/recipes/pipeline-artifact-write.json +9 -3
  27. package/recipes/pipeline-async-run-ops.json +18 -9
  28. package/recipes/pipeline-checkpoint-continuation.json +14 -3
  29. package/recipes/pipeline-development-tasking.json +12 -3
  30. package/recipes/pipeline-docs-maintenance.json +12 -3
  31. package/recipes/pipeline-media-library.json +12 -3
  32. package/recipes/pipeline-quorum-review.json +12 -9
  33. package/recipes/pipeline-release-readiness.json +27 -9
  34. package/recipes/pipeline-release-summary.json +89 -0
  35. package/recipes/pipeline-repo-health.json +12 -3
  36. package/recipes/pipeline-research-synthesis.json +11 -3
  37. package/recipes/pipeline-review-readiness.json +12 -6
  38. package/recipes/subagent-artifact.json +9 -3
  39. package/recipes/subagent-checkpoint.json +10 -3
  40. package/recipes/subagent-conflict-report.json +11 -3
  41. package/recipes/subagent-contradiction-map.json +11 -3
  42. package/recipes/subagent-critic.json +11 -3
  43. package/recipes/subagent-evidence-map.json +11 -3
  44. package/recipes/subagent-followup.json +10 -3
  45. package/recipes/subagent-judge.json +11 -3
  46. package/recipes/subagent-merge.json +11 -3
  47. package/recipes/subagent-message.json +8 -3
  48. package/recipes/subagent-normalize.json +11 -3
  49. package/recipes/subagent-plan.json +11 -3
  50. package/recipes/subagent-prompt.json +10 -3
  51. package/recipes/subagent-quorum.json +10 -7
  52. package/recipes/subagent-review-coordinator.json +14 -6
  53. package/recipes/subagent-review.json +11 -3
  54. package/recipes/subagent-task-card.json +11 -3
  55. package/recipes/subagent-tools.json +10 -3
  56. package/recipes/subagent-verify.json +11 -3
  57. package/recipes/subagents-prompts.json +10 -3
  58. package/recipes/utility-coordinator-lock-snapshot.json +14 -0
  59. package/recipes/utility-run-ops-snapshot.json +3 -3
  60. package/recipes/utility-skill-summary.json +14 -0
  61. package/scripts/coordinator-locker.mjs +272 -0
  62. package/scripts/music-player.mjs +2 -1
  63. package/scripts/recipe-utils.mjs +239 -81
  64. package/scripts/validate-recipe.mjs +28 -10
  65. package/skills/actors/SKILL.md +283 -0
  66. package/skills/swarm/SKILL.md +451 -0
  67. package/skills/swarm/references/development-swarm.md +596 -0
@@ -1,17 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
3
11
  import { dirname, extname, join, relative, resolve } from "node:path";
4
12
 
5
13
  function usage() {
6
14
  console.error(`Usage:
7
15
  recipe-utils.mjs run-summary <state-root>
8
- recipe-utils.mjs run-ops-snapshot <state-root> <message-file> [lines] [stale-minutes]
16
+ recipe-utils.mjs run-ops-snapshot <state-root> <run-id> [lines] [stale-minutes]
9
17
  recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
10
18
  recipe-utils.mjs changelog-section <file> <version>
11
19
  recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
12
20
  recipe-utils.mjs artifact-write <artifact-path> [create|overwrite|append]
13
21
  recipe-utils.mjs actor-message <type> [to] [from] [summary] [metadata-json] [correlation-id] [reply-to]
14
- recipe-utils.mjs package-summary <package-json>`);
22
+ recipe-utils.mjs package-summary <package-json>
23
+ recipe-utils.mjs skill-summary <skill-md> [package-json]`);
15
24
  }
16
25
 
17
26
  function fail(message) {
@@ -24,7 +33,8 @@ const MESSAGE_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
24
33
  function assertToken(value, label) {
25
34
  const normalized = String(value ?? "").trim();
26
35
  if (!normalized) fail(`${label} is required`);
27
- if (!ADDRESS_PATTERN.test(normalized)) fail(`${label} contains unsupported characters: ${value}`);
36
+ if (!ADDRESS_PATTERN.test(normalized))
37
+ fail(`${label} contains unsupported characters: ${value}`);
28
38
  return normalized;
29
39
  }
30
40
 
@@ -37,16 +47,19 @@ function validateActorAddress(address, label) {
37
47
  const rest = value.slice(separator + 1);
38
48
  if (kind === "branch") {
39
49
  const [run, branch, ...extra] = rest.split("/");
40
- if (extra.length > 0) fail(`${label} branch address has too many parts: ${address}`);
50
+ if (extra.length > 0)
51
+ fail(`${label} branch address has too many parts: ${address}`);
41
52
  return `branch:${assertToken(run, `${label} branch run`)}/${assertToken(branch, `${label} branch id`)}`;
42
53
  }
43
- if (["run", "session", "tool"].includes(kind)) return `${kind}:${assertToken(rest, label)}`;
54
+ if (["run", "session", "tool"].includes(kind))
55
+ return `${kind}:${assertToken(rest, label)}`;
44
56
  fail(`${label} has unsupported actor kind: ${kind}`);
45
57
  }
46
58
 
47
59
  function validateMessageType(type) {
48
60
  const value = String(type ?? "").trim();
49
- if (!MESSAGE_TYPE_PATTERN.test(value)) fail(`Invalid actor message type: ${type}`);
61
+ if (!MESSAGE_TYPE_PATTERN.test(value))
62
+ fail(`Invalid actor message type: ${type}`);
50
63
  return value;
51
64
  }
52
65
 
@@ -100,7 +113,13 @@ function collectRunSummary(rootValue) {
100
113
  run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
101
114
  status: getRunStatus(run, progress, result),
102
115
  recipe: run.recipe ?? run.recipe_file ?? "",
103
- updated: progress?.updatedAt ?? result?.completedAt ?? run.updated_at ?? run.completed_at ?? run.started_at ?? "",
116
+ updated:
117
+ progress?.updatedAt ??
118
+ result?.completedAt ??
119
+ run.updated_at ??
120
+ run.completed_at ??
121
+ run.started_at ??
122
+ "",
104
123
  });
105
124
  }
106
125
  rows.sort((a, b) =>
@@ -114,43 +133,82 @@ function runSummary(rootValue) {
114
133
  }
115
134
 
116
135
  function tailJsonl(fileValue, linesValue = "80") {
117
- const file = resolve(fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
136
+ const file = resolve(
137
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
138
+ );
118
139
  if (!existsSync(file)) return [];
119
140
  const lines = Number.parseInt(linesValue, 10);
120
141
  const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
121
- return readFileSync(file, "utf8").trimEnd().split("\n").filter(Boolean).slice(-count).map((line) => {
122
- try {
123
- return JSON.parse(line);
124
- } catch {
125
- return { raw: line };
126
- }
127
- });
142
+ return readFileSync(file, "utf8")
143
+ .trimEnd()
144
+ .split("\n")
145
+ .filter(Boolean)
146
+ .slice(-count)
147
+ .map((line) => {
148
+ try {
149
+ return JSON.parse(line);
150
+ } catch {
151
+ return { raw: line };
152
+ }
153
+ });
128
154
  }
129
155
 
130
- function runOpsSnapshot(rootValue, messageFileValue, linesValue = "80", staleMinutesValue = "60") {
156
+ function runOpsSnapshot(
157
+ rootValue,
158
+ runIdValue = "music",
159
+ linesValue = "80",
160
+ staleMinutesValue = "60",
161
+ ) {
131
162
  const runs = collectRunSummary(rootValue);
163
+ const root = resolve(
164
+ rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
165
+ );
166
+ const inspectedRun = String(runIdValue || "music");
167
+ const messageFile = join(root, inspectedRun, "outbox.jsonl");
132
168
  const staleMs = Number(staleMinutesValue) * 60 * 1000;
133
169
  const now = Date.now();
134
170
  const recommendations = runs.flatMap((run) => {
135
171
  const updatedMs = Date.parse(run.updated || "");
136
- const stale = Number.isFinite(updatedMs) && Number.isFinite(staleMs) && now - updatedMs > staleMs;
172
+ const stale =
173
+ Number.isFinite(updatedMs) &&
174
+ Number.isFinite(staleMs) &&
175
+ now - updatedMs > staleMs;
137
176
  if (run.status === "running" && stale) {
138
- return [{
139
- run: run.run,
140
- reason: "running-stale",
141
- suggested_message: { to: `run:${run.run}`, type: "control.stop", body: "stop" },
142
- }];
177
+ return [
178
+ {
179
+ run: run.run,
180
+ reason: "running-stale",
181
+ suggested_message: {
182
+ to: `run:${run.run}`,
183
+ type: "control.stop",
184
+ body: "stop",
185
+ },
186
+ },
187
+ ];
143
188
  }
144
189
  if (["failed", "exited", "killed"].includes(run.status)) {
145
- return [{
146
- run: run.run,
147
- reason: `terminal-${run.status}`,
148
- suggested_inspect: { target: `run:${run.run}`, view: "tail" },
149
- }];
190
+ return [
191
+ {
192
+ run: run.run,
193
+ reason: `terminal-${run.status}`,
194
+ suggested_inspect: { target: `run:${run.run}`, view: "tail" },
195
+ },
196
+ ];
150
197
  }
151
198
  return [];
152
199
  });
153
- console.log(JSON.stringify({ runs, messages: tailJsonl(messageFileValue, linesValue), recommendations }, null, 2));
200
+ console.log(
201
+ JSON.stringify(
202
+ {
203
+ runs,
204
+ inspectedRun,
205
+ messages: tailJsonl(messageFile, linesValue),
206
+ recommendations,
207
+ },
208
+ null,
209
+ 2,
210
+ ),
211
+ );
154
212
  }
155
213
 
156
214
  function playlist(
@@ -177,23 +235,38 @@ function playlist(
177
235
  else console.log(files.join("\n"));
178
236
  }
179
237
 
180
- function artifactManifest(pathValue, title = "Artifact", status = "draft", summary = "") {
181
- const path = resolve(pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
238
+ function artifactManifest(
239
+ pathValue,
240
+ title = "Artifact",
241
+ status = "draft",
242
+ summary = "",
243
+ ) {
244
+ const path = resolve(
245
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
246
+ );
182
247
  const exists = existsSync(path);
183
248
  const stat = exists ? statSync(path) : undefined;
184
- console.log(JSON.stringify({
185
- title,
186
- status,
187
- path,
188
- exists,
189
- bytes: stat?.size ?? 0,
190
- modified: stat?.mtime?.toISOString?.() ?? null,
191
- summary,
192
- }, null, 2));
249
+ console.log(
250
+ JSON.stringify(
251
+ {
252
+ title,
253
+ status,
254
+ path,
255
+ exists,
256
+ bytes: stat?.size ?? 0,
257
+ modified: stat?.mtime?.toISOString?.() ?? null,
258
+ summary,
259
+ },
260
+ null,
261
+ 2,
262
+ ),
263
+ );
193
264
  }
194
265
 
195
266
  function artifactWrite(pathValue, mode = "create") {
196
- const path = resolve(pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
267
+ const path = resolve(
268
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
269
+ );
197
270
  if (!["create", "overwrite", "append"].includes(mode)) {
198
271
  fail(`Invalid artifact write mode: ${mode}`);
199
272
  }
@@ -205,10 +278,20 @@ function artifactWrite(pathValue, mode = "create") {
205
278
  if (mode === "append") appendFileSync(path, content);
206
279
  else writeFileSync(path, content, "utf8");
207
280
  const stat = statSync(path);
208
- console.log(JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2));
281
+ console.log(
282
+ JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2),
283
+ );
209
284
  }
210
285
 
211
- function actorMessage(type = "event", to = "coordinator", from = "run:{run_id}", summary = "", metadataValue = "", correlationId = "", replyTo = "") {
286
+ function actorMessage(
287
+ type = "event",
288
+ to = "coordinator",
289
+ from = "run:{run_id}",
290
+ summary = "",
291
+ metadataValue = "",
292
+ correlationId = "",
293
+ replyTo = "",
294
+ ) {
212
295
  const messageType = validateMessageType(type);
213
296
  const messageTo = validateActorAddress(to, "message.to");
214
297
  const messageFrom = validateActorAddress(from, "message.from");
@@ -216,7 +299,8 @@ function actorMessage(type = "event", to = "coordinator", from = "run:{run_id}",
216
299
  if (metadataValue) {
217
300
  try {
218
301
  const parsed = JSON.parse(metadataValue);
219
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) metadata = parsed;
302
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
303
+ metadata = parsed;
220
304
  else fail("Actor message metadata must be a JSON object");
221
305
  } catch (error) {
222
306
  fail(`Invalid actor message metadata JSON: ${error.message}`);
@@ -232,47 +316,114 @@ function actorMessage(type = "event", to = "coordinator", from = "run:{run_id}",
232
316
  body = bodyText;
233
317
  }
234
318
  }
235
- console.log(JSON.stringify({
236
- to: messageTo,
237
- from: messageFrom,
238
- type: messageType,
239
- summary: summary || messageType,
240
- body,
241
- ...(correlationId ? { correlation_id: correlationId } : {}),
242
- ...(replyTo ? { reply_to: replyTo } : {}),
243
- metadata,
244
- }, null, 2));
319
+ console.log(
320
+ JSON.stringify(
321
+ {
322
+ to: messageTo,
323
+ from: messageFrom,
324
+ type: messageType,
325
+ summary: summary || messageType,
326
+ body,
327
+ ...(correlationId ? { correlation_id: correlationId } : {}),
328
+ ...(replyTo ? { reply_to: replyTo } : {}),
329
+ metadata,
330
+ },
331
+ null,
332
+ 2,
333
+ ),
334
+ );
245
335
  }
246
336
 
247
337
  function packageSummary(fileValue = "package.json") {
248
- const file = resolve(fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
338
+ const file = resolve(
339
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
340
+ );
249
341
  const pkg = readJson(file);
250
342
  if (!pkg) fail(`Package JSON not found or invalid: ${fileValue}`);
251
- const scripts = pkg.scripts && typeof pkg.scripts === "object"
252
- ? Object.keys(pkg.scripts).sort()
253
- : [];
254
- const dependencies = pkg.dependencies && typeof pkg.dependencies === "object"
255
- ? Object.keys(pkg.dependencies).sort()
256
- : [];
257
- const devDependencies = pkg.devDependencies && typeof pkg.devDependencies === "object"
258
- ? Object.keys(pkg.devDependencies).sort()
259
- : [];
260
- console.log(JSON.stringify({
261
- name: pkg.name ?? "",
262
- version: pkg.version ?? "",
263
- type: pkg.type ?? "",
264
- private: Boolean(pkg.private),
265
- packageManager: pkg.packageManager ?? "",
266
- files: Array.isArray(pkg.files) ? pkg.files : [],
267
- bin: pkg.bin ?? null,
268
- main: pkg.main ?? "",
269
- exports: pkg.exports ?? null,
270
- scripts,
271
- dependencyCount: dependencies.length,
272
- devDependencyCount: devDependencies.length,
273
- dependencies,
274
- devDependencies,
275
- }, null, 2));
343
+ const scripts =
344
+ pkg.scripts && typeof pkg.scripts === "object"
345
+ ? Object.keys(pkg.scripts).sort()
346
+ : [];
347
+ const dependencies =
348
+ pkg.dependencies && typeof pkg.dependencies === "object"
349
+ ? Object.keys(pkg.dependencies).sort()
350
+ : [];
351
+ const devDependencies =
352
+ pkg.devDependencies && typeof pkg.devDependencies === "object"
353
+ ? Object.keys(pkg.devDependencies).sort()
354
+ : [];
355
+ console.log(
356
+ JSON.stringify(
357
+ {
358
+ name: pkg.name ?? "",
359
+ version: pkg.version ?? "",
360
+ type: pkg.type ?? "",
361
+ private: Boolean(pkg.private),
362
+ packageManager: pkg.packageManager ?? "",
363
+ files: Array.isArray(pkg.files) ? pkg.files : [],
364
+ bin: pkg.bin ?? null,
365
+ main: pkg.main ?? "",
366
+ exports: pkg.exports ?? null,
367
+ scripts,
368
+ dependencyCount: dependencies.length,
369
+ devDependencyCount: devDependencies.length,
370
+ dependencies,
371
+ devDependencies,
372
+ },
373
+ null,
374
+ 2,
375
+ ),
376
+ );
377
+ }
378
+
379
+ function parseSkillFrontmatter(content) {
380
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
381
+ const fields = {};
382
+ for (const line of frontmatter.split(/\r?\n/)) {
383
+ const match = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
384
+ if (match) fields[match[1]] = match[2].trim();
385
+ }
386
+ return { frontmatter, fields };
387
+ }
388
+
389
+ function skillSummary(skillValue, packageValue = "package.json") {
390
+ const skillFile = resolve(
391
+ skillValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
392
+ );
393
+ const packageFile = resolve(
394
+ packageValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
395
+ );
396
+ const content = readFileSync(skillFile, "utf8");
397
+ const pkg = readJson(packageFile) ?? {};
398
+ const { frontmatter, fields } = parseSkillFrontmatter(content);
399
+ const scalarLines = frontmatter
400
+ .split(/\r?\n/)
401
+ .filter((line) => /^\w+:\s*\S/.test(line));
402
+ const extraColonLines = scalarLines.filter(
403
+ (line) => (line.match(/:/g) ?? []).length > 1,
404
+ );
405
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
406
+ const version =
407
+ fields.version ??
408
+ frontmatter.match(/^\s+version:\s*([^\n]+)\s*$/m)?.[1]?.trim() ??
409
+ "";
410
+ console.log(
411
+ JSON.stringify(
412
+ {
413
+ path: skillValue,
414
+ name: fields.name ?? "",
415
+ description: fields.description ?? "",
416
+ version,
417
+ packageVersion: pkg.version ?? "",
418
+ versionMatchesPackage: version === pkg.version,
419
+ frontmatterExtraColonLines: extraColonLines,
420
+ bodyLineCount: body.split(/\r?\n/).length,
421
+ headings: body.split(/\r?\n/).filter((line) => /^#{1,6}\s/.test(line)),
422
+ },
423
+ null,
424
+ 2,
425
+ ),
426
+ );
276
427
  }
277
428
 
278
429
  function changelogSection(fileValue, version) {
@@ -301,7 +452,12 @@ if (!command) {
301
452
  if (command === "run-summary")
302
453
  runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
303
454
  else if (command === "run-ops-snapshot")
304
- runOpsSnapshot(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs", args[1] ?? "~/.pi/agent/tmp/pi-actors/runs/music/outbox.jsonl", args[2], args[3]);
455
+ runOpsSnapshot(
456
+ args[0] ?? "~/.pi/agent/tmp/pi-actors/runs",
457
+ args[1] ?? "music",
458
+ args[2],
459
+ args[3],
460
+ );
305
461
  else if (command === "playlist")
306
462
  playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
307
463
  else if (command === "changelog-section")
@@ -314,6 +470,8 @@ else if (command === "actor-message")
314
470
  actorMessage(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
315
471
  else if (command === "package-summary")
316
472
  packageSummary(args[0] ?? "package.json");
473
+ else if (command === "skill-summary")
474
+ skillSummary(args[0] ?? "skills/actors/SKILL.md", args[1] ?? "package.json");
317
475
  else {
318
476
  usage();
319
477
  fail(`Unknown command: ${command}`);
@@ -13,7 +13,9 @@ Validates one template recipe file, or all *.json files in a directory when --al
13
13
  }
14
14
 
15
15
  function expandPath(value) {
16
- return resolve(String(value).replace(/^~(?=\/|$)/, process.env.HOME ?? homedir()));
16
+ return resolve(
17
+ String(value).replace(/^~(?=\/|$)/, process.env.HOME ?? homedir()),
18
+ );
17
19
  }
18
20
 
19
21
  function templateKind(template) {
@@ -21,7 +23,8 @@ function templateKind(template) {
21
23
  if (Array.isArray(template)) return "sequence";
22
24
  if (template && typeof template === "object") {
23
25
  if (typeof template.template === "string") return "leaf";
24
- if (Array.isArray(template.template)) return template.parallel === true ? "parallel" : "sequence";
26
+ if (Array.isArray(template.template))
27
+ return template.parallel === true ? "parallel" : "sequence";
25
28
  if (template.parallel === true) return "parallel";
26
29
  return "object";
27
30
  }
@@ -32,7 +35,8 @@ function recipeFiles(target, all) {
32
35
  if (!existsSync(target)) throw new Error(`Recipe path not found: ${target}`);
33
36
  const stat = statSync(target);
34
37
  if (stat.isFile()) return [target];
35
- if (!stat.isDirectory()) throw new Error(`Recipe path is not a file or directory: ${target}`);
38
+ if (!stat.isDirectory())
39
+ throw new Error(`Recipe path is not a file or directory: ${target}`);
36
40
  if (!all) throw new Error("Directory validation requires --all.");
37
41
  return readdirSync(target)
38
42
  .filter((file) => file.endsWith(".json"))
@@ -43,19 +47,33 @@ function recipeFiles(target, all) {
43
47
  function validateFile(file) {
44
48
  try {
45
49
  const config = readResolvedRecipeConfig(file);
46
- if (!config?.template) throw new Error("Recipe must define a non-empty template.");
50
+ if (!config?.template)
51
+ throw new Error("Recipe must define a non-empty template.");
47
52
  return {
48
53
  file,
49
54
  ok: true,
50
55
  name: config.name ?? "",
51
56
  async: Boolean(config.async),
52
57
  args: Array.isArray(config.args) ? config.args : [],
53
- defaults: config.defaults && typeof config.defaults === "object" ? Object.keys(config.defaults).sort() : [],
54
- imports: config.imports && typeof config.imports === "object" ? Object.keys(config.imports).sort() : [],
55
- mailbox: config.mailbox && typeof config.mailbox === "object" ? {
56
- accepts: Array.isArray(config.mailbox.accepts) ? config.mailbox.accepts : [],
57
- emits: Array.isArray(config.mailbox.emits) ? config.mailbox.emits : [],
58
- } : undefined,
58
+ defaults:
59
+ config.defaults && typeof config.defaults === "object"
60
+ ? Object.keys(config.defaults).sort()
61
+ : [],
62
+ imports:
63
+ config.imports && typeof config.imports === "object"
64
+ ? Object.keys(config.imports).sort()
65
+ : [],
66
+ mailbox:
67
+ config.mailbox && typeof config.mailbox === "object"
68
+ ? {
69
+ accepts: Array.isArray(config.mailbox.accepts)
70
+ ? config.mailbox.accepts
71
+ : [],
72
+ emits: Array.isArray(config.mailbox.emits)
73
+ ? config.mailbox.emits
74
+ : [],
75
+ }
76
+ : undefined,
59
77
  template: templateKind(config.template),
60
78
  };
61
79
  } catch (error) {