@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
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Detached async-runner entrypoint logic.
3
+ * Zones: async run process, command-template execution telemetry
4
+ */
5
+
6
+ import { appendFileSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ import { appendRecipeContextToPiArgs } from "./actor-recipe-context.ts";
10
+ import { execCommandTemplate } from "./command-templates.ts";
11
+ import { executeRegisteredTool, type ToolExecOptions } from "./execution.ts";
12
+ import { writeJsonAtomic } from "./file-state.ts";
13
+
14
+ function quoteCommandDetailPart(value: string): string {
15
+ if (value === "") return "''";
16
+ if (/^[A-Za-z0-9_/:=.,@%+\-]+$/.test(value)) return value;
17
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
18
+ }
19
+
20
+ function formatCommandDetail(command: string, args: string[]): string {
21
+ return [command, ...args].map(quoteCommandDetailPart).join(" ");
22
+ }
23
+
24
+ function summarizeCommandDetail(commandDetail: string): string {
25
+ return commandDetail.length > 160
26
+ ? `${commandDetail.slice(0, 157)}...`
27
+ : commandDetail;
28
+ }
29
+
30
+ export async function runAsyncRunner(stateDir = process.argv[2]): Promise<void> {
31
+ if (!stateDir) throw new Error("missing state dir");
32
+
33
+ const runPath = join(stateDir, "run.json");
34
+ const progressPath = join(stateDir, "progress.json");
35
+ const resultPath = join(stateDir, "result.json");
36
+ const eventsPath = join(stateDir, "events.jsonl");
37
+ const outboxPath = join(stateDir, "outbox.jsonl");
38
+ const stdoutPath = join(stateDir, "stdout.log");
39
+ const stderrPath = join(stateDir, "stderr.log");
40
+ const meta = JSON.parse(readFileSync(runPath, "utf8"));
41
+
42
+ function event(name: string, data: Record<string, unknown> = {}): void {
43
+ appendFileSync(
44
+ eventsPath,
45
+ `${JSON.stringify({ event: name, ts: new Date().toISOString(), ...data })}\n`,
46
+ );
47
+ }
48
+
49
+ function outbox(
50
+ name: string,
51
+ summary: string,
52
+ data: Record<string, unknown> = {},
53
+ delivery = "log",
54
+ level = "info",
55
+ ): void {
56
+ appendFileSync(
57
+ outboxPath,
58
+ `${JSON.stringify({ body: data, data, delivery, event: name, from: `run:${meta.run}`, level, summary, to: "coordinator", ts: new Date().toISOString(), type: name })}\n`,
59
+ );
60
+ }
61
+
62
+ function progress(phase: string, extra: Record<string, unknown> = {}): void {
63
+ writeJsonAtomic(progressPath, {
64
+ phase,
65
+ updatedAt: new Date().toISOString(),
66
+ ...extra,
67
+ });
68
+ }
69
+
70
+ let activeSubagents = 0;
71
+ let completedSubagents = 0;
72
+ const subagentFailures: Record<string, unknown>[] = [];
73
+
74
+ function getCommandDoneDelivery(result: { code: number }): string {
75
+ return result.code !== 0 || activeSubagents > 0 ? "followup" : "log";
76
+ }
77
+
78
+ function progressRunning(): void {
79
+ progress("running", {
80
+ activeSubagents,
81
+ completed: completedSubagents,
82
+ failures: subagentFailures,
83
+ });
84
+ }
85
+
86
+ async function observedExec(command: string, args: string[], options?: ToolExecOptions) {
87
+ const commandDetail = formatCommandDetail(command, args);
88
+ const execArgs = appendRecipeContextToPiArgs(
89
+ command,
90
+ args,
91
+ meta.recipe_context_records,
92
+ options?.actorRecipeContext,
93
+ );
94
+ activeSubagents += 1;
95
+ event("command.start", { activeSubagents, command: commandDetail });
96
+ progressRunning();
97
+ const result = await execCommandTemplate(command, execArgs, options);
98
+ activeSubagents = Math.max(0, activeSubagents - 1);
99
+ completedSubagents += 1;
100
+ if (result.code !== 0) {
101
+ subagentFailures.push({
102
+ code: result.code,
103
+ command: commandDetail,
104
+ killed: result.killed,
105
+ });
106
+ }
107
+ event("command.done", {
108
+ activeSubagents,
109
+ code: result.code,
110
+ command: commandDetail,
111
+ killed: result.killed,
112
+ });
113
+ outbox(
114
+ "command.done",
115
+ `Command ${summarizeCommandDetail(commandDetail)} completed with code ${result.code}`,
116
+ {
117
+ activeSubagents,
118
+ ...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
119
+ run_files: [stdoutPath, stderrPath, resultPath, eventsPath, outboxPath],
120
+ code: result.code,
121
+ command: commandDetail,
122
+ killed: result.killed,
123
+ },
124
+ getCommandDoneDelivery(result),
125
+ result.code === 0 ? "info" : "error",
126
+ );
127
+ progressRunning();
128
+ return result;
129
+ }
130
+
131
+ try {
132
+ event("run.runner.start", { pid: process.pid });
133
+ progressRunning();
134
+ const result = await executeRegisteredTool(
135
+ {
136
+ name: "run_actor",
137
+ description: "Detached command-template run actor",
138
+ template: meta.template,
139
+ args: [],
140
+ defaults: {},
141
+ },
142
+ meta.values || {},
143
+ observedExec,
144
+ meta.cwd,
145
+ );
146
+ const text = result.content?.[0]?.text || "";
147
+ appendFileSync(stdoutPath, text);
148
+ writeJsonAtomic(resultPath, {
149
+ code: result.details.code,
150
+ command: result.details.command,
151
+ killed: result.details.killed,
152
+ truncated: result.details.truncated,
153
+ completedAt: new Date().toISOString(),
154
+ });
155
+ progress("done", {
156
+ completed: 1,
157
+ failures: result.details.nonCriticalFailures || [],
158
+ });
159
+ event("run.done", { code: result.details.code });
160
+ } catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ appendFileSync(stderrPath, `${message}\n`);
163
+ writeJsonAtomic(resultPath, {
164
+ code: 1,
165
+ error: message,
166
+ killed: false,
167
+ completedAt: new Date().toISOString(),
168
+ });
169
+ progress("failed", { completed: 0, failures: [{ message }] });
170
+ event("run.failed", { error: message });
171
+ throw error;
172
+ }
173
+ }
package/lib/async-runs.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { spawn, spawnSync } from "node:child_process";
8
- import { randomUUID } from "node:crypto";
8
+ import { createHash, randomUUID } from "node:crypto";
9
9
  import {
10
10
  closeSync,
11
11
  constants,
@@ -14,7 +14,9 @@ import {
14
14
  openSync,
15
15
  readdirSync,
16
16
  readFileSync,
17
+ cpSync,
17
18
  readlinkSync,
19
+ renameSync,
18
20
  rmSync,
19
21
  statSync,
20
22
  writeFileSync,
@@ -31,6 +33,7 @@ import type {
31
33
  } from "./command-templates.ts";
32
34
  import { substituteCommandTemplateToken } from "./command-templates.ts";
33
35
  import { writeJsonAtomic } from "./file-state.ts";
36
+ import { readJsonFileResilient, readJsonlFileResilient } from "./state-readers.ts";
34
37
  import * as Paths from "./paths.ts";
35
38
  import * as RecipeReferences from "./recipe-references.ts";
36
39
  import * as RecipeUsage from "./recipe-usage.ts";
@@ -65,7 +68,7 @@ export interface AsyncRunStartParams {
65
68
  timeout?: number | string;
66
69
  delay?: number | string;
67
70
  output?: string;
68
- artifacts?: Record<string, string>;
71
+ artifacts?: Record<string, RunArtifactDeclaration>;
69
72
  mailbox?: RecipeReferences.TemplateRecipeMailbox;
70
73
  retire_when?: "children_terminal";
71
74
  retry?: number | string;
@@ -121,7 +124,7 @@ export interface AsyncRunMeta {
121
124
  tool?: string;
122
125
  template: CommandTemplateValue;
123
126
  values: Record<string, unknown>;
124
- artifacts?: Record<string, string>;
127
+ artifacts?: Record<string, RunArtifactDeclaration>;
125
128
  control?: AsyncRunControlEndpoint;
126
129
  mailbox?: RecipeReferences.TemplateRecipeMailbox;
127
130
  recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
@@ -160,23 +163,93 @@ function safeRunId(value: string | undefined): string {
160
163
  return run;
161
164
  }
162
165
 
166
+ export type RunArtifactDeclaration =
167
+ | string
168
+ | { path: string; kind?: string; media_type?: string; required?: boolean };
169
+
170
+ export interface RunArtifactManifestEntry {
171
+ exists: boolean;
172
+ kind?: string;
173
+ media_type?: string;
174
+ path: string;
175
+ required?: boolean;
176
+ sha256?: string;
177
+ size?: number;
178
+ }
179
+
163
180
  function resolveArtifactPaths(
164
- artifacts: Record<string, string> | undefined,
181
+ artifacts: Record<string, RunArtifactDeclaration> | undefined,
165
182
  values: Record<string, unknown>,
166
- ): Record<string, string> | undefined {
183
+ ): Record<string, RunArtifactDeclaration> | undefined {
167
184
  if (!artifacts) return undefined;
168
- const resolved: Record<string, string> = {};
185
+ const resolved: Record<string, RunArtifactDeclaration> = {};
169
186
  for (const [key, value] of Object.entries(artifacts)) {
170
187
  if (!key.trim()) continue;
171
- resolved[key] = substituteCommandTemplateToken(
172
- value,
173
- values,
174
- `recipe artifacts.${key}`,
175
- );
188
+ if (typeof value === "string") {
189
+ resolved[key] = substituteCommandTemplateToken(
190
+ value,
191
+ values,
192
+ `recipe artifacts.${key}`,
193
+ );
194
+ } else if (
195
+ value &&
196
+ typeof value === "object" &&
197
+ typeof value.path === "string"
198
+ ) {
199
+ resolved[key] = {
200
+ ...value,
201
+ path: substituteCommandTemplateToken(
202
+ value.path,
203
+ values,
204
+ `recipe artifacts.${key}.path`,
205
+ ),
206
+ };
207
+ }
176
208
  }
177
209
  return Object.keys(resolved).length > 0 ? resolved : undefined;
178
210
  }
179
211
 
212
+ export function resolveArtifactManifest(
213
+ artifacts: Record<string, RunArtifactDeclaration> | undefined,
214
+ ): Record<string, RunArtifactManifestEntry> | undefined {
215
+ if (!artifacts) return undefined;
216
+ const manifest: Record<string, RunArtifactManifestEntry> = {};
217
+ for (const [name, artifact] of Object.entries(artifacts)) {
218
+ const declaration =
219
+ typeof artifact === "string" ? { path: artifact } : artifact;
220
+ if (!declaration?.path) continue;
221
+ try {
222
+ const content = readFileSync(declaration.path);
223
+ manifest[name] = {
224
+ exists: true,
225
+ ...(declaration.kind ? { kind: declaration.kind } : {}),
226
+ ...(declaration.media_type
227
+ ? { media_type: declaration.media_type }
228
+ : {}),
229
+ path: declaration.path,
230
+ ...(declaration.required !== undefined
231
+ ? { required: declaration.required }
232
+ : {}),
233
+ sha256: createHash("sha256").update(content).digest("hex"),
234
+ size: content.byteLength,
235
+ };
236
+ } catch {
237
+ manifest[name] = {
238
+ exists: false,
239
+ ...(declaration.kind ? { kind: declaration.kind } : {}),
240
+ ...(declaration.media_type
241
+ ? { media_type: declaration.media_type }
242
+ : {}),
243
+ path: declaration.path,
244
+ ...(declaration.required !== undefined
245
+ ? { required: declaration.required }
246
+ : {}),
247
+ };
248
+ }
249
+ }
250
+ return Object.keys(manifest).length > 0 ? manifest : undefined;
251
+ }
252
+
180
253
  function resolveRunTemplate(params: AsyncRunStartParams): {
181
254
  template: CommandTemplateValue;
182
255
  } {
@@ -273,11 +346,10 @@ function resolveStartParams(params: AsyncRunStartParams): AsyncRunStartParams {
273
346
  }
274
347
 
275
348
  function readJson(path: string): Record<string, unknown> | undefined {
276
- try {
277
- return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
278
- } catch {
279
- return undefined;
280
- }
349
+ return readJsonFileResilient<Record<string, unknown> | undefined>(
350
+ path,
351
+ undefined,
352
+ ).value;
281
353
  }
282
354
 
283
355
  function isAlive(pid: number): boolean {
@@ -320,16 +392,12 @@ function tailLines(path: string, lines: number): string[] {
320
392
  function getInterruptedRunStatus(
321
393
  stateDir: string,
322
394
  ): "cancelled" | "killed" | undefined {
323
- const events = tailFile(join(stateDir, "events.jsonl"), 200);
324
- if (!events) return undefined;
325
- for (const line of events.split("\n").reverse()) {
326
- try {
327
- const event = JSON.parse(line) as Record<string, unknown>;
328
- if (event.event === "run.kill") return "killed";
329
- if (event.event === "run.cancel") return "cancelled";
330
- } catch {
331
- // Ignore malformed event lines.
332
- }
395
+ const events = readJsonlFileResilient<Record<string, unknown>>(
396
+ join(stateDir, "events.jsonl"),
397
+ ).records.slice(-200);
398
+ for (const event of events.reverse()) {
399
+ if (event.event === "run.kill") return "killed";
400
+ if (event.event === "run.cancel") return "cancelled";
333
401
  }
334
402
  return undefined;
335
403
  }
@@ -426,7 +494,11 @@ export function startRun(
426
494
  ? RecipeReferences.buildRecipeContextRecords(recipeFile)
427
495
  : undefined;
428
496
  if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
429
- RecipeUsage.recordRecipeLaunch(recipeFile);
497
+ RecipeUsage.recordRecipeLaunch(
498
+ recipeFile,
499
+ new Date(),
500
+ startParams.launch_source === "tool" ? "tool" : "spawn",
501
+ );
430
502
  }
431
503
  const outFd = openSync(stdout, "a");
432
504
  const errFd = openSync(stderr, "a");
@@ -611,6 +683,16 @@ export function getRunStatus(runOrDir: string): Record<string, unknown> {
611
683
  };
612
684
  }
613
685
 
686
+ export interface RunStateIndexEntry {
687
+ ownerId?: string;
688
+ recipe?: string;
689
+ run: string;
690
+ state_dir: string;
691
+ status: AsyncRunStatus;
692
+ tool?: string;
693
+ updated_at?: string;
694
+ }
695
+
614
696
  function matchesStatusFilter(
615
697
  status: unknown,
616
698
  filter: string | undefined,
@@ -621,30 +703,125 @@ function matchesStatusFilter(
621
703
  return status === filter;
622
704
  }
623
705
 
706
+ export function listRunStateDirs(
707
+ stateRoot = DEFAULT_STATE_ROOT,
708
+ depth = 0,
709
+ seen = new Set<string>(),
710
+ ): string[] {
711
+ if (!existsSync(stateRoot) || seen.has(resolve(stateRoot))) return [];
712
+ seen.add(resolve(stateRoot));
713
+ let entries: import("node:fs").Dirent[];
714
+ try {
715
+ entries = readdirSync(stateRoot, { withFileTypes: true });
716
+ } catch {
717
+ return [];
718
+ }
719
+ const result: string[] = [];
720
+ for (const entry of entries) {
721
+ if (!entry.isDirectory()) continue;
722
+ const child = join(stateRoot, entry.name);
723
+ if (existsSync(join(child, "run.json"))) result.push(child);
724
+ if (depth + 1 < 8) result.push(...listRunStateDirs(child, depth + 1, seen));
725
+ }
726
+ return result;
727
+ }
728
+
729
+ function runIndexPath(stateRoot: string): string {
730
+ return join(stateRoot, "index.json");
731
+ }
732
+
733
+ function indexEntryFromStatus(
734
+ status: Record<string, unknown>,
735
+ ): RunStateIndexEntry {
736
+ const progress =
737
+ status.progress && typeof status.progress === "object"
738
+ ? (status.progress as Record<string, unknown>)
739
+ : {};
740
+ return {
741
+ ...(typeof status.ownerId === "string" ? { ownerId: status.ownerId } : {}),
742
+ ...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
743
+ run: String(status.run),
744
+ state_dir: String(status.state_dir),
745
+ status: status.status as AsyncRunStatus,
746
+ ...(typeof status.tool === "string" ? { tool: status.tool } : {}),
747
+ updated_at:
748
+ typeof progress.updatedAt === "string"
749
+ ? progress.updatedAt
750
+ : typeof status.createdAt === "string"
751
+ ? status.createdAt
752
+ : new Date(0).toISOString(),
753
+ };
754
+ }
755
+
756
+ export function rebuildRunStateIndex(
757
+ stateRoot = DEFAULT_STATE_ROOT,
758
+ ): RunStateIndexEntry[] {
759
+ mkdirSync(stateRoot, { recursive: true });
760
+ const entries = listRunStateDirs(stateRoot)
761
+ .flatMap((stateDir) => {
762
+ try {
763
+ return [indexEntryFromStatus(getRunStatus(stateDir))];
764
+ } catch {
765
+ return [];
766
+ }
767
+ })
768
+ .sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? ""));
769
+ writeJsonAtomic(runIndexPath(stateRoot), {
770
+ entries,
771
+ rebuilt_at: new Date().toISOString(),
772
+ });
773
+ return entries;
774
+ }
775
+
776
+ export function readRunStateIndex(
777
+ stateRoot = DEFAULT_STATE_ROOT,
778
+ ): RunStateIndexEntry[] | undefined {
779
+ const index = readJson(runIndexPath(stateRoot));
780
+ if (!index || typeof index !== "object") return undefined;
781
+ const entries = (index as Record<string, unknown>).entries;
782
+ if (!Array.isArray(entries)) return undefined;
783
+ const valid = entries.filter((entry): entry is RunStateIndexEntry => {
784
+ const record =
785
+ entry && typeof entry === "object"
786
+ ? (entry as Record<string, unknown>)
787
+ : {};
788
+ return (
789
+ typeof record.run === "string" &&
790
+ typeof record.state_dir === "string" &&
791
+ typeof record.status === "string"
792
+ );
793
+ });
794
+ if (valid.some((entry) => !existsSync(join(entry.state_dir, "run.json"))))
795
+ return undefined;
796
+ return valid;
797
+ }
798
+
624
799
  export function listRuns(
625
800
  stateRoot = DEFAULT_STATE_ROOT,
626
801
  statusFilter?: string,
627
802
  ): Array<Record<string, unknown>> {
628
803
  if (!existsSync(stateRoot)) return [];
629
- const runs: Array<Record<string, unknown>> = [];
630
- for (const entry of readdirSync(stateRoot, { withFileTypes: true })) {
631
- if (!entry.isDirectory()) continue;
632
- try {
633
- const stateDir = join(stateRoot, entry.name);
634
- const status = getRunStatus(stateDir);
635
- if (!matchesStatusFilter(status.status, statusFilter)) continue;
636
- runs.push({
637
- run: status.run,
638
- state_dir: stateDir,
639
- status: status.status,
640
- ...(typeof status.tool === "string" ? { tool: status.tool } : {}),
641
- ...(typeof status.recipe === "string" ? { recipe: status.recipe } : {}),
642
- });
643
- } catch {
644
- // Ignore malformed run dirs.
645
- }
804
+ const indexed = readRunStateIndex(stateRoot);
805
+ if (indexed) {
806
+ return indexed
807
+ .filter((entry) => matchesStatusFilter(entry.status, statusFilter))
808
+ .map((entry) => ({
809
+ run: entry.run,
810
+ state_dir: entry.state_dir,
811
+ status: entry.status,
812
+ ...(entry.tool ? { tool: entry.tool } : {}),
813
+ ...(entry.recipe ? { recipe: entry.recipe } : {}),
814
+ }));
646
815
  }
647
- return runs;
816
+ return rebuildRunStateIndex(stateRoot)
817
+ .filter((entry) => matchesStatusFilter(entry.status, statusFilter))
818
+ .map((entry) => ({
819
+ run: entry.run,
820
+ state_dir: entry.state_dir,
821
+ status: entry.status,
822
+ ...(entry.tool ? { tool: entry.tool } : {}),
823
+ ...(entry.recipe ? { recipe: entry.recipe } : {}),
824
+ }));
648
825
  }
649
826
 
650
827
  export function tailRun(runOrDir: string, lines = 40): string {
@@ -732,13 +909,7 @@ function parseRunInboxLine(line: string): RunInboxMessage | undefined {
732
909
  }
733
910
 
734
911
  function readRunInboxMessagesFromStateDir(stateDir: string): RunInboxMessage[] {
735
- const file = runInboxFile(stateDir);
736
- if (!existsSync(file)) return [];
737
- return readFileSync(file, "utf8")
738
- .split("\n")
739
- .filter((line) => line.trim())
740
- .map(parseRunInboxLine)
741
- .filter((message): message is RunInboxMessage => Boolean(message));
912
+ return readJsonlFileResilient<RunInboxMessage>(runInboxFile(stateDir)).records;
742
913
  }
743
914
 
744
915
  function writeRunInboxMessages(
@@ -879,12 +1050,19 @@ export function appendRunOutboxEvent(
879
1050
  const run = String(status.run ?? runOrDir);
880
1051
  const type = event.type || event.event || "run.message";
881
1052
  const to = event.to || "coordinator";
1053
+ const metadata = event.metadata ?? {};
1054
+ const requiresResponse = metadata.requires_response === true;
882
1055
  const payload = {
883
1056
  ...(event.body !== undefined ? { body: event.body } : {}),
884
1057
  ...(event.correlation_id ? { correlation_id: event.correlation_id } : {}),
885
1058
  ...(event.data !== undefined ? { data: event.data } : {}),
886
1059
  delivery: normalizeRunOutboxDelivery(
887
- event.delivery ?? (to === "coordinator" ? "followup" : "log"),
1060
+ event.delivery ??
1061
+ (requiresResponse
1062
+ ? "followup"
1063
+ : to === "coordinator"
1064
+ ? "notify"
1065
+ : "log"),
888
1066
  ),
889
1067
  event: type,
890
1068
  from: event.from || `run:${run}`,
@@ -1242,6 +1420,77 @@ export function cancelRun(runOrDir: string): Record<string, unknown> {
1242
1420
  : result;
1243
1421
  }
1244
1422
 
1423
+ function assertTerminalRun(runOrDir: string): Record<string, unknown> {
1424
+ const status = getRunStatus(runOrDir);
1425
+ if (status.status === "running") {
1426
+ throw new Error("Only terminal runs can be archived or pruned.");
1427
+ }
1428
+ return status;
1429
+ }
1430
+
1431
+ function archivePathFor(run: string, stateDir: string): string {
1432
+ const archiveRoot = join(dirname(stateDir), "archived");
1433
+ mkdirSync(archiveRoot, { recursive: true });
1434
+ return join(
1435
+ archiveRoot,
1436
+ `${safeRunId(run)}-${new Date().toISOString().replace(/[:.]/g, "-")}`,
1437
+ );
1438
+ }
1439
+
1440
+ export function archiveRun(runOrDir: string): Record<string, unknown> {
1441
+ const status = assertTerminalRun(runOrDir);
1442
+ const stateDir = String(status.state_dir);
1443
+ const run = String(status.run ?? basename(stateDir));
1444
+ const archiveDir = archivePathFor(run, stateDir);
1445
+ renameSync(stateDir, archiveDir);
1446
+ mkdirSync(stateDir, { recursive: true });
1447
+ const tombstone = {
1448
+ archived: true,
1449
+ archive_dir: archiveDir,
1450
+ original_state_dir: stateDir,
1451
+ run,
1452
+ status: status.status,
1453
+ ts: new Date().toISOString(),
1454
+ };
1455
+ writeJsonAtomic(join(stateDir, "archive-tombstone.json"), tombstone);
1456
+ return tombstone;
1457
+ }
1458
+
1459
+ export function pruneRun(
1460
+ runOrDir: string,
1461
+ options: { preserveArtifacts?: boolean } = {},
1462
+ ): Record<string, unknown> {
1463
+ const status = assertTerminalRun(runOrDir);
1464
+ const stateDir = String(status.state_dir);
1465
+ const run = String(status.run ?? basename(stateDir));
1466
+ const manifest = resolveArtifactManifest(
1467
+ status.artifacts as Record<string, RunArtifactDeclaration> | undefined,
1468
+ );
1469
+ const preserved: Record<string, string> = {};
1470
+ if (options.preserveArtifacts && manifest) {
1471
+ const preserveRoot = join(
1472
+ dirname(stateDir),
1473
+ "preserved-artifacts",
1474
+ safeRunId(run),
1475
+ );
1476
+ mkdirSync(preserveRoot, { recursive: true });
1477
+ for (const [name, artifact] of Object.entries(manifest)) {
1478
+ if (!artifact.exists) continue;
1479
+ const target = join(preserveRoot, basename(artifact.path));
1480
+ cpSync(artifact.path, target, { force: true });
1481
+ preserved[name] = target;
1482
+ }
1483
+ }
1484
+ rmSync(stateDir, { recursive: true, force: true });
1485
+ return {
1486
+ pruned: true,
1487
+ preserved_artifacts: preserved,
1488
+ run,
1489
+ state_dir: stateDir,
1490
+ ts: new Date().toISOString(),
1491
+ };
1492
+ }
1493
+
1245
1494
  export function killRun(runOrDir: string): Record<string, unknown> {
1246
1495
  const result = stopRun(runOrDir, "SIGKILL", "run.kill");
1247
1496
  return Object.hasOwn(result, "stopped")
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dist build pipeline entrypoint logic.
3
+ * Zones: packaging, JavaScript-only distributive tree
4
+ */
5
+
6
+ import { spawnSync } from "node:child_process";
7
+ import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ function run(command: string, args: string[]): void {
11
+ const result = spawnSync(command, args, { stdio: "inherit" });
12
+ if (result.status !== 0) process.exit(result.status ?? 1);
13
+ }
14
+
15
+ export function buildDist(): void {
16
+ rmSync("dist", { recursive: true, force: true });
17
+ mkdirSync("dist", { recursive: true });
18
+
19
+ run("tsc", ["-p", "tsconfig.build.json"]);
20
+
21
+ for (const dir of ["scripts", "recipes", "fixtures", "skills"]) {
22
+ cpSync(dir, join("dist", dir), { recursive: true });
23
+ }
24
+
25
+ const builtScripts = readdirSync(join("dist", "scripts"))
26
+ .filter((name) => name.endsWith(".mjs"))
27
+ .map((name) => join("dist", "scripts", name));
28
+
29
+ run(process.execPath, ["--check", ...builtScripts]);
30
+ }