@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.
- package/AGENTS.md +125 -59
- package/BACKLOG.md +88 -403
- package/CHANGELOG.md +39 -0
- package/README.md +14 -1
- package/dist/fixtures/protocol/actor-message-branch.json +13 -0
- package/dist/fixtures/protocol/artifact-manifest.json +9 -0
- package/dist/fixtures/protocol/mailbox-contract.json +15 -0
- package/dist/fixtures/protocol/recipe-summary.json +16 -0
- package/dist/fixtures/protocol/room-message.json +11 -0
- package/dist/fixtures/protocol/room-roster.json +11 -0
- package/dist/fixtures/protocol/run-inbox-message.json +9 -0
- package/dist/fixtures/protocol/run-outbox-event.json +9 -0
- package/dist/fixtures/protocol/run-state.json +10 -0
- package/dist/index.js +18 -2
- package/dist/lib/actor-inspector-tui.d.ts +4 -0
- package/dist/lib/actor-inspector-tui.js +58 -44
- package/dist/lib/actor-rooms.d.ts +16 -0
- package/dist/lib/actor-rooms.js +123 -28
- package/dist/lib/actor-worker.d.ts +13 -0
- package/dist/lib/actor-worker.js +86 -0
- package/dist/lib/async-runner.d.ts +5 -0
- package/dist/lib/async-runner.js +134 -0
- package/dist/lib/async-runs.d.ts +34 -2
- package/dist/lib/async-runs.js +224 -50
- package/dist/lib/build-dist.d.ts +5 -0
- package/dist/lib/build-dist.js +24 -0
- package/dist/lib/command-templates.js +5 -5
- package/dist/lib/conformance.d.ts +12 -0
- package/dist/lib/conformance.js +28 -0
- package/dist/lib/coordinator.d.ts +5 -0
- package/dist/lib/coordinator.js +557 -0
- package/dist/lib/limits.d.ts +11 -0
- package/dist/lib/limits.js +11 -0
- package/dist/lib/locker.d.ts +5 -0
- package/dist/lib/locker.js +310 -0
- package/dist/lib/mailbox-loop.d.ts +41 -0
- package/dist/lib/mailbox-loop.js +62 -0
- package/dist/lib/observability.d.ts +2 -2
- package/dist/lib/observability.js +57 -57
- package/dist/lib/output.js +4 -5
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-discovery.js +1 -0
- package/dist/lib/recipe-references.d.ts +11 -2
- package/dist/lib/recipe-references.js +29 -4
- package/dist/lib/recipe-usage.d.ts +2 -1
- package/dist/lib/recipe-usage.js +15 -3
- package/dist/lib/recipe-utils.d.ts +5 -0
- package/dist/lib/recipe-utils.js +385 -0
- package/dist/lib/runtime-notifier.js +3 -7
- package/dist/lib/runtime.js +12 -2
- package/dist/lib/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +198 -39
- package/dist/lib/validate-recipe.d.ts +6 -0
- package/dist/lib/validate-recipe.js +104 -0
- package/dist/recipes/actor-worker.json +35 -0
- package/dist/recipes/coordinator-locker.json +45 -0
- package/dist/recipes/lens-swarm.json +66 -0
- package/dist/recipes/locker.json +45 -0
- package/dist/recipes/music-player.json +38 -0
- package/dist/recipes/pipeline-architect-coordinator.json +95 -0
- package/dist/recipes/pipeline-artifact-bundle.json +100 -0
- package/dist/recipes/pipeline-artifact-report.json +58 -0
- package/dist/recipes/pipeline-artifact-write.json +72 -0
- package/dist/recipes/pipeline-async-run-ops.json +70 -0
- package/dist/recipes/pipeline-checkpoint-continuation.json +67 -0
- package/dist/recipes/pipeline-development-tasking.json +81 -0
- package/dist/recipes/pipeline-docs-maintenance.json +80 -0
- package/dist/recipes/pipeline-media-library.json +59 -0
- package/dist/recipes/pipeline-quorum-review.json +79 -0
- package/dist/recipes/pipeline-release-readiness.json +110 -0
- package/dist/recipes/pipeline-release-summary.json +88 -0
- package/dist/recipes/pipeline-repo-health.json +89 -0
- package/dist/recipes/pipeline-research-synthesis.json +94 -0
- package/dist/recipes/pipeline-review-readiness.json +54 -0
- package/dist/recipes/pipeline-room-swarm.json +50 -0
- package/dist/recipes/subagent-artifact.json +32 -0
- package/dist/recipes/subagent-checkpoint.json +33 -0
- package/dist/recipes/subagent-conflict-report.json +32 -0
- package/dist/recipes/subagent-contradiction-map.json +33 -0
- package/dist/recipes/subagent-critic.json +35 -0
- package/dist/recipes/subagent-evidence-map.json +33 -0
- package/dist/recipes/subagent-followup.json +33 -0
- package/dist/recipes/subagent-judge.json +33 -0
- package/dist/recipes/subagent-merge.json +33 -0
- package/dist/recipes/subagent-message.json +34 -0
- package/dist/recipes/subagent-normalize.json +31 -0
- package/dist/recipes/subagent-plan.json +33 -0
- package/dist/recipes/subagent-prompt.json +28 -0
- package/dist/recipes/subagent-quorum.json +43 -0
- package/dist/recipes/subagent-review-coordinator.json +114 -0
- package/dist/recipes/subagent-review.json +37 -0
- package/dist/recipes/subagent-task-card.json +35 -0
- package/dist/recipes/subagent-tools.json +27 -0
- package/dist/recipes/subagent-verify.json +34 -0
- package/dist/recipes/subagents-prompts.json +51 -0
- package/dist/recipes/utility-actor-message.json +23 -0
- package/dist/recipes/utility-artifact-manifest.json +16 -0
- package/dist/recipes/utility-artifact-write.json +16 -0
- package/dist/recipes/utility-changelog-head.json +11 -0
- package/dist/recipes/utility-changelog-section.json +13 -0
- package/dist/recipes/utility-coordinator-lock-snapshot.json +13 -0
- package/dist/recipes/utility-git-log.json +11 -0
- package/dist/recipes/utility-git-status.json +9 -0
- package/dist/recipes/utility-jsonl-tail.json +10 -0
- package/dist/recipes/utility-markdown-index.json +14 -0
- package/dist/recipes/utility-package-summary.json +11 -0
- package/dist/recipes/utility-playlist-build.json +17 -0
- package/dist/recipes/utility-playlist-scan.json +11 -0
- package/dist/recipes/utility-run-ops-snapshot.json +17 -0
- package/dist/recipes/utility-run-state-files.json +13 -0
- package/dist/recipes/utility-run-summary.json +11 -0
- package/dist/recipes/utility-skill-summary.json +13 -0
- package/dist/recipes/utility-validate-recipe.json +13 -0
- package/dist/recipes/utility-validation-wrapper.json +13 -0
- package/dist/scripts/actor-worker.mjs +31 -0
- package/dist/scripts/async-runner.mjs +31 -0
- package/dist/scripts/build-dist.mjs +25 -0
- package/dist/scripts/conformance.mjs +33 -0
- package/dist/scripts/coordinator.mjs +31 -0
- package/dist/scripts/locker.mjs +33 -0
- package/dist/scripts/music-player.mjs +964 -0
- package/dist/scripts/recipe-utils.mjs +31 -0
- package/dist/scripts/validate-recipe.mjs +34 -0
- package/dist/skills/actors/SKILL.md +375 -0
- package/dist/skills/swarm/SKILL.md +467 -0
- package/dist/skills/swarm/references/development-swarm.md +596 -0
- package/docs/actor-messages.md +2 -2
- package/docs/async-runs.md +13 -1
- package/docs/template-recipes.md +2 -2
- package/docs/tool-registry.md +0 -1
- package/fixtures/protocol/actor-message-branch.json +13 -0
- package/fixtures/protocol/artifact-manifest.json +9 -0
- package/fixtures/protocol/mailbox-contract.json +15 -0
- package/fixtures/protocol/recipe-summary.json +16 -0
- package/fixtures/protocol/room-message.json +11 -0
- package/fixtures/protocol/room-roster.json +11 -0
- package/fixtures/protocol/run-inbox-message.json +9 -0
- package/fixtures/protocol/run-outbox-event.json +9 -0
- package/fixtures/protocol/run-state.json +10 -0
- package/index.ts +21 -0
- package/lib/actor-inspector-tui.ts +138 -59
- package/lib/actor-rooms.ts +241 -60
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +302 -53
- package/lib/build-dist.ts +30 -0
- package/lib/command-templates.ts +5 -5
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +649 -0
- package/lib/limits.ts +12 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +34 -23
- package/lib/output.ts +4 -6
- package/lib/prompts.ts +1 -1
- package/lib/recipe-discovery.ts +1 -0
- package/lib/recipe-references.ts +57 -6
- package/lib/recipe-usage.ts +31 -4
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/runtime.ts +31 -7
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +297 -58
- package/lib/validate-recipe.ts +110 -0
- package/package.json +11 -2
- package/recipes/actor-worker.json +35 -0
- package/recipes/pipeline-quorum-review.json +12 -7
- package/scripts/actor-worker.mjs +31 -0
- package/scripts/async-runner.mjs +11 -195
- package/scripts/build-dist.mjs +25 -0
- package/scripts/conformance.mjs +33 -0
- package/scripts/coordinator.mjs +19 -616
- package/scripts/locker.mjs +23 -322
- package/scripts/music-player.mjs +21 -2
- package/scripts/recipe-utils.mjs +20 -467
- package/scripts/validate-recipe.mjs +23 -113
- package/skills/actors/SKILL.md +7 -4
- package/skills/swarm/SKILL.md +3 -1
|
@@ -11,9 +11,18 @@ export interface TemplateRecipeImportBinding {
|
|
|
11
11
|
values?: Record<string, unknown>;
|
|
12
12
|
}
|
|
13
13
|
export type TemplateRecipeImport = string | TemplateRecipeImportBinding;
|
|
14
|
+
export type TemplateRecipeMailboxEntry = string | {
|
|
15
|
+
type: string;
|
|
16
|
+
body_schema?: unknown;
|
|
17
|
+
ack?: boolean | string;
|
|
18
|
+
idempotency?: string;
|
|
19
|
+
requires_response?: boolean;
|
|
20
|
+
level?: string;
|
|
21
|
+
summary?: string;
|
|
22
|
+
};
|
|
14
23
|
export interface TemplateRecipeMailbox {
|
|
15
|
-
accepts?:
|
|
16
|
-
emits?:
|
|
24
|
+
accepts?: TemplateRecipeMailboxEntry[];
|
|
25
|
+
emits?: TemplateRecipeMailboxEntry[];
|
|
17
26
|
}
|
|
18
27
|
export interface TemplateRecipeDefinition {
|
|
19
28
|
name?: string;
|
|
@@ -195,6 +195,29 @@ function parseMarkdownFrontmatterObject(lines) {
|
|
|
195
195
|
}
|
|
196
196
|
return result;
|
|
197
197
|
}
|
|
198
|
+
function normalizeMarkdownFrontmatterField(key, value) {
|
|
199
|
+
if (key === "args" && typeof value === "string") {
|
|
200
|
+
return value
|
|
201
|
+
.split(",")
|
|
202
|
+
.map((item) => item.trim())
|
|
203
|
+
.filter(Boolean);
|
|
204
|
+
}
|
|
205
|
+
if (key === "defaults" && Array.isArray(value)) {
|
|
206
|
+
return Object.fromEntries(value
|
|
207
|
+
.filter((item) => typeof item === "string")
|
|
208
|
+
.map((item) => {
|
|
209
|
+
const separator = item.indexOf(":");
|
|
210
|
+
return separator < 0
|
|
211
|
+
? [item.trim(), ""]
|
|
212
|
+
: [
|
|
213
|
+
item.slice(0, separator).trim(),
|
|
214
|
+
parseMarkdownScalar(item.slice(separator + 1)),
|
|
215
|
+
];
|
|
216
|
+
})
|
|
217
|
+
.filter(([name]) => Boolean(name)));
|
|
218
|
+
}
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
198
221
|
function parseMarkdownFrontmatter(value) {
|
|
199
222
|
const result = {};
|
|
200
223
|
const lines = value.split(/\r?\n/);
|
|
@@ -206,7 +229,7 @@ function parseMarkdownFrontmatter(value) {
|
|
|
206
229
|
if (!match)
|
|
207
230
|
continue;
|
|
208
231
|
if (match[2]) {
|
|
209
|
-
result[match[1]] = parseMarkdownScalar(match[2]);
|
|
232
|
+
result[match[1]] = normalizeMarkdownFrontmatterField(match[1], parseMarkdownScalar(match[2]));
|
|
210
233
|
continue;
|
|
211
234
|
}
|
|
212
235
|
const nested = [];
|
|
@@ -214,7 +237,7 @@ function parseMarkdownFrontmatter(value) {
|
|
|
214
237
|
index += 1;
|
|
215
238
|
nested.push(lines[index]);
|
|
216
239
|
}
|
|
217
|
-
result[match[1]] = parseMarkdownFrontmatterObject(nested);
|
|
240
|
+
result[match[1]] = normalizeMarkdownFrontmatterField(match[1], parseMarkdownFrontmatterObject(nested));
|
|
218
241
|
}
|
|
219
242
|
return result;
|
|
220
243
|
}
|
|
@@ -630,12 +653,14 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
|
|
|
630
653
|
mailbox: {
|
|
631
654
|
...(Array.isArray(substituted.mailbox.accepts)
|
|
632
655
|
? {
|
|
633
|
-
accepts: substituted.mailbox.accepts.filter((value) => typeof value === "string"
|
|
656
|
+
accepts: substituted.mailbox.accepts.filter((value) => typeof value === "string" ||
|
|
657
|
+
(isRecord(value) && typeof value.type === "string")),
|
|
634
658
|
}
|
|
635
659
|
: {}),
|
|
636
660
|
...(Array.isArray(substituted.mailbox.emits)
|
|
637
661
|
? {
|
|
638
|
-
emits: substituted.mailbox.emits.filter((value) => typeof value === "string"
|
|
662
|
+
emits: substituted.mailbox.emits.filter((value) => typeof value === "string" ||
|
|
663
|
+
(isRecord(value) && typeof value.type === "string")),
|
|
639
664
|
}
|
|
640
665
|
: {}),
|
|
641
666
|
},
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
* Zones: recipe telemetry, muscle-memory cleanup evidence
|
|
4
4
|
* Owns lightweight launch counters for user-owned recipe files
|
|
5
5
|
*/
|
|
6
|
-
export
|
|
6
|
+
export type RecipeLaunchKind = "direct" | "spawn" | "tool";
|
|
7
|
+
export declare function recordRecipeLaunch(path: string, now?: Date, kind?: RecipeLaunchKind): boolean;
|
package/dist/lib/recipe-usage.js
CHANGED
|
@@ -28,7 +28,10 @@ function getRecipeFingerprint(raw) {
|
|
|
28
28
|
const { usage: _usage, ...content } = raw;
|
|
29
29
|
return createHash("sha256").update(stableStringify(content)).digest("hex");
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
function launchCounterKey(kind) {
|
|
32
|
+
return `${kind}_calls`;
|
|
33
|
+
}
|
|
34
|
+
export function recordRecipeLaunch(path, now = new Date(), kind = "direct") {
|
|
32
35
|
if (!existsSync(path))
|
|
33
36
|
return false;
|
|
34
37
|
try {
|
|
@@ -37,16 +40,25 @@ export function recordRecipeLaunch(path, now = new Date()) {
|
|
|
37
40
|
return false;
|
|
38
41
|
const usage = isRecord(raw.usage) ? raw.usage : {};
|
|
39
42
|
const fingerprint = getRecipeFingerprint(raw);
|
|
40
|
-
const changed = typeof usage.fingerprint === "string" &&
|
|
43
|
+
const changed = typeof usage.fingerprint === "string" &&
|
|
44
|
+
usage.fingerprint !== fingerprint;
|
|
41
45
|
const nowIso = now.toISOString();
|
|
46
|
+
const counterKey = launchCounterKey(kind);
|
|
42
47
|
writeJsonAtomic(path, {
|
|
43
48
|
...raw,
|
|
44
49
|
usage: {
|
|
45
50
|
...usage,
|
|
46
51
|
calls: (changed ? 0 : normalizeCalls(usage.calls)) + 1,
|
|
52
|
+
[counterKey]: (changed ? 0 : normalizeCalls(usage[counterKey])) + 1,
|
|
47
53
|
last_called: nowIso,
|
|
54
|
+
launch_kind: kind,
|
|
48
55
|
fingerprint,
|
|
49
|
-
...(changed
|
|
56
|
+
...(changed
|
|
57
|
+
? {
|
|
58
|
+
reset_at: nowIso,
|
|
59
|
+
reset_reason: "recipe content fingerprint changed",
|
|
60
|
+
}
|
|
61
|
+
: {}),
|
|
50
62
|
},
|
|
51
63
|
});
|
|
52
64
|
return true;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe utility command bundle entrypoint logic.
|
|
3
|
+
* Zones: recipe utilities, deterministic helper subcommands
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
7
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
8
|
+
function usage() {
|
|
9
|
+
console.error(`Usage:
|
|
10
|
+
recipe-utils.mjs run-summary <state-root>
|
|
11
|
+
recipe-utils.mjs run-ops-snapshot <state-root> <run-id> [lines] [stale-minutes]
|
|
12
|
+
recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
|
|
13
|
+
recipe-utils.mjs changelog-section <file> <version>
|
|
14
|
+
recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
|
|
15
|
+
recipe-utils.mjs artifact-write <artifact-path> [create|overwrite|append]
|
|
16
|
+
recipe-utils.mjs actor-message <type> [to] [from] [summary] [metadata-json] [correlation-id] [reply-to]
|
|
17
|
+
recipe-utils.mjs package-summary <package-json>
|
|
18
|
+
recipe-utils.mjs skill-summary <skill-md> [package-json]`);
|
|
19
|
+
}
|
|
20
|
+
function fail(message) {
|
|
21
|
+
console.error(message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const ADDRESS_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
25
|
+
const MESSAGE_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
|
|
26
|
+
function assertToken(value, label) {
|
|
27
|
+
const normalized = String(value ?? "").trim();
|
|
28
|
+
if (!normalized)
|
|
29
|
+
fail(`${label} is required`);
|
|
30
|
+
if (!ADDRESS_PATTERN.test(normalized))
|
|
31
|
+
fail(`${label} contains unsupported characters: ${value}`);
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
function validateActorAddress(address, label) {
|
|
35
|
+
const value = String(address ?? "").trim();
|
|
36
|
+
if (value === "coordinator")
|
|
37
|
+
return value;
|
|
38
|
+
const separator = value.indexOf(":");
|
|
39
|
+
if (separator < 0)
|
|
40
|
+
fail(`${label} must include an actor kind: ${address}`);
|
|
41
|
+
const kind = value.slice(0, separator);
|
|
42
|
+
const rest = value.slice(separator + 1);
|
|
43
|
+
if (kind === "branch") {
|
|
44
|
+
const [run, branch, ...extra] = rest.split("/");
|
|
45
|
+
if (extra.length > 0)
|
|
46
|
+
fail(`${label} branch address has too many parts: ${address}`);
|
|
47
|
+
return `branch:${assertToken(run, `${label} branch run`)}/${assertToken(branch, `${label} branch id`)}`;
|
|
48
|
+
}
|
|
49
|
+
if (["run", "session", "tool"].includes(kind))
|
|
50
|
+
return `${kind}:${assertToken(rest, label)}`;
|
|
51
|
+
fail(`${label} has unsupported actor kind: ${kind}`);
|
|
52
|
+
}
|
|
53
|
+
function validateMessageType(type) {
|
|
54
|
+
const value = String(type ?? "").trim();
|
|
55
|
+
if (!MESSAGE_TYPE_PATTERN.test(value))
|
|
56
|
+
fail(`Invalid actor message type: ${type}`);
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
function walkFiles(dir, maxDepth = 2, depth = 0, out = []) {
|
|
60
|
+
if (depth > maxDepth || !existsSync(dir))
|
|
61
|
+
return out;
|
|
62
|
+
for (const entry of readdirSync(dir)) {
|
|
63
|
+
const path = join(dir, entry);
|
|
64
|
+
const stat = statSync(path);
|
|
65
|
+
if (stat.isDirectory())
|
|
66
|
+
walkFiles(path, maxDepth, depth + 1, out);
|
|
67
|
+
else if (stat.isFile())
|
|
68
|
+
out.push(path);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
function readJson(file) {
|
|
73
|
+
if (!existsSync(file))
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getRunStatus(run, progress, result) {
|
|
83
|
+
if (progress?.phase)
|
|
84
|
+
return progress.phase;
|
|
85
|
+
if (result?.code !== undefined)
|
|
86
|
+
return result.code === 0 ? "done" : "failed";
|
|
87
|
+
return run.status ?? "unknown";
|
|
88
|
+
}
|
|
89
|
+
function collectRunSummary(rootValue) {
|
|
90
|
+
const root = resolve(rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
91
|
+
const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
|
|
92
|
+
const rows = [];
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const run = readJson(file);
|
|
95
|
+
if (!run) {
|
|
96
|
+
rows.push({
|
|
97
|
+
run: relative(root, file),
|
|
98
|
+
status: "invalid-json",
|
|
99
|
+
recipe: "",
|
|
100
|
+
updated: "",
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const runDir = dirname(file);
|
|
105
|
+
const progress = readJson(join(runDir, "progress.json"));
|
|
106
|
+
const result = readJson(join(runDir, "result.json"));
|
|
107
|
+
rows.push({
|
|
108
|
+
run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
|
|
109
|
+
status: getRunStatus(run, progress, result),
|
|
110
|
+
recipe: run.recipe ?? run.recipe_file ?? "",
|
|
111
|
+
updated: progress?.updatedAt ??
|
|
112
|
+
result?.completedAt ??
|
|
113
|
+
run.updated_at ??
|
|
114
|
+
run.completed_at ??
|
|
115
|
+
run.started_at ??
|
|
116
|
+
"",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
rows.sort((a, b) => `${a.status}:${a.run}`.localeCompare(`${b.status}:${b.run}`));
|
|
120
|
+
return rows;
|
|
121
|
+
}
|
|
122
|
+
function runSummary(rootValue) {
|
|
123
|
+
console.log(JSON.stringify(collectRunSummary(rootValue), null, 2));
|
|
124
|
+
}
|
|
125
|
+
function tailJsonl(fileValue, linesValue = "80") {
|
|
126
|
+
const file = resolve(fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
127
|
+
if (!existsSync(file))
|
|
128
|
+
return [];
|
|
129
|
+
const lines = Number.parseInt(linesValue, 10);
|
|
130
|
+
const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
|
|
131
|
+
return readFileSync(file, "utf8")
|
|
132
|
+
.trimEnd()
|
|
133
|
+
.split("\n")
|
|
134
|
+
.filter(Boolean)
|
|
135
|
+
.slice(-count)
|
|
136
|
+
.map((line) => {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(line);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return { raw: line };
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function runOpsSnapshot(rootValue, runIdValue = "music", linesValue = "80", staleMinutesValue = "60") {
|
|
146
|
+
const runs = collectRunSummary(rootValue);
|
|
147
|
+
const root = resolve(rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
148
|
+
const inspectedRun = String(runIdValue || "music");
|
|
149
|
+
const messageFile = join(root, inspectedRun, "outbox.jsonl");
|
|
150
|
+
const staleMs = Number(staleMinutesValue) * 60 * 1000;
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
const recommendations = runs.flatMap((run) => {
|
|
153
|
+
const updatedMs = Date.parse(run.updated || "");
|
|
154
|
+
const stale = Number.isFinite(updatedMs) &&
|
|
155
|
+
Number.isFinite(staleMs) &&
|
|
156
|
+
now - updatedMs > staleMs;
|
|
157
|
+
if (run.status === "running" && stale) {
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
run: run.run,
|
|
161
|
+
reason: "running-stale",
|
|
162
|
+
suggested_message: {
|
|
163
|
+
to: `run:${run.run}`,
|
|
164
|
+
type: "control.stop",
|
|
165
|
+
body: "stop",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
if (["failed", "exited", "killed"].includes(run.status)) {
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
run: run.run,
|
|
174
|
+
reason: `terminal-${run.status}`,
|
|
175
|
+
suggested_inspect: { target: `run:${run.run}`, view: "tail" },
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
});
|
|
181
|
+
console.log(JSON.stringify({
|
|
182
|
+
runs,
|
|
183
|
+
inspectedRun,
|
|
184
|
+
messages: tailJsonl(messageFile, linesValue),
|
|
185
|
+
recommendations,
|
|
186
|
+
}, null, 2));
|
|
187
|
+
}
|
|
188
|
+
function playlist(sourceValue, extensionsValue = ".mp3,.ogg,.wav,.flac,.m4a", maxDepthValue = "2", outputMode = "paths") {
|
|
189
|
+
const source = resolve(sourceValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
190
|
+
const maxDepth = Number.parseInt(maxDepthValue, 10);
|
|
191
|
+
const extensions = new Set(extensionsValue
|
|
192
|
+
.split(",")
|
|
193
|
+
.map((item) => item.trim().toLowerCase())
|
|
194
|
+
.filter(Boolean));
|
|
195
|
+
const files = walkFiles(source, Number.isFinite(maxDepth) ? maxDepth : 2)
|
|
196
|
+
.filter((file) => extensions.has(extname(file).toLowerCase()))
|
|
197
|
+
.sort((a, b) => a.localeCompare(b));
|
|
198
|
+
if (outputMode === "m3u")
|
|
199
|
+
console.log(["#EXTM3U", ...files].join("\n"));
|
|
200
|
+
else if (outputMode === "inline")
|
|
201
|
+
console.log(files.join("|"));
|
|
202
|
+
else
|
|
203
|
+
console.log(files.join("\n"));
|
|
204
|
+
}
|
|
205
|
+
function artifactManifest(pathValue, title = "Artifact", status = "draft", summary = "") {
|
|
206
|
+
const path = resolve(pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
207
|
+
const exists = existsSync(path);
|
|
208
|
+
const stat = exists ? statSync(path) : undefined;
|
|
209
|
+
console.log(JSON.stringify({
|
|
210
|
+
title,
|
|
211
|
+
status,
|
|
212
|
+
path,
|
|
213
|
+
exists,
|
|
214
|
+
bytes: stat?.size ?? 0,
|
|
215
|
+
modified: stat?.mtime?.toISOString?.() ?? null,
|
|
216
|
+
summary,
|
|
217
|
+
}, null, 2));
|
|
218
|
+
}
|
|
219
|
+
function artifactWrite(pathValue, mode = "create") {
|
|
220
|
+
const path = resolve(pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
221
|
+
if (!["create", "overwrite", "append"].includes(mode)) {
|
|
222
|
+
fail(`Invalid artifact write mode: ${mode}`);
|
|
223
|
+
}
|
|
224
|
+
if (mode === "create" && existsSync(path)) {
|
|
225
|
+
fail(`Artifact already exists: ${path}`);
|
|
226
|
+
}
|
|
227
|
+
const content = readFileSync(0, "utf8");
|
|
228
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
229
|
+
if (mode === "append")
|
|
230
|
+
appendFileSync(path, content);
|
|
231
|
+
else
|
|
232
|
+
writeFileSync(path, content, "utf8");
|
|
233
|
+
const stat = statSync(path);
|
|
234
|
+
console.log(JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2));
|
|
235
|
+
}
|
|
236
|
+
function actorMessage(type = "event", to = "coordinator", from = "run:{run_id}", summary = "", metadataValue = "", correlationId = "", replyTo = "") {
|
|
237
|
+
const messageType = validateMessageType(type);
|
|
238
|
+
const messageTo = validateActorAddress(to, "message.to");
|
|
239
|
+
const messageFrom = validateActorAddress(from, "message.from");
|
|
240
|
+
let metadata = {};
|
|
241
|
+
if (metadataValue) {
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(metadataValue);
|
|
244
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
245
|
+
metadata = parsed;
|
|
246
|
+
else
|
|
247
|
+
fail("Actor message metadata must be a JSON object");
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
fail(`Invalid actor message metadata JSON: ${error.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const bodyText = readFileSync(0, "utf8");
|
|
254
|
+
const trimmed = bodyText.trim();
|
|
255
|
+
let body = bodyText;
|
|
256
|
+
if (trimmed) {
|
|
257
|
+
try {
|
|
258
|
+
body = JSON.parse(trimmed);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
body = bodyText;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log(JSON.stringify({
|
|
265
|
+
to: messageTo,
|
|
266
|
+
from: messageFrom,
|
|
267
|
+
type: messageType,
|
|
268
|
+
summary: summary || messageType,
|
|
269
|
+
body,
|
|
270
|
+
...(correlationId ? { correlation_id: correlationId } : {}),
|
|
271
|
+
...(replyTo ? { reply_to: replyTo } : {}),
|
|
272
|
+
metadata,
|
|
273
|
+
}, null, 2));
|
|
274
|
+
}
|
|
275
|
+
function packageSummary(fileValue = "package.json") {
|
|
276
|
+
const file = resolve(fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
277
|
+
const pkg = readJson(file);
|
|
278
|
+
if (!pkg)
|
|
279
|
+
fail(`Package JSON not found or invalid: ${fileValue}`);
|
|
280
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object"
|
|
281
|
+
? Object.keys(pkg.scripts).sort()
|
|
282
|
+
: [];
|
|
283
|
+
const dependencies = pkg.dependencies && typeof pkg.dependencies === "object"
|
|
284
|
+
? Object.keys(pkg.dependencies).sort()
|
|
285
|
+
: [];
|
|
286
|
+
const devDependencies = pkg.devDependencies && typeof pkg.devDependencies === "object"
|
|
287
|
+
? Object.keys(pkg.devDependencies).sort()
|
|
288
|
+
: [];
|
|
289
|
+
console.log(JSON.stringify({
|
|
290
|
+
name: pkg.name ?? "",
|
|
291
|
+
version: pkg.version ?? "",
|
|
292
|
+
type: pkg.type ?? "",
|
|
293
|
+
private: Boolean(pkg.private),
|
|
294
|
+
packageManager: pkg.packageManager ?? "",
|
|
295
|
+
files: Array.isArray(pkg.files) ? pkg.files : [],
|
|
296
|
+
bin: pkg.bin ?? null,
|
|
297
|
+
main: pkg.main ?? "",
|
|
298
|
+
exports: pkg.exports ?? null,
|
|
299
|
+
scripts,
|
|
300
|
+
dependencyCount: dependencies.length,
|
|
301
|
+
devDependencyCount: devDependencies.length,
|
|
302
|
+
dependencies,
|
|
303
|
+
devDependencies,
|
|
304
|
+
}, null, 2));
|
|
305
|
+
}
|
|
306
|
+
function parseSkillFrontmatter(content) {
|
|
307
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
308
|
+
const fields = {};
|
|
309
|
+
for (const line of frontmatter.split(/\r?\n/)) {
|
|
310
|
+
const match = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
311
|
+
if (match)
|
|
312
|
+
fields[match[1]] = match[2].trim();
|
|
313
|
+
}
|
|
314
|
+
return { frontmatter, fields };
|
|
315
|
+
}
|
|
316
|
+
function skillSummary(skillValue, packageValue = "package.json") {
|
|
317
|
+
const skillFile = resolve(skillValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
318
|
+
const packageFile = resolve(packageValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"));
|
|
319
|
+
const content = readFileSync(skillFile, "utf8");
|
|
320
|
+
const pkg = readJson(packageFile) ?? {};
|
|
321
|
+
const { frontmatter, fields } = parseSkillFrontmatter(content);
|
|
322
|
+
const scalarLines = frontmatter
|
|
323
|
+
.split(/\r?\n/)
|
|
324
|
+
.filter((line) => /^\w+:\s*\S/.test(line));
|
|
325
|
+
const extraColonLines = scalarLines.filter((line) => (line.match(/:/g) ?? []).length > 1);
|
|
326
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
327
|
+
const version = fields.version ??
|
|
328
|
+
frontmatter.match(/^\s+version:\s*([^\n]+)\s*$/m)?.[1]?.trim() ??
|
|
329
|
+
"";
|
|
330
|
+
console.log(JSON.stringify({
|
|
331
|
+
path: skillValue,
|
|
332
|
+
name: fields.name ?? "",
|
|
333
|
+
description: fields.description ?? "",
|
|
334
|
+
version,
|
|
335
|
+
packageVersion: pkg.version ?? "",
|
|
336
|
+
versionMatchesPackage: version === pkg.version,
|
|
337
|
+
frontmatterExtraColonLines: extraColonLines,
|
|
338
|
+
bodyLineCount: body.split(/\r?\n/).length,
|
|
339
|
+
headings: body.split(/\r?\n/).filter((line) => /^#{1,6}\s/.test(line)),
|
|
340
|
+
}, null, 2));
|
|
341
|
+
}
|
|
342
|
+
function changelogSection(fileValue, version) {
|
|
343
|
+
const file = resolve(fileValue);
|
|
344
|
+
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
345
|
+
const start = lines.findIndex((line) => line.startsWith("## ") && line.includes(version));
|
|
346
|
+
if (start < 0)
|
|
347
|
+
fail(`Version section not found: ${version}`);
|
|
348
|
+
let end = lines.length;
|
|
349
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
350
|
+
if (lines[index].startsWith("## ")) {
|
|
351
|
+
end = index;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log(lines.slice(start, end).join("\n").trimEnd());
|
|
356
|
+
}
|
|
357
|
+
export function runRecipeUtils(argv = process.argv.slice(2)) {
|
|
358
|
+
const [command, ...args] = argv;
|
|
359
|
+
if (!command) {
|
|
360
|
+
usage();
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
if (command === "run-summary")
|
|
364
|
+
runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
|
|
365
|
+
else if (command === "run-ops-snapshot")
|
|
366
|
+
runOpsSnapshot(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs", args[1] ?? "music", args[2], args[3]);
|
|
367
|
+
else if (command === "playlist")
|
|
368
|
+
playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
|
|
369
|
+
else if (command === "changelog-section")
|
|
370
|
+
changelogSection(args[0] ?? "CHANGELOG.md", args[1] ?? "Unreleased");
|
|
371
|
+
else if (command === "artifact-manifest")
|
|
372
|
+
artifactManifest(args[0] ?? "artifact.md", args[1], args[2], args[3]);
|
|
373
|
+
else if (command === "artifact-write")
|
|
374
|
+
artifactWrite(args[0] ?? "artifact.md", args[1] ?? "create");
|
|
375
|
+
else if (command === "actor-message")
|
|
376
|
+
actorMessage(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
|
|
377
|
+
else if (command === "package-summary")
|
|
378
|
+
packageSummary(args[0] ?? "package.json");
|
|
379
|
+
else if (command === "skill-summary")
|
|
380
|
+
skillSummary(args[0] ?? "skills/actors/SKILL.md", args[1] ?? "package.json");
|
|
381
|
+
else {
|
|
382
|
+
usage();
|
|
383
|
+
fail(`Unknown command: ${command}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, watch, } from "node:fs";
|
|
8
8
|
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { readJsonlFileResilient } from "./state-readers.js";
|
|
9
10
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
10
11
|
export function runtimeWakeFile(stateDir) {
|
|
11
12
|
return join(stateDir, "wake.jsonl");
|
|
@@ -61,13 +62,8 @@ export function parseRuntimeWakeEventLine(line) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
export function readRuntimeWakeEvents(stateDir) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return [];
|
|
67
|
-
return readFileSync(file, "utf8")
|
|
68
|
-
.split("\n")
|
|
69
|
-
.filter((line) => line.trim())
|
|
70
|
-
.map(parseRuntimeWakeEventLine)
|
|
65
|
+
return readJsonlFileResilient(runtimeWakeFile(stateDir))
|
|
66
|
+
.records.map((record) => parseRuntimeWakeEventLine(JSON.stringify(record)))
|
|
71
67
|
.filter((event) => Boolean(event));
|
|
72
68
|
}
|
|
73
69
|
export function createFileRuntimeNotifier(stateDir, options = {}) {
|
package/dist/lib/runtime.js
CHANGED
|
@@ -54,6 +54,15 @@ export function createAutoToolsRuntime(deps) {
|
|
|
54
54
|
runtimeTools.add(cfg.name);
|
|
55
55
|
runtimeToolFingerprints.set(cfg.name, fingerprint);
|
|
56
56
|
}
|
|
57
|
+
function isStartupActionableRegistryWarning(warning) {
|
|
58
|
+
if (warning.includes("invokes bash;") &&
|
|
59
|
+
warning.includes("trusted executable content"))
|
|
60
|
+
return false;
|
|
61
|
+
if (warning.includes("invokes bash;") &&
|
|
62
|
+
warning.includes("shell scripts are trusted executable content"))
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
57
66
|
function formatRecipeToolWarnings(warnings) {
|
|
58
67
|
const shadowed = warnings.filter((warning) => warning.includes(" shadows "));
|
|
59
68
|
const skipped = warnings.filter((warning) => warning.includes(" could not be exposed as a tool:"));
|
|
@@ -112,8 +121,9 @@ export function createAutoToolsRuntime(deps) {
|
|
|
112
121
|
}
|
|
113
122
|
registerRuntimeTool(cfg);
|
|
114
123
|
}
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
const startupWarnings = warnings.filter(isStartupActionableRegistryWarning);
|
|
125
|
+
if (startupWarnings.length > 0) {
|
|
126
|
+
notify(ctx, formatRecipeToolWarnings(startupWarnings), "warning");
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
129
|
return {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient state readers.
|
|
3
|
+
* Zones: file-backed actor state, JSON/JSONL diagnostics, inspect safety
|
|
4
|
+
* Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
|
|
5
|
+
*/
|
|
6
|
+
export interface StateReadDiagnostic {
|
|
7
|
+
line?: number;
|
|
8
|
+
message: string;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
export interface JsonReadResult<T> {
|
|
12
|
+
diagnostics: StateReadDiagnostic[];
|
|
13
|
+
value: T;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonlReadResult<T> {
|
|
16
|
+
diagnostics: StateReadDiagnostic[];
|
|
17
|
+
records: T[];
|
|
18
|
+
}
|
|
19
|
+
export declare function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T>;
|
|
20
|
+
export declare function readJsonlFileResilient<T>(path: string): JsonlReadResult<T>;
|
|
21
|
+
export declare function formatStateReadDiagnostics(diagnostics: StateReadDiagnostic[], limit?: number): string[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resilient state readers.
|
|
3
|
+
* Zones: file-backed actor state, JSON/JSONL diagnostics, inspect safety
|
|
4
|
+
* Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
function isEnoent(error) {
|
|
8
|
+
return error.code === "ENOENT";
|
|
9
|
+
}
|
|
10
|
+
function diagnosticMessage(error) {
|
|
11
|
+
return error instanceof Error ? error.message : String(error);
|
|
12
|
+
}
|
|
13
|
+
export function readJsonFileResilient(path, fallback) {
|
|
14
|
+
try {
|
|
15
|
+
return {
|
|
16
|
+
diagnostics: [],
|
|
17
|
+
value: JSON.parse(readFileSync(path, "utf8")),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (isEnoent(error))
|
|
22
|
+
return { diagnostics: [], value: fallback };
|
|
23
|
+
return {
|
|
24
|
+
diagnostics: [
|
|
25
|
+
{
|
|
26
|
+
message: diagnosticMessage(error),
|
|
27
|
+
path,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
value: fallback,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function readJsonlFileResilient(path) {
|
|
35
|
+
try {
|
|
36
|
+
const records = [];
|
|
37
|
+
const diagnostics = [];
|
|
38
|
+
const lines = readFileSync(path, "utf8").split("\n");
|
|
39
|
+
for (const [index, line] of lines.entries()) {
|
|
40
|
+
if (!line.trim())
|
|
41
|
+
continue;
|
|
42
|
+
try {
|
|
43
|
+
records.push(JSON.parse(line));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
line: index + 1,
|
|
48
|
+
message: diagnosticMessage(error),
|
|
49
|
+
path,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { diagnostics, records };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isEnoent(error))
|
|
57
|
+
return { diagnostics: [], records: [] };
|
|
58
|
+
return {
|
|
59
|
+
diagnostics: [
|
|
60
|
+
{
|
|
61
|
+
message: diagnosticMessage(error),
|
|
62
|
+
path,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
records: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function formatStateReadDiagnostics(diagnostics, limit = 5) {
|
|
70
|
+
return diagnostics.slice(0, limit).map((diagnostic) => {
|
|
71
|
+
const line = diagnostic.line === undefined ? "" : `:${diagnostic.line}`;
|
|
72
|
+
return `${diagnostic.path}${line}: ${diagnostic.message}`;
|
|
73
|
+
});
|
|
74
|
+
}
|