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.
Files changed (37) hide show
  1. package/CHANGELOG.md +556 -0
  2. package/README.md +10 -3
  3. package/docs/HARNESS_BACKLOG.md +51 -3
  4. package/docs/dynamic-workflows.md +315 -2
  5. package/docs/fix-plan-disabletools-exit-null.md +219 -0
  6. package/docs/troubleshooting.md +76 -0
  7. package/package.json +10 -3
  8. package/src/config/defaults.ts +8 -4
  9. package/src/extension/team-tool/doctor.ts +14 -0
  10. package/src/extension/team-tool/run.ts +2 -0
  11. package/src/runtime/background-runner.ts +1 -1
  12. package/src/runtime/capability-inventory.ts +20 -1
  13. package/src/runtime/child-pi.ts +109 -11
  14. package/src/runtime/deterministic-ast.ts +161 -0
  15. package/src/runtime/dwf-state-store.ts +97 -0
  16. package/src/runtime/dynamic-workflow-context.ts +381 -7
  17. package/src/runtime/dynamic-workflow-runner.ts +93 -2
  18. package/src/runtime/pi-args.ts +11 -0
  19. package/src/runtime/result-extractor.ts +72 -7
  20. package/src/runtime/task-output-context.ts +25 -9
  21. package/src/runtime/team-runner.ts +8 -3
  22. package/src/runtime/zombie-scanner.ts +297 -0
  23. package/src/schema/team-tool-schema.ts +28 -0
  24. package/src/skills/discover-skills.ts +61 -8
  25. package/src/skills/validate.ts +267 -0
  26. package/src/state/contracts.ts +1 -0
  27. package/src/state/state-store.ts +3 -0
  28. package/src/state/types.ts +9 -0
  29. package/src/ui/dashboard-panes/progress-pane.ts +5 -0
  30. package/src/ui/dwf-phase-display.ts +151 -0
  31. package/src/ui/keybinding-map.ts +128 -41
  32. package/src/ui/run-event-bus.ts +83 -0
  33. package/src/ui/run-snapshot-cache.ts +4 -0
  34. package/src/ui/snapshot-types.ts +3 -0
  35. package/src/workflows/workflow-config.ts +3 -0
  36. package/src/worktree/worktree-manager.ts +94 -0
  37. package/types/dwf.d.ts +187 -0
@@ -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
  };
@@ -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 {};