@llblab/pi-actors 0.22.5 → 0.24.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 (180) hide show
  1. package/AGENTS.md +125 -59
  2. package/BACKLOG.md +88 -403
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +14 -1
  5. package/dist/fixtures/protocol/actor-message-branch.json +13 -0
  6. package/dist/fixtures/protocol/artifact-manifest.json +9 -0
  7. package/dist/fixtures/protocol/mailbox-contract.json +15 -0
  8. package/dist/fixtures/protocol/recipe-summary.json +16 -0
  9. package/dist/fixtures/protocol/room-message.json +11 -0
  10. package/dist/fixtures/protocol/room-roster.json +11 -0
  11. package/dist/fixtures/protocol/run-inbox-message.json +9 -0
  12. package/dist/fixtures/protocol/run-outbox-event.json +9 -0
  13. package/dist/fixtures/protocol/run-state.json +10 -0
  14. package/dist/index.js +18 -2
  15. package/dist/lib/actor-inspector-tui.d.ts +4 -0
  16. package/dist/lib/actor-inspector-tui.js +58 -44
  17. package/dist/lib/actor-rooms.d.ts +16 -0
  18. package/dist/lib/actor-rooms.js +123 -28
  19. package/dist/lib/actor-worker.d.ts +13 -0
  20. package/dist/lib/actor-worker.js +86 -0
  21. package/dist/lib/async-runner.d.ts +5 -0
  22. package/dist/lib/async-runner.js +134 -0
  23. package/dist/lib/async-runs.d.ts +34 -2
  24. package/dist/lib/async-runs.js +224 -50
  25. package/dist/lib/build-dist.d.ts +5 -0
  26. package/dist/lib/build-dist.js +24 -0
  27. package/dist/lib/command-templates.js +5 -5
  28. package/dist/lib/conformance.d.ts +12 -0
  29. package/dist/lib/conformance.js +28 -0
  30. package/dist/lib/coordinator.d.ts +5 -0
  31. package/dist/lib/coordinator.js +557 -0
  32. package/dist/lib/limits.d.ts +11 -0
  33. package/dist/lib/limits.js +11 -0
  34. package/dist/lib/locker.d.ts +5 -0
  35. package/dist/lib/locker.js +310 -0
  36. package/dist/lib/mailbox-loop.d.ts +41 -0
  37. package/dist/lib/mailbox-loop.js +62 -0
  38. package/dist/lib/observability.d.ts +2 -2
  39. package/dist/lib/observability.js +57 -57
  40. package/dist/lib/output.js +4 -5
  41. package/dist/lib/prompts.d.ts +1 -1
  42. package/dist/lib/prompts.js +1 -1
  43. package/dist/lib/recipe-discovery.js +1 -0
  44. package/dist/lib/recipe-references.d.ts +11 -2
  45. package/dist/lib/recipe-references.js +29 -4
  46. package/dist/lib/recipe-usage.d.ts +2 -1
  47. package/dist/lib/recipe-usage.js +15 -3
  48. package/dist/lib/recipe-utils.d.ts +5 -0
  49. package/dist/lib/recipe-utils.js +385 -0
  50. package/dist/lib/runtime-notifier.js +3 -7
  51. package/dist/lib/runtime.js +12 -2
  52. package/dist/lib/state-readers.d.ts +21 -0
  53. package/dist/lib/state-readers.js +74 -0
  54. package/dist/lib/tools.js +198 -39
  55. package/dist/lib/validate-recipe.d.ts +6 -0
  56. package/dist/lib/validate-recipe.js +104 -0
  57. package/dist/recipes/actor-worker.json +35 -0
  58. package/dist/recipes/coordinator-locker.json +45 -0
  59. package/dist/recipes/lens-swarm.json +66 -0
  60. package/dist/recipes/locker.json +45 -0
  61. package/dist/recipes/music-player.json +38 -0
  62. package/dist/recipes/pipeline-architect-coordinator.json +95 -0
  63. package/dist/recipes/pipeline-artifact-bundle.json +100 -0
  64. package/dist/recipes/pipeline-artifact-report.json +58 -0
  65. package/dist/recipes/pipeline-artifact-write.json +72 -0
  66. package/dist/recipes/pipeline-async-run-ops.json +70 -0
  67. package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
  68. package/dist/recipes/pipeline-development-tasking.json +81 -0
  69. package/dist/recipes/pipeline-docs-maintenance.json +80 -0
  70. package/dist/recipes/pipeline-media-library.json +59 -0
  71. package/dist/recipes/pipeline-quorum-review.json +79 -0
  72. package/dist/recipes/pipeline-release-readiness.json +110 -0
  73. package/dist/recipes/pipeline-release-summary.json +88 -0
  74. package/dist/recipes/pipeline-repo-health.json +89 -0
  75. package/dist/recipes/pipeline-research-synthesis.json +94 -0
  76. package/dist/recipes/pipeline-review-readiness.json +54 -0
  77. package/dist/recipes/pipeline-room-swarm.json +50 -0
  78. package/dist/recipes/subagent-artifact.json +32 -0
  79. package/dist/recipes/subagent-checkpoint.json +33 -0
  80. package/dist/recipes/subagent-conflict-report.json +32 -0
  81. package/dist/recipes/subagent-contradiction-map.json +33 -0
  82. package/dist/recipes/subagent-critic.json +35 -0
  83. package/dist/recipes/subagent-evidence-map.json +33 -0
  84. package/dist/recipes/subagent-followup.json +33 -0
  85. package/dist/recipes/subagent-judge.json +33 -0
  86. package/dist/recipes/subagent-merge.json +33 -0
  87. package/dist/recipes/subagent-message.json +34 -0
  88. package/dist/recipes/subagent-normalize.json +31 -0
  89. package/dist/recipes/subagent-plan.json +33 -0
  90. package/dist/recipes/subagent-prompt.json +28 -0
  91. package/dist/recipes/subagent-quorum.json +43 -0
  92. package/dist/recipes/subagent-review-coordinator.json +114 -0
  93. package/dist/recipes/subagent-review.json +37 -0
  94. package/dist/recipes/subagent-task-card.json +35 -0
  95. package/dist/recipes/subagent-tools.json +27 -0
  96. package/dist/recipes/subagent-verify.json +34 -0
  97. package/dist/recipes/subagents-prompts.json +51 -0
  98. package/dist/recipes/utility-actor-message.json +23 -0
  99. package/dist/recipes/utility-artifact-manifest.json +16 -0
  100. package/dist/recipes/utility-artifact-write.json +16 -0
  101. package/dist/recipes/utility-changelog-head.json +11 -0
  102. package/dist/recipes/utility-changelog-section.json +13 -0
  103. package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
  104. package/dist/recipes/utility-git-log.json +11 -0
  105. package/dist/recipes/utility-git-status.json +9 -0
  106. package/dist/recipes/utility-jsonl-tail.json +10 -0
  107. package/dist/recipes/utility-markdown-index.json +14 -0
  108. package/dist/recipes/utility-package-summary.json +11 -0
  109. package/dist/recipes/utility-playlist-build.json +17 -0
  110. package/dist/recipes/utility-playlist-scan.json +11 -0
  111. package/dist/recipes/utility-run-ops-snapshot.json +17 -0
  112. package/dist/recipes/utility-run-state-files.json +13 -0
  113. package/dist/recipes/utility-run-summary.json +11 -0
  114. package/dist/recipes/utility-skill-summary.json +13 -0
  115. package/dist/recipes/utility-validate-recipe.json +13 -0
  116. package/dist/recipes/utility-validation-wrapper.json +13 -0
  117. package/dist/scripts/actor-worker.mjs +31 -0
  118. package/dist/scripts/async-runner.mjs +31 -0
  119. package/dist/scripts/build-dist.mjs +25 -0
  120. package/dist/scripts/conformance.mjs +33 -0
  121. package/dist/scripts/coordinator.mjs +31 -0
  122. package/dist/scripts/locker.mjs +33 -0
  123. package/dist/scripts/music-player.mjs +964 -0
  124. package/dist/scripts/recipe-utils.mjs +31 -0
  125. package/dist/scripts/validate-recipe.mjs +34 -0
  126. package/dist/skills/actors/SKILL.md +375 -0
  127. package/dist/skills/swarm/SKILL.md +467 -0
  128. package/dist/skills/swarm/references/development-swarm.md +596 -0
  129. package/docs/actor-messages.md +2 -2
  130. package/docs/async-runs.md +13 -1
  131. package/docs/template-recipes.md +2 -2
  132. package/docs/tool-registry.md +0 -1
  133. package/fixtures/protocol/actor-message-branch.json +13 -0
  134. package/fixtures/protocol/artifact-manifest.json +9 -0
  135. package/fixtures/protocol/mailbox-contract.json +15 -0
  136. package/fixtures/protocol/recipe-summary.json +16 -0
  137. package/fixtures/protocol/room-message.json +11 -0
  138. package/fixtures/protocol/room-roster.json +11 -0
  139. package/fixtures/protocol/run-inbox-message.json +9 -0
  140. package/fixtures/protocol/run-outbox-event.json +9 -0
  141. package/fixtures/protocol/run-state.json +10 -0
  142. package/index.ts +21 -0
  143. package/lib/actor-inspector-tui.ts +138 -59
  144. package/lib/actor-rooms.ts +241 -60
  145. package/lib/actor-worker.ts +118 -0
  146. package/lib/async-runner.ts +173 -0
  147. package/lib/async-runs.ts +302 -53
  148. package/lib/build-dist.ts +30 -0
  149. package/lib/command-templates.ts +5 -5
  150. package/lib/conformance.ts +46 -0
  151. package/lib/coordinator.ts +649 -0
  152. package/lib/limits.ts +12 -0
  153. package/lib/locker.ts +340 -0
  154. package/lib/mailbox-loop.ts +148 -0
  155. package/lib/observability.ts +34 -23
  156. package/lib/output.ts +4 -6
  157. package/lib/prompts.ts +1 -1
  158. package/lib/recipe-discovery.ts +1 -0
  159. package/lib/recipe-references.ts +57 -6
  160. package/lib/recipe-usage.ts +31 -4
  161. package/lib/recipe-utils.ts +486 -0
  162. package/lib/runtime-notifier.ts +4 -6
  163. package/lib/runtime.ts +31 -7
  164. package/lib/state-readers.ts +93 -0
  165. package/lib/tools.ts +297 -58
  166. package/lib/validate-recipe.ts +110 -0
  167. package/package.json +11 -2
  168. package/recipes/actor-worker.json +35 -0
  169. package/recipes/pipeline-quorum-review.json +12 -7
  170. package/scripts/actor-worker.mjs +31 -0
  171. package/scripts/async-runner.mjs +11 -195
  172. package/scripts/build-dist.mjs +25 -0
  173. package/scripts/conformance.mjs +33 -0
  174. package/scripts/coordinator.mjs +19 -616
  175. package/scripts/locker.mjs +23 -322
  176. package/scripts/music-player.mjs +21 -2
  177. package/scripts/recipe-utils.mjs +20 -467
  178. package/scripts/validate-recipe.mjs +23 -113
  179. package/skills/actors/SKILL.md +7 -4
  180. package/skills/swarm/SKILL.md +3 -1
