@llblab/pi-actors 0.24.8 → 0.26.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 (94) hide show
  1. package/BACKLOG.md +5 -9
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +1 -1
  4. package/dist/lib/actor-worker.d.ts +3 -0
  5. package/dist/lib/actor-worker.js +54 -6
  6. package/dist/lib/recipe-discovery.js +85 -4
  7. package/dist/lib/tools.js +15 -3
  8. package/dist/recipes/actor-worker.json +10 -6
  9. package/dist/recipes/lens-swarm.json +0 -2
  10. package/dist/recipes/pipeline-architect-coordinator.json +0 -2
  11. package/dist/recipes/pipeline-artifact-bundle.json +0 -2
  12. package/dist/recipes/pipeline-artifact-report.json +0 -2
  13. package/dist/recipes/pipeline-artifact-write.json +0 -2
  14. package/dist/recipes/pipeline-async-run-ops.json +0 -2
  15. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -2
  16. package/dist/recipes/pipeline-development-tasking.json +0 -2
  17. package/dist/recipes/pipeline-docs-maintenance.json +0 -2
  18. package/dist/recipes/pipeline-media-library.json +0 -2
  19. package/dist/recipes/pipeline-quorum-review.json +0 -2
  20. package/dist/recipes/pipeline-release-readiness.json +0 -2
  21. package/dist/recipes/pipeline-release-summary.json +0 -2
  22. package/dist/recipes/pipeline-repo-health.json +0 -2
  23. package/dist/recipes/pipeline-research-synthesis.json +0 -2
  24. package/dist/recipes/pipeline-review-readiness.json +0 -2
  25. package/dist/recipes/pipeline-room-swarm.json +0 -2
  26. package/dist/recipes/subagent-artifact.json +0 -2
  27. package/dist/recipes/subagent-checkpoint.json +0 -2
  28. package/dist/recipes/subagent-conflict-report.json +0 -2
  29. package/dist/recipes/subagent-contradiction-map.json +0 -2
  30. package/dist/recipes/subagent-critic.json +0 -2
  31. package/dist/recipes/subagent-evidence-map.json +0 -2
  32. package/dist/recipes/subagent-followup.json +0 -2
  33. package/dist/recipes/subagent-judge.json +0 -2
  34. package/dist/recipes/subagent-merge.json +0 -2
  35. package/dist/recipes/subagent-message.json +0 -2
  36. package/dist/recipes/subagent-normalize.json +0 -2
  37. package/dist/recipes/subagent-plan.json +0 -2
  38. package/dist/recipes/subagent-prompt.json +0 -2
  39. package/dist/recipes/subagent-quorum.json +0 -2
  40. package/dist/recipes/subagent-review-coordinator.json +0 -2
  41. package/dist/recipes/subagent-review.json +0 -2
  42. package/dist/recipes/subagent-task-card.json +0 -2
  43. package/dist/recipes/subagent-tools.json +0 -2
  44. package/dist/recipes/subagent-verify.json +0 -2
  45. package/dist/recipes/subagents-prompts.json +0 -2
  46. package/dist/skills/actors/SKILL.md +2 -2
  47. package/dist/skills/swarm/SKILL.md +1 -1
  48. package/docs/actor-messages.md +1 -1
  49. package/docs/template-recipes.md +2 -2
  50. package/docs/tool-registry.md +2 -1
  51. package/lib/actor-worker.ts +62 -6
  52. package/lib/recipe-discovery.ts +94 -4
  53. package/lib/tools.ts +20 -3
  54. package/package.json +1 -1
  55. package/recipes/actor-worker.json +10 -6
  56. package/recipes/lens-swarm.json +0 -2
  57. package/recipes/pipeline-architect-coordinator.json +0 -2
  58. package/recipes/pipeline-artifact-bundle.json +0 -2
  59. package/recipes/pipeline-artifact-report.json +0 -2
  60. package/recipes/pipeline-artifact-write.json +0 -2
  61. package/recipes/pipeline-async-run-ops.json +0 -2
  62. package/recipes/pipeline-checkpoint-continuation.json +0 -2
  63. package/recipes/pipeline-development-tasking.json +0 -2
  64. package/recipes/pipeline-docs-maintenance.json +0 -2
  65. package/recipes/pipeline-media-library.json +0 -2
  66. package/recipes/pipeline-quorum-review.json +0 -2
  67. package/recipes/pipeline-release-readiness.json +0 -2
  68. package/recipes/pipeline-release-summary.json +0 -2
  69. package/recipes/pipeline-repo-health.json +0 -2
  70. package/recipes/pipeline-research-synthesis.json +0 -2
  71. package/recipes/pipeline-review-readiness.json +0 -2
  72. package/recipes/pipeline-room-swarm.json +0 -2
  73. package/recipes/subagent-artifact.json +0 -2
  74. package/recipes/subagent-checkpoint.json +0 -2
  75. package/recipes/subagent-conflict-report.json +0 -2
  76. package/recipes/subagent-contradiction-map.json +0 -2
  77. package/recipes/subagent-critic.json +0 -2
  78. package/recipes/subagent-evidence-map.json +0 -2
  79. package/recipes/subagent-followup.json +0 -2
  80. package/recipes/subagent-judge.json +0 -2
  81. package/recipes/subagent-merge.json +0 -2
  82. package/recipes/subagent-message.json +0 -2
  83. package/recipes/subagent-normalize.json +0 -2
  84. package/recipes/subagent-plan.json +0 -2
  85. package/recipes/subagent-prompt.json +0 -2
  86. package/recipes/subagent-quorum.json +0 -2
  87. package/recipes/subagent-review-coordinator.json +0 -2
  88. package/recipes/subagent-review.json +0 -2
  89. package/recipes/subagent-task-card.json +0 -2
  90. package/recipes/subagent-tools.json +0 -2
  91. package/recipes/subagent-verify.json +0 -2
  92. package/recipes/subagents-prompts.json +0 -2
  93. package/skills/actors/SKILL.md +2 -2
  94. package/skills/swarm/SKILL.md +1 -1
