pi-crew 0.9.5 → 0.9.8
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/CHANGELOG.md +556 -0
- package/README.md +10 -3
- package/docs/HARNESS_BACKLOG.md +51 -3
- package/docs/dynamic-workflows.md +315 -2
- package/docs/fix-plan-disabletools-exit-null.md +219 -0
- package/docs/troubleshooting.md +76 -0
- package/package.json +10 -3
- package/src/config/defaults.ts +8 -4
- package/src/extension/team-tool/doctor.ts +14 -0
- package/src/extension/team-tool/run.ts +2 -0
- package/src/runtime/background-runner.ts +1 -1
- package/src/runtime/capability-inventory.ts +20 -1
- package/src/runtime/child-pi.ts +109 -11
- package/src/runtime/deterministic-ast.ts +161 -0
- package/src/runtime/dwf-state-store.ts +97 -0
- package/src/runtime/dynamic-workflow-context.ts +381 -7
- package/src/runtime/dynamic-workflow-runner.ts +93 -2
- package/src/runtime/pi-args.ts +11 -0
- package/src/runtime/result-extractor.ts +72 -7
- package/src/runtime/task-output-context.ts +25 -9
- package/src/runtime/team-runner.ts +8 -3
- package/src/runtime/zombie-scanner.ts +297 -0
- package/src/schema/team-tool-schema.ts +28 -0
- package/src/skills/discover-skills.ts +61 -8
- package/src/skills/validate.ts +267 -0
- package/src/state/contracts.ts +1 -0
- package/src/state/state-store.ts +3 -0
- package/src/state/types.ts +9 -0
- package/src/ui/dashboard-panes/progress-pane.ts +5 -0
- package/src/ui/dwf-phase-display.ts +151 -0
- package/src/ui/keybinding-map.ts +128 -41
- package/src/ui/run-event-bus.ts +83 -0
- package/src/ui/run-snapshot-cache.ts +4 -0
- package/src/ui/snapshot-types.ts +3 -0
- package/src/workflows/workflow-config.ts +3 -0
- package/src/worktree/worktree-manager.ts +94 -0
- package/types/dwf.d.ts +187 -0
package/src/ui/run-event-bus.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TeamEvent } from "../state/event-log.ts";
|
|
2
|
+
import { readEventsCursor } from "../state/event-log.ts";
|
|
2
3
|
|
|
3
4
|
export type RunEventType =
|
|
4
5
|
| "task_started"
|
|
@@ -59,6 +60,18 @@ export interface RunEventPayload {
|
|
|
59
60
|
timestamp?: string;
|
|
60
61
|
data?: unknown;
|
|
61
62
|
channel?: EventChannel;
|
|
63
|
+
/**
|
|
64
|
+
* L1: monotonic sequence from the durable event log
|
|
65
|
+
* (`TeamEvent.metadata.seq`). Present on events that originated from a
|
|
66
|
+
* logged TeamEvent (via emitFromTeamEvent). Absent on transient live-only
|
|
67
|
+
* events (e.g. worker_status from the stream bridge) that are never
|
|
68
|
+
* persisted and therefore cannot be replayed or deduped.
|
|
69
|
+
*
|
|
70
|
+
* Used by onWithReplay() to dedup: a live event with seq <= the last seq
|
|
71
|
+
* replayed to a subscriber is suppressed (it was already delivered from
|
|
72
|
+
* the durable log).
|
|
73
|
+
*/
|
|
74
|
+
seq?: number;
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export type RunEventCallback = (event: RunEventPayload) => void;
|
|
@@ -115,6 +128,73 @@ class RunEventBus {
|
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/**
|
|
132
|
+
* L1: subscribe with a catch-up replay from the durable event log.
|
|
133
|
+
*
|
|
134
|
+
* Closes the transient-subscriber-absence gap: when an overlay/widget is
|
|
135
|
+
* disposed and recreated (toggle, reconnect), live events emitted in that
|
|
136
|
+
* window are lost as notification triggers. This method replays the
|
|
137
|
+
* missed TeamEvents from the durable JSONL log BEFORE attaching the live
|
|
138
|
+
* listener, then dedups so events delivered both ways fire exactly once.
|
|
139
|
+
*
|
|
140
|
+
* Unlike deer-flow's 256-event RAM ring buffer (lost on crash), this uses
|
|
141
|
+
* pi-crew's existing durable `readEventsCursor` — O(new bytes) via
|
|
142
|
+
* byte-offset incremental reads, monotonic seq, tail-capped. Strictly
|
|
143
|
+
* better: survives crashes, bounded memory.
|
|
144
|
+
*
|
|
145
|
+
* @param runId Run to subscribe to (live listener scope).
|
|
146
|
+
* @param eventsPath Path to the run's events JSONL (manifest.eventsPath).
|
|
147
|
+
* @param lastSeenSeq Last seq the caller processed; events with seq > this
|
|
148
|
+
* are replayed. Pass 0 to replay everything.
|
|
149
|
+
* @param callback Receives both replayed and live events. Replayed
|
|
150
|
+
* events are delivered directly (NOT via emit, so no
|
|
151
|
+
* fan-out to other subscribers).
|
|
152
|
+
* @returns unsubscribe handle (detaches the live listener).
|
|
153
|
+
*/
|
|
154
|
+
onWithReplay(
|
|
155
|
+
runId: string,
|
|
156
|
+
eventsPath: string,
|
|
157
|
+
lastSeenSeq: number,
|
|
158
|
+
callback: RunEventCallback,
|
|
159
|
+
): () => void {
|
|
160
|
+
// Phase 1: replay missed events from the durable log directly to this
|
|
161
|
+
// callback. Bounded by limit; readEventsCursor already tail-caps.
|
|
162
|
+
let maxReplayedSeq = lastSeenSeq;
|
|
163
|
+
try {
|
|
164
|
+
const cursor = readEventsCursor(eventsPath, { sinceSeq: lastSeenSeq, limit: 1000 });
|
|
165
|
+
for (const teamEvent of cursor.events) {
|
|
166
|
+
const type = teamEventToRunEventType(teamEvent);
|
|
167
|
+
if (!type) continue; // not all TeamEvents map to a RunEventType
|
|
168
|
+
const payload: RunEventPayload = {
|
|
169
|
+
type,
|
|
170
|
+
runId: teamEvent.runId,
|
|
171
|
+
taskId: teamEvent.taskId,
|
|
172
|
+
timestamp: teamEvent.time,
|
|
173
|
+
data: teamEvent.data,
|
|
174
|
+
channel: classifyEventChannel(type),
|
|
175
|
+
seq: teamEvent.metadata?.seq,
|
|
176
|
+
};
|
|
177
|
+
try { callback(payload); } catch { /* subscriber errors are non-fatal */ }
|
|
178
|
+
if (typeof teamEvent.metadata?.seq === "number") {
|
|
179
|
+
maxReplayedSeq = Math.max(maxReplayedSeq, teamEvent.metadata.seq);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Log read failures are non-fatal — fall through to live-only
|
|
184
|
+
// subscription. The durable log may not exist yet for a brand-new run.
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Phase 2: attach the live listener with dedup. A live event whose seq
|
|
188
|
+
// was already replayed (seq <= maxReplayedSeq) is suppressed. Events
|
|
189
|
+
// without a seq (transient live-only, e.g. worker_status) always
|
|
190
|
+
// deliver — they are never persisted and thus never replayed.
|
|
191
|
+
const liveCallback: RunEventCallback = (event) => {
|
|
192
|
+
if (typeof event.seq === "number" && event.seq <= maxReplayedSeq) return;
|
|
193
|
+
callback(event);
|
|
194
|
+
};
|
|
195
|
+
return this.on(runId, liveCallback);
|
|
196
|
+
}
|
|
197
|
+
|
|
118
198
|
emit(event: RunEventPayload): void {
|
|
119
199
|
// Auto-classify channel if not already set.
|
|
120
200
|
// M2: Use local variable for routing, but also set on event
|
|
@@ -206,5 +286,8 @@ export function emitFromTeamEvent(event: TeamEvent): void {
|
|
|
206
286
|
taskId: event.taskId,
|
|
207
287
|
timestamp: event.time,
|
|
208
288
|
data: event.data,
|
|
289
|
+
// L1: stamp the durable-log seq so onWithReplay() can dedup live
|
|
290
|
+
// delivery against replayed events.
|
|
291
|
+
seq: event.metadata?.seq,
|
|
209
292
|
});
|
|
210
293
|
}
|
|
@@ -10,6 +10,7 @@ import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-st
|
|
|
10
10
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
11
|
import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
|
|
12
12
|
import { runEventBus } from "./run-event-bus.ts";
|
|
13
|
+
import { extractDwfPhaseState } from "./dwf-phase-display.ts";
|
|
13
14
|
import { sequencePath } from "../state/event-log.ts";
|
|
14
15
|
|
|
15
16
|
export interface RunSnapshotCache extends RunSnapshotCacheBase {
|
|
@@ -566,6 +567,7 @@ function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt" | "sl
|
|
|
566
567
|
groupJoins: input.groupJoins,
|
|
567
568
|
events: input.recentEvents.map((event) => [event.metadata?.seq, event.time, event.type, event.taskId, event.message, event.data?.reason]),
|
|
568
569
|
cancellationReason: input.cancellationReason,
|
|
570
|
+
dwfPhaseState: input.dwfPhaseState,
|
|
569
571
|
output: input.recentOutputLines,
|
|
570
572
|
stamps,
|
|
571
573
|
}));
|
|
@@ -684,6 +686,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
684
686
|
mailbox,
|
|
685
687
|
groupJoins,
|
|
686
688
|
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
689
|
+
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
687
690
|
recentEvents,
|
|
688
691
|
recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
|
|
689
692
|
};
|
|
@@ -730,6 +733,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
|
|
|
730
733
|
mailbox,
|
|
731
734
|
groupJoins,
|
|
732
735
|
cancellationReason: cancellationReasonFromEvents(recentEvents),
|
|
736
|
+
dwfPhaseState: extractDwfPhaseState(recentEvents),
|
|
733
737
|
recentEvents,
|
|
734
738
|
recentOutputLines: recentOutput,
|
|
735
739
|
};
|
package/src/ui/snapshot-types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
|
|
2
2
|
import type { TeamEvent } from "../state/event-log.ts";
|
|
3
3
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
4
|
+
import type { DwfPhaseState } from "./dwf-phase-display.ts";
|
|
4
5
|
|
|
5
6
|
export interface RunUiProgress {
|
|
6
7
|
total: number;
|
|
@@ -73,6 +74,8 @@ export interface RunUiSnapshot {
|
|
|
73
74
|
groupJoins?: RunUiGroupJoin[];
|
|
74
75
|
/** Structured cancellation reason from run.cancelled event data, when available. */
|
|
75
76
|
cancellationReason?: string;
|
|
77
|
+
/** DWF phase state derived from `recentEvents`. Null/absent for non-DWF runs. */
|
|
78
|
+
dwfPhaseState?: DwfPhaseState | null;
|
|
76
79
|
recentEvents: TeamEvent[];
|
|
77
80
|
recentOutputLines: string[];
|
|
78
81
|
}
|
|
@@ -45,6 +45,9 @@ export interface WorkflowConfig {
|
|
|
45
45
|
runtime?: "static" | "dynamic";
|
|
46
46
|
/** For runtime:"dynamic" — relative/absolute path to the .dwf.ts script. Unused for static. */
|
|
47
47
|
dynamicScript?: string;
|
|
48
|
+
/** For runtime:"dynamic" — per-workflow token budget. When set, ctx.agent() auto-rejects with
|
|
49
|
+
* ok:false once exhausted. Accumulated from each agent run's reported usage. */
|
|
50
|
+
maxTokenBudget?: number;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** A dynamic workflow (runtime === "dynamic"). steps is empty — the script is the source of truth. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import * as path from "node:path";
|
|
4
5
|
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
@@ -7,6 +8,7 @@ import { projectCrewRoot } from "../utils/paths.ts";
|
|
|
7
8
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
8
9
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
9
10
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
11
|
+
import { writeArtifact } from "../state/artifact-store.ts";
|
|
10
12
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
11
13
|
|
|
12
14
|
export interface PreparedTaskWorkspace {
|
|
@@ -460,3 +462,95 @@ export function captureWorktreeDiff(worktreePath: string): string {
|
|
|
460
462
|
return `Failed to capture worktree diff: ${message}`;
|
|
461
463
|
}
|
|
462
464
|
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* round-17 P2-4: Create an isolated git worktree for a single DWF agent call.
|
|
468
|
+
*
|
|
469
|
+
* Lightweight — does NOT require a TeamTaskState and does NOT depend on
|
|
470
|
+
* `manifest.workspaceMode === "worktree"` (DWF manifests use `single`). It
|
|
471
|
+
* reuses the same internal helpers as `prepareTaskWorkspace` (git, findGitRoot,
|
|
472
|
+
* assertCleanLeader, pruneStaleWorktrees, sanitizeBranchPart,
|
|
473
|
+
* linkNodeModulesIfPresent) but with a minimal, task-free signature.
|
|
474
|
+
*
|
|
475
|
+
* Returns `undefined` when worktree creation is unavailable (no git repo, dirty
|
|
476
|
+
* leader, git error) so the caller (`ctx.agent`) can fall back gracefully.
|
|
477
|
+
*/
|
|
478
|
+
export function prepareAgentWorktree(
|
|
479
|
+
manifest: TeamRunManifest,
|
|
480
|
+
agentId: string,
|
|
481
|
+
): PreparedTaskWorkspace | undefined {
|
|
482
|
+
try {
|
|
483
|
+
const repoRoot = findGitRoot(manifest.cwd);
|
|
484
|
+
const loadedConfig = loadConfig(manifest.cwd);
|
|
485
|
+
if (loadedConfig.config.requireCleanWorktreeLeader !== false) assertCleanLeader(repoRoot);
|
|
486
|
+
const sanitizedRunId = manifest.runId.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "") || "run";
|
|
487
|
+
const worktreeRoot = path.join(projectCrewRoot(manifest.cwd), DEFAULT_PATHS.state.worktreesSubdir, sanitizedRunId);
|
|
488
|
+
fs.mkdirSync(worktreeRoot, { recursive: true });
|
|
489
|
+
const sanitizedAgentId = sanitizeBranchPart(agentId);
|
|
490
|
+
const worktreePath = path.join(worktreeRoot, sanitizedAgentId);
|
|
491
|
+
const branch = `pi-crew/${sanitizedRunId}/${sanitizedAgentId}`;
|
|
492
|
+
pruneStaleWorktrees(repoRoot);
|
|
493
|
+
git(repoRoot, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
494
|
+
const nodeModulesLinked = loadedConfig.config.worktree?.linkNodeModules === true
|
|
495
|
+
? linkNodeModulesIfPresent(repoRoot, worktreePath)
|
|
496
|
+
: false;
|
|
497
|
+
return { cwd: worktreePath, worktreePath, branch, nodeModulesLinked };
|
|
498
|
+
} catch {
|
|
499
|
+
// Graceful fallback: no git repo, dirty leader, or git error → run normally.
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* round-17 P2-4: Remove a DWF agent worktree after the agent completes.
|
|
506
|
+
*
|
|
507
|
+
* Captures the worktree diff as an artifact before removal (best-effort), then
|
|
508
|
+
* removes the worktree, deletes the ephemeral branch, and prunes stale refs.
|
|
509
|
+
* NEVER throws — cleanup failures are logged via `logInternalError` so a
|
|
510
|
+
* worktree/branch leak never crashes a workflow.
|
|
511
|
+
*/
|
|
512
|
+
export function cleanupAgentWorktree(manifest: TeamRunManifest, worktreePath: string, branch?: string): void {
|
|
513
|
+
// Capture diff as artifact (best-effort).
|
|
514
|
+
try {
|
|
515
|
+
const diff = captureWorktreeDiff(worktreePath);
|
|
516
|
+
if (diff.trim() && !diff.startsWith("Failed to capture worktree diff")) {
|
|
517
|
+
writeArtifact(manifest.artifactsRoot, {
|
|
518
|
+
kind: "diff",
|
|
519
|
+
relativePath: `wf/worktree-diff-${Date.now()}-${randomBytes(2).toString("hex")}.diff`,
|
|
520
|
+
content: diff,
|
|
521
|
+
producer: "dynamic-workflow",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
} catch (error) {
|
|
525
|
+
logInternalError("worktree.agent-cleanup.diff", error, `worktreePath=${worktreePath}`);
|
|
526
|
+
}
|
|
527
|
+
// Remove worktree (best-effort). Try git first, then fall back to fs.rm.
|
|
528
|
+
try {
|
|
529
|
+
const repoRoot = findGitRoot(manifest.cwd);
|
|
530
|
+
git(repoRoot, ["worktree", "remove", "--force", worktreePath]);
|
|
531
|
+
} catch (error) {
|
|
532
|
+
logInternalError("worktree.agent-cleanup.remove", error, `worktreePath=${worktreePath}`);
|
|
533
|
+
try {
|
|
534
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
535
|
+
} catch (rmError) {
|
|
536
|
+
logInternalError("worktree.agent-cleanup.rm", rmError, `worktreePath=${worktreePath}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// Delete the ephemeral agent branch (best-effort) to avoid accumulation across
|
|
540
|
+
// many agent calls. The diff is already captured above; the branch holds no value.
|
|
541
|
+
if (branch) {
|
|
542
|
+
try {
|
|
543
|
+
const repoRoot = findGitRoot(manifest.cwd);
|
|
544
|
+
git(repoRoot, ["branch", "-D", branch]);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
logInternalError("worktree.agent-cleanup.branch", error, `branch=${branch}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
// Prune stale worktree refs (best-effort).
|
|
550
|
+
try {
|
|
551
|
+
const repoRoot = findGitRoot(manifest.cwd);
|
|
552
|
+
git(repoRoot, ["worktree", "prune"]);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
logInternalError("worktree.agent-cleanup.prune", error, `worktreePath=${worktreePath}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
package/types/dwf.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring types for pi-crew dynamic workflow scripts (`.dwf.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Round-14 P1-1: gives TS users IDE IntelliSense for the `ctx` object passed to a
|
|
5
|
+
* workflow script's `export default async function(ctx) { ... }`.
|
|
6
|
+
*
|
|
7
|
+
* pi-crew passes `ctx` as a parameter (NOT as ambient globals), so the types here are
|
|
8
|
+
* named exports. Import them in your workflow script:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import type { WorkflowCtx } from "pi-crew/workflow";
|
|
12
|
+
*
|
|
13
|
+
* export default async function run(ctx: WorkflowCtx): Promise<void> {
|
|
14
|
+
* ctx.log("starting");
|
|
15
|
+
* const res = await ctx.agent({ role: "explorer", prompt: "look around" });
|
|
16
|
+
* ctx.setResult(res.artifactPath ?? "");
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Alternatively, add a triple-slash reference so the package's type map is loaded:
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* /// <reference types="pi-crew/workflow" />
|
|
24
|
+
* import type { WorkflowCtx } from "pi-crew/workflow";
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* These interfaces mirror the runtime types in `src/runtime/dynamic-workflow-context.ts`.
|
|
28
|
+
* They are authoring-only (no runtime values); the real implementations live in the runner.
|
|
29
|
+
*
|
|
30
|
+
* ## Resume & Checkpoint (round-18 P2-3)
|
|
31
|
+
*
|
|
32
|
+
* The runner persists a checkpoint after every `ctx.agent()` call so that a crash
|
|
33
|
+
* (timeout, OOM, agent error) between calls does not lose all progress. When you run
|
|
34
|
+
* `team action='resume' runId='X'`, the runner re-executes the script from the top
|
|
35
|
+
* but **hydrates** `ctx.vars`, `ctx.budget.spent()`, the phase list, and the log
|
|
36
|
+
* buffer from the last checkpoint.
|
|
37
|
+
*
|
|
38
|
+
* Because the script re-runs from the top, write it **defensively** — check
|
|
39
|
+
* `ctx.vars` to skip already-completed work:
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* export default async function run(ctx) {
|
|
43
|
+
* // Defensive resume: skip the scan phase if it already ran.
|
|
44
|
+
* if (ctx.vars.lastPhase !== "scan") {
|
|
45
|
+
* const res = await ctx.agent({ role: "explorer", prompt: "scan" });
|
|
46
|
+
* ctx.vars.lastPhase = "scan"; // checkpointed after this call
|
|
47
|
+
* }
|
|
48
|
+
* // ... continue with analyze, using ctx.vars from the prior run
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* On a clean completion the checkpoint is deleted, so a re-run with the same runId
|
|
53
|
+
* starts fresh. A missing or corrupt checkpoint is treated as a fresh run.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
export interface AgentCallOpts {
|
|
57
|
+
prompt: string;
|
|
58
|
+
/** Role name (resolved via 4-tier chain) OR explicit agent name. */
|
|
59
|
+
role?: string;
|
|
60
|
+
/** Explicit agent name — bypasses team-role lookup. */
|
|
61
|
+
agent?: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
model?: string;
|
|
64
|
+
skill?: string[] | false;
|
|
65
|
+
maxTurns?: number;
|
|
66
|
+
graceTurns?: number;
|
|
67
|
+
/** Dependency artifact paths injected into the agent prompt. */
|
|
68
|
+
inputs?: string[];
|
|
69
|
+
/** Disable ALL tools for this call (pure-judgment / verdict steps). */
|
|
70
|
+
disableTools?: boolean;
|
|
71
|
+
/** Override the resolved agent's system prompt. */
|
|
72
|
+
systemPrompt?: string;
|
|
73
|
+
/** Round-13: optional TypeBox schema. When set, output is validated; mismatch yields ok:false. */
|
|
74
|
+
schema?: { readonly [key: string]: unknown };
|
|
75
|
+
/** round-17 P2-4: spawn this agent in an isolated git worktree. Useful when
|
|
76
|
+
* parallel agents modify files concurrently (avoids conflicts). The worktree
|
|
77
|
+
* is created from HEAD, the agent runs there, and on completion the diff is
|
|
78
|
+
* captured as an artifact before cleanup. Default false. If worktree creation
|
|
79
|
+
* fails (no git repo, dirty leader), the agent runs in the normal cwd with a
|
|
80
|
+
* warning. Backward compatible — omitting it is identical to `false`. */
|
|
81
|
+
worktree?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AgentResult {
|
|
85
|
+
ok: boolean;
|
|
86
|
+
text: string;
|
|
87
|
+
structured?: unknown;
|
|
88
|
+
usage?: { input?: number; output?: number; cost?: number; turns?: number };
|
|
89
|
+
runId?: string;
|
|
90
|
+
taskId?: string;
|
|
91
|
+
artifactPath?: string;
|
|
92
|
+
error?: string;
|
|
93
|
+
durationMs?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Round-14 P1-2: per-workflow token budget. */
|
|
97
|
+
export interface WorkflowBudget {
|
|
98
|
+
/** Configured budget, or null when unbounded. */
|
|
99
|
+
total: number | null;
|
|
100
|
+
/** Tokens consumed so far (accumulated from each ctx.agent() run's usage). */
|
|
101
|
+
spent(): number;
|
|
102
|
+
/** Tokens remaining; Infinity when total is null. */
|
|
103
|
+
remaining(): number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ReviewResult {
|
|
107
|
+
outcome: "accept" | "reject" | "changes_requested";
|
|
108
|
+
feedback: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Options for ctx.mail(). */
|
|
112
|
+
export interface MailOpts {
|
|
113
|
+
kind?: string;
|
|
114
|
+
taskId?: string;
|
|
115
|
+
replyTo?: string;
|
|
116
|
+
replyDeadline?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Options for ctx.review(). */
|
|
120
|
+
export interface ReviewOpts {
|
|
121
|
+
content?: string;
|
|
122
|
+
artifactPath?: string;
|
|
123
|
+
disableTools?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Options for ctx.retry(). */
|
|
127
|
+
export interface RetryOpts {
|
|
128
|
+
feedback?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The capability-locked context object passed to a `.dwf.ts` script's
|
|
133
|
+
* `export default async function(ctx)`. Exposes ONLY the documented methods —
|
|
134
|
+
* no raw manifest/process/require leaks.
|
|
135
|
+
*
|
|
136
|
+
* NOTE: v1 has NO vm sandbox; the script CAN reach process/require directly.
|
|
137
|
+
* The frozen ctx is a contract surface, not a security boundary. `.dwf.ts`
|
|
138
|
+
* scripts are postinstall-equivalent trust.
|
|
139
|
+
*/
|
|
140
|
+
export interface WorkflowCtx {
|
|
141
|
+
cwd: string;
|
|
142
|
+
runId: string;
|
|
143
|
+
goal?: string;
|
|
144
|
+
/** Script-local persistent variables.
|
|
145
|
+
*
|
|
146
|
+
* On resume (round-18 P2-3), these are hydrated from the last checkpoint so a
|
|
147
|
+
* re-run continues where it left off. Write defensive scripts that inspect
|
|
148
|
+
* `ctx.vars` to skip work already done in a prior (crashed) run. */
|
|
149
|
+
vars: Record<string, unknown>;
|
|
150
|
+
/** Abort signal (cancel/stop). */
|
|
151
|
+
signal: AbortSignal;
|
|
152
|
+
/** Concurrency semaphore (bounded by ctx concurrency). */
|
|
153
|
+
semaphore: import("../src/runtime/semaphore").Semaphore;
|
|
154
|
+
|
|
155
|
+
/** Spawn one agent, await result. Concurrency enforced by ctx.semaphore. */
|
|
156
|
+
agent(opts: AgentCallOpts): Promise<AgentResult>;
|
|
157
|
+
/** Bounded fan-out preserving order. */
|
|
158
|
+
fanOut<T>(items: T[], limit: number, fn: (item: T, i: number) => Promise<AgentResult>): Promise<AgentResult[]>;
|
|
159
|
+
/** Pipeline: sequential per-item stages, parallel across items (bounded by ctx.semaphore).
|
|
160
|
+
* Failed stage → null for that item (logged); other items continue. round-16 (P2-1). */
|
|
161
|
+
pipeline<TItem, TResult = unknown>(
|
|
162
|
+
items: TItem[],
|
|
163
|
+
...stages: Array<(previous: TResult, original: TItem, index: number) => Promise<TResult> | TResult>
|
|
164
|
+
): Promise<(TResult | null)[]>;
|
|
165
|
+
/** Run a reviewer agent over an artifact; parse {outcome, feedback}. */
|
|
166
|
+
review(taskId: string, reviewerRole?: string, opts?: ReviewOpts): Promise<ReviewResult>;
|
|
167
|
+
/** Re-run a task with feedback (wraps executeWithRetry). */
|
|
168
|
+
retry(taskId: string, opts?: RetryOpts): Promise<AgentResult>;
|
|
169
|
+
/** Send a mailbox message to another agent/leader. Returns the message id. */
|
|
170
|
+
mail(to: string, body: string, opts?: MailOpts): string;
|
|
171
|
+
/** Block until N mailbox replies arrive or deadline. */
|
|
172
|
+
gatherReplies(messageIds: string[], deadlineMs: number): Promise<unknown[]>;
|
|
173
|
+
/** Render a built-in plan template (full-implementation / standard-review). */
|
|
174
|
+
renderTemplate(name: string, vars: Record<string, string>): unknown;
|
|
175
|
+
/** Mark the final result. ONLY this artifact reaches the main context. */
|
|
176
|
+
setResult(artifactPath: string, meta?: Record<string, unknown>): void;
|
|
177
|
+
/** Round-12: mark the start of a named workflow phase (emits dwf.phase_started/_completed). Idempotent on the same title. */
|
|
178
|
+
phase(title: string): void;
|
|
179
|
+
/** Round-14 P1-3: append a workflow-level log line (emits a dwf.log event). */
|
|
180
|
+
log(message: unknown): void;
|
|
181
|
+
/** Round-14 P1-2: per-workflow token budget; ctx.agent() auto-rejects when exhausted. */
|
|
182
|
+
budget: WorkflowBudget;
|
|
183
|
+
/** Round-14 P1-5: typed workflow arguments (sourced from manifest.args). Defaults to {}. */
|
|
184
|
+
args<T = unknown>(): T;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export {};
|