pi-workflow-engine 0.11.0 → 0.12.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 +255 -97
- package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +0 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-attempt.ts +3 -1
- package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +35 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-replay.ts +27 -4
- package/.pi/extensions/pi-workflow-engine/src/agent-retry.ts +116 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +49 -45
- package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +95 -26
- package/.pi/extensions/pi-workflow-engine/src/agent-session-providers.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +158 -203
- package/.pi/extensions/pi-workflow-engine/src/agent-skills.ts +48 -42
- package/.pi/extensions/pi-workflow-engine/src/agent-workspace.ts +11 -3
- package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +75 -0
- package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +492 -0
- package/.pi/extensions/pi-workflow-engine/src/budget.ts +3 -3
- package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +38 -0
- package/.pi/extensions/pi-workflow-engine/src/command-completions.ts +36 -0
- package/.pi/extensions/pi-workflow-engine/src/concurrency.ts +70 -4
- package/.pi/extensions/pi-workflow-engine/src/diff-capture.ts +16 -130
- package/.pi/extensions/pi-workflow-engine/src/discovery.ts +9 -5
- package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +109 -60
- package/.pi/extensions/pi-workflow-engine/src/engine.ts +166 -18
- package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +4 -0
- package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +2 -2
- package/.pi/extensions/pi-workflow-engine/src/journal.ts +12 -6
- package/.pi/extensions/pi-workflow-engine/src/model-profile-command.ts +167 -0
- package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +310 -0
- package/.pi/extensions/pi-workflow-engine/src/options.ts +88 -1
- package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +50 -0
- package/.pi/extensions/pi-workflow-engine/src/progress.ts +72 -117
- package/.pi/extensions/pi-workflow-engine/src/provider-usage-limit.ts +189 -0
- package/.pi/extensions/pi-workflow-engine/src/research-contract.ts +89 -0
- package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +289 -0
- package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +74 -38
- package/.pi/extensions/pi-workflow-engine/src/review/code-review-orchestration.ts +49 -0
- package/.pi/extensions/pi-workflow-engine/src/review/review-report.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-flow.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/review/review-results-viewer.ts +11 -30
- package/.pi/extensions/pi-workflow-engine/src/review/review-session-coordinator.ts +5 -5
- package/.pi/extensions/pi-workflow-engine/src/review/review-snapshot.ts +3 -4
- package/.pi/extensions/pi-workflow-engine/src/review-diff-target.ts +130 -0
- package/.pi/extensions/pi-workflow-engine/src/session-identity.ts +6 -0
- package/.pi/extensions/pi-workflow-engine/src/structured-output.ts +32 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +44 -0
- package/.pi/extensions/pi-workflow-engine/src/tool-source-identity.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/tree-fingerprint.ts +1 -4
- package/.pi/extensions/pi-workflow-engine/src/types.ts +49 -15
- package/.pi/extensions/pi-workflow-engine/src/ui/dynamax-editor-decoration.ts +220 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +26 -6
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +88 -46
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-result-renderer.ts +2 -1
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-viewer-layout.ts +51 -0
- package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +12 -67
- package/.pi/extensions/pi-workflow-engine/src/usage.ts +124 -16
- package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +23 -3
- package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +1 -1
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +72 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +354 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +162 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +632 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +172 -0
- package/.pi/extensions/pi-workflow-engine/src/workflow-usage-limit-scheduler.ts +80 -0
- package/.pi/extensions/pi-workflow-engine/src/workflows.ts +2 -0
- package/.pi/extensions/pi-workflow-engine/src/worktree.ts +40 -3
- package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +18 -42
- package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +10 -17
- package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +8 -15
- package/.pi/extensions/pi-workflow-engine/workflows/research.ts +170 -0
- package/README.md +72 -141
- package/USAGE.md +211 -17
- package/package.json +1 -1
|
@@ -78,6 +78,8 @@ export interface ParallelSettledOptions {
|
|
|
78
78
|
export interface ParallelSettledError {
|
|
79
79
|
readonly name?: string;
|
|
80
80
|
readonly message: string;
|
|
81
|
+
readonly code?: string;
|
|
82
|
+
readonly details?: Readonly<Record<string, string | number | boolean | null>>;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export type ParallelSettledResult<T> =
|
|
@@ -229,14 +231,48 @@ async function runParallel<T, Result>(
|
|
|
229
231
|
function serializeParallelError(error: unknown): ParallelSettledError {
|
|
230
232
|
const message = readStringProperty(error, "message") ?? unknownErrorMessage(error);
|
|
231
233
|
const name = readStringProperty(error, "name");
|
|
232
|
-
|
|
234
|
+
const code = readStringProperty(error, "code");
|
|
235
|
+
const details = readSerializableDetails(error);
|
|
236
|
+
return {
|
|
237
|
+
...(name && name.length > 0 ? { name } : {}),
|
|
238
|
+
message,
|
|
239
|
+
...(code && code.length > 0 ? { code } : {}),
|
|
240
|
+
...(details ? { details } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readStringProperty(value: unknown, property: "message" | "name" | "code"): string | undefined {
|
|
245
|
+
const candidate = readProperty(value, property);
|
|
246
|
+
return typeof candidate === "string" ? candidate : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function readSerializableDetails(
|
|
250
|
+
value: unknown,
|
|
251
|
+
): Readonly<Record<string, string | number | boolean | null>> | undefined {
|
|
252
|
+
const candidate = readProperty(value, "details");
|
|
253
|
+
if (typeof candidate !== "object" || candidate === null) return undefined;
|
|
254
|
+
try {
|
|
255
|
+
const details: Record<string, string | number | boolean | null> = {};
|
|
256
|
+
for (const [key, detail] of Object.entries(candidate)) {
|
|
257
|
+
if (
|
|
258
|
+
typeof detail === "string" ||
|
|
259
|
+
typeof detail === "boolean" ||
|
|
260
|
+
detail === null ||
|
|
261
|
+
(typeof detail === "number" && Number.isFinite(detail))
|
|
262
|
+
) {
|
|
263
|
+
details[key] = detail;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return Object.keys(details).length > 0 ? details : undefined;
|
|
267
|
+
} catch {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
233
270
|
}
|
|
234
271
|
|
|
235
|
-
function
|
|
272
|
+
function readProperty(value: unknown, property: string): unknown {
|
|
236
273
|
if ((typeof value !== "object" && typeof value !== "function") || value === null) return undefined;
|
|
237
274
|
try {
|
|
238
|
-
|
|
239
|
-
return typeof candidate === "string" ? candidate : undefined;
|
|
275
|
+
return Reflect.get(value, property);
|
|
240
276
|
} catch {
|
|
241
277
|
return undefined;
|
|
242
278
|
}
|
|
@@ -275,6 +311,21 @@ export async function pipeline<Item, A, B, C>(
|
|
|
275
311
|
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
276
312
|
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
277
313
|
): Promise<Array<C | null>>;
|
|
314
|
+
export async function pipeline<Item, A, B, C, D>(
|
|
315
|
+
items: readonly Item[],
|
|
316
|
+
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
317
|
+
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
318
|
+
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
319
|
+
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
320
|
+
): Promise<Array<D | null>>;
|
|
321
|
+
export async function pipeline<Item, A, B, C, D, E>(
|
|
322
|
+
items: readonly Item[],
|
|
323
|
+
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
324
|
+
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
325
|
+
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
326
|
+
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
327
|
+
stage5: (prev: D, item: Item, index: number) => Promise<E>,
|
|
328
|
+
): Promise<Array<E | null>>;
|
|
278
329
|
export async function pipeline(
|
|
279
330
|
items: readonly unknown[],
|
|
280
331
|
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
@@ -302,6 +353,21 @@ export function bindPipeline(options: PipelineOptions): Pipeline {
|
|
|
302
353
|
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
303
354
|
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
304
355
|
): Promise<Array<C | null>>;
|
|
356
|
+
async function boundPipeline<Item, A, B, C, D>(
|
|
357
|
+
items: readonly Item[],
|
|
358
|
+
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
359
|
+
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
360
|
+
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
361
|
+
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
362
|
+
): Promise<Array<D | null>>;
|
|
363
|
+
async function boundPipeline<Item, A, B, C, D, E>(
|
|
364
|
+
items: readonly Item[],
|
|
365
|
+
stage1: (prev: Item, item: Item, index: number) => Promise<A>,
|
|
366
|
+
stage2: (prev: A, item: Item, index: number) => Promise<B>,
|
|
367
|
+
stage3: (prev: B, item: Item, index: number) => Promise<C>,
|
|
368
|
+
stage4: (prev: C, item: Item, index: number) => Promise<D>,
|
|
369
|
+
stage5: (prev: D, item: Item, index: number) => Promise<E>,
|
|
370
|
+
): Promise<Array<E | null>>;
|
|
305
371
|
async function boundPipeline(
|
|
306
372
|
items: readonly unknown[],
|
|
307
373
|
...stages: Array<(prev: unknown, item: unknown, index: number) => Promise<unknown>>
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
-
import { Type, type Static } from "typebox";
|
|
2
|
-
import { Value } from "typebox/value";
|
|
3
1
|
import type { PerfSink } from "./perf.ts";
|
|
4
2
|
import { runBoundedProcess, type BoundedProcessFailure } from "./process-runner.ts";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export const ReviewDiffTargetSchema = Type.Union([GitReviewDiffTargetSchema, PullRequestReviewDiffTargetSchema]);
|
|
22
|
-
|
|
23
|
-
export type GitReviewDiffTarget = Static<typeof GitReviewDiffTargetSchema>;
|
|
24
|
-
export type PullRequestReviewDiffTarget = Static<typeof PullRequestReviewDiffTargetSchema>;
|
|
25
|
-
|
|
26
|
-
/** Serializable semantic identity of the diff being reviewed. */
|
|
27
|
-
export type ReviewDiffTarget = Static<typeof ReviewDiffTargetSchema>;
|
|
3
|
+
import { parseAllowedDiffCommand, reviewDiffCommand, type ReviewDiffTarget } from "./review-diff-target.ts";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
formatReviewDiffTarget,
|
|
7
|
+
GitReviewDiffTargetSchema,
|
|
8
|
+
isReviewDiffTarget,
|
|
9
|
+
parseAllowedDiffCommand,
|
|
10
|
+
PullRequestReviewDiffTargetSchema,
|
|
11
|
+
reviewDiffCommand,
|
|
12
|
+
ReviewDiffTargetSchema,
|
|
13
|
+
reviewGitDiffBaseline,
|
|
14
|
+
type GitDiffBaselineTarget,
|
|
15
|
+
type GitReviewDiffTarget,
|
|
16
|
+
type PullRequestReviewDiffTarget,
|
|
17
|
+
type ReviewDiffTarget,
|
|
18
|
+
} from "./review-diff-target.ts";
|
|
28
19
|
|
|
29
20
|
interface DiffCaptureResultBase {
|
|
30
21
|
readonly stdout: string;
|
|
@@ -48,78 +39,6 @@ export interface DiffCaptureOptions {
|
|
|
48
39
|
readonly killGraceMs?: number;
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
const SAFE_REF_OR_PATH = /^[A-Za-z0-9_./:@~+=,\-]+$/;
|
|
52
|
-
const SAFE_GH_DIFF_FLAGS = new Set(["--color=never"]);
|
|
53
|
-
const SAFE_GIT_DIFF_FLAGS = new Set(["--binary", "--cached", "--staged", "--no-color", "--color=never", "--no-ext-diff"]);
|
|
54
|
-
|
|
55
|
-
export function parseAllowedDiffCommand(command: string): ReviewDiffTarget | { error: string } {
|
|
56
|
-
const tokens = command.trim().split(/\s+/).filter(Boolean);
|
|
57
|
-
if (tokens.length < 2) return { error: "empty or incomplete diff command" };
|
|
58
|
-
if (tokens[0] === "git" && tokens[1] === "diff") {
|
|
59
|
-
const parsed = parseGitDiff(tokens);
|
|
60
|
-
return "error" in parsed ? parsed : parsed.target;
|
|
61
|
-
}
|
|
62
|
-
if (tokens[0] === "gh" && tokens[1] === "pr" && tokens[2] === "diff" && /^\d+$/.test(tokens[3] ?? "")) {
|
|
63
|
-
const number = Number(tokens[3]);
|
|
64
|
-
if (!Number.isSafeInteger(number) || number <= 0) return { error: "pull-request number must be a positive safe integer" };
|
|
65
|
-
const flags = tokens.slice(4);
|
|
66
|
-
if (flags.includes("--patch")) return { error: "gh pr diff --patch is not supported because reviews require the cumulative pull-request diff" };
|
|
67
|
-
const unsupported = flags.find((token) => !SAFE_GH_DIFF_FLAGS.has(token));
|
|
68
|
-
if (unsupported) return { error: `unsupported gh pr diff option: ${unsupported}` };
|
|
69
|
-
return { kind: "pull-request", number };
|
|
70
|
-
}
|
|
71
|
-
return { error: "diff command is not in the git/gh allowlist" };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function parseGitDiff(
|
|
75
|
-
tokens: readonly string[],
|
|
76
|
-
): { readonly target: GitReviewDiffTarget; readonly baseline: GitDiffBaselineTarget } | { readonly error: string } {
|
|
77
|
-
const args = tokens.slice(2);
|
|
78
|
-
const safeArgs = ["diff", "--no-ext-diff"];
|
|
79
|
-
const operands: string[] = [];
|
|
80
|
-
let staged = false;
|
|
81
|
-
let pathMode = false;
|
|
82
|
-
|
|
83
|
-
for (const token of args) {
|
|
84
|
-
if (!SAFE_REF_OR_PATH.test(token)) return { error: `unsupported git diff token: ${token}` };
|
|
85
|
-
if (token === "--") {
|
|
86
|
-
pathMode = true;
|
|
87
|
-
safeArgs.push(token);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (!pathMode && token.startsWith("-")) {
|
|
91
|
-
if (!isAllowedGitDiffFlag(token)) return { error: `unsupported git diff option: ${token}` };
|
|
92
|
-
staged ||= token === "--cached" || token === "--staged";
|
|
93
|
-
if (token !== "--no-ext-diff") safeArgs.push(token);
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (!pathMode) operands.push(token);
|
|
97
|
-
safeArgs.push(token);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const baseline = classifyGitDiffBaseline(staged, operands);
|
|
101
|
-
return "error" in baseline ? baseline : { target: { kind: "git", args: safeArgs }, baseline };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function classifyGitDiffBaseline(staged: boolean, operands: readonly string[]): GitDiffBaselineTarget | { readonly error: string } {
|
|
105
|
-
if (operands.length > 1) {
|
|
106
|
-
return { error: "ambiguous git diff operands; use A..B or A...B for revisions, or -- before multiple paths" };
|
|
107
|
-
}
|
|
108
|
-
if (staged) return { kind: "index" };
|
|
109
|
-
|
|
110
|
-
const range = operands.find((operand) => operand.includes(".."));
|
|
111
|
-
if (range) {
|
|
112
|
-
const separator = range.includes("...") ? "..." : "..";
|
|
113
|
-
return { kind: "range", ref: range.slice(range.indexOf(separator) + separator.length) || "HEAD" };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { kind: "working-tree" };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isAllowedGitDiffFlag(token: string): boolean {
|
|
120
|
-
return SAFE_GIT_DIFF_FLAGS.has(token) || /^-U\d+$/.test(token) || /^--unified=\d+$/.test(token) || /^--inter-hunk-context=\d+$/.test(token);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
42
|
export async function captureDiff(command: string, options: DiffCaptureOptions): Promise<DiffCaptureResult> {
|
|
124
43
|
const parsed = parseAllowedDiffCommand(command);
|
|
125
44
|
if ("error" in parsed) {
|
|
@@ -157,39 +76,6 @@ export async function captureDiffTarget(target: ReviewDiffTarget, options: DiffC
|
|
|
157
76
|
return result;
|
|
158
77
|
}
|
|
159
78
|
|
|
160
|
-
export function reviewDiffCommand(target: ReviewDiffTarget): { readonly file: "git" | "gh"; readonly args: readonly string[] } {
|
|
161
|
-
return target.kind === "pull-request"
|
|
162
|
-
? { file: "gh", args: ["pr", "diff", String(target.number), "--color=never"] }
|
|
163
|
-
: { file: "git", args: target.args };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Canonical display form. Never execute this string directly; use reviewDiffCommand(). */
|
|
167
|
-
export function formatReviewDiffTarget(target: ReviewDiffTarget): string {
|
|
168
|
-
const command = reviewDiffCommand(target);
|
|
169
|
-
return [command.file, ...command.args].join(" ");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/** Derive reconstruction semantics from the canonical git command instead of persisting duplicate state. */
|
|
173
|
-
export function reviewGitDiffBaseline(target: GitReviewDiffTarget): GitDiffBaselineTarget {
|
|
174
|
-
const parsed = parseGitDiff(["git", ...target.args]);
|
|
175
|
-
if ("error" in parsed) throw new Error(`Invalid persisted git review target: ${parsed.error}`);
|
|
176
|
-
return parsed.baseline;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Validate persisted targets by round-tripping through the single command allowlist parser. */
|
|
180
|
-
export function isReviewDiffTarget(value: unknown): value is ReviewDiffTarget {
|
|
181
|
-
if (!Value.Check(ReviewDiffTargetSchema, value)) return false;
|
|
182
|
-
const parsed = parseAllowedDiffCommand(formatReviewDiffTarget(value));
|
|
183
|
-
return !("error" in parsed) && sameReviewDiffTarget(parsed, value);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function sameReviewDiffTarget(left: ReviewDiffTarget, right: ReviewDiffTarget): boolean {
|
|
187
|
-
if (left.kind !== right.kind) return false;
|
|
188
|
-
if (left.kind === "pull-request") return right.kind === "pull-request" && left.number === right.number;
|
|
189
|
-
if (right.kind !== "git" || left.args.length !== right.args.length) return false;
|
|
190
|
-
return left.args.every((arg, index) => arg === right.args[index]);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
79
|
function diffCaptureEnv(env: NodeJS.ProcessEnv | undefined): NodeJS.ProcessEnv {
|
|
194
80
|
return { ...(env ?? process.env), GIT_EXTERNAL_DIFF: "", GIT_DIFF_OPTS: "" };
|
|
195
81
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { LoadedWorkflow, WorkflowSourceIdentity } from "./types.ts";
|
|
6
6
|
import type { PerfSink } from "./perf.ts";
|
|
7
7
|
import { loadWorkflow, parseWorkflowModule } from "./workflow-module.ts";
|
|
8
8
|
import { BUILTIN_SOURCE_ROOT, BUILTIN_WORKFLOW_DEFINITIONS, BUILTIN_WORKFLOW_FILES } from "./workflows.ts";
|
|
9
9
|
import { captureTreeFingerprint } from "./tree-fingerprint.ts";
|
|
10
10
|
import { FINGERPRINT_EXCLUDED_RELATIVE_PATHS } from "./resume-context.ts";
|
|
11
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
11
12
|
|
|
12
13
|
export interface DiscoverWorkflowsOptions {
|
|
13
14
|
readonly refresh?: boolean;
|
|
@@ -15,6 +16,9 @@ export interface DiscoverWorkflowsOptions {
|
|
|
15
16
|
readonly userWorkflowDir?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const DISCOVERY_FINGERPRINT_MAX_BYTES = 32 << 20;
|
|
20
|
+
const DISCOVERY_FINGERPRINT_MAX_FILES = 4096;
|
|
21
|
+
|
|
18
22
|
const discoveryCache = new Map<string, Map<string, LoadedWorkflow>>();
|
|
19
23
|
/** Best-effort dynamic load of every `*.ts` workflow in a directory. */
|
|
20
24
|
async function loadDir(
|
|
@@ -46,7 +50,7 @@ async function loadDir(
|
|
|
46
50
|
} catch (error) {
|
|
47
51
|
// Drop-in loading depends on the runtime resolving TS + the bundled typebox.
|
|
48
52
|
// Failures here are non-fatal: the static registry still works.
|
|
49
|
-
console.error(`[workflow-engine] skipped ${name}: ${
|
|
53
|
+
console.error(`[workflow-engine] skipped ${name}: ${unknownErrorMessage(error)}`);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
return modules.map(({ path, module }) => loadWorkflow(module, sourceIdentity(path)));
|
|
@@ -56,8 +60,8 @@ async function captureDiscoveryFingerprint(root: string) {
|
|
|
56
60
|
return await captureTreeFingerprint({
|
|
57
61
|
root,
|
|
58
62
|
excludedRelativePaths: FINGERPRINT_EXCLUDED_RELATIVE_PATHS,
|
|
59
|
-
maxBytes:
|
|
60
|
-
maxFiles:
|
|
63
|
+
maxBytes: DISCOVERY_FINGERPRINT_MAX_BYTES,
|
|
64
|
+
maxFiles: DISCOVERY_FINGERPRINT_MAX_FILES,
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -66,7 +70,7 @@ async function captureDiscoveryFingerprint(root: string) {
|
|
|
66
70
|
* bundled example is always the verified one even if a same-named file is dropped in.
|
|
67
71
|
*/
|
|
68
72
|
export async function discoverWorkflows(repoDir: string, options: DiscoverWorkflowsOptions = {}): Promise<Map<string, LoadedWorkflow>> {
|
|
69
|
-
const userWorkflowDir = options.userWorkflowDir ?? join(
|
|
73
|
+
const userWorkflowDir = options.userWorkflowDir ?? join(getAgentDir(), "workflows");
|
|
70
74
|
const cacheKey = `${repoDir}\0${userWorkflowDir}`;
|
|
71
75
|
const cached = discoveryCache.get(cacheKey);
|
|
72
76
|
if (cached && !options.refresh) {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type
|
|
3
|
-
import type { KeyId } from "@earendil-works/pi-tui";
|
|
2
|
+
import { CustomEditor, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { EditorComponent, KeyId } from "@earendil-works/pi-tui";
|
|
4
|
+
import { completeCurrentArgument } from "./command-completions.ts";
|
|
4
5
|
import { resolveDynamaxShortcuts, type DynamaxShortcuts } from "./dynamax-shortcuts.ts";
|
|
6
|
+
import { sessionKey } from "./session-identity.ts";
|
|
7
|
+
import { unknownErrorMessage } from "./unknown-error.ts";
|
|
8
|
+
import {
|
|
9
|
+
decorateDynamaxEditor,
|
|
10
|
+
resolveDynamaxEffect,
|
|
11
|
+
type DynamaxAnimationScheduler,
|
|
12
|
+
type DynamaxEditorDecoration,
|
|
13
|
+
type DynamaxEffect,
|
|
14
|
+
} from "./ui/dynamax-editor-decoration.ts";
|
|
5
15
|
|
|
6
16
|
export interface DynamaxState {
|
|
7
17
|
sticky: boolean;
|
|
@@ -15,13 +25,34 @@ export interface DynamaxRuntime {
|
|
|
15
25
|
}
|
|
16
26
|
|
|
17
27
|
export type DynamaxRuntimeStore = Map<string, DynamaxRuntime>;
|
|
28
|
+
|
|
18
29
|
export interface DynamaxRegistrationOptions {
|
|
19
30
|
openInspector?: (ctx: ExtensionContext) => Promise<void> | void;
|
|
31
|
+
effect?: DynamaxEffect;
|
|
32
|
+
animationScheduler?: DynamaxAnimationScheduler;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
export const DYNAMAX_TOKEN_PATTERN = /(^|[^A-Za-z0-9_])dynamax([^A-Za-z0-9_]|$)/i;
|
|
23
36
|
export const DYNAMAX_STATUS_KEY = "dynamax";
|
|
24
|
-
|
|
37
|
+
|
|
38
|
+
type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
|
|
39
|
+
|
|
40
|
+
interface DynamaxEditorInstallation {
|
|
41
|
+
factory: EditorFactory;
|
|
42
|
+
previousFactory: EditorFactory | undefined;
|
|
43
|
+
decorations: Set<DynamaxEditorDecoration>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disposeDynamaxDecorations(decorations: Set<DynamaxEditorDecoration>): void {
|
|
47
|
+
for (const decoration of decorations) decoration.dispose();
|
|
48
|
+
decorations.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DYNAMAX_ACTION_COMPLETIONS = [
|
|
52
|
+
{ value: "on", description: "Keep Dynamax enabled for this session" },
|
|
53
|
+
{ value: "off", description: "Disable sticky and pending Dynamax modes" },
|
|
54
|
+
{ value: "status", description: "Show the current Dynamax state" },
|
|
55
|
+
] as const;
|
|
25
56
|
|
|
26
57
|
export const ADAPTIVE_WORKFLOW_GUIDANCE = `
|
|
27
58
|
Adaptive multi-pass workflows are optional. Use a simple single-pass fan-out when it is sufficient. When the first pass may expose gaps, conflicts, weak claims, or missing evidence:
|
|
@@ -40,7 +71,7 @@ The user has opted into dynamax multi-agent orchestration. The workflow tool is
|
|
|
40
71
|
Inline workflow rules:
|
|
41
72
|
- Use the injected Type object for schemas, for example Type.Object({ ok: Type.Boolean() }); do not import typebox.
|
|
42
73
|
- Do not use import statements or dynamic import() in inline workflow scripts.
|
|
43
|
-
- Set
|
|
74
|
+
- Set profile to "small", "medium", or "big" on each agent() call so routing remains explicit; use model/thinkingLevel only for an intentional override.
|
|
44
75
|
- Subagents receive no skills by default. Add \`skills: ["skill-name"]\` per agent only when that stage should load that skill; grant the smallest useful set.
|
|
45
76
|
- Provide exactly one of workflow.name or workflow.script.
|
|
46
77
|
|
|
@@ -57,12 +88,8 @@ export function createDynamaxRuntime(): DynamaxRuntime {
|
|
|
57
88
|
return { state: createDynamaxState() };
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
export function dynamaxSessionKey(ctx: Pick<ExtensionContext, "sessionManager">): string {
|
|
61
|
-
return ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
91
|
export function getDynamaxRuntime(store: DynamaxRuntimeStore, ctx: Pick<ExtensionContext, "sessionManager">): DynamaxRuntime {
|
|
65
|
-
const key =
|
|
92
|
+
const key = sessionKey(ctx);
|
|
66
93
|
const existing = store.get(key);
|
|
67
94
|
if (existing) return existing;
|
|
68
95
|
const runtime = createDynamaxRuntime();
|
|
@@ -78,12 +105,8 @@ export function markDynamaxOneShot(state: DynamaxState): void {
|
|
|
78
105
|
state.oneShotPending = true;
|
|
79
106
|
}
|
|
80
107
|
|
|
81
|
-
export function
|
|
82
|
-
state.sticky =
|
|
83
|
-
if (!sticky) {
|
|
84
|
-
state.oneShotPending = false;
|
|
85
|
-
state.turnActive = false;
|
|
86
|
-
}
|
|
108
|
+
export function enableDynamaxSticky(state: DynamaxState): void {
|
|
109
|
+
state.sticky = true;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
export function clearDynamax(state: DynamaxState): void {
|
|
@@ -92,12 +115,6 @@ export function clearDynamax(state: DynamaxState): void {
|
|
|
92
115
|
state.turnActive = false;
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
export function consumeDynamaxOneShot(state: DynamaxState): boolean {
|
|
96
|
-
const pending = state.oneShotPending;
|
|
97
|
-
state.oneShotPending = false;
|
|
98
|
-
return pending;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
118
|
export function isDynamaxActive(state: DynamaxState): boolean {
|
|
102
119
|
return state.sticky || state.oneShotPending || state.turnActive;
|
|
103
120
|
}
|
|
@@ -139,23 +156,84 @@ export function registerDynamax(
|
|
|
139
156
|
options: DynamaxRegistrationOptions = {},
|
|
140
157
|
): void {
|
|
141
158
|
const runtimes: DynamaxRuntimeStore = new Map();
|
|
159
|
+
const effect = options.effect ?? resolveDynamaxEffect();
|
|
160
|
+
let editorInstallation: DynamaxEditorInstallation | undefined;
|
|
142
161
|
const openInspector =
|
|
143
162
|
options.openInspector ??
|
|
144
163
|
((ctx: ExtensionContext): void => {
|
|
145
164
|
ctx.ui.notify("No workflow inspector is available yet", "warning");
|
|
146
165
|
});
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
const installEditor = (ctx: ExtensionContext): void => {
|
|
167
|
+
if (ctx.mode !== "tui" || effect === "off") return;
|
|
168
|
+
const previousFactory = ctx.ui.getEditorComponent();
|
|
169
|
+
if (editorInstallation && editorInstallation.factory === previousFactory) return;
|
|
170
|
+
|
|
171
|
+
const decorations = new Set<DynamaxEditorDecoration>();
|
|
172
|
+
const installation: DynamaxEditorInstallation = {
|
|
173
|
+
previousFactory,
|
|
174
|
+
decorations,
|
|
175
|
+
factory: (tui, theme, keybindings) => {
|
|
176
|
+
const decorate = (editor: EditorComponent): EditorComponent => {
|
|
177
|
+
const decoration = decorateDynamaxEditor(editor, () => tui.requestRender(), {
|
|
178
|
+
effect,
|
|
179
|
+
scheduler: options.animationScheduler,
|
|
180
|
+
isActive: () => editorInstallation === installation && ctx.ui.getEditorComponent() === installation.factory,
|
|
181
|
+
});
|
|
182
|
+
decorations.add(decoration);
|
|
183
|
+
return decoration.editor;
|
|
184
|
+
};
|
|
185
|
+
if (!previousFactory) return decorate(new CustomEditor(tui, theme, keybindings));
|
|
186
|
+
|
|
187
|
+
let editor: EditorComponent;
|
|
188
|
+
try {
|
|
189
|
+
editor = previousFactory(tui, theme, keybindings);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
ctx.ui.notify(
|
|
192
|
+
`Dynamax could not compose the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
|
|
193
|
+
"warning",
|
|
194
|
+
);
|
|
195
|
+
installation.previousFactory = undefined;
|
|
196
|
+
return decorate(new CustomEditor(tui, theme, keybindings));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
return decorate(editor);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
ctx.ui.notify(
|
|
203
|
+
`Dynamax could not decorate the existing custom editor (${unknownErrorMessage(error)}); using pi's stock-compatible CustomEditor so highlighting stays enabled`,
|
|
204
|
+
"warning",
|
|
205
|
+
);
|
|
206
|
+
return decorate(new CustomEditor(tui, theme, keybindings));
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.setEditorComponent(installation.factory);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
disposeDynamaxDecorations(decorations);
|
|
215
|
+
ctx.ui.notify(`Dynamax highlighting could not be installed: ${unknownErrorMessage(error)}`, "error");
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
editorInstallation = installation;
|
|
219
|
+
};
|
|
220
|
+
const uninstallEditor = (ctx: ExtensionContext): void => {
|
|
221
|
+
const installation = editorInstallation;
|
|
222
|
+
if (!installation) return;
|
|
223
|
+
editorInstallation = undefined;
|
|
224
|
+
disposeDynamaxDecorations(installation.decorations);
|
|
225
|
+
if (ctx.mode === "tui" && ctx.ui.getEditorComponent() === installation.factory) {
|
|
226
|
+
ctx.ui.setEditorComponent(installation.previousFactory);
|
|
151
227
|
}
|
|
152
228
|
};
|
|
153
229
|
|
|
154
230
|
pi.on("session_start", (_event, ctx) => {
|
|
231
|
+
installEditor(ctx);
|
|
155
232
|
updateDynamaxSurfaces(ctx, getDynamaxRuntime(runtimes, ctx), shortcuts);
|
|
156
233
|
});
|
|
157
234
|
|
|
158
235
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
236
|
+
uninstallEditor(ctx);
|
|
159
237
|
clearDynamaxSurfaces(ctx);
|
|
160
238
|
});
|
|
161
239
|
|
|
@@ -177,26 +255,6 @@ export function registerDynamax(
|
|
|
177
255
|
return { systemPrompt };
|
|
178
256
|
});
|
|
179
257
|
|
|
180
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
181
|
-
refreshIfVisible(ctx);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
pi.on("message_start", (_event, ctx) => {
|
|
185
|
-
refreshIfVisible(ctx);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
pi.on("message_update", (_event, ctx) => {
|
|
189
|
-
refreshIfVisible(ctx);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
pi.on("message_end", (_event, ctx) => {
|
|
193
|
-
refreshIfVisible(ctx);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
pi.on("turn_end", (_event, ctx) => {
|
|
197
|
-
refreshIfVisible(ctx);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
258
|
pi.on("agent_end", (_event, ctx) => {
|
|
201
259
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
202
260
|
runtime.runningWorkflow = undefined;
|
|
@@ -205,23 +263,17 @@ export function registerDynamax(
|
|
|
205
263
|
});
|
|
206
264
|
|
|
207
265
|
pi.on("tool_execution_start", (event, ctx) => {
|
|
266
|
+
if (event.toolName !== "workflow") return undefined;
|
|
208
267
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
209
|
-
|
|
210
|
-
runtime.runningWorkflow = workflowLabel(event.args);
|
|
211
|
-
}
|
|
268
|
+
runtime.runningWorkflow = workflowLabel(event.args);
|
|
212
269
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
213
270
|
return undefined;
|
|
214
271
|
});
|
|
215
272
|
|
|
216
|
-
pi.on("tool_execution_update", (_event, ctx) => {
|
|
217
|
-
refreshIfVisible(ctx);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
273
|
pi.on("tool_execution_end", (event, ctx) => {
|
|
274
|
+
if (event.toolName !== "workflow") return undefined;
|
|
221
275
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
222
|
-
|
|
223
|
-
runtime.runningWorkflow = undefined;
|
|
224
|
-
}
|
|
276
|
+
runtime.runningWorkflow = undefined;
|
|
225
277
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
226
278
|
return undefined;
|
|
227
279
|
});
|
|
@@ -244,6 +296,7 @@ export function registerDynamax(
|
|
|
244
296
|
|
|
245
297
|
pi.registerCommand("workflow:dynamax", {
|
|
246
298
|
description: "Toggle Dynamax workflow orchestration: /workflow:dynamax [on|off|status]",
|
|
299
|
+
getArgumentCompletions: (argumentPrefix) => completeCurrentArgument(argumentPrefix, DYNAMAX_ACTION_COMPLETIONS),
|
|
247
300
|
handler: async (args, ctx) => {
|
|
248
301
|
const runtime = getDynamaxRuntime(runtimes, ctx);
|
|
249
302
|
const action = args.trim().toLowerCase();
|
|
@@ -252,7 +305,7 @@ export function registerDynamax(
|
|
|
252
305
|
return;
|
|
253
306
|
}
|
|
254
307
|
if (action === "on") {
|
|
255
|
-
|
|
308
|
+
enableDynamaxSticky(runtime.state);
|
|
256
309
|
updateDynamaxSurfaces(ctx, runtime, shortcuts);
|
|
257
310
|
ctx.ui.notify("Dynamax workflow orchestration is on for this session", "info");
|
|
258
311
|
return;
|
|
@@ -274,8 +327,6 @@ export function registerDynamax(
|
|
|
274
327
|
|
|
275
328
|
export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">, runtime: DynamaxRuntime, shortcuts: DynamaxShortcuts): void {
|
|
276
329
|
if (!ctx.hasUI) return;
|
|
277
|
-
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
278
|
-
ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
|
|
279
330
|
if (!isDynamaxActive(runtime.state) && !runtime.runningWorkflow) {
|
|
280
331
|
clearDynamaxSurfaces(ctx);
|
|
281
332
|
return;
|
|
@@ -287,9 +338,7 @@ export function updateDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui"
|
|
|
287
338
|
|
|
288
339
|
export function clearDynamaxSurfaces(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
289
340
|
if (!ctx.hasUI) return;
|
|
290
|
-
ctx.ui.setWidget(DYNAMAX_WIDGET_KEY, undefined);
|
|
291
341
|
ctx.ui.setStatus(DYNAMAX_STATUS_KEY, undefined);
|
|
292
|
-
ctx.ui.setStatus(DYNAMAX_WIDGET_KEY, undefined);
|
|
293
342
|
}
|
|
294
343
|
|
|
295
344
|
function workflowLabel(args: unknown): string {
|