@piewf/cli 5.17.0 → 5.17.2

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/README.md CHANGED
@@ -26,7 +26,7 @@ Roles come from the bundled starter roles, `~/.pi/agent/pi-extensible-workflows/
26
26
  ## piewf
27
27
 
28
28
  ```sh
29
- piewf doctor [--role <role>] [--prompt <text>] [--json]
29
+ piewf doctor [role|role-file] [--role <role>] [--prompt <text>] [--json]
30
30
  piewf doctor cleanup [--older-than-days <days>] [--yes]
31
31
  piewf inspect [session-id] [--json|--summary] [--failed]
32
32
  piewf transcript <session-file>
package/dist/src/cli.js CHANGED
@@ -803,7 +803,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
803
803
  const stderr = options.stderr ?? ((text) => { process.stderr.write(text); });
804
804
  if (args[0] === "doctor" && args[1] !== "cleanup") {
805
805
  if (args.slice(1).some((arg) => arg === "--help" || arg === "-h")) {
806
- write("Usage: piewf doctor [role] [--role <role>] [--prompt <text>] [--json]\n");
806
+ write("Usage: piewf doctor [role|role-file] [--role <role>] [--prompt <text>] [--json]\n");
807
807
  return 0;
808
808
  }
809
809
  try {
@@ -902,7 +902,7 @@ export async function runCli(args, options = {}, write = (text) => { process.std
902
902
  return 1;
903
903
  }
904
904
  }
905
- 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");
905
+ write("Usage: piewf doctor [role|role-file] [--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");
906
906
  return 1;
907
907
  }
908
908
  if (process.argv[1] && sameFilesystemPath(fileURLToPath(import.meta.url), process.argv[1])) {
@@ -3,7 +3,7 @@ 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
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";
6
- import { acquireSessionLease, hasLiveSessionLease, projectSessionsDirectory, RunStore } from "pi-extensible-workflows/persistence";
6
+ import { acquireSessionLease, hasLiveSessionLease, json, projectSessionsDirectory, RunStore } from "pi-extensible-workflows/persistence";
7
7
  const DAY_MS = 24 * 60 * 60 * 1000;
8
8
  const REQUIRED_RUN_FILES = ["workflow.js", "state.json", "snapshot.json", "journal.json", "ownership.json", "worktrees.json", "borrowed-worktrees.json", "system-prompts.json"];
9
9
  const OPTIONAL_RUN_FILES = new Set(["result.json", "summary.json"]);
@@ -22,7 +22,6 @@ function positiveDays(value) { if (!Number.isSafeInteger(value) || value < 1 ||
22
22
  throw new Error("older-than-days must be a positive integer"); return value; }
23
23
  function runItem(entry, action, reason) { return { sessionId: entry.sessionId, runId: entry.runId, action, state: entry.run.state, stateMtimeMs: entry.stateMtimeMs, path: entry.store.directory, ...(reason ? { reason } : {}) }; }
24
24
  function sameNames(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); }
25
- async function jsonFile(path) { return JSON.parse(await readFile(path, "utf8")); }
26
25
  async function requiredFile(path) { const info = await lstat(path); if (!info.isFile())
27
26
  throw new Error(`Required artifact is not a regular file: ${path}`); }
28
27
  function stringList(value, label, nonEmpty = false) { if (!isList(value) || value.some((item) => typeof item !== "string" || (nonEmpty && !item)))
@@ -271,7 +270,7 @@ function validateJournal(value) { if (!object(value) || !object(value.completed)
271
270
  validateBudget(decision.proposed);
272
271
  } }
273
272
  async function validateSystemPrompts(store) {
274
- const value = await jsonFile(store.systemPromptPath());
273
+ const value = await json(store.systemPromptPath());
275
274
  if (object(value) && value.version === 2) {
276
275
  await store.systemPrompts();
277
276
  return;
@@ -312,14 +311,14 @@ async function validateRunArtifacts(store, workflowScript, state) {
312
311
  if (await readFile(join(store.directory, "workflow.js"), "utf8") !== workflowScript)
313
312
  throw new Error("Persisted workflow source does not match its launch snapshot");
314
313
  await validateSystemPrompts(store);
315
- const result = await jsonFile(join(store.directory, "result.json")).catch((error) => { if (isNodeError(error, "ENOENT"))
314
+ const result = await json(join(store.directory, "result.json")).catch((error) => { if (isNodeError(error, "ENOENT"))
316
315
  return undefined; throw error; });
317
316
  if (result === undefined && state === "completed")
318
317
  throw new Error("Completed run result is missing");
319
318
  if (result !== undefined && !jsonValue(result))
320
319
  throw new Error("Persisted workflow result is invalid");
321
- validateJournal(await jsonFile(join(store.directory, "journal.json")));
322
- const rawOwnership = await jsonFile(join(store.directory, "ownership.json"));
320
+ validateJournal(await json(join(store.directory, "journal.json")));
321
+ const rawOwnership = await json(join(store.directory, "ownership.json"));
323
322
  if (!isList(rawOwnership))
324
323
  throw new Error("Persisted ownership records are invalid");
325
324
  const ownershipIds = new Set();
@@ -362,7 +361,7 @@ async function scanSession(cwd, sessionId, home, expectedLease) {
362
361
  const ownerPath = join(runsPath, "owner.json");
363
362
  let liveLease = false;
364
363
  if (expectedLease && expectedLease.path === ownerPath) {
365
- const owned = await jsonFile(ownerPath);
364
+ const owned = await json(ownerPath);
366
365
  if (!object(owned) || owned.token !== expectedLease.token)
367
366
  throw new Error("Session ownership lease changed before deletion");
368
367
  }
@@ -1,5 +1,5 @@
1
- import { readFileSync, readdirSync, statSync } from "node:fs";
2
- import { basename, dirname, extname, join } from "node:path";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { basename, dirname, extname, join, resolve } 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
5
  import { DEFAULT_SETTINGS, canonicalPath, createLocalPiSession, errorText, isNodeError, isObject, loadSettings, prepareAgentSetupForInspection, resolveAgentResourcePolicy, resolveWorkflowSettings, resolveModelReference, resourcePatternHasMagic, parseThinking, registeredWorkflowFunctions, registeredWorkflowRoleDirectoryRegistrations, workflowProjectSettingsPath, workflowSettingsPath, } from "pi-extensible-workflows";
@@ -207,10 +207,9 @@ function validateModel(value, known, available, source, diagnostics, aliases, dy
207
207
  diagnostics.push(diagnostic("error", "MODEL_INVALID", message, source, message.includes("thinking") ? THINKING_HINT : "Use provider/model:thinking."));
208
208
  }
209
209
  }
210
- function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
211
- let definition;
210
+ function parseRole(path, diagnostics, source) {
212
211
  try {
213
- definition = parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
212
+ return parseRoleMarkdown(readFileSync(path, "utf8"), true, path);
214
213
  }
215
214
  catch (error) {
216
215
  if (usesLegacyRoleSelectors(path)) {
@@ -221,6 +220,8 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
221
220
  diagnostics.push(diagnostic("error", "ROLE_FRONTMATTER", source ? `${roleProvenance(source)} contains invalid role at "${path}": ${message}` : message, path, "Fix the role YAML frontmatter."));
222
221
  return undefined;
223
222
  }
223
+ }
224
+ function inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath) {
224
225
  const toolSelectorDiagnostic = positiveOnlyToolSelectorDiagnostic(path, definition.tools);
225
226
  if (toolSelectorDiagnostic)
226
227
  diagnostics.push(toolSelectorDiagnostic);
@@ -236,8 +237,13 @@ function inspectRole(path, activeTools, knownModels, availableModels, diagnostic
236
237
  for (const selector of definition.tools ?? []) {
237
238
  const tool = selector.startsWith("!") ? selector.slice(1) : selector;
238
239
  if (!selector.startsWith("!") && !resourcePatternHasMagic(selector) && !activeTools.has(tool))
239
- 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."));
240
+ diagnostics.push(diagnostic("warning", "ROLE_TOOL_INACTIVE", `Tool is not in Pi's headless active tool list: ${tool}`, path, "Doctor cannot see tools that extensions add when a session starts; otherwise use a tool listed under Pi active tools or enable its Pi extension."));
240
241
  }
242
+ }
243
+ function inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, source) {
244
+ const definition = parseRole(path, diagnostics, source);
245
+ if (definition)
246
+ inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
241
247
  return definition;
242
248
  }
243
249
  function matchResourcePolicy(policy, pi) {
@@ -435,15 +441,13 @@ export async function doctor(options = {}) {
435
441
  const starterPath = starterOverrides.get(file.path);
436
442
  const overriddenBy = starterOverriddenBy.get(file.path);
437
443
  roles.push({ name: file.name, path: file.path, scope: "extension", active: overriddenBy === undefined, extension: file.extension, ...(starterPath ? { overrides: starterPath } : {}), ...(overriddenBy ? { overriddenBy } : {}) });
438
- const definition = inspectRole(file.path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath, { directory: file.directory, extension: file.extension });
439
- if (definition)
440
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, file.path, diagnostics, file.name);
444
+ const definition = parseRole(file.path, diagnostics, { directory: file.directory, extension: file.extension });
441
445
  if (duplicateExtensionNames.has(file.name))
442
446
  continue;
443
447
  if (extensionPaths.get(file.name) !== file.path)
444
448
  continue;
445
449
  if (definition)
446
- definitions.set(file.name, definition);
450
+ definitions.set(file.name, { path: file.path, definition });
447
451
  }
448
452
  const globalPaths = new Map();
449
453
  const globalRoleDirs = workflowRoleDirectories(agentDir);
@@ -459,11 +463,9 @@ export async function doctor(options = {}) {
459
463
  extension.overriddenBy = path;
460
464
  }
461
465
  }
462
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
463
- if (definition)
464
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
466
+ const definition = parseRole(path, diagnostics);
465
467
  if (definition)
466
- definitions.set(name, definition);
468
+ definitions.set(name, { path, definition });
467
469
  else
468
470
  definitions.delete(name);
469
471
  }
@@ -490,24 +492,49 @@ export async function doctor(options = {}) {
490
492
  extension.overriddenBy = path;
491
493
  }
492
494
  }
493
- const definition = inspectRole(path, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
495
+ const definition = parseRole(path, diagnostics);
494
496
  if (definition)
495
- validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
496
- if (definition)
497
- definitions.set(name, definition);
497
+ definitions.set(name, { path, definition });
498
498
  else
499
499
  definitions.delete(name);
500
500
  }
501
+ for (const [name, { path, definition }] of definitions) {
502
+ inspectRoleUsage(path, definition, activeTools, knownModels, availableModels, diagnostics, aliases, dynamicAliases, settingsPath);
503
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, diagnostics, name);
504
+ }
501
505
  const rolePaths = new Set(roles.map(({ path }) => path));
502
506
  if (diagnostics.some(({ code, source }) => source !== undefined && rolePaths.has(source) && (code === "ROLE_FRONTMATTER" || code === "AGENT_RESOURCE_SELECTOR_MIGRATION")))
503
507
  diagnostics.push(diagnostic("error", "ROLE_LOAD_BLOCKED", "Workflow role loading is blocked because the runtime rejects the complete role set when any active role file is invalid.", undefined, "Fix the reported role file before launching workflows."));
504
508
  let roleInspection;
505
509
  if (options.role !== undefined) {
506
- const activeRole = roles.find(({ name, active }) => name === options.role && active);
507
- const definition = activeRole ? definitions.get(options.role) : undefined;
508
- if (!activeRole || !definition)
509
- diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
510
+ let target;
511
+ //NOTE: installed role names drop ".md", so a target ending in ".md" is a role file path.
512
+ if (options.role.endsWith(".md")) {
513
+ const path = resolve(cwd, options.role);
514
+ if (!existsSync(path) || !statSync(path).isFile())
515
+ diagnostics.push(diagnostic("error", "ROLE_FILE_NOT_FOUND", `Role file not found: ${path}`, path));
516
+ else {
517
+ const name = basename(path, ".md");
518
+ const found = [];
519
+ const definition = inspectRole(path, activeTools, knownModels, availableModels, found, aliases, dynamicAliases, settingsPath);
520
+ if (definition)
521
+ validateDoctorExtensionSettings(registry, mergeWorkflowExtensionSettings(settings.extensionSettings, definition.extensionSettings), "role", cwd, pi.trust.trusted, path, found, name);
522
+ // Discovery already reported an installed file; add only what is new.
523
+ const known = new Set(diagnostics.map((item) => JSON.stringify(item)));
524
+ diagnostics.push(...found.filter((item) => !known.has(JSON.stringify(item))));
525
+ if (definition)
526
+ target = { name, path, definition };
527
+ }
528
+ }
510
529
  else {
530
+ const effective = definitions.get(options.role);
531
+ if (!effective)
532
+ diagnostics.push(diagnostic("error", "ROLE_NOT_FOUND", `Active role not found: ${options.role}`, options.role));
533
+ else
534
+ target = { name: options.role, ...effective };
535
+ }
536
+ if (target) {
537
+ const { name, path, definition } = target;
511
538
  const rootReference = pi.model ? `${pi.model.provider}/${pi.model.model}` : pi.availableModels[0] ?? pi.knownModels[0];
512
539
  if (!rootReference)
513
540
  diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", "Cannot inspect a role because Pi has no registered model"));
@@ -525,12 +552,12 @@ export async function doctor(options = {}) {
525
552
  const dynamic = await registry.resolveModelAliases({ cwd, projectTrusted: pi.trust.trusted, rootModel, knownModels, availableModels, signal: new AbortController().signal });
526
553
  roleAliases = { ...aliases, ...dynamic };
527
554
  }
528
- roleInspection = await inspectRoleSession(cwd, agentDir, options.role, definition, activeRole.path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
555
+ roleInspection = await inspectRoleSession(cwd, agentDir, name, definition, path, resourcePolicy, rootModel, [...activeTools], roleAliases, knownModels, availableModels, settingsPath, settings.extensionSettings, options.prompt ?? "", registry.agentSetupHooks(), diagnostics);
529
556
  if (roleInspection)
530
557
  diagnostics.push(...roleInspection.setup.diagnostics);
531
558
  }
532
559
  catch (error) {
533
- diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", errorText(error), activeRole.path));
560
+ diagnostics.push(diagnostic("error", "ROLE_INSPECTION_MODEL", errorText(error), path));
534
561
  }
535
562
  }
536
563
  }
@@ -113,7 +113,8 @@ function transcript(manager) {
113
113
  }
114
114
  function readTranscript(path) {
115
115
  try {
116
- if (!existsSync(path) || !statSync(path).isFile() || statSync(path).size === 0)
116
+ const info = existsSync(path) ? statSync(path) : undefined;
117
+ if (!info?.isFile() || info.size === 0)
117
118
  return undefined;
118
119
  const manager = SessionManager.open(path);
119
120
  if (!manager.getHeader())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.17.0",
3
+ "version": "5.17.2",
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
  "@earendil-works/pi-coding-agent": "0.85.0",
46
46
  "@earendil-works/pi-server": "0.85.0",
47
47
  "@earendil-works/pi-tui": "0.85.0",
48
- "pi-extensible-workflows": "5.17.0",
48
+ "pi-extensible-workflows": "5.17.2",
49
49
  "typebox": "1.3.7"
50
50
  },
51
51
  "publishConfig": {