pi-workflow-engine 0.12.0 → 1.0.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.
- package/.pi/extensions/pi-workflow-engine/index.ts +106 -391
- package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +90 -0
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +20 -2
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +48 -62
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +46 -67
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +14 -37
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +5 -11
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +15 -21
- package/.pi/extensions/pi-workflow-engine/src/agent-session-identity.ts +13 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +17 -5
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +93 -100
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +11 -43
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +18 -18
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +33 -86
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +37 -86
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +16 -23
- package/.pi/extensions/pi-workflow-engine/src/dynamax-shortcuts.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +39 -98
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +37 -80
- package/.pi/extensions/pi-workflow-engine/src/finalizers.ts +2 -10
- package/.pi/extensions/pi-workflow-engine/src/git.ts +55 -0
- package/.pi/extensions/pi-workflow-engine/src/guards.ts +8 -0
- package/.pi/extensions/pi-workflow-engine/src/identity-canonicalization.ts +7 -19
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +171 -106
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +1 -12
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +37 -8
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +15 -66
- package/.pi/extensions/pi-workflow-engine/src/options.ts +97 -16
- package/.pi/extensions/pi-workflow-engine/src/perf.ts +43 -72
- package/.pi/extensions/pi-workflow-engine/src/process-runner.ts +14 -4
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +79 -80
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +28 -20
- package/.pi/extensions/pi-workflow-engine/src/replay-path-identity.ts +2 -7
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +0 -1
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +128 -142
- package/.pi/extensions/pi-workflow-engine/src/review/github-pr-comments.ts +14 -31
- package/.pi/extensions/pi-workflow-engine/src/review/patch-validation.ts +119 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-actions.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-diff-lines.ts +54 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-fix-workflow.ts +41 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-format.ts +51 -116
- package/.pi/extensions/pi-workflow-engine/src/review/review-handoff.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-issues.ts +9 -14
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +3 -7
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +158 -241
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +28 -38
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +35 -44
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +13 -14
- package/.pi/extensions/pi-workflow-engine/src/text.ts +29 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +16 -28
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +34 -14
- package/.pi/extensions/pi-workflow-engine/src/types.ts +9 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +193 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/research-result-renderer.ts +177 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/result-layout.ts +209 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +17 -44
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +703 -235
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-picker.ts +48 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +239 -126
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +461 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +91 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +316 -98
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +21 -32
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +178 -188
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +19 -24
- package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +17 -37
- package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +79 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +4 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +77 -63
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +42 -29
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +15 -14
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +27 -31
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +1 -5
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +3 -6
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +55 -110
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +101 -203
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +51 -187
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +33 -109
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +36 -106
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +74 -66
- package/USAGE.md +90 -17
- package/package.json +1 -2
- package/skills/workflow-code-review-actions/SKILL.md +12 -23
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +0 -81
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +0 -49
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +0 -52
- package/.pi/settings.json +0 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
3
|
-
import type { EditorComponent, KeyId } from "@earendil-works/pi-tui";
|
|
1
|
+
import { CustomEditor, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { EditorComponent } from "@earendil-works/pi-tui";
|
|
4
3
|
import { completeCurrentArgument } from "./command-completions.ts";
|
|
5
|
-
import {
|
|
4
|
+
import type { DynamaxShortcuts } from "./dynamax-shortcuts.ts";
|
|
6
5
|
import { sessionKey } from "./session-identity.ts";
|
|
7
6
|
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
7
|
+
import { dot, GLYPH } from "./ui/kit.ts";
|
|
8
|
+
import { setWorkflowInspectorShortcut } from "./ui/workflow-widget.ts";
|
|
8
9
|
import {
|
|
9
10
|
decorateDynamaxEditor,
|
|
10
11
|
resolveDynamaxEffect,
|
|
@@ -21,17 +22,21 @@ export interface DynamaxState {
|
|
|
21
22
|
|
|
22
23
|
export interface DynamaxRuntime {
|
|
23
24
|
state: DynamaxState;
|
|
24
|
-
runningWorkflow?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type DynamaxRuntimeStore = Map<string, DynamaxRuntime>;
|
|
28
28
|
|
|
29
29
|
export interface DynamaxRegistrationOptions {
|
|
30
|
-
openInspector
|
|
30
|
+
openInspector: (ctx: ExtensionContext) => Promise<void> | void;
|
|
31
31
|
effect?: DynamaxEffect;
|
|
32
32
|
animationScheduler?: DynamaxAnimationScheduler;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface DynamaxHandle {
|
|
36
|
+
/** Opt the next agent run in. The `input` hook ignores prompts this extension sends (pi tags them `source: "extension"`). */
|
|
37
|
+
markOneShot(ctx: ExtensionContext): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
|
|
36
41
|
export const DYNAMAX_STATUS_KEY = "dynamax";
|
|
37
42
|
|
|
@@ -78,8 +83,6 @@ Inline workflow rules:
|
|
|
78
83
|
${ADAPTIVE_WORKFLOW_GUIDANCE}
|
|
79
84
|
`;
|
|
80
85
|
|
|
81
|
-
const DYNAMAX_CONTEXT_CUSTOM_TYPE = "workflow-dynamax-reminder";
|
|
82
|
-
|
|
83
86
|
export function createDynamaxState(): DynamaxState {
|
|
84
87
|
return { sticky: false, oneShotPending: false, turnActive: false };
|
|
85
88
|
}
|
|
@@ -126,16 +129,15 @@ export function describeDynamaxState(state: DynamaxState): string {
|
|
|
126
129
|
return `sticky ${sticky}; one-shot ${oneShot}${turn}`;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
return `dynamax: ${mode} | /workflow:dynamax on|off | ${inspectorHint}`;
|
|
132
|
+
/**
|
|
133
|
+
* Footer status: `◆ dynamax` while sticky, `◆ dynamax · next prompt` while a one-shot waits for its
|
|
134
|
+
* prompt, `◆ dynamax · this turn` while that turn runs. A running workflow names itself in its own
|
|
135
|
+
* status (`◆ repo-scan · Scan 2/3 · 4s`), so this one only states the mode.
|
|
136
|
+
*/
|
|
137
|
+
export function dynamaxStatusText(state: DynamaxState, theme: Theme): string {
|
|
138
|
+
const mode = state.sticky ? undefined : state.oneShotPending ? "next prompt" : state.turnActive ? "this turn" : undefined;
|
|
139
|
+
const label = `${theme.fg("accent", GLYPH.workflow)} ${theme.fg("accent", "dynamax")}`;
|
|
140
|
+
return mode ? `${label}${dot(theme)}${theme.fg("muted", mode)}` : label;
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
export function appendDynamaxSystemReminder(systemPrompt: string, state: DynamaxState): string {
|
|
@@ -145,24 +147,16 @@ export function appendDynamaxSystemReminder(systemPrompt: string, state: Dynamax
|
|
|
145
147
|
return `${systemPrompt}\n\n${DYNAMAX_REMINDER.trim()}`;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
export function
|
|
149
|
-
if (!state.sticky) return messages;
|
|
150
|
-
return [...messages, createDynamaxContextMessage()];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function registerDynamax(
|
|
154
|
-
pi: ExtensionAPI,
|
|
155
|
-
shortcuts: DynamaxShortcuts = resolveDynamaxShortcuts(),
|
|
156
|
-
options: DynamaxRegistrationOptions = {},
|
|
157
|
-
): void {
|
|
150
|
+
export function registerDynamax(pi: ExtensionAPI, shortcuts: DynamaxShortcuts, options: DynamaxRegistrationOptions): DynamaxHandle {
|
|
158
151
|
const runtimes: DynamaxRuntimeStore = new Map();
|
|
159
152
|
const effect = options.effect ?? resolveDynamaxEffect();
|
|
153
|
+
setWorkflowInspectorShortcut(shortcuts.inspector);
|
|
160
154
|
let editorInstallation: DynamaxEditorInstallation | undefined;
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
155
|
+
const markOneShot = (ctx: ExtensionContext): void => {
|
|
156
|
+
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
157
|
+
markDynamaxOneShot(runtime.state);
|
|
158
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
159
|
+
};
|
|
166
160
|
const installEditor = (ctx: ExtensionContext): void => {
|
|
167
161
|
if (ctx.mode !== "tui" || effect === "off") return;
|
|
168
162
|
const previousFactory = ctx.ui.getEditorComponent();
|
|
@@ -229,7 +223,7 @@ export function registerDynamax(
|
|
|
229
223
|
|
|
230
224
|
pi.on("session_start", (_event, ctx) => {
|
|
231
225
|
installEditor(ctx);
|
|
232
|
-
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx)
|
|
226
|
+
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx));
|
|
233
227
|
});
|
|
234
228
|
|
|
235
229
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
@@ -238,12 +232,7 @@ export function registerDynamax(
|
|
|
238
232
|
});
|
|
239
233
|
|
|
240
234
|
pi.on("input", (event, ctx) => {
|
|
241
|
-
if (event.source
|
|
242
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
243
|
-
if (hasDynamaxToken(event.text)) {
|
|
244
|
-
markDynamaxOneShot(runtime.state);
|
|
245
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
246
|
-
}
|
|
235
|
+
if (event.source !== "extension" && hasDynamaxToken(event.text)) markOneShot(ctx);
|
|
247
236
|
return { action: "continue" };
|
|
248
237
|
});
|
|
249
238
|
|
|
@@ -251,45 +240,21 @@ export function registerDynamax(
|
|
|
251
240
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
252
241
|
const systemPrompt = appendDynamaxSystemReminder(event.systemPrompt, runtime.state);
|
|
253
242
|
if (systemPrompt === event.systemPrompt) return undefined;
|
|
254
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
243
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
255
244
|
return { systemPrompt };
|
|
256
245
|
});
|
|
257
246
|
|
|
258
247
|
pi.on("agent_end", (_event, ctx) => {
|
|
259
248
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
260
|
-
runtime.runningWorkflow = undefined;
|
|
261
249
|
if (!runtime.state.sticky) runtime.state.turnActive = false;
|
|
262
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
pi.on("tool_execution_start", (event, ctx) => {
|
|
266
|
-
if (event.toolName !== "workflow") return undefined;
|
|
267
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
268
|
-
runtime.runningWorkflow = workflowLabel(event.args);
|
|
269
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
270
|
-
return undefined;
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
pi.on("tool_execution_end", (event, ctx) => {
|
|
274
|
-
if (event.toolName !== "workflow") return undefined;
|
|
275
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
276
|
-
runtime.runningWorkflow = undefined;
|
|
277
|
-
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
278
|
-
return undefined;
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
pi.on("context", (event, ctx) => {
|
|
282
|
-
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
283
|
-
const messages = appendDynamaxContextReminder(event.messages, runtime.state);
|
|
284
|
-
if (messages === event.messages) return undefined;
|
|
285
|
-
return { messages };
|
|
250
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
286
251
|
});
|
|
287
252
|
|
|
288
253
|
if (shortcuts.inspector) {
|
|
289
254
|
pi.registerShortcut(shortcuts.inspector, {
|
|
290
255
|
description: "Open workflow inspector",
|
|
291
256
|
handler: async (ctx) => {
|
|
292
|
-
await openInspector(ctx);
|
|
257
|
+
await options.openInspector(ctx);
|
|
293
258
|
},
|
|
294
259
|
});
|
|
295
260
|
}
|
|
@@ -306,13 +271,13 @@ export function registerDynamax(
|
|
|
306
271
|
}
|
|
307
272
|
if (action === "on") {
|
|
308
273
|
enableDynamaxSticky(runtime.state);
|
|
309
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
274
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
310
275
|
ctx.ui.notify("Dynamax workflow orchestration is on for this session", "info");
|
|
311
276
|
return;
|
|
312
277
|
}
|
|
313
278
|
if (action === "off") {
|
|
314
279
|
clearDynamax(runtime.state);
|
|
315
|
-
updateDynamaxSurfaces(ctx, runtime
|
|
280
|
+
updateDynamaxSurfaces(ctx, runtime);
|
|
316
281
|
ctx.ui.notify("Dynamax workflow orchestration is off", "info");
|
|
317
282
|
return;
|
|
318
283
|
}
|
|
@@ -323,44 +288,20 @@ export function registerDynamax(
|
|
|
323
288
|
ctx.ui.notify("Usage: /workflow:dynamax [on|off|status]", "warning");
|
|
324
289
|
},
|
|
325
290
|
});
|
|
291
|
+
|
|
292
|
+
return { markOneShot };
|
|
326
293
|
}
|
|
327
294
|
|
|
328
|
-
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime
|
|
295
|
+
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime): void {
|
|
329
296
|
if (!ctx.hasUI) return;
|
|
330
|
-
if (!isDynamaxActive(runtime.state)
|
|
297
|
+
if (!isDynamaxActive(runtime.state)) {
|
|
331
298
|
clearDynamaxSurfaces(ctx);
|
|
332
299
|
return;
|
|
333
300
|
}
|
|
334
|
-
|
|
335
|
-
const status = dynamaxWidgetLine(runtime.state, shortcuts.inspector, runtime.runningWorkflow);
|
|
336
|
-
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, status);
|
|
301
|
+
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, dynamaxStatusText(runtime.state, ctx.ui.theme));
|
|
337
302
|
}
|
|
338
303
|
|
|
339
304
|
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
340
305
|
if (!ctx.hasUI) return;
|
|
341
306
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
|
|
342
307
|
}
|
|
343
|
-
|
|
344
|
-
function workflowLabel(args: unknown): string {
|
|
345
|
-
if (!isRecord(args)) return "workflow";
|
|
346
|
-
const name = args.name;
|
|
347
|
-
if (typeof name === "string" && name.trim()) return name.trim();
|
|
348
|
-
const script = args.script;
|
|
349
|
-
if (typeof script === "string" && script.trim()) return "inline workflow";
|
|
350
|
-
return "workflow";
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function createDynamaxContextMessage(): AgentMessage {
|
|
354
|
-
return {
|
|
355
|
-
role: "custom",
|
|
356
|
-
customType: DYNAMAX_CONTEXT_CUSTOM_TYPE,
|
|
357
|
-
content: DYNAMAX_REMINDER.trim(),
|
|
358
|
-
display: false,
|
|
359
|
-
details: { dynamax: true, sticky: true },
|
|
360
|
-
timestamp: Date.now(),
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
365
|
-
return typeof value === "object" && value !== null;
|
|
366
|
-
}
|
|
@@ -8,15 +8,14 @@ import { abortReason, isWorkflowPauseError, linkAbortSignal, throwIfAborted } fr
|
|
|
8
8
|
import { createBudget } from "./budget.ts";
|
|
9
9
|
import { runAgent, type AgentExecutionOptions, type RunContext } from "./agent-runner.ts";
|
|
10
10
|
import { ProgressTracker } from "./progress.ts";
|
|
11
|
-
import { createPerfRecorder,
|
|
11
|
+
import { createPerfRecorder, formatPerfSummary, type PerfSink } from "./perf.ts";
|
|
12
12
|
import { createWorkflowUsageRecorder, type WorkflowUsageSink } from "./usage.ts";
|
|
13
13
|
import {
|
|
14
14
|
resolveWorkflowRunOptions,
|
|
15
15
|
type ResolvedWorkflowRunOptions,
|
|
16
16
|
} from "./options.ts";
|
|
17
17
|
import type { AgentOptions, IsolatedAgentResult, LoadedWorkflow, WorkflowApi, WorkflowProgressEvent, WorkflowRef, WorkflowRunOptions } from "./types.ts";
|
|
18
|
-
import {
|
|
19
|
-
import { WORKFLOW_VIEWER_OVERLAY_OPTIONS } from "./ui/workflow-viewer-layout.ts";
|
|
18
|
+
import { showWorkflowInspector } from "./ui/workflow-inspector.ts";
|
|
20
19
|
import { createWorkflowJournal, createWorkflowRunId, pruneWorkflowJournals, workflowJournalPath } from "./journal.ts";
|
|
21
20
|
import { WorktreeRegistry } from "./worktree.ts";
|
|
22
21
|
import { runFinalizers } from "./finalizers.ts";
|
|
@@ -97,8 +96,10 @@ export async function runResolvedWorkflow(
|
|
|
97
96
|
): Promise<unknown> {
|
|
98
97
|
const runId = resolvedOptions.runId ?? createWorkflowRunId();
|
|
99
98
|
let durableProgress: DurableWorkflowRun | undefined;
|
|
100
|
-
const progress = new ProgressTracker(ctx, mod.meta.name, runId, (snapshot) => durableProgress?.updateProgress(snapshot)
|
|
101
|
-
|
|
99
|
+
const progress = new ProgressTracker(ctx, mod.meta.name, runId, (snapshot) => durableProgress?.updateProgress(snapshot), {
|
|
100
|
+
plannedPhases: mod.meta.phases?.map((phase) => phase.title),
|
|
101
|
+
display: resolvedOptions.background ? "background" : "live",
|
|
102
|
+
});
|
|
102
103
|
const perf = resolvedOptions.perfRecorder ?? createPerfRecorder(resolvedOptions.perf);
|
|
103
104
|
const usage = createWorkflowUsageRecorder((snapshot) => progress.updateUsage(snapshot));
|
|
104
105
|
const budget = createBudget(resolvedOptions.budget, usage);
|
|
@@ -122,6 +123,7 @@ export async function runResolvedWorkflow(
|
|
|
122
123
|
},
|
|
123
124
|
);
|
|
124
125
|
durableProgress = durableRun;
|
|
126
|
+
const progressSource = { snapshot: () => progress.snapshot(), state: () => durableRun.state };
|
|
125
127
|
const worktrees = dependencies.worktrees ?? new WorktreeRegistry(ctx.cwd);
|
|
126
128
|
const runAbortController = new AbortController();
|
|
127
129
|
const unlinkContextAbortSignal = linkAbortSignal(ctx.signal, runAbortController);
|
|
@@ -129,11 +131,7 @@ export async function runResolvedWorkflow(
|
|
|
129
131
|
const workflowOutcome = await captureOutcome(async () => {
|
|
130
132
|
await notifyLifecycleObserver(progress, "progress source callback", () => resolvedOptions.onProgressSource?.(progressSource));
|
|
131
133
|
if (resolvedOptions.inspect && ctx.hasUI && ctx.mode === "tui") {
|
|
132
|
-
void ctx.ui
|
|
133
|
-
.custom<void>(
|
|
134
|
-
(tui, theme, _keybindings, done) => new WorkflowInspector(() => progress.snapshot(), tui, theme, () => done(undefined)),
|
|
135
|
-
WORKFLOW_VIEWER_OVERLAY_OPTIONS,
|
|
136
|
-
)
|
|
134
|
+
void showWorkflowInspector(ctx.ui, progressSource.snapshot, () => ({ state: progressSource.state() }))
|
|
137
135
|
.catch((error: unknown) => {
|
|
138
136
|
try {
|
|
139
137
|
progress.log(`inspector failed: ${unknownErrorMessage(error)}`);
|
|
@@ -206,69 +204,44 @@ export async function runResolvedWorkflow(
|
|
|
206
204
|
}),
|
|
207
205
|
);
|
|
208
206
|
|
|
209
|
-
|
|
207
|
+
let outcome: Outcome<unknown> = workflowOutcome;
|
|
208
|
+
if (!finalizationOutcome.ok) {
|
|
209
|
+
outcome = workflowOutcome.ok
|
|
210
|
+
? finalizationOutcome
|
|
211
|
+
: { ok: false, error: combinedWorkflowError(workflowOutcome.error, finalizationOutcome.error) };
|
|
212
|
+
}
|
|
213
|
+
const pauseError = outcome.ok ? undefined : backgroundPauseError(outcome.error, ctx.signal, resolvedOptions.signal);
|
|
214
|
+
if (outcome.ok) {
|
|
210
215
|
durableRun.transition({
|
|
211
216
|
state: "completed",
|
|
212
217
|
progress: progress.snapshot(),
|
|
213
218
|
usage: usage.snapshot(),
|
|
214
|
-
result:
|
|
219
|
+
result: outcome.value,
|
|
215
220
|
});
|
|
216
|
-
} else if (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (pauseError instanceof WorkflowProviderUsageLimitError) {
|
|
230
|
-
if (resolvedOptions.background === undefined) {
|
|
231
|
-
durableRun.transition({
|
|
232
|
-
state: "failed",
|
|
233
|
-
progress: progress.snapshot(),
|
|
234
|
-
usage: usage.snapshot(),
|
|
235
|
-
error: pauseError,
|
|
236
|
-
});
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const providerPause = createProviderUsageLimitPauseRecord(
|
|
240
|
-
pauseError,
|
|
241
|
-
resolvedOptions,
|
|
242
|
-
mod.source.kind === "file" && args.length === 0,
|
|
243
|
-
);
|
|
244
|
-
durableRun.transition({
|
|
245
|
-
state: "paused",
|
|
246
|
-
progress: progress.snapshot(),
|
|
247
|
-
...providerPause,
|
|
248
|
-
});
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
durableRun.transition({
|
|
252
|
-
state: "paused",
|
|
253
|
-
progress: progress.snapshot(),
|
|
254
|
-
message: unknownErrorMessage(pauseError),
|
|
255
|
-
});
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
221
|
+
} else if (pauseError instanceof WorkflowProviderUsageLimitError) {
|
|
222
|
+
durableRun.transition({
|
|
223
|
+
state: "paused",
|
|
224
|
+
progress: progress.snapshot(),
|
|
225
|
+
...createProviderUsageLimitPauseRecord(pauseError, resolvedOptions, mod.source.kind === "file" && args.length === 0),
|
|
226
|
+
});
|
|
227
|
+
} else if (pauseError) {
|
|
228
|
+
durableRun.transition({
|
|
229
|
+
state: "paused",
|
|
230
|
+
progress: progress.snapshot(),
|
|
231
|
+
message: unknownErrorMessage(pauseError),
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
258
234
|
durableRun.transition({
|
|
259
235
|
state: ctx.signal?.aborted || resolvedOptions.signal?.aborted ? "stopped" : "failed",
|
|
260
236
|
progress: progress.snapshot(),
|
|
261
237
|
usage: usage.snapshot(),
|
|
262
|
-
error,
|
|
238
|
+
error: outcome.error,
|
|
263
239
|
});
|
|
264
240
|
}
|
|
241
|
+
await durableRun.flush().catch(() => undefined);
|
|
265
242
|
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
throw finalizationOutcome.error;
|
|
269
|
-
}
|
|
270
|
-
if (finalizationOutcome.ok) throw workflowOutcome.error;
|
|
271
|
-
throw combinedWorkflowError(workflowOutcome.error, finalizationOutcome.error);
|
|
243
|
+
if (outcome.ok) return outcome.value;
|
|
244
|
+
throw outcome.error;
|
|
272
245
|
}
|
|
273
246
|
|
|
274
247
|
function backgroundPauseError(error: unknown, ...signals: Array<AbortSignal | undefined>): unknown {
|
|
@@ -315,7 +288,7 @@ async function finalizeWorkflowRun(input: WorkflowFinalizationInput): Promise<vo
|
|
|
315
288
|
if (!input.options.perf) return;
|
|
316
289
|
const snapshot = input.perf.snapshot();
|
|
317
290
|
await input.options.onPerfSnapshot?.(snapshot);
|
|
318
|
-
input.progress.log(formatPerfSummary(snapshot));
|
|
291
|
+
input.progress.log(formatPerfSummary(snapshot.aggregates));
|
|
319
292
|
},
|
|
320
293
|
},
|
|
321
294
|
...input.unlinkSignals.map((run, index) => ({
|
|
@@ -343,13 +316,7 @@ async function finalizeWorkflowRun(input: WorkflowFinalizationInput): Promise<vo
|
|
|
343
316
|
},
|
|
344
317
|
],
|
|
345
318
|
{
|
|
346
|
-
onBestEffortFailure: (failure) => {
|
|
347
|
-
try {
|
|
348
|
-
input.progress.log(`${failure.name} failed: ${unknownErrorMessage(failure.error)}`);
|
|
349
|
-
} catch {
|
|
350
|
-
// Observer failures must never replace the workflow outcome.
|
|
351
|
-
}
|
|
352
|
-
},
|
|
319
|
+
onBestEffortFailure: (failure) => input.progress.log(`${failure.name} failed: ${unknownErrorMessage(failure.error)}`),
|
|
353
320
|
},
|
|
354
321
|
);
|
|
355
322
|
}
|
|
@@ -491,24 +458,14 @@ function createWorkflowScope(progress: WorkflowProgress, prefix: string, namespa
|
|
|
491
458
|
function namespaceProgressEvent(namespace: string, event: WorkflowProgressEvent): WorkflowProgressEvent {
|
|
492
459
|
switch (event.type) {
|
|
493
460
|
case "counter":
|
|
494
|
-
return { ...event, key: `${namespace}.${event.key}` };
|
|
495
461
|
case "counter_delta":
|
|
462
|
+
case "summary":
|
|
496
463
|
return { ...event, key: `${namespace}.${event.key}` };
|
|
497
464
|
case "lane_item":
|
|
498
465
|
return { ...event, lane: `${namespace} ▸ ${event.lane}` };
|
|
499
|
-
case "summary":
|
|
500
|
-
return { ...event, key: `${namespace}.${event.key}` };
|
|
501
466
|
}
|
|
502
467
|
}
|
|
503
468
|
|
|
504
|
-
function formatPerfSummary(snapshot: PerfSnapshot): string {
|
|
505
|
-
const parts = snapshot.aggregates
|
|
506
|
-
.filter((aggregate) => aggregate.count > 0)
|
|
507
|
-
.slice(0, 5)
|
|
508
|
-
.map((aggregate) => `${aggregate.name} ${Math.round(aggregate.total)}ms`);
|
|
509
|
-
return parts.length > 0 ? `perf: ${parts.join(", ")}` : "perf: no samples";
|
|
510
|
-
}
|
|
511
|
-
|
|
512
469
|
async function notifyLifecycleObserver(
|
|
513
470
|
progress: ProgressTracker,
|
|
514
471
|
name: string,
|
|
@@ -10,7 +10,6 @@ export interface Finalizer {
|
|
|
10
10
|
|
|
11
11
|
export interface FinalizerFailure {
|
|
12
12
|
readonly name: string;
|
|
13
|
-
readonly criticality: FinalizerCriticality;
|
|
14
13
|
readonly error: unknown;
|
|
15
14
|
}
|
|
16
15
|
|
|
@@ -35,24 +34,18 @@ export class RequiredFinalizerError extends AggregateError {
|
|
|
35
34
|
export async function runFinalizers(
|
|
36
35
|
finalizers: readonly Finalizer[],
|
|
37
36
|
options: RunFinalizersOptions = {},
|
|
38
|
-
): Promise<
|
|
37
|
+
): Promise<void> {
|
|
39
38
|
const requiredFailures: FinalizerFailure[] = [];
|
|
40
|
-
const bestEffortFailures: FinalizerFailure[] = [];
|
|
41
39
|
|
|
42
40
|
for (const finalizer of finalizers) {
|
|
43
41
|
try {
|
|
44
42
|
await finalizer.run();
|
|
45
43
|
} catch (error) {
|
|
46
|
-
const failure: FinalizerFailure = {
|
|
47
|
-
name: finalizer.name,
|
|
48
|
-
criticality: finalizer.criticality,
|
|
49
|
-
error,
|
|
50
|
-
};
|
|
44
|
+
const failure: FinalizerFailure = { name: finalizer.name, error };
|
|
51
45
|
if (finalizer.criticality === "required") {
|
|
52
46
|
requiredFailures.push(failure);
|
|
53
47
|
continue;
|
|
54
48
|
}
|
|
55
|
-
bestEffortFailures.push(failure);
|
|
56
49
|
try {
|
|
57
50
|
options.onBestEffortFailure?.(failure);
|
|
58
51
|
} catch {
|
|
@@ -62,5 +55,4 @@ export async function runFinalizers(
|
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
if (requiredFailures.length > 0) throw new RequiredFinalizerError(requiredFailures);
|
|
65
|
-
return bestEffortFailures;
|
|
66
58
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { runBoundedProcess, type BoundedProcessResult } from "./process-runner.ts";
|
|
3
|
+
import { isPathWithin } from "./tree-fingerprint.ts";
|
|
4
|
+
|
|
5
|
+
export interface GitCommandOptions {
|
|
6
|
+
/** The GitHub CLI shares the runner so pull-request commands get the same bounds. Defaults to `git`. */
|
|
7
|
+
readonly file?: "git" | "gh";
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly args: readonly string[];
|
|
10
|
+
/** Names the command in its abort, timeout, output-limit, and exit messages. */
|
|
11
|
+
readonly label: string;
|
|
12
|
+
readonly signal?: AbortSignal;
|
|
13
|
+
readonly timeoutMs: number;
|
|
14
|
+
readonly maxBufferBytes?: number;
|
|
15
|
+
readonly stdin?: string;
|
|
16
|
+
/** Overrides merged over `process.env`. */
|
|
17
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runGit(options: GitCommandOptions): Promise<BoundedProcessResult> {
|
|
21
|
+
const { label, timeoutMs, maxBufferBytes } = options;
|
|
22
|
+
const env = { ...process.env, ...options.env };
|
|
23
|
+
// Drop env-level diff overrides so git emits plain patches. Setting them to an empty string
|
|
24
|
+
// instead makes a plain `git diff` try to run an empty external diff command.
|
|
25
|
+
delete env.GIT_EXTERNAL_DIFF;
|
|
26
|
+
delete env.GIT_DIFF_OPTS;
|
|
27
|
+
return await runBoundedProcess({
|
|
28
|
+
file: options.file ?? "git",
|
|
29
|
+
args: options.args,
|
|
30
|
+
cwd: options.cwd,
|
|
31
|
+
env,
|
|
32
|
+
stdin: options.stdin,
|
|
33
|
+
signal: options.signal,
|
|
34
|
+
timeoutMs,
|
|
35
|
+
maxBufferBytes,
|
|
36
|
+
abortError: `${label} aborted`,
|
|
37
|
+
timeoutError: `${label} timed out after ${timeoutMs}ms`,
|
|
38
|
+
maxBufferError: maxBufferBytes === undefined ? undefined : `${label} exceeded ${maxBufferBytes} bytes`,
|
|
39
|
+
exitError: (stderr, code, signal) => stderr.trim() || `${label} exited with code ${code ?? `signal ${signal ?? "unknown"}`}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A full SHA-1 or SHA-256 Git object ID. */
|
|
44
|
+
export function isGitObjectId(value: string): boolean {
|
|
45
|
+
return /^[0-9a-f]{40,64}$/i.test(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve `git rev-parse --show-toplevel` output to a root that contains `cwd`. */
|
|
49
|
+
export function parseGitTopLevel(output: string, cwd: string): string | undefined {
|
|
50
|
+
const withoutLf = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
51
|
+
const value = withoutLf.endsWith("\r") ? withoutLf.slice(0, -1) : withoutLf;
|
|
52
|
+
if (value.length === 0 || value.includes("\n") || value.includes("\0")) return undefined;
|
|
53
|
+
const root = resolve(cwd, value);
|
|
54
|
+
return isPathWithin(root, cwd) ? root : undefined;
|
|
55
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** A non-null, non-array object whose fields can be inspected by name. */
|
|
2
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isFiniteNumber(value: unknown): value is number {
|
|
7
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
8
|
+
}
|
|
@@ -2,15 +2,9 @@ export type IdentityCanonicalization =
|
|
|
2
2
|
| { readonly kind: "verified"; readonly value: string }
|
|
3
3
|
| { readonly kind: "unverifiable"; readonly reason: string };
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly maxNodes?: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const DEFAULT_MAX_BYTES = 1 << 20;
|
|
12
|
-
const DEFAULT_MAX_DEPTH = 64;
|
|
13
|
-
const DEFAULT_MAX_NODES = 16_384;
|
|
5
|
+
const MAX_BYTES = 1 << 20;
|
|
6
|
+
const MAX_DEPTH = 64;
|
|
7
|
+
const MAX_NODES = 16_384;
|
|
14
8
|
|
|
15
9
|
/**
|
|
16
10
|
* Produce a deterministic, bounded identity for JSON-like data.
|
|
@@ -19,13 +13,7 @@ const DEFAULT_MAX_NODES = 16_384;
|
|
|
19
13
|
* accessors, proxies, or values JSON cannot represent. Those inputs disable
|
|
20
14
|
* replay instead of throwing or being silently collapsed to the same key.
|
|
21
15
|
*/
|
|
22
|
-
export function canonicalizeIdentity(
|
|
23
|
-
value: unknown,
|
|
24
|
-
options: IdentityCanonicalizationOptions = {},
|
|
25
|
-
): IdentityCanonicalization {
|
|
26
|
-
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
27
|
-
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
28
|
-
const maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
16
|
+
export function canonicalizeIdentity(value: unknown): IdentityCanonicalization {
|
|
29
17
|
const active = new WeakSet<object>();
|
|
30
18
|
const chunks: string[] = [];
|
|
31
19
|
let bytes = 0;
|
|
@@ -33,14 +21,14 @@ export function canonicalizeIdentity(
|
|
|
33
21
|
|
|
34
22
|
const append = (chunk: string): void => {
|
|
35
23
|
bytes += Buffer.byteLength(chunk);
|
|
36
|
-
if (bytes >
|
|
24
|
+
if (bytes > MAX_BYTES) throw new IdentityCanonicalizationError(`identity exceeded ${MAX_BYTES} bytes`);
|
|
37
25
|
chunks.push(chunk);
|
|
38
26
|
};
|
|
39
27
|
|
|
40
28
|
const visit = (current: unknown, depth: number): void => {
|
|
41
29
|
nodes += 1;
|
|
42
|
-
if (nodes >
|
|
43
|
-
if (depth >
|
|
30
|
+
if (nodes > MAX_NODES) throw new IdentityCanonicalizationError(`identity exceeded ${MAX_NODES} values`);
|
|
31
|
+
if (depth > MAX_DEPTH) throw new IdentityCanonicalizationError(`identity exceeded ${MAX_DEPTH} levels`);
|
|
44
32
|
|
|
45
33
|
if (current === null) {
|
|
46
34
|
append("n;");
|