pi-extensible-workflows 2.0.0 → 3.1.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 (44) hide show
  1. package/README.md +18 -11
  2. package/dist/src/agent-execution.d.ts +4 -94
  3. package/dist/src/agent-execution.js +34 -208
  4. package/dist/src/budget.d.ts +38 -0
  5. package/dist/src/budget.js +160 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +60 -8
  8. package/dist/src/doctor-cleanup.d.ts +41 -0
  9. package/dist/src/doctor-cleanup.js +597 -0
  10. package/dist/src/doctor.d.ts +7 -2
  11. package/dist/src/doctor.js +127 -22
  12. package/dist/src/execution.d.ts +17 -0
  13. package/dist/src/execution.js +630 -0
  14. package/dist/src/host.d.ts +101 -0
  15. package/dist/src/host.js +3084 -0
  16. package/dist/src/index.d.ts +13 -634
  17. package/dist/src/index.js +10 -4432
  18. package/dist/src/persistence.d.ts +9 -19
  19. package/dist/src/persistence.js +175 -61
  20. package/dist/src/registry.d.ts +43 -0
  21. package/dist/src/registry.js +279 -0
  22. package/dist/src/session-inspector.js +5 -3
  23. package/dist/src/types.d.ts +692 -0
  24. package/dist/src/types.js +27 -0
  25. package/dist/src/utils.d.ts +32 -0
  26. package/dist/src/utils.js +168 -0
  27. package/dist/src/validation.d.ts +28 -0
  28. package/dist/src/validation.js +832 -0
  29. package/package.json +3 -2
  30. package/skills/pi-extensible-workflows/SKILL.md +69 -34
  31. package/src/agent-execution.ts +37 -189
  32. package/src/budget.ts +75 -0
  33. package/src/cli.ts +47 -7
  34. package/src/doctor-cleanup.ts +337 -0
  35. package/src/doctor.ts +117 -24
  36. package/src/execution.ts +540 -0
  37. package/src/host.ts +2598 -0
  38. package/src/index.ts +14 -3856
  39. package/src/persistence.ts +122 -37
  40. package/src/registry.ts +240 -0
  41. package/src/session-inspector.ts +5 -3
  42. package/src/types.ts +158 -0
  43. package/src/utils.ts +142 -0
  44. package/src/validation.ts +688 -0
