@piewf/cli 5.1.1 → 5.3.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/dist/src/bundles.js +44 -11
- package/dist/src/cli.js +52 -25
- package/dist/src/doctor-cleanup.js +74 -55
- package/dist/src/doctor.js +39 -15
- package/dist/src/session-inspector.js +32 -17
- package/package.json +3 -2
package/dist/src/bundles.js
CHANGED
|
@@ -2,13 +2,50 @@ import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, re
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
function isJsonObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function readJsonObject(path) {
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
11
|
+
return isJsonObject(value) ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function readPackageMetadata(path) {
|
|
18
|
+
const value = readJsonObject(path);
|
|
19
|
+
if (!value)
|
|
20
|
+
return undefined;
|
|
21
|
+
const metadata = {};
|
|
22
|
+
if (typeof value.name === "string")
|
|
23
|
+
metadata.name = value.name;
|
|
24
|
+
if (typeof value.version === "string")
|
|
25
|
+
metadata.version = value.version;
|
|
26
|
+
if (typeof value.bin === "string")
|
|
27
|
+
metadata.bin = value.bin;
|
|
28
|
+
else if (isJsonObject(value.bin)) {
|
|
29
|
+
const bin = {};
|
|
30
|
+
let valid = true;
|
|
31
|
+
for (const [name, entry] of Object.entries(value.bin)) {
|
|
32
|
+
if (typeof entry !== "string") {
|
|
33
|
+
valid = false;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
bin[name] = entry;
|
|
37
|
+
}
|
|
38
|
+
if (valid)
|
|
39
|
+
metadata.bin = bin;
|
|
40
|
+
}
|
|
41
|
+
return metadata;
|
|
42
|
+
}
|
|
5
43
|
function packageJson() {
|
|
6
44
|
const directory = dirname(fileURLToPath(import.meta.url));
|
|
7
45
|
for (const path of [join(directory, "../package.json"), join(directory, "../../package.json")]) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
catch { /* Try the source and built layouts. */ }
|
|
46
|
+
const metadata = readPackageMetadata(path);
|
|
47
|
+
if (metadata)
|
|
48
|
+
return metadata;
|
|
12
49
|
}
|
|
13
50
|
return {};
|
|
14
51
|
}
|
|
@@ -300,7 +337,6 @@ function workflowModule(workflow, functionSource, withRoles, aliasTargets, exten
|
|
|
300
337
|
" registerWorkflowExtension({",
|
|
301
338
|
` version: ${JSON.stringify("1.0.0")},`,
|
|
302
339
|
` headline: ${JSON.stringify("Portable workflow bundle")},`,
|
|
303
|
-
` description: ${JSON.stringify(workflow.description)},`,
|
|
304
340
|
...(withRoles ? [` roleDirectories: [new URL("./roles", import.meta.url)],`] : []),
|
|
305
341
|
...(aliases.length ? [` modelAliases: { ${aliases.map(([name, target]) => `${JSON.stringify(name)}: { resolve: () => ${JSON.stringify(target)} }`).join(", ")} },`] : []),
|
|
306
342
|
" functions: {",
|
|
@@ -354,12 +390,9 @@ function copyResources(root, resources) {
|
|
|
354
390
|
throw new Error(`Bundle resource may contain credentials and cannot be selected: ${source}`);
|
|
355
391
|
let name = basename(source);
|
|
356
392
|
if (kind === "dependencies") {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
name = packageName;
|
|
361
|
-
}
|
|
362
|
-
catch { /* Files can be dependency entry points. */ }
|
|
393
|
+
const packageName = readPackageMetadata(join(source, "package.json"))?.name;
|
|
394
|
+
if (typeof packageName === "string" && packageName.trim())
|
|
395
|
+
name = packageName;
|
|
363
396
|
}
|
|
364
397
|
if (!name || name === "." || name === ".." || name.includes("\\") || name.startsWith("/"))
|
|
365
398
|
throw new Error(`Invalid bundle resource name: ${name}`);
|
package/dist/src/cli.js
CHANGED
|
@@ -8,17 +8,27 @@ import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionF
|
|
|
8
8
|
import { Value } from "typebox/value";
|
|
9
9
|
import { doctor, doctorExitCode, formatDoctorReport } from "./doctor.js";
|
|
10
10
|
import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
|
|
11
|
-
import workflowExtension, { formatWorkflowProgress, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
|
|
11
|
+
import workflowExtension, { formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
|
|
12
12
|
import { portableEngineVersion, portablePiVersion, writePortableWorkflowBundle } from "./bundles.js";
|
|
13
13
|
import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
|
|
14
|
+
import { isPersistedRun } from "pi-extensible-workflows/persistence";
|
|
14
15
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
15
16
|
function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
16
|
-
function
|
|
17
|
+
function requiredArg(args, index) {
|
|
18
|
+
const value = args[index];
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
throw new Error("Missing argument");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function typedOptionKey(options, key) { return Object.prototype.hasOwnProperty.call(options, key); }
|
|
24
|
+
function clone(value) { const cloned = structuredClone(value); if (!jsonValue(cloned))
|
|
25
|
+
throw new Error("Invalid JSON passed to --input"); return cloned; }
|
|
17
26
|
function kebabCase(value) { return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase(); }
|
|
27
|
+
function isCliScalar(value) { return value === "string" || value === "integer" || value === "number" || value === "boolean"; }
|
|
18
28
|
function scalarType(schema) {
|
|
19
29
|
if (!object(schema) || typeof schema.type !== "string")
|
|
20
30
|
return undefined;
|
|
21
|
-
return
|
|
31
|
+
return isCliScalar(schema.type) ? schema.type : undefined;
|
|
22
32
|
}
|
|
23
33
|
function schemaPlan(schema) {
|
|
24
34
|
if (!object(schema) || schema.type !== "object")
|
|
@@ -40,8 +50,14 @@ function schemaPlan(schema) {
|
|
|
40
50
|
return { fields, ...(requiredScalars.length === 1 ? { positional: requiredScalars[0] } : {}) };
|
|
41
51
|
}
|
|
42
52
|
function scalarLabel(type) { return type === "integer" ? "integer" : type; }
|
|
43
|
-
function scalarFieldType(field) {
|
|
44
|
-
|
|
53
|
+
function scalarFieldType(field) {
|
|
54
|
+
if (field.type !== "array")
|
|
55
|
+
return field.type;
|
|
56
|
+
if (!isCliScalar(field.itemType))
|
|
57
|
+
throw new Error("Invalid array field");
|
|
58
|
+
return field.itemType;
|
|
59
|
+
}
|
|
60
|
+
function fieldLabel(field) { return field.type === "array" ? `${field.option} <${scalarLabel(scalarFieldType(field))}>` : `${field.option}${field.type === "boolean" ? "" : ` <${scalarLabel(field.type)}>`}`; }
|
|
45
61
|
function fieldDescription(field) {
|
|
46
62
|
const description = typeof field.schema.description === "string" ? field.schema.description.trim() : "";
|
|
47
63
|
const required = field.required ? "required" : "optional";
|
|
@@ -109,15 +125,19 @@ export function parseWorkflowCliArgs(schema, rawArgs) {
|
|
|
109
125
|
let endOptions = false;
|
|
110
126
|
const assign = (field, raw) => {
|
|
111
127
|
if (field.type === "array") {
|
|
112
|
-
const
|
|
113
|
-
values.
|
|
128
|
+
const current = result[field.name];
|
|
129
|
+
const values = Array.isArray(current) ? current : [];
|
|
130
|
+
const itemSchema = field.schema.items;
|
|
131
|
+
if (!object(itemSchema))
|
|
132
|
+
throw new Error("Invalid array field");
|
|
133
|
+
values.push(coerce(raw, scalarFieldType(field), itemSchema));
|
|
114
134
|
result[field.name] = values;
|
|
115
135
|
}
|
|
116
136
|
else
|
|
117
137
|
result[field.name] = coerce(raw, field.type, field.schema);
|
|
118
138
|
};
|
|
119
139
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
120
|
-
const token = rawArgs
|
|
140
|
+
const token = requiredArg(rawArgs, index);
|
|
121
141
|
if (token === "--") {
|
|
122
142
|
endOptions = true;
|
|
123
143
|
continue;
|
|
@@ -147,7 +167,7 @@ export function parseWorkflowCliArgs(schema, rawArgs) {
|
|
|
147
167
|
if (equals >= 0)
|
|
148
168
|
assign(field, token.slice(equals + 1));
|
|
149
169
|
else if (rawArgs[index + 1] === "true" || rawArgs[index + 1] === "false")
|
|
150
|
-
assign(field, rawArgs
|
|
170
|
+
assign(field, requiredArg(rawArgs, ++index));
|
|
151
171
|
else
|
|
152
172
|
result[field.name] = true;
|
|
153
173
|
}
|
|
@@ -222,7 +242,7 @@ export function parseDoctorArgs(rawArgs) {
|
|
|
222
242
|
let role;
|
|
223
243
|
let prompt;
|
|
224
244
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
225
|
-
const token = rawArgs
|
|
245
|
+
const token = requiredArg(rawArgs, index);
|
|
226
246
|
const equals = token.indexOf("=");
|
|
227
247
|
const option = equals >= 0 ? token.slice(0, equals) : token;
|
|
228
248
|
if (option === "--role" || option === "--prompt") {
|
|
@@ -258,7 +278,7 @@ export function parseDoctorCleanupArgs(rawArgs) {
|
|
|
258
278
|
let yes = false;
|
|
259
279
|
let seenDays = false;
|
|
260
280
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
261
|
-
const token = rawArgs
|
|
281
|
+
const token = requiredArg(rawArgs, index);
|
|
262
282
|
if (token === "--yes") {
|
|
263
283
|
yes = true;
|
|
264
284
|
continue;
|
|
@@ -302,6 +322,8 @@ function stripTrustOptions(rawArgs) {
|
|
|
302
322
|
}
|
|
303
323
|
return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
|
|
304
324
|
}
|
|
325
|
+
function isHeadlessWorkflowResult(value) { return object(value) && Array.isArray(value.content) && value.content.every((entry) => object(entry) && typeof entry.type === "string" && typeof entry.text === "string"); }
|
|
326
|
+
function isHeadlessWorkflowTool(value) { return object(value) && value.name === "workflow" && typeof value.execute === "function"; }
|
|
305
327
|
async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
306
328
|
const cwd = options.cwd ?? process.cwd();
|
|
307
329
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
@@ -324,6 +346,8 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
324
346
|
for (const handler of extension.handlers.get("project_trust") ?? []) {
|
|
325
347
|
try {
|
|
326
348
|
const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
|
|
349
|
+
if (!object(result))
|
|
350
|
+
continue;
|
|
327
351
|
if (result.trusted === "undecided")
|
|
328
352
|
continue;
|
|
329
353
|
if (result.trusted !== "yes" && result.trusted !== "no")
|
|
@@ -363,7 +387,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
363
387
|
events: { emit() { } },
|
|
364
388
|
};
|
|
365
389
|
workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
|
|
366
|
-
const workflowTool = tools.find(
|
|
390
|
+
const workflowTool = tools.find(isHeadlessWorkflowTool);
|
|
367
391
|
if (!workflowTool)
|
|
368
392
|
throw new Error("The workflow runtime could not be initialized");
|
|
369
393
|
return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
|
|
@@ -409,7 +433,7 @@ else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}
|
|
|
409
433
|
linkSync(tempPath, destination);
|
|
410
434
|
}
|
|
411
435
|
catch (error) {
|
|
412
|
-
if (error
|
|
436
|
+
if (isNodeError(error, "EEXIST"))
|
|
413
437
|
throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
|
|
414
438
|
throw error;
|
|
415
439
|
}
|
|
@@ -502,13 +526,15 @@ async function invokeWorkflow(fn, args, runtime, options, context) {
|
|
|
502
526
|
return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
|
|
503
527
|
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
|
|
504
528
|
try {
|
|
505
|
-
const result = await runtime.workflowTool.execute(randomUUID(), { name: fn.name, script: `return await ${fn.name}(args);`, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) &&
|
|
529
|
+
const result = await runtime.workflowTool.execute(randomUUID(), { name: fn.name, script: `return await ${fn.name}(args);`, args, foreground: true }, options.signal, (update) => { if (object(update) && object(update.details) && isPersistedRun(update.details.run))
|
|
506
530
|
progress.update(update.details.run); }, context);
|
|
531
|
+
if (!isHeadlessWorkflowResult(result))
|
|
532
|
+
throw new Error("Workflow returned an invalid result");
|
|
507
533
|
const details = object(result.details) ? result.details : {};
|
|
508
534
|
const runId = typeof details.runId === "string" ? details.runId : undefined;
|
|
509
535
|
if (runId)
|
|
510
536
|
announceRunId(runId);
|
|
511
|
-
if (has(details, "value"))
|
|
537
|
+
if (has(details, "value") && jsonValue(details.value))
|
|
512
538
|
return { value: details.value, ...(runId ? { runId } : {}) };
|
|
513
539
|
const first = result.content[0];
|
|
514
540
|
if (!first || first.type !== "text")
|
|
@@ -557,7 +583,7 @@ async function runWorkflowCli(rawArgs, options) {
|
|
|
557
583
|
options.write(workflowUsage());
|
|
558
584
|
return args.length ? 0 : 1;
|
|
559
585
|
}
|
|
560
|
-
const name = args
|
|
586
|
+
const name = requiredArg(args, 0);
|
|
561
587
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime, context) => {
|
|
562
588
|
const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
|
|
563
589
|
const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
|
|
@@ -582,13 +608,13 @@ async function exportWorkflowCli(rawArgs, options) {
|
|
|
582
608
|
options.write(exportUsage());
|
|
583
609
|
return args.length ? 0 : 1;
|
|
584
610
|
}
|
|
585
|
-
const workflowName = args
|
|
611
|
+
const workflowName = requiredArg(args, 0);
|
|
586
612
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
587
613
|
let name;
|
|
588
614
|
let output;
|
|
589
615
|
let force = false;
|
|
590
616
|
for (let index = 1; index < args.length; index += 1) {
|
|
591
|
-
const arg = args
|
|
617
|
+
const arg = requiredArg(args, index);
|
|
592
618
|
if (arg === "--force") {
|
|
593
619
|
force = true;
|
|
594
620
|
continue;
|
|
@@ -640,7 +666,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
640
666
|
options.write(bundleUsage());
|
|
641
667
|
return args.length ? 0 : 1;
|
|
642
668
|
}
|
|
643
|
-
const workflowName = args
|
|
669
|
+
const workflowName = requiredArg(args, 0);
|
|
644
670
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
645
671
|
let name;
|
|
646
672
|
let output;
|
|
@@ -648,7 +674,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
648
674
|
const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
|
|
649
675
|
const resources = { extensions: [], skills: [], static: [], dependencies: [] };
|
|
650
676
|
for (let index = 1; index < args.length; index += 1) {
|
|
651
|
-
const arg = args
|
|
677
|
+
const arg = requiredArg(args, index);
|
|
652
678
|
if (arg === "--force") {
|
|
653
679
|
force = true;
|
|
654
680
|
continue;
|
|
@@ -657,7 +683,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
657
683
|
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
658
684
|
const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
|
|
659
685
|
const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
|
|
660
|
-
if (option === "--name" || option === "--output" ||
|
|
686
|
+
if (option === "--name" || option === "--output" || typedOptionKey(requirementOptions, option) || typedOptionKey(resourceOptions, option)) {
|
|
661
687
|
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
662
688
|
if (!value)
|
|
663
689
|
throw new Error(`Missing value for ${option}`);
|
|
@@ -665,9 +691,9 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
665
691
|
name = value;
|
|
666
692
|
else if (option === "--output")
|
|
667
693
|
output = value;
|
|
668
|
-
else if (
|
|
694
|
+
else if (typedOptionKey(requirementOptions, option))
|
|
669
695
|
requirements[requirementOptions[option]].push(value);
|
|
670
|
-
else
|
|
696
|
+
else if (typedOptionKey(resourceOptions, option))
|
|
671
697
|
resources[resourceOptions[option]].push(value);
|
|
672
698
|
continue;
|
|
673
699
|
}
|
|
@@ -758,10 +784,11 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
758
784
|
}
|
|
759
785
|
if (args[0] === "transcript" && args.length === 2) {
|
|
760
786
|
try {
|
|
787
|
+
const transcript = requiredArg(args, 1);
|
|
761
788
|
if (options.transcript)
|
|
762
|
-
await options.transcript(
|
|
789
|
+
await options.transcript(transcript);
|
|
763
790
|
else
|
|
764
|
-
write(`${transcriptFileLines(
|
|
791
|
+
write(`${transcriptFileLines(transcript).join("\n")}\n`);
|
|
765
792
|
return 0;
|
|
766
793
|
}
|
|
767
794
|
catch (error) {
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { validateBudget } from "pi-extensible-workflows";
|
|
6
6
|
import { RUN_STATES } from "pi-extensible-workflows";
|
|
7
|
-
import { jsonValue, validateModelAliases } from "pi-extensible-workflows";
|
|
7
|
+
import { isNodeError, jsonValue, validateModelAliases } from "pi-extensible-workflows";
|
|
8
8
|
import { validateSchema } from "pi-extensible-workflows";
|
|
9
9
|
import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, RunStore } from "pi-extensible-workflows/persistence";
|
|
10
10
|
const TERMINAL_STATES = new Set(["completed", "failed", "stopped"]);
|
|
@@ -18,6 +18,13 @@ const SCHEDULER_STATES = new Set(["queued", "running", "waiting_for_child", "pau
|
|
|
18
18
|
const BUDGET_DIMENSIONS = new Set(["tokens", "costUsd", "durationMs", "agentLaunches"]);
|
|
19
19
|
const BUDGET_EVENT_TYPES = new Set(["soft_crossed", "hard_overrun", "hard_exhausted", "adjustment_requested", "adjustment_approved", "adjustment_rejected"]);
|
|
20
20
|
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
21
|
+
function isList(value) { return Array.isArray(value); }
|
|
22
|
+
function isThinking(value) { return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].some((candidate) => candidate === value); }
|
|
23
|
+
function isRunState(value) { return RUN_STATES.some((candidate) => candidate === value); }
|
|
24
|
+
function isAgentState(value) { return typeof value === "string" && AGENT_STATES.has(value); }
|
|
25
|
+
function isSchedulerState(value) { return typeof value === "string" && SCHEDULER_STATES.has(value); }
|
|
26
|
+
function isToolCallState(value) { return ["running", "completed", "failed"].some((candidate) => candidate === value); }
|
|
27
|
+
function isActivityKind(value) { return ["reasoning", "tool", "text"].some((candidate) => candidate === value); }
|
|
21
28
|
function textError(error) { return error instanceof Error ? error.message : String(error); }
|
|
22
29
|
function positiveDays(value) { if (!Number.isSafeInteger(value) || value < 1 || !Number.isFinite(value * DAY_MS))
|
|
23
30
|
throw new Error("older-than-days must be a positive integer"); return value; }
|
|
@@ -26,17 +33,17 @@ function sameNames(left, right) { return left.length === right.length && left.ev
|
|
|
26
33
|
async function jsonFile(path) { return JSON.parse(await readFile(path, "utf8")); }
|
|
27
34
|
async function requiredFile(path) { const info = await lstat(path); if (!info.isFile())
|
|
28
35
|
throw new Error(`Required artifact is not a regular file: ${path}`); }
|
|
29
|
-
function stringList(value, label, nonEmpty = false) { if (!
|
|
36
|
+
function stringList(value, label, nonEmpty = false) { if (!isList(value) || value.some((item) => typeof item !== "string" || (nonEmpty && !item)))
|
|
30
37
|
throw new Error(`${label} is invalid`); }
|
|
31
38
|
function optionalString(value, label) { if (value !== undefined && typeof value !== "string")
|
|
32
39
|
throw new Error(`${label} is invalid`); }
|
|
33
|
-
function nonNegativeInteger(value, label) { if (!Number.isSafeInteger(value) || value < 0)
|
|
40
|
+
function nonNegativeInteger(value, label) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
34
41
|
throw new Error(`${label} is invalid`); }
|
|
35
|
-
function positiveInteger(value, label) { if (!Number.isSafeInteger(value) || value < 1)
|
|
42
|
+
function positiveInteger(value, label) { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
36
43
|
throw new Error(`${label} is invalid`); }
|
|
37
44
|
function finiteNumber(value, label) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
38
45
|
throw new Error(`${label} is invalid`); }
|
|
39
|
-
function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !
|
|
46
|
+
function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !isThinking(value.thinking)))
|
|
40
47
|
throw new Error(`${label} is invalid`); }
|
|
41
48
|
function accounting(value, label) { if (!object(value))
|
|
42
49
|
throw new Error(`${label} is invalid`); for (const key of ["input", "output", "cacheRead", "cacheWrite", "cost"])
|
|
@@ -47,7 +54,7 @@ function resourceExclusions(value, label) { if (value === undefined)
|
|
|
47
54
|
stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
|
|
48
55
|
stringList(value.extensions, `${label}.extensions`); }
|
|
49
56
|
function agentDefinition(value, label) { if (!object(value))
|
|
50
|
-
throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !
|
|
57
|
+
throw new Error(`${label} is invalid`); optionalString(value.prompt, `${label}.prompt`); optionalString(value.description, `${label}.description`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
|
|
51
58
|
throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined)
|
|
52
59
|
stringList(value.tools, `${label}.tools`); resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`); }
|
|
53
60
|
function optionalRole(value, label) {
|
|
@@ -63,7 +70,7 @@ function optionalRole(value, label) {
|
|
|
63
70
|
for (const key of ["model", "description"])
|
|
64
71
|
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string")
|
|
65
72
|
throw new Error(`${label}.${key} is invalid`);
|
|
66
|
-
if (value.thinking !== undefined && value.thinking !== null && !
|
|
73
|
+
if (value.thinking !== undefined && value.thinking !== null && !isThinking(value.thinking))
|
|
67
74
|
throw new Error(`${label}.thinking is invalid`);
|
|
68
75
|
if (value.tools !== undefined && value.tools !== null)
|
|
69
76
|
stringList(value.tools, `${label}.tools`);
|
|
@@ -75,22 +82,20 @@ function optionalRole(value, label) {
|
|
|
75
82
|
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
76
83
|
}
|
|
77
84
|
function validateScheduledOptions(value, label) { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd)
|
|
78
|
-
throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !
|
|
85
|
+
throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinking(value.thinking))
|
|
79
86
|
throw new Error(`${label}.thinking is invalid`); optionalRole(value.role, `${label}.role`); if (value.schema !== undefined)
|
|
80
87
|
validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined)
|
|
81
88
|
nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null)
|
|
82
89
|
positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions)))
|
|
83
90
|
throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) {
|
|
84
|
-
if (!object(value.agentIdentity) || !
|
|
91
|
+
if (!object(value.agentIdentity) || !isList(value.agentIdentity.structuralPath) || value.agentIdentity.structuralPath.some((part) => typeof part !== "string") || typeof value.agentIdentity.callSite !== "string")
|
|
85
92
|
throw new Error(`${label}.agentIdentity is invalid`);
|
|
86
93
|
positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`);
|
|
87
94
|
optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`);
|
|
88
95
|
optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`);
|
|
89
96
|
} }
|
|
90
|
-
function validateSessionReference(value, label) {
|
|
91
|
-
|
|
92
|
-
throw new Error(`${label} is invalid`);
|
|
93
|
-
}
|
|
97
|
+
function validateSessionReference(value, label) { if (!object(value) || typeof value.transport !== "string" || !value.transport || typeof value.sessionId !== "string" || !value.sessionId || (value.locator !== undefined && !jsonValue(value.locator)))
|
|
98
|
+
throw new Error(`${label} is invalid`); }
|
|
94
99
|
function validateAgentSetup(value, label) {
|
|
95
100
|
if (!object(value))
|
|
96
101
|
throw new Error(`${label} is invalid`);
|
|
@@ -102,7 +107,7 @@ function validateAgentSetup(value, label) {
|
|
|
102
107
|
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
103
108
|
}
|
|
104
109
|
function validateAgent(value, label) {
|
|
105
|
-
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path ||
|
|
110
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.name !== "string" || !value.name || typeof value.path !== "string" || !value.path || !isAgentState(value.state))
|
|
106
111
|
throw new Error(`${label} is invalid`);
|
|
107
112
|
optionalString(value.systemPrompt, `${label}.systemPrompt`);
|
|
108
113
|
optionalString(value.prompt, `${label}.prompt`);
|
|
@@ -118,7 +123,7 @@ function validateAgent(value, label) {
|
|
|
118
123
|
stringList(value.tools, `${label}.tools`);
|
|
119
124
|
nonNegativeInteger(value.attempts, `${label}.attempts`);
|
|
120
125
|
if (value.attemptDetails !== undefined) {
|
|
121
|
-
if (!
|
|
126
|
+
if (!isList(value.attemptDetails))
|
|
122
127
|
throw new Error(`${label}.attemptDetails is invalid`);
|
|
123
128
|
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
124
129
|
const at = `${label}.attemptDetails[${String(index)}]`;
|
|
@@ -126,8 +131,9 @@ function validateAgent(value, label) {
|
|
|
126
131
|
throw new Error(`${at} is invalid`);
|
|
127
132
|
validateAgentSetup(attempt.setup, `${at}.setup`);
|
|
128
133
|
if (attempt.session !== undefined) {
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
const session = attempt.session;
|
|
135
|
+
validateSessionReference(session, `${at}.session`);
|
|
136
|
+
if (session.transport !== attempt.transport)
|
|
131
137
|
throw new Error(`${at}.session transport does not match attempt transport`);
|
|
132
138
|
}
|
|
133
139
|
accounting(attempt.accounting, `${at}.accounting`);
|
|
@@ -138,14 +144,14 @@ function validateAgent(value, label) {
|
|
|
138
144
|
if (value.accounting !== undefined)
|
|
139
145
|
accounting(value.accounting, `${label}.accounting`);
|
|
140
146
|
if (value.toolCalls !== undefined) {
|
|
141
|
-
if (!
|
|
147
|
+
if (!isList(value.toolCalls))
|
|
142
148
|
throw new Error(`${label}.toolCalls is invalid`);
|
|
143
149
|
for (const [index, call] of value.toolCalls.entries())
|
|
144
|
-
if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !
|
|
150
|
+
if (!object(call) || typeof call.id !== "string" || !call.id || typeof call.name !== "string" || !call.name || !isToolCallState(call.state))
|
|
145
151
|
throw new Error(`${label}.toolCalls[${String(index)}] is invalid`);
|
|
146
152
|
}
|
|
147
153
|
if (value.activity !== undefined) {
|
|
148
|
-
if (!object(value.activity) || !
|
|
154
|
+
if (!object(value.activity) || !isActivityKind(value.activity.kind) || typeof value.activity.text !== "string")
|
|
149
155
|
throw new Error(`${label}.activity is invalid`);
|
|
150
156
|
}
|
|
151
157
|
if (value.lastEventAt !== undefined)
|
|
@@ -155,20 +161,18 @@ function validateUsage(value, label) { if (!object(value))
|
|
|
155
161
|
throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"])
|
|
156
162
|
finiteNumber(value[key], `${label}.${key}`); }
|
|
157
163
|
function validateBudgetEvents(value) { if (value === undefined)
|
|
158
|
-
return; if (!
|
|
164
|
+
return; if (!isList(value))
|
|
159
165
|
throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) {
|
|
160
166
|
const label = `budgetEvents[${String(index)}]`;
|
|
161
|
-
if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !
|
|
167
|
+
if (!object(event) || typeof event.type !== "string" || !BUDGET_EVENT_TYPES.has(event.type) || !Number.isSafeInteger(event.budgetVersion) || Number(event.budgetVersion) < 1 || !isList(event.dimensions) || event.dimensions.some((dimension) => typeof dimension !== "string" || !BUDGET_DIMENSIONS.has(dimension)) || typeof event.at !== "number" || !Number.isFinite(event.at) || event.limits === undefined)
|
|
162
168
|
throw new Error(`${label} is invalid`);
|
|
163
169
|
validateUsage(event.usage, `${label}.usage`);
|
|
164
170
|
validateBudget(event.limits);
|
|
165
171
|
} }
|
|
166
|
-
function validateRunRecord(
|
|
167
|
-
|
|
168
|
-
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !RUN_STATES.includes(value.state) || !Array.isArray(value.agents) || !Array.isArray(value.agentSessions) || Object.hasOwn(value, "nativeSessions"))
|
|
172
|
+
function validateRunRecord(value) {
|
|
173
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || typeof value.workflowName !== "string" || !value.workflowName || typeof value.cwd !== "string" || !value.cwd || typeof value.sessionId !== "string" || !value.sessionId || !isRunState(value.state) || !isList(value.agents) || !isList(value.agentSessions) || Object.hasOwn(value, "nativeSessions"))
|
|
169
174
|
throw new Error("Persisted run state is invalid");
|
|
170
|
-
const agents = value.agents;
|
|
171
|
-
agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
|
|
175
|
+
const agents = value.agents.map((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); return agent; });
|
|
172
176
|
const agentIds = new Set();
|
|
173
177
|
for (const agent of agents) {
|
|
174
178
|
const id = agent.id;
|
|
@@ -178,36 +182,36 @@ function validateRunRecord(run) {
|
|
|
178
182
|
}
|
|
179
183
|
for (const agent of agents) {
|
|
180
184
|
const parentId = agent.parentId;
|
|
181
|
-
if (parentId !== undefined &&
|
|
185
|
+
if (parentId !== undefined && !agentIds.has(parentId))
|
|
182
186
|
throw new Error("Persisted agent has a missing parent");
|
|
183
187
|
const seen = new Set();
|
|
184
|
-
let parent =
|
|
188
|
+
let parent = parentId;
|
|
185
189
|
while (parent) {
|
|
186
190
|
if (seen.has(parent))
|
|
187
191
|
throw new Error("Persisted agent parent cycle");
|
|
188
192
|
seen.add(parent);
|
|
189
193
|
const parentAgent = agents.find((candidate) => candidate.id === parent);
|
|
190
|
-
parent =
|
|
194
|
+
parent = parentAgent?.parentId;
|
|
191
195
|
}
|
|
192
196
|
}
|
|
193
|
-
const
|
|
194
|
-
for (const [index, session] of sessions.entries())
|
|
197
|
+
for (const [index, session] of value.agentSessions.entries())
|
|
195
198
|
validateSessionReference(session, `agentSessions[${String(index)}]`);
|
|
196
199
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
197
200
|
optionalString(value.failedAt, "Persisted failed path");
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
const retry = value.retry;
|
|
202
|
+
if (retry !== undefined) {
|
|
203
|
+
if (!object(retry) || typeof retry.sourceRunId !== "string" || !retry.sourceRunId || typeof retry.lineageRootRunId !== "string" || !retry.lineageRootRunId)
|
|
200
204
|
throw new Error("Persisted retry provenance is invalid");
|
|
201
|
-
const sourceRunId =
|
|
202
|
-
stringList(
|
|
203
|
-
stringList(
|
|
204
|
-
stringList(
|
|
205
|
+
const sourceRunId = retry.sourceRunId;
|
|
206
|
+
stringList(retry.completedPaths, "Persisted retry completed paths");
|
|
207
|
+
stringList(retry.incompletePaths, "Persisted retry incomplete paths");
|
|
208
|
+
stringList(retry.namedWorktrees, "Persisted retry named worktrees");
|
|
205
209
|
if (value.parentRunId !== sourceRunId)
|
|
206
210
|
throw new Error("Persisted retry parent does not match its source");
|
|
207
211
|
}
|
|
208
212
|
optionalString(value.phase, "Persisted phase");
|
|
209
213
|
if (value.phaseHistory !== undefined) {
|
|
210
|
-
if (!
|
|
214
|
+
if (!isList(value.phaseHistory))
|
|
211
215
|
throw new Error("Persisted phase history is invalid");
|
|
212
216
|
for (const phase of value.phaseHistory) {
|
|
213
217
|
if (!object(phase) || typeof phase.phase !== "string" || !phase.phase)
|
|
@@ -215,6 +219,18 @@ function validateRunRecord(run) {
|
|
|
215
219
|
nonNegativeInteger(phase.afterAgent, "Persisted phase history afterAgent");
|
|
216
220
|
}
|
|
217
221
|
}
|
|
222
|
+
if (value.phaseHistoryIndex !== undefined)
|
|
223
|
+
nonNegativeInteger(value.phaseHistoryIndex, "Persisted phase history index");
|
|
224
|
+
if (value.activeShellsByPhase !== undefined) {
|
|
225
|
+
if (!isList(value.activeShellsByPhase))
|
|
226
|
+
throw new Error("Persisted phase shell activity is invalid");
|
|
227
|
+
for (const activity of value.activeShellsByPhase) {
|
|
228
|
+
if (!object(activity) || !Number.isSafeInteger(activity.phaseIndex) || Number(activity.phaseIndex) < -1)
|
|
229
|
+
throw new Error("Persisted phase shell activity is invalid");
|
|
230
|
+
positiveInteger(activity.active, "Persisted phase shell activity count");
|
|
231
|
+
finiteNumber(activity.startedAt, "Persisted phase shell activity start");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
218
234
|
if (value.error !== undefined && (!object(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string"))
|
|
219
235
|
throw new Error("Persisted run error is invalid");
|
|
220
236
|
validateBudget(value.budget);
|
|
@@ -224,7 +240,7 @@ function validateRunRecord(run) {
|
|
|
224
240
|
validateUsage(value.usage, "Persisted usage");
|
|
225
241
|
validateBudgetEvents(value.budgetEvents);
|
|
226
242
|
if (value.events !== undefined) {
|
|
227
|
-
if (!
|
|
243
|
+
if (!isList(value.events))
|
|
228
244
|
throw new Error("Persisted run events are invalid");
|
|
229
245
|
for (const event of value.events)
|
|
230
246
|
if (!object(event) || typeof event.type !== "string" || typeof event.message !== "string")
|
|
@@ -232,7 +248,7 @@ function validateRunRecord(run) {
|
|
|
232
248
|
}
|
|
233
249
|
}
|
|
234
250
|
function validateSnapshot(snapshot) {
|
|
235
|
-
if (!object(snapshot) || typeof snapshot.script !== "string" || !snapshot.script || !jsonValue(snapshot.args) || !object(snapshot.metadata) || typeof snapshot.metadata.name !== "string" || !snapshot.metadata.name || (snapshot.metadata.description !== undefined && typeof snapshot.metadata.description !== "string") || !object(snapshot.settings) || !Number.isSafeInteger(snapshot.settings.concurrency) || Number(snapshot.settings.concurrency) < 1 || Number(snapshot.settings.concurrency) > 16 || !
|
|
251
|
+
if (!object(snapshot) || typeof snapshot.script !== "string" || !snapshot.script || !jsonValue(snapshot.args) || !object(snapshot.metadata) || typeof snapshot.metadata.name !== "string" || !snapshot.metadata.name || (snapshot.metadata.description !== undefined && typeof snapshot.metadata.description !== "string") || !object(snapshot.settings) || !Number.isSafeInteger(snapshot.settings.concurrency) || Number(snapshot.settings.concurrency) < 1 || Number(snapshot.settings.concurrency) > 16 || snapshot.settings.backgroundWidget !== undefined && typeof snapshot.settings.backgroundWidget !== "boolean" || !isList(snapshot.models) || snapshot.models.some((modelName) => typeof modelName !== "string") || !isList(snapshot.tools) || snapshot.tools.some((tool) => typeof tool !== "string") || !isList(snapshot.agentTypes) || snapshot.agentTypes.some((agentType) => typeof agentType !== "string") || !isList(snapshot.schemas))
|
|
236
252
|
throw new Error("Persisted launch snapshot is invalid");
|
|
237
253
|
if (snapshot.identityVersion !== undefined)
|
|
238
254
|
positiveInteger(snapshot.identityVersion, "Persisted snapshot identity version");
|
|
@@ -278,7 +294,7 @@ async function validateSystemPrompts(store) {
|
|
|
278
294
|
await store.systemPrompts();
|
|
279
295
|
return;
|
|
280
296
|
}
|
|
281
|
-
if (!object(value) || value.version !== 1 || !
|
|
297
|
+
if (!object(value) || value.version !== 1 || !isList(value.entries))
|
|
282
298
|
throw new Error("Persisted system prompts are invalid");
|
|
283
299
|
for (const [index, entry] of value.entries.entries()) {
|
|
284
300
|
const label = `system-prompts.entries[${String(index)}]`;
|
|
@@ -300,6 +316,13 @@ async function validateRunDirectory(store) {
|
|
|
300
316
|
throw new Error(`Run artifact is not a regular file: ${join(store.directory, entry.name)}`);
|
|
301
317
|
}
|
|
302
318
|
}
|
|
319
|
+
function validateOwnershipRecord(value, label, ownershipIds) {
|
|
320
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || ownershipIds.has(value.id) || typeof value.label !== "string" || !value.label || !isSchedulerState(value.state))
|
|
321
|
+
throw new Error(`${label} is invalid`);
|
|
322
|
+
optionalString(value.parentId, `${label}.parentId`);
|
|
323
|
+
optionalString(value.prompt, `${label}.prompt`);
|
|
324
|
+
validateScheduledOptions(value.options, `${label}.options`);
|
|
325
|
+
}
|
|
303
326
|
async function validateRunArtifacts(store, workflowScript, state) {
|
|
304
327
|
await validateRunDirectory(store);
|
|
305
328
|
for (const name of REQUIRED_RUN_FILES)
|
|
@@ -307,7 +330,7 @@ async function validateRunArtifacts(store, workflowScript, state) {
|
|
|
307
330
|
if (await readFile(join(store.directory, "workflow.js"), "utf8") !== workflowScript)
|
|
308
331
|
throw new Error("Persisted workflow source does not match its launch snapshot");
|
|
309
332
|
await validateSystemPrompts(store);
|
|
310
|
-
const result = await jsonFile(join(store.directory, "result.json")).catch((error) => { if (error
|
|
333
|
+
const result = await jsonFile(join(store.directory, "result.json")).catch((error) => { if (isNodeError(error, "ENOENT"))
|
|
311
334
|
return undefined; throw error; });
|
|
312
335
|
if (result === undefined && state === "completed")
|
|
313
336
|
throw new Error("Completed run result is missing");
|
|
@@ -315,21 +338,17 @@ async function validateRunArtifacts(store, workflowScript, state) {
|
|
|
315
338
|
throw new Error("Persisted workflow result is invalid");
|
|
316
339
|
validateJournal(await jsonFile(join(store.directory, "journal.json")));
|
|
317
340
|
const rawOwnership = await jsonFile(join(store.directory, "ownership.json"));
|
|
318
|
-
if (!
|
|
341
|
+
if (!isList(rawOwnership))
|
|
319
342
|
throw new Error("Persisted ownership records are invalid");
|
|
320
|
-
const ownership = rawOwnership;
|
|
321
343
|
const ownershipIds = new Set();
|
|
322
|
-
|
|
344
|
+
const ownership = rawOwnership.map((record, index) => {
|
|
323
345
|
const label = `ownership[${String(index)}]`;
|
|
324
|
-
|
|
325
|
-
throw new Error(`${label} is invalid`);
|
|
346
|
+
validateOwnershipRecord(record, label, ownershipIds);
|
|
326
347
|
ownershipIds.add(record.id);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
validateScheduledOptions(record.options, `${label}.options`);
|
|
330
|
-
}
|
|
348
|
+
return record;
|
|
349
|
+
});
|
|
331
350
|
for (const record of ownership)
|
|
332
|
-
if (
|
|
351
|
+
if (record.parentId !== undefined && !ownershipIds.has(record.parentId))
|
|
333
352
|
throw new Error("Persisted ownership parent is missing");
|
|
334
353
|
await store.validateDeletionWorktrees();
|
|
335
354
|
const borrowed = await store.borrowedWorktrees();
|
|
@@ -467,7 +486,7 @@ function planSession(scan, cutoffMs) {
|
|
|
467
486
|
}
|
|
468
487
|
return { candidates: scan.runs.filter(({ runId }) => oldTerminal.has(runId) && !protectedRuns.has(runId)), skipped };
|
|
469
488
|
}
|
|
470
|
-
function deletionOrder(
|
|
489
|
+
function deletionOrder(_scan, candidates) {
|
|
471
490
|
const candidateIds = new Set(candidates.map(({ runId }) => runId));
|
|
472
491
|
const remaining = new Set(candidateIds);
|
|
473
492
|
const ordered = [];
|
|
@@ -487,7 +506,7 @@ async function storedSessionIds(cwd, home) {
|
|
|
487
506
|
entries = await sessionEntries(path);
|
|
488
507
|
}
|
|
489
508
|
catch (error) {
|
|
490
|
-
if (error
|
|
509
|
+
if (isNodeError(error, "ENOENT"))
|
|
491
510
|
return [];
|
|
492
511
|
throw error;
|
|
493
512
|
}
|
package/dist/src/doctor.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
1
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
4
4
|
import { ModelRuntime, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { DEFAULT_SETTINGS, createLocalPiSession, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
|
|
5
|
+
import { DEFAULT_SETTINGS, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
|
|
6
6
|
import { loadingRegistry } from "pi-extensible-workflows";
|
|
7
7
|
import { disabledResources, unmatchedResourcePatterns } from "pi-extensible-workflows";
|
|
8
8
|
const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
|
|
@@ -20,16 +20,23 @@ function isDynamicModelAlias(value, aliases) {
|
|
|
20
20
|
const name = match?.[1];
|
|
21
21
|
return Boolean(name && (match[2] === undefined || parseThinking(match[2]) !== undefined) && aliases.has(name));
|
|
22
22
|
}
|
|
23
|
+
function isCredential(value) {
|
|
24
|
+
if (!isObject(value))
|
|
25
|
+
return false;
|
|
26
|
+
if (value.type === "api_key")
|
|
27
|
+
return (value.key === undefined || typeof value.key === "string") && (value.env === undefined || isObject(value.env) && Object.values(value.env).every((entry) => typeof entry === "string"));
|
|
28
|
+
return value.type === "oauth" && typeof value.refresh === "string" && typeof value.access === "string" && typeof value.expires === "number";
|
|
29
|
+
}
|
|
23
30
|
async function readCredentials(agentDir) {
|
|
24
31
|
const credentials = new InMemoryCredentialStore();
|
|
25
32
|
try {
|
|
26
33
|
const parsed = JSON.parse(readFileSync(join(agentDir, "auth.json"), "utf8"));
|
|
27
|
-
if (
|
|
34
|
+
if (!isObject(parsed))
|
|
28
35
|
throw new Error("Pi auth.json must be an object");
|
|
29
|
-
await Promise.all(Object.entries(parsed).
|
|
36
|
+
await Promise.all(Object.entries(parsed).flatMap(([provider, credential]) => isCredential(credential) ? [credentials.modify(provider, async () => credential)] : []));
|
|
30
37
|
}
|
|
31
38
|
catch (error) {
|
|
32
|
-
if (error
|
|
39
|
+
if (!isNodeError(error, "ENOENT"))
|
|
33
40
|
throw error;
|
|
34
41
|
}
|
|
35
42
|
return credentials;
|
|
@@ -40,11 +47,11 @@ function savedTrust(cwd, agentDir) {
|
|
|
40
47
|
parsed = JSON.parse(readFileSync(join(agentDir, "trust.json"), "utf8"));
|
|
41
48
|
}
|
|
42
49
|
catch (error) {
|
|
43
|
-
if (error
|
|
50
|
+
if (isNodeError(error, "ENOENT"))
|
|
44
51
|
return undefined;
|
|
45
52
|
throw error;
|
|
46
53
|
}
|
|
47
|
-
if (
|
|
54
|
+
if (!isObject(parsed))
|
|
48
55
|
throw new Error("Pi trust.json must be an object");
|
|
49
56
|
let current = canonical(cwd);
|
|
50
57
|
while (current !== dirname(current)) {
|
|
@@ -106,12 +113,28 @@ async function discoverPi(cwd, agentDir) {
|
|
|
106
113
|
process.env.PI_OFFLINE = previousOffline;
|
|
107
114
|
}
|
|
108
115
|
}
|
|
116
|
+
function isRoleFile(dir, entry) {
|
|
117
|
+
if (extname(entry.name) !== ".md")
|
|
118
|
+
return false;
|
|
119
|
+
if (entry.isFile())
|
|
120
|
+
return true;
|
|
121
|
+
if (!entry.isSymbolicLink())
|
|
122
|
+
return false;
|
|
123
|
+
try {
|
|
124
|
+
return statSync(join(dir, entry.name)).isFile();
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (isNodeError(error, "ENOENT"))
|
|
128
|
+
return false;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
109
132
|
function roleFiles(dir) {
|
|
110
133
|
try {
|
|
111
|
-
return readdirSync(dir, { withFileTypes: true }).filter((entry) =>
|
|
134
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => isRoleFile(dir, entry)).map((entry) => join(dir, entry.name)).sort();
|
|
112
135
|
}
|
|
113
136
|
catch (error) {
|
|
114
|
-
if (error
|
|
137
|
+
if (isNodeError(error, "ENOENT"))
|
|
115
138
|
return [];
|
|
116
139
|
throw error;
|
|
117
140
|
}
|
|
@@ -128,7 +151,7 @@ function scanExtensionRoleFiles(registrations) {
|
|
|
128
151
|
for (const registration of registrations) {
|
|
129
152
|
try {
|
|
130
153
|
const entries = readdirSync(registration.path, { withFileTypes: true });
|
|
131
|
-
const roleFiles = entries.filter((entry) =>
|
|
154
|
+
const roleFiles = entries.filter((entry) => isRoleFile(registration.path, entry));
|
|
132
155
|
if (!roleFiles.length)
|
|
133
156
|
empty.push(registration);
|
|
134
157
|
for (const entry of roleFiles)
|
|
@@ -161,7 +184,8 @@ function validateModel(value, known, available, source, diagnostics, aliases, dy
|
|
|
161
184
|
diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
|
|
162
185
|
}
|
|
163
186
|
catch (error) {
|
|
164
|
-
|
|
187
|
+
const message = errorText(error);
|
|
188
|
+
diagnostics.push(diagnostic("error", "MODEL_INVALID", message, source, message.includes("thinking") ? THINKING_HINT : "Use provider/model or provider/model:thinking."));
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
191
|
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
|
|
@@ -244,7 +268,7 @@ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath,
|
|
|
244
268
|
return undefined;
|
|
245
269
|
}
|
|
246
270
|
finally {
|
|
247
|
-
session.dispose();
|
|
271
|
+
await session.dispose();
|
|
248
272
|
}
|
|
249
273
|
}
|
|
250
274
|
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
@@ -258,7 +282,7 @@ export async function doctor(options = {}) {
|
|
|
258
282
|
settings = loadSettings(settingsPath);
|
|
259
283
|
}
|
|
260
284
|
catch (error) {
|
|
261
|
-
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", error
|
|
285
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), settingsPath, "Fix or remove the invalid workflow settings file."));
|
|
262
286
|
}
|
|
263
287
|
let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
|
|
264
288
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
@@ -267,7 +291,7 @@ export async function doctor(options = {}) {
|
|
|
267
291
|
pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
|
|
268
292
|
}
|
|
269
293
|
catch (error) {
|
|
270
|
-
diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${error
|
|
294
|
+
diagnostics.push(diagnostic("error", "PI_DISCOVERY", `Pi headless discovery failed: ${errorText(error)}`, undefined, "Open and trust the project in Pi, fix extension errors, then rerun doctor."));
|
|
271
295
|
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
|
|
272
296
|
}
|
|
273
297
|
if (options.activeTools)
|
|
@@ -311,7 +335,7 @@ export async function doctor(options = {}) {
|
|
|
311
335
|
const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
|
|
312
336
|
const modelAliases = [
|
|
313
337
|
...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
|
|
314
|
-
...registeredModelAliases.map(({ name, version, headline
|
|
338
|
+
...registeredModelAliases.map(({ name, version, headline }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline })),
|
|
315
339
|
].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
|
|
316
340
|
const roles = [];
|
|
317
341
|
const definitions = new Map();
|
|
@@ -14,27 +14,39 @@ function text(content) {
|
|
|
14
14
|
const parts = content;
|
|
15
15
|
return parts.flatMap((part) => typeof part === "object" && part !== null && "type" in part && part.type === "text" && "text" in part && typeof part.text === "string" ? [part.text] : []).join("");
|
|
16
16
|
}
|
|
17
|
+
function isTranscriptPart(value) {
|
|
18
|
+
if (typeof value !== "object" || value === null || !("type" in value))
|
|
19
|
+
return false;
|
|
20
|
+
if (value.type === "text")
|
|
21
|
+
return "text" in value && typeof value.text === "string";
|
|
22
|
+
if (value.type === "thinking")
|
|
23
|
+
return "thinking" in value && typeof value.thinking === "string";
|
|
24
|
+
if (value.type === "toolCall")
|
|
25
|
+
return "name" in value && typeof value.name === "string";
|
|
26
|
+
return value.type === "image";
|
|
27
|
+
}
|
|
28
|
+
function isTranscriptMessage(value) {
|
|
29
|
+
if (typeof value !== "object" || value === null)
|
|
30
|
+
return false;
|
|
31
|
+
return (!(("role" in value) && value.role !== undefined) || typeof value.role === "string") && (!(("toolName" in value) && value.toolName !== undefined) || typeof value.toolName === "string") && (!(("customType" in value) && value.customType !== undefined) || typeof value.customType === "string");
|
|
32
|
+
}
|
|
17
33
|
function transcriptPartLines(part) {
|
|
18
|
-
if (
|
|
34
|
+
if (!isTranscriptPart(part))
|
|
19
35
|
return [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (value.type === "image")
|
|
28
|
-
return ["[image]"];
|
|
29
|
-
return [];
|
|
36
|
+
if (part.type === "text")
|
|
37
|
+
return part.text.split("\n");
|
|
38
|
+
if (part.type === "thinking")
|
|
39
|
+
return ["Thinking:", ...part.thinking.split("\n")];
|
|
40
|
+
if (part.type === "toolCall")
|
|
41
|
+
return [`Tool call: ${part.name}`, JSON.stringify(part.arguments, null, 2)];
|
|
42
|
+
return ["[image]"];
|
|
30
43
|
}
|
|
31
44
|
function transcriptMessageLines(message) {
|
|
32
|
-
if (
|
|
45
|
+
if (!isTranscriptMessage(message))
|
|
33
46
|
return ["(invalid message)"];
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const content = Array.isArray(value.content) ? value.content.flatMap(transcriptPartLines) : typeof value.content === "string" ? value.content.split("\n") : [];
|
|
47
|
+
const role = message.role ?? "message";
|
|
48
|
+
const label = role === "toolResult" && message.toolName !== undefined ? `${role}: ${message.toolName}` : role === "custom" && message.customType !== undefined ? `${role}: ${message.customType}` : role;
|
|
49
|
+
const content = Array.isArray(message.content) ? message.content.flatMap(transcriptPartLines) : typeof message.content === "string" ? message.content.split("\n") : [];
|
|
38
50
|
return [`[${label}]`, ...(content.length ? content : ["(empty)"])];
|
|
39
51
|
}
|
|
40
52
|
export function transcriptLines(entries) {
|
|
@@ -58,8 +70,11 @@ function mergedModels(groups) {
|
|
|
58
70
|
return [...totals].map(([model, cost]) => ({ model, cost })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
59
71
|
}
|
|
60
72
|
const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
73
|
+
function isThinkingLevel(value) {
|
|
74
|
+
return typeof value === "string" && thinkingLevels.some((level) => level === value);
|
|
75
|
+
}
|
|
61
76
|
function parsedThinking(value) {
|
|
62
|
-
return
|
|
77
|
+
return isThinkingLevel(value) ? value : undefined;
|
|
63
78
|
}
|
|
64
79
|
function modelName(provider, model) {
|
|
65
80
|
return typeof provider === "string" && provider && typeof model === "string" && model ? `${provider}/${model}` : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piewf/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "CLI for pi-extensible-workflows",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@earendil-works/pi-ai": "*",
|
|
31
31
|
"@earendil-works/pi-coding-agent": "*",
|
|
32
|
+
"@earendil-works/pi-tui": "*",
|
|
32
33
|
"typebox": "*"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
},
|
|
46
47
|
"license": "MIT",
|
|
47
48
|
"dependencies": {
|
|
48
|
-
"pi-extensible-workflows": "5.
|
|
49
|
+
"pi-extensible-workflows": "5.3.0"
|
|
49
50
|
},
|
|
50
51
|
"publishConfig": {
|
|
51
52
|
"access": "public"
|