pi-crew 0.9.5 → 0.9.7
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 +494 -0
- package/README.md +1 -1
- 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 +8 -2
- 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/child-pi.ts +101 -10
- 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/team-runner.ts +8 -3
- package/src/runtime/zombie-scanner.ts +297 -0
- package/src/schema/team-tool-schema.ts +28 -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/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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWF phase display — pure functions for extracting DWF phase state from the
|
|
3
|
+
* run's recent event window and rendering phase markers (▶/✓/⏸) in the
|
|
4
|
+
* progress pane.
|
|
5
|
+
*
|
|
6
|
+
* round-15 (P1-4). These functions are side-effect free and perform no I/O;
|
|
7
|
+
* they derive phase state entirely from the `recentEvents` slice already
|
|
8
|
+
* tailed by `run-snapshot-cache.ts`. Non-DWF runs (no `dwf.phase_*` events)
|
|
9
|
+
* yield `null`, so the progress pane stays unchanged for them.
|
|
10
|
+
*/
|
|
11
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export type DwfPhaseStatus = "running" | "completed" | "pending";
|
|
18
|
+
|
|
19
|
+
export interface DwfPhaseEntry {
|
|
20
|
+
/** Phase title as passed to `ctx.phase(title)`. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Derived lifecycle status for display. */
|
|
23
|
+
status: DwfPhaseStatus;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DwfPhaseState {
|
|
27
|
+
/** Ordered list of phases seen in the event window (first-seen order). */
|
|
28
|
+
phases: DwfPhaseEntry[];
|
|
29
|
+
/** Name of the currently running phase, or null if all are completed. */
|
|
30
|
+
currentPhase: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RenderDwfPhaseOptions {
|
|
34
|
+
/** When true, render ASCII fallback markers instead of Unicode glyphs. */
|
|
35
|
+
ascii?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Markers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
// Unicode markers — consistent with the ▸/● glyphs already used in the dashboard.
|
|
43
|
+
const MARKER_RUNNING = "▶";
|
|
44
|
+
const MARKER_COMPLETED = "✓";
|
|
45
|
+
const MARKER_PENDING = "⏸";
|
|
46
|
+
|
|
47
|
+
// ASCII fallbacks for terminals that mis-render the Unicode glyphs above.
|
|
48
|
+
const MARKER_RUNNING_ASCII = "[>]";
|
|
49
|
+
const MARKER_COMPLETED_ASCII = "[v]";
|
|
50
|
+
const MARKER_PENDING_ASCII = "[ ]";
|
|
51
|
+
|
|
52
|
+
const DWF_PHASE_HEADER = " ── DWF Phases ──";
|
|
53
|
+
|
|
54
|
+
function markerFor(status: DwfPhaseStatus, ascii: boolean): string {
|
|
55
|
+
if (ascii) {
|
|
56
|
+
if (status === "running") return MARKER_RUNNING_ASCII;
|
|
57
|
+
if (status === "completed") return MARKER_COMPLETED_ASCII;
|
|
58
|
+
return MARKER_PENDING_ASCII;
|
|
59
|
+
}
|
|
60
|
+
if (status === "running") return MARKER_RUNNING;
|
|
61
|
+
if (status === "completed") return MARKER_COMPLETED;
|
|
62
|
+
return MARKER_PENDING;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Extraction
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function phaseNameFrom(event: TeamEvent): string | undefined {
|
|
70
|
+
const phase = event.data?.phase;
|
|
71
|
+
return typeof phase === "string" && phase.length > 0 ? phase : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Derive DWF phase state from a (chronological) event window.
|
|
76
|
+
*
|
|
77
|
+
* Returns `null` when the window contains no `dwf.phase_started` /
|
|
78
|
+
* `dwf.phase_completed` events — i.e. a non-DWF run — so callers can short
|
|
79
|
+
* circuit phase rendering entirely.
|
|
80
|
+
*
|
|
81
|
+
* Because the window is bounded, the oldest phase events may have scrolled
|
|
82
|
+
* off. A phase whose `dwf.phase_started` scrolled off but whose
|
|
83
|
+
* `dwf.phase_completed` is still visible is still tracked (as completed). A
|
|
84
|
+
* phase that started but whose completion scrolled off and which is not the
|
|
85
|
+
* current phase is shown as `pending` (indeterminate).
|
|
86
|
+
*/
|
|
87
|
+
export function extractDwfPhaseState(events: TeamEvent[]): DwfPhaseState | null {
|
|
88
|
+
const order: string[] = [];
|
|
89
|
+
const seen = new Set<string>();
|
|
90
|
+
const completed = new Set<string>();
|
|
91
|
+
let currentPhase: string | null = null;
|
|
92
|
+
|
|
93
|
+
const remember = (phase: string): void => {
|
|
94
|
+
if (!seen.has(phase)) {
|
|
95
|
+
seen.add(phase);
|
|
96
|
+
order.push(phase);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
for (const event of events) {
|
|
101
|
+
if (event.type === "dwf.phase_started") {
|
|
102
|
+
const phase = phaseNameFrom(event);
|
|
103
|
+
if (phase === undefined) continue;
|
|
104
|
+
remember(phase);
|
|
105
|
+
// The most recent phase_started marks the running phase.
|
|
106
|
+
currentPhase = phase;
|
|
107
|
+
} else if (event.type === "dwf.phase_completed") {
|
|
108
|
+
const phase = phaseNameFrom(event);
|
|
109
|
+
if (phase === undefined) continue;
|
|
110
|
+
remember(phase);
|
|
111
|
+
completed.add(phase);
|
|
112
|
+
// If the phase just closed was the running one, it is no longer running.
|
|
113
|
+
if (phase === currentPhase) {
|
|
114
|
+
currentPhase = null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (order.length === 0) return null;
|
|
120
|
+
|
|
121
|
+
const phases: DwfPhaseEntry[] = order.map((name) => {
|
|
122
|
+
if (name === currentPhase) return { name, status: "running" as const };
|
|
123
|
+
if (completed.has(name)) return { name, status: "completed" as const };
|
|
124
|
+
return { name, status: "pending" as const };
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { phases, currentPhase };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Rendering
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render phase marker lines for the progress pane.
|
|
136
|
+
*
|
|
137
|
+
* - One line per phase: ` ▶ Phase: Scan`, ` ✓ Phase: Scan`, ` ⏸ Phase: Review`.
|
|
138
|
+
* - A grouping header is emitted only when more than one phase is present.
|
|
139
|
+
* - When `options.ascii` is true, ASCII fallback markers are used.
|
|
140
|
+
*
|
|
141
|
+
* Always returns a non-empty array (the caller guarantees a non-null state).
|
|
142
|
+
*/
|
|
143
|
+
export function renderDwfPhaseLines(state: DwfPhaseState, options?: RenderDwfPhaseOptions): string[] {
|
|
144
|
+
const ascii = options?.ascii === true;
|
|
145
|
+
const lines: string[] = [];
|
|
146
|
+
if (state.phases.length > 1) lines.push(DWF_PHASE_HEADER);
|
|
147
|
+
for (const entry of state.phases) {
|
|
148
|
+
lines.push(` ${markerFor(entry.status, ascii)} Phase: ${entry.name}`);
|
|
149
|
+
}
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
@@ -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 {};
|