@llblab/pi-actors 0.33.0 → 0.34.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 (155) hide show
  1. package/AGENTS.md +22 -11
  2. package/BACKLOG.md +3 -34
  3. package/CHANGELOG.md +14 -0
  4. package/dist/index.js +10 -10
  5. package/dist/lib/async-runs.d.ts +23 -75
  6. package/dist/lib/async-runs.js +49 -801
  7. package/dist/lib/command-templates.d.ts +3 -3
  8. package/dist/lib/command-templates.js +3 -3
  9. package/dist/lib/config.d.ts +2 -2
  10. package/dist/lib/config.js +5 -5
  11. package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
  12. package/dist/lib/{output.js → execution-output.js} +3 -3
  13. package/dist/lib/execution.js +1 -1
  14. package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
  15. package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
  16. package/dist/lib/mailbox-loop.d.ts +3 -3
  17. package/dist/lib/mailbox-loop.js +3 -3
  18. package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
  19. package/dist/lib/{actor-messages.js → messages.js} +3 -3
  20. package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
  21. package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
  22. package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
  23. package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
  24. package/dist/lib/registry.d.ts +1 -1
  25. package/dist/lib/registry.js +22 -22
  26. package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
  27. package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
  28. package/dist/lib/runs-artifacts.d.ts +21 -0
  29. package/dist/lib/runs-artifacts.js +68 -0
  30. package/dist/lib/runs-control.d.ts +13 -0
  31. package/dist/lib/runs-control.js +59 -0
  32. package/dist/lib/runs-identity.d.ts +5 -0
  33. package/dist/lib/runs-identity.js +11 -0
  34. package/dist/lib/runs-index.d.ts +18 -0
  35. package/dist/lib/runs-index.js +119 -0
  36. package/dist/lib/runs-mailbox.d.ts +25 -0
  37. package/dist/lib/runs-mailbox.js +146 -0
  38. package/dist/lib/runs-messages.d.ts +15 -0
  39. package/dist/lib/runs-messages.js +179 -0
  40. package/dist/lib/runs-outbox.d.ts +41 -0
  41. package/dist/lib/runs-outbox.js +87 -0
  42. package/dist/lib/runs-process.d.ts +7 -0
  43. package/dist/lib/runs-process.js +36 -0
  44. package/dist/lib/runs-retention.d.ts +8 -0
  45. package/dist/lib/runs-retention.js +56 -0
  46. package/dist/lib/runs-start.d.ts +9 -0
  47. package/dist/lib/runs-start.js +67 -0
  48. package/dist/lib/runs-status.d.ts +8 -0
  49. package/dist/lib/runs-status.js +60 -0
  50. package/dist/lib/runtime.d.ts +2 -2
  51. package/dist/lib/runtime.js +5 -5
  52. package/dist/lib/schema.d.ts +9 -0
  53. package/dist/lib/schema.js +40 -0
  54. package/dist/lib/tools-access.d.ts +19 -0
  55. package/dist/lib/tools-access.js +44 -0
  56. package/dist/lib/tools-inspect.d.ts +11 -0
  57. package/dist/lib/tools-inspect.js +534 -0
  58. package/dist/lib/tools-local.d.ts +14 -0
  59. package/dist/lib/tools-local.js +156 -0
  60. package/dist/lib/tools-mailbox.d.ts +8 -0
  61. package/dist/lib/tools-mailbox.js +48 -0
  62. package/dist/lib/tools-message.d.ts +9 -0
  63. package/dist/lib/tools-message.js +428 -0
  64. package/dist/lib/tools-register.d.ts +18 -0
  65. package/dist/lib/tools-register.js +40 -0
  66. package/dist/lib/tools-response.d.ts +21 -0
  67. package/dist/lib/tools-response.js +219 -0
  68. package/dist/lib/tools-spawn.d.ts +12 -0
  69. package/dist/lib/tools-spawn.js +159 -0
  70. package/dist/lib/tools.d.ts +8 -39
  71. package/dist/lib/tools.js +11 -1490
  72. package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
  73. package/dist/scripts/actor-worker.mjs +190 -7
  74. package/dist/scripts/async-runner.mjs +171 -8
  75. package/dist/scripts/build-dist.mjs +1 -1
  76. package/dist/scripts/conformance.mjs +3 -3
  77. package/dist/scripts/coordinator.mjs +733 -11
  78. package/dist/scripts/locker.mjs +345 -16
  79. package/dist/scripts/recipe-utils.mjs +478 -14
  80. package/dist/scripts/validate-recipe.mjs +110 -10
  81. package/dist/skills/actors/SKILL.md +1 -1
  82. package/dist/skills/swarm/SKILL.md +1 -1
  83. package/docs/recipe-library.md +1 -1
  84. package/docs/template-recipes.md +1 -1
  85. package/index.ts +11 -18
  86. package/lib/async-runs.ts +124 -1016
  87. package/lib/command-templates.ts +3 -3
  88. package/lib/config.ts +7 -7
  89. package/lib/{output.ts → execution-output.ts} +3 -3
  90. package/lib/execution.ts +5 -1
  91. package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
  92. package/lib/mailbox-loop.ts +3 -3
  93. package/lib/{actor-messages.ts → messages.ts} +3 -3
  94. package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
  95. package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
  96. package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
  97. package/lib/registry.ts +37 -24
  98. package/lib/{actor-rooms.ts → rooms.ts} +4 -4
  99. package/lib/runs-artifacts.ts +96 -0
  100. package/lib/runs-control.ts +85 -0
  101. package/lib/runs-identity.ts +14 -0
  102. package/lib/runs-index.ts +156 -0
  103. package/lib/runs-mailbox.ts +208 -0
  104. package/lib/runs-messages.ts +252 -0
  105. package/lib/runs-outbox.ts +144 -0
  106. package/lib/runs-process.ts +51 -0
  107. package/lib/runs-retention.ts +77 -0
  108. package/lib/runs-start.ts +98 -0
  109. package/lib/runs-status.ts +78 -0
  110. package/lib/runtime.ts +6 -6
  111. package/lib/schema.ts +55 -0
  112. package/lib/tools-access.ts +68 -0
  113. package/lib/tools-inspect.ts +735 -0
  114. package/lib/tools-local.ts +234 -0
  115. package/lib/tools-mailbox.ts +56 -0
  116. package/lib/tools-message.ts +569 -0
  117. package/lib/tools-register.ts +64 -0
  118. package/lib/tools-response.ts +267 -0
  119. package/lib/tools-spawn.ts +240 -0
  120. package/lib/tools.ts +20 -2040
  121. package/package.json +1 -1
  122. package/recipes/utility-coordinator-lock-snapshot.json +1 -1
  123. package/scripts/actor-worker.mjs +190 -7
  124. package/scripts/async-runner.mjs +171 -8
  125. package/scripts/build-dist.mjs +1 -1
  126. package/scripts/conformance.mjs +3 -3
  127. package/scripts/coordinator.mjs +733 -11
  128. package/scripts/locker.mjs +345 -16
  129. package/scripts/recipe-utils.mjs +478 -14
  130. package/scripts/validate-recipe.mjs +110 -10
  131. package/skills/actors/SKILL.md +1 -1
  132. package/skills/swarm/SKILL.md +1 -1
  133. package/dist/lib/actor-worker.d.ts +0 -16
  134. package/dist/lib/actor-worker.js +0 -134
  135. package/dist/lib/async-runner.d.ts +0 -5
  136. package/dist/lib/async-runner.js +0 -134
  137. package/dist/lib/coordinator.d.ts +0 -5
  138. package/dist/lib/coordinator.js +0 -660
  139. package/dist/lib/locker.d.ts +0 -5
  140. package/dist/lib/locker.js +0 -310
  141. package/dist/lib/recipe-utils.d.ts +0 -5
  142. package/dist/lib/recipe-utils.js +0 -385
  143. package/dist/lib/validate-recipe.d.ts +0 -6
  144. package/dist/lib/validate-recipe.js +0 -104
  145. package/lib/actor-worker.ts +0 -174
  146. package/lib/async-runner.ts +0 -173
  147. package/lib/coordinator.ts +0 -742
  148. package/lib/locker.ts +0 -340
  149. package/lib/recipe-utils.ts +0 -486
  150. package/lib/validate-recipe.ts +0 -110
  151. /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
  152. /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
  153. /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
  154. /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
  155. /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
