@llblab/pi-actors 0.42.0 → 0.42.2

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 (54) hide show
  1. package/AGENTS.md +2 -2
  2. package/BACKLOG.md +1 -5
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +1 -1
  5. package/dist/lib/async-runs.d.ts +12 -0
  6. package/dist/lib/async-runs.js +53 -12
  7. package/dist/lib/command-templates.js +1 -1
  8. package/dist/lib/inspector-overlay.d.ts +5 -1
  9. package/dist/lib/inspector-overlay.js +104 -36
  10. package/dist/lib/inspector.js +1 -1
  11. package/dist/lib/observability.d.ts +12 -1
  12. package/dist/lib/observability.js +159 -80
  13. package/dist/lib/prompts.d.ts +1 -1
  14. package/dist/lib/prompts.js +1 -1
  15. package/dist/lib/runs-control.d.ts +2 -0
  16. package/dist/lib/runs-control.js +14 -1
  17. package/dist/lib/runs-ownership.js +17 -3
  18. package/dist/lib/runs-process.js +4 -3
  19. package/dist/lib/runs-start.js +1 -0
  20. package/dist/lib/runs-status.js +3 -0
  21. package/dist/lib/tools-inspect.js +2 -1
  22. package/dist/lib/tools-local.js +17 -2
  23. package/dist/lib/tools-spawn.js +10 -1
  24. package/dist/scripts/async-runner.mjs +24 -24
  25. package/dist/scripts/build-dist.mjs +6 -1
  26. package/dist/scripts/conformance.mjs +6 -1
  27. package/dist/scripts/recipe-utils.mjs +3 -3
  28. package/dist/skills/actors/SKILL.md +3 -3
  29. package/dist/skills/swarm/SKILL.md +1 -1
  30. package/docs/actor-inspector.md +3 -3
  31. package/docs/async-runs.md +10 -4
  32. package/docs/recipe-library.md +1 -1
  33. package/docs/tool-registry.md +2 -0
  34. package/lib/async-runs.ts +72 -12
  35. package/lib/command-templates.ts +1 -1
  36. package/lib/inspector-overlay.ts +129 -36
  37. package/lib/inspector.ts +1 -1
  38. package/lib/observability.ts +194 -76
  39. package/lib/prompts.ts +1 -1
  40. package/lib/runs-control.ts +20 -1
  41. package/lib/runs-ownership.ts +22 -3
  42. package/lib/runs-process.ts +4 -3
  43. package/lib/runs-start.ts +1 -0
  44. package/lib/runs-status.ts +5 -0
  45. package/lib/tools-inspect.ts +2 -1
  46. package/lib/tools-local.ts +21 -2
  47. package/lib/tools-spawn.ts +14 -1
  48. package/package.json +4 -3
  49. package/scripts/async-runner.mjs +24 -24
  50. package/scripts/build-dist.mjs +6 -1
  51. package/scripts/conformance.mjs +6 -1
  52. package/scripts/recipe-utils.mjs +3 -3
  53. package/skills/actors/SKILL.md +3 -3
  54. package/skills/swarm/SKILL.md +1 -1
package/lib/prompts.ts CHANGED
@@ -29,7 +29,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
29
29
  - Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
30
30
  - Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.
31
31
  - Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.
32
- - Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.
32
+ - Run state lives under ~/.pi/agent/tmp/pi-actors/runs. Inspect intentionally and avoid busy-polling. Terminal and coordinator-bound notifications queue as Pi follow-ups so concurrently completed actors can reach the coordinator after current work instead of steering between tool calls. Terminal follow-up content stays minimal: run, status, one base path, and relative artifact names only; semantic output stays in non-LLM details and run state. When a deferred actor result gates the next step, wait for its terminal follow-up; do not schedule continuation loops, repeatedly inspect, or mutate its reviewed scope while it runs. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue/stuck run.
33
33
  - Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
34
34
  - Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.
35
35
  - For any non-trivial actor use or pi-actors change, read the bundled actors skill first. Before launching multiple actors/subagents for parallel implementation, independent artifact generation, delegated audit, or review, also read the bundled swarm skill; the coordinator owns decomposition, disjoint scopes, launch correctness, integration, and final validation. For deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