@@ -18,8 +18,6 @@
18
18
  },
19
19
  "mailbox": {
20
20
  "accepts": [
21
- "control.stop",
22
- "control.cancel",
23
21
  "control.kill"
24
22
  ],
25
23
  "emits": [
@@ -22,8 +22,6 @@
22
22
  },
23
23
  "mailbox": {
24
24
  "accepts": [
25
- "control.stop",
26
- "control.cancel",
27
25
  "control.kill"
28
26
  ],
29
27
  "emits": [
@@ -16,8 +16,6 @@
16
16
  },
17
17
  "mailbox": {
18
18
  "accepts": [
19
- "control.stop",
20
- "control.cancel",
21
19
  "control.kill"
22
20
  ],
23
21
  "emits": [
@@ -18,8 +18,6 @@
18
18
  },
19
19
  "mailbox": {
20
20
  "accepts": [
21
- "control.stop",
22
- "control.cancel",
23
21
  "control.kill"
24
22
  ],
25
23
  "emits": [
@@ -14,8 +14,6 @@
14
14
  },
15
15
  "mailbox": {
16
16
  "accepts": [
17
- "control.stop",
18
- "control.cancel",
19
17
  "control.kill"
20
18
  ],
21
19
  "emits": [
@@ -17,8 +17,6 @@
17
17
  },
18
18
  "mailbox": {
19
19
  "accepts": [
20
- "control.stop",
21
- "control.cancel",
22
20
  "control.kill"
23
21
  ],
24
22
  "emits": [
@@ -36,8 +36,6 @@
36
36
  },
37
37
  "mailbox": {
38
38
  "accepts": [
39
- "control.stop",
40
- "control.cancel",
41
39
  "control.kill"
42
40
  ],
43
41
  "emits": [
@@ -22,8 +22,6 @@
22
22
  },
23
23
  "mailbox": {
24
24
  "accepts": [
25
- "control.stop",
26
- "control.cancel",
27
25
  "control.kill"
28
26
  ],
29
27
  "emits": [
@@ -20,8 +20,6 @@
20
20
  },
21
21
  "mailbox": {
22
22
  "accepts": [
23
- "control.stop",
24
- "control.cancel",
25
23
  "control.kill"
26
24
  ],
27
25
  "emits": [
@@ -13,8 +13,6 @@
13
13
  },
14
14
  "mailbox": {
15
15
  "accepts": [
16
- "control.stop",
17
- "control.cancel",
18
16
  "control.kill"
19
17
  ],
20
18
  "emits": [
@@ -19,8 +19,6 @@
19
19
  },
20
20
  "mailbox": {
21
21
  "accepts": [
22
- "control.stop",
23
- "control.cancel",
24
22
  "control.kill"
25
23
  ],
26
24
  "emits": [
@@ -25,8 +25,6 @@
25
25
  },
26
26
  "mailbox": {
27
27
  "accepts": [
28
- "control.stop",
29
- "control.cancel",
30
28
  "control.kill"
31
29
  ],
32
30
  "emits": [
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.24.8
5
+ version: 0.26.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -277,7 +277,7 @@ Use packaged recipes by name with `spawn file=<name>` for async actors, or regis
277
277
  - [`locker`](../../recipes/locker.json): modular queue + acquire/renew/release lease locks + journaled locker messages + platform-adapted control metadata.
278
278
  - [`utility-coordinator-lock-snapshot`](../../recipes/utility-coordinator-lock-snapshot.json): one-shot JSON snapshot of a coordinator-locker state directory.
279
279
  - [`music-player`](../../recipes/music-player.json): background local/URL/directory/playlist playback actor controlled by messages.
280
- - [`actor-worker`](../../recipes/actor-worker.json): canonical mailbox-backed branch worker demo that claims branch inbox work, emits room-visible task lifecycle messages, and stops on standard control messages.
280
+ - [`actor-worker`](../../recipes/actor-worker.json): canonical mailbox-backed branch worker reference that claims branch inbox work, emits room-visible task lifecycle messages, writes compact `worker-status.json`, optionally writes per-task result artifacts under `worker-artifacts/`, surfaces stale-claim counts when `stale_claim_ms` is set, and terminates on `control.kill`.
281
281
 
282
282
  ### Subagent Atoms
283
283
 
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.24.8
5
+ version: 0.26.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -164,7 +164,7 @@ Recipes can declare their conversational surface:
164
164
  "control.continue",
165
165
  "control.revise",
166
166
  "control.approve",
167
- "control.stop"
167
+ "control.kill"
168
168
  ],
169
169
  "emits": ["checkpoint.needs_scope", "branch.done", "run.done"]
170
170
  }
@@ -80,7 +80,7 @@ defaults:
80
80
  scope: docs
81
81
  mailbox:
82
82
  accepts:
83
- - control.stop
83
+ - control.kill
84
84
  ---
85
85
 
86
86
  Human notes can explain intent, examples, or review guidance.
@@ -155,7 +155,7 @@ Use recipe-level `mailbox` to document the semantic messages a recipe actor acce
155
155
  "control.continue",
156
156
  "control.revise",
157
157
  "control.approve",
158
- "control.stop"
158
+ "control.kill"
159
159
  ],
160
160
  "emits": ["checkpoint.needs_scope", "branch.done", "run.done"]
161
161
  }
@@ -23,10 +23,11 @@ Inspect the discovered registry with:
23
23
 
24
24
  ```text
25
25
  inspect target=recipes view=status
26
+ inspect target=recipes view=doctor
26
27
  inspect target=recipes view=summary verbose=true
27
28
  ```
28
29
 
29
- The summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled.
30
+ The summary reports active, shadowed, invalid, disabled, and diagnostic entries so operators can answer why a tool is present, hidden, broken, or disabled. The doctor view keeps the same registry evidence but promotes an advisory action surface: compact output includes the highest-priority `top` remediation plus ordered actions for invalid/blocking, disabled, risky shell-boundary, and shadowed recipes. Verbose inspection keeps the structured `remediations`, `top_action`, diagnostic details, and blocked lower-priority candidate paths when a broken or disabled higher-priority recipe masks a fallback.
30
31
 
31
32
  ## Registering Tools
32
33
 
@@ -3,17 +3,20 @@
3
3
  * Zones: worker demo, mailbox loop recipe runtime
4
4
  */
5
5
 
6
- import { appendFileSync } from "node:fs";
6
+ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
 
9
- import { appendRoomMessage, ensureRoomMember } from "./actor-rooms.ts";
9
+ import { appendRoomMessage, ensureRoomMember, readBranchInboxMessages } from "./actor-rooms.ts";
10
10
  import { handleMailboxLoopOnce, isMailboxLoopStopMessage } from "./mailbox-loop.ts";
11
11
 
12
12
  interface ActorWorkerArgs {
13
+ artifact_dir?: string;
13
14
  branch?: string;
14
15
  poll_ms?: string | number;
15
16
  run?: string;
17
+ stale_claim_ms?: string | number;
16
18
  state_dir?: string;
19
+ write_artifacts?: string | boolean;
17
20
  }
18
21
 
19
22
  export function parseActorWorkerArgs(argv: string[]): ActorWorkerArgs {
@@ -41,12 +44,23 @@ function text(value: unknown): string {
41
44
  return typeof value === "string" ? value : JSON.stringify(value);
42
45
  }
43
46
 
47
+ function enabled(value: unknown): boolean {
48
+ return value !== false && value !== "false" && value !== "0";
49
+ }
50
+
51
+ function safeName(value: unknown): string {
52
+ return String(value ?? "unknown").replaceAll(/[^A-Za-z0-9_.-]+/g, "_");
53
+ }
54
+
44
55
  export async function runActorWorker(argv = process.argv.slice(2)): Promise<void> {
45
56
  const args = parseActorWorkerArgs(argv);
46
57
  const stateDir = String(args.state_dir ?? "");
47
58
  const run = String(args.run ?? "");
48
59
  const branch = String(args.branch ?? "worker");
49
60
  const pollMs = Math.max(50, Number(args.poll_ms ?? 1000));
61
+ const staleClaimMs = Math.max(0, Number(args.stale_claim_ms ?? 0));
62
+ const artifactDir = String(args.artifact_dir ?? join(stateDir, "worker-artifacts"));
63
+ const writeArtifacts = enabled(args.write_artifacts ?? true);
50
64
 
51
65
  if (!stateDir || !run) {
52
66
  throw new Error("usage: actor-worker.mjs --state-dir <dir> --run <id> [--branch worker] [--poll-ms 1000]");
@@ -55,6 +69,8 @@ export async function runActorWorker(argv = process.argv.slice(2)): Promise<void
55
69
  const branchAddress = `branch:${run}/${branch}`;
56
70
  const roomAddress = `room:${run}`;
57
71
  const journalPath = join(stateDir, "worker-events.jsonl");
72
+ const statusPath = join(stateDir, "worker-status.json");
73
+ if (writeArtifacts) mkdirSync(artifactDir, { recursive: true });
58
74
 
59
75
  function journal(event: string, data: Record<string, unknown> = {}): void {
60
76
  appendFileSync(
@@ -63,6 +79,30 @@ export async function runActorWorker(argv = process.argv.slice(2)): Promise<void
63
79
  );
64
80
  }
65
81
 
82
+ function staleClaims(): number {
83
+ if (staleClaimMs <= 0) return 0;
84
+ const cutoff = Date.now() - staleClaimMs;
85
+ return readBranchInboxMessages(stateDir, run, branchAddress).filter((record) => {
86
+ if (record.status !== "claimed" || !record.claimed_at) return false;
87
+ const claimedAt = Date.parse(record.claimed_at);
88
+ return Number.isFinite(claimedAt) && claimedAt < cutoff;
89
+ }).length;
90
+ }
91
+
92
+ function status(state: string, data: Record<string, unknown> = {}): void {
93
+ writeFileSync(
94
+ statusPath,
95
+ `${JSON.stringify({ artifact_dir: artifactDir, branch, run, stale_claims: staleClaims(), state, ts: new Date().toISOString(), ...data }, null, 2)}\n`,
96
+ );
97
+ }
98
+
99
+ function writeResultArtifact(id: unknown, result: string): string | undefined {
100
+ if (!writeArtifacts) return undefined;
101
+ const path = join(artifactDir, `${safeName(id)}.txt`);
102
+ writeFileSync(path, `${result}\n`);
103
+ return path;
104
+ }
105
+
66
106
  function room(type: string, summary: string, body: Record<string, unknown> = {}): void {
67
107
  appendRoomMessage(stateDir, "main", {
68
108
  body,
@@ -82,7 +122,10 @@ export async function runActorWorker(argv = process.argv.slice(2)): Promise<void
82
122
  `${branch} joined as mailbox worker`,
83
123
  );
84
124
  room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
85
- journal("worker.started", { branch, run });
125
+ journal("worker.started", { artifact_dir: artifactDir, branch, run, stale_claim_ms: staleClaimMs });
126
+ status("awaiting_assignment", { handled: 0, stale_claim_ms: staleClaimMs });
127
+
128
+ let handled = 0;
86
129
 
87
130
  let stopping = false;
88
131
  while (!stopping) {
@@ -93,6 +136,7 @@ export async function runActorWorker(argv = process.argv.slice(2)): Promise<void
93
136
  stopping = true;
94
137
  room("actor.leave", `${branch} stopping`, { branch, reason: message.type });
95
138
  journal("worker.stopping", { id: message.id, type: message.type });
139
+ status("stopping", { handled, reason: message.type });
96
140
  return;
97
141
  }
98
142
  room("task.claim", `${branch} claimed ${message.type}`, {
@@ -100,19 +144,31 @@ export async function runActorWorker(argv = process.argv.slice(2)): Promise<void
100
144
  id: message.id,
101
145
  type: message.type,
102
146
  });
147
+ const result = text(message.body);
148
+ const artifact = writeResultArtifact(message.id, result);
149
+ handled += 1;
103
150
  room("task.result", `${branch} handled ${message.type}`, {
151
+ artifact,
104
152
  branch,
105
153
  id: message.id,
106
- result: text(message.body),
154
+ result,
107
155
  type: message.type,
108
156
  });
109
157
  room("awaiting_assignment", `${branch} awaiting assignment`, { branch });
110
- journal("task.handled", { id: message.id, type: message.type });
158
+ journal("task.handled", { artifact, id: message.id, type: message.type });
159
+ status("awaiting_assignment", {
160
+ handled,
161
+ last_artifact: artifact,
162
+ last_id: message.id,
163
+ last_type: message.type,
164
+ stale_claim_ms: staleClaimMs,
165
+ });
111
166
  },
112
167
  { owner: branchAddress },
113
168
  );
114
169
  if (!result.handled) await sleep(pollMs);
115
170
  }
116
171
 
117
- journal("worker.done", { branch, run });
172
+ journal("worker.done", { branch, handled, run });
173
+ status("done", { handled });
118
174
  }
@@ -403,6 +403,10 @@ function diagnosticSeverity(message: string): "info" | "warning" | "error" {
403
403
  }
404
404
 
405
405
  function diagnosticSuggestedAction(message: string): string {
406
+ if (/world-writable|group-writable/i.test(message))
407
+ return "tighten recipe root permissions";
408
+ if (/invokes bash/i.test(message))
409
+ return "audit the trusted shell boundary and keep only if intentional";
406
410
  if (/must define template/i.test(message))
407
411
  return "add a template field or remove the recipe";
408
412
  if (/JSON|Expected|parse/i.test(message))
@@ -414,10 +418,6 @@ function diagnosticSuggestedAction(message: string): string {
414
418
  return "split large prompt or data into separate files";
415
419
  if (/repeat must/i.test(message))
416
420
  return "use a positive repeat count or an array-typed repeat source";
417
- if (/world-writable|group-writable/i.test(message))
418
- return "tighten recipe root permissions";
419
- if (/invokes bash/i.test(message))
420
- return "audit the trusted shell boundary or move details to recipe doctor";
421
421
  if (/shadows/i.test(message))
422
422
  return "confirm the active override or rename one recipe";
423
423
  if (/disabled/i.test(message))
@@ -455,6 +455,93 @@ function diagnosticDetails(
455
455
  });
456
456
  }
457
457
 
458
+ function remediationForEntry(
459
+ entry: DiscoveredRecipe,
460
+ activePath: string | undefined,
461
+ ): Record<string, unknown> | undefined {
462
+ const riskyDiagnostics = entry.diagnostics.filter(
463
+ (message) => diagnosticSeverity(message) === "warning",
464
+ );
465
+ const blockedCandidate = entry.shadows[0];
466
+ if (entry.invalid) {
467
+ return {
468
+ id: entry.id,
469
+ kind: blockedCandidate ? "blocking_invalid" : "invalid",
470
+ severity: "error",
471
+ path: entry.path,
472
+ ...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
473
+ reason: blockedCandidate
474
+ ? "invalid higher-priority recipe blocks a lower-priority candidate"
475
+ : "recipe is invalid and cannot be exposed as a tool",
476
+ action: "fix recipe syntax/config, or disable/delete/archive it to restore fallback",
477
+ };
478
+ }
479
+ if (entry.disabled && entry.active) {
480
+ return {
481
+ id: entry.id,
482
+ kind: blockedCandidate ? "blocking_disabled" : "disabled",
483
+ severity: "warning",
484
+ path: entry.path,
485
+ ...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
486
+ reason: blockedCandidate
487
+ ? "disabled higher-priority recipe intentionally blocks a lower-priority candidate"
488
+ : "recipe is disabled and not exposed as a tool",
489
+ action: "keep disabled intentionally, re-enable, or delete/archive the file",
490
+ };
491
+ }
492
+ if (riskyDiagnostics.length > 0) {
493
+ return {
494
+ id: entry.id,
495
+ kind: "risky_shell_boundary",
496
+ severity: "warning",
497
+ path: entry.path,
498
+ reason: riskyDiagnostics[0],
499
+ action: "audit trusted command boundary; keep only if the recipe is local and intentional",
500
+ };
501
+ }
502
+ if (entry.shadowed) {
503
+ return {
504
+ id: entry.id,
505
+ kind: "shadowed",
506
+ severity: "info",
507
+ path: entry.path,
508
+ ...(activePath ? { active_path: activePath } : {}),
509
+ reason: activePath
510
+ ? `shadowed by ${activePath}`
511
+ : "shadowed by a higher-priority recipe",
512
+ action: "keep as fallback/component, merge, rename, delete, or archive",
513
+ };
514
+ }
515
+ return undefined;
516
+ }
517
+
518
+ function remediationRank(item: Record<string, unknown>): number {
519
+ const kind = String(item.kind ?? "");
520
+ if (kind === "blocking_invalid") return 0;
521
+ if (kind === "invalid") return 1;
522
+ if (kind === "blocking_disabled") return 2;
523
+ if (kind === "risky_shell_boundary") return 3;
524
+ if (kind === "disabled") return 4;
525
+ if (kind === "shadowed") return 5;
526
+ return 6;
527
+ }
528
+
529
+ function discoveryRemediations(
530
+ result: RecipeDiscoveryResult,
531
+ ): Array<Record<string, unknown>> {
532
+ return result.entries
533
+ .map((entry) =>
534
+ remediationForEntry(entry, result.active.get(entry.id)?.path),
535
+ )
536
+ .filter((entry): entry is Record<string, unknown> => Boolean(entry))
537
+ .sort(
538
+ (a, b) =>
539
+ remediationRank(a) - remediationRank(b) ||
540
+ String(a.id).localeCompare(String(b.id)) ||
541
+ String(a.path).localeCompare(String(b.path)),
542
+ );
543
+ }
544
+
458
545
  function recommendationForEntry(
459
546
  entry: DiscoveredRecipe,
460
547
  activePath: string | undefined,
@@ -483,6 +570,7 @@ export function summarizeDiscovery(
483
570
  String(a.id).localeCompare(String(b.id)) ||
484
571
  String(a.path).localeCompare(String(b.path)),
485
572
  );
573
+ const remediations = discoveryRemediations(result);
486
574
  return {
487
575
  active: [...result.active.values()]
488
576
  .map((entry) => ({
@@ -520,6 +608,8 @@ export function summarizeDiscovery(
520
608
  .map((entry) => ({ id: entry.id, path: entry.path }))
521
609
  .sort((a, b) => a.id.localeCompare(b.id)),
522
610
  recommendations,
611
+ remediations,
612
+ top_action: remediations[0],
523
613
  diagnostics: result.diagnostics,
524
614
  diagnostic_details: diagnosticDetails(result),
525
615
  integrity_manifest: createRecipeIntegrityManifest(result),
package/lib/tools.ts CHANGED
@@ -470,6 +470,9 @@ function compactRecipeDoctor(summary: Record<string, unknown>): string {
470
470
  const details = Array.isArray(summary.diagnostic_details)
471
471
  ? (summary.diagnostic_details as Array<Record<string, unknown>>)
472
472
  : [];
473
+ const remediations = Array.isArray(summary.remediations)
474
+ ? (summary.remediations as Array<Record<string, unknown>>)
475
+ : [];
473
476
  const recommendations = Array.isArray(summary.recommendations)
474
477
  ? (summary.recommendations as Array<Record<string, unknown>>)
475
478
  : [];
@@ -479,12 +482,26 @@ function compactRecipeDoctor(summary: Record<string, unknown>): string {
479
482
  if (severity === "error" || severity === "warning" || severity === "info")
480
483
  counts[severity] += 1;
481
484
  }
485
+ const topAction = asRecord(summary.top_action);
482
486
  const lines = [
483
- `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} recommendations=${recommendations.length}`,
487
+ `recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
484
488
  ];
485
- for (const detail of details.slice(0, 8)) {
489
+ if (Object.keys(topAction).length > 0) {
490
+ const action = compactPreview(
491
+ topAction.action,
492
+ Limits.DOCTOR_ACTION_PREVIEW_CHARS,
493
+ );
494
+ lines.push(
495
+ `top severity=${String(topAction.severity ?? "info")} kind=${String(topAction.kind ?? "inspect")} id=${String(topAction.id ?? "root")} action=${action ?? "inspect"}`,
496
+ );
497
+ }
498
+ for (const item of remediations.slice(0, 8)) {
499
+ const action = compactPreview(item.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
500
+ const blocked = item.blocked_candidate
501
+ ? ` blocked=${compactPreview(item.blocked_candidate, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
502
+ : "";
486
503
  lines.push(
487
- `${String(detail.severity ?? "info")} id=${String(detail.id ?? "root")} action=${compactPreview(detail.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS) ?? "inspect"}`,
504
+ `${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
488
505
  );
489
506
  }
490
507
  return `\n${lines.join("\n")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.24.8",
3
+ "version": "0.26.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -4,18 +4,20 @@
4
4
  "args": [
5
5
  "repo:path",
6
6
  "branch:string",
7
- "poll_ms:int"
7
+ "poll_ms:int",
8
+ "stale_claim_ms:int",
9
+ "write_artifacts:bool"
8
10
  ],
9
11
  "defaults": {
10
12
  "repo": "~/.pi/agent/extensions/pi-actors",
11
13
  "branch": "worker",
12
- "poll_ms": "1000"
14
+ "poll_ms": "1000",
15
+ "stale_claim_ms": "0",
16
+ "write_artifacts": "true"
13
17
  },
14
18
  "mailbox": {
15
19
  "accepts": [
16
20
  "task.assign",
17
- "control.stop",
18
- "control.cancel",
19
21
  "control.kill"
20
22
  ],
21
23
  "emits": [
@@ -29,7 +31,9 @@
29
31
  ]
30
32
  },
31
33
  "artifacts": {
32
- "journal": "{state_dir}/worker-events.jsonl"
34
+ "journal": "{state_dir}/worker-events.jsonl",
35
+ "status": "{state_dir}/worker-status.json",
36
+ "results": "{state_dir}/worker-artifacts"
33
37
  },
34
- "template": "{repo}/scripts/actor-worker.mjs --state-dir {state_dir} --run {run_id} --branch {branch} --poll-ms {poll_ms}"
38
+ "template": "{repo}/scripts/actor-worker.mjs --state-dir {state_dir} --run {run_id} --branch {branch} --poll-ms {poll_ms} --stale-claim-ms {stale_claim_ms} --write-artifacts {write_artifacts}"
35
39
  }
@@ -30,8 +30,6 @@
30
30
  },
31
31
  "mailbox": {
32
32
  "accepts": [
33
- "control.stop",
34
- "control.cancel",
35
33
  "control.kill"
36
34
  ],
37
35
  "emits": [
@@ -26,8 +26,6 @@
26
26
  },
27
27
  "mailbox": {
28
28
  "accepts": [
29
- "control.stop",
30
- "control.cancel",
31
29
  "control.kill"
32
30
  ],
33
31
  "emits": [
@@ -35,8 +35,6 @@
35
35
  },
36
36
  "mailbox": {
37
37
  "accepts": [
38
- "control.stop",
39
- "control.cancel",
40
38
  "control.kill"
41
39
  ],
42
40
  "emits": [
@@ -18,8 +18,6 @@
18
18
  },
19
19
  "mailbox": {
20
20
  "accepts": [
21
- "control.stop",
22
- "control.cancel",
23
21
  "control.kill"
24
22
  ],
25
23
  "emits": [
@@ -21,8 +21,6 @@
21
21
  },
22
22
  "mailbox": {
23
23
  "accepts": [
24
- "control.stop",
25
- "control.cancel",
26
24
  "control.kill"
27
25
  ],
28
26
  "emits": [
@@ -24,8 +24,6 @@
24
24
  },
25
25
  "mailbox": {
26
26
  "accepts": [
27
- "control.stop",
28
- "control.cancel",
29
27
  "control.kill"
30
28
  ],
31
29
  "emits": [
@@ -22,8 +22,6 @@
22
22
  "accepts": [
23
23
  "control.reply",
24
24
  "control.revise",
25
- "control.stop",
26
- "control.cancel",
27
25
  "control.kill"
28
26
  ],
29
27
  "emits": [
@@ -24,8 +24,6 @@
24
24
  },
25
25
  "mailbox": {
26
26
  "accepts": [
27
- "control.stop",
28
- "control.cancel",
29
27
  "control.kill"
30
28
  ],
31
29
  "emits": [
@@ -23,8 +23,6 @@
23
23
  },
24
24
  "mailbox": {
25
25
  "accepts": [
26
- "control.stop",
27
- "control.cancel",
28
26
  "control.kill"
29
27
  ],
30
28
  "emits": [
@@ -23,8 +23,6 @@
23
23
  },
24
24
  "mailbox": {
25
25
  "accepts": [
26
- "control.stop",
27
- "control.cancel",
28
26
  "control.kill"
29
27
  ],
30
28
  "emits": [
@@ -20,8 +20,6 @@
20
20
  },
21
21
  "mailbox": {
22
22
  "accepts": [
23
- "control.stop",
24
- "control.cancel",
25
23
  "control.kill"
26
24
  ],
27
25
  "emits": [