@llblab/pi-actors 0.14.3 → 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 +20 -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
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ appendFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { dirname, join, resolve } from "node:path";
11
+ import { spawnSync } from "node:child_process";
12
+ import readline from "node:readline";
13
+
14
+ function parseArgs(argv) {
15
+ const args = { mode: "serve", stateDir: "", leaseMs: 600000, lines: 20 };
16
+ for (let index = 0; index < argv.length; index += 1) {
17
+ const arg = argv[index];
18
+ if (arg === "serve" || arg === "snapshot") args.mode = arg;
19
+ else if (arg === "--state-dir") args.stateDir = argv[++index] ?? "";
20
+ else if (arg === "--lease-ms")
21
+ args.leaseMs = Number(argv[++index] ?? args.leaseMs);
22
+ else if (arg === "--lines")
23
+ args.lines = Number(argv[++index] ?? args.lines);
24
+ }
25
+ if (!args.stateDir) throw new Error("--state-dir is required");
26
+ if (!Number.isFinite(args.leaseMs) || args.leaseMs <= 0)
27
+ args.leaseMs = 600000;
28
+ if (!Number.isFinite(args.lines) || args.lines <= 0) args.lines = 20;
29
+ return args;
30
+ }
31
+
32
+ const { mode, stateDir, leaseMs, lines } = parseArgs(process.argv.slice(2));
33
+ const queuePath = join(stateDir, "queue.json");
34
+ const locksPath = join(stateDir, "locks.json");
35
+ const journalPath = join(stateDir, "journal.jsonl");
36
+ const outboxPath = join(stateDir, "outbox.jsonl");
37
+ const controlPath = join(stateDir, "control.fifo");
38
+ mkdirSync(stateDir, { recursive: true });
39
+
40
+ function readJson(path, fallback) {
41
+ if (!existsSync(path)) return fallback;
42
+ try {
43
+ return JSON.parse(readFileSync(path, "utf8"));
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
48
+ function writeJson(path, value) {
49
+ mkdirSync(dirname(path), { recursive: true });
50
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
51
+ }
52
+ function journal(event, data = {}) {
53
+ appendFileSync(
54
+ journalPath,
55
+ `${JSON.stringify({ event, ts: new Date().toISOString(), ...data })}\n`,
56
+ );
57
+ }
58
+ function outbox(type, summary, body = {}, level = "info") {
59
+ appendFileSync(
60
+ outboxPath,
61
+ `${JSON.stringify({ to: "coordinator", from: `run:${process.env.run_id ?? "coordinator-locker"}`, type, event: type, summary, body, data: body, delivery: "followup", level, ts: new Date().toISOString() })}\n`,
62
+ );
63
+ }
64
+ function now() {
65
+ return Date.now();
66
+ }
67
+ function cleanExpiredLocks(locks) {
68
+ const current = now();
69
+ const kept = {};
70
+ for (const [key, lock] of Object.entries(locks)) {
71
+ if (Number(lock.expiresAt) > current) kept[key] = lock;
72
+ else journal("lock.expired", { resource: key, owner: lock.owner });
73
+ }
74
+ return kept;
75
+ }
76
+ function normalizeMessage(line) {
77
+ const trimmed = line.trim();
78
+ if (!trimmed) return undefined;
79
+ if (["stop", "quit", "exit", "cancel"].includes(trimmed.toLowerCase())) {
80
+ return { type: "control.stop", body: {} };
81
+ }
82
+ try {
83
+ return JSON.parse(trimmed);
84
+ } catch {
85
+ return { type: "coord.enqueue", body: { task: trimmed } };
86
+ }
87
+ }
88
+ function tailJournal(count) {
89
+ if (!existsSync(journalPath)) return [];
90
+ return readFileSync(journalPath, "utf8")
91
+ .trimEnd()
92
+ .split("\n")
93
+ .filter(Boolean)
94
+ .slice(-count)
95
+ .map((line) => {
96
+ try {
97
+ return JSON.parse(line);
98
+ } catch {
99
+ return { raw: line };
100
+ }
101
+ });
102
+ }
103
+ function printSnapshot() {
104
+ const locks = cleanExpiredLocks(readJson(locksPath, {}));
105
+ writeJson(locksPath, locks);
106
+ const queue = readJson(queuePath, { items: [] });
107
+ console.log(
108
+ JSON.stringify(
109
+ {
110
+ queueDepth: Array.isArray(queue.items) ? queue.items.length : 0,
111
+ queue,
112
+ locks,
113
+ journal: tailJournal(lines),
114
+ },
115
+ null,
116
+ 2,
117
+ ),
118
+ );
119
+ }
120
+ function nextTask(queue, locks) {
121
+ const items = Array.isArray(queue.items) ? queue.items : [];
122
+ const index = items.findIndex((item) => {
123
+ const resources = Array.isArray(item.resources) ? item.resources : [];
124
+ return resources.every((resource) => !locks[resource]);
125
+ });
126
+ if (index < 0) return undefined;
127
+ return items.splice(index, 1)[0];
128
+ }
129
+ function handle(message) {
130
+ const type = message.type || message.event || "coord.enqueue";
131
+ const body =
132
+ message.body && typeof message.body === "object" ? message.body : message;
133
+ let queue = readJson(queuePath, { items: [] });
134
+ let locks = cleanExpiredLocks(readJson(locksPath, {}));
135
+ if (type === "control.stop" || type === "control.cancel") {
136
+ writeJson(locksPath, locks);
137
+ journal("control.stop", {});
138
+ outbox("coord.stopped", "Coordinator locker stopped", {
139
+ queueDepth: queue.items?.length ?? 0,
140
+ });
141
+ process.exit(0);
142
+ }
143
+ if (type === "coord.enqueue") {
144
+ const item = {
145
+ id: body.id || `task-${Date.now()}`,
146
+ task: body.task ?? body,
147
+ resources: body.resources ?? [],
148
+ enqueuedAt: new Date().toISOString(),
149
+ };
150
+ queue.items = [...(queue.items ?? []), item];
151
+ writeJson(queuePath, queue);
152
+ writeJson(locksPath, locks);
153
+ journal("coord.enqueued", { id: item.id, resources: item.resources });
154
+ outbox("coord.enqueued", `Queued ${item.id}`, {
155
+ id: item.id,
156
+ queueDepth: queue.items.length,
157
+ });
158
+ return;
159
+ }
160
+ if (type === "coord.claim") {
161
+ const owner = body.owner || message.from || "worker";
162
+ const item = nextTask(queue, locks);
163
+ if (!item) {
164
+ writeJson(queuePath, queue);
165
+ writeJson(locksPath, locks);
166
+ outbox("coord.empty", "No claimable task", {
167
+ owner,
168
+ queueDepth: queue.items?.length ?? 0,
169
+ });
170
+ return;
171
+ }
172
+ for (const resource of item.resources ?? [])
173
+ locks[resource] = { owner, task: item.id, expiresAt: now() + leaseMs };
174
+ writeJson(queuePath, queue);
175
+ writeJson(locksPath, locks);
176
+ journal("coord.assigned", {
177
+ id: item.id,
178
+ owner,
179
+ resources: item.resources,
180
+ });
181
+ outbox("coord.assigned", `Assigned ${item.id}`, { owner, task: item });
182
+ return;
183
+ }
184
+ if (type === "lock.acquire") {
185
+ const resource = body.resource;
186
+ const owner = body.owner || message.from || "worker";
187
+ if (!resource) throw new Error("lock.acquire body.resource is required");
188
+ if (locks[resource])
189
+ outbox(
190
+ "lock.denied",
191
+ `Lock denied ${resource}`,
192
+ { resource, owner, current: locks[resource] },
193
+ "warning",
194
+ );
195
+ else {
196
+ locks[resource] = { owner, expiresAt: now() + leaseMs };
197
+ outbox("lock.granted", `Lock granted ${resource}`, { resource, owner });
198
+ }
199
+ writeJson(locksPath, locks);
200
+ return;
201
+ }
202
+ if (type === "lock.renew") {
203
+ const resource = body.resource;
204
+ const owner = body.owner || message.from || "worker";
205
+ if (!resource) throw new Error("lock.renew body.resource is required");
206
+ const current = locks[resource];
207
+ if (!current) {
208
+ outbox("lock.denied", `Lock renew denied ${resource}`, { resource, owner, reason: "missing" }, "warning");
209
+ } else if (current.owner !== owner) {
210
+ outbox("lock.denied", `Lock renew denied ${resource}`, { resource, owner, current }, "warning");
211
+ } else {
212
+ locks[resource] = { ...current, expiresAt: now() + leaseMs };
213
+ outbox("lock.renewed", `Lock renewed ${resource}`, { resource, owner });
214
+ }
215
+ writeJson(locksPath, locks);
216
+ return;
217
+ }
218
+ if (type === "lock.release") {
219
+ const resource = body.resource;
220
+ if (resource) delete locks[resource];
221
+ writeJson(locksPath, locks);
222
+ outbox("lock.released", `Lock released ${resource}`, { resource });
223
+ return;
224
+ }
225
+ if (type === "coord.complete" || type === "coord.fail") {
226
+ journal(type, body);
227
+ outbox(
228
+ type,
229
+ `${type} ${body.id ?? ""}`.trim(),
230
+ body,
231
+ type === "coord.fail" ? "error" : "info",
232
+ );
233
+ writeJson(locksPath, locks);
234
+ writeJson(queuePath, queue);
235
+ return;
236
+ }
237
+ journal("coord.unknown", { type, body });
238
+ outbox("coord.unknown", `Unknown message ${type}`, { type, body }, "warning");
239
+ }
240
+
241
+ if (mode === "snapshot") {
242
+ printSnapshot();
243
+ process.exit(0);
244
+ }
245
+
246
+ if (!existsSync(controlPath)) {
247
+ const result = spawnSync("mkfifo", [controlPath]);
248
+ if (result.status !== 0)
249
+ throw new Error(`mkfifo failed: ${result.stderr?.toString?.() ?? ""}`);
250
+ }
251
+ writeJson(queuePath, readJson(queuePath, { items: [] }));
252
+ writeJson(locksPath, cleanExpiredLocks(readJson(locksPath, {})));
253
+ journal("coord.started", { leaseMs });
254
+ outbox("coord.started", "Coordinator locker ready", { leaseMs });
255
+
256
+ while (true) {
257
+ const stream = await import("node:fs").then((fs) =>
258
+ fs.createReadStream(controlPath, { encoding: "utf8" }),
259
+ );
260
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
261
+ for await (const line of rl) {
262
+ const message = normalizeMessage(line);
263
+ if (!message) continue;
264
+ try {
265
+ handle(message);
266
+ } catch (error) {
267
+ const text = error instanceof Error ? error.message : String(error);
268
+ journal("coord.error", { error: text });
269
+ outbox("coord.error", text, { error: text }, "error");
270
+ }
271
+ }
272
+ }
@@ -227,7 +227,8 @@ function loadPlaylist(source) {
227
227
  } else {
228
228
  fail(`source not found: ${sourceArg}`, 66);
229
229
  }
230
- if (tracks.length === 0) fail(`source has no playable tracks: ${sourceArg}`, 66);
230
+ if (tracks.length === 0)
231
+ fail(`source has no playable tracks: ${sourceArg}`, 66);
231
232
  return tracks;
232
233
  }
233
234