@llblab/pi-actors 0.22.3 → 0.22.4
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 +456 -81
- package/CHANGELOG.md +16 -0
- package/dist/index.js +1 -1
- package/dist/lib/actor-rooms.d.ts +8 -2
- package/dist/lib/actor-rooms.js +58 -9
- package/dist/lib/async-runs.js +32 -10
- package/dist/lib/observability.js +28 -10
- package/dist/lib/recipe-discovery.js +121 -16
- package/dist/lib/recipe-references.d.ts +1 -0
- package/dist/lib/recipe-references.js +40 -1
- package/dist/lib/registry.js +5 -1
- package/dist/lib/tools.js +25 -8
- package/index.ts +1 -1
- package/lib/actor-rooms.ts +79 -11
- package/lib/async-runs.ts +124 -88
- package/lib/observability.ts +85 -24
- package/lib/recipe-discovery.ts +186 -41
- package/lib/recipe-references.ts +88 -14
- package/lib/registry.ts +7 -1
- package/lib/tools.ts +42 -15
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
package/dist/lib/actor-rooms.js
CHANGED
|
@@ -272,12 +272,47 @@ function updateRosterForMessage(stateDir, room, message, receivedAt) {
|
|
|
272
272
|
}
|
|
273
273
|
return roster;
|
|
274
274
|
}
|
|
275
|
-
|
|
275
|
+
function readBranchInboxFile(stateDir, run, address, limit = 40) {
|
|
276
|
+
const branch = branchIdFromAddress(address, run);
|
|
277
|
+
if (!branch)
|
|
278
|
+
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
279
|
+
try {
|
|
280
|
+
const lines = readJsonlTailLines(branchInboxFile(stateDir, branch), limit);
|
|
281
|
+
const messages = [];
|
|
282
|
+
let corrupted = 0;
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
try {
|
|
285
|
+
messages.push(JSON.parse(line));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
corrupted += 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { corrupted, messages };
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
if (error.code === "ENOENT")
|
|
295
|
+
return { corrupted: 0, messages: [] };
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function readAllBranchInboxLines(stateDir, run, address) {
|
|
276
300
|
const branch = branchIdFromAddress(address, run);
|
|
277
301
|
if (!branch)
|
|
278
302
|
throw new Error(`Expected branch:${run}/<branch>; got ${address}`);
|
|
279
303
|
try {
|
|
280
|
-
|
|
304
|
+
const content = fs.readFileSync(branchInboxFile(stateDir, branch), "utf8");
|
|
305
|
+
return content
|
|
306
|
+
.split("\n")
|
|
307
|
+
.filter((line) => line.length > 0)
|
|
308
|
+
.map((line) => {
|
|
309
|
+
try {
|
|
310
|
+
return { message: JSON.parse(line) };
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return { raw: line };
|
|
314
|
+
}
|
|
315
|
+
});
|
|
281
316
|
}
|
|
282
317
|
catch (error) {
|
|
283
318
|
if (error.code === "ENOENT")
|
|
@@ -285,6 +320,12 @@ export function readBranchInboxMessages(stateDir, run, address, limit = 40) {
|
|
|
285
320
|
throw error;
|
|
286
321
|
}
|
|
287
322
|
}
|
|
323
|
+
export function readBranchInboxMessages(stateDir, run, address, limit = 40) {
|
|
324
|
+
return readBranchInboxFile(stateDir, run, address, limit).messages;
|
|
325
|
+
}
|
|
326
|
+
export function readBranchInboxDiagnostics(stateDir, run, address, limit = 40) {
|
|
327
|
+
return readBranchInboxFile(stateDir, run, address, limit);
|
|
328
|
+
}
|
|
288
329
|
export function getBranchInboxTerminalRetainLimit() {
|
|
289
330
|
const value = Number(process.env.PI_ACTORS_BRANCH_INBOX_TERMINAL_RETAINED ?? "");
|
|
290
331
|
return Number.isInteger(value) && value >= 0
|
|
@@ -320,19 +361,27 @@ export function updateBranchInboxMessageStatus(stateDir, run, address, id, statu
|
|
|
320
361
|
const releaseLock = acquireBranchInboxLock(stateDir, branch);
|
|
321
362
|
try {
|
|
322
363
|
const file = branchInboxFile(stateDir, branch);
|
|
323
|
-
const
|
|
364
|
+
const records = readAllBranchInboxLines(stateDir, run, address);
|
|
324
365
|
let changed = false;
|
|
325
366
|
const timestampKey = `${status}_at`;
|
|
326
|
-
const updated =
|
|
327
|
-
if (message.id !== id)
|
|
328
|
-
return
|
|
367
|
+
const updated = records.map((record) => {
|
|
368
|
+
if (!("message" in record) || record.message.id !== id)
|
|
369
|
+
return record;
|
|
329
370
|
changed = true;
|
|
330
|
-
return { ...message, ...metadata, [timestampKey]: new Date().toISOString(), status };
|
|
371
|
+
return { message: { ...record.message, ...metadata, [timestampKey]: new Date().toISOString(), status } };
|
|
331
372
|
});
|
|
332
373
|
if (!changed)
|
|
333
374
|
return false;
|
|
334
|
-
const
|
|
335
|
-
|
|
375
|
+
const validMessages = updated.flatMap((record) => "message" in record ? [record.message] : []);
|
|
376
|
+
const compactedIds = new Set(compactBranchInboxMessages(validMessages).map((message) => message.id));
|
|
377
|
+
const lines = updated.flatMap((record) => {
|
|
378
|
+
if ("raw" in record)
|
|
379
|
+
return [record.raw];
|
|
380
|
+
if (record.message.id && !compactedIds.has(record.message.id))
|
|
381
|
+
return [];
|
|
382
|
+
return [JSON.stringify(record.message)];
|
|
383
|
+
});
|
|
384
|
+
fs.writeFileSync(file, lines.length ? `${lines.join("\n")}\n` : "");
|
|
336
385
|
notifyActorWake(stateDir, address, "branch.inbox.status", { id, status });
|
|
337
386
|
return true;
|
|
338
387
|
}
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Zones: async runtime, lifecycle, state files
|
|
4
4
|
* Owns detached run state, observation, log tailing, listing, and cancellation safety
|
|
5
5
|
*/
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { closeSync, constants, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
9
9
|
import { createConnection } from "node:net";
|
|
10
10
|
import { platform } from "node:os";
|
|
@@ -22,7 +22,8 @@ const DEFAULT_STATE_ROOT = Paths.getRunStateRoot();
|
|
|
22
22
|
const DEFAULT_RECIPE_ROOT = Paths.getRecipeRoot();
|
|
23
23
|
function packageRoot() {
|
|
24
24
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
if (basename(moduleDir) === "lib" &&
|
|
25
|
+
if (basename(moduleDir) === "lib" &&
|
|
26
|
+
basename(dirname(moduleDir)) === "dist") {
|
|
26
27
|
return dirname(dirname(moduleDir));
|
|
27
28
|
}
|
|
28
29
|
return dirname(moduleDir);
|
|
@@ -263,7 +264,8 @@ export function startRun(params, cwd) {
|
|
|
263
264
|
? resolveRecipeFile(startParams.file)
|
|
264
265
|
: undefined;
|
|
265
266
|
const recipe = startParams.name || getRunIdFromFile(recipeFile);
|
|
266
|
-
const includeActorRecipeContext = startParams.actor_context !== false &&
|
|
267
|
+
const includeActorRecipeContext = startParams.actor_context !== false &&
|
|
268
|
+
startParams.actor_context !== "off";
|
|
267
269
|
const recipeContextRecords = recipeFile && includeActorRecipeContext
|
|
268
270
|
? RecipeReferences.buildRecipeContextRecords(recipeFile)
|
|
269
271
|
: undefined;
|
|
@@ -290,7 +292,9 @@ export function startRun(params, cwd) {
|
|
|
290
292
|
argv: [process.execPath, ...argv],
|
|
291
293
|
createdAt: new Date().toISOString(),
|
|
292
294
|
cwd,
|
|
293
|
-
...(startParams.launch_source
|
|
295
|
+
...(startParams.launch_source
|
|
296
|
+
? { launch_source: startParams.launch_source }
|
|
297
|
+
: {}),
|
|
294
298
|
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
295
299
|
pid: 0,
|
|
296
300
|
...(recipe ? { recipe } : {}),
|
|
@@ -582,7 +586,9 @@ export function claimRunInboxMessage(runOrDir, owner = "runtime", statuses = ["q
|
|
|
582
586
|
...messages[index],
|
|
583
587
|
claimed_at: new Date().toISOString(),
|
|
584
588
|
claimed_by: owner,
|
|
585
|
-
id: typeof messages[index].id === "string"
|
|
589
|
+
id: typeof messages[index].id === "string"
|
|
590
|
+
? messages[index].id
|
|
591
|
+
: randomUUID(),
|
|
586
592
|
status: "claimed",
|
|
587
593
|
};
|
|
588
594
|
messages[index] = claimed;
|
|
@@ -669,9 +675,10 @@ function appendRunInboxMessage(stateDir, message) {
|
|
|
669
675
|
let record;
|
|
670
676
|
try {
|
|
671
677
|
const parsed = JSON.parse(message);
|
|
672
|
-
record =
|
|
673
|
-
|
|
674
|
-
|
|
678
|
+
record =
|
|
679
|
+
parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
680
|
+
? parsed
|
|
681
|
+
: { body: parsed, type: "run.message" };
|
|
675
682
|
}
|
|
676
683
|
catch {
|
|
677
684
|
record = { body: message, type: "run.message" };
|
|
@@ -781,6 +788,7 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
|
781
788
|
control: "inbox.jsonl",
|
|
782
789
|
control_path: endpoint.path,
|
|
783
790
|
control_type: endpoint.type,
|
|
791
|
+
inbox_id: inboxId,
|
|
784
792
|
queued: true,
|
|
785
793
|
run,
|
|
786
794
|
sent: true,
|
|
@@ -800,6 +808,7 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
|
800
808
|
control: "control.fifo",
|
|
801
809
|
control_path: endpoint.path,
|
|
802
810
|
control_type: endpoint.type,
|
|
811
|
+
inbox_id: inboxId,
|
|
803
812
|
run,
|
|
804
813
|
sent: true,
|
|
805
814
|
state_dir: stateDir,
|
|
@@ -813,13 +822,24 @@ export async function sendRunMessage(runOrDir, message, options = {}) {
|
|
|
813
822
|
control: endpoint.path,
|
|
814
823
|
control_path: endpoint.path,
|
|
815
824
|
control_type: endpoint.type,
|
|
825
|
+
inbox_id: inboxId,
|
|
816
826
|
run,
|
|
817
827
|
sent: true,
|
|
818
828
|
state_dir: stateDir,
|
|
819
829
|
};
|
|
820
830
|
}
|
|
821
831
|
catch (error) {
|
|
822
|
-
|
|
832
|
+
const deliveryError = error instanceof Error ? error.message : String(error);
|
|
833
|
+
throw Object.assign(new Error(`Run control endpoint is not ready: ${endpoint.path}: ${deliveryError}`), {
|
|
834
|
+
control_path: endpoint.path,
|
|
835
|
+
control_type: endpoint.type,
|
|
836
|
+
delivery_error: deliveryError,
|
|
837
|
+
inbox_id: inboxId,
|
|
838
|
+
queued: true,
|
|
839
|
+
run,
|
|
840
|
+
sent: false,
|
|
841
|
+
state_dir: stateDir,
|
|
842
|
+
});
|
|
823
843
|
}
|
|
824
844
|
}
|
|
825
845
|
export function getRunProcessSignalPlan(pid, signal, runtimePlatform = process.platform) {
|
|
@@ -842,7 +862,9 @@ function signalOwnedRunProcess(pid, signal) {
|
|
|
842
862
|
if (plan.command && plan.args) {
|
|
843
863
|
const result = spawnSync(plan.command, plan.args, { encoding: "utf8" });
|
|
844
864
|
if (result.status !== 0) {
|
|
845
|
-
throw new Error(result.stderr?.trim() ||
|
|
865
|
+
throw new Error(result.stderr?.trim() ||
|
|
866
|
+
result.stdout?.trim() ||
|
|
867
|
+
`${plan.command} failed`);
|
|
846
868
|
}
|
|
847
869
|
return plan;
|
|
848
870
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns ambient summaries, terminal events, and run outbox delivery for detached command-template runs
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
8
8
|
import * as AsyncRuns from "./async-runs.js";
|
|
9
9
|
import * as Paths from "./paths.js";
|
|
10
10
|
const TERMINAL = new Set([
|
|
@@ -324,14 +324,22 @@ export async function executeRunRetirements(summary, options) {
|
|
|
324
324
|
const results = [];
|
|
325
325
|
for (const candidate of findRunRetirementCandidates(summary)) {
|
|
326
326
|
if (options.attempted?.has(candidate.stateDir)) {
|
|
327
|
-
results.push({
|
|
327
|
+
results.push({
|
|
328
|
+
action: "skip",
|
|
329
|
+
run: candidate.run,
|
|
330
|
+
stateDir: candidate.stateDir,
|
|
331
|
+
});
|
|
328
332
|
continue;
|
|
329
333
|
}
|
|
330
334
|
options.attempted?.add(candidate.stateDir);
|
|
331
335
|
try {
|
|
332
336
|
await options.sendStop(candidate);
|
|
333
337
|
options.notify?.(`Retiring actor ${candidate.run} after child runs reached terminal state`, "info");
|
|
334
|
-
results.push({
|
|
338
|
+
results.push({
|
|
339
|
+
action: "stop",
|
|
340
|
+
run: candidate.run,
|
|
341
|
+
stateDir: candidate.stateDir,
|
|
342
|
+
});
|
|
335
343
|
continue;
|
|
336
344
|
}
|
|
337
345
|
catch (error) {
|
|
@@ -343,13 +351,19 @@ export async function executeRunRetirements(summary, options) {
|
|
|
343
351
|
: `Actor retirement skipped for ${candidate.run}: ${error instanceof Error ? error.message : String(error)}`, cancelled ? "warning" : "error");
|
|
344
352
|
results.push({
|
|
345
353
|
action: cancelled ? "cancel" : "skip",
|
|
346
|
-
...(cancelled
|
|
354
|
+
...(cancelled
|
|
355
|
+
? {}
|
|
356
|
+
: {
|
|
357
|
+
error: error instanceof Error ? error.message : String(error),
|
|
358
|
+
}),
|
|
347
359
|
run: candidate.run,
|
|
348
360
|
stateDir: candidate.stateDir,
|
|
349
361
|
});
|
|
350
362
|
}
|
|
351
363
|
catch (cancelError) {
|
|
352
|
-
const message = cancelError instanceof Error
|
|
364
|
+
const message = cancelError instanceof Error
|
|
365
|
+
? cancelError.message
|
|
366
|
+
: String(cancelError);
|
|
353
367
|
options.notify?.(`Actor retirement failed for ${candidate.run}: ${message}`, "error");
|
|
354
368
|
results.push({
|
|
355
369
|
action: "failed",
|
|
@@ -362,10 +376,14 @@ export async function executeRunRetirements(summary, options) {
|
|
|
362
376
|
}
|
|
363
377
|
return results;
|
|
364
378
|
}
|
|
379
|
+
function runObservationKey(run) {
|
|
380
|
+
return run.stateDir ?? run.run;
|
|
381
|
+
}
|
|
365
382
|
export function detectRunTransitions(previous, summary) {
|
|
366
383
|
const transitions = [];
|
|
367
384
|
for (const run of summary.runs) {
|
|
368
|
-
const
|
|
385
|
+
const key = runObservationKey(run);
|
|
386
|
+
const old = previous.get(key);
|
|
369
387
|
if (old && old !== run.status && TERMINAL.has(run.status)) {
|
|
370
388
|
transitions.push({
|
|
371
389
|
from: old,
|
|
@@ -379,7 +397,7 @@ export function detectRunTransitions(previous, summary) {
|
|
|
379
397
|
...(run.tool ? { tool: run.tool } : {}),
|
|
380
398
|
});
|
|
381
399
|
}
|
|
382
|
-
previous.set(
|
|
400
|
+
previous.set(key, run.status);
|
|
383
401
|
}
|
|
384
402
|
return transitions;
|
|
385
403
|
}
|
|
@@ -440,11 +458,11 @@ function readOutboxLines(run) {
|
|
|
440
458
|
return content ? content.split("\n") : [];
|
|
441
459
|
}
|
|
442
460
|
export function pruneRunObservationState(previousStatuses, previousLineCounts, summary, terminalRuns = []) {
|
|
443
|
-
const activeRuns = new Set(summary.runs.map((run) => run
|
|
461
|
+
const activeRuns = new Set(summary.runs.map((run) => runObservationKey(run)));
|
|
444
462
|
const terminalRunSet = new Set(terminalRuns);
|
|
445
463
|
const terminalLineKeys = new Set(summary.runs
|
|
446
|
-
.filter((run) => terminalRunSet.has(run
|
|
447
|
-
.map((run) => run
|
|
464
|
+
.filter((run) => terminalRunSet.has(runObservationKey(run)))
|
|
465
|
+
.map((run) => runObservationKey(run)));
|
|
448
466
|
const activeLineKeys = new Set(summary.runs.map((run) => run.stateDir ?? run.run));
|
|
449
467
|
for (const run of terminalRunSet)
|
|
450
468
|
previousStatuses.delete(run);
|
|
@@ -24,7 +24,9 @@ function assertToolSafeRepeatConfig(config, argTypes, defaults) {
|
|
|
24
24
|
const trimmed = node.repeat.trim();
|
|
25
25
|
if (!/^\d+$/.test(trimmed)) {
|
|
26
26
|
const match = trimmed.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\.length\}?$/);
|
|
27
|
-
if (!match ||
|
|
27
|
+
if (!match ||
|
|
28
|
+
(argTypes[match[1]]?.kind !== "array" &&
|
|
29
|
+
!Array.isArray(defaults[match[1]])))
|
|
28
30
|
throw new Error("Command template repeat must be a positive integer or {array.length} with an array argument/default.");
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -39,11 +41,16 @@ function listRecipeFiles(root) {
|
|
|
39
41
|
(entry.name.endsWith(".json") || entry.name.endsWith(".md")) &&
|
|
40
42
|
entry.name !== "legacy-tool-registry-migration-report.json")
|
|
41
43
|
.map((entry) => join(root, entry.name))
|
|
42
|
-
.sort((a, b) => a
|
|
44
|
+
.sort((a, b) => a
|
|
45
|
+
.replace(/\.md$/, ".json")
|
|
46
|
+
.localeCompare(b.replace(/\.md$/, ".json")) ||
|
|
47
|
+
(a.endsWith(".json") ? -1 : 1));
|
|
43
48
|
}
|
|
44
49
|
function getRecipeConfigDiagnostics(file, config) {
|
|
45
|
-
if (!config)
|
|
46
|
-
|
|
50
|
+
if (!config) {
|
|
51
|
+
const reason = RecipeReferences.diagnoseRawRecipeConfigFailure(file);
|
|
52
|
+
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
53
|
+
}
|
|
47
54
|
const commandTemplateConfig = typeof config.template === "object" && config.template !== null
|
|
48
55
|
? config.template
|
|
49
56
|
: config;
|
|
@@ -94,7 +101,14 @@ function filesForSource(source) {
|
|
|
94
101
|
const defaultTool = source.defaultTool === true;
|
|
95
102
|
const mutableUsage = source.mutableUsage === true;
|
|
96
103
|
if (source.file)
|
|
97
|
-
return [
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
root: source.root ?? source.file,
|
|
107
|
+
file: source.file,
|
|
108
|
+
defaultTool,
|
|
109
|
+
mutableUsage,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
98
112
|
return source.root
|
|
99
113
|
? listRecipeFiles(source.root).map((file) => ({
|
|
100
114
|
root: source.root,
|
|
@@ -139,7 +153,9 @@ export function discoverRecipeSources(sources) {
|
|
|
139
153
|
const diagnostics = getRecipeRootDiagnostics(sources);
|
|
140
154
|
for (const [id, bucket] of byId) {
|
|
141
155
|
bucket.sort((a, b) => a.priority - b.priority ||
|
|
142
|
-
a.path
|
|
156
|
+
a.path
|
|
157
|
+
.replace(/\.md$/, ".json")
|
|
158
|
+
.localeCompare(b.path.replace(/\.md$/, ".json")) ||
|
|
143
159
|
(a.path.endsWith(".json") ? -1 : 1));
|
|
144
160
|
const winner = bucket[0];
|
|
145
161
|
winner.active = true;
|
|
@@ -209,7 +225,13 @@ function cleanupRecommendation(entry) {
|
|
|
209
225
|
id: entry.id,
|
|
210
226
|
path: entry.path,
|
|
211
227
|
reason: "user recipe is a component, not an active tool",
|
|
212
|
-
actions: [
|
|
228
|
+
actions: [
|
|
229
|
+
"keep component",
|
|
230
|
+
"move into tool root",
|
|
231
|
+
"merge",
|
|
232
|
+
"delete",
|
|
233
|
+
"archive",
|
|
234
|
+
],
|
|
213
235
|
};
|
|
214
236
|
}
|
|
215
237
|
if (entry.shadows.length > 0) {
|
|
@@ -241,6 +263,66 @@ export function createRecipeIntegrityManifest(result) {
|
|
|
241
263
|
})
|
|
242
264
|
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path));
|
|
243
265
|
}
|
|
266
|
+
function diagnosticSeverity(message) {
|
|
267
|
+
if (/invalid|failed to load|not found|cyclic|exceeds|must define|repeat must/i.test(message)) {
|
|
268
|
+
return "error";
|
|
269
|
+
}
|
|
270
|
+
if (/world-writable|group-writable|invokes bash|eval|destructive|unsafe/i.test(message)) {
|
|
271
|
+
return "warning";
|
|
272
|
+
}
|
|
273
|
+
return "info";
|
|
274
|
+
}
|
|
275
|
+
function diagnosticSuggestedAction(message) {
|
|
276
|
+
if (/must define template/i.test(message))
|
|
277
|
+
return "add a template field or remove the recipe";
|
|
278
|
+
if (/JSON|Expected|parse/i.test(message))
|
|
279
|
+
return "fix recipe syntax or archive the file";
|
|
280
|
+
if (/Markdown recipe/i.test(message))
|
|
281
|
+
return "fix frontmatter and add a fenced template or recipe block";
|
|
282
|
+
if (/cyclic/i.test(message))
|
|
283
|
+
return "break the import cycle";
|
|
284
|
+
if (/exceeds.*size/i.test(message))
|
|
285
|
+
return "split large prompt or data into separate files";
|
|
286
|
+
if (/repeat must/i.test(message))
|
|
287
|
+
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
|
+
if (/shadows/i.test(message))
|
|
293
|
+
return "confirm the active override or rename one recipe";
|
|
294
|
+
if (/disabled/i.test(message))
|
|
295
|
+
return "keep disabled intentionally or delete/archive the file";
|
|
296
|
+
return "inspect the recipe and fix or archive it if unexpected";
|
|
297
|
+
}
|
|
298
|
+
function diagnosticDetails(result) {
|
|
299
|
+
const details = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const push = (message, entry) => {
|
|
302
|
+
const key = `${entry?.path ?? "root"}\n${message}`;
|
|
303
|
+
if (seen.has(key))
|
|
304
|
+
return;
|
|
305
|
+
seen.add(key);
|
|
306
|
+
details.push({
|
|
307
|
+
...(entry ? { id: entry.id, path: entry.path } : {}),
|
|
308
|
+
action: diagnosticSuggestedAction(message),
|
|
309
|
+
message,
|
|
310
|
+
severity: diagnosticSeverity(message),
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
for (const message of result.diagnostics)
|
|
314
|
+
push(message);
|
|
315
|
+
for (const entry of result.entries) {
|
|
316
|
+
for (const message of entry.diagnostics)
|
|
317
|
+
push(message, entry);
|
|
318
|
+
}
|
|
319
|
+
return details.sort((a, b) => {
|
|
320
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
321
|
+
return (rank[a.severity] -
|
|
322
|
+
rank[b.severity] ||
|
|
323
|
+
String(a.message).localeCompare(String(b.message)));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
244
326
|
function recommendationForEntry(entry, activePath) {
|
|
245
327
|
const recommendation = cleanupRecommendation(entry);
|
|
246
328
|
if (!recommendation)
|
|
@@ -257,9 +339,11 @@ export function summarizeDiscovery(result) {
|
|
|
257
339
|
const recommendations = result.entries
|
|
258
340
|
.map((entry) => recommendationForEntry(entry, result.active.get(entry.id)?.path))
|
|
259
341
|
.filter((entry) => Boolean(entry))
|
|
260
|
-
.sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
|
|
342
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)) ||
|
|
343
|
+
String(a.path).localeCompare(String(b.path)));
|
|
261
344
|
return {
|
|
262
|
-
active: [...result.active.values()]
|
|
345
|
+
active: [...result.active.values()]
|
|
346
|
+
.map((entry) => ({
|
|
263
347
|
id: entry.id,
|
|
264
348
|
path: entry.path,
|
|
265
349
|
description: entry.config?.description,
|
|
@@ -267,15 +351,26 @@ export function summarizeDiscovery(result) {
|
|
|
267
351
|
disabled: entry.disabled,
|
|
268
352
|
invalid: entry.invalid,
|
|
269
353
|
shadows: entry.shadows,
|
|
270
|
-
...(recipeUsage(entry.config)
|
|
271
|
-
|
|
354
|
+
...(recipeUsage(entry.config)
|
|
355
|
+
? { usage: recipeUsage(entry.config) }
|
|
356
|
+
: {}),
|
|
357
|
+
}))
|
|
358
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
272
359
|
shadowed: result.entries
|
|
273
360
|
.filter((entry) => entry.shadowed)
|
|
274
|
-
.map((entry) => ({
|
|
361
|
+
.map((entry) => ({
|
|
362
|
+
id: entry.id,
|
|
363
|
+
path: entry.path,
|
|
364
|
+
shadowedBy: result.active.get(entry.id)?.path,
|
|
365
|
+
}))
|
|
275
366
|
.sort((a, b) => a.id.localeCompare(b.id) || a.path.localeCompare(b.path)),
|
|
276
367
|
invalid: result.entries
|
|
277
368
|
.filter((entry) => entry.invalid)
|
|
278
|
-
.map((entry) => ({
|
|
369
|
+
.map((entry) => ({
|
|
370
|
+
id: entry.id,
|
|
371
|
+
path: entry.path,
|
|
372
|
+
diagnostics: entry.diagnostics,
|
|
373
|
+
}))
|
|
279
374
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
280
375
|
disabled: result.entries
|
|
281
376
|
.filter((entry) => entry.disabled)
|
|
@@ -283,6 +378,7 @@ export function summarizeDiscovery(result) {
|
|
|
283
378
|
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
284
379
|
recommendations,
|
|
285
380
|
diagnostics: result.diagnostics,
|
|
381
|
+
diagnostic_details: diagnosticDetails(result),
|
|
286
382
|
integrity_manifest: createRecipeIntegrityManifest(result),
|
|
287
383
|
};
|
|
288
384
|
}
|
|
@@ -297,7 +393,10 @@ export function toRegisteredTool(entry) {
|
|
|
297
393
|
? {
|
|
298
394
|
...argTemplate,
|
|
299
395
|
...(cfg.args !== undefined ? { args: cfg.args } : {}),
|
|
300
|
-
defaults: {
|
|
396
|
+
defaults: {
|
|
397
|
+
...(argTemplate.defaults ?? {}),
|
|
398
|
+
...(cfg.defaults ?? {}),
|
|
399
|
+
},
|
|
301
400
|
}
|
|
302
401
|
: { args: cfg.args, defaults: cfg.defaults ?? {}, template: argTemplate };
|
|
303
402
|
const explicitArgTypes = Object.fromEntries((cfg.args ?? []).map((arg) => {
|
|
@@ -312,13 +411,19 @@ export function toRegisteredTool(entry) {
|
|
|
312
411
|
template,
|
|
313
412
|
recipe: cfg,
|
|
314
413
|
args: Schema.getToolArgNames(argTemplateConfig),
|
|
315
|
-
defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
414
|
+
defaults: Object.fromEntries(Object.entries(cfg.defaults ?? {}).map(([key, value]) => [
|
|
415
|
+
key,
|
|
416
|
+
String(value),
|
|
417
|
+
])),
|
|
316
418
|
...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
|
|
317
419
|
...(entry.mutableUsage ? { sourcePath: entry.path } : {}),
|
|
318
420
|
...(cfg.args ? { storedArgs: cfg.args } : {}),
|
|
319
421
|
...(cfg.defaults
|
|
320
422
|
? {
|
|
321
|
-
storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [
|
|
423
|
+
storedDefaults: Object.fromEntries(Object.entries(cfg.defaults).map(([key, value]) => [
|
|
424
|
+
key,
|
|
425
|
+
String(value),
|
|
426
|
+
])),
|
|
322
427
|
}
|
|
323
428
|
: {}),
|
|
324
429
|
};
|
|
@@ -57,6 +57,7 @@ export interface ReadResolvedRecipeConfigOptions {
|
|
|
57
57
|
}
|
|
58
58
|
export declare function resolveRecipePath(value: string, recipeRoot?: string): string;
|
|
59
59
|
export declare function getRecipePath(value: unknown, recipeRoot?: string): string | undefined;
|
|
60
|
+
export declare function diagnoseRawRecipeConfigFailure(path: string): string | undefined;
|
|
60
61
|
export declare function readRawRecipeConfig(path: string): Record<string, unknown> | undefined;
|
|
61
62
|
export declare function getRecipeIdFromPath(file: string): string;
|
|
62
63
|
export declare function readResolvedRecipeConfig(file: string, stack?: string[], options?: ReadResolvedRecipeConfigOptions): TemplateRecipeConfig | undefined;
|
|
@@ -29,7 +29,10 @@ export function resolveRecipePath(value, recipeRoot = Paths.getRecipeRoot()) {
|
|
|
29
29
|
}
|
|
30
30
|
function isBareRecipeName(value) {
|
|
31
31
|
const trimmed = value.trim();
|
|
32
|
-
return Boolean(trimmed) &&
|
|
32
|
+
return (Boolean(trimmed) &&
|
|
33
|
+
!trimmed.includes("/") &&
|
|
34
|
+
!trimmed.startsWith("~") &&
|
|
35
|
+
!trimmed.includes("{"));
|
|
33
36
|
}
|
|
34
37
|
function recipeNameFiles(value) {
|
|
35
38
|
const trimmed = value.trim();
|
|
@@ -260,6 +263,42 @@ function parseMarkdownRecipeConfig(content) {
|
|
|
260
263
|
}
|
|
261
264
|
return { ...frontmatter, template: text };
|
|
262
265
|
}
|
|
266
|
+
export function diagnoseRawRecipeConfigFailure(path) {
|
|
267
|
+
if (!existsSync(path))
|
|
268
|
+
return "file does not exist";
|
|
269
|
+
const size = statSync(path).size;
|
|
270
|
+
if (size > MAX_RECIPE_FILE_BYTES) {
|
|
271
|
+
return `file exceeds size limit ${MAX_RECIPE_FILE_BYTES} bytes`;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const content = readFileSync(path, "utf8");
|
|
275
|
+
if (path.endsWith(".md")) {
|
|
276
|
+
const lines = content.split(/\r?\n/);
|
|
277
|
+
if (lines[0]?.trim() !== "---") {
|
|
278
|
+
return "Markdown recipe must start with frontmatter";
|
|
279
|
+
}
|
|
280
|
+
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
281
|
+
if (end === -1)
|
|
282
|
+
return "Markdown recipe frontmatter is not closed";
|
|
283
|
+
const parsed = parseMarkdownRecipeConfig(content);
|
|
284
|
+
if (!parsed)
|
|
285
|
+
return "Markdown recipe has no executable recipe/template fence";
|
|
286
|
+
if (!Object.hasOwn(parsed, "template"))
|
|
287
|
+
return "recipe must define template";
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
const raw = JSON.parse(content);
|
|
291
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
292
|
+
return "JSON recipe must be an object";
|
|
293
|
+
}
|
|
294
|
+
if (!Object.hasOwn(raw, "template"))
|
|
295
|
+
return "recipe must define template";
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
return error instanceof Error ? error.message : String(error);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
263
302
|
export function readRawRecipeConfig(path) {
|
|
264
303
|
if (!existsSync(path))
|
|
265
304
|
return undefined;
|
package/dist/lib/registry.js
CHANGED
|
@@ -82,7 +82,11 @@ function getInputTemplate(value) {
|
|
|
82
82
|
throw new Error(Output.formatToolText("Tool template sequence is empty."));
|
|
83
83
|
return value;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
if (value && typeof value === "object") {
|
|
86
|
+
CommandTemplates.expandCommandTemplateConfigs(value);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(Output.formatToolText("Tool template must be a string, object, or sequence."));
|
|
86
90
|
}
|
|
87
91
|
function buildConfig(name, input, existing) {
|
|
88
92
|
const explicitArgs = input.args === undefined
|