@piewf/cli 5.13.1 → 5.14.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/cli.js +7 -13
- package/dist/src/doctor-cleanup.js +13 -20
- package/dist/src/doctor.js +34 -24
- package/dist/src/session-inspector.js +8 -20
- package/package.json +2 -2
package/dist/src/cli.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { chmodSync, linkSync, mkdirSync, mkdtempSync,
|
|
3
|
+
import { chmodSync, linkSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { basename, dirname, extname, join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { ProjectTrustStore, SessionManager, SettingsManager, createAgentSessionFromServices, createAgentSessionServices, getAgentDir, hasTrustRequiringProjectResources } from "@earendil-works/pi-coding-agent";
|
|
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, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, registeredWorkflowFunctionSources, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
|
|
11
|
+
import workflowExtension, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, sameFilesystemPath, 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
14
|
import { isPersistedRun, listPersistedSessionIds, listRunIds } from "pi-extensible-workflows/persistence";
|
|
15
15
|
import { shareTrajectoryRun } from "pi-extensible-workflows/trajectory";
|
|
16
|
-
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
17
16
|
function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
|
|
18
17
|
function requiredArg(args, index) {
|
|
19
18
|
const value = args[index];
|
|
@@ -97,7 +96,7 @@ function coerce(raw, type, schema) {
|
|
|
97
96
|
}
|
|
98
97
|
else if (type === "number") {
|
|
99
98
|
value = Number(raw);
|
|
100
|
-
if (!Number.isFinite(value))
|
|
99
|
+
if (!raw.trim() || !Number.isFinite(value))
|
|
101
100
|
throw new Error(`Invalid number: ${raw}`);
|
|
102
101
|
}
|
|
103
102
|
else {
|
|
@@ -717,13 +716,8 @@ async function exportWorkflowCli(rawArgs, options) {
|
|
|
717
716
|
writeLauncher(destination, workflowName, force);
|
|
718
717
|
if (!output) {
|
|
719
718
|
const binDir = join(homedir(), ".local", "bin");
|
|
720
|
-
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean)
|
|
721
|
-
|
|
722
|
-
}
|
|
723
|
-
catch {
|
|
724
|
-
return entry;
|
|
725
|
-
} });
|
|
726
|
-
if (!pathEntries.includes(binDir))
|
|
719
|
+
const pathEntries = (process.env.PATH ?? "").split(":").filter(Boolean);
|
|
720
|
+
if (!pathEntries.some((entry) => sameFilesystemPath(entry, binDir)))
|
|
727
721
|
options.stderr(`Warning: ${binDir} is not in PATH\n`);
|
|
728
722
|
}
|
|
729
723
|
options.write(`Exported ${destination}\n`);
|
|
@@ -910,7 +904,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
|
|
|
910
904
|
write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json] | inspect [session-id] [--json|--summary] [--failed] | transcript <session-file> | share <run-id> | bundle <workflow-name> [--name <command>] [--output <path>] [--force] | run <workflow-name> [workflow arguments] | run --script <path> [--name <workflow-name>] [--input <json>] | export <workflow-name> [--name <command>] [--output <path>] [--force] [--bundle]\n");
|
|
911
905
|
return 1;
|
|
912
906
|
}
|
|
913
|
-
if (process.argv[1] && import.meta.url
|
|
907
|
+
if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
|
|
914
908
|
const controller = new AbortController();
|
|
915
909
|
const onSignal = () => { controller.abort(); };
|
|
916
910
|
process.once("SIGINT", onSignal);
|
|
@@ -2,27 +2,20 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import { validateBudget } from "pi-extensible-workflows";
|
|
6
|
-
import { RUN_STATES } from "pi-extensible-workflows";
|
|
7
|
-
import { errorText, isNodeError, jsonValue, validateModelAliases } from "pi-extensible-workflows";
|
|
8
|
-
import { validateSchema } from "pi-extensible-workflows";
|
|
5
|
+
import { AGENT_STATES as CORE_AGENT_STATES, BUDGET_DIMENSIONS as CORE_BUDGET_DIMENSIONS, BUDGET_EVENT_TYPES as CORE_BUDGET_EVENT_TYPES, HARD_TERMINAL_RUN_STATES, RUN_STATES, errorText, isNodeError, isThinkingLevel, jsonValue, object, validateBudget, validateModelAliases, validateSchema } from "pi-extensible-workflows";
|
|
9
6
|
import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, RunStore } from "pi-extensible-workflows/persistence";
|
|
10
|
-
const TERMINAL_STATES = new Set(["completed", "failed", "stopped"]);
|
|
11
7
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
12
8
|
const REQUIRED_RUN_FILES = ["workflow.js", "state.json", "snapshot.json", "journal.json", "ownership.json", "worktrees.json", "borrowed-worktrees.json", "system-prompts.json"];
|
|
13
9
|
const OPTIONAL_RUN_FILES = new Set(["result.json", "summary.json"]);
|
|
14
10
|
const RUN_DIRECTORIES = new Set(["worktrees", ".system-prompts"]);
|
|
15
11
|
const RUN_FILES = new Set([...REQUIRED_RUN_FILES, ...OPTIONAL_RUN_FILES]);
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const BUDGET_DIMENSIONS = new Set(
|
|
19
|
-
const BUDGET_EVENT_TYPES = new Set(
|
|
20
|
-
function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
|
12
|
+
// String-keyed views of the core vocabularies: persisted files are read as unknown, so membership is checked on plain strings.
|
|
13
|
+
const AGENT_STATES = new Set(CORE_AGENT_STATES);
|
|
14
|
+
const BUDGET_DIMENSIONS = new Set(CORE_BUDGET_DIMENSIONS);
|
|
15
|
+
const BUDGET_EVENT_TYPES = new Set(CORE_BUDGET_EVENT_TYPES);
|
|
21
16
|
function isList(value) { return Array.isArray(value); }
|
|
22
|
-
function isThinking(value) { return ["off", "minimal", "low", "medium", "high", "xhigh", "max"].some((candidate) => candidate === value); }
|
|
23
17
|
function isRunState(value) { return RUN_STATES.some((candidate) => candidate === value); }
|
|
24
18
|
function isAgentState(value) { return typeof value === "string" && AGENT_STATES.has(value); }
|
|
25
|
-
function isSchedulerState(value) { return typeof value === "string" && SCHEDULER_STATES.has(value); }
|
|
26
19
|
function isToolCallState(value) { return ["running", "completed", "failed"].some((candidate) => candidate === value); }
|
|
27
20
|
function isActivityKind(value) { return ["reasoning", "tool", "text"].some((candidate) => candidate === value); }
|
|
28
21
|
function positiveDays(value) { if (!Number.isSafeInteger(value) || value < 1 || !Number.isFinite(value * DAY_MS))
|
|
@@ -42,7 +35,7 @@ function positiveInteger(value, label) { if (typeof value !== "number" || !Numbe
|
|
|
42
35
|
throw new Error(`${label} is invalid`); }
|
|
43
36
|
function finiteNumber(value, label) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
44
37
|
throw new Error(`${label} is invalid`); }
|
|
45
|
-
function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !
|
|
38
|
+
function model(value, label) { if (!object(value) || typeof value.provider !== "string" || !value.provider || typeof value.model !== "string" || !value.model || (value.thinking !== undefined && !isThinkingLevel(value.thinking)))
|
|
46
39
|
throw new Error(`${label} is invalid`); }
|
|
47
40
|
function accounting(value, label) { if (!object(value))
|
|
48
41
|
throw new Error(`${label} is invalid`); for (const key of ["input", "output", "cacheRead", "cacheWrite", "cost"])
|
|
@@ -54,7 +47,7 @@ function resourceSelectors(value, label) { if (value === undefined)
|
|
|
54
47
|
stringList(value.extensions, `${label}.extensions`); if (value.tools !== undefined)
|
|
55
48
|
stringList(value.tools, `${label}.tools`); }
|
|
56
49
|
function agentDefinition(value, label) { if (!object(value))
|
|
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 && !
|
|
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 && !isThinkingLevel(value.thinking))
|
|
58
51
|
throw new Error(`${label}.thinking is invalid`); if (value.tools !== undefined)
|
|
59
52
|
stringList(value.tools, `${label}.tools`); if (value.skills !== undefined)
|
|
60
53
|
stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
|
|
@@ -69,7 +62,7 @@ function validateScheduledOptions(value, label) { if (!object(value) || typeof v
|
|
|
69
62
|
throw new Error(`${label} is invalid`); optionalString(value.requestedLabel, `${label}.requestedLabel`); optionalString(value.parentBreadcrumb, `${label}.parentBreadcrumb`); stringList(value.tools, `${label}.tools`); if (value.skills !== undefined)
|
|
70
63
|
stringList(value.skills, `${label}.skills`); if (value.extensions !== undefined)
|
|
71
64
|
stringList(value.extensions, `${label}.extensions`); if (value.contextFiles !== undefined)
|
|
72
|
-
stringList(value.contextFiles, `${label}.contextFiles`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !
|
|
65
|
+
stringList(value.contextFiles, `${label}.contextFiles`); optionalString(value.worktreeOwner, `${label}.worktreeOwner`); optionalString(value.model, `${label}.model`); if (value.thinking !== undefined && !isThinkingLevel(value.thinking))
|
|
73
66
|
throw new Error(`${label}.thinking is invalid`); optionalRole(value.role, `${label}.role`); if (value.schema !== undefined)
|
|
74
67
|
validateSchema(value.schema, `${label}.schema`); if (value.retries !== undefined)
|
|
75
68
|
nonNegativeInteger(value.retries, `${label}.retries`); if (value.timeoutMs !== undefined && value.timeoutMs !== null)
|
|
@@ -146,7 +139,7 @@ function validateAgent(value, label) {
|
|
|
146
139
|
finiteNumber(value.lastEventAt, `${label}.lastEventAt`);
|
|
147
140
|
}
|
|
148
141
|
function validateUsage(value, label) { if (!object(value))
|
|
149
|
-
throw new Error(`${label} is invalid`); for (const key of
|
|
142
|
+
throw new Error(`${label} is invalid`); for (const key of BUDGET_DIMENSIONS)
|
|
150
143
|
finiteNumber(value[key], `${label}.${key}`); }
|
|
151
144
|
function validateBudgetEvents(value) { if (value === undefined)
|
|
152
145
|
return; if (!isList(value))
|
|
@@ -306,7 +299,7 @@ async function validateRunDirectory(store) {
|
|
|
306
299
|
}
|
|
307
300
|
}
|
|
308
301
|
function validateOwnershipRecord(value, label, ownershipIds) {
|
|
309
|
-
if (!object(value) || typeof value.id !== "string" || !value.id || ownershipIds.has(value.id) || typeof value.label !== "string" || !value.label || !
|
|
302
|
+
if (!object(value) || typeof value.id !== "string" || !value.id || ownershipIds.has(value.id) || typeof value.label !== "string" || !value.label || !isAgentState(value.state))
|
|
310
303
|
throw new Error(`${label} is invalid`);
|
|
311
304
|
optionalString(value.parentId, `${label}.parentId`);
|
|
312
305
|
optionalString(value.prompt, `${label}.prompt`);
|
|
@@ -443,7 +436,7 @@ async function recheckCandidate(entry, cutoffMs) {
|
|
|
443
436
|
return "State record disappeared before deletion";
|
|
444
437
|
const loaded = await entry.store.load().catch((error) => { throw new Error(`Candidate could not be reloaded: ${errorText(error)}`); });
|
|
445
438
|
validateRunRecord(loaded.run);
|
|
446
|
-
if (loaded.run.id !== entry.runId || loaded.run.state !== entry.run.state || !
|
|
439
|
+
if (loaded.run.id !== entry.runId || loaded.run.state !== entry.run.state || !HARD_TERMINAL_RUN_STATES.has(loaded.run.state))
|
|
447
440
|
return "Candidate state changed before deletion";
|
|
448
441
|
const after = await stat(join(entry.store.directory, "state.json"));
|
|
449
442
|
if (before.mtimeMs !== after.mtimeMs || after.mtimeMs !== entry.stateMtimeMs)
|
|
@@ -455,7 +448,7 @@ async function recheckCandidate(entry, cutoffMs) {
|
|
|
455
448
|
function planSession(scan, cutoffMs) {
|
|
456
449
|
if (scan.liveLease)
|
|
457
450
|
return { candidates: [], skipped: scan.runs.map((entry) => runItem(entry, "skipped", "Session has a live ownership lease")) };
|
|
458
|
-
const oldTerminal = new Set(scan.runs.filter(({ run, stateMtimeMs }) =>
|
|
451
|
+
const oldTerminal = new Set(scan.runs.filter(({ run, stateMtimeMs }) => HARD_TERMINAL_RUN_STATES.has(run.state) && stateMtimeMs < cutoffMs).map(({ runId }) => runId));
|
|
459
452
|
const protectedRuns = new Set();
|
|
460
453
|
const visit = (runId) => { if (protectedRuns.has(runId))
|
|
461
454
|
return; protectedRuns.add(runId); const entry = scan.runs.find(({ runId: current }) => current === runId); for (const dependency of entry?.dependencies ?? [])
|
|
@@ -466,7 +459,7 @@ function planSession(scan, cutoffMs) {
|
|
|
466
459
|
visit(dependency);
|
|
467
460
|
const skipped = [];
|
|
468
461
|
for (const entry of scan.runs) {
|
|
469
|
-
if (!
|
|
462
|
+
if (!HARD_TERMINAL_RUN_STATES.has(entry.run.state))
|
|
470
463
|
skipped.push(runItem(entry, "skipped", `Run state ${entry.run.state} is active or resumable`));
|
|
471
464
|
else if (entry.stateMtimeMs >= cutoffMs)
|
|
472
465
|
skipped.push(runItem(entry, "skipped", "State record is not older than the cutoff"));
|
package/dist/src/doctor.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { readFileSync, readdirSync,
|
|
2
|
-
import { basename, dirname, extname, join
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
3
3
|
import { InMemoryCredentialStore, InMemoryModelsStore } 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, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
|
|
5
|
+
import { DEFAULT_SETTINGS, canonicalPath, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, parseRoleMarkdown, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowRoleDirectories, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
|
|
6
6
|
import { loadingRegistry } from "pi-extensible-workflows";
|
|
7
|
-
import { selectResourcesByLayers, unmatchedResourcePatterns } from "pi-extensible-workflows";
|
|
7
|
+
import { selectResourcesByLayers, unmatchedResourcePatterns, mergeWorkflowExtensionSettings } from "pi-extensible-workflows";
|
|
8
8
|
const THINKING_HINT = "Use off, minimal, low, medium, high, xhigh, or max.";
|
|
9
9
|
const AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE = "https://github.com/vekexasia/pi-extensible-workflows/issues/205";
|
|
10
10
|
const AGENT_RESOURCE_SELECTOR_MIGRATION_MESSAGE = `\`disabledAgentResources\` is no longer supported by #205. Migrate to direct \`skills\`, \`extensions\`, and \`tools\` selectors: legacy patterns exclude resources and \`!pattern\` re-enables them, while new selectors include matches and \`!pattern\` excludes them. See ${AGENT_RESOURCE_SELECTOR_MIGRATION_ISSUE}`;
|
|
@@ -25,15 +25,6 @@ function usesLegacyRoleSelectors(path) {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
function canonical(path) {
|
|
29
|
-
const absolute = resolve(path);
|
|
30
|
-
try {
|
|
31
|
-
return realpathSync(absolute);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return absolute;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
28
|
function isDynamicModelAlias(value, aliases) {
|
|
38
29
|
const match = /^([^/\s:]+)(?::([^\s]+))?$/.exec(value);
|
|
39
30
|
const name = match?.[1];
|
|
@@ -72,7 +63,7 @@ function savedTrust(cwd, agentDir) {
|
|
|
72
63
|
}
|
|
73
64
|
if (!isObject(parsed))
|
|
74
65
|
throw new Error("Pi trust.json must be an object");
|
|
75
|
-
let current =
|
|
66
|
+
let current = canonicalPath(cwd);
|
|
76
67
|
while (current !== dirname(current)) {
|
|
77
68
|
const value = parsed[current];
|
|
78
69
|
if (value === true || value === false)
|
|
@@ -249,7 +240,7 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
|
|
|
249
240
|
return definition;
|
|
250
241
|
}
|
|
251
242
|
function matchResourcePolicy(policy, pi) {
|
|
252
|
-
const extensions = [...new Set((pi.extensions ?? []).map(
|
|
243
|
+
const extensions = [...new Set((pi.extensions ?? []).map(canonicalPath))];
|
|
253
244
|
const skills = [...new Set(pi.skills ?? [])];
|
|
254
245
|
const tools = [...new Set(pi.activeTools)];
|
|
255
246
|
const layers = policy.selectorSources;
|
|
@@ -258,12 +249,12 @@ function matchResourcePolicy(policy, pi) {
|
|
|
258
249
|
const selectedTools = selectResourcesByLayers([layers.global.tools, layers.project.tools], tools);
|
|
259
250
|
return { ...policy, selectedSkills, selectedExtensions, selectedTools, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, skills), unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, extensions), unmatchedTools: unmatchedResourcePatterns(policy.effective.tools ?? [], tools) };
|
|
260
251
|
}
|
|
261
|
-
async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, prompt, hooks, diagnostics) {
|
|
252
|
+
async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath, basePolicy, rootModel, activeTools, aliases, knownModels, availableModels, settingsPath, extensionSettings, prompt, hooks, diagnostics) {
|
|
262
253
|
const setupDiagnostics = [];
|
|
263
254
|
const signal = new AbortController().signal;
|
|
264
255
|
const transport = { id: "doctor-local", createSession: async () => { throw new Error("Doctor inspection does not create transport sessions"); } };
|
|
265
256
|
const run = { cwd, sessionId: "doctor", runId: "doctor", workflow: { name: "doctor" }, args: null, signal };
|
|
266
|
-
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 };
|
|
257
|
+
const root = { cwd, model: { ...rootModel }, tools: new Set(activeTools), agentDefinitions: { [roleName]: definition }, agentDir, extensionSettings, modelAliases: aliases, knownModels, availableModels, settingsPath, agentSetupHooks: hooks, agentResourcePolicy: () => structuredClone(basePolicy), runContext: run };
|
|
267
258
|
const options = { label: roleName, workflowName: "doctor", role: roleName };
|
|
268
259
|
let prepared;
|
|
269
260
|
try {
|
|
@@ -313,13 +304,22 @@ async function inspectRoleSession(cwd, agentDir, roleName, definition, rolePath,
|
|
|
313
304
|
}
|
|
314
305
|
}
|
|
315
306
|
function resourcePolicySource(settingsSource) { return settingsSource; }
|
|
307
|
+
function validateDoctorExtensionSettings(registry, value, source, cwd, projectTrusted, settingsPath, diagnostics, role) {
|
|
308
|
+
try {
|
|
309
|
+
registry.validateExtensionSettings(value, { source, cwd, projectTrusted, settingsPath, ...(role === undefined ? {} : { role }) });
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
diagnostics.push(diagnostic("error", "SETTINGS_INVALID", errorText(error), `${settingsPath}.extensionSettings`, "Fix the extension-owned settings reported in this error."));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
316
315
|
export async function doctor(options = {}) {
|
|
317
|
-
const cwd =
|
|
318
|
-
const agentDir =
|
|
319
|
-
const settingsPath =
|
|
316
|
+
const cwd = canonicalPath(options.cwd ?? process.cwd());
|
|
317
|
+
const agentDir = canonicalPath(options.agentDir ?? getAgentDir());
|
|
318
|
+
const settingsPath = canonicalPath(options.settingsPath ?? workflowSettingsPath(agentDir));
|
|
320
319
|
const projectSettingsPath = workflowProjectSettingsPath(cwd);
|
|
321
320
|
const legacyGlobalSettings = usesLegacySettings(settingsPath);
|
|
322
321
|
const diagnostics = [];
|
|
322
|
+
const registry = options.registry ?? loadingRegistry();
|
|
323
323
|
let settings = DEFAULT_SETTINGS;
|
|
324
324
|
try {
|
|
325
325
|
settings = loadSettings(settingsPath);
|
|
@@ -351,6 +351,11 @@ export async function doctor(options = {}) {
|
|
|
351
351
|
const resolved = resolveWorkflowSettings(cwd, pi.trust.trusted, settingsPath);
|
|
352
352
|
settings = resolved.effective;
|
|
353
353
|
settingsSources = resolved.sources;
|
|
354
|
+
if (resolved.global.extensionSettings !== undefined)
|
|
355
|
+
validateDoctorExtensionSettings(registry, resolved.global.extensionSettings, "global", cwd, pi.trust.trusted, resolved.globalSettingsPath, diagnostics);
|
|
356
|
+
if (pi.trust.trusted && resolved.project.extensionSettings !== undefined)
|
|
357
|
+
validateDoctorExtensionSettings(registry, resolved.project.extensionSettings, "project", cwd, true, resolved.projectSettingsPath, diagnostics);
|
|
358
|
+
validateDoctorExtensionSettings(registry, resolved.effective.extensionSettings, "effective", cwd, pi.trust.trusted, settingsSources.extensionSettings ?? settingsPath, diagnostics);
|
|
354
359
|
}
|
|
355
360
|
catch (error) {
|
|
356
361
|
const message = errorText(error);
|
|
@@ -384,7 +389,6 @@ export async function doctor(options = {}) {
|
|
|
384
389
|
const knownModels = new Set(pi.knownModels);
|
|
385
390
|
const availableModels = new Set(pi.availableModels);
|
|
386
391
|
const aliases = settings.modelAliases ?? {};
|
|
387
|
-
const registry = options.registry ?? loadingRegistry();
|
|
388
392
|
const registeredModelAliases = registry.modelAliases();
|
|
389
393
|
const dynamicAliases = new Set(registeredModelAliases.map(({ name }) => name).filter((name) => !Object.prototype.hasOwnProperty.call(aliases, name)));
|
|
390
394
|
const modelAliases = [
|
|
@@ -430,6 +434,8 @@ export async function doctor(options = {}) {
|
|
|
430
434
|
const overriddenBy = starterOverriddenBy.get(file.path);
|
|
431
435
|
roles.push({ name: file.name, path: file.path, scope: "extension", active: overriddenBy === undefined, extension: file.extension, ...(starterPath ? { overrides: starterPath } : {}), ...(overriddenBy ? { overriddenBy } : {}) });
|
|
432
436
|
const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
|
|
437
|
+
if (definition)
|
|
438
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, file.path, diagnostics, file.name);
|
|
433
439
|
if (duplicateExtensionNames.has(file.name))
|
|
434
440
|
continue;
|
|
435
441
|
if (extensionPaths.get(file.name) !== file.path)
|
|
@@ -452,6 +458,8 @@ export async function doctor(options = {}) {
|
|
|
452
458
|
}
|
|
453
459
|
}
|
|
454
460
|
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
461
|
+
if (definition)
|
|
462
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
|
|
455
463
|
if (definition)
|
|
456
464
|
definitions.set(name, definition);
|
|
457
465
|
else
|
|
@@ -481,6 +489,8 @@ export async function doctor(options = {}) {
|
|
|
481
489
|
}
|
|
482
490
|
}
|
|
483
491
|
const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
|
|
492
|
+
if (definition)
|
|
493
|
+
validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
|
|
484
494
|
if (definition)
|
|
485
495
|
definitions.set(name, definition);
|
|
486
496
|
else
|
|
@@ -513,7 +523,7 @@ export async function doctor(options = {}) {
|
|
|
513
523
|
const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
|
|
514
524
|
roleAliases = { ...aliases, ...dynamic };
|
|
515
525
|
}
|
|
516
|
-
roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
|
|
526
|
+
roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
|
|
517
527
|
if (roleInspection)
|
|
518
528
|
diagnostics.push(...roleInspection.setup.diagnostics);
|
|
519
529
|
}
|
|
@@ -530,7 +540,7 @@ export async function doctor(options = {}) {
|
|
|
530
540
|
const severityOrder = { error: 0, warning: 1 };
|
|
531
541
|
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));
|
|
532
542
|
roles.sort((left, right) => left.name.localeCompare(right.name) || left.scope.localeCompare(right.scope));
|
|
533
|
-
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(
|
|
543
|
+
return { cwd, agentDir, settingsPath, settings, settingsSources, trust: pi.trust, activeTools: [...activeTools].sort(), piExtensions: [...new Set((pi.extensions ?? []).map(canonicalPath))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
|
|
534
544
|
}
|
|
535
545
|
function count(report, severity) { return report.diagnostics.filter((item) => item.severity === severity).length; }
|
|
536
546
|
export function doctorExitCode(report) { return count(report, "error") > 0 ? 1 : 0; }
|
|
@@ -608,7 +618,7 @@ export function formatDoctorReport(report) {
|
|
|
608
618
|
`- Agent dir: \`${report.agentDir}\``,
|
|
609
619
|
`- Global workflow settings: \`${report.settingsPath}\``,
|
|
610
620
|
`- Project workflow settings: \`${report.resourcePolicy.projectSettingsPath}\` (${report.resourcePolicy.projectTrusted ? "trusted" : "ignored: project untrusted"})`,
|
|
611
|
-
`- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
|
|
621
|
+
`- Effective setting sources: concurrency=\`${report.settingsSources.concurrency}\`, modelAliases=\`${report.settingsSources.modelAliases}\`, skills=\`${report.settingsSources.skills ?? "(none)"}\`, extensions=\`${report.settingsSources.extensions ?? "(none)"}\`, extensionSettings=\`${report.settingsSources.extensionSettings ?? "(none)"}\`, tools=\`${report.settingsSources.tools ?? "(none)"}\``,
|
|
612
622
|
`- Limits: concurrency=${String(report.settings.concurrency)}`,
|
|
613
623
|
"",
|
|
614
624
|
"## Trust/resources",
|
|
@@ -4,7 +4,7 @@ import { emitKeypressEvents } from "node:readline";
|
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin, stdout } from "node:process";
|
|
6
6
|
import { highlightCode, initTheme, SessionManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { errorText, formatBudgetStatus, inspectWorkflowScript } from "pi-extensible-workflows";
|
|
7
|
+
import { errorText, formatBudgetStatus, inspectWorkflowScript, parseThinking } from "pi-extensible-workflows";
|
|
8
8
|
import { listPersistedSessionIds, listRunIds, RunStore } from "pi-extensible-workflows/persistence";
|
|
9
9
|
function text(content) {
|
|
10
10
|
if (typeof content === "string")
|
|
@@ -69,13 +69,6 @@ function mergedModels(groups) {
|
|
|
69
69
|
totals.set(item.model, (totals.get(item.model) ?? 0) + item.cost);
|
|
70
70
|
return [...totals].map(([model, cost]) => ({ model, cost })).sort((a, b) => b.cost - a.cost || a.model.localeCompare(b.model));
|
|
71
71
|
}
|
|
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
|
-
}
|
|
76
|
-
function parsedThinking(value) {
|
|
77
|
-
return isThinkingLevel(value) ? value : undefined;
|
|
78
|
-
}
|
|
79
72
|
function modelName(provider, model) {
|
|
80
73
|
return typeof provider === "string" && provider && typeof model === "string" && model ? `${provider}/${model}` : undefined;
|
|
81
74
|
}
|
|
@@ -93,7 +86,7 @@ function transcript(manager) {
|
|
|
93
86
|
continue;
|
|
94
87
|
}
|
|
95
88
|
if (entry.type === "thinking_level_change") {
|
|
96
|
-
thinking =
|
|
89
|
+
thinking = parseThinking(entry.thinkingLevel);
|
|
97
90
|
if (thinking === undefined)
|
|
98
91
|
throw new Error("Invalid thinking policy");
|
|
99
92
|
continue;
|
|
@@ -180,7 +173,7 @@ async function loadRuns(cwd, sessionId, home) {
|
|
|
180
173
|
}
|
|
181
174
|
return runs;
|
|
182
175
|
}
|
|
183
|
-
|
|
176
|
+
function agentReport(agent) {
|
|
184
177
|
const fallbackModel = `${agent.model.provider}/${agent.model.model}`;
|
|
185
178
|
const fallbackThinking = agent.model.thinking;
|
|
186
179
|
const attempts = [];
|
|
@@ -188,15 +181,10 @@ async function agentReport(agent) {
|
|
|
188
181
|
const setup = attempt.setup;
|
|
189
182
|
const path = attemptTranscriptPath(attempt);
|
|
190
183
|
const log = path ? readTranscript(path) : undefined;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
const model = `${setup.model.provider}/${setup.model.model}`;
|
|
198
|
-
const cost = attempt.accounting.cost;
|
|
199
|
-
attempts.push({ attempt: attempt.attempt, prompt: "(transcript unavailable)", model, ...(setup.model.thinking !== undefined ? { thinking: setup.model.thinking } : {}), cost, models: [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
|
|
184
|
+
const model = log?.model ?? `${setup.model.provider}/${setup.model.model}`;
|
|
185
|
+
const thinking = log ? log.thinking : setup.model.thinking;
|
|
186
|
+
const cost = log?.cost ?? attempt.accounting.cost;
|
|
187
|
+
attempts.push({ attempt: attempt.attempt, prompt: log?.prompt ?? "(transcript unavailable)", model, ...(thinking !== undefined ? { thinking } : {}), cost, models: log?.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
|
|
200
188
|
}
|
|
201
189
|
if (!attempts.length) {
|
|
202
190
|
const cost = agent.accounting?.cost ?? 0;
|
|
@@ -230,7 +218,7 @@ export async function loadSessionReport(path, home = homedir()) {
|
|
|
230
218
|
const runId = resultRunId(result);
|
|
231
219
|
const loaded = runId ? runs.get(runId) : undefined;
|
|
232
220
|
const args = call.arguments;
|
|
233
|
-
const agents = loaded ?
|
|
221
|
+
const agents = loaded ? loaded.run.agents.map(agentReport) : [];
|
|
234
222
|
const models = mergedModels(agents.flatMap(({ attempts }) => attempts.map(({ models: attemptModels }) => attemptModels)));
|
|
235
223
|
const name = loaded?.run.workflowName ?? (typeof args.name === "string" ? args.name : "workflow");
|
|
236
224
|
const description = typeof args.description === "string" ? args.description : loaded?.snapshot.metadata.description;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piewf/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"description": "CLI for pi-extensible-workflows",
|
|
5
5
|
"homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-coding-agent": "0.85.0",
|
|
44
44
|
"@earendil-works/pi-server": "0.85.0",
|
|
45
45
|
"@earendil-works/pi-tui": "0.85.0",
|
|
46
|
-
"pi-extensible-workflows": "5.
|
|
46
|
+
"pi-extensible-workflows": "5.14.0",
|
|
47
47
|
"typebox": "1.3.7"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|