pi-extensible-workflows 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +15 -17
  2. package/dist/src/agent-execution.d.ts +4 -80
  3. package/dist/src/agent-execution.js +20 -10
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +48 -2
  6. package/dist/src/doctor-cleanup.d.ts +41 -0
  7. package/dist/src/doctor-cleanup.js +597 -0
  8. package/dist/src/doctor.d.ts +7 -2
  9. package/dist/src/doctor.js +127 -22
  10. package/dist/src/execution.js +13 -4
  11. package/dist/src/host.d.ts +83 -4
  12. package/dist/src/host.js +1246 -410
  13. package/dist/src/index.d.ts +5 -2
  14. package/dist/src/index.js +3 -1
  15. package/dist/src/persistence.d.ts +3 -0
  16. package/dist/src/persistence.js +49 -1
  17. package/dist/src/registry.d.ts +21 -9
  18. package/dist/src/registry.js +131 -21
  19. package/dist/src/session-inspector.js +4 -2
  20. package/dist/src/types.d.ts +135 -7
  21. package/dist/src/utils.d.ts +6 -0
  22. package/dist/src/utils.js +45 -5
  23. package/dist/src/validation.d.ts +6 -2
  24. package/dist/src/validation.js +157 -31
  25. package/dist/src/workflow-artifacts.d.ts +13 -0
  26. package/dist/src/workflow-artifacts.js +39 -0
  27. package/examples/workflow-extension-template/README.md +37 -0
  28. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  29. package/examples/workflow-extension-template/index.js +51 -0
  30. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  31. package/package.json +5 -3
  32. package/skills/pi-extensible-workflows/SKILL.md +45 -18
  33. package/src/agent-execution.ts +23 -37
  34. package/src/cli.ts +33 -2
  35. package/src/doctor-cleanup.ts +337 -0
  36. package/src/doctor.ts +117 -24
  37. package/src/execution.ts +13 -4
  38. package/src/host.ts +1039 -366
  39. package/src/index.ts +5 -2
  40. package/src/persistence.ts +35 -1
  41. package/src/registry.ts +108 -25
  42. package/src/session-inspector.ts +4 -2
  43. package/src/types.ts +53 -8
  44. package/src/utils.ts +39 -5
  45. package/src/validation.ts +130 -31
  46. package/src/workflow-artifacts.ts +34 -0
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Turn multi-agent tasks into deterministic jobs that fan out in parallel, pause for approval, and resume without rerunning completed work.
8
8
 