@@ -26,9 +26,21 @@ export interface TemplateRecipeImportBinding {
26
26
 
27
27
  export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
28
28
 
29
+ export type TemplateRecipeMailboxEntry =
30
+ | string
31
+ | {
32
+ type: string;
33
+ body_schema?: unknown;
34
+ ack?: boolean | string;
35
+ idempotency?: string;
36
+ requires_response?: boolean;
37
+ level?: string;
38
+ summary?: string;
39
+ };
40
+
29
41
  export interface TemplateRecipeMailbox {
30
- accepts?: string[];
31
- emits?: string[];
42
+ accepts?: TemplateRecipeMailboxEntry[];
43
+ emits?: TemplateRecipeMailboxEntry[];
32
44
  }
33
45
 
34
46
  export interface TemplateRecipeDefinition {
@@ -299,6 +311,35 @@ function parseMarkdownFrontmatterObject(
299
311
  return result;
300
312
  }
301
313
 
314
+ function normalizeMarkdownFrontmatterField(
315
+ key: string,
316
+ value: unknown,
317
+ ): unknown {
318
+ if (key === "args" && typeof value === "string") {
319
+ return value
320
+ .split(",")
321
+ .map((item) => item.trim())
322
+ .filter(Boolean);
323
+ }
324
+ if (key === "defaults" && Array.isArray(value)) {
325
+ return Object.fromEntries(
326
+ value
327
+ .filter((item): item is string => typeof item === "string")
328
+ .map((item) => {
329
+ const separator = item.indexOf(":");
330
+ return separator < 0
331
+ ? [item.trim(), ""]
332
+ : [
333
+ item.slice(0, separator).trim(),
334
+ parseMarkdownScalar(item.slice(separator + 1)),
335
+ ];
336
+ })
337
+ .filter(([name]) => Boolean(name)),
338
+ );
339
+ }
340
+ return value;
341
+ }
342
+
302
343
  function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
303
344
  const result: Record<string, unknown> = {};
304
345
  const lines = value.split(/\r?\n/);
@@ -308,7 +349,10 @@ function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
308
349
  const match = line.match(/^([A-Za-z_][A-Za-z0-9_.-]*):\s*(.*)$/);
309
350
  if (!match) continue;
310
351
  if (match[2]) {
311
- result[match[1]] = parseMarkdownScalar(match[2]);
352
+ result[match[1]] = normalizeMarkdownFrontmatterField(
353
+ match[1],
354
+ parseMarkdownScalar(match[2]),
355
+ );
312
356
  continue;
313
357
  }
314
358
  const nested: string[] = [];
@@ -316,7 +360,10 @@ function parseMarkdownFrontmatter(value: string): Record<string, unknown> {
316
360
  index += 1;
317
361
  nested.push(lines[index]);
318
362
  }
319
- result[match[1]] = parseMarkdownFrontmatterObject(nested);
363
+ result[match[1]] = normalizeMarkdownFrontmatterField(
364
+ match[1],
365
+ parseMarkdownFrontmatterObject(nested),
366
+ );
320
367
  }
321
368
  return result;
322
369
  }
