@jameslovespancakes/pi-plus 1.0.10 → 1.0.12

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 (120) hide show
  1. package/README.md +44 -20
  2. package/config/skills/workflow-code-review-actions/SKILL.md +44 -0
  3. package/package.json +6 -3
  4. package/src/core/accounts/oauth-pool.ts +102 -0
  5. package/src/core/accounts/registry.ts +8 -25
  6. package/src/core/accounts/routing.ts +122 -0
  7. package/src/core/anthropic/client-identity.ts +9 -64
  8. package/src/core/anthropic/quota.ts +8 -53
  9. package/src/core/anthropic/routing.ts +40 -115
  10. package/src/core/anthropic/store.ts +7 -9
  11. package/src/core/catalog/quality.ts +33 -15
  12. package/src/core/codex/quota.ts +8 -2
  13. package/src/core/codex/store.ts +12 -9
  14. package/src/core/config.ts +4 -17
  15. package/src/core/store.ts +13 -12
  16. package/src/domains/models/provider-picker.ts +1 -1
  17. package/src/domains/setup/index.ts +23 -23
  18. package/src/domains/subscriptions/accounts-picker.ts +18 -37
  19. package/src/domains/subscriptions/accounts.ts +23 -29
  20. package/src/domains/subscriptions/footer.ts +48 -29
  21. package/src/domains/subscriptions/index.ts +11 -27
  22. package/src/domains/subscriptions/provider.ts +38 -103
  23. package/src/domains/subscriptions/providers/anthropic.ts +5 -17
  24. package/src/domains/subscriptions/providers/codex.ts +171 -4
  25. package/src/domains/subscriptions/providers/hosted.ts +18 -0
  26. package/src/domains/subscriptions/providers/oauth-pool.ts +273 -0
  27. package/src/domains/subscriptions/routing.ts +19 -11
  28. package/src/domains/workflows/LICENSE.md +21 -0
  29. package/src/domains/workflows/index.ts +836 -0
  30. package/src/domains/workflows/runtime/advisory-challenge.ts +75 -0
  31. package/src/domains/workflows/runtime/advisory-evidence.ts +90 -0
  32. package/src/domains/workflows/runtime/advisory-schema.ts +83 -0
  33. package/src/domains/workflows/runtime/agent-attempt.ts +147 -0
  34. package/src/domains/workflows/runtime/agent-limits.ts +43 -0
  35. package/src/domains/workflows/runtime/agent-replay.ts +399 -0
  36. package/src/domains/workflows/runtime/agent-retry.ts +116 -0
  37. package/src/domains/workflows/runtime/agent-runner-types.ts +96 -0
  38. package/src/domains/workflows/runtime/agent-runner.ts +203 -0
  39. package/src/domains/workflows/runtime/agent-session-identity.ts +256 -0
  40. package/src/domains/workflows/runtime/agent-session-providers.ts +50 -0
  41. package/src/domains/workflows/runtime/agent-session.ts +382 -0
  42. package/src/domains/workflows/runtime/agent-skills.ts +270 -0
  43. package/src/domains/workflows/runtime/agent-workspace.ts +103 -0
  44. package/src/domains/workflows/runtime/background-workflow-tool.ts +75 -0
  45. package/src/domains/workflows/runtime/background-workflows.ts +492 -0
  46. package/src/domains/workflows/runtime/budget.ts +53 -0
  47. package/src/domains/workflows/runtime/cancellation.ts +87 -0
  48. package/src/domains/workflows/runtime/command-completions.ts +36 -0
  49. package/src/domains/workflows/runtime/concurrency.ts +403 -0
  50. package/src/domains/workflows/runtime/debug.ts +3 -0
  51. package/src/domains/workflows/runtime/diff-capture.ts +81 -0
  52. package/src/domains/workflows/runtime/discovery.ts +137 -0
  53. package/src/domains/workflows/runtime/dynamax-shortcuts.ts +122 -0
  54. package/src/domains/workflows/runtime/dynamax.ts +330 -0
  55. package/src/domains/workflows/runtime/engine.ts +543 -0
  56. package/src/domains/workflows/runtime/filesystem-error.ts +4 -0
  57. package/src/domains/workflows/runtime/finalizers.ts +66 -0
  58. package/src/domains/workflows/runtime/identity-canonicalization.ts +138 -0
  59. package/src/domains/workflows/runtime/identity-fingerprint.ts +15 -0
  60. package/src/domains/workflows/runtime/inline-workflow.ts +403 -0
  61. package/src/domains/workflows/runtime/journal.ts +313 -0
  62. package/src/domains/workflows/runtime/model-profiles.ts +310 -0
  63. package/src/domains/workflows/runtime/options.ts +157 -0
  64. package/src/domains/workflows/runtime/perf.ts +146 -0
  65. package/src/domains/workflows/runtime/pi-compat.ts +32 -0
  66. package/src/domains/workflows/runtime/process-runner.ts +260 -0
  67. package/src/domains/workflows/runtime/progress-types.ts +59 -0
  68. package/src/domains/workflows/runtime/progress.ts +400 -0
  69. package/src/domains/workflows/runtime/provider-usage-limit.ts +191 -0
  70. package/src/domains/workflows/runtime/replay-path-identity.ts +48 -0
  71. package/src/domains/workflows/runtime/research-contract.ts +89 -0
  72. package/src/domains/workflows/runtime/research-evidence.ts +289 -0
  73. package/src/domains/workflows/runtime/resume-context.ts +751 -0
  74. package/src/domains/workflows/runtime/review/code-review-orchestration.ts +24 -0
  75. package/src/domains/workflows/runtime/review/github-pr-comments.ts +249 -0
  76. package/src/domains/workflows/runtime/review/patch-validation.ts +120 -0
  77. package/src/domains/workflows/runtime/review/review-actions.ts +180 -0
  78. package/src/domains/workflows/runtime/review/review-budget.ts +45 -0
  79. package/src/domains/workflows/runtime/review/review-fix-workflow.ts +153 -0
  80. package/src/domains/workflows/runtime/review/review-format.ts +133 -0
  81. package/src/domains/workflows/runtime/review/review-handoff.ts +38 -0
  82. package/src/domains/workflows/runtime/review/review-issues.ts +87 -0
  83. package/src/domains/workflows/runtime/review/review-report.ts +42 -0
  84. package/src/domains/workflows/runtime/review/review-results-flow.ts +52 -0
  85. package/src/domains/workflows/runtime/review/review-results-viewer.ts +312 -0
  86. package/src/domains/workflows/runtime/review/review-session-coordinator.ts +153 -0
  87. package/src/domains/workflows/runtime/review/review-snapshot.ts +294 -0
  88. package/src/domains/workflows/runtime/review-diff-target.ts +130 -0
  89. package/src/domains/workflows/runtime/session-identity.ts +6 -0
  90. package/src/domains/workflows/runtime/structured-output.ts +32 -0
  91. package/src/domains/workflows/runtime/tool-capabilities.ts +44 -0
  92. package/src/domains/workflows/runtime/tool-source-identity.ts +150 -0
  93. package/src/domains/workflows/runtime/tree-fingerprint.ts +404 -0
  94. package/src/domains/workflows/runtime/types.ts +255 -0
  95. package/src/domains/workflows/runtime/ui/display-text.ts +13 -0
  96. package/src/domains/workflows/runtime/ui/dynamax-editor-decoration.ts +220 -0
  97. package/src/domains/workflows/runtime/ui/workflow-format.ts +165 -0
  98. package/src/domains/workflows/runtime/ui/workflow-inspector.ts +398 -0
  99. package/src/domains/workflows/runtime/ui/workflow-result-renderer.ts +171 -0
  100. package/src/domains/workflows/runtime/ui/workflow-viewer-layout.ts +51 -0
  101. package/src/domains/workflows/runtime/ui/workflow-widget.ts +78 -0
  102. package/src/domains/workflows/runtime/unknown-error.ts +8 -0
  103. package/src/domains/workflows/runtime/usage.ts +341 -0
  104. package/src/domains/workflows/runtime/workflow-advisory-utils.ts +245 -0
  105. package/src/domains/workflows/runtime/workflow-execution.ts +121 -0
  106. package/src/domains/workflows/runtime/workflow-module.ts +75 -0
  107. package/src/domains/workflows/runtime/workflow-run-background.ts +72 -0
  108. package/src/domains/workflows/runtime/workflow-run-controller.ts +342 -0
  109. package/src/domains/workflows/runtime/workflow-run-history.ts +162 -0
  110. package/src/domains/workflows/runtime/workflow-run-record.ts +635 -0
  111. package/src/domains/workflows/runtime/workflow-run-store.ts +176 -0
  112. package/src/domains/workflows/runtime/workflow-usage-limit-scheduler.ts +80 -0
  113. package/src/domains/workflows/runtime/workflows.ts +40 -0
  114. package/src/domains/workflows/runtime/worktree.ts +581 -0
  115. package/src/domains/workflows/workflows/code-review.ts +232 -0
  116. package/src/domains/workflows/workflows/diagnose.ts +154 -0
  117. package/src/domains/workflows/workflows/perf-review.ts +149 -0
  118. package/src/domains/workflows/workflows/refactor-scout.ts +143 -0
  119. package/src/domains/workflows/workflows/research.ts +169 -0
  120. package/src/services/usage-service.ts +6 -30