9
- [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html)
9
+ [Documentation](https://vekexasia.github.io/pi-extensible-workflows/) | [Developer guide](https://vekexasia.github.io/pi-extensible-workflows/developers.html) | [Agent guide](https://vekexasia.github.io/pi-extensible-workflows/agents.html) | [Extension authoring](https://vekexasia.github.io/pi-extensible-workflows/extensions.html) | [Video overview](https://youtu.be/qAiivspEHmU)
10
10
 
11
11
  Requires Node.js 22.19 or newer. This is a trusted Pi extension with the same filesystem and process access as Pi.
12
12
 
@@ -20,35 +20,32 @@ For source installs and local development, see the [installation guide](https://
20
20
 
21
21
  ## Capabilities
22
22
 
23
- The main Pi agent acts as the orchestrator: it writes workflow scripts on the fly for each task. Pi extensions can add reusable functions and variables to those scripts; every registered function is also directly runnable as a top-level workflow.
24
-
25
- Inline workflow launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
26
-
27
- A workflow can fan out across specialized agents, combine their results, and resume without rerunning completed work.
23
+ The default path is a named inline workflow: write a `script` that fans out independent work with `parallel(...)`, awaits the keyed results, passes them into one summarizing `agent(...)`, and returns. Inline launches require a non-empty `name`; registered function launches reject `name` and use their registered function name as the run name. Runs are backgrounded by default; set `foreground: true` when the final value must be returned in the same tool call.
28
24
 
29
25
  ```js
30
26
  const reviews = await parallel("review", {
31
27
  correctness: () => agent("Review the current changes for correctness issues."),
32
- security: () => agent("Review the current changes for security risks.", {
33
- role: "security-specialist",
34
- }),
28
+ security: () => agent("Review the current changes for security risks."),
35
29
  tests: () => agent("Review the current changes for missing test coverage."),
36
30
  });
37
31
 
38
- const summary = await agent(
39
- prompt("Deduplicate and prioritize these findings:\n\n{reviews}", { reviews }),
32
+ return await agent(
33
+ prompt("Summarize and prioritize these findings:\n\n{reviews}", { reviews }),
40
34
  );
41
-
42
- return summary;
43
35
  ```
44
36
 
37
+ **Advanced capabilities:** Use registered functions, `outputSchema`, budgets, checkpoints, worktrees, retry/resume, CLI export, and `pipeline(...)` when the task requires them. They remain available without complicating the basic inline path. Workflow worktree scopes always use the explicit `withWorktree(name, callback)` form.
38
+
39
+ The main Pi agent writes these scripts on the fly for each task; extensions can add reusable functions and variables, and completed workflows can resume without rerunning completed work.
40
+
45
41
  Learn more about roles, workflow contracts, and extension APIs in the documentation:
46
42
 
47
43
  - [Workflow tool and invocation API](https://vekexasia.github.io/pi-extensible-workflows/developers.html#tool-api)
48
44
  - [Global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings)
49
45
  - [Aggregate run budgets](https://vekexasia.github.io/pi-extensible-workflows/developers.html#budgets)
50
46
  - [Workflow DSL and worktrees](https://vekexasia.github.io/pi-extensible-workflows/developers.html#dsl)
51
- - [Reusable extension primitives](https://vekexasia.github.io/pi-extensible-workflows/developers.html#extensions)
47
+ - [Extension authoring guide](https://vekexasia.github.io/pi-extensible-workflows/extensions.html)
48
+ - [Copy-paste extension template](https://github.com/vekexasia/pi-extensible-workflows/tree/main/examples/workflow-extension-template)
52
49
  - [Run artifacts and lifecycle events](https://vekexasia.github.io/pi-extensible-workflows/developers.html#lifecycle)
53
50
  - [Run inspection and recovery](https://vekexasia.github.io/pi-extensible-workflows/developers.html#operations)
54
51
  - [Agent patterns and model selection](https://vekexasia.github.io/pi-extensible-workflows/agents.html#patterns)
@@ -56,20 +53,21 @@ Learn more about roles, workflow contracts, and extension APIs in the documentat
56
53
 
57
54
  ## Configuration
58
55
 
59
- Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.json` by default and configure concurrency, model aliases, and workflow-agent skill or extension exclusions. Trusted projects can add resource exclusions at `<project>/.pi/pi-extensible-workflows/settings.json`; they cannot override global aliases or concurrency. See [global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings) for the schema and merge rules.
56
+ Global workflow settings live at `~/.pi/agent/pi-extensible-workflows/settings.json` by default. Trusted projects can add partial overrides at `<project>/.pi/pi-extensible-workflows/settings.json`; precedence is defaults < global < trusted project < per-run options. Project collections replace global collections, so `{}` and empty arrays clear inherited aliases or resource exclusions. Untrusted project settings are ignored. See [global and project settings](https://vekexasia.github.io/pi-extensible-workflows/developers.html#settings) for the schema, trust gate, source reporting, and merge rules.
60
57
 
61
58
  ## CLI
62
59
 
63
60
  ```sh
64
61
  npx pi-extensible-workflows doctor
62
+ npx pi-extensible-workflows doctor cleanup [--older-than-days <days>] [--yes]
65
63
  npx pi-extensible-workflows inspect [session-id]
66
64
  npx pi-extensible-workflows transcript <session-file>
67
65
  npx pi-extensible-workflows run <workflow-name> [workflow arguments]
68
66
  npx pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]
69
67
  ```
70
68
 
71
- `doctor` validates the installation and active Pi resources. `inspect` opens a read-only terminal view of persisted workflow runs. `transcript` renders a session transcript to stdout. `run` derives flat CLI arguments and help from a registered function's input schema. Use `--input '<json>'` for nested or otherwise complex inputs. It executes in the current working directory, writes the final JSON result to stdout, and writes progress and errors to stderr. `export` creates an executable POSIX launcher in `~/.local/bin` by default.
72
- `run` and `export` accept the trust overrides `--approve` and `--no-approve`; the generated launcher forwards its arguments to `run`. `--` ends launcher option parsing, and later tokens are passed to workflow input instead of being interpreted as launcher options.
69
+ `doctor` validates the installation and active Pi resources and remains read-only. `doctor cleanup` is an explicit, dry-run-first maintenance command: it previews terminal workflow runs older than 90 days across the current project's stored sessions and deletes them only with `--yes`; use `--older-than-days` to change the positive age threshold. It protects active, corrupt, leased, and dependency-linked runs. `inspect` opens a read-only terminal view of persisted workflow runs. `transcript` renders a session transcript to stdout. `run` derives flat CLI arguments and help from a registered function's input schema. Use `--input '<json>'` for nested or otherwise complex inputs. It executes in the current working directory, writes the final JSON result to stdout, and writes progress and errors to stderr. `export` creates an executable POSIX launcher in `~/.local/bin` by default.
70
+ `run` and `export` accept the trust overrides `--approve` and `--no-approve`; the generated launcher forwards its arguments to `run`. `--` ends launcher option parsing, and later tokens are passed to workflow input instead of being interpreted as launcher options. Headless `run` and generated `export` launchers cannot execute workflows containing checkpoints; use the Pi workflow tool/UI path for checkpointed workflows.
73
71
  Launch snapshots use identity version 5. Cold resume rejects older snapshots, including v4 snapshots created with the previous worktree or registered-function naming contracts, with `RESUME_INCOMPATIBLE`; relaunch the workflow instead.
74
72
 
75
73
  ## Development
@@ -1,22 +1,8 @@
1
- import { type AgentSessionEvent, type InlineExtension, type SessionStats, type ToolDefinition } from "@earendil-works/pi-coding-agent";
1
+ import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
3
- type AgentMessage = {
4
- role: string;
5
- content?: unknown;
6
- stopReason?: string;
7
- errorMessage?: string;
8
- usage?: {
9
- input: number;
10
- output: number;
11
- cacheRead: number;
12
- cacheWrite: number;
13
- cost: {
14
- total: number;
15
- };
16
- };
17
- };
18
- import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, WorkflowRunContext } from "./types.js";
3
+ import type { AgentIdentity, AgentResourceExclusions, AgentResourcePolicy, AgentSetupSummary, JsonSchema, JsonValue, ModelSpec, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput, WorkflowRunContext } from "./types.js";
19
4
  import type { RunStore } from "./persistence.js";
5
+ export type { AgentSetup, AgentSetupContext, AgentSetupHook, NativeSession, RegisteredAgentSetupHook, SessionFactory, SessionInput } from "./types.js";
20
6
  export interface AgentBudgetHooks {
21
7
  beforeAttempt(): void;
22
8
  beforeTurn(): void;
@@ -121,68 +107,6 @@ export interface AgentExecutionResult {
121
107
  attempts: readonly AgentAttempt[];
122
108
  cwd: string;
123
109
  }
124
- export interface AgentSetup {
125
- prompt: string;
126
- options: Record<string, JsonValue>;
127
- sessionInput: SessionInput;
128
- createSession: SessionFactory;
129
- }
130
- export interface AgentSetupContext {
131
- readonly run: Readonly<WorkflowRunContext>;
132
- readonly identity: Readonly<AgentIdentity>;
133
- readonly attempt: number;
134
- readonly signal: AbortSignal;
135
- }
136
- export interface AgentSetupHook {
137
- priority?: number;
138
- setup: (agent: AgentSetup, context: Readonly<AgentSetupContext>) => void | Promise<void>;
139
- }
140
- export interface RegisteredAgentSetupHook {
141
- name: string;
142
- priority: number;
143
- setup: AgentSetupHook["setup"];
144
- }
145
- type NativeSessionStats = Pick<SessionStats, "tokens" | "cost">;
146
- export interface NativeSession {
147
- readonly sessionId: string;
148
- readonly sessionFile: string | undefined;
149
- readonly messages: readonly AgentMessage[];
150
- getSessionStats(): NativeSessionStats;
151
- readonly systemPrompt?: string;
152
- readonly model?: {
153
- provider: string;
154
- model?: string;
155
- id?: string;
156
- };
157
- readonly agent?: {
158
- state: {
159
- tools: readonly {
160
- name: string;
161
- }[];
162
- };
163
- };
164
- getLeafId?: () => string | null;
165
- getToolDefinitions?: () => unknown;
166
- subscribe?(listener: (event: AgentSessionEvent) => void): () => void;
167
- prompt(text: string): Promise<void>;
168
- steer?(text: string): Promise<void>;
169
- abort?(): Promise<void>;
170
- dispose(): void;
171
- }
172
- export interface SessionInput {
173
- cwd: string;
174
- model: ModelSpec;
175
- tools: string[];
176
- sessionLabel: string;
177
- agentDir?: string;
178
- customTools?: ToolDefinition[];
179
- resultTool?: ToolDefinition;
180
- systemPromptAppend?: string;
181
- extensionFactories?: InlineExtension[];
182
- resourcePolicy?: AgentResourcePolicy;
183
- options?: Record<string, JsonValue>;
184
- }
185
- export type SessionFactory = (input: SessionInput) => Promise<NativeSession>;
186
110
  export declare function createNativeAgentSession(input: SessionInput): Promise<NativeSession>;
187
111
  export declare class WorkflowAgentExecutor {
188
112
  private readonly root;
@@ -265,6 +189,7 @@ export declare class FairAgentScheduler {
265
189
  private readonly writeOwnership?;
266
190
  constructor(runner: ScheduledAgentRunner, sessionLimit?: number, writeOwnership?: OwnershipWriter | undefined);
267
191
  addRun(runId: string, limit?: number, beforeLaunch?: () => void): void;
192
+ updateRunLimit(runId: string, limit: number): void;
268
193
  spawn(runId: string, prompt: string, options: ScheduledAgentOptions, parentId?: string): {
269
194
  id: string;
270
195
  result: Promise<ScheduledAgentResult>;
@@ -282,4 +207,3 @@ export declare class FairAgentScheduler {
282
207
  restoreRun(runId: string, limit: number, ownership: readonly OwnershipRecord[], beforeLaunch?: () => void): void;
283
208
  flush(): Promise<void>;
284
209
  }
285
- export {};
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { Type } from "@earendil-works/pi-ai";
4
4
  import { Value } from "typebox/value";
5
5
  import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
6
- import { jsonObject, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference } from "./utils.js";
6
+ import { jsonObject, disabledResources, mergeAgentResourceExclusions, modelAliasName, modelCapability, resolveModelReference, unmatchedResourcePatterns } from "./utils.js";
7
7
  import { WorkflowError } from "./types.js";
8
8
  function parseModel(value, fallback, thinking, aliases = {}, knownModels, settingsPath) {
9
9
  if (!value)
@@ -69,14 +69,17 @@ export async function createNativeAgentSession(input) {
69
69
  settingsManager.setProjectTrusted(policy.projectTrusted);
70
70
  const packageManager = new DefaultPackageManager({ cwd: input.cwd, agentDir, settingsManager });
71
71
  const resolved = await packageManager.resolve();
72
- const disabledExtensions = new Set(policy.effective.extensions);
73
- const extensionPaths = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)).filter((path) => !disabledExtensions.has(canonicalSourcePath(path))))];
72
+ const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)))];
73
+ const excludedExtensions = new Set(disabledResources(policy.effective.extensions, discoveredExtensions));
74
+ const extensionPaths = discoveredExtensions.filter((path) => !excludedExtensions.has(path));
75
+ Object.assign(policy, { excludedExtensions: [...excludedExtensions], unmatchedExtensions: unmatchedResourcePatterns(policy.effective.extensions, discoveredExtensions) });
74
76
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
75
77
  const updateSkillMatches = (skills) => {
76
- const names = new Set(skills.map(({ name }) => name));
77
- Object.assign(policy, { unmatchedSkills: policy.effective.skills.filter((name) => !names.has(name)) });
78
+ const names = [...new Set(skills.map(({ name }) => name))];
79
+ const excludedSkills = disabledResources(policy.effective.skills, names);
80
+ Object.assign(policy, { excludedSkills, unmatchedSkills: unmatchedResourcePatterns(policy.effective.skills, names) });
81
+ return new Set(excludedSkills);
78
82
  };
79
- const disabledSkills = new Set(policy.effective.skills);
80
83
  resourceLoader = new DefaultResourceLoader({
81
84
  cwd: input.cwd,
82
85
  agentDir,
@@ -87,14 +90,12 @@ export async function createNativeAgentSession(input) {
87
90
  additionalSkillPaths: skillPaths,
88
91
  ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}),
89
92
  skillsOverride: (base) => {
90
- updateSkillMatches(base.skills);
93
+ const disabledSkills = updateSkillMatches(base.skills);
91
94
  return { ...base, skills: base.skills.filter(({ name }) => !disabledSkills.has(name)) };
92
95
  },
93
96
  ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}),
94
97
  });
95
98
  await resourceLoader.reload();
96
- const discoveredExtensions = new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalSourcePath(path)));
97
- Object.assign(policy, { unmatchedExtensions: policy.effective.extensions.filter((path) => !discoveredExtensions.has(canonicalSourcePath(path))) });
98
99
  }
99
100
  else if (input.systemPromptAppend || input.extensionFactories?.length) {
100
101
  resourceLoader = new DefaultResourceLoader({ cwd: input.cwd, agentDir, ...(input.extensionFactories?.length ? { extensionFactories: input.extensionFactories } : {}), ...(input.systemPromptAppend ? { appendSystemPromptOverride: (base) => [...base, input.systemPromptAppend ?? ""] } : {}) });
@@ -133,7 +134,7 @@ function fallbackSetupContext(root, options, signal) {
133
134
  return { run, identity: Object.freeze({ ...identity, structuralPath: Object.freeze([...identity.structuralPath]) }) };
134
135
  }
135
136
  function resourcePolicySummary(policy) {
136
- return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
137
+ return { skills: [...policy.effective.skills], extensions: [...policy.effective.extensions], excludedSkills: [...(policy.excludedSkills ?? [])], excludedExtensions: [...(policy.excludedExtensions ?? [])], unmatchedSkills: [...policy.unmatchedSkills], unmatchedExtensions: [...policy.unmatchedExtensions] };
137
138
  }
138
139
  async function prepareAgentSetup(root, createSession, task, options, resolved, cwd, attempt, signal, customTools, resultTool) {
139
140
  const setupSignal = signal ?? root.runContext?.signal ?? new AbortController().signal;
@@ -520,6 +521,15 @@ export class FairAgentScheduler {
520
521
  this.#runs.set(runId, { limit, ...(beforeLaunch ? { beforeLaunch } : {}), logical: 0, active: 0, queue: [] });
521
522
  this.#runOrder.push(runId);
522
523
  }
524
+ updateRunLimit(runId, limit) {
525
+ const run = this.#runs.get(runId);
526
+ if (!run)
527
+ throw new WorkflowError("INTERNAL_ERROR", `Unknown scheduler run: ${runId}`);
528
+ if (!Number.isInteger(limit) || limit < 1 || limit > this.sessionLimit)
529
+ throw new WorkflowError("INVALID_SETTINGS", "Invalid run concurrency");
530
+ run.limit = limit;
531
+ this.#dispatch();
532
+ }
523
533
  spawn(runId, prompt, options, parentId) {
524
534
  const run = this.#runs.get(runId);
525
535
  if (!run)
package/dist/src/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { type DoctorOptions } from "./doctor.js";
3
+ import { type DoctorCleanupOptions } from "./doctor-cleanup.js";
3
4
  import { type JsonSchema, type JsonValue } from "./index.js";
4
5
  import type { WorkflowCatalogFunction } from "./index.js";
5
6
  export interface CliOptions extends DoctorOptions {
@@ -12,4 +13,5 @@ export interface CliOptions extends DoctorOptions {
12
13
  }
13
14
  export declare function formatWorkflowCliHelp(fn: WorkflowCatalogFunction, command?: string): string;
14
15
  export declare function parseWorkflowCliArgs(schema: JsonSchema, rawArgs: readonly string[]): Record<string, JsonValue>;
16
+ export declare function parseDoctorCleanupArgs(rawArgs: readonly string[]): Required<Pick<DoctorCleanupOptions, "olderThanDays" | "yes">>;
15
17
  export declare function runCli(args: readonly string[], options?: CliOptions, write?: (text: string) => void): Promise<number>;
package/dist/src/cli.js CHANGED
@@ -7,7 +7,8 @@ import { fileURLToPath, pathToFileURL } 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
- import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog } from "./index.js";
10
+ import { doctorCleanup, doctorCleanupExitCode, formatDoctorCleanupReport } from "./doctor-cleanup.js";
11
+ import workflowExtension, { formatWorkflowProgress, truncateWorkflowProgress, workflowCatalog, workflowSettingsPath } from "./index.js";
11
12
  import { runSessionInspector, transcriptFileLines } from "./session-inspector.js";
12
13
  function object(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }
13
14
  function has(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
@@ -193,6 +194,34 @@ function launcherHelpLines() {
193
194
  }
194
195
  function workflowUsage() { return [`Usage: pi-extensible-workflows run <workflow-name> [workflow arguments] | export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
195
196
  function exportUsage() { return [`Usage: pi-extensible-workflows export <workflow-name> [--name <command>] [--output <path>] [--force]`, "", "Launcher options:", ...launcherHelpLines()].join("\n") + "\n"; }
197
+ export function parseDoctorCleanupArgs(rawArgs) {
198
+ let olderThanDays = 90;
199
+ let yes = false;
200
+ let seenDays = false;
201
+ for (let index = 0; index < rawArgs.length; index += 1) {
202
+ const token = rawArgs[index];
203
+ if (token === "--yes") {
204
+ yes = true;
205
+ continue;
206
+ }
207
+ const inline = token.startsWith("--older-than-days=") ? token.slice("--older-than-days=".length) : undefined;
208
+ if (token === "--older-than-days" || inline !== undefined) {
209
+ if (seenDays)
210
+ throw new Error("--older-than-days may only be provided once");
211
+ const raw = inline ?? rawArgs[++index];
212
+ if (raw === undefined || !/^[1-9]\d*$/.test(raw))
213
+ throw new Error("older-than-days must be a positive integer");
214
+ const parsed = Number(raw);
215
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
216
+ throw new Error("older-than-days must be a positive integer");
217
+ olderThanDays = parsed;
218
+ seenDays = true;
219
+ continue;
220
+ }
221
+ throw new Error(`Unknown cleanup option: ${token}`);
222
+ }
223
+ return { olderThanDays, yes };
224
+ }
196
225
  function stripTrustOptions(rawArgs) {
197
226
  const args = [];
198
227
  let trustOverride;
@@ -278,7 +307,7 @@ async function createWorkflowRuntime(options, shutdownHandlers = []) {
278
307
  const workflowTool = tools.find((tool) => object(tool) && tool.name === "workflow");
279
308
  if (!workflowTool)
280
309
  throw new Error("The workflow runtime could not be initialized");
281
- return { catalog: workflowCatalog(), services, workflowTool, shutdownHandlers };
310
+ return { catalog: workflowCatalog({ cwd, projectTrusted: settingsManager.isProjectTrusted(), globalSettingsPath: workflowSettingsPath(agentDir) }), services, workflowTool, shutdownHandlers };
282
311
  }
283
312
  function availableModelInfo(services, available = false) {
284
313
  const models = available ? services.modelRuntime.getAvailableSnapshot() : services.modelRuntime.getModels();
@@ -512,6 +541,23 @@ export async function runCli(args, options = {}, write = (text) => { process.std
512
541
  write(formatDoctorReport(report));
513
542
  return doctorExitCode(report);
514
543
  }
544
+ if (args[0] === "doctor" && args[1] === "cleanup") {
545
+ if (args.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
546
+ write("Usage: pi-extensible-workflows doctor cleanup [--older-than-days <days>] [--yes]\n");
547
+ return 0;
548
+ }
549
+ try {
550
+ const parsed = parseDoctorCleanupArgs(args.slice(2));
551
+ const cleanupOptions = { ...parsed, ...(options.cwd !== undefined ? { cwd: options.cwd } : {}) };
552
+ const report = await doctorCleanup(cleanupOptions);
553
+ write(formatDoctorCleanupReport(report));
554
+ return doctorCleanupExitCode(report);
555
+ }
556
+ catch (error) {
557
+ stderr(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
558
+ return 1;
559
+ }
560
+ }
515
561
  if (args[0] === "inspect" && args.length <= 2) {
516
562
  try {
517
563
  await (options.inspect ?? runSessionInspector)(args[1]);
@@ -0,0 +1,41 @@
1
+ export interface DoctorCleanupOptions {
2
+ cwd?: string;
3
+ home?: string;
4
+ olderThanDays?: number;
5
+ yes?: boolean;
6
+ now?: number;
7
+ }
8
+ export interface CleanupRunResult {
9
+ sessionId: string;
10
+ runId: string;
11
+ action: "candidate" | "skipped" | "deleted" | "failed";
12
+ state: string;
13
+ stateMtimeMs: number;
14
+ path: string;
15
+ reason?: string;
16
+ }
17
+ export interface CleanupFailure {
18
+ sessionId: string;
19
+ runId?: string;
20
+ message: string;
21
+ }
22
+ export interface CleanupSessionReport {
23
+ sessionId: string;
24
+ path: string;
25
+ status: "preview" | "cleaned" | "skipped" | "failed";
26
+ reason?: string;
27
+ }
28
+ export interface DoctorCleanupReport {
29
+ cwd: string;
30
+ cutoffMs: number;
31
+ olderThanDays: number;
32
+ yes: boolean;
33
+ sessions: readonly CleanupSessionReport[];
34
+ candidates: readonly CleanupRunResult[];
35
+ skipped: readonly CleanupRunResult[];
36
+ deleted: readonly CleanupRunResult[];
37
+ failures: readonly CleanupFailure[];
38
+ }
39
+ export declare function doctorCleanup(options?: DoctorCleanupOptions): Promise<DoctorCleanupReport>;
40
+ export declare function doctorCleanupExitCode(report: DoctorCleanupReport): 0 | 1;
41
+ export declare function formatDoctorCleanupReport(report: DoctorCleanupReport): string;