@@ -42,6 +42,7 @@ export function getRunProcessSignalPlan(
42
42
  export interface RunProcessSignalDeps {
43
43
  killProcess?: typeof process.kill;
44
44
  runtimePlatform?: NodeJS.Platform;
45
+ spawnProcess?: typeof spawnSync;
45
46
  verifyIdentity?: (
46
47
  pid: number,
47
48
  expected: RunProcessIdentity,
@@ -70,8 +71,26 @@ export function signalOwnedRunProcess(
70
71
  }
71
72
  const plan = getRunProcessSignalPlan(pid, signal, runtimePlatform);
72
73
  if (plan.command && plan.args) {
73
- const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
74
+ const spawnProcess = deps.spawnProcess ?? spawnSync;
75
+ let result = spawnProcess(plan.command, plan.args, { encoding: "utf8" });
76
+ if (
77
+ runtimePlatform === "win32" &&
78
+ result.status !== 0 &&
79
+ !plan.args.includes("/F")
80
+ ) {
81
+ result = spawnProcess(plan.command, [...plan.args, "/F"], {
82
+ encoding: "utf8",
83
+ });
84
+ }
74
85
  if (result.status !== 0) {
86
+ if (expectedIdentity) {
87
+ const finalProof = (deps.verifyIdentity ?? verifyRunProcessIdentity)(
88
+ pid,
89
+ expectedIdentity,
90
+ runtimePlatform,
91
+ );
92
+ if (finalProof.status === "dead_pid") return plan;
93
+ }
75
94
  throw new Error(
76
95
  result.stderr?.trim() ||
77
96
  result.stdout?.trim() ||
@@ -12,7 +12,8 @@ import {
12
12
  realpathSync,
13
13
  } from "node:fs";
14
14
  import { randomUUID } from "node:crypto";
15
- import { join, resolve } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import { isAbsolute, join, relative, resolve } from "node:path";
16
17
 
17
18
  import { writeJsonAtomic } from "./file-state.ts";
18
19
 
@@ -35,13 +36,31 @@ function comparablePath(path: string): string {
35
36
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
36
37
  }
37
38
 
39
+ function isSystemTempRootAlias(resolved: string, canonical: string): boolean {
40
+ const tempRoot = resolve(tmpdir());
41
+ const relativeStateDir = relative(tempRoot, resolved);
42
+ if (
43
+ relativeStateDir === ".." ||
44
+ relativeStateDir.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
45
+ isAbsolute(relativeStateDir)
46
+ ) {
47
+ return false;
48
+ }
49
+ const canonicalTempRoot = realpathSync.native(tempRoot);
50
+ const expectedCanonical = resolve(canonicalTempRoot, relativeStateDir);
51
+ return comparablePath(canonical) === comparablePath(expectedCanonical);
52
+ }
53
+
38
54
  function assertCanonicalDirectory(stateDir: string): string {
39
55
  const resolved = resolve(stateDir);
40
56
  if (lstatSync(resolved).isSymbolicLink()) {
41
57
  throw new Error(`Run state directory cannot be a symlink: ${resolved}`);
42
58
  }
43
- const canonical = realpathSync(resolved);
44
- if (comparablePath(canonical) !== comparablePath(resolved)) {
59
+ const canonical = realpathSync.native(resolved);
60
+ if (
61
+ comparablePath(canonical) !== comparablePath(resolved) &&
62
+ !isSystemTempRootAlias(resolved, canonical)
63
+ ) {
45
64
  throw new Error(`Run state directory has an ambiguous symlink alias: ${resolved}`);
46
65
  }
47
66
  return resolved;
@@ -6,7 +6,7 @@
6
6
  import { spawnSync } from "node:child_process";
7
7
  import { existsSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
8
8
  import { platform } from "node:os";
9
- import { resolve } from "node:path";
9
+ import { posix, win32 } from "node:path";
10
10
 
11
11
  export interface RunProcessIdentity {
12
12
  command: string;
@@ -125,8 +125,9 @@ export function captureRunProcessIdentity(
125
125
  if (!identity.command.includes(runnerPath) || !identity.command.includes(stateDir)) {
126
126
  return undefined;
127
127
  }
128
- const resolvedCwd = resolve(cwd);
129
- const canonicalCwd = existsSync(resolvedCwd)
128
+ const pathApi = runtimePlatform === "win32" ? win32 : posix;
129
+ const resolvedCwd = pathApi.resolve(cwd);
130
+ const canonicalCwd = runtimePlatform === platform() && existsSync(resolvedCwd)
130
131
  ? realpathSync.native(resolvedCwd)
131
132
  : resolvedCwd;
132
133
  const expectedCwd =
package/lib/runs-start.ts CHANGED
@@ -93,6 +93,7 @@ export function prepareStateDirForStart(
93
93
  "result.json",
94
94
  "stderr.log",
95
95
  "stdout.log",
96
+ "terminal-delivery-failure.json",
96
97
  "terminal-handled.json",
97
98
  ]) {
98
99
  rmSync(join(stateDir, file), { force: true });
@@ -61,6 +61,9 @@ export function buildRunStatus(
61
61
  ? "running"
62
62
  : (getInterruptedRunStatus(stateDir) ?? "exited");
63
63
  const terminalHandled = readJson(join(stateDir, "terminal-handled.json"));
64
+ const terminalDeliveryFailure = readJson(
65
+ join(stateDir, "terminal-delivery-failure.json"),
66
+ );
64
67
  return {
65
68
  ...meta,
66
69
  eventsFile: join(stateDir, "events.jsonl"),
@@ -70,6 +73,8 @@ export function buildRunStatus(
70
73
  process_identity_status: processIdentity.status,
71
74
  progress: readJson(join(stateDir, "progress.json")) || null,
72
75
  result: result || null,
76
+ ...(terminalDeliveryFailure
77
+ ? { terminal_delivery_failure: terminalDeliveryFailure } : {}),
73
78
  ...(terminalHandled ? { terminal_handled: terminalHandled } : {}),
74
79
  state_dir: String(meta.state_dir ?? stateDir),
75
80
  stderrLog: join(stateDir, "stderr.log"),
@@ -7,6 +7,7 @@
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
10
11
 
11
12
  import * as AsyncRuns from "./async-runs.ts";
12
13
  import * as Limits from "./limits.ts";
@@ -269,7 +270,7 @@ function getPiActorsRuntimeStatus(): Record<string, unknown> {
269
270
  } catch {
270
271
  git_commit = undefined;
271
272
  }
272
- const entrypoint = new URL(import.meta.url).pathname;
273
+ const entrypoint = fileURLToPath(import.meta.url);
273
274
  return {
274
275
  automatic_recipe_review: Paths.isAutomaticRecipeReviewEnabled(),
275
276
  entrypoint,
@@ -143,6 +143,14 @@ export function createRuntimeToolDefinition(
143
143
  paramSchema.run_id = Schema.stringSchema(
144
144
  "Optional run id override for this async template recipe invocation.",
145
145
  );
146
+ if (isAsyncRecipe) {
147
+ paramSchema.correlation_id = Schema.stringSchema(
148
+ "Optional workflow correlation id preserved in terminal follow-up delivery.",
149
+ );
150
+ paramSchema.transport_context = Schema.looseObjectSchema(
151
+ "Optional originating transport route preserved for detached terminal follow-up.",
152
+ );
153
+ }
146
154
  return {
147
155
  name: cfg.name,
148
156
  label: cfg.name,
@@ -155,7 +163,7 @@ export function createRuntimeToolDefinition(
155
163
  )
156
164
  : Prompts.formatRegisteredToolPromptSnippet(cfg.template),
157
165
  async execute(
158
- _toolCallId: string,
166
+ toolCallId: string,
159
167
  params: unknown,
160
168
  signal: AbortSignal | undefined,
161
169
  _onUpdate: unknown,
@@ -172,7 +180,7 @@ export function createRuntimeToolDefinition(
172
180
  }
173
181
  if (isAsyncRecipe) {
174
182
  const input = params as Record<string, unknown>;
175
- const { run_id, ...values } = input;
183
+ const { correlation_id, run_id, transport_context, ...values } = input;
176
184
  const base = cfg.recipe ? cfg.recipe : { file: String(cfg.template) };
177
185
  const runId =
178
186
  typeof run_id === "string" && run_id.trim()
@@ -182,6 +190,17 @@ export function createRuntimeToolDefinition(
182
190
  {
183
191
  ...base,
184
192
  launch_source: "tool",
193
+ launch_correlation: {
194
+ ...(typeof correlation_id === "string"
195
+ ? { correlation_id } : {}),
196
+ tool_call_id: toolCallId,
197
+ },
198
+ ...(transport_context &&
199
+ typeof transport_context === "object" &&
200
+ !Array.isArray(transport_context)
201
+ ? {
202
+ transport_context: transport_context as Record<string, unknown>,
203
+ } : {}),
185
204
  ownerId: getRunOwnerId(ctx),
186
205
  run_id: runId,
187
206
  tool: cfg.name,
@@ -144,6 +144,9 @@ export function createSpawnToolDefinition<
144
144
  as: Schema.stringSchema(
145
145
  "Optional actor address for the spawned run, e.g. run:<id>.",
146
146
  ),
147
+ correlation_id: Schema.stringSchema(
148
+ "Optional workflow correlation id preserved in terminal follow-up delivery.",
149
+ ),
147
150
  file: Schema.stringSchema(
148
151
  "Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
149
152
  ),
@@ -162,6 +165,9 @@ export function createSpawnToolDefinition<
162
165
  values: Schema.looseObjectSchema(
163
166
  "Runtime placeholder values passed to the actor.",
164
167
  ),
168
+ transport_context: Schema.looseObjectSchema(
169
+ "Optional originating transport route preserved for detached terminal follow-up, e.g. Telegram chat_id and thread_id.",
170
+ ),
165
171
  verbose: Schema.booleanSchema(
166
172
  "Return full JSON instead of compact text.",
167
173
  ),
@@ -169,7 +175,7 @@ export function createSpawnToolDefinition<
169
175
  [],
170
176
  ),
171
177
  async execute(
172
- _toolCallId: string,
178
+ toolCallId: string,
173
179
  params: unknown,
174
180
  _signal: AbortSignal | undefined,
175
181
  _onUpdate: unknown,
@@ -196,7 +202,14 @@ export function createSpawnToolDefinition<
196
202
  {
197
203
  file: recipe,
198
204
  launch_source: "spawn",
205
+ launch_correlation: {
206
+ ...(typeof input.correlation_id === "string"
207
+ ? { correlation_id: input.correlation_id } : {}),
208
+ tool_call_id: toolCallId,
209
+ },
199
210
  ownerId: getRunOwnerId(ctx),
211
+ ...(input.transport_context
212
+ ? { transport_context: asRecord(input.transport_context) } : {}),
200
213
  run_id: runId,
201
214
  ...(input.template !== undefined
202
215
  ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.42.0",
3
+ "version": "0.42.2",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -27,11 +27,12 @@
27
27
  "scripts": {
28
28
  "check": "node --experimental-strip-types -e \"await import('./index.ts'); console.log('pi-actors: extension import ok')\"",
29
29
  "conformance": "node scripts/conformance.mjs",
30
- "test": "node --experimental-strip-types --test tests/*.test.ts",
30
+ "test": "node --experimental-strip-types --test --test-concurrency=1 tests/*.test.ts",
31
31
  "pack:dry": "npm pack --dry-run",
32
32
  "recipes:qa": "node scripts/validate-recipe.mjs recipes --all --qa --summary",
33
33
  "release:validate": "npm run validate && node scripts/release-gates.mjs",
34
- "validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run conformance && npm audit --audit-level=high && npm run pack:dry",
34
+ "validate": "npx tsc --noEmit && npm run recipes:qa && npm run build && npm run check && npm test && npm run conformance && npm run pack:dry",
35
+ "audit:dependencies": "npm audit --audit-level=high --omit=peer",
35
36
  "build": "node scripts/build-dist.mjs",
36
37
  "prepack": "npm run build"
37
38
  },
@@ -135,7 +135,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
135
135
  try {
136
136
  return readdirSync(sessionDir, { withFileTypes: true })
137
137
  .filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
138
- .map((entry) => relative(stateDir, join(sessionDir, entry.name)))
138
+ .map((entry) => relative(stateDir, join(sessionDir, entry.name)).replaceAll("\\", "/"))
139
139
  .sort();
140
140
  } catch {
141
141
  return [];
@@ -167,12 +167,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
167
167
  ...(recipeContext ? { recipe_context: recipeContext } : {}),
168
168
  command: commandDetail,
169
169
  ...(materialized.promptFile
170
- ? { prompt_file: relative(stateDir, materialized.promptFile) }
170
+ ? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
171
171
  : {}),
172
172
  ...(materialized.promptBytes
173
173
  ? { prompt_bytes: materialized.promptBytes }
174
174
  : {}),
175
- ...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
175
+ ...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
176
176
  attempts: [],
177
177
  semantic_acceptance:
178
178
  options?.evidenceContext?.acceptOutput === "review_evidence" ||
@@ -209,11 +209,11 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
209
209
  attempts.push({
210
210
  attempt,
211
211
  stdout: {
212
- path: relative(stateDir, stdoutFile),
212
+ path: relative(stateDir, stdoutFile).replaceAll("\\", "/"),
213
213
  bytes: existsSync(stdoutFile) ? statSync(stdoutFile).size : 0,
214
214
  },
215
215
  stderr: {
216
- path: relative(stateDir, stderrFile),
216
+ path: relative(stateDir, stderrFile).replaceAll("\\", "/"),
217
217
  bytes: existsSync(stderrFile) ? statSync(stderrFile).size : 0,
218
218
  },
219
219
  });
@@ -248,12 +248,12 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
248
248
  ...(recipeContext ? { recipe_context: recipeContext } : {}),
249
249
  command: commandDetail,
250
250
  ...(materialized.promptFile
251
- ? { prompt_file: relative(stateDir, materialized.promptFile) }
251
+ ? { prompt_file: relative(stateDir, materialized.promptFile).replaceAll("\\", "/") }
252
252
  : {}),
253
253
  ...(materialized.promptBytes
254
254
  ? { prompt_bytes: materialized.promptBytes }
255
255
  : {}),
256
- ...(sessionDir ? { session_dir: relative(stateDir, sessionDir) } : {}),
256
+ ...(sessionDir ? { session_dir: relative(stateDir, sessionDir).replaceAll("\\", "/") } : {}),
257
257
  ...(commandSessionFiles(sessionDir).length > 0
258
258
  ? { session_files: commandSessionFiles(sessionDir) }
259
259
  : {}),
@@ -386,7 +386,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
386
386
  command: commandDetail,
387
387
  ...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
388
388
  ...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
389
- ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
389
+ ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
390
390
  });
391
391
  progressRunning();
392
392
  const captureDir = join(stateDir, "captures", commandId);
@@ -457,7 +457,7 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
457
457
  ...captureDetails(result),
458
458
  ...(materialized.promptFile ? { prompt_file: materialized.promptFile } : {}),
459
459
  ...(materialized.promptBytes ? { prompt_bytes: materialized.promptBytes } : {}),
460
- ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir) } : {}),
460
+ ...(session.sessionDir ? { session_dir: relative(stateDir, session.sessionDir).replaceAll("\\", "/") } : {}),
461
461
  ...(commandSessionFiles(session.sessionDir).length > 0
462
462
  ? { session_files: commandSessionFiles(session.sessionDir) }
463
463
  : {}),
@@ -517,6 +517,11 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
517
517
  };
518
518
  throw error;
519
519
  }
520
+ writeEvidenceManifest("done");
521
+ progress("done", {
522
+ completed: 1,
523
+ failures: result.details.nonCriticalFailures || [],
524
+ });
520
525
  writeJsonAtomic(resultPath, {
521
526
  code: result.details.code,
522
527
  command: result.details.command,
@@ -525,26 +530,11 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
525
530
  truncated: result.details.truncated,
526
531
  completedAt: new Date().toISOString(),
527
532
  });
528
- writeEvidenceManifest("done");
529
- progress("done", {
530
- completed: 1,
531
- failures: result.details.nonCriticalFailures || [],
532
- });
533
533
  event("run.done", { code: result.details.code });
534
534
  } catch (error) {
535
535
  const message = error instanceof Error ? error.message : String(error);
536
536
  const details = error && typeof error === "object" ? error.details : undefined;
537
537
  appendFileSync(stderrPath, `${message}\n`);
538
- writeJsonAtomic(resultPath, {
539
- code: typeof details?.code === "number" ? details.code : 1,
540
- error: message,
541
- killed: Boolean(details?.killed),
542
- ...(Array.isArray(details?.branches) ? { branches: details.branches } : {}),
543
- ...(details?.failureReason ? { failure_reason: details.failureReason } : {}),
544
- ...(meta.model_policy ? { model_policy: meta.model_policy } : {}),
545
- ...(details?.softQuorum ? { soft_quorum: details.softQuorum } : {}),
546
- completedAt: new Date().toISOString(),
547
- });
548
538
  writeEvidenceManifest("failed");
549
539
  progress("failed", {
550
540
  completed: 0,
@@ -555,6 +545,16 @@ export async function runAsyncRunner(stateDir = process.argv[2]) {
555
545
  : [{ message }],
556
546
  ...(details?.failureReason ? { failureReason: details.failureReason } : {}),
557
547
  });
548
+ writeJsonAtomic(resultPath, {
549
+ code: typeof details?.code === "number" ? details.code : 1,
550
+ error: message,
551
+ killed: Boolean(details?.killed),
552
+ ...(Array.isArray(details?.branches) ? { branches: details.branches } : {}),
553
+ ...(details?.failureReason ? { failure_reason: details.failureReason } : {}),
554
+ ...(meta.model_policy ? { model_policy: meta.model_policy } : {}),
555
+ ...(details?.softQuorum ? { soft_quorum: details.softQuorum } : {}),
556
+ completedAt: new Date().toISOString(),
557
+ });
558
558
  event("run.failed", {
559
559
  error: message,
560
560
  ...(details?.failureReason ? { failure_reason: details.failureReason } : {}),
@@ -20,13 +20,18 @@ import { join } from "node:path";
20
20
 
21
21
  function run(command, args) {
22
22
  const result = spawnSync(command, args, { stdio: "inherit" });
23
+ if (result.error) throw result.error;
23
24
  if (result.status !== 0) process.exit(result.status ?? 1);
24
25
  }
25
26
 
26
27
  rmSync("dist", { recursive: true, force: true });
27
28
  mkdirSync("dist", { recursive: true });
28
29
 
29
- run("tsc", ["-p", "tsconfig.build.json"]);
30
+ run(process.execPath, [
31
+ join("node_modules", "typescript", "bin", "tsc"),
32
+ "-p",
33
+ "tsconfig.build.json",
34
+ ]);
30
35
 
31
36
  mkdirSync(join("dist", "pi-actors"), { recursive: true });
32
37
  writeFileSync(
@@ -28,7 +28,12 @@ function packageRoot() {
28
28
 
29
29
  const result = spawnSync(
30
30
  process.execPath,
31
- ["--experimental-strip-types", "--test", ...conformanceSuites],
31
+ [
32
+ "--experimental-strip-types",
33
+ "--test",
34
+ "--test-concurrency=1",
35
+ ...conformanceSuites,
36
+ ],
32
37
  { cwd: packageRoot(), encoding: "utf8", stdio: "pipe" },
33
38
  );
34
39
 
@@ -16,7 +16,7 @@ import {
16
16
  statSync,
17
17
  writeFileSync,
18
18
  } from "node:fs";
19
- import { dirname, extname, join, relative, resolve } from "node:path";
19
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
20
20
 
21
21
  function usage() {
22
22
  console.error(`Usage:
@@ -101,7 +101,7 @@ function collectRunSummary(rootValue) {
101
101
  const root = resolve(
102
102
  rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
103
103
  );
104
- const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
104
+ const files = walkFiles(root, 2).filter((file) => basename(file) === "run.json");
105
105
  const rows = [];
106
106
  for (const file of files) {
107
107
  const run = readJson(file);
@@ -118,7 +118,7 @@ function collectRunSummary(rootValue) {
118
118
  const progress = readJson(join(runDir, "progress.json"));
119
119
  const result = readJson(join(runDir, "result.json"));
120
120
  rows.push({
121
- run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
121
+ run: run.run_id ?? run.run ?? relative(root, file).split(sep)[0],
122
122
  status: getRunStatus(run, progress, result),
123
123
  recipe: run.recipe ?? run.recipe_file ?? "",
124
124
  updated:
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use, including parallel actor launches, subagent fanout, and autonomous coordinator workflows. 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.42.0
5
+ version: 0.42.2
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -69,7 +69,7 @@ Rules:
69
69
  - Command-template strings execute directly without a shell. Operators such as `&&`, `||`, pipes, redirects, and `cd` remain literal argv unless an explicit trusted shell is the executable. Prefer absolute paths or template arrays for sequencing; put non-trivial shell behavior in a reviewed script.
70
70
  - Use `file`/`recipe` for saved recipes; bare names resolve under `~/.pi/agent/recipes`.
71
71
  - Use inline `template` for one-off experiments; promote useful repeats to recipes.
72
- - When a successful actor follow-up suggests persistence, decide whether the pattern deserves durable tool memory; call `register_tool` yourself only when the evidence is strong, and ask before writing the user recipe root.
72
+ - Terminal follow-up context contains only run id, status, one base path, and relative artifact names. Inspect the run for contents; semantic output and correlation remain in non-LLM details and state. Decide whether a successful pattern deserves durable tool memory only after inspection, and ask before writing the user recipe root.
73
73
  - Use stable `as` names when you will inspect or message the actor later.
74
74
  - Public run state is runtime-owned; do not pass custom `state_dir` paths. This keeps `run:<id>` addressability and retention on one boundary.
75
75
  - `async: true` on the recipe is the detached run switch.
@@ -126,7 +126,7 @@ Views:
126
126
  - `artifacts`: declared artifact paths/status plus the same bounded owned review-evidence manifest when present.
127
127
  - `recipes` target: registry summary for active, shadowed, invalid, disabled, and diagnostic recipe entries.
128
128
 
129
- Let terminal notifications arrive. They queue through Pi's follow-up delivery mode, so a busy coordinator finishes its current work before receiving concurrently completed actor results; the host's `followUpMode` controls whether queued results arrive together or one at a time. File watching accelerates delivery, while a bounded ten-second terminal-only reconciliation pass recovers missed or failed watcher activity without replaying outbox traffic; watcher degradation and rearm remain visible diagnostics. When a deferred actor result gates the next step, wait for that terminal follow-up instead of scheduling continuation loops, repeatedly inspecting, or mutating the actor's reviewed scope. Idle coordinators still start a normal turn through `triggerTurn: true`. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue or stuck run.
129
+ Let terminal notifications arrive. They queue through Pi's follow-up delivery mode, so a busy coordinator finishes its current work before receiving concurrently completed actor results; the host's `followUpMode` controls whether queued results arrive together or one at a time. Their LLM context content stays limited to run id, status, one base path, and relative artifact names; inspect state for raw output while correlation and semantic details remain outside LLM context. File watching accelerates delivery, while a bounded ten-second terminal-only reconciliation pass recovers missed or failed watcher activity without replaying outbox traffic; watcher degradation and rearm remain visible diagnostics. When a deferred actor result gates the next step, wait for that terminal follow-up instead of scheduling continuation loops, repeatedly inspecting, or mutating the actor's reviewed scope. Idle coordinators still start a normal turn through `triggerTurn: true`. Inspect early only for an operator request, a meaningful actor event, or diagnosis of an overdue or stuck run.
130
130
 
131
131
  ## Runtime Communication Rules
132
132
 
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent and actor orchestration with scoped locks, fanout, and quorum consensus. Use before launching multiple parallel actors or subagents for independent implementation, artifact generation, review, delegated audit, coordinated execution, or any workflow that needs autonomous coordinator decomposition and integration.
4
4
  metadata:
5
- version: 0.42.0
5
+ version: 0.42.2
6
6
  ---
7
7
 
8
8
  # Swarm