@@ -0,0 +1,121 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ResolvedWorkflowRunOptions } from "./options.ts";
3
+ import type { PerfAggregate, PerfSink, PerfSnapshot } from "./perf.ts";
4
+ import type { WorkflowProgressSnapshot } from "./progress-types.ts";
5
+ import type {
6
+ LoadedWorkflow,
7
+ WorkflowProgressSource,
8
+ WorkflowRef,
9
+ WorkflowRunMetadata,
10
+ } from "./types.ts";
11
+ import type { WorkflowUsageSnapshot } from "./usage.ts";
12
+
13
+ export interface WorkflowPerfDetails {
14
+ readonly enabled: boolean;
15
+ readonly startedAt: number;
16
+ readonly aggregates: readonly PerfAggregate[];
17
+ }
18
+
19
+ export interface WorkflowResultEnvelope {
20
+ readonly name: string;
21
+ readonly result: unknown;
22
+ readonly completedAt: number;
23
+ readonly usage?: WorkflowUsageSnapshot;
24
+ readonly perf?: WorkflowPerfDetails;
25
+ readonly runId?: string;
26
+ readonly resumedFromRunId?: string;
27
+ }
28
+
29
+ export type ResolvedWorkflowRunner = (
30
+ ctx: ExtensionContext,
31
+ mod: LoadedWorkflow,
32
+ args: string,
33
+ options: ResolvedWorkflowRunOptions,
34
+ ) => Promise<unknown>;
35
+
36
+ export interface WorkflowExecutionInput {
37
+ readonly ctx: ExtensionContext;
38
+ readonly name: string;
39
+ readonly mod: LoadedWorkflow;
40
+ readonly args: string;
41
+ readonly options: ResolvedWorkflowRunOptions;
42
+ readonly perfRecorder?: PerfSink;
43
+ readonly runResolvedWorkflow: ResolvedWorkflowRunner;
44
+ readonly resolveWorkflow: (ref: WorkflowRef) => Promise<LoadedWorkflow>;
45
+ readonly onProgressSource?: (source: WorkflowProgressSource | undefined) => void | Promise<void>;
46
+ readonly onProgressSnapshot?: (snapshot: WorkflowProgressSnapshot) => void | Promise<void>;
47
+ }
48
+
49
+ export interface WorkflowExecution {
50
+ readonly metadata?: WorkflowRunMetadata;
51
+ readonly envelope: WorkflowResultEnvelope;
52
+ }
53
+
54
+ /** Shared command/tool execution path. The engine remains lazy-loaded by the caller. */
55
+ export async function executeWorkflowInvocation(input: WorkflowExecutionInput): Promise<WorkflowExecution> {
56
+ let perfSnapshot: PerfSnapshot | undefined;
57
+ let usageSnapshot: WorkflowUsageSnapshot | undefined;
58
+ let runMetadata: WorkflowRunMetadata | undefined;
59
+ const { options } = input;
60
+ const runOptions: ResolvedWorkflowRunOptions = {
61
+ ...options,
62
+ perfRecorder: input.perfRecorder,
63
+ resolveWorkflow: input.resolveWorkflow,
64
+ onProgressSource(source) {
65
+ return notifyLifecycleObservers(
66
+ () => input.onProgressSource?.(source),
67
+ () => options.onProgressSource?.(source),
68
+ );
69
+ },
70
+ onPerfSnapshot(snapshot) {
71
+ perfSnapshot = snapshot;
72
+ return notifyLifecycleObservers(() => options.onPerfSnapshot?.(snapshot));
73
+ },
74
+ onUsageSnapshot(snapshot) {
75
+ usageSnapshot = snapshot;
76
+ return notifyLifecycleObservers(() => options.onUsageSnapshot?.(snapshot));
77
+ },
78
+ onRunMetadata(metadata) {
79
+ runMetadata = metadata;
80
+ return notifyLifecycleObservers(() => options.onRunMetadata?.(metadata));
81
+ },
82
+ onProgressSnapshot(snapshot) {
83
+ return notifyLifecycleObservers(
84
+ () => input.onProgressSnapshot?.(snapshot),
85
+ () => options.onProgressSnapshot?.(snapshot),
86
+ );
87
+ },
88
+ };
89
+ const result = await input.runResolvedWorkflow(input.ctx, input.mod, input.args, runOptions);
90
+ const perf = compactPerfSnapshot(perfSnapshot);
91
+ return {
92
+ metadata: runMetadata,
93
+ envelope: {
94
+ name: input.name,
95
+ result,
96
+ completedAt: Date.now(),
97
+ usage: usageSnapshot,
98
+ perf,
99
+ runId: runMetadata?.runId,
100
+ resumedFromRunId: runMetadata?.resumedFromRunId,
101
+ },
102
+ };
103
+ }
104
+
105
+ function compactPerfSnapshot(snapshot: PerfSnapshot | undefined): WorkflowPerfDetails | undefined {
106
+ if (!snapshot?.enabled) return undefined;
107
+ return { enabled: true, startedAt: snapshot.startedAt, aggregates: snapshot.aggregates };
108
+ }
109
+
110
+ async function notifyLifecycleObservers(...observers: ReadonlyArray<() => void | Promise<void>>): Promise<void> {
111
+ const failures: unknown[] = [];
112
+ for (const observer of observers) {
113
+ try {
114
+ await observer();
115
+ } catch (error) {
116
+ failures.push(error);
117
+ }
118
+ }
119
+ if (failures.length === 1) throw failures[0];
120
+ if (failures.length > 1) throw new AggregateError(failures, "Workflow lifecycle observers failed");
121
+ }
@@ -0,0 +1,75 @@
1
+ import type {
2
+ LoadedWorkflow,
3
+ WorkflowMeta,
4
+ WorkflowModule,
5
+ WorkflowSourceIdentity,
6
+ } from "./types.ts";
7
+ import type { WorktreeBaseline } from "./worktree.ts";
8
+
9
+ type WorkflowMetaCandidate = {
10
+ readonly name?: unknown;
11
+ readonly description?: unknown;
12
+ readonly phases?: unknown;
13
+ };
14
+
15
+ type WorkflowModuleCandidate = {
16
+ readonly meta?: unknown;
17
+ readonly default?: unknown;
18
+ };
19
+
20
+ /** Validate and normalize workflow metadata without executing workflow code. */
21
+ export function parseWorkflowMeta(value: unknown): { meta: WorkflowMeta } | { reason: string } {
22
+ if (!isRecord(value)) return { reason: "meta export must be an object" };
23
+ return parseWorkflowMetaObject(value);
24
+ }
25
+
26
+ export function parseWorkflowModule(value: unknown): { module: WorkflowModule } | { reason: string } {
27
+ if (!isRecord(value)) return { reason: "module export is not an object" };
28
+ const candidate = value as WorkflowModuleCandidate;
29
+ const meta = candidate.meta;
30
+ if (!isRecord(meta)) return { reason: "missing meta export" };
31
+
32
+ const parsedMeta = parseWorkflowMetaObject(meta);
33
+ if ("reason" in parsedMeta) return parsedMeta;
34
+ if (!isWorkflowRun(candidate.default)) return { reason: "default export must be a function" };
35
+
36
+ return { module: { meta: parsedMeta.meta, default: candidate.default } };
37
+ }
38
+
39
+ /** Attach engine-owned source provenance after an authored module has been validated. */
40
+ export function loadWorkflow(
41
+ module: WorkflowModule,
42
+ source: WorkflowSourceIdentity,
43
+ isolatedWorktreeBaseline?: WorktreeBaseline,
44
+ ): LoadedWorkflow {
45
+ const loaded = { meta: module.meta, default: module.default, source };
46
+ return isolatedWorktreeBaseline === undefined
47
+ ? loaded
48
+ : { ...loaded, isolatedWorktreeBaseline };
49
+ }
50
+
51
+ function parseWorkflowMetaObject(meta: WorkflowMetaCandidate): { meta: WorkflowMeta } | { reason: string } {
52
+ if (typeof meta.name !== "string") return { reason: "meta.name must be a string" };
53
+
54
+ const description = meta.description;
55
+ if (description !== undefined && typeof description !== "string") return { reason: "meta.description must be a string when provided" };
56
+
57
+ const phases = meta.phases;
58
+ if (phases !== undefined && !isWorkflowPhases(phases)) return { reason: "meta.phases must be an array of { title: string }" };
59
+
60
+ const workflowMeta: WorkflowMeta = { name: meta.name, description: description ?? "" };
61
+ if (phases !== undefined) workflowMeta.phases = phases;
62
+ return { meta: workflowMeta };
63
+ }
64
+
65
+ function isWorkflowRun(value: unknown): value is WorkflowModule["default"] {
66
+ return typeof value === "function";
67
+ }
68
+
69
+ function isWorkflowPhases(value: unknown): value is Array<{ title: string }> {
70
+ return Array.isArray(value) && value.every((phase) => isRecord(phase) && typeof phase.title === "string");
71
+ }
72
+
73
+ function isRecord(value: unknown): value is Record<string, unknown> {
74
+ return typeof value === "object" && value !== null;
75
+ }
@@ -0,0 +1,72 @@
1
+ import type { WorkflowBackgroundOrigin } from "./types.ts";
2
+ import type { WorkflowRunRecord } from "./workflow-run-record.ts";
3
+
4
+ const MAX_BACKGROUND_TEXT = 2_048;
5
+
6
+ export type WorkflowRunDelivery =
7
+ | { readonly state: "pending" }
8
+ | { readonly state: "delivered"; readonly deliveredAt: number }
9
+ | { readonly state: "unavailable"; readonly attemptedAt: number; readonly message: string };
10
+
11
+ export interface PersistedWorkflowBackground {
12
+ readonly origin: WorkflowBackgroundOrigin;
13
+ readonly delivery: WorkflowRunDelivery;
14
+ }
15
+
16
+ export function createPersistedWorkflowBackground(
17
+ origin: WorkflowBackgroundOrigin | undefined,
18
+ ): PersistedWorkflowBackground | undefined {
19
+ if (!origin) return undefined;
20
+ return {
21
+ origin: {
22
+ sessionId: boundedText(origin.sessionId),
23
+ requestedAt: origin.requestedAt,
24
+ },
25
+ delivery: { state: "pending" },
26
+ };
27
+ }
28
+
29
+ export function updateWorkflowRunDelivery(
30
+ record: WorkflowRunRecord,
31
+ delivery: WorkflowRunDelivery,
32
+ at = Date.now(),
33
+ ): WorkflowRunRecord {
34
+ if (!record.background) throw new Error(`Workflow run ${record.runId} is not a background run.`);
35
+ return {
36
+ ...record,
37
+ updatedAt: at,
38
+ background: {
39
+ origin: record.background.origin,
40
+ delivery: delivery.state === "unavailable"
41
+ ? { ...delivery, message: boundedText(delivery.message) }
42
+ : delivery,
43
+ },
44
+ };
45
+ }
46
+
47
+ export function isPersistedWorkflowBackground(value: unknown): value is PersistedWorkflowBackground {
48
+ if (!isRecord(value) || !isRecord(value.origin) || !isRecord(value.delivery)) return false;
49
+ if (typeof value.origin.sessionId !== "string" || !isFiniteNumber(value.origin.requestedAt)) return false;
50
+ switch (value.delivery.state) {
51
+ case "pending":
52
+ return true;
53
+ case "delivered":
54
+ return isFiniteNumber(value.delivery.deliveredAt);
55
+ case "unavailable":
56
+ return isFiniteNumber(value.delivery.attemptedAt) && typeof value.delivery.message === "string";
57
+ default:
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function boundedText(value: string): string {
63
+ return value.length <= MAX_BACKGROUND_TEXT ? value : `${value.slice(0, MAX_BACKGROUND_TEXT - 1)}…`;
64
+ }
65
+
66
+ function isFiniteNumber(value: unknown): value is number {
67
+ return typeof value === "number" && Number.isFinite(value);
68
+ }
69
+
70
+ function isRecord(value: unknown): value is Record<string, unknown> {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value);
72
+ }
@@ -0,0 +1,342 @@
1
+ import type {
2
+ ExtensionCommandContext,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
6
+ import { BackgroundWorkflowCoordinator } from "./background-workflows.ts";
7
+ import { backgroundUnavailableResult, startBackgroundWorkflowTool } from "./background-workflow-tool.ts";
8
+ import { validateWorkflowRunId } from "./journal.ts";
9
+ import { resolveWorkflowRunOptions, type ResolvedWorkflowRunOptions } from "./options.ts";
10
+ import type { LoadedWorkflow } from "./types.ts";
11
+ import {
12
+ availableWorkflowRunActions,
13
+ canRelaunchWorkflowRun,
14
+ formatWorkflowRunDetails,
15
+ formatWorkflowRunHistory,
16
+ formatWorkflowRunSummary,
17
+ isWorkflowRunLifecycleAction,
18
+ parseWorkflowRunsCommand,
19
+ retainedWorkflowRunOutcome,
20
+ WORKFLOW_RUN_ACTIONS,
21
+ WORKFLOW_RUN_HISTORY_LIMIT,
22
+ type WorkflowRunLifecycleAction,
23
+ } from "./workflow-run-history.ts";
24
+ import { transitionWorkflowRun, type WorkflowRunRecord } from "./workflow-run-record.ts";
25
+ import { ProjectWorkflowRunStore, type WorkflowRunStore } from "./workflow-run-store.ts";
26
+ import { unknownErrorMessage } from "./unknown-error.ts";
27
+ import { emptyWorkflowUsageTotals } from "./usage.ts";
28
+ import {
29
+ WorkflowUsageLimitScheduler,
30
+ type WorkflowUsageLimitSchedulerClock,
31
+ } from "./workflow-usage-limit-scheduler.ts";
32
+ import { WorkflowInspector } from "./ui/workflow-inspector.ts";
33
+ import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
34
+ import { completeCurrentArgument, splitArgumentPrefix } from "./command-completions.ts";
35
+
36
+ type WorkflowRunCompletionContext = Pick<ExtensionContext, "cwd" | "sessionManager">;
37
+
38
+ interface WorkflowRunControllerDependencies {
39
+ readonly resolveWorkflow: (name: string) => Promise<LoadedWorkflow | undefined>;
40
+ readonly execute: (
41
+ ctx: ExtensionContext,
42
+ name: string,
43
+ workflow: LoadedWorkflow,
44
+ options: ResolvedWorkflowRunOptions,
45
+ ) => Promise<void>;
46
+ readonly storeForCwd?: (cwd: string) => WorkflowRunStore;
47
+ readonly schedulerClock?: WorkflowUsageLimitSchedulerClock;
48
+ readonly log?: (message: string) => void;
49
+ }
50
+
51
+ export class WorkflowRunController {
52
+ private readonly storeForCwd: (cwd: string) => WorkflowRunStore;
53
+ private readonly usageLimitScheduler: WorkflowUsageLimitScheduler;
54
+ private readonly log: (message: string) => void;
55
+ private completionContext: WorkflowRunCompletionContext | undefined;
56
+
57
+ constructor(
58
+ private readonly background: BackgroundWorkflowCoordinator,
59
+ private readonly dependencies: WorkflowRunControllerDependencies,
60
+ ) {
61
+ this.storeForCwd = dependencies.storeForCwd ?? ((cwd) => new ProjectWorkflowRunStore(cwd));
62
+ this.log = dependencies.log ?? ((message) => process.stderr.write(`${message}\n`));
63
+ this.usageLimitScheduler = new WorkflowUsageLimitScheduler(
64
+ (ctx, runId, attempt) => this.autoResume(ctx, runId, attempt),
65
+ dependencies.schedulerClock,
66
+ dependencies.log,
67
+ );
68
+ }
69
+
70
+ async runSettled(ctx: ExtensionContext, runId: string): Promise<void> {
71
+ const record = await this.loadRecord(ctx.cwd, runId);
72
+ if (record && canRelaunchWorkflowRun(record)) this.usageLimitScheduler.arm(ctx, record);
73
+ }
74
+
75
+ async sessionStarted(ctx: ExtensionContext): Promise<void> {
76
+ this.completionContext = { cwd: ctx.cwd, sessionManager: ctx.sessionManager };
77
+ this.usageLimitScheduler.activateSession(ctx);
78
+ try {
79
+ for (const record of await this.storeForCwd(ctx.cwd).list()) {
80
+ if (canRelaunchWorkflowRun(record)) this.usageLimitScheduler.arm(ctx, record);
81
+ }
82
+ } catch (error) {
83
+ this.log(`[workflow] provider-limit recovery could not load run history: ${unknownErrorMessage(error)}`);
84
+ }
85
+ }
86
+
87
+ sessionShutdown(ctx: Pick<ExtensionContext, "sessionManager">): void {
88
+ this.usageLimitScheduler.cancelSession(ctx);
89
+ this.completionContext = undefined;
90
+ }
91
+
92
+ async handleCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
93
+ const command = parseWorkflowRunsCommand(args);
94
+ if (command.kind === "error") {
95
+ ctx.ui.notify(command.message, "warning");
96
+ return;
97
+ }
98
+ if (command.kind === "action") {
99
+ await this.perform(command.action, command.runId, ctx);
100
+ return;
101
+ }
102
+ if (!ctx.hasUI) {
103
+ const records = await this.listRecent(ctx.cwd);
104
+ ctx.ui.notify(formatWorkflowRunHistory(records, this.background.activeRunIds(ctx)), "info");
105
+ return;
106
+ }
107
+ await this.openRunSelector(ctx);
108
+ }
109
+
110
+ async inspectStoredRun(ctx: ExtensionContext, runId: string): Promise<boolean> {
111
+ const record = await this.loadRecord(ctx.cwd, runId);
112
+ if (!record) return false;
113
+ await this.inspect(record, ctx);
114
+ return true;
115
+ }
116
+
117
+ private async openRunSelector(ctx: ExtensionCommandContext): Promise<void> {
118
+ while (true) {
119
+ const records = await this.listRecent(ctx.cwd);
120
+ const active = this.background.activeRunIds(ctx);
121
+ if (records.length === 0) {
122
+ ctx.ui.notify(formatWorkflowRunHistory(records, active), "info");
123
+ return;
124
+ }
125
+ const options = records.map((record) =>
126
+ formatWorkflowRunSummary(record, active.has(record.runId))
127
+ );
128
+ const selected = await ctx.ui.select("Workflow Runs", options);
129
+ if (!selected) return;
130
+ const selectedIndex = options.indexOf(selected);
131
+ const selectedRecord = records[selectedIndex];
132
+ const record = selectedRecord && await this.loadRecord(ctx.cwd, selectedRecord.runId);
133
+ if (!record) {
134
+ ctx.ui.notify("The selected workflow run is no longer available.", "warning");
135
+ continue;
136
+ }
137
+ const actions = availableWorkflowRunActions(record, active.has(record.runId));
138
+ const selectedAction = await ctx.ui.select(
139
+ `${record.workflow.name} · ${record.runId}`,
140
+ [...actions],
141
+ );
142
+ const action = actions.find((candidate) => candidate === selectedAction);
143
+ if (!action) continue;
144
+ if (action === "inspect") await this.inspect(record, ctx);
145
+ else await this.perform(action, record.runId, ctx);
146
+ }
147
+ }
148
+
149
+ private async inspect(record: WorkflowRunRecord, ctx: ExtensionContext): Promise<void> {
150
+ if (!ctx.hasUI || ctx.mode !== "tui") {
151
+ ctx.ui.notify(
152
+ formatWorkflowRunDetails(record, this.background.activeRunIds(ctx).has(record.runId)),
153
+ "info",
154
+ );
155
+ return;
156
+ }
157
+ await ctx.ui.custom<void>(
158
+ (tui, theme, _keybindings, done) => new WorkflowInspector(
159
+ () => record.progress,
160
+ tui,
161
+ theme,
162
+ () => done(undefined),
163
+ { label: `${record.state.toUpperCase()} outcome`, text: retainedWorkflowRunOutcome(record) },
164
+ ),
165
+ WORKFLOW_VIEWER_OVERLAY_OPTIONS,
166
+ );
167
+ }
168
+
169
+ private async perform(
170
+ action: WorkflowRunLifecycleAction,
171
+ runId: string,
172
+ ctx: ExtensionContext,
173
+ ): Promise<void> {
174
+ const record = await this.loadRecord(ctx.cwd, runId);
175
+ if (!record) {
176
+ ctx.ui.notify(`Workflow run ${runId} was not found.`, "warning");
177
+ return;
178
+ }
179
+ if (action === "inspect") {
180
+ await this.inspect(record, ctx);
181
+ return;
182
+ }
183
+ const available = availableWorkflowRunActions(
184
+ record,
185
+ this.background.activeRunIds(ctx).has(record.runId),
186
+ );
187
+ if (!available.includes(action)) {
188
+ ctx.ui.notify(`Action ${action} is not available for ${record.state} run ${runId}.`, "warning");
189
+ return;
190
+ }
191
+
192
+ try {
193
+ if (action === "stop") {
194
+ if (record.state === "paused") {
195
+ this.usageLimitScheduler.cancel(runId);
196
+ const stopped = transitionWorkflowRun(record, {
197
+ state: "stopped",
198
+ progress: record.progress,
199
+ usage: record.usage ?? record.progress.usage ?? {
200
+ agents: [],
201
+ totals: emptyWorkflowUsageTotals(),
202
+ assistantMessages: 0,
203
+ },
204
+ error: new Error("Workflow stopped by user."),
205
+ });
206
+ await this.storeForCwd(ctx.cwd).save(stopped);
207
+ await this.background.durableRunSettled(ctx, runId);
208
+ ctx.ui.notify(`Workflow run ${runId} is now stopped.`, "info");
209
+ return;
210
+ }
211
+ const stopped = await this.background.stop(ctx, runId);
212
+ ctx.ui.notify(`Workflow run ${runId} is now ${stopped.state}.`, "info");
213
+ return;
214
+ }
215
+ this.usageLimitScheduler.cancel(runId);
216
+ const message = await this.relaunch(ctx, record, action);
217
+ ctx.ui.notify(message, "info");
218
+ } catch (error) {
219
+ ctx.ui.notify(`Workflow ${action} failed: ${unknownErrorMessage(error)}`, "error");
220
+ }
221
+ }
222
+
223
+ private async relaunch(
224
+ ctx: ExtensionContext,
225
+ record: WorkflowRunRecord,
226
+ action: "resume" | "restart",
227
+ ): Promise<string> {
228
+ const unavailable = backgroundUnavailableResult(ctx.mode);
229
+ if (unavailable) {
230
+ const first = unavailable.content[0];
231
+ throw new Error(first?.type === "text" ? first.text : "background workflows are unavailable");
232
+ }
233
+ const workflow = await this.dependencies.resolveWorkflow(record.workflow.name);
234
+ if (!workflow) throw new Error(`registered workflow ${record.workflow.name} is unavailable`);
235
+ if (workflow.source.kind !== "file") {
236
+ throw new Error(`registered workflow ${record.workflow.name} no longer has verifiable file provenance`);
237
+ }
238
+ if (
239
+ action === "resume"
240
+ && workflow.source.fingerprint !== record.workflow.sourceFingerprint
241
+ ) {
242
+ throw new Error("workflow source changed, so journal replay cannot resume safely");
243
+ }
244
+ const options = resolveWorkflowRunOptions({
245
+ inspect: false,
246
+ perf: record.options.perf,
247
+ concurrency: record.options.concurrency ?? undefined,
248
+ parallelSubmissionLimit: record.options.parallelSubmissionLimit ?? undefined,
249
+ maxAgents: record.options.maxAgents ?? undefined,
250
+ agentTimeoutMs: record.options.agentTimeoutMs ?? undefined,
251
+ agentRetries: record.options.agentRetries,
252
+ autoResumeOnUsageLimit: record.options.autoResumeOnUsageLimit,
253
+ usageLimitMaxAttempts: record.options.usageLimitMaxAttempts,
254
+ usageLimitMaxDelayMs: record.options.usageLimitMaxDelayMs,
255
+ usageLimitAttempt: action === "resume"
256
+ ? (record.state === "paused" ? record.pause?.attempt : undefined) ?? record.options.usageLimitAttempt
257
+ : 0,
258
+ budget: record.options.budget ?? undefined,
259
+ resultViewer: "skip",
260
+ resumeFromRunId: action === "resume" ? record.runId : undefined,
261
+ });
262
+ const result = await startBackgroundWorkflowTool({
263
+ coordinator: this.background,
264
+ ctx,
265
+ name: workflow.meta.name,
266
+ options,
267
+ execute: (backgroundCtx, backgroundOptions) =>
268
+ this.dependencies.execute(backgroundCtx, workflow.meta.name, workflow, backgroundOptions),
269
+ });
270
+ const first = result.content[0];
271
+ const message = first?.type === "text" ? first.text : `Workflow ${action} started.`;
272
+ if (typeof result.details.error === "string") throw new Error(message);
273
+ return message;
274
+ }
275
+
276
+ private async autoResume(ctx: ExtensionContext, runId: string, attempt: number): Promise<void> {
277
+ const record = await this.loadRecord(ctx.cwd, runId);
278
+ if (
279
+ record?.state !== "paused"
280
+ || record.pause?.kind !== "provider_usage_limit"
281
+ || !record.pause.autoResume
282
+ || record.pause.attempt !== attempt
283
+ || !canRelaunchWorkflowRun(record)
284
+ ) {
285
+ return;
286
+ }
287
+ const message = await this.relaunch(ctx, record, "resume");
288
+ ctx.ui.notify(message, "info");
289
+ }
290
+
291
+ private async loadRecord(cwd: string, runId: string): Promise<WorkflowRunRecord | undefined> {
292
+ try {
293
+ validateWorkflowRunId(runId);
294
+ } catch {
295
+ return undefined;
296
+ }
297
+ return await this.storeForCwd(cwd).load(runId);
298
+ }
299
+
300
+ private async listRecent(cwd: string): Promise<WorkflowRunRecord[]> {
301
+ const records = await this.storeForCwd(cwd).list();
302
+ return records
303
+ .sort((left, right) => right.createdAt - left.createdAt)
304
+ .slice(0, WORKFLOW_RUN_HISTORY_LIMIT);
305
+ }
306
+
307
+ async argumentCompletions(argumentPrefix: string): Promise<AutocompleteItem[] | null> {
308
+ const ctx = this.completionContext;
309
+ const parts = splitArgumentPrefix(argumentPrefix);
310
+ if (parts.completed.length === 0) {
311
+ return completeCurrentArgument(argumentPrefix, WORKFLOW_RUN_ACTIONS);
312
+ }
313
+ if (parts.completed.length !== 1 || !ctx) return null;
314
+ const action = parts.completed[0];
315
+ if (!action || !isWorkflowRunLifecycleAction(action)) return null;
316
+ const records = await this.listRecent(ctx.cwd);
317
+ const active = this.background.activeRunIds(ctx);
318
+ return completeCurrentArgument(
319
+ argumentPrefix,
320
+ records
321
+ .filter((record) => availableWorkflowRunActions(record, active.has(record.runId)).includes(action))
322
+ .map((record) => ({
323
+ value: record.runId,
324
+ description: `${record.state} · ${record.workflow.name}`,
325
+ })),
326
+ );
327
+ }
328
+
329
+ async inspectorArgumentCompletions(argumentPrefix: string): Promise<AutocompleteItem[] | null> {
330
+ const ctx = this.completionContext;
331
+ const parts = splitArgumentPrefix(argumentPrefix);
332
+ if (parts.completed.length > 0) return null;
333
+ const records = ctx ? await this.listRecent(ctx.cwd) : [];
334
+ return completeCurrentArgument(argumentPrefix, [
335
+ { value: "last", description: "Inspect the current or most recent in-session workflow" },
336
+ ...records.map((record) => ({
337
+ value: record.runId,
338
+ description: `${record.state} · ${record.workflow.name}`,
339
+ })),
340
+ ]);
341
+ }
342
+ }