@@ -0,0 +1,38 @@
1
+ import { type BudgetDimension, type BudgetEvent, type WorkflowBudget, type WorkflowBudgetPatch, type WorkflowBudgetUsage, type AgentAccounting, type RunState } from "./types.js";
2
+ export declare function validateBudget(value: unknown): WorkflowBudget | undefined;
3
+ export declare function validateBudgetPatch(value: unknown): WorkflowBudgetPatch;
4
+ export declare function budgetUsage(value?: Partial<WorkflowBudgetUsage>): WorkflowBudgetUsage;
5
+ export declare class WorkflowBudgetRuntime {
6
+ #private;
7
+ readonly budget: WorkflowBudget | undefined;
8
+ readonly version: number;
9
+ constructor(budget: WorkflowBudget | undefined, version?: number, usage?: Partial<WorkflowBudgetUsage>, events?: readonly BudgetEvent[], options?: {
10
+ now?: () => number;
11
+ onChange?: () => void;
12
+ active?: boolean;
13
+ });
14
+ get usage(): WorkflowBudgetUsage;
15
+ get events(): readonly BudgetEvent[];
16
+ get hardExhausted(): boolean;
17
+ checkAgentLaunch(): void;
18
+ beforeAttempt(): void;
19
+ beforeTurn(): void;
20
+ afterTurn(accounting: AgentAccounting, final: boolean): void;
21
+ instruction(agentId?: string): string | undefined;
22
+ forAgent(agentId: string): {
23
+ beforeAttempt: () => void;
24
+ beforeTurn: () => void;
25
+ afterTurn: (accounting: AgentAccounting, final: boolean) => void;
26
+ instruction: () => string | undefined;
27
+ };
28
+ transition(state: RunState): void;
29
+ recordEvent(event: BudgetEvent): void;
30
+ snapshot(): {
31
+ usage: WorkflowBudgetUsage;
32
+ budgetEvents: readonly BudgetEvent[];
33
+ };
34
+ }
35
+ export declare function mergeBudget(budget: WorkflowBudget | undefined, patch: WorkflowBudgetPatch): WorkflowBudget | undefined;
36
+ export declare function budgetRelaxed(previous: WorkflowBudget | undefined, next: WorkflowBudget | undefined): boolean;
37
+ export declare function exhaustedBudgetDimensions(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): BudgetDimension[];
38
+ export declare function resumeBudgetAllowed(budget: WorkflowBudget | undefined, usage: WorkflowBudgetUsage): boolean;
@@ -0,0 +1,160 @@
1
+ import { WorkflowError } from "./types.js";
2
+ import { fail, object } from "./utils.js";
3
+ function nonNegativeInteger(value) { return Number.isInteger(value) && value >= 0; }
4
+ function nonNegativeFinite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0; }
5
+ export function validateBudget(value) {
6
+ if (value === undefined)
7
+ return undefined;
8
+ if (!object(value))
9
+ fail("INVALID_METADATA", "budget must be an object");
10
+ const result = {};
11
+ for (const [dimension, raw] of Object.entries(value)) {
12
+ if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
13
+ fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
14
+ if (!object(raw))
15
+ fail("INVALID_METADATA", `${dimension} budget must be an object`);
16
+ if (Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
17
+ fail("INVALID_METADATA", `${dimension} budget has an unknown limit`);
18
+ const isCost = dimension === "costUsd";
19
+ for (const key of ["soft", "hard"])
20
+ if (raw[key] !== undefined && !(isCost ? nonNegativeFinite(raw[key]) : nonNegativeInteger(raw[key])))
21
+ fail("INVALID_METADATA", `${dimension}.${key} must be a non-negative ${isCost ? "finite number" : "integer"}`);
22
+ if (raw.soft !== undefined && raw.soft !== null && raw.hard !== undefined && raw.hard !== null && raw.soft >= raw.hard)
23
+ fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
24
+ const limits = {};
25
+ if (raw.soft !== undefined)
26
+ limits.soft = raw.soft;
27
+ if (raw.hard !== undefined)
28
+ limits.hard = raw.hard;
29
+ if (Object.keys(limits).length)
30
+ result[dimension] = limits;
31
+ }
32
+ return Object.freeze(result);
33
+ }
34
+ export function validateBudgetPatch(value) {
35
+ if (!object(value))
36
+ fail("INVALID_METADATA", "budget patch must be an object");
37
+ const result = {};
38
+ for (const [dimension, raw] of Object.entries(value)) {
39
+ if (!["tokens", "costUsd", "durationMs", "agentLaunches"].includes(dimension))
40
+ fail("INVALID_METADATA", `Unknown budget dimension: ${dimension}`);
41
+ if (raw === null) {
42
+ result[dimension] = null;
43
+ continue;
44
+ }
45
+ if (!object(raw) || Object.keys(raw).some((key) => key !== "soft" && key !== "hard"))
46
+ fail("INVALID_METADATA", `${dimension} budget patch must contain only soft and hard`);
47
+ const limits = {};
48
+ for (const key of ["soft", "hard"])
49
+ if (Object.prototype.hasOwnProperty.call(raw, key)) {
50
+ if (raw[key] === null)
51
+ limits[key] = null;
52
+ else {
53
+ const checked = validateBudget({ [dimension]: { [key]: raw[key] } })?.[dimension];
54
+ if (checked?.[key] !== undefined)
55
+ limits[key] = checked[key];
56
+ }
57
+ }
58
+ if (limits.soft !== null && limits.hard !== null && limits.soft !== undefined && limits.hard !== undefined && limits.soft >= limits.hard)
59
+ fail("INVALID_METADATA", `${dimension}.soft must be less than hard`);
60
+ result[dimension] = limits;
61
+ }
62
+ return result;
63
+ }
64
+ export function budgetUsage(value) { return { tokens: value?.tokens ?? 0, costUsd: value?.costUsd ?? 0, durationMs: value?.durationMs ?? 0, agentLaunches: value?.agentLaunches ?? 0 }; }
65
+ export class WorkflowBudgetRuntime {
66
+ budget;
67
+ version;
68
+ #now;
69
+ #onChange;
70
+ #injected = new Set();
71
+ #seen = new Set();
72
+ #active;
73
+ #activeSince;
74
+ #usage;
75
+ #events;
76
+ #turnAccounting;
77
+ constructor(budget, version = 1, usage, events = [], options = {}) {
78
+ this.budget = budget;
79
+ this.version = version;
80
+ this.#now = options.now ?? (() => Date.now());
81
+ this.#onChange = options.onChange;
82
+ this.#active = options.active ?? true;
83
+ this.#activeSince = this.#active ? this.#now() : undefined;
84
+ this.#usage = budgetUsage(usage);
85
+ this.#events = [...events];
86
+ for (const event of events)
87
+ if (event.budgetVersion === version)
88
+ this.#seen.add(event.type);
89
+ }
90
+ get usage() { this.#syncDuration(); return { ...this.#usage }; }
91
+ get events() { return this.#events; }
92
+ get hardExhausted() { return this.#events.some((event) => event.type === "hard_exhausted" && event.budgetVersion === this.version); }
93
+ checkAgentLaunch() { this.#checkHard(["agentLaunches"]); }
94
+ beforeAttempt() { this.#checkHard(["agentLaunches"]); this.#usage.agentLaunches += 1; this.#evaluate(); }
95
+ beforeTurn() { this.#syncDuration(); this.#evaluate(); this.#checkHard(["tokens", "costUsd", "durationMs"]); }
96
+ afterTurn(accounting, final) { this.#syncDuration(); this.#applyTurn(accounting, final, this.#turnAccounting); this.#turnAccounting = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }
97
+ #applyTurn(accounting, final, previous = { input: 0, output: 0, cost: 0 }) { this.#usage.tokens += Math.max(0, accounting.input - previous.input) + Math.max(0, accounting.output - previous.output); this.#usage.costUsd += Math.max(0, accounting.cost - previous.cost); this.#evaluate(); if (!final)
98
+ this.#checkHard(["tokens", "costUsd", "durationMs"]); }
99
+ instruction(agentId = "agent") { if (!this.#hasSoftCrossed() || this.#injected.has(agentId))
100
+ return undefined; this.#injected.add(agentId); return `The workflow budget soft limit has been reached. Finish the requested output now, preserving any required output schema. Current usage: ${JSON.stringify(this.usage)}. Do not start additional model work unless it is required to produce the final requested result.`; }
101
+ forAgent(agentId) { let attempt = 0; let previous; return { beforeAttempt: () => { attempt += 1; previous = undefined; this.beforeAttempt(); }, beforeTurn: () => { this.beforeTurn(); }, afterTurn: (accounting, final) => { this.#applyTurn(accounting, final, previous); previous = { input: accounting.input, output: accounting.output, cost: accounting.cost }; }, instruction: () => this.instruction(`${agentId}:${String(attempt + 1)}`) }; }
102
+ transition(state) { const active = state === "running"; if (active === this.#active)
103
+ return; if (active) {
104
+ this.#active = true;
105
+ this.#activeSince = this.#now();
106
+ }
107
+ else {
108
+ this.#syncDuration();
109
+ this.#evaluate();
110
+ this.#active = false;
111
+ this.#activeSince = undefined;
112
+ } this.#onChange?.(); }
113
+ #syncDuration() { if (this.#active && this.#activeSince !== undefined) {
114
+ const now = this.#now();
115
+ this.#usage.durationMs += Math.max(0, now - this.#activeSince);
116
+ this.#activeSince = now;
117
+ } }
118
+ #hasSoftCrossed() { return !!this.budget && Object.entries(this.budget).some(([dimension, limits]) => limits.soft !== undefined && this.#usage[dimension] >= limits.soft); }
119
+ #checkHard(dimensions) { const exhausted = dimensions.filter((dimension) => { const hard = this.budget?.[dimension]?.hard; return hard !== undefined && this.#usage[dimension] >= hard; }); if (!exhausted.length)
120
+ return; this.#record("hard_exhausted", exhausted); const detail = exhausted.map((dimension) => `${dimension} usage=${String(this.#usage[dimension])} hard=${String(this.budget?.[dimension]?.hard)}`).join(", "); throw new WorkflowError("BUDGET_EXHAUSTED", `Budget version ${String(this.version)} exhausted: ${detail}`); }
121
+ #evaluate() { const budget = this.budget; if (!budget)
122
+ return; const soft = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.soft !== undefined && this.#usage[dimension] >= limits.soft; }); if (soft.length)
123
+ this.#record("soft_crossed", soft); const overrun = Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && this.#usage[dimension] > limits.hard; }); if (overrun.length)
124
+ this.#record("hard_overrun", overrun); }
125
+ #record(type, dimensions) { if (this.#seen.has(type))
126
+ return; this.#seen.add(type); this.#events.push({ type, budgetVersion: this.version, dimensions: [...dimensions], usage: this.usage, limits: structuredClone(this.budget ?? {}), at: this.#now() }); this.#onChange?.(); }
127
+ recordEvent(event) { this.#events.push(structuredClone(event)); }
128
+ snapshot() { return { usage: this.usage, budgetEvents: [...this.#events] }; }
129
+ }
130
+ export function mergeBudget(budget, patch) { const merged = structuredClone(budget ?? {}); for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"])
131
+ if (Object.prototype.hasOwnProperty.call(patch, dimension)) {
132
+ const value = patch[dimension];
133
+ if (value === null) {
134
+ Reflect.deleteProperty(merged, dimension);
135
+ continue;
136
+ }
137
+ const next = { ...(merged[dimension] ?? {}) };
138
+ for (const key of ["soft", "hard"])
139
+ if (value && Object.prototype.hasOwnProperty.call(value, key)) {
140
+ const limit = value[key];
141
+ if (limit === null)
142
+ Reflect.deleteProperty(next, key);
143
+ else if (limit !== undefined)
144
+ next[key] = limit;
145
+ }
146
+ if (Object.keys(next).length)
147
+ merged[dimension] = next;
148
+ else
149
+ Reflect.deleteProperty(merged, dimension);
150
+ } return validateBudget(merged); }
151
+ export function budgetRelaxed(previous, next) { for (const dimension of ["tokens", "costUsd", "durationMs", "agentLaunches"]) {
152
+ const oldLimit = previous?.[dimension];
153
+ const newLimit = next?.[dimension];
154
+ for (const key of ["soft", "hard"])
155
+ if ((oldLimit?.[key] !== undefined && newLimit?.[key] === undefined) || (oldLimit?.[key] !== undefined && newLimit?.[key] !== undefined && newLimit[key] > oldLimit[key]))
156
+ return true;
157
+ } return false; }
158
+ export function exhaustedBudgetDimensions(budget, usage) { if (!budget)
159
+ return []; return Object.keys(budget).filter((dimension) => { const limits = budget[dimension]; return limits !== undefined && limits.hard !== undefined && usage[dimension] >= limits.hard; }); }
160
+ export function resumeBudgetAllowed(budget, usage) { return exhaustedBudgetDimensions(budget, usage).length === 0; }
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, 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();
@@ -321,21 +350,27 @@ function writeLauncher(destination, workflowName, force) {
321
350
  rmSync(tempDir, { recursive: true, force: true });
322
351
  }
323
352
  }
353
+ function terminalProgressStyles(enabled) {
354
+ const style = (code) => enabled ? (text) => `\x1b[${String(code)}m${text}\x1b[0m` : (text) => text;
355
+ return { accent: style(36), success: style(32), error: style(31), warning: style(33), muted: style(90), dim: style(2), bold: style(1) };
356
+ }
324
357
  class CliProgress {
325
358
  stderr;
326
- tty;
327
359
  #lastStable = "";
328
360
  #lines = 0;
329
361
  #frame = 0;
330
362
  #run;
331
363
  #timer;
364
+ #interactive;
365
+ #styles;
332
366
  constructor(stderr, tty) {
333
367
  this.stderr = stderr;
334
- this.tty = tty;
368
+ this.#interactive = tty && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
369
+ this.#styles = terminalProgressStyles(this.#interactive);
335
370
  }
336
371
  update(run) {
337
- const stable = formatWorkflowProgress(run, "◇");
338
- if (!this.tty) {
372
+ const stable = formatWorkflowProgress(run, "◇", this.#styles);
373
+ if (!this.#interactive) {
339
374
  if (stable !== this.#lastStable) {
340
375
  this.#lastStable = stable;
341
376
  this.stderr(`${stable}\n`);
@@ -352,7 +387,7 @@ class CliProgress {
352
387
  return;
353
388
  const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][this.#frame++ % 10] ?? "◇";
354
389
  const width = process.stderr.columns || 80;
355
- const text = formatWorkflowProgress(this.#run, spinner).split("\n").map((line) => line.length <= width ? line : `${line.slice(0, Math.max(0, width - 1))}…`).join("\n");
390
+ const text = truncateWorkflowProgress(formatWorkflowProgress(this.#run, spinner, this.#styles), width).join("\n");
356
391
  this.stderr(`${this.#lines ? `\x1b[${String(this.#lines)}A` : ""}${this.#lines ? "" : "\x1b[?25l"}\x1b[0J${text}\n`);
357
392
  this.#lines = text.split("\n").length;
358
393
  }
@@ -361,7 +396,7 @@ class CliProgress {
361
396
  clearInterval(this.#timer);
362
397
  this.#timer = undefined;
363
398
  }
364
- if (this.tty && this.#lines) {
399
+ if (this.#interactive && this.#lines) {
365
400
  this.stderr(`\x1b[${String(this.#lines)}A\x1b[0J\x1b[?25h`);
366
401
  this.#lines = 0;
367
402
  }
@@ -506,6 +541,23 @@ export async function runCli(args, options = {}, write = (text) => { process.std
506
541
  write(formatDoctorReport(report));
507
542
  return doctorExitCode(report);
508
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
+ }
509
561
  if (args[0] === "inspect" && args.length <= 2) {
510
562
  try {
511
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;