pi-ultracode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -0
- package/examples/workflows/loop-until-dry-bugs.workflow.js +74 -0
- package/extensions/ultracode.ts +56 -0
- package/package.json +57 -0
- package/src/commands.ts +161 -0
- package/src/index.ts +55 -0
- package/src/mode.ts +196 -0
- package/src/prompts.ts +83 -0
- package/src/workflow/agent-runner.ts +329 -0
- package/src/workflow/agent-types.ts +195 -0
- package/src/workflow/display.ts +245 -0
- package/src/workflow/journal.ts +0 -0
- package/src/workflow/json-schema.ts +116 -0
- package/src/workflow/parser.ts +213 -0
- package/src/workflow/registry.ts +65 -0
- package/src/workflow/runtime.ts +651 -0
- package/src/workflow/structured-output.ts +48 -0
- package/src/workflow/tool.ts +350 -0
- package/src/workflow/worktree.ts +381 -0
- package/types/workflow.d.ts +86 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git worktree isolation for workflow subagents.
|
|
3
|
+
*
|
|
4
|
+
* When an agent() call requests `isolation: 'worktree'`, the subagent runs in a
|
|
5
|
+
* throwaway git worktree on a detached branch so parallel file-mutating agents
|
|
6
|
+
* don't clobber each other. After the agent finishes we capture a diff; a
|
|
7
|
+
* worktree with no changes is removed immediately ("auto-removed if unchanged").
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface Worktree {
|
|
17
|
+
path: string;
|
|
18
|
+
/** cwd the subagent should use (worktree root joined with the original relative cwd). */
|
|
19
|
+
agentCwd: string;
|
|
20
|
+
branch: string;
|
|
21
|
+
baseCommit: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorktreeDiff {
|
|
25
|
+
filesChanged: number;
|
|
26
|
+
insertions: number;
|
|
27
|
+
deletions: number;
|
|
28
|
+
diffStat: string;
|
|
29
|
+
patch: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function git(cwd: string, args: string[]): string {
|
|
33
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tryGit(cwd: string, args: string[]): string | undefined {
|
|
37
|
+
try {
|
|
38
|
+
return git(cwd, args);
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Like tryGit but does NOT `.trim()` — required for `git diff --binary` output,
|
|
45
|
+
* whose trailing blank lines are part of the patch format and must not be stripped. */
|
|
46
|
+
function tryGitRaw(cwd: string, args: string[]): string | undefined {
|
|
47
|
+
try {
|
|
48
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isGitRepo(cwd: string): boolean {
|
|
55
|
+
return tryGit(cwd, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create an isolated worktree for one subagent. Throws if `cwd` is not inside a
|
|
60
|
+
* git repository (the caller falls back to a shared cwd in that case).
|
|
61
|
+
*/
|
|
62
|
+
export function createWorktree(cwd: string, runId: string, index: number): Worktree {
|
|
63
|
+
const toplevel = tryGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
64
|
+
if (!toplevel) throw new Error("isolation: 'worktree' requires the working directory to be inside a git repository");
|
|
65
|
+
|
|
66
|
+
const baseCommit = tryGit(cwd, ["rev-parse", "HEAD"]) ?? "";
|
|
67
|
+
if (!baseCommit) {
|
|
68
|
+
throw new Error("isolation: 'worktree' requires at least one commit in the repository");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const safeRun = runId.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 24) || "run";
|
|
72
|
+
const branch = `ultracode/${safeRun}-${index}`;
|
|
73
|
+
const worktreePath = path.join(os.tmpdir(), `ultracode-wt-${safeRun}-${index}`);
|
|
74
|
+
|
|
75
|
+
// Best-effort GC of orphaned + stale ultracode worktrees from earlier runs.
|
|
76
|
+
// Rate-limited so a fleet of worktree agents doesn't hammer tmpdir on every create.
|
|
77
|
+
const _now = Date.now();
|
|
78
|
+
if (_now - lastReapAt > REAP_INTERVAL_MS) {
|
|
79
|
+
lastReapAt = _now;
|
|
80
|
+
reapStaleWorktrees(toplevel);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Clean up any stale worktree from a crashed prior run.
|
|
84
|
+
removeWorktreeQuiet(toplevel, worktreePath, branch);
|
|
85
|
+
|
|
86
|
+
git(toplevel, ["worktree", "add", "--detach", worktreePath, baseCommit]);
|
|
87
|
+
// Move onto a named branch so the diff has a stable ref and cleanup is unambiguous.
|
|
88
|
+
tryGit(worktreePath, ["checkout", "-B", branch]);
|
|
89
|
+
|
|
90
|
+
linkNodeModules(toplevel, worktreePath);
|
|
91
|
+
|
|
92
|
+
const relativeCwd = path.relative(toplevel, path.resolve(cwd));
|
|
93
|
+
const agentCwd = relativeCwd && !relativeCwd.startsWith("..") ? path.join(worktreePath, relativeCwd) : worktreePath;
|
|
94
|
+
|
|
95
|
+
return { path: worktreePath, agentCwd, branch, baseCommit };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Stage everything and capture the diff vs the base commit. */
|
|
99
|
+
export function captureWorktreeDiff(worktree: Worktree): WorktreeDiff {
|
|
100
|
+
tryGit(worktree.path, ["add", "-A"]);
|
|
101
|
+
// core.quotepath=false keeps non-ASCII paths unquoted in the patch text (so
|
|
102
|
+
// applyPatch's path parser targets the real file); --binary carries literal
|
|
103
|
+
// binary patch data so binary changes are applicable + recoverable.
|
|
104
|
+
const numstat = tryGit(worktree.path, ["-c", "core.quotepath=false", "diff", "--cached", "--numstat", worktree.baseCommit]) ?? "";
|
|
105
|
+
const diffStat = tryGit(worktree.path, ["-c", "core.quotepath=false", "diff", "--cached", "--stat", worktree.baseCommit]) ?? "";
|
|
106
|
+
const patch = tryGitRaw(worktree.path, ["-c", "core.quotepath=false", "diff", "--cached", "--binary", worktree.baseCommit]) ?? "";
|
|
107
|
+
|
|
108
|
+
let filesChanged = 0;
|
|
109
|
+
let insertions = 0;
|
|
110
|
+
let deletions = 0;
|
|
111
|
+
for (const line of numstat.split("\n")) {
|
|
112
|
+
if (!line.trim()) continue;
|
|
113
|
+
filesChanged++;
|
|
114
|
+
const [add, del] = line.split("\t");
|
|
115
|
+
if (add && add !== "-") insertions += Number(add) || 0;
|
|
116
|
+
if (del && del !== "-") deletions += Number(del) || 0;
|
|
117
|
+
}
|
|
118
|
+
return { filesChanged, insertions, deletions, diffStat, patch };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function hasChanges(diff: WorktreeDiff): boolean {
|
|
122
|
+
return diff.filesChanged > 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// GC cadence + staleness threshold for ultracode worktrees + patch files in tmpdir.
|
|
126
|
+
const REAP_INTERVAL_MS = 60_000; // reap at most once per minute per process
|
|
127
|
+
const STALE_WORKTREE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // entries older than this are reaped
|
|
128
|
+
let lastReapAt = 0;
|
|
129
|
+
|
|
130
|
+
/** Generate a unique tmp path for a patch file. Uses crypto-strong randomness
|
|
131
|
+
* (not a per-realm counter) so the name is collision-proof across calls,
|
|
132
|
+
* milliseconds, processes, AND worker threads (which share a pid but have
|
|
133
|
+
* separate module realms — a per-realm counter would collide on call 0). */
|
|
134
|
+
export function patchTmpPath(): string {
|
|
135
|
+
return path.join(os.tmpdir(), `ultracode-patch-${process.pid}-${randomBytes(8).toString("hex")}.patch`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Apply a captured patch back onto the original working tree. */
|
|
139
|
+
export function applyPatch(cwd: string, patch: string): boolean {
|
|
140
|
+
if (!patch.trim()) return false;
|
|
141
|
+
const paths = patchedFiles(patch);
|
|
142
|
+
// Snapshot the pre-apply working-tree content of every path the patch touches,
|
|
143
|
+
// so a failed apply (which leaves conflict markers + an unmerged index) can be
|
|
144
|
+
// reverted to exactly this state, preserving any pre-existing uncommitted edits.
|
|
145
|
+
const before = new Map<string, Buffer | null>();
|
|
146
|
+
for (const p of paths) {
|
|
147
|
+
try {
|
|
148
|
+
before.set(p, fs.readFileSync(path.join(cwd, p)));
|
|
149
|
+
} catch {
|
|
150
|
+
before.set(p, null); // path does not exist yet (the patch adds it)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Unique per call (see patchTmpPath): previously the name only depended on pid
|
|
154
|
+
// + cwd.length, so concurrent applies (worker threads sharing a pid, or a
|
|
155
|
+
// future async apply) with equal-length cwds collided on the same tmp file.
|
|
156
|
+
const tmp = patchTmpPath();
|
|
157
|
+
try {
|
|
158
|
+
fs.writeFileSync(tmp, patch.endsWith("\n") ? patch : `${patch}\n`);
|
|
159
|
+
git(cwd, ["apply", "--3way", tmp]);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
// `git apply --3way` on a real conflict writes conflict markers into the file
|
|
163
|
+
// and leaves an unmerged (UU) index entry; `git checkout --` then refuses with
|
|
164
|
+
// "path is unmerged". Reset the index entry and restore the pre-apply content.
|
|
165
|
+
revertPatchedPaths(cwd, paths, before);
|
|
166
|
+
return false;
|
|
167
|
+
} finally {
|
|
168
|
+
try {
|
|
169
|
+
fs.unlinkSync(tmp);
|
|
170
|
+
} catch {
|
|
171
|
+
// ignore
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Every path a patch touches (adds, modifies, deletes, renames). Parses the
|
|
177
|
+
* `+++ b/<path>`, `--- a/<path>`, and `diff --git a/<x> b/<y>` headers, skipping
|
|
178
|
+
* `/dev/null`. Deletions (`+++ /dev/null`) are captured via their `--- a/` side
|
|
179
|
+
* so a failed apply can restore the deleted file too. */
|
|
180
|
+
function patchedFiles(patch: string): string[] {
|
|
181
|
+
const out: string[] = [];
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
const push = (p: string) => {
|
|
184
|
+
if (p && p !== "/dev/null" && !seen.has(p)) {
|
|
185
|
+
seen.add(p);
|
|
186
|
+
out.push(p);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
for (const line of patch.split("\n")) {
|
|
190
|
+
let m = line.match(/^\+\+\+ b\/(.+)$/);
|
|
191
|
+
if (m) {
|
|
192
|
+
push(m[1]);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
m = line.match(/^--- a\/(.+)$/);
|
|
196
|
+
if (m) {
|
|
197
|
+
push(m[1]);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
201
|
+
if (m) {
|
|
202
|
+
push(m[1]);
|
|
203
|
+
push(m[2]);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Restore patched paths to their pre-apply state, clearing conflict markers + UU index. */
|
|
210
|
+
function revertPatchedPaths(
|
|
211
|
+
cwd: string,
|
|
212
|
+
paths: string[],
|
|
213
|
+
before: Map<string, Buffer | null>,
|
|
214
|
+
): void {
|
|
215
|
+
for (const p of paths) {
|
|
216
|
+
// Clear any unmerged index entry left by `git apply --3way` (idempotent).
|
|
217
|
+
tryGit(cwd, ["reset", "HEAD", "--", p]);
|
|
218
|
+
const prev = before.get(p);
|
|
219
|
+
if (prev == null) {
|
|
220
|
+
// Path did not exist before apply; remove any marker file the conflict wrote.
|
|
221
|
+
try {
|
|
222
|
+
fs.rmSync(path.join(cwd, p), { force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
try {
|
|
228
|
+
fs.writeFileSync(path.join(cwd, p), prev);
|
|
229
|
+
} catch {
|
|
230
|
+
// best-effort revert
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Persist a patch that could not be auto-applied (3-way conflict) to a durable
|
|
238
|
+
* file under `<dir>/<runId>-<id>-<label>.patch`. Called before the worktree is
|
|
239
|
+
* force-removed so the agent's changes are recoverable instead of lost. Includes
|
|
240
|
+
* the agent sequence id so two same-label agents in one run don't overwrite each
|
|
241
|
+
* other's rescue patch.
|
|
242
|
+
*/
|
|
243
|
+
export function writeRescuePatch(
|
|
244
|
+
dir: string,
|
|
245
|
+
runId: string,
|
|
246
|
+
id: number,
|
|
247
|
+
label: string,
|
|
248
|
+
patch: string,
|
|
249
|
+
): string {
|
|
250
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
251
|
+
const safeRun = runId.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 24) || "run";
|
|
252
|
+
const safeLabel = label.replace(/[^A-Za-z0-9_-]/g, "").slice(0, 40) || "agent";
|
|
253
|
+
const file = path.join(dir, `${safeRun}-${id}-${safeLabel}.patch`);
|
|
254
|
+
fs.writeFileSync(file, patch.endsWith("\n") ? patch : `${patch}\n`);
|
|
255
|
+
return file;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function removeWorktree(worktree: Worktree): void {
|
|
259
|
+
const toplevel = tryGit(worktree.path, ["rev-parse", "--show-toplevel"]);
|
|
260
|
+
const repo = toplevel ?? path.dirname(worktree.path);
|
|
261
|
+
removeWorktreeQuiet(repo, worktree.path, worktree.branch);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function removeWorktreeQuiet(repo: string, worktreePath: string, branch: string): void {
|
|
265
|
+
tryGit(repo, ["worktree", "remove", "--force", worktreePath]);
|
|
266
|
+
// worktree remove may fail if the dir was deleted manually; prune + rmrf to be safe.
|
|
267
|
+
tryGit(repo, ["worktree", "prune"]);
|
|
268
|
+
try {
|
|
269
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
270
|
+
} catch {
|
|
271
|
+
// ignore
|
|
272
|
+
}
|
|
273
|
+
tryGit(repo, ["branch", "-D", branch]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Reap stale ultracode worktrees + leaked patch files from tmpdir. Entries
|
|
278
|
+
* older than the staleness threshold (24h default) are removed: tracked
|
|
279
|
+
* worktrees (registered with `toplevel`) via full `git worktree remove` + branch
|
|
280
|
+
* delete; untracked `ultracode-wt-*` dirs via rmSync; `ultracode-patch-*` files
|
|
281
|
+
* via unlink.
|
|
282
|
+
*
|
|
283
|
+
* Limitation: `git worktree list` is repo-scoped, so a worktree owned by a
|
|
284
|
+
* DIFFERENT repo is "untracked" here and is rmSync'd after the threshold (its
|
|
285
|
+
* branch in the owning repo is NOT cleaned — there is no safe way to find the
|
|
286
|
+
* owner from the dir alone). The same 24h threshold applies to untracked dirs as
|
|
287
|
+
* to tracked worktrees, so cross-repo in-flight and kept worktrees survive as
|
|
288
|
+
* long as same-repo ones. If git can't list worktrees, the reap bails safe
|
|
289
|
+
* rather than treating every entry as orphaned.
|
|
290
|
+
*
|
|
291
|
+
* Exported for tests; createWorktree calls this rate-limited.
|
|
292
|
+
*/
|
|
293
|
+
export function reapStaleWorktrees(
|
|
294
|
+
toplevel: string,
|
|
295
|
+
opts: { maxAgeMs?: number } = {},
|
|
296
|
+
): void {
|
|
297
|
+
const maxAgeMs = opts.maxAgeMs ?? STALE_WORKTREE_MAX_AGE_MS;
|
|
298
|
+
// Drop git's tracking of worktrees whose dirs are already gone.
|
|
299
|
+
tryGit(toplevel, ["worktree", "prune"]);
|
|
300
|
+
// Bail safe if we can't list worktrees: treating every entry as orphaned would
|
|
301
|
+
// destroy in-flight/kept worktrees from other repos (git worktree list is
|
|
302
|
+
// repo-scoped, so only the current repo's worktrees are `tracked` below).
|
|
303
|
+
const list = tryGit(toplevel, ["worktree", "list", "--porcelain"]);
|
|
304
|
+
if (list == null) return;
|
|
305
|
+
const canonicalPath = (value: string): string => {
|
|
306
|
+
try {
|
|
307
|
+
return fs.realpathSync(value);
|
|
308
|
+
} catch {
|
|
309
|
+
return path.resolve(value);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const tracked = new Set(
|
|
313
|
+
list
|
|
314
|
+
.split("\n")
|
|
315
|
+
.filter((l) => l.startsWith("worktree "))
|
|
316
|
+
.map((l) => canonicalPath(l.slice("worktree ".length).trim())),
|
|
317
|
+
);
|
|
318
|
+
let entries: string[];
|
|
319
|
+
try {
|
|
320
|
+
entries = fs.readdirSync(os.tmpdir());
|
|
321
|
+
} catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const name of entries) {
|
|
326
|
+
if (!name.startsWith("ultracode-wt-")) continue;
|
|
327
|
+
const wtPath = path.join(os.tmpdir(), name);
|
|
328
|
+
let mtime: number;
|
|
329
|
+
try {
|
|
330
|
+
mtime = fs.statSync(wtPath).mtimeMs;
|
|
331
|
+
} catch {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (now - mtime <= maxAgeMs) continue; // recent: in-flight or within the recovery window
|
|
335
|
+
if (tracked.has(canonicalPath(wtPath))) {
|
|
336
|
+
// Tracked + stale (kept from an old run in THIS repo): full remove + branch delete.
|
|
337
|
+
const branch = `ultracode/${name.slice("ultracode-wt-".length)}`;
|
|
338
|
+
removeWorktreeQuiet(toplevel, wtPath, branch);
|
|
339
|
+
} else {
|
|
340
|
+
// Untracked by this repo: could be a crash orphan, OR a worktree owned by a
|
|
341
|
+
// DIFFERENT repo (git worktree list is repo-scoped, so we can't tell). The
|
|
342
|
+
// 24h threshold matches the same-repo kept-worktree window, so cross-repo
|
|
343
|
+
// in-flight/kept worktrees survive as long as same-repo ones. The owning
|
|
344
|
+
// repo's branch is NOT cleaned here (no safe way to find it).
|
|
345
|
+
try {
|
|
346
|
+
fs.rmSync(wtPath, { recursive: true, force: true });
|
|
347
|
+
} catch {
|
|
348
|
+
// ignore
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Also reap crash-leaked patch tmp files (ultracode-patch-*), same threshold.
|
|
353
|
+
for (const name of entries) {
|
|
354
|
+
if (!name.startsWith("ultracode-patch-")) continue;
|
|
355
|
+
const fp = path.join(os.tmpdir(), name);
|
|
356
|
+
let mtime: number;
|
|
357
|
+
try {
|
|
358
|
+
mtime = fs.statSync(fp).mtimeMs;
|
|
359
|
+
} catch {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (now - mtime > maxAgeMs) {
|
|
363
|
+
try {
|
|
364
|
+
fs.unlinkSync(fp);
|
|
365
|
+
} catch {
|
|
366
|
+
// ignore
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function linkNodeModules(toplevel: string, worktreePath: string): void {
|
|
373
|
+
const src = path.join(toplevel, "node_modules");
|
|
374
|
+
const dest = path.join(worktreePath, "node_modules");
|
|
375
|
+
if (!fs.existsSync(src) || fs.existsSync(dest)) return;
|
|
376
|
+
try {
|
|
377
|
+
fs.symlinkSync(src, dest, "dir");
|
|
378
|
+
} catch {
|
|
379
|
+
// unsupported filesystem; subagents just won't have node_modules linked
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ambient globals available inside pi-ultracode workflow scripts.
|
|
3
|
+
*
|
|
4
|
+
* Add this to a saved workflow file for editor IntelliSense:
|
|
5
|
+
*
|
|
6
|
+
* /// <reference types="pi-ultracode/workflow" />
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {};
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface WorkflowMeta {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
whenToUse?: string;
|
|
16
|
+
phases?: Array<{ title: string; detail?: string; model?: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface JsonSchema {
|
|
20
|
+
type?: string | string[];
|
|
21
|
+
properties?: Record<string, JsonSchema>;
|
|
22
|
+
items?: JsonSchema | JsonSchema[];
|
|
23
|
+
required?: string[];
|
|
24
|
+
additionalProperties?: boolean | JsonSchema;
|
|
25
|
+
enum?: unknown[];
|
|
26
|
+
const?: unknown;
|
|
27
|
+
anyOf?: JsonSchema[];
|
|
28
|
+
oneOf?: JsonSchema[];
|
|
29
|
+
description?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WorkflowAgentOptions {
|
|
34
|
+
/** Short label shown in live progress (2-5 words). */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** Assign this agent to a progress group explicitly. */
|
|
37
|
+
phase?: string;
|
|
38
|
+
/** JSON Schema for structured output; agent() then returns the validated object. */
|
|
39
|
+
schema?: JsonSchema;
|
|
40
|
+
/** Override the subagent model by pattern, e.g. "sonnet" or "anthropic/...:high". */
|
|
41
|
+
model?: string;
|
|
42
|
+
/** Run the agent in an isolated git worktree (for parallel file mutation). */
|
|
43
|
+
isolation?: "worktree";
|
|
44
|
+
/** Use a custom subagent role/system-prompt (built-in or discovered). */
|
|
45
|
+
agentType?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface WorkflowBudget {
|
|
49
|
+
total: number | null;
|
|
50
|
+
spent(): number;
|
|
51
|
+
remaining(): number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Spawn a subagent. Returns final text, or a validated object when opts.schema is set. */
|
|
55
|
+
function agent<T = unknown>(prompt: string, options?: WorkflowAgentOptions): Promise<T>;
|
|
56
|
+
|
|
57
|
+
/** Run independent tasks concurrently (a barrier). Pass functions, not promises. */
|
|
58
|
+
function parallel<T = unknown>(thunks: Array<() => Promise<T>>): Promise<T[]>;
|
|
59
|
+
|
|
60
|
+
/** Run each item through sequential stages while items fan out (no barrier). */
|
|
61
|
+
function pipeline<TItem = unknown, TResult = unknown>(
|
|
62
|
+
items: TItem[],
|
|
63
|
+
...stages: Array<(previous: unknown, original: TItem, index: number) => TResult | Promise<TResult>>
|
|
64
|
+
): Promise<TResult[]>;
|
|
65
|
+
|
|
66
|
+
/** Run a saved workflow (by name) or { scriptPath } inline; one level of nesting. */
|
|
67
|
+
function workflow<T = unknown>(nameOrRef: string | { scriptPath: string }, args?: unknown): Promise<T>;
|
|
68
|
+
|
|
69
|
+
/** Mark the current phase for progress grouping. */
|
|
70
|
+
function phase(title: string): void;
|
|
71
|
+
|
|
72
|
+
/** Append a workflow-level log line. */
|
|
73
|
+
function log(message: unknown): void;
|
|
74
|
+
|
|
75
|
+
/** JSON value passed via the tool's `args` parameter. */
|
|
76
|
+
const args: unknown;
|
|
77
|
+
|
|
78
|
+
/** Working directory for the workflow and its subagents. */
|
|
79
|
+
const cwd: string;
|
|
80
|
+
|
|
81
|
+
/** Deterministic process shim exposing only cwd(). */
|
|
82
|
+
const process: { cwd(): string };
|
|
83
|
+
|
|
84
|
+
/** Real output-token budget tracker for the run. */
|
|
85
|
+
const budget: WorkflowBudget;
|
|
86
|
+
}
|