@piewf/cli 5.0.0 → 5.2.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 +46 -11
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.js +104 -31
- package/dist/src/doctor-cleanup.js +98 -55
- package/dist/src/doctor.d.ts +40 -0
- package/dist/src/doctor.js +187 -22
- package/dist/src/session-inspector.js +32 -17
- package/package.json +2 -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: {",
|
|
@@ -328,6 +364,8 @@ function roleMarkdown(role) {
|
|
|
328
364
|
metadata.push(`tools: ${JSON.stringify(role.tools)}`);
|
|
329
365
|
if (role.overrideSystemPrompt !== undefined)
|
|
330
366
|
metadata.push(`overrideSystemPrompt: ${String(role.overrideSystemPrompt)}`);
|
|
367
|
+
if (role.contextFiles !== undefined)
|
|
368
|
+
metadata.push(`contextFiles: ${JSON.stringify(role.contextFiles)}`);
|
|
331
369
|
if (role.disabledAgentResources !== undefined)
|
|
332
370
|
metadata.push(`disabledAgentResources: ${JSON.stringify(role.disabledAgentResources)}`);
|
|
333
371
|
metadata.push("---");
|
|
@@ -352,12 +390,9 @@ function copyResources(root, resources) {
|
|
|
352
390
|
throw new Error(`Bundle resource may contain credentials and cannot be selected: ${source}`);
|
|
353
391
|
let name = basename(source);
|
|
354
392
|
if (kind === "dependencies") {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
name = packageName;
|
|
359
|
-
}
|
|
360
|
-
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;
|
|
361
396
|
}
|
|
362
397
|
if (!name || name === "." || name === ".." || name.includes("\\") || name.startsWith("/"))
|
|
363
398
|
throw new Error(`Invalid bundle resource name: ${name}`);
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -15,5 +15,9 @@ export interface CliOptions extends DoctorOptions {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function formatWorkflowCliHelp(fn: WorkflowCatalogFunction, command?: string): string;
|
|
17
17
|
export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readonly string[]): Record<string, JsonValue>;
|
|
18
|
+
export declare function parseDoctorArgs(rawArgs: readonly string[]): {
|
|
19
|
+
role?: string;
|
|
20
|
+
prompt?: string;
|
|
21
|
+
};
|
|
18
22
|
export declare function parseDoctorCleanupArgs(rawArgs: readonly string[]): Required<Pick<DoctorCleanupOptions, "olderThanDays" | "yes">>;
|
|
19
23
|
export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;
|
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
|
}
|
|
@@ -218,12 +238,47 @@ function parseInspectArgs(rawArgs) {
|
|
|
218
238
|
}
|
|
219
239
|
return { ...(sessionId ? { sessionId } : {}), mode: failedOnly && mode === "tui" ? "summary" : mode, failedOnly };
|
|
220
240
|
}
|
|
241
|
+
export function parseDoctorArgs(rawArgs) {
|
|
242
|
+
let role;
|
|
243
|
+
let prompt;
|
|
244
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
245
|
+
const token = requiredArg(rawArgs, index);
|
|
246
|
+
const equals = token.indexOf("=");
|
|
247
|
+
const option = equals >= 0 ? token.slice(0, equals) : token;
|
|
248
|
+
if (option === "--role" || option === "--prompt") {
|
|
249
|
+
const value = equals >= 0 ? token.slice(equals + 1) : rawArgs[++index];
|
|
250
|
+
if (!value)
|
|
251
|
+
throw new Error(`Missing value for ${option}`);
|
|
252
|
+
if (option === "--role") {
|
|
253
|
+
if (role !== undefined)
|
|
254
|
+
throw new Error("--role may only be provided once");
|
|
255
|
+
role = value;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
if (prompt !== undefined)
|
|
259
|
+
throw new Error("--prompt may only be provided once");
|
|
260
|
+
prompt = value;
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (token === "--help" || token === "-h")
|
|
265
|
+
throw new Error("help");
|
|
266
|
+
if (token.startsWith("--"))
|
|
267
|
+
throw new Error(`Unknown doctor option: ${token}`);
|
|
268
|
+
if (role !== undefined)
|
|
269
|
+
throw new Error(`Unexpected argument: ${token}`);
|
|
270
|
+
role = token;
|
|
271
|
+
}
|
|
272
|
+
if (prompt !== undefined && role === undefined)
|
|
273
|
+
throw new Error("--prompt requires --role");
|
|
274
|
+
return { ...(role === undefined ? {} : { role }), ...(prompt === undefined ? {} : { prompt }) };
|
|
275
|
+
}
|
|
221
276
|
export function parseDoctorCleanupArgs(rawArgs) {
|
|
222
277
|
let olderThanDays = 90;
|
|
223
278
|
let yes = false;
|
|
224
279
|
let seenDays = false;
|
|
225
280
|
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
226
|
-
const token = rawArgs
|
|
281
|
+
const token = requiredArg(rawArgs, index);
|
|
227
282
|
if (token === "--yes") {
|
|
228
283
|
yes = true;
|
|
229
284
|
continue;
|
|
@@ -267,6 +322,8 @@ function stripTrustOptions(rawArgs) {
|
|
|
267
322
|
}
|
|
268
323
|
return { args, ...(trustOverride !== undefined ? { trustOverride } : {}) };
|
|
269
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"; }
|
|
270
327
|
async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
271
328
|
const cwd = options.cwd ?? process.cwd();
|
|
272
329
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
@@ -289,6 +346,8 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
289
346
|
for (const handler of extension.handlers.get("project_trust") ?? []) {
|
|
290
347
|
try {
|
|
291
348
|
const result = await handler({ type: "project_trust", cwd }, projectTrustContext);
|
|
349
|
+
if (!object(result))
|
|
350
|
+
continue;
|
|
292
351
|
if (result.trusted === "undecided")
|
|
293
352
|
continue;
|
|
294
353
|
if (result.trusted !== "yes" && result.trusted !== "no")
|
|
@@ -328,7 +387,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
|
|
|
328
387
|
events: { emit() { } },
|
|
329
388
|
};
|
|
330
389
|
workflowExtension(headlessPi, homedir(), undefined, undefined, agentDir, options.skillPaths);
|
|
331
|
-
const workflowTool = tools.find(
|
|
390
|
+
const workflowTool = tools.find(isHeadlessWorkflowTool);
|
|
332
391
|
if (!workflowTool)
|
|
333
392
|
throw new Error("The workflow runtime could not be initialized");
|
|
334
393
|
return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
|
|
@@ -360,8 +419,8 @@ import { homedir } from "node:os";
|
|
|
360
419
|
import { join } from "node:path";
|
|
361
420
|
import { pathToFileURL } from "node:url";
|
|
362
421
|
let cli;
|
|
363
|
-
try { cli = await import(
|
|
364
|
-
if (!cli) try { cli = await import(
|
|
422
|
+
try { cli = await import(pathToFileURL(join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "npm", "node_modules", "@piewf/cli", "dist", "src", "cli.js")).href); } catch {}
|
|
423
|
+
if (!cli) try { cli = await import(import.meta.resolve("@piewf/cli")); } catch {}
|
|
365
424
|
if (cli) process.exitCode = await cli.runCli(["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)]);
|
|
366
425
|
else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}, ...process.argv.slice(2)], { stdio: "inherit" }); if (result.error) { console.error("Could not resolve @piewf/cli; install it or put piewf on PATH."); process.exitCode = 1; } else process.exitCode = result.status ?? 1; }
|
|
367
426
|
`;
|
|
@@ -374,7 +433,7 @@ else { const result = spawnSync("piewf", ["run", ${JSON.stringify(workflowName)}
|
|
|
374
433
|
linkSync(tempPath, destination);
|
|
375
434
|
}
|
|
376
435
|
catch (error) {
|
|
377
|
-
if (error
|
|
436
|
+
if (isNodeError(error, "EEXIST"))
|
|
378
437
|
throw new Error(`Destination already exists: ${destination}; use --force to replace it`, { cause: error });
|
|
379
438
|
throw error;
|
|
380
439
|
}
|
|
@@ -467,13 +526,15 @@ async function invokeWorkflow(fn, args, runtime, options, context) {
|
|
|
467
526
|
return; announcedRunId = runId; options.stderr(`Run ID: ${runId}\n`); };
|
|
468
527
|
const progress = new CliProgress(options.stderr, options.isTTY ?? process.stderr.isTTY, announceRunId);
|
|
469
528
|
try {
|
|
470
|
-
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))
|
|
471
530
|
progress.update(update.details.run); }, context);
|
|
531
|
+
if (!isHeadlessWorkflowResult(result))
|
|
532
|
+
throw new Error("Workflow returned an invalid result");
|
|
472
533
|
const details = object(result.details) ? result.details : {};
|
|
473
534
|
const runId = typeof details.runId === "string" ? details.runId : undefined;
|
|
474
535
|
if (runId)
|
|
475
536
|
announceRunId(runId);
|
|
476
|
-
if (has(details, "value"))
|
|
537
|
+
if (has(details, "value") && jsonValue(details.value))
|
|
477
538
|
return { value: details.value, ...(runId ? { runId } : {}) };
|
|
478
539
|
const first = result.content[0];
|
|
479
540
|
if (!first || first.type !== "text")
|
|
@@ -522,7 +583,7 @@ async function runWorkflowCli(rawArgs, options) {
|
|
|
522
583
|
options.write(workflowUsage());
|
|
523
584
|
return args.length ? 0 : 1;
|
|
524
585
|
}
|
|
525
|
-
const name = args
|
|
586
|
+
const name = requiredArg(args, 0);
|
|
526
587
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime, context) => {
|
|
527
588
|
const help = args.slice(1).some((arg) => arg === "--help" || arg === "-h");
|
|
528
589
|
const fn = runtime.catalog.functions.find((candidate) => candidate.name === name);
|
|
@@ -547,13 +608,13 @@ async function exportWorkflowCli(rawArgs, options) {
|
|
|
547
608
|
options.write(exportUsage());
|
|
548
609
|
return args.length ? 0 : 1;
|
|
549
610
|
}
|
|
550
|
-
const workflowName = args
|
|
611
|
+
const workflowName = requiredArg(args, 0);
|
|
551
612
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
552
613
|
let name;
|
|
553
614
|
let output;
|
|
554
615
|
let force = false;
|
|
555
616
|
for (let index = 1; index < args.length; index += 1) {
|
|
556
|
-
const arg = args
|
|
617
|
+
const arg = requiredArg(args, index);
|
|
557
618
|
if (arg === "--force") {
|
|
558
619
|
force = true;
|
|
559
620
|
continue;
|
|
@@ -605,7 +666,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
605
666
|
options.write(bundleUsage());
|
|
606
667
|
return args.length ? 0 : 1;
|
|
607
668
|
}
|
|
608
|
-
const workflowName = args
|
|
669
|
+
const workflowName = requiredArg(args, 0);
|
|
609
670
|
return withWorkflowRuntime({ ...options, ...(parsed.trustOverride !== undefined ? { trustOverride: parsed.trustOverride } : {}) }, async (runtime) => {
|
|
610
671
|
let name;
|
|
611
672
|
let output;
|
|
@@ -613,7 +674,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
613
674
|
const requirements = { roles: [], aliases: [], tools: [], commands: [], environment: [] };
|
|
614
675
|
const resources = { extensions: [], skills: [], static: [], dependencies: [] };
|
|
615
676
|
for (let index = 1; index < args.length; index += 1) {
|
|
616
|
-
const arg = args
|
|
677
|
+
const arg = requiredArg(args, index);
|
|
617
678
|
if (arg === "--force") {
|
|
618
679
|
force = true;
|
|
619
680
|
continue;
|
|
@@ -622,7 +683,7 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
622
683
|
const option = equals >= 0 ? arg.slice(0, equals) : arg;
|
|
623
684
|
const requirementOptions = { "--role": "roles", "--alias": "aliases", "--tool": "tools", "--command": "commands", "--environment": "environment" };
|
|
624
685
|
const resourceOptions = { "--extension": "extensions", "--skill": "skills", "--resource": "static", "--dependency": "dependencies" };
|
|
625
|
-
if (option === "--name" || option === "--output" ||
|
|
686
|
+
if (option === "--name" || option === "--output" || typedOptionKey(requirementOptions, option) || typedOptionKey(resourceOptions, option)) {
|
|
626
687
|
const value = equals >= 0 ? arg.slice(equals + 1) : args[++index];
|
|
627
688
|
if (!value)
|
|
628
689
|
throw new Error(`Missing value for ${option}`);
|
|
@@ -630,9 +691,9 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
630
691
|
name = value;
|
|
631
692
|
else if (option === "--output")
|
|
632
693
|
output = value;
|
|
633
|
-
else if (
|
|
694
|
+
else if (typedOptionKey(requirementOptions, option))
|
|
634
695
|
requirements[requirementOptions[option]].push(value);
|
|
635
|
-
else
|
|
696
|
+
else if (typedOptionKey(resourceOptions, option))
|
|
636
697
|
resources[resourceOptions[option]].push(value);
|
|
637
698
|
continue;
|
|
638
699
|
}
|
|
@@ -674,10 +735,21 @@ async function bundleWorkflowCli(rawArgs, options) {
|
|
|
674
735
|
}
|
|
675
736
|
export async function runCli(args, options = {}, write = (text) => { process.stdout.write(text); }) {
|
|
676
737
|
const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
|
|
677
|
-
if (args[0] === "doctor" && args
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
738
|
+
if (args[0] === "doctor" && args[1] !== "cleanup") {
|
|
739
|
+
if (args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
|
|
740
|
+
write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>]\n");
|
|
741
|
+
return 0;
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
const parsed = parseDoctorArgs(args.slice(1));
|
|
745
|
+
const report = await doctor({ ...options, ...parsed });
|
|
746
|
+
write(formatDoctorReport(report));
|
|
747
|
+
return doctorExitCode(report);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
751
|
+
return 1;
|
|
752
|
+
}
|
|
681
753
|
}
|
|
682
754
|
if (args[0] === "doctor" && args[1] === "cleanup") {
|
|
683
755
|
if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
|
|
@@ -712,10 +784,11 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
712
784
|
}
|
|
713
785
|
if (args[0] === "transcript" && args.length === 2) {
|
|
714
786
|
try {
|
|
787
|
+
const transcript = requiredArg(args, 1);
|
|
715
788
|
if (options.transcript)
|
|
716
|
-
await options.transcript(
|
|
789
|
+
await options.transcript(transcript);
|
|
717
790
|
else
|
|
718
|
-
write(`${transcriptFileLines(
|
|
791
|
+
write(`${transcriptFileLines(transcript).join("\n")}\n`);
|
|
719
792
|
return 0;
|
|
720
793
|
}
|
|
721
794
|
catch (error) {
|
|
@@ -735,7 +808,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
735
808
|
return 1;
|
|
736
809
|
}
|
|
737
810
|
}
|
|
738
|
-
write("Usage: piewf doctor | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
|
|
811
|
+
write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
|
|
739
812
|
return 1;
|
|
740
813
|
}
|
|
741
814
|
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
@@ -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,26 +54,48 @@ 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`); }
|
|
60
|
+
function optionalRole(value, label) {
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
return;
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
if (!value.trim())
|
|
65
|
+
throw new Error(`${label} is invalid`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!object(value) || typeof value.name !== "string" || !value.name.trim())
|
|
69
|
+
throw new Error(`${label} is invalid`);
|
|
70
|
+
for (const key of ["model", "description"])
|
|
71
|
+
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string")
|
|
72
|
+
throw new Error(`${label}.${key} is invalid`);
|
|
73
|
+
if (value.thinking !== undefined && value.thinking !== null && !isThinking(value.thinking))
|
|
74
|
+
throw new Error(`${label}.thinking is invalid`);
|
|
75
|
+
if (value.tools !== undefined && value.tools !== null)
|
|
76
|
+
stringList(value.tools, `${label}.tools`);
|
|
77
|
+
if (value.overrideSystemPrompt !== undefined && value.overrideSystemPrompt !== null && typeof value.overrideSystemPrompt !== "boolean")
|
|
78
|
+
throw new Error(`${label}.overrideSystemPrompt is invalid`);
|
|
79
|
+
if (value.contextFiles !== undefined && value.contextFiles !== null)
|
|
80
|
+
stringList(value.contextFiles, `${label}.contextFiles`);
|
|
81
|
+
if (value.disabledAgentResources !== undefined && value.disabledAgentResources !== null)
|
|
82
|
+
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
83
|
+
}
|
|
53
84
|
function validateScheduledOptions(value, label) { if (!object(value) || typeof value.label !== "string" || !value.label || typeof value.cwd !== "string" || !value.cwd)
|
|
54
|
-
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 && !
|
|
55
|
-
throw new Error(`${label}.thinking is invalid`);
|
|
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))
|
|
86
|
+
throw new Error(`${label}.thinking is invalid`); optionalRole(value.role, `${label}.role`); if (value.schema !== undefined)
|
|
56
87
|
validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined)
|
|
57
88
|
nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null)
|
|
58
89
|
positiveInteger(value.timeoutMs, `${label}.timeoutMs`); if (value.agentOptions !== undefined && (!object(value.agentOptions) || !jsonValue(value.agentOptions)))
|
|
59
90
|
throw new Error(`${label}.agentOptions is invalid`); if (value.agentIdentity !== undefined) {
|
|
60
|
-
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")
|
|
61
92
|
throw new Error(`${label}.agentIdentity is invalid`);
|
|
62
93
|
positiveInteger(value.agentIdentity.occurrence, `${label}.agentIdentity.occurrence`);
|
|
63
94
|
optionalString(value.agentIdentity.parentBreadcrumb, `${label}.agentIdentity.parentBreadcrumb`);
|
|
64
95
|
optionalString(value.agentIdentity.worktreeOwner, `${label}.agentIdentity.worktreeOwner`);
|
|
65
96
|
} }
|
|
66
|
-
function validateSessionReference(value, label) {
|
|
67
|
-
|
|
68
|
-
throw new Error(`${label} is invalid`);
|
|
69
|
-
}
|
|
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`); }
|
|
70
99
|
function validateAgentSetup(value, label) {
|
|
71
100
|
if (!object(value))
|
|
72
101
|
throw new Error(`${label} is invalid`);
|
|
@@ -78,7 +107,7 @@ function validateAgentSetup(value, label) {
|
|
|
78
107
|
resourceExclusions(value.disabledAgentResources, `${label}.disabledAgentResources`);
|
|
79
108
|
}
|
|
80
109
|
function validateAgent(value, label) {
|
|
81
|
-
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))
|
|
82
111
|
throw new Error(`${label} is invalid`);
|
|
83
112
|
optionalString(value.systemPrompt, `${label}.systemPrompt`);
|
|
84
113
|
optionalString(value.prompt, `${label}.prompt`);
|
|
@@ -94,7 +123,7 @@ function validateAgent(value, label) {
|
|
|
94
123
|
stringList(value.tools, `${label}.tools`);
|
|
95
124
|
nonNegativeInteger(value.attempts, `${label}.attempts`);
|
|
96
125
|
if (value.attemptDetails !== undefined) {
|
|
97
|
-
if (!
|
|
126
|
+
if (!isList(value.attemptDetails))
|
|
98
127
|
throw new Error(`${label}.attemptDetails is invalid`);
|
|
99
128
|
for (const [index, attempt] of value.attemptDetails.entries()) {
|
|
100
129
|
const at = `${label}.attemptDetails[${String(index)}]`;
|
|
@@ -102,8 +131,9 @@ function validateAgent(value, label) {
|
|
|
102
131
|
throw new Error(`${at} is invalid`);
|
|
103
132
|
validateAgentSetup(attempt.setup, `${at}.setup`);
|
|
104
133
|
if (attempt.session !== undefined) {
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
const session = attempt.session;
|
|
135
|
+
validateSessionReference(session, `${at}.session`);
|
|
136
|
+
if (session.transport !== attempt.transport)
|
|
107
137
|
throw new Error(`${at}.session transport does not match attempt transport`);
|
|
108
138
|
}
|
|
109
139
|
accounting(attempt.accounting, `${at}.accounting`);
|
|
@@ -114,14 +144,14 @@ function validateAgent(value, label) {
|
|
|
114
144
|
if (value.accounting !== undefined)
|
|
115
145
|
accounting(value.accounting, `${label}.accounting`);
|
|
116
146
|
if (value.toolCalls !== undefined) {
|
|
117
|
-
if (!
|
|
147
|
+
if (!isList(value.toolCalls))
|
|
118
148
|
throw new Error(`${label}.toolCalls is invalid`);
|
|
119
149
|
for (const [index, call] of value.toolCalls.entries())
|
|
120
|
-
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))
|
|
121
151
|
throw new Error(`${label}.toolCalls[${String(index)}] is invalid`);
|
|
122
152
|
}
|
|
123
153
|
if (value.activity !== undefined) {
|
|
124
|
-
if (!object(value.activity) || !
|
|
154
|
+
if (!object(value.activity) || !isActivityKind(value.activity.kind) || typeof value.activity.text !== "string")
|
|
125
155
|
throw new Error(`${label}.activity is invalid`);
|
|
126
156
|
}
|
|
127
157
|
if (value.lastEventAt !== undefined)
|
|
@@ -131,20 +161,18 @@ function validateUsage(value, label) { if (!object(value))
|
|
|
131
161
|
throw new Error(`${label} is invalid`); for (const key of ["tokens", "costUsd", "durationMs", "agentLaunches"])
|
|
132
162
|
finiteNumber(value[key], `${label}.${key}`); }
|
|
133
163
|
function validateBudgetEvents(value) { if (value === undefined)
|
|
134
|
-
return; if (!
|
|
164
|
+
return; if (!isList(value))
|
|
135
165
|
throw new Error("Persisted budget events are invalid"); for (const [index, event] of value.entries()) {
|
|
136
166
|
const label = `budgetEvents[${String(index)}]`;
|
|
137
|
-
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)
|
|
138
168
|
throw new Error(`${label} is invalid`);
|
|
139
169
|
validateUsage(event.usage, `${label}.usage`);
|
|
140
170
|
validateBudget(event.limits);
|
|
141
171
|
} }
|
|
142
|
-
function validateRunRecord(
|
|
143
|
-
|
|
144
|
-
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"))
|
|
145
174
|
throw new Error("Persisted run state is invalid");
|
|
146
|
-
const agents = value.agents;
|
|
147
|
-
agents.forEach((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); });
|
|
175
|
+
const agents = value.agents.map((agent, index) => { validateAgent(agent, `agents[${String(index)}]`); return agent; });
|
|
148
176
|
const agentIds = new Set();
|
|
149
177
|
for (const agent of agents) {
|
|
150
178
|
const id = agent.id;
|
|
@@ -154,36 +182,36 @@ function validateRunRecord(run) {
|
|
|
154
182
|
}
|
|
155
183
|
for (const agent of agents) {
|
|
156
184
|
const parentId = agent.parentId;
|
|
157
|
-
if (parentId !== undefined &&
|
|
185
|
+
if (parentId !== undefined && !agentIds.has(parentId))
|
|
158
186
|
throw new Error("Persisted agent has a missing parent");
|
|
159
187
|
const seen = new Set();
|
|
160
|
-
let parent =
|
|
188
|
+
let parent = parentId;
|
|
161
189
|
while (parent) {
|
|
162
190
|
if (seen.has(parent))
|
|
163
191
|
throw new Error("Persisted agent parent cycle");
|
|
164
192
|
seen.add(parent);
|
|
165
193
|
const parentAgent = agents.find((candidate) => candidate.id === parent);
|
|
166
|
-
parent =
|
|
194
|
+
parent = parentAgent?.parentId;
|
|
167
195
|
}
|
|
168
196
|
}
|
|
169
|
-
const
|
|
170
|
-
for (const [index, session] of sessions.entries())
|
|
197
|
+
for (const [index, session] of value.agentSessions.entries())
|
|
171
198
|
validateSessionReference(session, `agentSessions[${String(index)}]`);
|
|
172
199
|
optionalString(value.parentRunId, "Persisted parent run");
|
|
173
200
|
optionalString(value.failedAt, "Persisted failed path");
|
|
174
|
-
|
|
175
|
-
|
|
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)
|
|
176
204
|
throw new Error("Persisted retry provenance is invalid");
|
|
177
|
-
const sourceRunId =
|
|
178
|
-
stringList(
|
|
179
|
-
stringList(
|
|
180
|
-
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");
|
|
181
209
|
if (value.parentRunId !== sourceRunId)
|
|
182
210
|
throw new Error("Persisted retry parent does not match its source");
|
|
183
211
|
}
|
|
184
212
|
optionalString(value.phase, "Persisted phase");
|
|
185
213
|
if (value.phaseHistory !== undefined) {
|
|
186
|
-
if (!
|
|
214
|
+
if (!isList(value.phaseHistory))
|
|
187
215
|
throw new Error("Persisted phase history is invalid");
|
|
188
216
|
for (const phase of value.phaseHistory) {
|
|
189
217
|
if (!object(phase) || typeof phase.phase !== "string" || !phase.phase)
|
|
@@ -191,6 +219,18 @@ function validateRunRecord(run) {
|
|
|
191
219
|
nonNegativeInteger(phase.afterAgent, "Persisted phase history afterAgent");
|
|
192
220
|
}
|
|
193
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
|
+
}
|
|
194
234
|
if (value.error !== undefined && (!object(value.error) || typeof value.error.code !== "string" || typeof value.error.message !== "string"))
|
|
195
235
|
throw new Error("Persisted run error is invalid");
|
|
196
236
|
validateBudget(value.budget);
|
|
@@ -200,7 +240,7 @@ function validateRunRecord(run) {
|
|
|
200
240
|
validateUsage(value.usage, "Persisted usage");
|
|
201
241
|
validateBudgetEvents(value.budgetEvents);
|
|
202
242
|
if (value.events !== undefined) {
|
|
203
|
-
if (!
|
|
243
|
+
if (!isList(value.events))
|
|
204
244
|
throw new Error("Persisted run events are invalid");
|
|
205
245
|
for (const event of value.events)
|
|
206
246
|
if (!object(event) || typeof event.type !== "string" || typeof event.message !== "string")
|
|
@@ -208,7 +248,7 @@ function validateRunRecord(run) {
|
|
|
208
248
|
}
|
|
209
249
|
}
|
|
210
250
|
function validateSnapshot(snapshot) {
|
|
211
|
-
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 || !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))
|
|
212
252
|
throw new Error("Persisted launch snapshot is invalid");
|
|
213
253
|
if (snapshot.identityVersion !== undefined)
|
|
214
254
|
positiveInteger(snapshot.identityVersion, "Persisted snapshot identity version");
|
|
@@ -254,7 +294,7 @@ async function validateSystemPrompts(store) {
|
|
|
254
294
|
await store.systemPrompts();
|
|
255
295
|
return;
|
|
256
296
|
}
|
|
257
|
-
if (!object(value) || value.version !== 1 || !
|
|
297
|
+
if (!object(value) || value.version !== 1 || !isList(value.entries))
|
|
258
298
|
throw new Error("Persisted system prompts are invalid");
|
|
259
299
|
for (const [index, entry] of value.entries.entries()) {
|
|
260
300
|
const label = `system-prompts.entries[${String(index)}]`;
|
|
@@ -276,6 +316,13 @@ async function validateRunDirectory(store) {
|
|
|
276
316
|
throw new Error(`Run artifact is not a regular file: ${join(store.directory, entry.name)}`);
|
|
277
317
|
}
|
|
278
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
|
+
}
|
|
279
326
|
async function validateRunArtifacts(store, workflowScript, state) {
|
|
280
327
|
await validateRunDirectory(store);
|
|
281
328
|
for (const name of REQUIRED_RUN_FILES)
|
|
@@ -283,7 +330,7 @@ async function validateRunArtifacts(store, workflowScript, state) {
|
|
|
283
330
|
if (await readFile(join(store.directory, "workflow.js"), "utf8") !== workflowScript)
|
|
284
331
|
throw new Error("Persisted workflow source does not match its launch snapshot");
|
|
285
332
|
await validateSystemPrompts(store);
|
|
286
|
-
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"))
|
|
287
334
|
return undefined; throw error; });
|
|
288
335
|
if (result === undefined && state === "completed")
|
|
289
336
|
throw new Error("Completed run result is missing");
|
|
@@ -291,21 +338,17 @@ async function validateRunArtifacts(store, workflowScript, state) {
|
|
|
291
338
|
throw new Error("Persisted workflow result is invalid");
|
|
292
339
|
validateJournal(await jsonFile(join(store.directory, "journal.json")));
|
|
293
340
|
const rawOwnership = await jsonFile(join(store.directory, "ownership.json"));
|
|
294
|
-
if (!
|
|
341
|
+
if (!isList(rawOwnership))
|
|
295
342
|
throw new Error("Persisted ownership records are invalid");
|
|
296
|
-
const ownership = rawOwnership;
|
|
297
343
|
const ownershipIds = new Set();
|
|
298
|
-
|
|
344
|
+
const ownership = rawOwnership.map((record, index) => {
|
|
299
345
|
const label = `ownership[${String(index)}]`;
|
|
300
|
-
|
|
301
|
-
throw new Error(`${label} is invalid`);
|
|
346
|
+
validateOwnershipRecord(record, label, ownershipIds);
|
|
302
347
|
ownershipIds.add(record.id);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
validateScheduledOptions(record.options, `${label}.options`);
|
|
306
|
-
}
|
|
348
|
+
return record;
|
|
349
|
+
});
|
|
307
350
|
for (const record of ownership)
|
|
308
|
-
if (
|
|
351
|
+
if (record.parentId !== undefined && !ownershipIds.has(record.parentId))
|
|
309
352
|
throw new Error("Persisted ownership parent is missing");
|
|
310
353
|
await store.validateDeletionWorktrees();
|
|
311
354
|
const borrowed = await store.borrowedWorktrees();
|
|
@@ -443,7 +486,7 @@ function planSession(scan, cutoffMs) {
|
|
|
443
486
|
}
|
|
444
487
|
return { candidates: scan.runs.filter(({ runId }) => oldTerminal.has(runId) && !protectedRuns.has(runId)), skipped };
|
|
445
488
|
}
|
|
446
|
-
function deletionOrder(
|
|
489
|
+
function deletionOrder(_scan, candidates) {
|
|
447
490
|
const candidateIds = new Set(candidates.map(({ runId }) => runId));
|
|
448
491
|
const remaining = new Set(candidateIds);
|
|
449
492
|
const ordered = [];
|
|
@@ -463,7 +506,7 @@ async function storedSessionIds(cwd, home) {
|
|
|
463
506
|
entries = await sessionEntries(path);
|
|
464
507
|
}
|
|
465
508
|
catch (error) {
|
|
466
|
-
if (error
|
|
509
|
+
if (isNodeError(error, "ENOENT"))
|
|
467
510
|
return [];
|
|
468
511
|
throw error;
|
|
469
512
|
}
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -27,8 +27,42 @@ export interface DoctorTrust {
|
|
|
27
27
|
trusted: boolean;
|
|
28
28
|
source: string;
|
|
29
29
|
}
|
|
30
|
+
export interface DoctorRoleInspection {
|
|
31
|
+
role: string;
|
|
32
|
+
path: string;
|
|
33
|
+
model: {
|
|
34
|
+
provider: string;
|
|
35
|
+
model: string;
|
|
36
|
+
thinking?: string;
|
|
37
|
+
inherited?: boolean;
|
|
38
|
+
};
|
|
39
|
+
tools: readonly string[];
|
|
40
|
+
resources: {
|
|
41
|
+
skills: readonly string[];
|
|
42
|
+
extensions: readonly string[];
|
|
43
|
+
excludedSkills: readonly string[];
|
|
44
|
+
excludedExtensions: readonly string[];
|
|
45
|
+
unmatchedSkills: readonly string[];
|
|
46
|
+
unmatchedExtensions: readonly string[];
|
|
47
|
+
};
|
|
48
|
+
systemPrompt: {
|
|
49
|
+
probe: string;
|
|
50
|
+
expandedProbe: string;
|
|
51
|
+
text: string;
|
|
52
|
+
source?: string;
|
|
53
|
+
};
|
|
54
|
+
setup: {
|
|
55
|
+
hooks: readonly string[];
|
|
56
|
+
diagnostics: readonly DoctorDiagnostic[];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
30
59
|
export interface DoctorPiState {
|
|
31
60
|
trust: DoctorTrust;
|
|
61
|
+
model?: {
|
|
62
|
+
provider: string;
|
|
63
|
+
model: string;
|
|
64
|
+
thinking?: string;
|
|
65
|
+
};
|
|
32
66
|
activeTools: readonly string[];
|
|
33
67
|
knownModels: readonly string[];
|
|
34
68
|
availableModels: readonly string[];
|
|
@@ -48,16 +82,22 @@ export interface DoctorReport {
|
|
|
48
82
|
settingsSources: WorkflowSettingsSources;
|
|
49
83
|
trust: DoctorTrust;
|
|
50
84
|
activeTools: readonly string[];
|
|
85
|
+
piExtensions: readonly string[];
|
|
86
|
+
piSkills: readonly string[];
|
|
51
87
|
roles: readonly DoctorRole[];
|
|
52
88
|
functions: readonly DoctorFunction[];
|
|
53
89
|
resourcePolicy: AgentResourcePolicy;
|
|
54
90
|
modelAliases: readonly WorkflowCatalogModelAlias[];
|
|
91
|
+
roleTarget?: string;
|
|
92
|
+
roleInspection?: DoctorRoleInspection;
|
|
55
93
|
diagnostics: readonly DoctorDiagnostic[];
|
|
56
94
|
}
|
|
57
95
|
export interface DoctorOptions {
|
|
58
96
|
cwd?: string;
|
|
59
97
|
agentDir?: string;
|
|
60
98
|
settingsPath?: string;
|
|
99
|
+
role?: string;
|
|
100
|
+
prompt?: string;
|
|
61
101
|
discoverPi?: (cwd: string, agentDir: string) => Promise<DoctorPiState>;
|
|
62
102
|
activeTools?: readonly string[];
|
|
63
103
|
registry?: WorkflowRegistryApi;
|
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, loadSettings, 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)) {
|
|
@@ -86,6 +93,7 @@ async function discoverPi(cwd, agentDir) {
|
|
|
86
93
|
const skills = services.resourceLoader.getSkills().skills;
|
|
87
94
|
return {
|
|
88
95
|
trust: { required, trusted, source },
|
|
96
|
+
model: { provider: model.provider, model: model.id, thinking: session.thinkingLevel },
|
|
89
97
|
activeTools,
|
|
90
98
|
knownModels: allModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
91
99
|
availableModels: availableModels.map(({ provider, id }) => `${provider}/${id}`),
|
|
@@ -105,12 +113,28 @@ async function discoverPi(cwd, agentDir) {
|
|
|
105
113
|
process.env.PI_OFFLINE = previousOffline;
|
|
106
114
|
}
|
|
107
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
|
+
}
|
|
108
132
|
function roleFiles(dir) {
|
|
109
133
|
try {
|
|
110
|
-
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();
|
|
111
135
|
}
|
|
112
136
|
catch (error) {
|
|
113
|
-
if (error
|
|
137
|
+
if (isNodeError(error, "ENOENT"))
|
|
114
138
|
return [];
|
|
115
139
|
throw error;
|
|
116
140
|
}
|
|
@@ -127,7 +151,7 @@ function scanExtensionRoleFiles(registrations) {
|
|
|
127
151
|
for (const registration of registrations) {
|
|
128
152
|
try {
|
|
129
153
|
const entries = readdirSync(registration.path, { withFileTypes: true });
|
|
130
|
-
const roleFiles = entries.filter((entry) =>
|
|
154
|
+
const roleFiles = entries.filter((entry) => isRoleFile(registration.path, entry));
|
|
131
155
|
if (!roleFiles.length)
|
|
132
156
|
empty.push(registration);
|
|
133
157
|
for (const entry of roleFiles)
|
|
@@ -160,7 +184,8 @@ function validateModel(value, known, available, source, diagnostics, aliases, dy
|
|
|
160
184
|
diagnostics.push(diagnostic("warning", "MODEL_UNAVAILABLE", `Model is valid-shaped but unavailable: ${name}`, source));
|
|
161
185
|
}
|
|
162
186
|
catch (error) {
|
|
163
|
-
|
|
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."));
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
|
|
@@ -184,7 +209,7 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
|
|
|
184
209
|
validateModel(definition.model, knownModels, availableModels, path, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
185
210
|
for (const tool of definition.tools ?? [])
|
|
186
211
|
if (!activeTools.has(tool))
|
|
187
|
-
diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under
|
|
212
|
+
diagnostics.push(diagnostic("error", "ROLE_TOOL_INACTIVE", `Tool is unknown or inactive: ${tool}`, path, "Use a tool listed under Pi active tools or enable its Pi extension."));
|
|
188
213
|
return definition;
|
|
189
214
|
}
|
|
190
215
|
function matchResourcePolicy(policy, pi) {
|
|
@@ -192,6 +217,60 @@ function matchResourcePolicy(policy, pi) {
|
|
|
192
217
|
const skills = [...new Set(pi.skills ?? [])];
|
|
193
218
|
return { ...policy, excludedSkills: disabledResources(policy.effective.skills, skills), excludedExtensions: disabledResources(policy.effective.extensions, extensions), unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions) };
|
|
194
219
|
}
|
|
220
|
+
async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, prompt, hooks, diagnostics) {
|
|
221
|
+
const setupDiagnostics = [];
|
|
222
|
+
const signal = new AbortController().signal;
|
|
223
|
+
const transport = { id: "doctor-local", createSession: async () => { throw new Error("Doctor inspection does not create transport sessions"); } };
|
|
224
|
+
const run = { cwd, sessionId: "doctor", runId: "doctor", workflow: { name: "doctor" }, args: null, signal };
|
|
225
|
+
const root = { cwd, model: { ...rootModel }, tools: new Set(activeTools), agentDefinitions: { [roleName]: definition }, agentDir, modelAliases: aliases, knownModels, availableModels, settingsPath, agentSetupHooks: hooks, agentResourcePolicy: () => structuredClone(basePolicy), runContext: run };
|
|
226
|
+
const options = { label: roleName, workflowName: "doctor", role: roleName };
|
|
227
|
+
let prepared;
|
|
228
|
+
try {
|
|
229
|
+
prepared = await prepareAgentSetupForInspection(root, prompt, options, transport);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", error instanceof Error ? error.message : String(error), rolePath));
|
|
233
|
+
diagnostics.push(...setupDiagnostics);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
if (prepared.failure) {
|
|
237
|
+
const error = prepared.failure.error;
|
|
238
|
+
const code = prepared.failure.hook ? "ROLE_SETUP_HOOK" : "ROLE_INSPECTION";
|
|
239
|
+
setupDiagnostics.push(diagnostic("error", code, `${prepared.failure.hook ? `Role setup hook ${prepared.failure.hook} failed: ` : ""}${error instanceof Error ? error.message : String(error)}`, prepared.failure.hook ?? rolePath));
|
|
240
|
+
diagnostics.push(...setupDiagnostics);
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
const session = await (async () => { try {
|
|
244
|
+
return await createLocalPiSession({ ...prepared.setup.sessionInput, sessionManager: SessionManager.inMemory() });
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", error instanceof Error ? error.message : String(error), rolePath));
|
|
248
|
+
return undefined;
|
|
249
|
+
} })();
|
|
250
|
+
if (!session) {
|
|
251
|
+
diagnostics.push(...setupDiagnostics);
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const promptResult = await session.preparePrompt(prompt);
|
|
256
|
+
const resources = session.getResourceInspection();
|
|
257
|
+
const state = session.agent?.state;
|
|
258
|
+
const inherited = prepared.setup.sessionInput.model.provider === rootModel.provider && prepared.setup.sessionInput.model.model === rootModel.model && prepared.setup.sessionInput.model.thinking === rootModel.thinking;
|
|
259
|
+
const actualModel = session.model?.provider && (session.model.model ?? session.model.id) ? { provider: session.model.provider, model: session.model.model ?? session.model.id ?? prepared.setup.sessionInput.model.model, ...(session.thinkingLevel ? { thinking: session.thinkingLevel } : {}), ...(inherited ? { inherited: true } : {}) } : { ...prepared.setup.sessionInput.model, ...(inherited ? { inherited: true } : {}) };
|
|
260
|
+
const policy = prepared.setup.sessionInput.resourcePolicy ?? basePolicy;
|
|
261
|
+
for (const item of [...resources.diagnostics, ...promptResult.diagnostics])
|
|
262
|
+
setupDiagnostics.push(diagnostic(item.type === "error" ? "error" : "warning", "ROLE_INSPECTION", item.message, item.source));
|
|
263
|
+
return { role: roleName, path: rolePath, model: actualModel, tools: state?.tools.map(({ name }) => name) ?? [...prepared.setup.sessionInput.tools], resources: { skills: resources.skills, extensions: resources.extensions, excludedSkills: policy.excludedSkills ?? [], excludedExtensions: policy.excludedExtensions ?? [], unmatchedSkills: policy.unmatchedSkills, unmatchedExtensions: policy.unmatchedExtensions }, systemPrompt: { probe: prompt, expandedProbe: promptResult.expandedPrompt, text: promptResult.systemPrompt, ...(resources.systemPromptSource ? { source: resources.systemPromptSource } : {}) }, setup: { hooks: prepared.summary.hookNames, diagnostics: setupDiagnostics } };
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
setupDiagnostics.push(diagnostic("error", "ROLE_INSPECTION", error instanceof Error ? error.message : String(error), rolePath));
|
|
267
|
+
diagnostics.push(...setupDiagnostics);
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
await session.dispose();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
195
274
|
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
196
275
|
export async function doctor(options = {}) {
|
|
197
276
|
const cwd = canonical(options.cwd ?? process.cwd());
|
|
@@ -203,7 +282,7 @@ export async function doctor(options = {}) {
|
|
|
203
282
|
settings = loadSettings(settingsPath);
|
|
204
283
|
}
|
|
205
284
|
catch (error) {
|
|
206
|
-
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."));
|
|
207
286
|
}
|
|
208
287
|
let settingsSources = { concurrency: settingsPath, modelAliases: settingsPath, disabledAgentResources: settingsPath };
|
|
209
288
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
@@ -212,7 +291,7 @@ export async function doctor(options = {}) {
|
|
|
212
291
|
pi = await (options.discoverPi ?? discoverPi)(cwd, agentDir);
|
|
213
292
|
}
|
|
214
293
|
catch (error) {
|
|
215
|
-
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."));
|
|
216
295
|
pi = { trust: { required: false, trusted: false, source: "discovery failed" }, activeTools: [], knownModels: [], availableModels: [], extensionErrors: [], functions: {} };
|
|
217
296
|
}
|
|
218
297
|
if (options.activeTools)
|
|
@@ -256,7 +335,7 @@ export async function doctor(options = {}) {
|
|
|
256
335
|
const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
|
|
257
336
|
const modelAliases = [
|
|
258
337
|
...Object.keys(aliases).map((name) => ({ name, kind: "static", provenance: settingsSources.modelAliases })),
|
|
259
|
-
...registeredModelAliases.map(({ name, version, headline
|
|
338
|
+
...registeredModelAliases.map(({ name, version, headline }) => ({ name, kind: "dynamic", provenance: `extension: ${headline}`, version, headline })),
|
|
260
339
|
].sort((left, right) => left.name.localeCompare(right.name) || left.kind.localeCompare(right.kind));
|
|
261
340
|
const roles = [];
|
|
262
341
|
const definitions = new Map();
|
|
@@ -335,6 +414,40 @@ export async function doctor(options = {}) {
|
|
|
335
414
|
else
|
|
336
415
|
definitions.delete(name);
|
|
337
416
|
}
|
|
417
|
+
let roleInspection;
|
|
418
|
+
if (options.role !== undefined) {
|
|
419
|
+
const activeRole = roles.find(({ name, active }) => name === options.role && active);
|
|
420
|
+
const definition = activeRole ? definitions.get(options.role) : undefined;
|
|
421
|
+
if (!activeRole || !definition)
|
|
422
|
+
diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
|
|
423
|
+
else {
|
|
424
|
+
const rootReference = pi.model ? `${pi.model.provider}/${pi.model.model}` : pi.availableModels[0] ?? pi.knownModels[0];
|
|
425
|
+
if (!rootReference)
|
|
426
|
+
diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", "Cannot inspect a role because Pi has no registered model"));
|
|
427
|
+
else {
|
|
428
|
+
let rootModel;
|
|
429
|
+
try {
|
|
430
|
+
if (pi.model) {
|
|
431
|
+
const thinking = parseThinking(pi.model.thinking);
|
|
432
|
+
rootModel = { provider: pi.model.provider, model: pi.model.model, ...(thinking ? { thinking } : {}) };
|
|
433
|
+
}
|
|
434
|
+
else
|
|
435
|
+
rootModel = resolveModelReference(rootReference, aliases, knownModels, settingsPath);
|
|
436
|
+
let roleAliases = aliases;
|
|
437
|
+
if (definition.model && isDynamicModelAlias(definition.model, dynamicAliases)) {
|
|
438
|
+
const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
|
|
439
|
+
roleAliases = { ...aliases, ...dynamic };
|
|
440
|
+
}
|
|
441
|
+
roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
|
|
442
|
+
if (roleInspection)
|
|
443
|
+
diagnostics.push(...roleInspection.setup.diagnostics);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", error instanceof Error ? error.message : String(error), activeRole.path));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
338
451
|
const functions = [];
|
|
339
452
|
for (const [name, fn] of Object.entries(pi.functions).sort(([left], [right]) => left.localeCompare(right))) {
|
|
340
453
|
functions.push({ name, description: fn.description, valid: true });
|
|
@@ -342,11 +455,55 @@ export async function doctor(options = {}) {
|
|
|
342
455
|
const severityOrder = { error: 0, warning: 1 };
|
|
343
456
|
diagnostics.sort((left, right) => severityOrder[left.severity] - severityOrder[right.severity] || (left.source ?? "").localeCompare(right.source ?? "") || left.code.localeCompare(right.code) || left.message.localeCompare(right.message));
|
|
344
457
|
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
345
|
-
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), roles, functions, modelAliases, resourcePolicy, diagnostics };
|
|
458
|
+
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(canonical))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
|
|
346
459
|
}
|
|
347
460
|
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
348
461
|
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
462
|
+
function exposedValues(values, excluded) {
|
|
463
|
+
const disabled = new Set(excluded);
|
|
464
|
+
return values.filter((value) => !disabled.has(value));
|
|
465
|
+
}
|
|
466
|
+
function nestedValues(label, values) {
|
|
467
|
+
return [`- ${label}:`, ...(values.length ? values.map((value) => ` - \`${value}\``) : [" - (none)"])];
|
|
468
|
+
}
|
|
469
|
+
function roleInspectionLines(inspection) {
|
|
470
|
+
return [
|
|
471
|
+
`- Role: \`${inspection.role}\` - \`${inspection.path}\``,
|
|
472
|
+
`- Model: \`${inspection.model.provider}/${inspection.model.model}\` (${inspection.model.inherited ? "inherited, " : ""}${inspection.model.thinking ?? "off"})`,
|
|
473
|
+
...nestedValues("Tools", inspection.tools),
|
|
474
|
+
...nestedValues("Effective skills", inspection.resources.skills),
|
|
475
|
+
...nestedValues("Effective extensions", inspection.resources.extensions),
|
|
476
|
+
...nestedValues("Excluded skills", inspection.resources.excludedSkills),
|
|
477
|
+
...nestedValues("Excluded extensions", inspection.resources.excludedExtensions),
|
|
478
|
+
...nestedValues("Unmatched skills", inspection.resources.unmatchedSkills),
|
|
479
|
+
...nestedValues("Unmatched extensions", inspection.resources.unmatchedExtensions),
|
|
480
|
+
`- Prompt probe: ${inspection.systemPrompt.probe ? JSON.stringify(inspection.systemPrompt.probe) : "empty"}`,
|
|
481
|
+
`- Expanded probe: ${JSON.stringify(inspection.systemPrompt.expandedProbe)}`,
|
|
482
|
+
`- System prompt source: ${inspection.systemPrompt.source ?? "(none)"}`,
|
|
483
|
+
"### Final system prompt",
|
|
484
|
+
"```",
|
|
485
|
+
inspection.systemPrompt.text,
|
|
486
|
+
"```",
|
|
487
|
+
...nestedValues("Applied setup hooks", inspection.setup.hooks),
|
|
488
|
+
`- Setup diagnostics: ${String(inspection.setup.diagnostics.length)}`,
|
|
489
|
+
];
|
|
490
|
+
}
|
|
349
491
|
export function formatDoctorReport(report) {
|
|
492
|
+
if (report.roleInspection || report.roleTarget !== undefined) {
|
|
493
|
+
const lines = [
|
|
494
|
+
"# pi-extensible-workflows doctor",
|
|
495
|
+
"",
|
|
496
|
+
"## Role inspection",
|
|
497
|
+
...(report.roleInspection ? roleInspectionLines(report.roleInspection) : [`- Role: \`${report.roleTarget ?? "(unknown)"}\``, "- Inspection unavailable"]),
|
|
498
|
+
"",
|
|
499
|
+
"## Diagnostics",
|
|
500
|
+
...(report.diagnostics.length ? report.diagnostics.map((item) => `- [${item.severity}] ${item.code}${item.source ? ` \`${item.source}\`` : ""}: ${item.message}${item.hint ? ` Fix: ${item.hint}` : ""}`) : ["- [ok] No diagnostics"]),
|
|
501
|
+
"",
|
|
502
|
+
"## Summary",
|
|
503
|
+
`- ${String(count(report, "error"))} error(s), ${String(count(report, "warning"))} warning(s)`,
|
|
504
|
+
];
|
|
505
|
+
return `${lines.join("\n")}\n`;
|
|
506
|
+
}
|
|
350
507
|
const lines = [
|
|
351
508
|
"# pi-extensible-workflows doctor",
|
|
352
509
|
"",
|
|
@@ -361,7 +518,16 @@ export function formatDoctorReport(report) {
|
|
|
361
518
|
"## Trust/resources",
|
|
362
519
|
`- [${report.trust.trusted ? "ok" : "warning"}] ${report.trust.source}`,
|
|
363
520
|
"",
|
|
364
|
-
"##
|
|
521
|
+
"## Pi active tools",
|
|
522
|
+
...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
|
|
523
|
+
"",
|
|
524
|
+
"## Pi active extensions",
|
|
525
|
+
...(report.piExtensions.length ? report.piExtensions.map((extension) => `- \`${extension}\``) : ["- None resolved"]),
|
|
526
|
+
"",
|
|
527
|
+
"## Pi active skills",
|
|
528
|
+
...(report.piSkills.length ? report.piSkills.map((skill) => `- \`${skill}\``) : ["- None resolved"]),
|
|
529
|
+
"",
|
|
530
|
+
"## Workflow agent resources (after disabledAgentResources)",
|
|
365
531
|
`- Global settings: \`${report.resourcePolicy.globalSettingsPath}\``,
|
|
366
532
|
`- Global skills: ${report.resourcePolicy.global.skills.join(", ") || "(none)"}`,
|
|
367
533
|
`- Global extensions: ${report.resourcePolicy.global.extensions.join(", ") || "(none)"}`,
|
|
@@ -370,14 +536,13 @@ export function formatDoctorReport(report) {
|
|
|
370
536
|
`- Project extensions: ${report.resourcePolicy.project.extensions.join(", ") || "(none)"}`,
|
|
371
537
|
`- Effective skills: ${report.resourcePolicy.effective.skills.join(", ") || "(none)"}`,
|
|
372
538
|
`- Effective extensions: ${report.resourcePolicy.effective.extensions.join(", ") || "(none)"}`,
|
|
373
|
-
|
|
374
|
-
|
|
539
|
+
...nestedValues("Exposed skills", exposedValues(report.piSkills, report.resourcePolicy.excludedSkills ?? [])),
|
|
540
|
+
...nestedValues("Exposed extensions", exposedValues(report.piExtensions, report.resourcePolicy.excludedExtensions ?? [])),
|
|
541
|
+
...nestedValues("Disabled skills", report.resourcePolicy.excludedSkills ?? []),
|
|
542
|
+
...nestedValues("Disabled extensions", report.resourcePolicy.excludedExtensions ?? []),
|
|
375
543
|
`- Unmatched skills: ${report.resourcePolicy.unmatchedSkills.join(", ") || "(none)"}`,
|
|
376
544
|
`- Unmatched extensions: ${report.resourcePolicy.unmatchedExtensions.join(", ") || "(none)"}`,
|
|
377
545
|
"",
|
|
378
|
-
"## Active tools",
|
|
379
|
-
...(report.activeTools.length ? report.activeTools.map((tool) => `- \`${tool}\``) : ["- None resolved"]),
|
|
380
|
-
"",
|
|
381
546
|
"## Roles",
|
|
382
547
|
...(report.roles.length ? report.roles.map((role) => `- \`${role.name}\` (${role.scope}, ${role.active ? "active" : role.overriddenBy ? `overridden by ${role.overriddenBy}` : "inactive: project untrusted"}) - \`${role.path}\`${role.extension ? `; ${extensionLabel(role.extension)} role directory "${dirname(role.path)}"` : ""}${role.overrides ? `; overrides \`${role.overrides}\`` : ""}`) : ["- None found"]),
|
|
383
548
|
"",
|
|
@@ -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.2.0",
|
|
4
4
|
"description": "CLI for pi-extensible-workflows",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"pi-extensible-workflows": "5.
|
|
48
|
+
"pi-extensible-workflows": "5.2.0"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|