pi-workflow-engine 0.10.0 → 0.10.2
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.
|
@@ -358,6 +358,8 @@ Use the workflow tool with a script argument, not a saved workflow name.
|
|
|
358
358
|
The script must start with export const meta = { ... } and default-export an async workflow function.
|
|
359
359
|
Use the injected Type object for schemas. Do not import anything or use dynamic import().
|
|
360
360
|
Set thinkingLevel explicitly on each agent() call.
|
|
361
|
+
Always pass a plain string as the first api.agent() argument; build prompts with template strings before calling agent().
|
|
362
|
+
If using \`isolation: "worktree"\`, remember api.agent() returns \`{ result, patch, changed }\`; read \`.result\` for the agent answer and \`.patch\` for the diff.
|
|
361
363
|
When the run is budgeted, guard expensive loops with \`while (api.budget.total && api.budget.remaining() > N) { ... }\`; api.agent() throws once the budget is spent.
|
|
362
364
|
Subagents receive no skills by default. When the brief asks for a skill or a stage clearly benefits from one, pass \`skills: ["skill-name"]\` on that agent call only.
|
|
363
365
|
Do not edit files unless the user explicitly requested edits.`;
|
|
@@ -566,6 +568,8 @@ export default function workflowEngine(pi: ExtensionAPI): void {
|
|
|
566
568
|
"Inline workflow scripts must use the injected `Type` object for schemas and must not contain imports or dynamic import().",
|
|
567
569
|
"Inline scripts may compose registered workflows in-process via `api.workflow(\"<name>\", args)` (e.g. `await api.workflow(\"code-review\", \"HEAD~3\")`); it returns the sub-workflow's result and nests one level only.",
|
|
568
570
|
"Subagents receive no skills by default. In inline workflows, pass `skills: [\"skill-name\"]` per `agent()` call when the user asks for a skill or a stage should use one; grant only the needed skills.",
|
|
571
|
+
"Always pass a plain string as the first `api.agent()` argument; build prompts with template strings before calling agent().",
|
|
572
|
+
"When using `isolation: \"worktree\"`, `api.agent()` returns `{ result, patch, changed }`; use `.result` for the answer and `.patch` for the isolated diff.",
|
|
569
573
|
"If an inline subagent needs grep/find/code-search helpers, use `tools: [\"read\", \"bash\", \"grep\", \"find\", \"ls\"]` plus `toolHints: [\"search\"]` so installed tools such as ast-grep, mgrep, ffgrep, or fffind are discovered dynamically.",
|
|
570
574
|
"`api.budget` exposes `{ total, spent(), remaining() }` (output tokens). When the run is budgeted, scale fleets from `budget.total` and guard loops with `while (budget.total && budget.remaining() > N) { await api.agent(...) }`; `api.agent()` throws once the ceiling is reached.",
|
|
571
575
|
"Every workflow tool call must provide exactly one of `name` or `script`, never both.",
|
|
@@ -354,6 +354,10 @@ async function recordJournalResult(rc: RunContext, label: string, key: string, v
|
|
|
354
354
|
* no event-stream parsing is needed.
|
|
355
355
|
*/
|
|
356
356
|
export async function runAgent(rc: RunContext, prompt: string, opts: AgentOptions = {}): Promise<unknown> {
|
|
357
|
+
if (typeof prompt !== "string") {
|
|
358
|
+
throw new Error(`agent() prompt must be a string; received ${describeAgentPrompt(prompt)}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
357
361
|
const label = opts.label ?? "agent";
|
|
358
362
|
const phase = opts.phase ?? "Workflow";
|
|
359
363
|
const tags = { label, phase };
|
|
@@ -553,6 +557,12 @@ async function isolatedResult(rc: RunContext, worktreePath: string, result: unkn
|
|
|
553
557
|
return { result, patch: patch.patch, changed: patch.changed };
|
|
554
558
|
}
|
|
555
559
|
|
|
560
|
+
function describeAgentPrompt(value: unknown): string {
|
|
561
|
+
if (value === null) return "null";
|
|
562
|
+
if (Array.isArray(value)) return "array";
|
|
563
|
+
return typeof value;
|
|
564
|
+
}
|
|
565
|
+
|
|
556
566
|
function errorMessage(error: unknown): string {
|
|
557
567
|
return error instanceof Error ? error.message : String(error);
|
|
558
568
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { join, resolve, sep } from "node:path";
|
|
4
5
|
import { runBoundedProcess } from "./process-runner.ts";
|
|
5
6
|
|
|
6
7
|
export interface WorktreeGitCommandOptions {
|
|
@@ -24,11 +25,13 @@ export interface WorktreeGitRunner {
|
|
|
24
25
|
|
|
25
26
|
export interface WorktreeRef {
|
|
26
27
|
readonly path: string;
|
|
28
|
+
readonly snapshot?: boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface WorktreeAddFailure {
|
|
30
32
|
readonly path: string;
|
|
31
33
|
readonly error: string;
|
|
34
|
+
readonly snapshot?: boolean;
|
|
32
35
|
readonly cleanup?: WorktreeGitCommandResult;
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -67,6 +70,7 @@ let pathCounter = 0;
|
|
|
67
70
|
|
|
68
71
|
export class WorktreeRegistry {
|
|
69
72
|
private readonly paths = new Set<string>();
|
|
73
|
+
private readonly snapshots = new Set<string>();
|
|
70
74
|
private readonly runner: WorktreeGitRunner;
|
|
71
75
|
private readonly patchCapture: WorktreePatchCapture;
|
|
72
76
|
private readonly timeoutMs: number;
|
|
@@ -92,6 +96,7 @@ export class WorktreeRegistry {
|
|
|
92
96
|
async add(signal?: AbortSignal): Promise<WorktreeRef | WorktreeAddFailure> {
|
|
93
97
|
const added = await addWorktree({ repoCwd: this.repoCwd, runner: this.runner, signal, timeoutMs: this.timeoutMs });
|
|
94
98
|
this.register(added.path);
|
|
99
|
+
if (added.snapshot === true) this.snapshots.add(added.path);
|
|
95
100
|
if ("error" in added) {
|
|
96
101
|
const cleanup = await this.remove(added.path);
|
|
97
102
|
return { ...added, cleanup };
|
|
@@ -104,13 +109,16 @@ export class WorktreeRegistry {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
async remove(path: string): Promise<WorktreeGitCommandResult> {
|
|
107
|
-
const result = await removeWorktree({ repoCwd: this.repoCwd, path, runner: this.runner, timeoutMs: this.timeoutMs }).catch((error: unknown) => ({
|
|
112
|
+
const result = await removeWorktree({ repoCwd: this.repoCwd, path, runner: this.runner, timeoutMs: this.timeoutMs, snapshot: this.snapshots.has(path) }).catch((error: unknown) => ({
|
|
108
113
|
ok: false,
|
|
109
114
|
stdout: "",
|
|
110
115
|
stderr: "",
|
|
111
116
|
error: formatError(error),
|
|
112
117
|
}));
|
|
113
|
-
if (result.ok)
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
this.paths.delete(path);
|
|
120
|
+
this.snapshots.delete(path);
|
|
121
|
+
}
|
|
114
122
|
return result;
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -163,8 +171,16 @@ export async function addWorktree(options: {
|
|
|
163
171
|
timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
164
172
|
})
|
|
165
173
|
.catch((error: unknown) => ({ ok: false, stdout: "", stderr: "", error: formatError(error) }));
|
|
166
|
-
if (
|
|
167
|
-
|
|
174
|
+
if (result.ok) return { path };
|
|
175
|
+
|
|
176
|
+
const message = result.error ?? (result.stderr.trim() || "git worktree add failed");
|
|
177
|
+
if (!isInvalidHeadError(message)) return { path, error: message };
|
|
178
|
+
|
|
179
|
+
const snapshotError = await createUnbornRepoSnapshot({ repoCwd: options.repoCwd, path, timeoutMs: options.timeoutMs, signal: options.signal })
|
|
180
|
+
.then(() => undefined)
|
|
181
|
+
.catch((error: unknown) => formatError(error));
|
|
182
|
+
if (snapshotError !== undefined) return { path, snapshot: true, error: `git worktree add failed (${message}); unborn-repo snapshot fallback failed: ${snapshotError}` };
|
|
183
|
+
return { path, snapshot: true };
|
|
168
184
|
}
|
|
169
185
|
|
|
170
186
|
export async function removeWorktree(options: {
|
|
@@ -172,7 +188,12 @@ export async function removeWorktree(options: {
|
|
|
172
188
|
readonly path: string;
|
|
173
189
|
readonly runner?: WorktreeGitRunner;
|
|
174
190
|
readonly timeoutMs?: number;
|
|
191
|
+
readonly snapshot?: boolean;
|
|
175
192
|
}): Promise<WorktreeGitCommandResult> {
|
|
193
|
+
if (options.snapshot === true) {
|
|
194
|
+
await rm(options.path, { recursive: true, force: true });
|
|
195
|
+
return { ok: true, stdout: "", stderr: "" };
|
|
196
|
+
}
|
|
176
197
|
return await (options.runner ?? spawnGitRunner).runGit({
|
|
177
198
|
cwd: options.repoCwd,
|
|
178
199
|
args: ["worktree", "remove", "--force", options.path],
|
|
@@ -211,6 +232,38 @@ export async function captureWorktreePatch(options: {
|
|
|
211
232
|
return { patch: diff.stdout, changed: diff.stdout.trim().length > 0 };
|
|
212
233
|
}
|
|
213
234
|
|
|
235
|
+
async function createUnbornRepoSnapshot(options: {
|
|
236
|
+
readonly repoCwd: string;
|
|
237
|
+
readonly path: string;
|
|
238
|
+
readonly signal?: AbortSignal;
|
|
239
|
+
readonly timeoutMs?: number;
|
|
240
|
+
}): Promise<void> {
|
|
241
|
+
const targetRoot = resolve(options.path);
|
|
242
|
+
await rm(options.path, { recursive: true, force: true });
|
|
243
|
+
await mkdir(options.path, { recursive: true });
|
|
244
|
+
await cp(options.repoCwd, options.path, {
|
|
245
|
+
recursive: true,
|
|
246
|
+
force: true,
|
|
247
|
+
filter: (source) => {
|
|
248
|
+
const resolvedSource = resolve(source);
|
|
249
|
+
if (resolvedSource === targetRoot || resolvedSource.startsWith(`${targetRoot}${sep}`)) return false;
|
|
250
|
+
return !resolvedSource.split(/[\\/]/).includes(".git");
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
await runGitCommand({ cwd: options.path, args: ["init"], signal: options.signal, timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS });
|
|
254
|
+
await runGitCommand({ cwd: options.path, args: ["add", "-A"], signal: options.signal, timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS });
|
|
255
|
+
await runGitCommand({
|
|
256
|
+
cwd: options.path,
|
|
257
|
+
args: ["-c", "user.name=pi-workflow", "-c", "user.email=pi-workflow@example.invalid", "commit", "--allow-empty", "-m", "pi workflow baseline"],
|
|
258
|
+
signal: options.signal,
|
|
259
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isInvalidHeadError(message: string): boolean {
|
|
264
|
+
return /invalid reference:\s*HEAD/i.test(message) || /ambiguous argument ['"]?HEAD/i.test(message) || /unknown revision or path.*HEAD/i.test(message);
|
|
265
|
+
}
|
|
266
|
+
|
|
214
267
|
export const spawnGitRunner: WorktreeGitRunner = {
|
|
215
268
|
async runGit(options) {
|
|
216
269
|
return await runGitCommand(options);
|
|
@@ -218,11 +271,14 @@ export const spawnGitRunner: WorktreeGitRunner = {
|
|
|
218
271
|
};
|
|
219
272
|
|
|
220
273
|
async function runGitCommand(options: WorktreeGitCommandOptions): Promise<WorktreeGitCommandResult> {
|
|
274
|
+
const env = { ...process.env };
|
|
275
|
+
delete env.GIT_EXTERNAL_DIFF;
|
|
276
|
+
delete env.GIT_DIFF_OPTS;
|
|
221
277
|
return await runBoundedProcess({
|
|
222
278
|
file: "git",
|
|
223
279
|
args: options.args,
|
|
224
280
|
cwd: options.cwd,
|
|
225
|
-
env
|
|
281
|
+
env,
|
|
226
282
|
signal: options.signal,
|
|
227
283
|
timeoutMs: options.timeoutMs,
|
|
228
284
|
maxBufferBytes: options.maxBufferBytes,
|
package/package.json
CHANGED