@@ -834,14 +881,18 @@ export function readResolvedRecipeConfig(
834
881
  ...(Array.isArray(substituted.mailbox.accepts)
835
882
  ? {
836
883
  accepts: substituted.mailbox.accepts.filter(
837
- (value): value is string => typeof value === "string",
884
+ (value): value is TemplateRecipeMailboxEntry =>
885
+ typeof value === "string" ||
886
+ (isRecord(value) && typeof value.type === "string"),
838
887
  ),
839
888
  }
840
889
  : {}),
841
890
  ...(Array.isArray(substituted.mailbox.emits)
842
891
  ? {
843
892
  emits: substituted.mailbox.emits.filter(
844
- (value): value is string => typeof value === "string",
893
+ (value): value is TemplateRecipeMailboxEntry =>
894
+ typeof value === "string" ||
895
+ (isRecord(value) && typeof value.type === "string"),
845
896
  ),
846
897
  }
847
898
  : {}),
@@ -11,9 +11,14 @@ import { writeJsonAtomic } from "./file-state.ts";
11
11
 
12
12
  interface RecipeUsageRecord {
13
13
  calls?: number;
14
- last_called?: string;
14
+ direct_calls?: number;
15
15
  fingerprint?: string;
16
+ last_called?: string;
17
+ launch_kind?: string;
16
18
  reset_at?: string;
19
+ reset_reason?: string;
20
+ spawn_calls?: number;
21
+ tool_calls?: number;
17
22
  }
18
23
 
19
24
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -40,23 +45,45 @@ function getRecipeFingerprint(raw: Record<string, unknown>): string {
40
45
  return createHash("sha256").update(stableStringify(content)).digest("hex");
41
46
  }
42
47
 
43
- export function recordRecipeLaunch(path: string, now = new Date()): boolean {
48
+ export type RecipeLaunchKind = "direct" | "spawn" | "tool";
49
+
50
+ function launchCounterKey(
51
+ kind: RecipeLaunchKind,
52
+ ): "direct_calls" | "spawn_calls" | "tool_calls" {
53
+ return `${kind}_calls` as "direct_calls" | "spawn_calls" | "tool_calls";
54
+ }
55
+
56
+ export function recordRecipeLaunch(
57
+ path: string,
58
+ now = new Date(),
59
+ kind: RecipeLaunchKind = "direct",
60
+ ): boolean {
44
61
  if (!existsSync(path)) return false;
45
62
  try {
46
63
  const raw = JSON.parse(readFileSync(path, "utf8"));
47
64
  if (!isRecord(raw)) return false;
48
65
  const usage: RecipeUsageRecord = isRecord(raw.usage) ? raw.usage : {};
49
66
  const fingerprint = getRecipeFingerprint(raw);
50
- const changed = typeof usage.fingerprint === "string" && usage.fingerprint !== fingerprint;
67
+ const changed =
68
+ typeof usage.fingerprint === "string" &&
69
+ usage.fingerprint !== fingerprint;
51
70
  const nowIso = now.toISOString();
71
+ const counterKey = launchCounterKey(kind);
52
72
  writeJsonAtomic(path, {
53
73
  ...raw,
54
74
  usage: {
55
75
  ...usage,
56
76
  calls: (changed ? 0 : normalizeCalls(usage.calls)) + 1,
77
+ [counterKey]: (changed ? 0 : normalizeCalls(usage[counterKey])) + 1,
57
78
  last_called: nowIso,
79
+ launch_kind: kind,
58
80
  fingerprint,
59
- ...(changed ? { reset_at: nowIso } : {}),
81
+ ...(changed
82
+ ? {
83
+ reset_at: nowIso,
84
+ reset_reason: "recipe content fingerprint changed",
85
+ }
86
+ : {}),
60
87
  },
61
88
  });
62
89
  return true;
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Recipe utility command bundle entrypoint logic.
3
+ * Zones: recipe utilities, deterministic helper subcommands
4
+ */
5
+
6
+ // @ts-nocheck
7
+
8
+ import {
9
+ appendFileSync,
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ readdirSync,
14
+ statSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { dirname, extname, join, relative, resolve } from "node:path";
18
+
19
+ function usage() {
20
+ console.error(`Usage:
21
+ recipe-utils.mjs run-summary <state-root>
22
+ recipe-utils.mjs run-ops-snapshot <state-root> <run-id> [lines] [stale-minutes]
23
+ recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
24
+ recipe-utils.mjs changelog-section <file> <version>
25
+ recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
26
+ recipe-utils.mjs artifact-write <artifact-path> [create|overwrite|append]
27
+ recipe-utils.mjs actor-message <type> [to] [from] [summary] [metadata-json] [correlation-id] [reply-to]
28
+ recipe-utils.mjs package-summary <package-json>
29
+ recipe-utils.mjs skill-summary <skill-md> [package-json]`);
30
+ }
31
+
32
+ function fail(message) {
33
+ console.error(message);
34
+ process.exit(1);
35
+ }
36
+
37
+ const ADDRESS_PATTERN = /^[A-Za-z0-9_.-]+$/;
38
+ const MESSAGE_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
39
+ function assertToken(value, label) {
40
+ const normalized = String(value ?? "").trim();
41
+ if (!normalized) fail(`${label} is required`);
42
+ if (!ADDRESS_PATTERN.test(normalized))
43
+ fail(`${label} contains unsupported characters: ${value}`);
44
+ return normalized;
45
+ }
46
+
47
+ function validateActorAddress(address, label) {
48
+ const value = String(address ?? "").trim();
49
+ if (value === "coordinator") return value;
50
+ const separator = value.indexOf(":");
51
+ if (separator < 0) fail(`${label} must include an actor kind: ${address}`);
52
+ const kind = value.slice(0, separator);
53
+ const rest = value.slice(separator + 1);
54
+ if (kind === "branch") {
55
+ const [run, branch, ...extra] = rest.split("/");
56
+ if (extra.length > 0)
57
+ fail(`${label} branch address has too many parts: ${address}`);
58
+ return `branch:${assertToken(run, `${label} branch run`)}/${assertToken(branch, `${label} branch id`)}`;
59
+ }
60
+ if (["run", "session", "tool"].includes(kind))
61
+ return `${kind}:${assertToken(rest, label)}`;
62
+ fail(`${label} has unsupported actor kind: ${kind}`);
63
+ }
64
+
65
+ function validateMessageType(type) {
66
+ const value = String(type ?? "").trim();
67
+ if (!MESSAGE_TYPE_PATTERN.test(value))
68
+ fail(`Invalid actor message type: ${type}`);
69
+ return value;
70
+ }
71
+
72
+ function walkFiles(dir, maxDepth = 2, depth = 0, out = []) {
73
+ if (depth > maxDepth || !existsSync(dir)) return out;
74
+ for (const entry of readdirSync(dir)) {
75
+ const path = join(dir, entry);
76
+ const stat = statSync(path);
77
+ if (stat.isDirectory()) walkFiles(path, maxDepth, depth + 1, out);
78
+ else if (stat.isFile()) out.push(path);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function readJson(file) {
84
+ if (!existsSync(file)) return undefined;
85
+ try {
86
+ return JSON.parse(readFileSync(file, "utf8"));
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ function getRunStatus(run, progress, result) {
93
+ if (progress?.phase) return progress.phase;
94
+ if (result?.code !== undefined) return result.code === 0 ? "done" : "failed";
95
+ return run.status ?? "unknown";
96
+ }
97
+
98
+ function collectRunSummary(rootValue) {
99
+ const root = resolve(
100
+ rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
101
+ );
102
+ const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
103
+ const rows = [];
104
+ for (const file of files) {
105
+ const run = readJson(file);
106
+ if (!run) {
107
+ rows.push({
108
+ run: relative(root, file),
109
+ status: "invalid-json",
110
+ recipe: "",
111
+ updated: "",
112
+ });
113
+ continue;
114
+ }
115
+ const runDir = dirname(file);
116
+ const progress = readJson(join(runDir, "progress.json"));
117
+ const result = readJson(join(runDir, "result.json"));
118
+ rows.push({
119
+ run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
120
+ status: getRunStatus(run, progress, result),
121
+ recipe: run.recipe ?? run.recipe_file ?? "",
122
+ updated:
123
+ progress?.updatedAt ??
124
+ result?.completedAt ??
125
+ run.updated_at ??
126
+ run.completed_at ??
127
+ run.started_at ??
128
+ "",
129
+ });
130
+ }
131
+ rows.sort((a, b) =>
132
+ `${a.status}:${a.run}`.localeCompare(`${b.status}:${b.run}`),
133
+ );
134
+ return rows;
135
+ }
136
+
137
+ function runSummary(rootValue) {
138
+ console.log(JSON.stringify(collectRunSummary(rootValue), null, 2));
139
+ }
140
+
141
+ function tailJsonl(fileValue, linesValue = "80") {
142
+ const file = resolve(
143
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
144
+ );
145
+ if (!existsSync(file)) return [];
146
+ const lines = Number.parseInt(linesValue, 10);
147
+ const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
148
+ return readFileSync(file, "utf8")
149
+ .trimEnd()
150
+ .split("\n")
151
+ .filter(Boolean)
152
+ .slice(-count)
153
+ .map((line) => {
154
+ try {
155
+ return JSON.parse(line);
156
+ } catch {
157
+ return { raw: line };
158
+ }
159
+ });
160
+ }
161
+
162
+ function runOpsSnapshot(
163
+ rootValue,
164
+ runIdValue = "music",
165
+ linesValue = "80",
166
+ staleMinutesValue = "60",
167
+ ) {
168
+ const runs = collectRunSummary(rootValue);
169
+ const root = resolve(
170
+ rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
171
+ );
172
+ const inspectedRun = String(runIdValue || "music");
173
+ const messageFile = join(root, inspectedRun, "outbox.jsonl");
174
+ const staleMs = Number(staleMinutesValue) * 60 * 1000;
175
+ const now = Date.now();
176
+ const recommendations = runs.flatMap((run) => {
177
+ const updatedMs = Date.parse(run.updated || "");
178
+ const stale =
179
+ Number.isFinite(updatedMs) &&
180
+ Number.isFinite(staleMs) &&
181
+ now - updatedMs > staleMs;
182
+ if (run.status === "running" && stale) {
183
+ return [
184
+ {
185
+ run: run.run,
186
+ reason: "running-stale",
187
+ suggested_message: {
188
+ to: `run:${run.run}`,
189
+ type: "control.stop",
190
+ body: "stop",
191
+ },
192
+ },
193
+ ];
194
+ }
195
+ if (["failed", "exited", "killed"].includes(run.status)) {
196
+ return [
197
+ {
198
+ run: run.run,
199
+ reason: `terminal-${run.status}`,
200
+ suggested_inspect: { target: `run:${run.run}`, view: "tail" },
201
+ },
202
+ ];
203
+ }
204
+ return [];
205
+ });
206
+ console.log(
207
+ JSON.stringify(
208
+ {
209
+ runs,
210
+ inspectedRun,
211
+ messages: tailJsonl(messageFile, linesValue),
212
+ recommendations,
213
+ },
214
+ null,
215
+ 2,
216
+ ),
217
+ );
218
+ }
219
+
220
+ function playlist(
221
+ sourceValue,
222
+ extensionsValue = ".mp3,.ogg,.wav,.flac,.m4a",
223
+ maxDepthValue = "2",
224
+ outputMode = "paths",
225
+ ) {
226
+ const source = resolve(
227
+ sourceValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
228
+ );
229
+ const maxDepth = Number.parseInt(maxDepthValue, 10);
230
+ const extensions = new Set(
231
+ extensionsValue
232
+ .split(",")
233
+ .map((item) => item.trim().toLowerCase())
234
+ .filter(Boolean),
235
+ );
236
+ const files = walkFiles(source, Number.isFinite(maxDepth) ? maxDepth : 2)
237
+ .filter((file) => extensions.has(extname(file).toLowerCase()))
238
+ .sort((a, b) => a.localeCompare(b));
239
+ if (outputMode === "m3u") console.log(["#EXTM3U", ...files].join("\n"));
240
+ else if (outputMode === "inline") console.log(files.join("|"));
241
+ else console.log(files.join("\n"));
242
+ }
243
+
244
+ function artifactManifest(
245
+ pathValue,
246
+ title = "Artifact",
247
+ status = "draft",
248
+ summary = "",
249
+ ) {
250
+ const path = resolve(
251
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
252
+ );
253
+ const exists = existsSync(path);
254
+ const stat = exists ? statSync(path) : undefined;
255
+ console.log(
256
+ JSON.stringify(
257
+ {
258
+ title,
259
+ status,
260
+ path,
261
+ exists,
262
+ bytes: stat?.size ?? 0,
263
+ modified: stat?.mtime?.toISOString?.() ?? null,
264
+ summary,
265
+ },
266
+ null,
267
+ 2,
268
+ ),
269
+ );
270
+ }
271
+
272
+ function artifactWrite(pathValue, mode = "create") {
273
+ const path = resolve(
274
+ pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
275
+ );
276
+ if (!["create", "overwrite", "append"].includes(mode)) {
277
+ fail(`Invalid artifact write mode: ${mode}`);
278
+ }
279
+ if (mode === "create" && existsSync(path)) {
280
+ fail(`Artifact already exists: ${path}`);
281
+ }
282
+ const content = readFileSync(0, "utf8");
283
+ mkdirSync(dirname(path), { recursive: true });
284
+ if (mode === "append") appendFileSync(path, content);
285
+ else writeFileSync(path, content, "utf8");
286
+ const stat = statSync(path);
287
+ console.log(
288
+ JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2),
289
+ );
290
+ }
291
+
292
+ function actorMessage(
293
+ type = "event",
294
+ to = "coordinator",
295
+ from = "run:{run_id}",
296
+ summary = "",
297
+ metadataValue = "",
298
+ correlationId = "",
299
+ replyTo = "",
300
+ ) {
301
+ const messageType = validateMessageType(type);
302
+ const messageTo = validateActorAddress(to, "message.to");
303
+ const messageFrom = validateActorAddress(from, "message.from");
304
+ let metadata = {};
305
+ if (metadataValue) {
306
+ try {
307
+ const parsed = JSON.parse(metadataValue);
308
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
309
+ metadata = parsed;
310
+ else fail("Actor message metadata must be a JSON object");
311
+ } catch (error) {
312
+ fail(`Invalid actor message metadata JSON: ${error.message}`);
313
+ }
314
+ }
315
+ const bodyText = readFileSync(0, "utf8");
316
+ const trimmed = bodyText.trim();
317
+ let body = bodyText;
318
+ if (trimmed) {
319
+ try {
320
+ body = JSON.parse(trimmed);
321
+ } catch {
322
+ body = bodyText;
323
+ }
324
+ }
325
+ console.log(
326
+ JSON.stringify(
327
+ {
328
+ to: messageTo,
329
+ from: messageFrom,
330
+ type: messageType,
331
+ summary: summary || messageType,
332
+ body,
333
+ ...(correlationId ? { correlation_id: correlationId } : {}),
334
+ ...(replyTo ? { reply_to: replyTo } : {}),
335
+ metadata,
336
+ },
337
+ null,
338
+ 2,
339
+ ),
340
+ );
341
+ }
342
+
343
+ function packageSummary(fileValue = "package.json") {
344
+ const file = resolve(
345
+ fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
346
+ );
347
+ const pkg = readJson(file);
348
+ if (!pkg) fail(`Package JSON not found or invalid: ${fileValue}`);
349
+ const scripts =
350
+ pkg.scripts && typeof pkg.scripts === "object"
351
+ ? Object.keys(pkg.scripts).sort()
352
+ : [];
353
+ const dependencies =
354
+ pkg.dependencies && typeof pkg.dependencies === "object"
355
+ ? Object.keys(pkg.dependencies).sort()
356
+ : [];
357
+ const devDependencies =
358
+ pkg.devDependencies && typeof pkg.devDependencies === "object"
359
+ ? Object.keys(pkg.devDependencies).sort()
360
+ : [];
361
+ console.log(
362
+ JSON.stringify(
363
+ {
364
+ name: pkg.name ?? "",
365
+ version: pkg.version ?? "",
366
+ type: pkg.type ?? "",
367
+ private: Boolean(pkg.private),
368
+ packageManager: pkg.packageManager ?? "",
369
+ files: Array.isArray(pkg.files) ? pkg.files : [],
370
+ bin: pkg.bin ?? null,
371
+ main: pkg.main ?? "",
372
+ exports: pkg.exports ?? null,
373
+ scripts,
374
+ dependencyCount: dependencies.length,
375
+ devDependencyCount: devDependencies.length,
376
+ dependencies,
377
+ devDependencies,
378
+ },
379
+ null,
380
+ 2,
381
+ ),
382
+ );
383
+ }
384
+
385
+ function parseSkillFrontmatter(content) {
386
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
387
+ const fields = {};
388
+ for (const line of frontmatter.split(/\r?\n/)) {
389
+ const match = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
390
+ if (match) fields[match[1]] = match[2].trim();
391
+ }
392
+ return { frontmatter, fields };
393
+ }
394
+
395
+ function skillSummary(skillValue, packageValue = "package.json") {
396
+ const skillFile = resolve(
397
+ skillValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
398
+ );
399
+ const packageFile = resolve(
400
+ packageValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
401
+ );
402
+ const content = readFileSync(skillFile, "utf8");
403
+ const pkg = readJson(packageFile) ?? {};
404
+ const { frontmatter, fields } = parseSkillFrontmatter(content);
405
+ const scalarLines = frontmatter
406
+ .split(/\r?\n/)
407
+ .filter((line) => /^\w+:\s*\S/.test(line));
408
+ const extraColonLines = scalarLines.filter(
409
+ (line) => (line.match(/:/g) ?? []).length > 1,
410
+ );
411
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
412
+ const version =
413
+ fields.version ??
414
+ frontmatter.match(/^\s+version:\s*([^\n]+)\s*$/m)?.[1]?.trim() ??
415
+ "";
416
+ console.log(
417
+ JSON.stringify(
418
+ {
419
+ path: skillValue,
420
+ name: fields.name ?? "",
421
+ description: fields.description ?? "",
422
+ version,
423
+ packageVersion: pkg.version ?? "",
424
+ versionMatchesPackage: version === pkg.version,
425
+ frontmatterExtraColonLines: extraColonLines,
426
+ bodyLineCount: body.split(/\r?\n/).length,
427
+ headings: body.split(/\r?\n/).filter((line) => /^#{1,6}\s/.test(line)),
428
+ },
429
+ null,
430
+ 2,
431
+ ),
432
+ );
433
+ }
434
+
435
+ function changelogSection(fileValue, version) {
436
+ const file = resolve(fileValue);
437
+ const lines = readFileSync(file, "utf8").split(/\r?\n/);
438
+ const start = lines.findIndex(
439
+ (line) => line.startsWith("## ") && line.includes(version),
440
+ );
441
+ if (start < 0) fail(`Version section not found: ${version}`);
442
+ let end = lines.length;
443
+ for (let index = start + 1; index < lines.length; index += 1) {
444
+ if (lines[index].startsWith("## ")) {
445
+ end = index;
446
+ break;
447
+ }
448
+ }
449
+ console.log(lines.slice(start, end).join("\n").trimEnd());
450
+ }
451
+
452
+ export function runRecipeUtils(argv = process.argv.slice(2)) {
453
+ const [command, ...args] = argv;
454
+ if (!command) {
455
+ usage();
456
+ process.exit(1);
457
+ }
458
+
459
+ if (command === "run-summary")
460
+ runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
461
+ else if (command === "run-ops-snapshot")
462
+ runOpsSnapshot(
463
+ args[0] ?? "~/.pi/agent/tmp/pi-actors/runs",
464
+ args[1] ?? "music",
465
+ args[2],
466
+ args[3],
467
+ );
468
+ else if (command === "playlist")
469
+ playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
470
+ else if (command === "changelog-section")
471
+ changelogSection(args[0] ?? "CHANGELOG.md", args[1] ?? "Unreleased");
472
+ else if (command === "artifact-manifest")
473
+ artifactManifest(args[0] ?? "artifact.md", args[1], args[2], args[3]);
474
+ else if (command === "artifact-write")
475
+ artifactWrite(args[0] ?? "artifact.md", args[1] ?? "create");
476
+ else if (command === "actor-message")
477
+ actorMessage(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
478
+ else if (command === "package-summary")
479
+ packageSummary(args[0] ?? "package.json");
480
+ else if (command === "skill-summary")
481
+ skillSummary(args[0] ?? "skills/actors/SKILL.md", args[1] ?? "package.json");
482
+ else {
483
+ usage();
484
+ fail(`Unknown command: ${command}`);
485
+ }
486
+ }
@@ -16,6 +16,8 @@ import {
16
16
  } from "node:fs";
17
17
  import { basename, dirname, join } from "node:path";
18
18
 
19
+ import { readJsonlFileResilient } from "./state-readers.ts";
20
+
19
21
  export interface RuntimeWakeEvent {
20
22
  actor: string;
21
23
  id: string;
@@ -124,12 +126,8 @@ export function parseRuntimeWakeEventLine(
124
126
  }
125
127
 
126
128
  export function readRuntimeWakeEvents(stateDir: string): RuntimeWakeEvent[] {
127
- const file = runtimeWakeFile(stateDir);
128
- if (!existsSync(file)) return [];
129
- return readFileSync(file, "utf8")
130
- .split("\n")
131
- .filter((line) => line.trim())
132
- .map(parseRuntimeWakeEventLine)
129
+ return readJsonlFileResilient<Record<string, unknown>>(runtimeWakeFile(stateDir))
130
+ .records.map((record) => parseRuntimeWakeEventLine(JSON.stringify(record)))
133
131
  .filter((event): event is RuntimeWakeEvent => Boolean(event));
134
132
  }
135
133