@piewf/cli 5.13.0 → 5.13.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.
@@ -1,7 +1,11 @@
1
1
  import type { AgentDefinition, WorkflowCatalogFunction } from "pi-extensible-workflows";
2
+ export interface PortableWorkflowSource {
3
+ module: string;
4
+ export: string;
5
+ }
2
6
  export interface PortableWorkflowManifest {
3
7
  format: "pi-extensible-workflows-bundle";
4
- version: 1;
8
+ version: 1 | 2;
5
9
  command: string;
6
10
  workflow: {
7
11
  name: string;
@@ -21,6 +25,11 @@ export interface PortableWorkflowManifest {
21
25
  environment: readonly string[];
22
26
  };
23
27
  aliasTargets?: Readonly<Record<string, string>>;
28
+ source?: Readonly<PortableWorkflowSource>;
29
+ bundler?: {
30
+ esbuild: string;
31
+ };
32
+ dependencies?: readonly string[];
24
33
  payload?: {
25
34
  extensions?: readonly string[];
26
35
  skills?: readonly string[];
@@ -38,7 +47,8 @@ export interface PortableWorkflowBundleInput {
38
47
  destination: string;
39
48
  command: string;
40
49
  workflow: WorkflowCatalogFunction;
41
- functionSource: string;
50
+ source: PortableWorkflowSource;
51
+ dependencies?: readonly string[];
42
52
  piVersion?: string;
43
53
  engineVersion?: string;
44
54
  force?: boolean;
@@ -49,5 +59,4 @@ export interface PortableWorkflowBundleInput {
49
59
  }
50
60
  export declare function portableEngineVersion(): string;
51
61
  export declare function portablePiVersion(): string;
52
- export declare function normalizePortableFunctionSource(functionSource: string): string;
53
- export declare function writePortableWorkflowBundle(input: PortableWorkflowBundleInput): PortableWorkflowManifest;
62
+ export declare function writePortableWorkflowBundle(input: PortableWorkflowBundleInput): Promise<PortableWorkflowManifest>;
@@ -1,7 +1,9 @@
1
1
  import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
4
+ import { builtinModules } from "node:module";
5
+ import { createRequire } from "node:module";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
7
  function isJsonObject(value) {
6
8
  return typeof value === "object" && value !== null && !Array.isArray(value);
7
9
  }
@@ -316,42 +318,147 @@ function runnerSource() {
316
318
  "}",
317
319
  ].join("\n") + "\n";
318
320
  }
319
- export function normalizePortableFunctionSource(functionSource) {
320
- const source = functionSource.trim();
321
- if (!source || source.includes("[native code]"))
322
- throw new Error("Workflow function source is unavailable");
323
- if (/^(async\s+)?run\s*\(/.test(source))
324
- return source.replace(/^(async\s+)?run\s*\(/, "$1function run(");
325
- if (/^\*\s*run\s*\(/.test(source))
326
- return source.replace(/^\*\s*run\s*\(/, "function* run(");
327
- return source;
328
- }
329
- function workflowModule(workflow, functionSource, withRoles, aliasTargets, extensionModules) {
321
+ function bundledWorkflowModule(workflow, source, withRoles, aliasTargets, extensionModules) {
330
322
  const aliases = Object.entries(aliasTargets);
331
- const source = normalizePortableFunctionSource(functionSource);
323
+ const modules = ["./extension.mjs", ...extensionModules.map((name) => `./extensions/${name}`)];
332
324
  return [
333
- `const run = ${source};`,
334
325
  "export async function register(registerWorkflowExtension) {",
335
- ...extensionModules.map((name, index) => ` const extension${String(index)} = await import(${JSON.stringify(`./extensions/${name}`)});`),
336
- ...extensionModules.map((_name, index) => ` if (typeof extension${String(index)}.default === "function") await extension${String(index)}.default();`),
326
+ " const captured = [];",
327
+ " const previousCapture = globalThis.__pi_bundle_capture;",
328
+ " globalThis.__pi_bundle_capture = (extension) => { captured.push(extension); };",
329
+ " try {",
330
+ ...modules.map((name, index) => ` const extension${String(index)} = await import(${JSON.stringify(name)});`),
331
+ ` const factory = extension0[${JSON.stringify(source.export)}];`,
332
+ ` if (typeof factory !== "function") throw new Error(${JSON.stringify(`Workflow extension export ${source.export} is not a function`)});`,
333
+ " await factory();",
334
+ ...modules.slice(1).map((_name, index) => ` if (typeof extension${String(index + 1)}.default === "function") await extension${String(index + 1)}.default();`),
335
+ " } finally {",
336
+ " if (previousCapture === undefined) delete globalThis.__pi_bundle_capture; else globalThis.__pi_bundle_capture = previousCapture;",
337
+ " }",
338
+ ` const extension = captured.find((candidate) => candidate?.functions?.[${JSON.stringify(workflow.name)}]);`,
339
+ ` if (!extension) throw new Error("Bundled extension does not register workflow ${workflow.name}");`,
340
+ " for (const candidate of captured) if (candidate !== extension) registerWorkflowExtension(candidate);",
337
341
  " registerWorkflowExtension({",
338
- ` version: ${JSON.stringify("1.0.0")},`,
339
- ` headline: ${JSON.stringify("Portable workflow bundle")},`,
340
- ...(withRoles ? [` roleDirectories: [new URL("./roles", import.meta.url)],`] : []),
341
- ...(aliases.length ? [` modelAliases: { ${aliases.map(([name, target]) => `${JSON.stringify(name)}: { resolve: () => ${JSON.stringify(target)} }`).join(", ")} },`] : []),
342
- " functions: {",
343
- ` [${JSON.stringify(workflow.name)}]: {`,
344
- ` description: ${JSON.stringify(workflow.description)},`,
345
- ` input: ${JSON.stringify(workflow.input)},`,
346
- ` output: ${JSON.stringify(workflow.output)},`,
347
- " run,",
348
- " },",
349
- " },",
342
+ " ...extension,",
343
+ " source: new URL(\"./extension.mjs\", import.meta.url).href,",
344
+ ...(aliases.length ? [` modelAliases: { ...(extension.modelAliases ?? {}), ${aliases.map(([name, target]) => `${JSON.stringify(name)}: { resolve: () => ${JSON.stringify(target)} }`).join(", ")} },`] : []),
345
+ ` functions: { [${JSON.stringify(workflow.name)}]: extension.functions[${JSON.stringify(workflow.name)}] },`,
346
+ ` roleDirectories: ${withRoles ? "[new URL(\"./roles\", import.meta.url)]" : "[]"},`,
350
347
  " });",
351
348
  "}",
352
349
  "",
353
350
  ].join("\n");
354
351
  }
352
+ const nodeBuiltins = new Set(builtinModules);
353
+ function packageName(specifier) {
354
+ const parts = specifier.split("/");
355
+ return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0] ?? specifier;
356
+ }
357
+ function isPackageSpecifier(specifier) {
358
+ return !specifier.startsWith(".") && !specifier.startsWith("/") && !specifier.startsWith("#") && !specifier.startsWith("node:") && !specifier.includes(":");
359
+ }
360
+ function isAllowedExternal(specifier) {
361
+ return nodeBuiltins.has(specifier) || packageName(specifier) === "pi-extensible-workflows";
362
+ }
363
+ const packageNamePattern = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
364
+ async function loadEsbuild() {
365
+ let modulePath;
366
+ try {
367
+ modulePath = createRequire(join(process.cwd(), "package.json")).resolve("esbuild");
368
+ }
369
+ catch (error) {
370
+ throw new Error("Portable workflow bundling requires optional esbuild. Install esbuild in the project where you run piewf bundle, for example with `npm install --save-dev esbuild`, and retry.", { cause: error });
371
+ }
372
+ try {
373
+ const loaded = await import(pathToFileURL(modulePath).href);
374
+ return loaded;
375
+ }
376
+ catch (error) {
377
+ throw new Error("Portable workflow bundling could not load esbuild from the current project. Install it with `npm install --save-dev esbuild` and retry.", { cause: error });
378
+ }
379
+ }
380
+ function sourceModulePath(source) {
381
+ try {
382
+ const url = new URL(source);
383
+ if (url.protocol !== "file:")
384
+ throw new Error("only file URLs are supported");
385
+ return fileURLToPath(url);
386
+ }
387
+ catch (error) {
388
+ throw new Error(`Workflow source must be a file URL: ${source}`, { cause: error });
389
+ }
390
+ }
391
+ function dependencyNames(dependencies) {
392
+ const rawDependencies = dependencies;
393
+ if (rawDependencies !== undefined && !Array.isArray(rawDependencies))
394
+ throw new Error("Workflow bundle dependencies must be non-empty package names");
395
+ const values = dependencies ?? [];
396
+ if (values.some((dependency) => typeof dependency !== "string" || !dependency.trim()))
397
+ throw new Error("Workflow bundle dependencies must be non-empty package names");
398
+ const names = [...new Set(values.map((dependency) => dependency.trim()))];
399
+ if (names.some((dependency) => !packageNamePattern.test(dependency)))
400
+ throw new Error("Workflow bundle dependencies must be package names");
401
+ return names.sort();
402
+ }
403
+ function isBuildFailure(error) {
404
+ return error instanceof Error && Array.isArray(error.errors);
405
+ }
406
+ function bundleFailure(error) {
407
+ if (!isBuildFailure(error))
408
+ return error instanceof Error ? error : new Error(String(error));
409
+ const dynamicImport = error.errors.find((message) => message.id === "unsupported-dynamic-import");
410
+ if (!dynamicImport)
411
+ return error;
412
+ const file = dynamicImport.location ? resolve(dynamicImport.location.file) : "the workflow extension";
413
+ return new Error(`Unsupported dynamic import in ${file}: dynamic imports must use a string-literal module path, otherwise the module is not bundleable`, { cause: error });
414
+ }
415
+ async function bundleExtension(sourcePath, sourceExport, dependencies) {
416
+ const esbuild = await loadEsbuild();
417
+ const undeclared = new Set();
418
+ const result = await esbuild.build({
419
+ entryPoints: [sourcePath],
420
+ bundle: true,
421
+ format: "esm",
422
+ platform: "node",
423
+ nodePaths: [
424
+ join(process.cwd(), "node_modules"),
425
+ join(dirname(fileURLToPath(import.meta.url)), "../../node_modules"),
426
+ join(dirname(fileURLToPath(import.meta.url)), "../../../node_modules"),
427
+ join(dirname(fileURLToPath(import.meta.url)), "../../../../node_modules"),
428
+ ],
429
+ write: false,
430
+ metafile: true,
431
+ logLevel: "silent",
432
+ logOverride: { "unsupported-dynamic-import": "error" },
433
+ plugins: [{
434
+ name: "portable-workflow-dependencies",
435
+ setup(build) {
436
+ build.onResolve({ filter: /.*/ }, (args) => {
437
+ if (!isPackageSpecifier(args.path))
438
+ return undefined;
439
+ const name = packageName(args.path);
440
+ if (isAllowedExternal(args.path))
441
+ return { path: args.path, external: true };
442
+ if (name.startsWith("@earendil-works/"))
443
+ return { errors: [{ text: `Pi packages (@earendil-works/*) cannot be bundled; use the pi-extensible-workflows API instead: ${name}` }] };
444
+ if (dependencies.includes(name))
445
+ return undefined;
446
+ undeclared.add(name);
447
+ return { path: args.path, external: true };
448
+ });
449
+ },
450
+ }],
451
+ }).catch((error) => { throw bundleFailure(error); });
452
+ if (undeclared.size)
453
+ throw new Error(`Undeclared dependencies: ${[...undeclared].sort().join(", ")}`);
454
+ const output = result.outputFiles[0];
455
+ if (!output)
456
+ throw new Error("esbuild produced no workflow extension output");
457
+ const exports = Object.values(result.metafile.outputs).flatMap(({ exports: names }) => names);
458
+ if (!exports.includes(sourceExport))
459
+ throw new Error(`Bundled workflow extension does not export ${sourceExport}`);
460
+ return { source: output.text, esbuild: esbuild.version };
461
+ }
355
462
  function roleMarkdown(role) {
356
463
  const metadata = ["---"];
357
464
  if (role.description !== undefined)
@@ -413,13 +520,16 @@ function copyResources(root, resources) {
413
520
  copy("dependencies", resources.dependencies);
414
521
  return Object.keys(payload).length ? payload : undefined;
415
522
  }
416
- function extensionPackageShim(paths) {
523
+ function extensionPackageShim(paths, bundledSource) {
417
524
  const importedNames = new Set();
418
- for (const path of paths) {
525
+ const sources = paths.map((path) => {
419
526
  if (!/\.(?:c|m)?js$/.test(path))
420
527
  throw new Error(`Selected extension must be a JavaScript module file: ${path}`);
421
- const source = readFileSync(path, "utf8");
422
- for (const match of source.matchAll(/import\s*\{([^}]+)\}\s*from\s*["']pi-extensible-workflows["']/g)) {
528
+ return readFileSync(path, "utf8");
529
+ });
530
+ sources.push(bundledSource);
531
+ for (const source of sources) {
532
+ for (const match of source.matchAll(/import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']pi-extensible-workflows["']/g)) {
423
533
  for (const part of (match[1] ?? "").split(",")) {
424
534
  const imported = part.trim().split(/\s+as\s+/, 1)[0]?.trim();
425
535
  if (imported && /^[A-Za-z_$][\w$]*$/.test(imported))
@@ -427,13 +537,13 @@ function extensionPackageShim(paths) {
427
537
  }
428
538
  }
429
539
  }
430
- return [...importedNames].map((name) => `export const ${name} = globalThis.__pi_bundle_api.${name};`).join("\n") + "\n";
540
+ return [...importedNames].map((name) => name === "registerWorkflowExtension" ? "export const registerWorkflowExtension = (extension) => globalThis.__pi_bundle_capture ? globalThis.__pi_bundle_capture(extension) : globalThis.__pi_bundle_api.registerWorkflowExtension(extension);" : `export const ${name} = globalThis.__pi_bundle_api.${name};`).join("\n") + "\n";
431
541
  }
432
- export function writePortableWorkflowBundle(input) {
542
+ function baseManifest(input, version) {
433
543
  const engineVersion = input.engineVersion ?? portableEngineVersion();
434
- const manifest = {
544
+ return {
435
545
  format: "pi-extensible-workflows-bundle",
436
- version: 1,
546
+ version,
437
547
  command: input.command,
438
548
  workflow: { name: input.workflow.name, description: input.workflow.description, input: input.workflow.input, output: input.workflow.output },
439
549
  runtime: { pi: input.piVersion?.trim() || "unknown", "@piewf/cli": engineVersion.trim() || "unknown" },
@@ -446,6 +556,8 @@ export function writePortableWorkflowBundle(input) {
446
556
  },
447
557
  ...(input.aliasTargets && Object.keys(input.aliasTargets).length ? { aliasTargets: Object.freeze({ ...input.aliasTargets }) } : {}),
448
558
  };
559
+ }
560
+ function writeBundleFiles(input, manifest, workflowSource, bundledExtensionSource) {
449
561
  const parent = dirname(input.destination);
450
562
  mkdirSync(parent, { recursive: true });
451
563
  if (existsSync(input.destination) && !input.force)
@@ -468,15 +580,13 @@ export function writePortableWorkflowBundle(input) {
468
580
  if (copiedPayload)
469
581
  manifest.payload = copiedPayload;
470
582
  const extensionPaths = input.resources?.extensions ?? [];
471
- const extensionModules = extensionPaths.map((source) => basename(source));
472
- if (extensionPaths.length) {
473
- const packageDirectory = join(payload, "node_modules", "pi-extensible-workflows");
474
- mkdirSync(packageDirectory, { recursive: true });
475
- writeFileSync(join(packageDirectory, "package.json"), '{"type":"module","exports":"./index.mjs"}\n', { encoding: "utf8", mode: 0o600 });
476
- writeFileSync(join(packageDirectory, "index.mjs"), extensionPackageShim(extensionPaths), { encoding: "utf8", mode: 0o600 });
477
- }
583
+ writeFileSync(join(payload, "extension.mjs"), bundledExtensionSource, { encoding: "utf8", mode: 0o600 });
584
+ const packageDirectory = join(payload, "node_modules", "pi-extensible-workflows");
585
+ mkdirSync(packageDirectory, { recursive: true });
586
+ writeFileSync(join(packageDirectory, "package.json"), '{"type":"module","exports":"./index.mjs"}\n', { encoding: "utf8", mode: 0o600 });
587
+ writeFileSync(join(packageDirectory, "index.mjs"), extensionPackageShim(extensionPaths, bundledExtensionSource), { encoding: "utf8", mode: 0o600 });
478
588
  writeFileSync(join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
479
- writeFileSync(join(payload, "workflow.mjs"), workflowModule(input.workflow, input.functionSource, Object.keys(roles).length > 0, input.aliasTargets ?? {}, extensionModules), { encoding: "utf8", mode: 0o600 });
589
+ writeFileSync(join(payload, "workflow.mjs"), workflowSource, { encoding: "utf8", mode: 0o600 });
480
590
  writeFileSync(join(payload, "runner.mjs"), runnerSource(), { encoding: "utf8", mode: 0o700 });
481
591
  const launcher = join(temporary, input.command);
482
592
  writeFileSync(launcher, shellLauncher(), { encoding: "utf8", mode: 0o755 });
@@ -491,3 +601,12 @@ export function writePortableWorkflowBundle(input) {
491
601
  }
492
602
  return manifest;
493
603
  }
604
+ export async function writePortableWorkflowBundle(input) {
605
+ if (typeof input.source.export !== "string" || !input.source.export.trim())
606
+ throw new Error("Workflow source export must be a non-empty name");
607
+ const sourcePath = sourceModulePath(input.source.module);
608
+ const dependencies = dependencyNames(input.dependencies);
609
+ const bundled = await bundleExtension(sourcePath, input.source.export, dependencies);
610
+ const manifest = { ...baseManifest(input, 2), source: Object.freeze({ module: basename(sourcePath), export: input.source.export }), bundler: { esbuild: bundled.esbuild }, dependencies: Object.freeze([...dependencies]) };
611
+ return writeBundleFiles(input, manifest, bundledWorkflowModule(input.workflow, input.source, Object.keys(input.roles ?? {}).length > 0, input.aliasTargets ?? {}, input.resources?.extensions?.map((source) => basename(source)) ?? []), bundled.source);
612
+ }
package/dist/src/cli.js CHANGED
@@ -8,12 +8,11 @@ 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, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, registeredWorkflowFunctions, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "pi-extensible-workflows";
11
+ import workflowExtension, { errorText, formatWorkflowProgress, isNodeError, jsonValue, loadAgentDefinitions, object, registeredWorkflowFunctionSources, 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 {
@@ -777,9 +776,9 @@ async function bundleWorkflowCli(rawArgs, options) {
777
776
  const fn = runtime.catalog.functions.find((candidate) => candidate.name === workflowName);
778
777
  if (!fn)
779
778
  throw new Error(`Unknown workflow function: ${workflowName}`);
780
- const registered = registeredWorkflowFunctions()[workflowName];
781
- if (!registered)
782
- throw new Error(`Workflow ${workflowName} is not exportable because its registered implementation is unavailable`);
779
+ const source = registeredWorkflowFunctionSources()[workflowName];
780
+ if (!source)
781
+ throw new Error(`Workflow ${workflowName} is not exportable; add \`source: import.meta.url\` to extension ${fn.headline}`);
783
782
  const definitions = requirements.roles.length ? loadAgentDefinitions(options.cwd ?? process.cwd(), options.agentDir ?? getAgentDir(), runtime.services.settingsManager.isProjectTrusted()) : {};
784
783
  const roles = Object.fromEntries(requirements.roles.map((role) => {
785
784
  if (!role || role === "." || role === ".." || role.includes("/") || role.includes("\\"))
@@ -798,7 +797,7 @@ async function bundleWorkflowCli(rawArgs, options) {
798
797
  return typeof target === "string" ? [[name, target]] : [];
799
798
  }));
800
799
  const selectedResources = Object.values(resources).some((entries) => entries.length) ? resources : undefined;
801
- writePortableWorkflowBundle({ destination, command, workflow: fn, functionSource: registered.run.toString(), requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
800
+ await writePortableWorkflowBundle({ destination, command, workflow: fn, source: { module: source.module, export: source.export }, dependencies: source.dependencies, requirements, aliasTargets, roles, ...(selectedResources ? { resources: selectedResources } : {}), piVersion: portablePiVersion(), engineVersion: portableEngineVersion(), force });
802
801
  options.write(`Bundled ${workflowName} at ${destination}\n`);
803
802
  options.write(`Run ${join(destination, command)} setup before launching the workflow.\n`);
804
803
  return 0;
@@ -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"));
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piewf/cli",
3
- "version": "5.13.0",
3
+ "version": "5.13.2",
4
4
  "description": "CLI for pi-extensible-workflows",
5
5
  "homepage": "https://vekexasia.github.io/pi-extensible-workflows/",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  "devDependencies": {
30
30
  "@eslint/js": "10.0.1",
31
31
  "@types/node": "24.12.4",
32
+ "esbuild": "^0.28.2",
32
33
  "eslint": "10.0.3",
33
34
  "typescript": "5.9.3",
34
35
  "typescript-eslint": "8.63.0"
@@ -42,7 +43,7 @@
42
43
  "@earendil-works/pi-coding-agent": "0.85.0",
43
44
  "@earendil-works/pi-server": "0.85.0",
44
45
  "@earendil-works/pi-tui": "0.85.0",
45
- "pi-extensible-workflows": "5.13.0",
46
+ "pi-extensible-workflows": "5.13.2",
46
47
  "typebox": "1.3.7"
47
48
  },
48
49
  "publishConfig": {