@llblab/pi-actors 0.23.0 → 0.24.1
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 -60
- package/BACKLOG.md +133 -176
- package/CHANGELOG.md +27 -0
- package/README.md +13 -0
- 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 +3 -2
- package/dist/lib/actor-inspector-tui.js +5 -32
- package/dist/lib/actor-rooms.d.ts +8 -0
- package/dist/lib/actor-rooms.js +64 -20
- 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.js +9 -28
- 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 +574 -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 +51 -53
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +1 -1
- package/dist/lib/recipe-references.js +25 -2
- 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/state-readers.d.ts +21 -0
- package/dist/lib/state-readers.js +74 -0
- package/dist/lib/tools.js +1 -1
- 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 +33 -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 +377 -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 +11 -0
- package/docs/template-recipes.md +1 -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 +3 -0
- package/lib/actor-inspector-tui.ts +11 -34
- package/lib/actor-rooms.ts +88 -18
- package/lib/actor-worker.ts +118 -0
- package/lib/async-runner.ts +173 -0
- package/lib/async-runs.ts +12 -22
- package/lib/conformance.ts +46 -0
- package/lib/coordinator.ts +664 -0
- package/lib/locker.ts +340 -0
- package/lib/mailbox-loop.ts +148 -0
- package/lib/observability.ts +24 -19
- package/lib/prompts.ts +1 -1
- package/lib/recipe-references.ts +37 -2
- package/lib/recipe-utils.ts +486 -0
- package/lib/runtime-notifier.ts +4 -6
- package/lib/state-readers.ts +93 -0
- package/lib/tools.ts +1 -1
- package/lib/validate-recipe.ts +110 -0
- package/package.json +10 -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 -201
- package/scripts/build-dist.mjs +33 -0
- package/scripts/conformance.mjs +21 -35
- package/scripts/coordinator.mjs +15 -625
- package/scripts/locker.mjs +20 -332
- package/scripts/recipe-utils.mjs +17 -477
- package/scripts/validate-recipe.mjs +18 -121
- package/skills/actors/SKILL.md +8 -3
- package/skills/swarm/SKILL.md +3 -1
package/scripts/recipe-utils.mjs
CHANGED
|
@@ -1,491 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Recipe utility command bundle.
|
|
4
|
+
* Recipe utility command bundle shim.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* extraction, artifact manifests/writes, actor-message envelopes, package
|
|
9
|
-
* summaries, and skill summaries.
|
|
10
|
-
*
|
|
11
|
-
* Keep outputs compact and machine-readable where possible because recipe-utils
|
|
12
|
-
* often feeds command-template pipelines and actor context directly.
|
|
6
|
+
* Runtime logic lives in lib/recipe-utils.ts and is compiled to
|
|
7
|
+
* dist/lib/recipe-utils.js for installed JS-only packages.
|
|
13
8
|
*/
|
|
14
9
|
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
mkdirSync,
|
|
19
|
-
readFileSync,
|
|
20
|
-
readdirSync,
|
|
21
|
-
statSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from "node:fs";
|
|
24
|
-
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
25
|
-
|
|
26
|
-
function usage() {
|
|
27
|
-
console.error(`Usage:
|
|
28
|
-
recipe-utils.mjs run-summary <state-root>
|
|
29
|
-
recipe-utils.mjs run-ops-snapshot <state-root> <run-id> [lines] [stale-minutes]
|
|
30
|
-
recipe-utils.mjs playlist <source-dir> [extensions] [max-depth] [paths|m3u|inline]
|
|
31
|
-
recipe-utils.mjs changelog-section <file> <version>
|
|
32
|
-
recipe-utils.mjs artifact-manifest <artifact-path> <title> <status> [summary]
|
|
33
|
-
recipe-utils.mjs artifact-write <artifact-path> [create|overwrite|append]
|
|
34
|
-
recipe-utils.mjs actor-message <type> [to] [from] [summary] [metadata-json] [correlation-id] [reply-to]
|
|
35
|
-
recipe-utils.mjs package-summary <package-json>
|
|
36
|
-
recipe-utils.mjs skill-summary <skill-md> [package-json]`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function fail(message) {
|
|
40
|
-
console.error(message);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const ADDRESS_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
45
|
-
const MESSAGE_TYPE_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]*$/;
|
|
46
|
-
function assertToken(value, label) {
|
|
47
|
-
const normalized = String(value ?? "").trim();
|
|
48
|
-
if (!normalized) fail(`${label} is required`);
|
|
49
|
-
if (!ADDRESS_PATTERN.test(normalized))
|
|
50
|
-
fail(`${label} contains unsupported characters: ${value}`);
|
|
51
|
-
return normalized;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function validateActorAddress(address, label) {
|
|
55
|
-
const value = String(address ?? "").trim();
|
|
56
|
-
if (value === "coordinator") return value;
|
|
57
|
-
const separator = value.indexOf(":");
|
|
58
|
-
if (separator < 0) fail(`${label} must include an actor kind: ${address}`);
|
|
59
|
-
const kind = value.slice(0, separator);
|
|
60
|
-
const rest = value.slice(separator + 1);
|
|
61
|
-
if (kind === "branch") {
|
|
62
|
-
const [run, branch, ...extra] = rest.split("/");
|
|
63
|
-
if (extra.length > 0)
|
|
64
|
-
fail(`${label} branch address has too many parts: ${address}`);
|
|
65
|
-
return `branch:${assertToken(run, `${label} branch run`)}/${assertToken(branch, `${label} branch id`)}`;
|
|
66
|
-
}
|
|
67
|
-
if (["run", "session", "tool"].includes(kind))
|
|
68
|
-
return `${kind}:${assertToken(rest, label)}`;
|
|
69
|
-
fail(`${label} has unsupported actor kind: ${kind}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function validateMessageType(type) {
|
|
73
|
-
const value = String(type ?? "").trim();
|
|
74
|
-
if (!MESSAGE_TYPE_PATTERN.test(value))
|
|
75
|
-
fail(`Invalid actor message type: ${type}`);
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function walkFiles(dir, maxDepth = 2, depth = 0, out = []) {
|
|
80
|
-
if (depth > maxDepth || !existsSync(dir)) return out;
|
|
81
|
-
for (const entry of readdirSync(dir)) {
|
|
82
|
-
const path = join(dir, entry);
|
|
83
|
-
const stat = statSync(path);
|
|
84
|
-
if (stat.isDirectory()) walkFiles(path, maxDepth, depth + 1, out);
|
|
85
|
-
else if (stat.isFile()) out.push(path);
|
|
86
|
-
}
|
|
87
|
-
return out;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function readJson(file) {
|
|
91
|
-
if (!existsSync(file)) return undefined;
|
|
92
|
-
try {
|
|
93
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
94
|
-
} catch {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
98
13
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
if (result?.code !== undefined) return result.code === 0 ? "done" : "failed";
|
|
102
|
-
return run.status ?? "unknown";
|
|
14
|
+
function packageRoot() {
|
|
15
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
103
16
|
}
|
|
104
17
|
|
|
105
|
-
function
|
|
106
|
-
const root =
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
const files = walkFiles(root, 2).filter((file) => file.endsWith("/run.json"));
|
|
110
|
-
const rows = [];
|
|
111
|
-
for (const file of files) {
|
|
112
|
-
const run = readJson(file);
|
|
113
|
-
if (!run) {
|
|
114
|
-
rows.push({
|
|
115
|
-
run: relative(root, file),
|
|
116
|
-
status: "invalid-json",
|
|
117
|
-
recipe: "",
|
|
118
|
-
updated: "",
|
|
119
|
-
});
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const runDir = dirname(file);
|
|
123
|
-
const progress = readJson(join(runDir, "progress.json"));
|
|
124
|
-
const result = readJson(join(runDir, "result.json"));
|
|
125
|
-
rows.push({
|
|
126
|
-
run: run.run_id ?? run.run ?? relative(root, file).split("/")[0],
|
|
127
|
-
status: getRunStatus(run, progress, result),
|
|
128
|
-
recipe: run.recipe ?? run.recipe_file ?? "",
|
|
129
|
-
updated:
|
|
130
|
-
progress?.updatedAt ??
|
|
131
|
-
result?.completedAt ??
|
|
132
|
-
run.updated_at ??
|
|
133
|
-
run.completed_at ??
|
|
134
|
-
run.started_at ??
|
|
135
|
-
"",
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
rows.sort((a, b) =>
|
|
139
|
-
`${a.status}:${a.run}`.localeCompare(`${b.status}:${b.run}`),
|
|
140
|
-
);
|
|
141
|
-
return rows;
|
|
18
|
+
function mainModulePath() {
|
|
19
|
+
const root = packageRoot();
|
|
20
|
+
const compiled = join(root, "dist", "lib", "recipe-utils.js");
|
|
21
|
+
return existsSync(compiled) ? compiled : join(root, "lib", "recipe-utils.ts");
|
|
142
22
|
}
|
|
143
23
|
|
|
144
|
-
|
|
145
|
-
console.log(JSON.stringify(collectRunSummary(rootValue), null, 2));
|
|
146
|
-
}
|
|
24
|
+
const { runRecipeUtils } = await import(pathToFileURL(mainModulePath()).href);
|
|
147
25
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
152
|
-
if (!existsSync(file)) return [];
|
|
153
|
-
const lines = Number.parseInt(linesValue, 10);
|
|
154
|
-
const count = Number.isFinite(lines) && lines > 0 ? lines : 80;
|
|
155
|
-
return readFileSync(file, "utf8")
|
|
156
|
-
.trimEnd()
|
|
157
|
-
.split("\n")
|
|
158
|
-
.filter(Boolean)
|
|
159
|
-
.slice(-count)
|
|
160
|
-
.map((line) => {
|
|
161
|
-
try {
|
|
162
|
-
return JSON.parse(line);
|
|
163
|
-
} catch {
|
|
164
|
-
return { raw: line };
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function runOpsSnapshot(
|
|
170
|
-
rootValue,
|
|
171
|
-
runIdValue = "music",
|
|
172
|
-
linesValue = "80",
|
|
173
|
-
staleMinutesValue = "60",
|
|
174
|
-
) {
|
|
175
|
-
const runs = collectRunSummary(rootValue);
|
|
176
|
-
const root = resolve(
|
|
177
|
-
rootValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
178
|
-
);
|
|
179
|
-
const inspectedRun = String(runIdValue || "music");
|
|
180
|
-
const messageFile = join(root, inspectedRun, "outbox.jsonl");
|
|
181
|
-
const staleMs = Number(staleMinutesValue) * 60 * 1000;
|
|
182
|
-
const now = Date.now();
|
|
183
|
-
const recommendations = runs.flatMap((run) => {
|
|
184
|
-
const updatedMs = Date.parse(run.updated || "");
|
|
185
|
-
const stale =
|
|
186
|
-
Number.isFinite(updatedMs) &&
|
|
187
|
-
Number.isFinite(staleMs) &&
|
|
188
|
-
now - updatedMs > staleMs;
|
|
189
|
-
if (run.status === "running" && stale) {
|
|
190
|
-
return [
|
|
191
|
-
{
|
|
192
|
-
run: run.run,
|
|
193
|
-
reason: "running-stale",
|
|
194
|
-
suggested_message: {
|
|
195
|
-
to: `run:${run.run}`,
|
|
196
|
-
type: "control.stop",
|
|
197
|
-
body: "stop",
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
if (["failed", "exited", "killed"].includes(run.status)) {
|
|
203
|
-
return [
|
|
204
|
-
{
|
|
205
|
-
run: run.run,
|
|
206
|
-
reason: `terminal-${run.status}`,
|
|
207
|
-
suggested_inspect: { target: `run:${run.run}`, view: "tail" },
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
}
|
|
211
|
-
return [];
|
|
212
|
-
});
|
|
213
|
-
console.log(
|
|
214
|
-
JSON.stringify(
|
|
215
|
-
{
|
|
216
|
-
runs,
|
|
217
|
-
inspectedRun,
|
|
218
|
-
messages: tailJsonl(messageFile, linesValue),
|
|
219
|
-
recommendations,
|
|
220
|
-
},
|
|
221
|
-
null,
|
|
222
|
-
2,
|
|
223
|
-
),
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function playlist(
|
|
228
|
-
sourceValue,
|
|
229
|
-
extensionsValue = ".mp3,.ogg,.wav,.flac,.m4a",
|
|
230
|
-
maxDepthValue = "2",
|
|
231
|
-
outputMode = "paths",
|
|
232
|
-
) {
|
|
233
|
-
const source = resolve(
|
|
234
|
-
sourceValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
235
|
-
);
|
|
236
|
-
const maxDepth = Number.parseInt(maxDepthValue, 10);
|
|
237
|
-
const extensions = new Set(
|
|
238
|
-
extensionsValue
|
|
239
|
-
.split(",")
|
|
240
|
-
.map((item) => item.trim().toLowerCase())
|
|
241
|
-
.filter(Boolean),
|
|
242
|
-
);
|
|
243
|
-
const files = walkFiles(source, Number.isFinite(maxDepth) ? maxDepth : 2)
|
|
244
|
-
.filter((file) => extensions.has(extname(file).toLowerCase()))
|
|
245
|
-
.sort((a, b) => a.localeCompare(b));
|
|
246
|
-
if (outputMode === "m3u") console.log(["#EXTM3U", ...files].join("\n"));
|
|
247
|
-
else if (outputMode === "inline") console.log(files.join("|"));
|
|
248
|
-
else console.log(files.join("\n"));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function artifactManifest(
|
|
252
|
-
pathValue,
|
|
253
|
-
title = "Artifact",
|
|
254
|
-
status = "draft",
|
|
255
|
-
summary = "",
|
|
256
|
-
) {
|
|
257
|
-
const path = resolve(
|
|
258
|
-
pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
259
|
-
);
|
|
260
|
-
const exists = existsSync(path);
|
|
261
|
-
const stat = exists ? statSync(path) : undefined;
|
|
262
|
-
console.log(
|
|
263
|
-
JSON.stringify(
|
|
264
|
-
{
|
|
265
|
-
title,
|
|
266
|
-
status,
|
|
267
|
-
path,
|
|
268
|
-
exists,
|
|
269
|
-
bytes: stat?.size ?? 0,
|
|
270
|
-
modified: stat?.mtime?.toISOString?.() ?? null,
|
|
271
|
-
summary,
|
|
272
|
-
},
|
|
273
|
-
null,
|
|
274
|
-
2,
|
|
275
|
-
),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function artifactWrite(pathValue, mode = "create") {
|
|
280
|
-
const path = resolve(
|
|
281
|
-
pathValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
282
|
-
);
|
|
283
|
-
if (!["create", "overwrite", "append"].includes(mode)) {
|
|
284
|
-
fail(`Invalid artifact write mode: ${mode}`);
|
|
285
|
-
}
|
|
286
|
-
if (mode === "create" && existsSync(path)) {
|
|
287
|
-
fail(`Artifact already exists: ${path}`);
|
|
288
|
-
}
|
|
289
|
-
const content = readFileSync(0, "utf8");
|
|
290
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
291
|
-
if (mode === "append") appendFileSync(path, content);
|
|
292
|
-
else writeFileSync(path, content, "utf8");
|
|
293
|
-
const stat = statSync(path);
|
|
294
|
-
console.log(
|
|
295
|
-
JSON.stringify({ path, mode, bytes: stat.size, written: true }, null, 2),
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function actorMessage(
|
|
300
|
-
type = "event",
|
|
301
|
-
to = "coordinator",
|
|
302
|
-
from = "run:{run_id}",
|
|
303
|
-
summary = "",
|
|
304
|
-
metadataValue = "",
|
|
305
|
-
correlationId = "",
|
|
306
|
-
replyTo = "",
|
|
307
|
-
) {
|
|
308
|
-
const messageType = validateMessageType(type);
|
|
309
|
-
const messageTo = validateActorAddress(to, "message.to");
|
|
310
|
-
const messageFrom = validateActorAddress(from, "message.from");
|
|
311
|
-
let metadata = {};
|
|
312
|
-
if (metadataValue) {
|
|
313
|
-
try {
|
|
314
|
-
const parsed = JSON.parse(metadataValue);
|
|
315
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
316
|
-
metadata = parsed;
|
|
317
|
-
else fail("Actor message metadata must be a JSON object");
|
|
318
|
-
} catch (error) {
|
|
319
|
-
fail(`Invalid actor message metadata JSON: ${error.message}`);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const bodyText = readFileSync(0, "utf8");
|
|
323
|
-
const trimmed = bodyText.trim();
|
|
324
|
-
let body = bodyText;
|
|
325
|
-
if (trimmed) {
|
|
326
|
-
try {
|
|
327
|
-
body = JSON.parse(trimmed);
|
|
328
|
-
} catch {
|
|
329
|
-
body = bodyText;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
console.log(
|
|
333
|
-
JSON.stringify(
|
|
334
|
-
{
|
|
335
|
-
to: messageTo,
|
|
336
|
-
from: messageFrom,
|
|
337
|
-
type: messageType,
|
|
338
|
-
summary: summary || messageType,
|
|
339
|
-
body,
|
|
340
|
-
...(correlationId ? { correlation_id: correlationId } : {}),
|
|
341
|
-
...(replyTo ? { reply_to: replyTo } : {}),
|
|
342
|
-
metadata,
|
|
343
|
-
},
|
|
344
|
-
null,
|
|
345
|
-
2,
|
|
346
|
-
),
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function packageSummary(fileValue = "package.json") {
|
|
351
|
-
const file = resolve(
|
|
352
|
-
fileValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
353
|
-
);
|
|
354
|
-
const pkg = readJson(file);
|
|
355
|
-
if (!pkg) fail(`Package JSON not found or invalid: ${fileValue}`);
|
|
356
|
-
const scripts =
|
|
357
|
-
pkg.scripts && typeof pkg.scripts === "object"
|
|
358
|
-
? Object.keys(pkg.scripts).sort()
|
|
359
|
-
: [];
|
|
360
|
-
const dependencies =
|
|
361
|
-
pkg.dependencies && typeof pkg.dependencies === "object"
|
|
362
|
-
? Object.keys(pkg.dependencies).sort()
|
|
363
|
-
: [];
|
|
364
|
-
const devDependencies =
|
|
365
|
-
pkg.devDependencies && typeof pkg.devDependencies === "object"
|
|
366
|
-
? Object.keys(pkg.devDependencies).sort()
|
|
367
|
-
: [];
|
|
368
|
-
console.log(
|
|
369
|
-
JSON.stringify(
|
|
370
|
-
{
|
|
371
|
-
name: pkg.name ?? "",
|
|
372
|
-
version: pkg.version ?? "",
|
|
373
|
-
type: pkg.type ?? "",
|
|
374
|
-
private: Boolean(pkg.private),
|
|
375
|
-
packageManager: pkg.packageManager ?? "",
|
|
376
|
-
files: Array.isArray(pkg.files) ? pkg.files : [],
|
|
377
|
-
bin: pkg.bin ?? null,
|
|
378
|
-
main: pkg.main ?? "",
|
|
379
|
-
exports: pkg.exports ?? null,
|
|
380
|
-
scripts,
|
|
381
|
-
dependencyCount: dependencies.length,
|
|
382
|
-
devDependencyCount: devDependencies.length,
|
|
383
|
-
dependencies,
|
|
384
|
-
devDependencies,
|
|
385
|
-
},
|
|
386
|
-
null,
|
|
387
|
-
2,
|
|
388
|
-
),
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function parseSkillFrontmatter(content) {
|
|
393
|
-
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
394
|
-
const fields = {};
|
|
395
|
-
for (const line of frontmatter.split(/\r?\n/)) {
|
|
396
|
-
const match = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
397
|
-
if (match) fields[match[1]] = match[2].trim();
|
|
398
|
-
}
|
|
399
|
-
return { frontmatter, fields };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function skillSummary(skillValue, packageValue = "package.json") {
|
|
403
|
-
const skillFile = resolve(
|
|
404
|
-
skillValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
405
|
-
);
|
|
406
|
-
const packageFile = resolve(
|
|
407
|
-
packageValue.replace(/^~(?=\/|$)/, process.env.HOME ?? "~"),
|
|
408
|
-
);
|
|
409
|
-
const content = readFileSync(skillFile, "utf8");
|
|
410
|
-
const pkg = readJson(packageFile) ?? {};
|
|
411
|
-
const { frontmatter, fields } = parseSkillFrontmatter(content);
|
|
412
|
-
const scalarLines = frontmatter
|
|
413
|
-
.split(/\r?\n/)
|
|
414
|
-
.filter((line) => /^\w+:\s*\S/.test(line));
|
|
415
|
-
const extraColonLines = scalarLines.filter(
|
|
416
|
-
(line) => (line.match(/:/g) ?? []).length > 1,
|
|
417
|
-
);
|
|
418
|
-
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
|
|
419
|
-
const version =
|
|
420
|
-
fields.version ??
|
|
421
|
-
frontmatter.match(/^\s+version:\s*([^\n]+)\s*$/m)?.[1]?.trim() ??
|
|
422
|
-
"";
|
|
423
|
-
console.log(
|
|
424
|
-
JSON.stringify(
|
|
425
|
-
{
|
|
426
|
-
path: skillValue,
|
|
427
|
-
name: fields.name ?? "",
|
|
428
|
-
description: fields.description ?? "",
|
|
429
|
-
version,
|
|
430
|
-
packageVersion: pkg.version ?? "",
|
|
431
|
-
versionMatchesPackage: version === pkg.version,
|
|
432
|
-
frontmatterExtraColonLines: extraColonLines,
|
|
433
|
-
bodyLineCount: body.split(/\r?\n/).length,
|
|
434
|
-
headings: body.split(/\r?\n/).filter((line) => /^#{1,6}\s/.test(line)),
|
|
435
|
-
},
|
|
436
|
-
null,
|
|
437
|
-
2,
|
|
438
|
-
),
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function changelogSection(fileValue, version) {
|
|
443
|
-
const file = resolve(fileValue);
|
|
444
|
-
const lines = readFileSync(file, "utf8").split(/\r?\n/);
|
|
445
|
-
const start = lines.findIndex(
|
|
446
|
-
(line) => line.startsWith("## ") && line.includes(version),
|
|
447
|
-
);
|
|
448
|
-
if (start < 0) fail(`Version section not found: ${version}`);
|
|
449
|
-
let end = lines.length;
|
|
450
|
-
for (let index = start + 1; index < lines.length; index += 1) {
|
|
451
|
-
if (lines[index].startsWith("## ")) {
|
|
452
|
-
end = index;
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
console.log(lines.slice(start, end).join("\n").trimEnd());
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const [command, ...args] = process.argv.slice(2);
|
|
460
|
-
if (!command) {
|
|
461
|
-
usage();
|
|
26
|
+
try {
|
|
27
|
+
runRecipeUtils(process.argv.slice(2));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
462
30
|
process.exit(1);
|
|
463
31
|
}
|
|
464
|
-
|
|
465
|
-
if (command === "run-summary")
|
|
466
|
-
runSummary(args[0] ?? "~/.pi/agent/tmp/pi-actors/runs");
|
|
467
|
-
else if (command === "run-ops-snapshot")
|
|
468
|
-
runOpsSnapshot(
|
|
469
|
-
args[0] ?? "~/.pi/agent/tmp/pi-actors/runs",
|
|
470
|
-
args[1] ?? "music",
|
|
471
|
-
args[2],
|
|
472
|
-
args[3],
|
|
473
|
-
);
|
|
474
|
-
else if (command === "playlist")
|
|
475
|
-
playlist(args[0] ?? "~/Music", args[1], args[2], args[3]);
|
|
476
|
-
else if (command === "changelog-section")
|
|
477
|
-
changelogSection(args[0] ?? "CHANGELOG.md", args[1] ?? "Unreleased");
|
|
478
|
-
else if (command === "artifact-manifest")
|
|
479
|
-
artifactManifest(args[0] ?? "artifact.md", args[1], args[2], args[3]);
|
|
480
|
-
else if (command === "artifact-write")
|
|
481
|
-
artifactWrite(args[0] ?? "artifact.md", args[1] ?? "create");
|
|
482
|
-
else if (command === "actor-message")
|
|
483
|
-
actorMessage(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
|
|
484
|
-
else if (command === "package-summary")
|
|
485
|
-
packageSummary(args[0] ?? "package.json");
|
|
486
|
-
else if (command === "skill-summary")
|
|
487
|
-
skillSummary(args[0] ?? "skills/actors/SKILL.md", args[1] ?? "package.json");
|
|
488
|
-
else {
|
|
489
|
-
usage();
|
|
490
|
-
fail(`Unknown command: ${command}`);
|
|
491
|
-
}
|
|
@@ -1,137 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Template recipe validator CLI.
|
|
4
|
+
* Template recipe validator CLI shim.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* recipe
|
|
8
|
-
* It supports source-tree TypeScript during development and compiled dist
|
|
9
|
-
* modules when installed from npm.
|
|
10
|
-
*
|
|
11
|
-
* Keep it read-only: validation should report parse/import/schema problems
|
|
12
|
-
* without registering tools, launching runs, or mutating user recipes.
|
|
6
|
+
* Runtime logic lives in lib/validate-recipe.ts and is compiled to
|
|
7
|
+
* dist/lib/validate-recipe.js for installed JS-only packages.
|
|
13
8
|
*/
|
|
14
9
|
|
|
15
|
-
import { existsSync
|
|
16
|
-
import {
|
|
17
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
18
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
13
|
|
|
20
|
-
function scriptFile() {
|
|
21
|
-
return fileURLToPath(import.meta.url);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function packageRoot() {
|
|
25
|
-
return dirname(dirname(
|
|
15
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
function
|
|
18
|
+
function mainModulePath() {
|
|
29
19
|
const root = packageRoot();
|
|
30
|
-
const compiled = join(root, "dist", "lib",
|
|
31
|
-
return existsSync(compiled) ? compiled : join(root, "lib",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const { readResolvedRecipeConfig } = await import(
|
|
35
|
-
pathToFileURL(libModulePath("recipe-references")).href
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
function usage() {
|
|
39
|
-
console.error(`Usage:
|
|
40
|
-
validate-recipe.mjs <recipe-file-or-dir> [--all]
|
|
41
|
-
|
|
42
|
-
Validates one template recipe file, or all *.json/*.md files in a directory when --all is set.`);
|
|
20
|
+
const compiled = join(root, "dist", "lib", "validate-recipe.js");
|
|
21
|
+
return existsSync(compiled) ? compiled : join(root, "lib", "validate-recipe.ts");
|
|
43
22
|
}
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
return resolve(
|
|
47
|
-
String(value).replace(/^~(?=\/|$)/, process.env.HOME ?? homedir()),
|
|
48
|
-
);
|
|
49
|
-
}
|
|
24
|
+
const { validateRecipes } = await import(pathToFileURL(mainModulePath()).href);
|
|
50
25
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return "object";
|
|
60
|
-
}
|
|
61
|
-
return "unknown";
|
|
26
|
+
try {
|
|
27
|
+
const report = validateRecipes(process.argv.slice(2));
|
|
28
|
+
if (report.help) console.error(report.usage);
|
|
29
|
+
else console.log(JSON.stringify(report, null, 2));
|
|
30
|
+
process.exit(report.ok ? 0 : 1);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
33
|
+
process.exit(1);
|
|
62
34
|
}
|
|
63
|
-
|
|
64
|
-
function recipeFiles(target, all) {
|
|
65
|
-
if (!existsSync(target)) throw new Error(`Recipe path not found: ${target}`);
|
|
66
|
-
const stat = statSync(target);
|
|
67
|
-
if (stat.isFile()) return [target];
|
|
68
|
-
if (!stat.isDirectory())
|
|
69
|
-
throw new Error(`Recipe path is not a file or directory: ${target}`);
|
|
70
|
-
if (!all) throw new Error("Directory validation requires --all.");
|
|
71
|
-
return readdirSync(target)
|
|
72
|
-
.filter((file) => file.endsWith(".json") || file.endsWith(".md"))
|
|
73
|
-
.sort((a, b) => a.localeCompare(b))
|
|
74
|
-
.map((file) => resolve(target, file));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function validateFile(file) {
|
|
78
|
-
try {
|
|
79
|
-
const config = readResolvedRecipeConfig(file);
|
|
80
|
-
if (!config?.template)
|
|
81
|
-
throw new Error("Recipe must define a non-empty template.");
|
|
82
|
-
return {
|
|
83
|
-
file,
|
|
84
|
-
ok: true,
|
|
85
|
-
name: config.name ?? "",
|
|
86
|
-
async: Boolean(config.async),
|
|
87
|
-
args: Array.isArray(config.args) ? config.args : [],
|
|
88
|
-
defaults:
|
|
89
|
-
config.defaults && typeof config.defaults === "object"
|
|
90
|
-
? Object.keys(config.defaults).sort()
|
|
91
|
-
: [],
|
|
92
|
-
imports:
|
|
93
|
-
config.imports && typeof config.imports === "object"
|
|
94
|
-
? Object.keys(config.imports).sort()
|
|
95
|
-
: [],
|
|
96
|
-
mailbox:
|
|
97
|
-
config.mailbox && typeof config.mailbox === "object"
|
|
98
|
-
? {
|
|
99
|
-
accepts: Array.isArray(config.mailbox.accepts)
|
|
100
|
-
? config.mailbox.accepts
|
|
101
|
-
: [],
|
|
102
|
-
emits: Array.isArray(config.mailbox.emits)
|
|
103
|
-
? config.mailbox.emits
|
|
104
|
-
: [],
|
|
105
|
-
}
|
|
106
|
-
: undefined,
|
|
107
|
-
template: templateKind(config.template),
|
|
108
|
-
};
|
|
109
|
-
} catch (error) {
|
|
110
|
-
return {
|
|
111
|
-
file,
|
|
112
|
-
ok: false,
|
|
113
|
-
error: error instanceof Error ? error.message : String(error),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const args = process.argv.slice(2);
|
|
119
|
-
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
120
|
-
const all = args.includes("--all");
|
|
121
|
-
if (!targetArg || args.includes("--help") || args.includes("-h")) {
|
|
122
|
-
usage();
|
|
123
|
-
process.exit(targetArg ? 0 : 1);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const files = recipeFiles(expandPath(targetArg), all);
|
|
127
|
-
const results = files.map(validateFile);
|
|
128
|
-
const failed = results.filter((result) => !result.ok).length;
|
|
129
|
-
const report = {
|
|
130
|
-
ok: failed === 0,
|
|
131
|
-
total: results.length,
|
|
132
|
-
passed: results.length - failed,
|
|
133
|
-
failed,
|
|
134
|
-
results,
|
|
135
|
-
};
|
|
136
|
-
console.log(JSON.stringify(report, null, 2));
|
|
137
|
-
process.exit(report.ok ? 0 : 1);
|