@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 CHANGED
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
- import { chmodSync, linkSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
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 { pathToFileURL } from "node:url";
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).map((entry) => { try {
721
- return realpathSync(entry);
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 === pathToFileURL(realpathSync(process.argv[1])).href) {
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
- const AGENT_STATES = new Set(["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"]);
17
- const SCHEDULER_STATES = new Set(["queued", "running", "waiting_for_child", "paused", "retrying", "completed", "failed", "cancelled"]);
18
- const BUDGET_DIMENSIONS = new Set(["tokens", "costUsd", "durationMs", "agentLaunches"]);
19
- const BUDGET_EVENT_TYPES = new Set(["soft_crossed", "hard_overrun", "hard_exhausted", "adjustment_requested", "adjustment_approved", "adjustment_rejected"]);
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 && !isThinking(value.thinking)))
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 && !isThinking(value.thinking))
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 && !isThinking(value.thinking))
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 ["tokens", "costUsd", "durationMs", "agentLaunches"])
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 || !isSchedulerState(value.state))
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 || !TERMINAL_STATES.has(loaded.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 }) => TERMINAL_STATES.has(run.state) && stateMtimeMs < cutoffMs).map(({ runId }) => runId));
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 (!TERMINAL_STATES.has(entry.run.state))
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"));
@@ -1,10 +1,10 @@
1
- import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
- import { basename, dirname, extname, join, resolve } from "node:path";
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 = canonical(cwd);
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(canonical))];
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 = canonical(options.cwd ?? process.cwd());
318
- const agentDir = canonical(options.agentDir ?? getAgentDir());
319
- const settingsPath = canonical(options.settingsPath ?? workflowSettingsPath(agentDir));
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(canonical))].sort(), piSkills: [...new Set(pi.skills ?? [])].sort(), roles, functions, modelAliases, resourcePolicy, ...(options.role !== undefined ? { roleTarget: options.role } : {}), ...(roleInspection ? { roleInspection } : {}), diagnostics };
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 = parsedThinking(entry.thinkingLevel);
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
- async function agentReport(agent) {
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
- if (log) {
192
- const model = log.model ?? `${setup.model.provider}/${setup.model.model}`;
193
- const cost = log.cost;
194
- attempts.push({ attempt: attempt.attempt, prompt: log.prompt ?? "(transcript unavailable)", model, ...(log.thinking !== undefined ? { thinking: log.thinking } : {}), cost, models: log.models.length ? log.models : [{ model, cost }], ...(attempt.error ? { error: `${attempt.error.code}: ${attempt.error.message}` } : {}), setup });
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 ? await Promise.all(loaded.run.agents.map(agentReport)) : [];
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.13.1",
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.13.1",
46
+ "pi-extensible-workflows": "5.14.0",
47
47
  "typebox": "1.3.7"
48
48
  },
49
49
  "publishConfig": {