@@ -1,27 +1,491 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Recipe utility command bundle shim.
5
- *
6
- * Runtime logic lives in lib/recipe-utils.ts and is compiled to
7
- * dist/lib/recipe-utils.js for installed JS-only packages.
4
+ * Recipe utility command bundle.
5
+ * Zones: recipe utilities, deterministic helper subcommands
8
6
  */
9
7
 
10
- import { existsSync } from "node:fs";
11
- import { dirname, join } from "node:path";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
8
+ // @ts-nocheck
13
9
 
14
- function packageRoot() {
15
- return dirname(dirname(fileURLToPath(import.meta.url)));
10
+ import {
11
+ appendFileSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { dirname, extname, join, relative, resolve } from "node:path";
20
+
21
+ function usage() {
22
+ console.error(`Usage:
23
+ recipe-utils.mjs run-summary <state-root>
24
+ recipe-utils.mjs run-ops-snapshot <state-root> <run-id> [lines] [stale-minutes]
25
+ recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
26
+ recipe-utils.mjs changelog-section <file> <version>
27
+ recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
28
+ recipe-utils.mjs artifact-write <artifact-path> [create|overwrite|append]
29
+ recipe-utils.mjs actor-message <type> [to] [from] [summary] [metadata-json] [correlation-id] [reply-to]
30
+ recipe-utils.mjs package-summary <package-json>
31
+ recipe-utils.mjs skill-summary <skill-md> [package-json]`);
32
+ }
33
+
34
+ function fail(message) {
35
+ console.error(message);
36
+ process.exit(1);
37
+ }
38
+
39
+ const ADDRESS_PATTERN = /^[A-Za-z0-9_.-]+$/;
40
+ const MESSAGE_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
41
+ function assertToken(value, label) {
42
+ const normalized = String(value ?? "").trim();
43
+ if (!normalized) fail(`${label} is required`);
44
+ if (!ADDRESS_PATTERN.test(normalized))
45
+ fail(`${label} contains unsupported characters: ${value}`);
46
+ return normalized;
47
+ }
48
+
49
+ function validateActorAddress(address, label) {
50
+ const value = String(address ?? "").trim();
51
+ if (value === "coordinator") return value;
52
+ const separator = value.indexOf(":");
53
+ if (separator < 0) fail(`${label} must include an actor kind: ${address}`);
54
+ const kind = value.slice(0, separator);
55
+ const rest = value.slice(separator + 1);
56
+ if (kind === "branch") {
57
+ const [run, branch, ...extra] = rest.split("/");
58
+ if (extra.length > 0)
59
+ fail(`${label} branch address has too many parts: ${address}`);
60
+ return `branch:${assertToken(run, `${label} branch run`)}/${assertToken(branch, `${label} branch id`)}`;
61
+ }
62
+ if (["run", "session", "tool"].includes(kind))
63
+ return `${kind}:${assertToken(rest, label)}`;
64
+ fail(`${label} has unsupported actor kind: ${kind}`);
65
+ }
66
+
67
+ function validateMessageType(type) {
68
+ const value = String(type ?? "").trim();
69
+ if (!MESSAGE_TYPE_PATTERN.test(value))
70
+ fail(`Invalid actor message type: ${type}`);
71
+ return value;
72
+ }
73
+
74
+ function walkFiles(dir, maxDepth = 2, depth = 0, out = []) {
75
+ if (depth > maxDepth || !existsSync(dir)) return out;
76
+ for (const entry of readdirSync(dir)) {
77
+ const path = join(dir, entry);
78
+ const stat = statSync(path);
79
+ if (stat.isDirectory()) walkFiles(path, maxDepth, depth + 1, out);
80
+ else if (stat.isFile()) out.push(path);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ function readJson(file) {
86
+ if (!existsSync(file)) return undefined;
87
+ try {
88
+ return JSON.parse(readFileSync(file, "utf8"));
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ function getRunStatus(run, progress, result) {
95
+ if (progress?.phase) return progress.phase;
96
+ if (result?.code !== undefined) return result.code === 0 ? "done" : "failed";
97
+ return run.status ?? "unknown";
98
+ }
99
+
100
+ function collectRunSummary(rootValue) {
101
+ const root = resolve(
102
+ rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
103
+ );
104
+ const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
105
+ const rows = [];
106
+ for (const file of files) {
107
+ const run = readJson(file);
108
+ if (!run) {
109
+ rows.push({
110
+ run: relative(root, file),
111
+ status: "invalid-json",
112
+ recipe: "",
113
+ updated: "",
114
+ });
115
+ continue;
116
+ }
117
+ const runDir = dirname(file);
118
+ const progress = readJson(join(runDir, "progress.json"));
119
+ const result = readJson(join(runDir, "result.json"));
120
+ rows.push({
121
+ run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
122
+ status: getRunStatus(run, progress, result),
123
+ recipe: run.recipe ?? run.recipe_file ?? "",
124
+ updated:
125
+ progress?.updatedAt ??
126
+ result?.completedAt ??
127
+ run.updated_at ??
128
+ run.completed_at ??
129
+ run.started_at ??
130
+ "",
131
+ });
132
+ }
133
+ rows.sort((a, b) =>
134
+ `${a.status}:${a.run}`.localeCompare(`${b.status}:${b.run}`),
135
+ );
136
+ return rows;
137
+ }
138
+
139
+ function runSummary(rootValue) {
140
+ console.log(JSON.stringify(collectRunSummary(rootValue), null, 2));
141
+ }
142
+
143
+ function tailJsonl(fileValue, linesValue = "80") {
144
+ const file = resolve(
145
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
146
+ );
147
+ if (!existsSync(file)) return [];
148
+ const lines = Number.parseInt(linesValue, 10);
149
+ const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
150
+ return readFileSync(file, "utf8")
151
+ .trimEnd()
152
+ .split("\n")
153
+ .filter(Boolean)
154
+ .slice(-count)
155
+ .map((line) => {
156
+ try {
157
+ return JSON.parse(line);
158
+ } catch {
159
+ return { raw: line };
160
+ }
161
+ });
16
162
  }
17
163
 
18
- function mainModulePath() {
19
- const root = packageRoot();
20
- const compiled = join(root, "dist", "lib", "recipe-utils.js");
21
- return existsSync(compiled) ? compiled : join(root, "lib", "recipe-utils.ts");
164
+ function runOpsSnapshot(
165
+ rootValue,
166
+ runIdValue = "music",
167
+ linesValue = "80",
168
+ staleMinutesValue = "60",
169
+ ) {
170
+ const runs = collectRunSummary(rootValue);
171
+ const root = resolve(
172
+ rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
173
+ );
174
+ const inspectedRun = String(runIdValue || "music");
175
+ const messageFile = join(root, inspectedRun, "outbox.jsonl");
176
+ const staleMs = Number(staleMinutesValue) * 60 * 1000;
177
+ const now = Date.now();
178
+ const recommendations = runs.flatMap((run) => {
179
+ const updatedMs = Date.parse(run.updated || "");
180
+ const stale =
181
+ Number.isFinite(updatedMs) &&
182
+ Number.isFinite(staleMs) &&
183
+ now - updatedMs > staleMs;
184
+ if (run.status === "running" && stale) {
185
+ return [
186
+ {
187
+ run: run.run,
188
+ reason: "running-stale",
189
+ suggested_message: {
190
+ to: `run:${run.run}`,
191
+ type: "control.stop",
192
+ body: "stop",
193
+ },
194
+ },
195
+ ];
196
+ }
197
+ if (["failed", "exited", "killed"].includes(run.status)) {
198
+ return [
199
+ {
200
+ run: run.run,
201
+ reason: `terminal-${run.status}`,
202
+ suggested_inspect: { target: `run:${run.run}`, view: "tail" },
203
+ },
204
+ ];
205
+ }
206
+ return [];
207
+ });
208
+ console.log(
209
+ JSON.stringify(
210
+ {
211
+ runs,
212
+ inspectedRun,
213
+ messages: tailJsonl(messageFile, linesValue),
214
+ recommendations,
215
+ },
216
+ null,
217
+ 2,
218
+ ),
219
+ );
22
220
  }
23
221
 
24
- const { runRecipeUtils } = await import(pathToFileURL(mainModulePath()).href);
222
+ function playlist(
223
+ sourceValue,
224
+ extensionsValue = ".mp3,.ogg,.wav,.flac,.m4a",
225
+ maxDepthValue = "2",
226
+ outputMode = "paths",
227
+ ) {
228
+ const source = resolve(
229
+ sourceValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
230
+ );
231
+ const maxDepth = Number.parseInt(maxDepthValue, 10);
232
+ const extensions = new Set(
233
+ extensionsValue
234
+ .split(",")
235
+ .map((item) => item.trim().toLowerCase())
236
+ .filter(Boolean),
237
+ );
238
+ const files = walkFiles(source, Number.isFinite(maxDepth) ? maxDepth : 2)
239
+ .filter((file) => extensions.has(extname(file).toLowerCase()))
240
+ .sort((a, b) => a.localeCompare(b));
241
+ if (outputMode === "m3u") console.log(["#EXTM3U", ...files].join("\n"));
242
+ else if (outputMode === "inline") console.log(files.join("|"));
243
+ else console.log(files.join("\n"));
244
+ }
245
+
246
+ function artifactManifest(
247
+ pathValue,
248
+ title = "Artifact",
249
+ status = "draft",
250
+ summary = "",
251
+ ) {
252
+ const path = resolve(
253
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
254
+ );
255
+ const exists = existsSync(path);
256
+ const stat = exists ? statSync(path) : undefined;
257
+ console.log(
258
+ JSON.stringify(
259
+ {
260
+ title,
261
+ status,
262
+ path,
263
+ exists,
264
+ bytes: stat?.size ?? 0,
265
+ modified: stat?.mtime?.toISOString?.() ?? null,
266
+ summary,
267
+ },
268
+ null,
269
+ 2,
270
+ ),
271
+ );
272
+ }
273
+
274
+ function artifactWrite(pathValue, mode = "create") {
275
+ const path = resolve(
276
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
277
+ );
278
+ if (!["create", "overwrite", "append"].includes(mode)) {
279
+ fail(`Invalid artifact write mode: ${mode}`);
280
+ }
281
+ if (mode === "create" && existsSync(path)) {
282
+ fail(`Artifact already exists: ${path}`);
283
+ }
284
+ const content = readFileSync(0, "utf8");
285
+ mkdirSync(dirname(path), { recursive: true });
286
+ if (mode === "append") appendFileSync(path, content);
287
+ else writeFileSync(path, content, "utf8");
288
+ const stat = statSync(path);
289
+ console.log(
290
+ JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2),
291
+ );
292
+ }
293
+
294
+ function actorMessage(
295
+ type = "event",
296
+ to = "coordinator",
297
+ from = "run:{run_id}",
298
+ summary = "",
299
+ metadataValue = "",
300
+ correlationId = "",
301
+ replyTo = "",
302
+ ) {
303
+ const messageType = validateMessageType(type);
304
+ const messageTo = validateActorAddress(to, "message.to");
305
+ const messageFrom = validateActorAddress(from, "message.from");
306
+ let metadata = {};
307
+ if (metadataValue) {
308
+ try {
309
+ const parsed = JSON.parse(metadataValue);
310
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
311
+ metadata = parsed;
312
+ else fail("Actor message metadata must be a JSON object");
313
+ } catch (error) {
314
+ fail(`Invalid actor message metadata JSON: ${error.message}`);
315
+ }
316
+ }
317
+ const bodyText = readFileSync(0, "utf8");
318
+ const trimmed = bodyText.trim();
319
+ let body = bodyText;
320
+ if (trimmed) {
321
+ try {
322
+ body = JSON.parse(trimmed);
323
+ } catch {
324
+ body = bodyText;
325
+ }
326
+ }
327
+ console.log(
328
+ JSON.stringify(
329
+ {
330
+ to: messageTo,
331
+ from: messageFrom,
332
+ type: messageType,
333
+ summary: summary || messageType,
334
+ body,
335
+ ...(correlationId ? { correlation_id: correlationId } : {}),
336
+ ...(replyTo ? { reply_to: replyTo } : {}),
337
+ metadata,
338
+ },
339
+ null,
340
+ 2,
341
+ ),
342
+ );
343
+ }
344
+
345
+ function packageSummary(fileValue = "package.json") {
346
+ const file = resolve(
347
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
348
+ );
349
+ const pkg = readJson(file);
350
+ if (!pkg) fail(`Package JSON not found or invalid: ${fileValue}`);
351
+ const scripts =
352
+ pkg.scripts && typeof pkg.scripts === "object"
353
+ ? Object.keys(pkg.scripts).sort()
354
+ : [];
355
+ const dependencies =
356
+ pkg.dependencies && typeof pkg.dependencies === "object"
357
+ ? Object.keys(pkg.dependencies).sort()
358
+ : [];
359
+ const devDependencies =
360
+ pkg.devDependencies && typeof pkg.devDependencies === "object"
361
+ ? Object.keys(pkg.devDependencies).sort()
362
+ : [];
363
+ console.log(
364
+ JSON.stringify(
365
+ {
366
+ name: pkg.name ?? "",
367
+ version: pkg.version ?? "",
368
+ type: pkg.type ?? "",
369
+ private: Boolean(pkg.private),
370
+ packageManager: pkg.packageManager ?? "",
371
+ files: Array.isArray(pkg.files) ? pkg.files : [],
372
+ bin: pkg.bin ?? null,
373
+ main: pkg.main ?? "",
374
+ exports: pkg.exports ?? null,
375
+ scripts,
376
+ dependencyCount: dependencies.length,
377
+ devDependencyCount: devDependencies.length,
378
+ dependencies,
379
+ devDependencies,
380
+ },
381
+ null,
382
+ 2,
383
+ ),
384
+ );
385
+ }
386
+
387
+ function parseSkillFrontmatter(content) {
388
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
389
+ const fields = {};
390
+ for (const line of frontmatter.split(/\r?\n/)) {
391
+ const match = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
392
+ if (match) fields[match[1]] = match[2].trim();
393
+ }
394
+ return { frontmatter, fields };
395
+ }
396
+
397
+ function skillSummary(skillValue, packageValue = "package.json") {
398
+ const skillFile = resolve(
399
+ skillValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
400
+ );
401
+ const packageFile = resolve(
402
+ packageValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
403
+ );
404
+ const content = readFileSync(skillFile, "utf8");
405
+ const pkg = readJson(packageFile) ?? {};
406
+ const { frontmatter, fields } = parseSkillFrontmatter(content);
407
+ const scalarLines = frontmatter
408
+ .split(/\r?\n/)
409
+ .filter((line) => /^\w+:\s*\S/.test(line));
410
+ const extraColonLines = scalarLines.filter(
411
+ (line) => (line.match(/:/g) ?? []).length > 1,
412
+ );
413
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
414
+ const version =
415
+ fields.version ??
416
+ frontmatter.match(/^\s+version:\s*([^\n]+)\s*$/m)?.[1]?.trim() ??
417
+ "";
418
+ console.log(
419
+ JSON.stringify(
420
+ {
421
+ path: skillValue,
422
+ name: fields.name ?? "",
423
+ description: fields.description ?? "",
424
+ version,
425
+ packageVersion: pkg.version ?? "",
426
+ versionMatchesPackage: version === pkg.version,
427
+ frontmatterExtraColonLines: extraColonLines,
428
+ bodyLineCount: body.split(/\r?\n/).length,
429
+ headings: body.split(/\r?\n/).filter((line) => /^#{1,6}\s/.test(line)),
430
+ },
431
+ null,
432
+ 2,
433
+ ),
434
+ );
435
+ }
436
+
437
+ function changelogSection(fileValue, version) {
438
+ const file = resolve(fileValue);
439
+ const lines = readFileSync(file, "utf8").split(/\r?\n/);
440
+ const start = lines.findIndex(
441
+ (line) => line.startsWith("## ") && line.includes(version),
442
+ );
443
+ if (start < 0) fail(`Version section not found: ${version}`);
444
+ let end = lines.length;
445
+ for (let index = start + 1; index < lines.length; index += 1) {
446
+ if (lines[index].startsWith("## ")) {
447
+ end = index;
448
+ break;
449
+ }
450
+ }
451
+ console.log(lines.slice(start, end).join("\n").trimEnd());
452
+ }
453
+
454
+ function runRecipeUtils(argv = process.argv.slice(2)) {
455
+ const [command, ...args] = argv;
456
+ if (!command) {
457
+ usage();
458
+ process.exit(1);
459
+ }
460
+
461
+ if (command === "run-summary")
462
+ runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
463
+ else if (command === "run-ops-snapshot")
464
+ runOpsSnapshot(
465
+ args[0] ?? "~/.pi/agent/tmp/pi-actors/runs",
466
+ args[1] ?? "music",
467
+ args[2],
468
+ args[3],
469
+ );
470
+ else if (command === "playlist")
471
+ playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
472
+ else if (command === "changelog-section")
473
+ changelogSection(args[0] ?? "CHANGELOG.md", args[1] ?? "Unreleased");
474
+ else if (command === "artifact-manifest")
475
+ artifactManifest(args[0] ?? "artifact.md", args[1], args[2], args[3]);
476
+ else if (command === "artifact-write")
477
+ artifactWrite(args[0] ?? "artifact.md", args[1] ?? "create");
478
+ else if (command === "actor-message")
479
+ actorMessage(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
480
+ else if (command === "package-summary")
481
+ packageSummary(args[0] ?? "package.json");
482
+ else if (command === "skill-summary")
483
+ skillSummary(args[0] ?? "skills/actors/SKILL.md", args[1] ?? "package.json");
484
+ else {
485
+ usage();
486
+ fail(`Unknown command: ${command}`);
487
+ }
488
+ }
25
489
 
26
490
  try {
27
491
  runRecipeUtils(process.argv.slice(2));
@@ -1,27 +1,127 @@
1
- #!/usr/bin/env -S node --experimental-strip-types
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Template recipe validator CLI shim.
4
+ * Template recipe validator CLI.
5
5
  *
6
- * Runtime logic lives in lib/validate-recipe.ts and is compiled to
7
- * dist/lib/validate-recipe.js for installed JS-only packages.
6
+ * Owns CLI parsing and report formatting directly. Recipe parsing remains in
7
+ * the reusable recipe-reference domain.
8
8
  */
9
9
 
10
- import { existsSync } from "node:fs";
11
- import { dirname, join } from "node:path";
10
+ import { existsSync, readdirSync, statSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
12
13
  import { fileURLToPath, pathToFileURL } from "node:url";
13
14
 
14
15
  function packageRoot() {
15
16
  return dirname(dirname(fileURLToPath(import.meta.url)));
16
17
  }
17
18
 
18
- function mainModulePath() {
19
+ async function importRuntimeModule(name) {
19
20
  const root = packageRoot();
20
- const compiled = join(root, "dist", "lib", "validate-recipe.js");
21
- return existsSync(compiled) ? compiled : join(root, "lib", "validate-recipe.ts");
21
+ const compiled = join(root, "dist", "lib", `${name}.js`);
22
+ const source = join(root, "lib", `${name}.ts`);
23
+ return await import(pathToFileURL(existsSync(compiled) ? compiled : source).href);
22
24
  }
23
25
 
24
- const { validateRecipes } = await import(pathToFileURL(mainModulePath()).href);
26
+ const { readResolvedRecipeConfig } = await importRuntimeModule("recipes-references");
27
+
28
+ export function validateRecipeUsage() {
29
+ return `Usage:
30
+ validate-recipe.mjs <recipe-file-or-dir> [--all]
31
+
32
+ Validates one template recipe file, or all *.json/*.md files in a directory when --all is set.`;
33
+ }
34
+
35
+ function expandPath(value) {
36
+ return resolve(String(value).replace(/^~(?=\/|$)/, process.env.HOME ?? homedir()));
37
+ }
38
+
39
+ function templateKind(template) {
40
+ if (typeof template === "string") return "leaf";
41
+ if (Array.isArray(template)) return "sequence";
42
+ if (template && typeof template === "object") {
43
+ const node = template;
44
+ if (typeof node.template === "string") return "leaf";
45
+ if (Array.isArray(node.template))
46
+ return node.parallel === true ? "parallel" : "sequence";
47
+ if (node.parallel === true) return "parallel";
48
+ return "object";
49
+ }
50
+ return "unknown";
51
+ }
52
+
53
+ function recipeFiles(target, all) {
54
+ if (!existsSync(target)) throw new Error(`Recipe path not found: ${target}`);
55
+ const stat = statSync(target);
56
+ if (stat.isFile()) return [target];
57
+ if (!stat.isDirectory())
58
+ throw new Error(`Recipe path is not a file or directory: ${target}`);
59
+ if (!all) throw new Error("Directory validation requires --all.");
60
+ return readdirSync(target)
61
+ .filter((file) => file.endsWith(".json") || file.endsWith(".md"))
62
+ .sort((a, b) => a.localeCompare(b))
63
+ .map((file) => resolve(target, file));
64
+ }
65
+
66
+ function validateFile(file) {
67
+ try {
68
+ const config = readResolvedRecipeConfig(file);
69
+ if (!config?.template)
70
+ throw new Error("Recipe must define a non-empty template.");
71
+ return {
72
+ file,
73
+ ok: true,
74
+ name: config.name ?? "",
75
+ async: Boolean(config.async),
76
+ args: Array.isArray(config.args) ? config.args : [],
77
+ defaults:
78
+ config.defaults && typeof config.defaults === "object"
79
+ ? Object.keys(config.defaults).sort()
80
+ : [],
81
+ imports:
82
+ config.imports && typeof config.imports === "object"
83
+ ? Object.keys(config.imports).sort()
84
+ : [],
85
+ mailbox:
86
+ config.mailbox && typeof config.mailbox === "object"
87
+ ? {
88
+ accepts: Array.isArray(config.mailbox.accepts)
89
+ ? config.mailbox.accepts
90
+ : [],
91
+ emits: Array.isArray(config.mailbox.emits)
92
+ ? config.mailbox.emits
93
+ : [],
94
+ }
95
+ : undefined,
96
+ template: templateKind(config.template),
97
+ };
98
+ } catch (error) {
99
+ return {
100
+ file,
101
+ ok: false,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ };
104
+ }
105
+ }
106
+
107
+ export function validateRecipes(argv) {
108
+ const targetArg = argv.find((arg) => !arg.startsWith("-"));
109
+ const all = argv.includes("--all");
110
+ if (!targetArg || argv.includes("--help") || argv.includes("-h")) {
111
+ return { help: true, ok: Boolean(targetArg), usage: validateRecipeUsage() };
112
+ }
113
+
114
+ const files = recipeFiles(expandPath(targetArg), all);
115
+ const results = files.map(validateFile);
116
+ const failed = results.filter((result) => !result.ok).length;
117
+ return {
118
+ ok: failed === 0,
119
+ total: results.length,
120
+ passed: results.length - failed,
121
+ failed,
122
+ results,
123
+ };
124
+ }
25
125
 
26
126
  try {
27
127
  const report = validateRecipes(process.argv.slice(2));
@@ -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.33.0
5
+ version: 0.34.1
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -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.33.0
5
+ version: 0.34.1
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -125,7 +125,7 @@ The repeatable smoke surface is the normal validation suite:
125
125
  npm test
126
126
  ```
127
127
 
128
- The scenario coverage is intentionally local-first and bounded: shared room coordination and roster snapshots (`actor-rooms` / `tools` tests), direct branch delivery and claim/handle transitions (`tools` and coordinator tests), inspector navigation (`actor-inspector-tui` tests), recipe context injection (`actor-recipe-context` / async-run tests), recipe persistence suggestions (`observability` tests), and opt-in retirement candidate/execution smoke (`observability` / async-run tests). These scenarios exercise public `spawn` / `message` / `inspect` behavior or the packaged script surfaces rather than relying on manual swarm demos.
128
+ The scenario coverage is intentionally local-first and bounded: shared room coordination and roster snapshots (`rooms` / `tools` tests), direct branch delivery and claim/handle transitions (`tools` and coordinator tests), inspector navigation (`inspector` tests), recipe context injection (`recipes-context` / async-runs tests), recipe persistence suggestions (`observability` tests), and opt-in retirement candidate/execution smoke (`observability` / async-runs tests). These scenarios exercise public `spawn` / `message` / `inspect` behavior or the packaged script surfaces rather than relying on manual swarm demos.
129
129
 
130
130
  ## Music Player
131
131
 
@@ -69,7 +69,7 @@ A file-backed recipe's id comes from its filename, not a JSON `name` field. Lega
69
69
 
70
70
  ## Markdown Authoring
71
71
 
72
- Markdown recipes use `.md` files with YAML-like frontmatter for recipe metadata and one fenced executable block for the recipe/template body. Runtime behavior comes only from frontmatter plus the fenced block; surrounding prose is advisory for humans and future recipe-context use.
72
+ Markdown recipes use `.md` files with YAML-like frontmatter for recipe metadata and one fenced executable block for the recipe/template body. Runtime behavior comes only from frontmatter plus the fenced block; surrounding prose is advisory for humans and future recipes-context use.
73
73
 
74
74
  ````markdown
75
75
  ---