@llblab/pi-actors 0.24.7 → 0.25.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.
- package/BACKLOG.md +8 -10
- package/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/lib/mailbox-loop.js +1 -3
- package/dist/lib/recipe-discovery.js +85 -4
- package/dist/lib/tools.js +15 -3
- package/dist/skills/actors/SKILL.md +3 -3
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/async-runs.md +1 -1
- package/docs/recipe-library.md +1 -1
- package/docs/template-recipes.md +1 -1
- package/docs/tool-registry.md +2 -1
- package/lib/mailbox-loop.ts +1 -5
- package/lib/recipe-discovery.ts +94 -4
- package/lib/tools.ts +20 -3
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -3
- package/skills/swarm/SKILL.md +1 -1
package/BACKLOG.md
CHANGED
|
@@ -165,7 +165,7 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
|
|
|
165
165
|
### M-08 Recipe Doctor Remediation UX
|
|
166
166
|
|
|
167
167
|
- Priority: High.
|
|
168
|
-
- Status:
|
|
168
|
+
- Status: Done.
|
|
169
169
|
- Goal: Turn recipe doctor output into an operator action surface, not just a diagnostic listing.
|
|
170
170
|
- Why now: Recipe registry warnings are intentionally actionable; the next value is helping operators decide whether to fix, disable, delete, or inspect a recipe without hiding the warning.
|
|
171
171
|
- Direction:
|
|
@@ -211,15 +211,13 @@ The backlog is intentionally pruned to the 20% of work most likely to deliver 80
|
|
|
211
211
|
### M-11 Actor Termination Semantics
|
|
212
212
|
|
|
213
213
|
- Priority: Medium.
|
|
214
|
-
- Status: Open.
|
|
214
|
+
- Status: Open; core mailbox-loop hotfix landed in 0.24.8.
|
|
215
215
|
- Goal: Make `control.kill` the canonical parent-to-actor termination action while keeping `control.stop` and `control.cancel` as actor-domain messages whose meaning depends on the actor protocol.
|
|
216
216
|
- Why now: Mailbox workers need a clearer lifecycle boundary before v2 patterns harden. Treating `stop`, `cancel`, and `kill` as equivalent stop messages blurs actor termination with domain-specific task or playback control.
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
- Reframe `control.
|
|
221
|
-
- Split mailbox-loop helper semantics so lifecycle termination detection is distinct from general control-message detection.
|
|
222
|
-
- While the package is pre-1.0, allow a small intentional minor-version contract break: remove legacy treatment that aliases `control.stop` or `control.cancel` to actor termination.
|
|
217
|
+
- Remaining direction:
|
|
218
|
+
- Audit packaged recipe `mailbox.accepts` declarations so `control.stop` and `control.cancel` appear only when actor-specific behavior is meaningful.
|
|
219
|
+
- Preserve `control.kill` as the universal lifecycle action for a parent/supervisor terminating an actor or run.
|
|
220
|
+
- Reframe any remaining docs that imply `control.stop` or `control.cancel` are generic runtime termination aliases.
|
|
223
221
|
- Do not preserve compatibility shims for the old stop/cancel-as-termination behavior before the first 1.0 major release unless a concrete safety issue appears during implementation.
|
|
224
222
|
- Acceptance:
|
|
225
223
|
- Docs and actors skill advertise `control.kill` as canonical parent-to-actor termination.
|
|
@@ -242,8 +240,8 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
242
240
|
## Suggested Milestone Order
|
|
243
241
|
|
|
244
242
|
```text
|
|
245
|
-
0.25 —
|
|
246
|
-
M-
|
|
243
|
+
0.25 — Worker maturity:
|
|
244
|
+
M-09
|
|
247
245
|
|
|
248
246
|
0.26 — Package contract hardening and lifecycle semantics:
|
|
249
247
|
M-10, M-11
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.25.0: Recipe Doctor Remediation UX
|
|
6
|
+
|
|
7
|
+
- `[Recipe Doctor]` Added prioritized advisory remediations to recipe doctor output, including a compact top action, structured verbose remediation entries, blocked lower-priority candidates for invalid/disabled overrides, and ordered coverage for invalid, disabled, shadowed, and risky shell-boundary recipes.
|
|
8
|
+
- `[Docs]` Documented the recipe doctor remediation surface in the tool registry guide.
|
|
9
|
+
|
|
10
|
+
## 0.24.8: Mailbox Loop Kill Contract Hotfix
|
|
11
|
+
|
|
12
|
+
- `[Mailbox Loop]` Aligned generic mailbox-loop termination with the actor-message kill contract: only `control.kill` stops generic loop drains; `control.stop` and `control.cancel` remain actor-domain messages unless a recipe handles them explicitly.
|
|
13
|
+
- `[Docs]` Updated worker and recipe guidance so backlog implementer shutdown examples use `control.kill` for runtime termination and describe `control.stop` as domain-local vocabulary.
|
|
14
|
+
|
|
5
15
|
## 0.24.7: Actor Message Kill Contract Hotfix
|
|
6
16
|
|
|
7
17
|
- `[Actor Messages]` Narrowed runtime termination by actor message to `control.kill` only; `control.stop` and `control.cancel` now remain recipe-local mailbox vocabulary instead of aliases for killing/cancelling a run.
|
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ Steer it through messages:
|
|
|
118
118
|
|
|
119
119
|
```text
|
|
120
120
|
message to=run:docs_review type=control.continue body=continue
|
|
121
|
-
message to=run:docs_review type=control.
|
|
121
|
+
message to=run:docs_review type=control.kill body=stop
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
## Actor Rooms
|
package/dist/lib/mailbox-loop.js
CHANGED
|
@@ -9,9 +9,7 @@ export function isMailboxLoopStopMessage(message) {
|
|
|
9
9
|
const type = message && typeof message === "object" && "type" in message
|
|
10
10
|
? message.type
|
|
11
11
|
: undefined;
|
|
12
|
-
return
|
|
13
|
-
type === "control.cancel" ||
|
|
14
|
-
type === "control.kill");
|
|
12
|
+
return type === "control.kill";
|
|
15
13
|
}
|
|
16
14
|
function messageId(message) {
|
|
17
15
|
return typeof message?.id === "string" ? message.id : undefined;
|
|
@@ -273,6 +273,10 @@ function diagnosticSeverity(message) {
|
|
|
273
273
|
return "info";
|
|
274
274
|
}
|
|
275
275
|
function diagnosticSuggestedAction(message) {
|
|
276
|
+
if (/world-writable|group-writable/i.test(message))
|
|
277
|
+
return "tighten recipe root permissions";
|
|
278
|
+
if (/invokes bash/i.test(message))
|
|
279
|
+
return "audit the trusted shell boundary and keep only if intentional";
|
|
276
280
|
if (/must define template/i.test(message))
|
|
277
281
|
return "add a template field or remove the recipe";
|
|
278
282
|
if (/JSON|Expected|parse/i.test(message))
|
|
@@ -285,10 +289,6 @@ function diagnosticSuggestedAction(message) {
|
|
|
285
289
|
return "split large prompt or data into separate files";
|
|
286
290
|
if (/repeat must/i.test(message))
|
|
287
291
|
return "use a positive repeat count or an array-typed repeat source";
|
|
288
|
-
if (/world-writable|group-writable/i.test(message))
|
|
289
|
-
return "tighten recipe root permissions";
|
|
290
|
-
if (/invokes bash/i.test(message))
|
|
291
|
-
return "audit the trusted shell boundary or move details to recipe doctor";
|
|
292
292
|
if (/shadows/i.test(message))
|
|
293
293
|
return "confirm the active override or rename one recipe";
|
|
294
294
|
if (/disabled/i.test(message))
|
|
@@ -323,6 +323,84 @@ function diagnosticDetails(result) {
|
|
|
323
323
|
String(a.message).localeCompare(String(b.message)));
|
|
324
324
|
});
|
|
325
325
|
}
|
|
326
|
+
function remediationForEntry(entry, activePath) {
|
|
327
|
+
const riskyDiagnostics = entry.diagnostics.filter((message) => diagnosticSeverity(message) === "warning");
|
|
328
|
+
const blockedCandidate = entry.shadows[0];
|
|
329
|
+
if (entry.invalid) {
|
|
330
|
+
return {
|
|
331
|
+
id: entry.id,
|
|
332
|
+
kind: blockedCandidate ? "blocking_invalid" : "invalid",
|
|
333
|
+
severity: "error",
|
|
334
|
+
path: entry.path,
|
|
335
|
+
...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
|
|
336
|
+
reason: blockedCandidate
|
|
337
|
+
? "invalid higher-priority recipe blocks a lower-priority candidate"
|
|
338
|
+
: "recipe is invalid and cannot be exposed as a tool",
|
|
339
|
+
action: "fix recipe syntax/config, or disable/delete/archive it to restore fallback",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (entry.disabled && entry.active) {
|
|
343
|
+
return {
|
|
344
|
+
id: entry.id,
|
|
345
|
+
kind: blockedCandidate ? "blocking_disabled" : "disabled",
|
|
346
|
+
severity: "warning",
|
|
347
|
+
path: entry.path,
|
|
348
|
+
...(blockedCandidate ? { blocked_candidate: blockedCandidate } : {}),
|
|
349
|
+
reason: blockedCandidate
|
|
350
|
+
? "disabled higher-priority recipe intentionally blocks a lower-priority candidate"
|
|
351
|
+
: "recipe is disabled and not exposed as a tool",
|
|
352
|
+
action: "keep disabled intentionally, re-enable, or delete/archive the file",
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (riskyDiagnostics.length > 0) {
|
|
356
|
+
return {
|
|
357
|
+
id: entry.id,
|
|
358
|
+
kind: "risky_shell_boundary",
|
|
359
|
+
severity: "warning",
|
|
360
|
+
path: entry.path,
|
|
361
|
+
reason: riskyDiagnostics[0],
|
|
362
|
+
action: "audit trusted command boundary; keep only if the recipe is local and intentional",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (entry.shadowed) {
|
|
366
|
+
return {
|
|
367
|
+
id: entry.id,
|
|
368
|
+
kind: "shadowed",
|
|
369
|
+
severity: "info",
|
|
370
|
+
path: entry.path,
|
|
371
|
+
...(activePath ? { active_path: activePath } : {}),
|
|
372
|
+
reason: activePath
|
|
373
|
+
? `shadowed by ${activePath}`
|
|
374
|
+
: "shadowed by a higher-priority recipe",
|
|
375
|
+
action: "keep as fallback/component, merge, rename, delete, or archive",
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
function remediationRank(item) {
|
|
381
|
+
const kind = String(item.kind ?? "");
|
|
382
|
+
if (kind === "blocking_invalid")
|
|
383
|
+
return 0;
|
|
384
|
+
if (kind === "invalid")
|
|
385
|
+
return 1;
|
|
386
|
+
if (kind === "blocking_disabled")
|
|
387
|
+
return 2;
|
|
388
|
+
if (kind === "risky_shell_boundary")
|
|
389
|
+
return 3;
|
|
390
|
+
if (kind === "disabled")
|
|
391
|
+
return 4;
|
|
392
|
+
if (kind === "shadowed")
|
|
393
|
+
return 5;
|
|
394
|
+
return 6;
|
|
395
|
+
}
|
|
396
|
+
function discoveryRemediations(result) {
|
|
397
|
+
return result.entries
|
|
398
|
+
.map((entry) => remediationForEntry(entry, result.active.get(entry.id)?.path))
|
|
399
|
+
.filter((entry) => Boolean(entry))
|
|
400
|
+
.sort((a, b) => remediationRank(a) - remediationRank(b) ||
|
|
401
|
+
String(a.id).localeCompare(String(b.id)) ||
|
|
402
|
+
String(a.path).localeCompare(String(b.path)));
|
|
403
|
+
}
|
|
326
404
|
function recommendationForEntry(entry, activePath) {
|
|
327
405
|
const recommendation = cleanupRecommendation(entry);
|
|
328
406
|
if (!recommendation)
|
|
@@ -341,6 +419,7 @@ export function summarizeDiscovery(result) {
|
|
|
341
419
|
.filter((entry) => Boolean(entry))
|
|
342
420
|
.sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
|
|
343
421
|
String(a.path).localeCompare(String(b.path)));
|
|
422
|
+
const remediations = discoveryRemediations(result);
|
|
344
423
|
return {
|
|
345
424
|
active: [...result.active.values()]
|
|
346
425
|
.map((entry) => ({
|
|
@@ -378,6 +457,8 @@ export function summarizeDiscovery(result) {
|
|
|
378
457
|
.map((entry) => ({ id: entry.id, path: entry.path }))
|
|
379
458
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
380
459
|
recommendations,
|
|
460
|
+
remediations,
|
|
461
|
+
top_action: remediations[0],
|
|
381
462
|
diagnostics: result.diagnostics,
|
|
382
463
|
diagnostic_details: diagnosticDetails(result),
|
|
383
464
|
integrity_manifest: createRecipeIntegrityManifest(result),
|
package/dist/lib/tools.js
CHANGED
|
@@ -387,6 +387,9 @@ function compactRecipeDoctor(summary) {
|
|
|
387
387
|
const details = Array.isArray(summary.diagnostic_details)
|
|
388
388
|
? summary.diagnostic_details
|
|
389
389
|
: [];
|
|
390
|
+
const remediations = Array.isArray(summary.remediations)
|
|
391
|
+
? summary.remediations
|
|
392
|
+
: [];
|
|
390
393
|
const recommendations = Array.isArray(summary.recommendations)
|
|
391
394
|
? summary.recommendations
|
|
392
395
|
: [];
|
|
@@ -396,11 +399,20 @@ function compactRecipeDoctor(summary) {
|
|
|
396
399
|
if (severity === "error" || severity === "warning" || severity === "info")
|
|
397
400
|
counts[severity] += 1;
|
|
398
401
|
}
|
|
402
|
+
const topAction = asRecord(summary.top_action);
|
|
399
403
|
const lines = [
|
|
400
|
-
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} recommendations=${recommendations.length}`,
|
|
404
|
+
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
|
|
401
405
|
];
|
|
402
|
-
|
|
403
|
-
|
|
406
|
+
if (Object.keys(topAction).length > 0) {
|
|
407
|
+
const action = compactPreview(topAction.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
|
|
408
|
+
lines.push(`top severity=${String(topAction.severity ?? "info")} kind=${String(topAction.kind ?? "inspect")} id=${String(topAction.id ?? "root")} action=${action ?? "inspect"}`);
|
|
409
|
+
}
|
|
410
|
+
for (const item of remediations.slice(0, 8)) {
|
|
411
|
+
const action = compactPreview(item.action, Limits.DOCTOR_ACTION_PREVIEW_CHARS);
|
|
412
|
+
const blocked = item.blocked_candidate
|
|
413
|
+
? ` blocked=${compactPreview(item.blocked_candidate, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
|
|
414
|
+
: "";
|
|
415
|
+
lines.push(`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`);
|
|
404
416
|
}
|
|
405
417
|
return `\n${lines.join("\n")}`;
|
|
406
418
|
}
|
|
@@ -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.
|
|
5
|
+
version: 0.25.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -150,9 +150,9 @@ When using actors as backlog implementers, avoid one-shot subagents that exit af
|
|
|
150
150
|
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
151
151
|
3. Actor executes and validates the slice.
|
|
152
152
|
4. Actor posts `task.result` and `awaiting_assignment`.
|
|
153
|
-
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.
|
|
153
|
+
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.kill`.
|
|
154
154
|
|
|
155
|
-
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, actor-message utility recipes for structured handoffs, and `lib/mailbox-loop.ts` helpers when writing mailbox-consuming workers. Mailbox loops should claim one run or branch inbox message at a time, mark success as `handled`, mark exceptions as `failed`, and
|
|
155
|
+
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, actor-message utility recipes for structured handoffs, and `lib/mailbox-loop.ts` helpers when writing mailbox-consuming workers. Mailbox loops should claim one run or branch inbox message at a time, mark success as `handled`, mark exceptions as `failed`, and treat only `control.kill` as the generic loop termination message; `control.stop` and `control.cancel` are actor-domain messages only when the recipe declares and handles them. Bounded drains may process available work until `control.kill` or a max-message guard. If the existing recipe library cannot express the scenario, add missing reusable component recipes first, then compose the higher-level workflow from them. Supervisors should route coordinator assignments by `body.actor`, preserve the assignment as an object rather than a JSON string, and keep stopped-worker summaries tied to the original actor list.
|
|
156
156
|
|
|
157
157
|
Current packaged building blocks:
|
|
158
158
|
|
package/docs/async-runs.md
CHANGED
|
@@ -146,7 +146,7 @@ The stable loop is:
|
|
|
146
146
|
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
147
147
|
3. Actor completes the slice, validates, and posts `task.result` plus `awaiting_assignment`.
|
|
148
148
|
4. Actor remains alive and waits for the next coordinator message.
|
|
149
|
-
5. Coordinator either sends another `task.assign` or sends `control.
|
|
149
|
+
5. Coordinator either sends another `task.assign` or sends `control.kill` after confirming no actionable work remains.
|
|
150
150
|
|
|
151
151
|
Implementer recipes should declare this contract in `mailbox.accepts` and `mailbox.emits`. They should not self-terminate after a successful slice, and they should not silently self-select a new task unless the coordinator deliberately configured that policy for the run. This keeps task choice centralized while preserving actor-local execution autonomy.
|
|
152
152
|
|
package/docs/recipe-library.md
CHANGED
|
@@ -199,4 +199,4 @@ Cross-platform smoke checklist:
|
|
|
199
199
|
- Only play trusted local files or URLs.
|
|
200
200
|
- Volume is clamped to `0..100` by the wrapper.
|
|
201
201
|
- Prefer a stable `run_id` such as `music` when the operator expects to control the run by name.
|
|
202
|
-
- Use `message type=control.kill`
|
|
202
|
+
- Use `message type=control.kill` for runtime termination; `control.stop` is a player-domain pause/stop command, not a generic run-kill alias.
|
package/docs/template-recipes.md
CHANGED
|
@@ -187,7 +187,7 @@ Recipes do not declare a second event-delivery policy. A running actor emits add
|
|
|
187
187
|
```json
|
|
188
188
|
{
|
|
189
189
|
"mailbox": {
|
|
190
|
-
"accepts": ["control.
|
|
190
|
+
"accepts": ["control.kill"],
|
|
191
191
|
"emits": ["command.done", "run.done", "run.failed"]
|
|
192
192
|
},
|
|
193
193
|
"template": "run-subtask {prompt}"
|
package/docs/tool-registry.md
CHANGED
|
@@ -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
|
|
package/lib/mailbox-loop.ts
CHANGED
|
@@ -57,11 +57,7 @@ export function isMailboxLoopStopMessage(message: unknown): boolean {
|
|
|
57
57
|
message && typeof message === "object" && "type" in message
|
|
58
58
|
? (message as { type?: unknown }).type
|
|
59
59
|
: undefined;
|
|
60
|
-
return
|
|
61
|
-
type === "control.stop" ||
|
|
62
|
-
type === "control.cancel" ||
|
|
63
|
-
type === "control.kill"
|
|
64
|
-
);
|
|
60
|
+
return type === "control.kill";
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
function messageId(
|
package/lib/recipe-discovery.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
package/skills/actors/SKILL.md
CHANGED
|
@@ -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.
|
|
5
|
+
version: 0.25.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -150,9 +150,9 @@ When using actors as backlog implementers, avoid one-shot subagents that exit af
|
|
|
150
150
|
2. Actor posts `task.claim` to `room:<run>` before editing.
|
|
151
151
|
3. Actor executes and validates the slice.
|
|
152
152
|
4. Actor posts `task.result` and `awaiting_assignment`.
|
|
153
|
-
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.
|
|
153
|
+
5. Actor stays alive until the coordinator sends another `task.assign` or an explicit `control.kill`.
|
|
154
154
|
|
|
155
|
-
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, actor-message utility recipes for structured handoffs, and `lib/mailbox-loop.ts` helpers when writing mailbox-consuming workers. Mailbox loops should claim one run or branch inbox message at a time, mark success as `handled`, mark exceptions as `failed`, and
|
|
155
|
+
Use `front`/`back` actors for opposite backlog ends when reducing overlap. Implementer workflows should be packaged as reusable recipe composition, not bespoke scripts: use `coordinator-locker` for queue/assignment/locking, subagent launcher recipes for execution cells, actor-message utility recipes for structured handoffs, and `lib/mailbox-loop.ts` helpers when writing mailbox-consuming workers. Mailbox loops should claim one run or branch inbox message at a time, mark success as `handled`, mark exceptions as `failed`, and treat only `control.kill` as the generic loop termination message; `control.stop` and `control.cancel` are actor-domain messages only when the recipe declares and handles them. Bounded drains may process available work until `control.kill` or a max-message guard. If the existing recipe library cannot express the scenario, add missing reusable component recipes first, then compose the higher-level workflow from them. Supervisors should route coordinator assignments by `body.actor`, preserve the assignment as an object rather than a JSON string, and keep stopped-worker summaries tied to the original actor list.
|
|
156
156
|
|
|
157
157
|
Current packaged building blocks:
|
|
158
158
|
|
package/skills/swarm/SKILL.md
CHANGED