pi-crew 0.4.0 → 0.5.1

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 CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.1] — Integration + End-to-End Tests (2026-05-26)
4
+
5
+ ### Integration
6
+ - **team-tool.ts**: Wire P1-P6 into switch statement
7
+ - `action='graph'` — load/save/list run graphs
8
+ - `action='onboard'` — team onboarding generator
9
+ - `action='explain'` — task explain context
10
+ - `action='cache'` — run result caching lookup
11
+ - `action='checkpoint'` — checkpoint retrieval
12
+ - `action='search'` — BM25 ranked agent/team search
13
+ - **team-tool-schema.ts**: Add 6 new actions to schema
14
+ - **Type fixes**: run-graph.ts, run-cache.ts, checkpoint.ts, team-onboard.ts
15
+ - **P0 .gitignore**: ensureCrewDirectory auto-updates .gitignore
16
+
17
+ ### Tests
18
+ - 8/8 new action tests pass
19
+ - 10/10 end-to-end feature tests pass
20
+ - All 1796 unit + 45 integration passing
21
+ - CI: Ubuntu/macOS/Windows all passing
22
+
23
+ ---
24
+
25
+ ## [0.5.0]
26
+
27
+ ### New Features: P0-P6 from Understand-Anything Research
28
+
29
+ #### P0: Auto-Setup .crew Directory
30
+ - `ensureCrewDirectory()` creates full directory structure on first run
31
+ - `gitignore-manager.ts` auto-updates `.gitignore` with `.crew/` entries
32
+ - Creates: `state/runs`, `state/subagents`, `artifacts`, `cache`, `graphs`, `audit`
33
+ - README.md explains `.crew` directory purpose
34
+
35
+ #### P1: BM25 Agent/Team Search
36
+ - `BM25Search` class with configurable k1/b parameters
37
+ - `searchAgents(query)` — ranked agent search by name/description/skills
38
+ - `searchTeams(query)` — ranked team search by name/description/roles
39
+
40
+ #### P2: Team Onboarding Generator
41
+ - `buildTeamOnboarding()` generates markdown from run history
42
+ - Shows: past runs, stats, usage examples, available teams
43
+ - `loadRunSummaries()` helper for run history loading
44
+
45
+ #### P3: Task Explain Context
46
+ - `handleExplain(runId, taskId)` — full run or individual task explanation
47
+ - `buildTaskExplainContext()` — causal chain, layers, files produced
48
+ - `formatTaskExplain()` — markdown output with why/what/connections
49
+
50
+ #### P4: Unified Run Graph
51
+ - `buildRunGraph()` — consolidates manifest + tasks into single graph
52
+ - `saveRunGraph()` / `loadRunGraph()` — persist to `.crew/graphs/`
53
+ - `listRunGraphs()` — enumerate archived graphs
54
+
55
+ #### P5: Run Result Caching
56
+ - `computeRunCacheKey()` — SHA-256 hash of goal+team+workflow
57
+ - `getCachedRun()` / `saveRunToCache()` — TTL-based cache (default 1h)
58
+ - `clearCache()` / `getCacheStats()` — cache management
59
+
60
+ #### P6: Agent Checkpointing
61
+ - `FileCheckpointStore` — checkpoints in `.crew/state/runs/<runId>/checkpoints/`
62
+ - `saveCheckpoint()` / `loadCheckpoint()` / `clearCheckpoint()`
63
+ - `hasCheckpoint()` / `listCheckpoints()` for recovery
64
+
65
+ ### Tests
66
+ - 56 new unit tests (all passing)
67
+ - Total: 1796 unit tests + 45 integration tests passing
68
+
69
+ ### Bug Fixes
70
+ - Worktree test teardown: clean `.crew/` before git checks for clean repository
71
+
72
+ ---
73
+
3
74
  ## [0.4.0] — 9arm-skills Enforcement Patterns & Integration Tests (2026-05-26)
4
75
 
5
76
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-crew",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
5
5
  "author": "baphuongna",
6
6
  "license": "MIT",
@@ -0,0 +1,174 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { projectCrewRoot } from "../utils/paths.ts";
4
+ import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
5
+ import type { TeamRunManifest } from "../state/types.ts";
6
+
7
+ export interface OnboardingOptions {
8
+ team?: string;
9
+ limit?: number;
10
+ cwd?: string;
11
+ }
12
+
13
+ interface RunSummary {
14
+ runId: string;
15
+ status: string;
16
+ goal: string;
17
+ team: string;
18
+ createdAt: string;
19
+ completedAt?: string;
20
+ taskCount: number;
21
+ }
22
+
23
+ /**
24
+ * Load run summaries from the state directory.
25
+ */
26
+ function loadRunSummaries(cwd: string, options: OnboardingOptions = {}): RunSummary[] {
27
+ const crewRoot = projectCrewRoot(cwd);
28
+ const runsRoot = path.join(crewRoot, "state", "runs");
29
+
30
+ if (!fs.existsSync(runsRoot)) return [];
31
+
32
+ const limit = options.limit ?? 5;
33
+ const teamFilter = options.team;
34
+
35
+ const entries = fs.readdirSync(runsRoot)
36
+ .filter((e) => {
37
+ try {
38
+ return fs.statSync(path.join(runsRoot, e)).isDirectory();
39
+ } catch {
40
+ return false;
41
+ }
42
+ })
43
+ .sort()
44
+ .reverse()
45
+ .slice(0, 100);
46
+
47
+ const summaries: RunSummary[] = [];
48
+
49
+ for (const runId of entries) {
50
+ if (summaries.length >= limit) break;
51
+
52
+ const manifestPath = path.join(runsRoot, runId, "manifest.json");
53
+ if (!fs.existsSync(manifestPath)) continue;
54
+
55
+ try {
56
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest & { completedAt?: string };
57
+
58
+ if (teamFilter && raw.team !== teamFilter) continue;
59
+
60
+ summaries.push({
61
+ runId,
62
+ status: raw.status,
63
+ goal: raw.goal ?? "",
64
+ team: raw.team,
65
+ createdAt: raw.createdAt,
66
+ completedAt: raw.completedAt ?? raw.updatedAt,
67
+ taskCount: 0, // tasks stored separately, not in manifest
68
+ });
69
+ } catch {
70
+ continue;
71
+ }
72
+ }
73
+
74
+ return summaries;
75
+ }
76
+
77
+ /**
78
+ * Format duration in minutes.
79
+ */
80
+ function formatDuration(createdAt: string, completedAt?: string): string {
81
+ const start = new Date(createdAt).getTime();
82
+ const end = completedAt ? new Date(completedAt).getTime() : Date.now();
83
+ const minutes = Math.round((end - start) / 1000 / 60);
84
+ if (minutes < 1) return "<1m";
85
+ if (minutes >= 60) return `${Math.round(minutes / 60)}h`;
86
+ return `${minutes}m`;
87
+ }
88
+
89
+ /**
90
+ * Build markdown onboarding guide for a team.
91
+ */
92
+ export function buildTeamOnboarding(team: string, cwd: string, options: OnboardingOptions = {}): string {
93
+ const runs = loadRunSummaries(cwd, { ...options, team });
94
+
95
+ const lines: string[] = [];
96
+
97
+ // Header
98
+ lines.push(`# Team: ${team}`);
99
+ lines.push("");
100
+ lines.push(`> Multi-agent ${team} team for pi-crew.`);
101
+ lines.push("");
102
+
103
+ // Overview stats
104
+ const completed = runs.filter((r) => r.status === "completed");
105
+ const failed = runs.filter((r) => r.status === "failed" || r.status === "cancelled");
106
+
107
+ let avgDuration = 0;
108
+ if (completed.length > 0) {
109
+ const totalMs = completed.reduce((sum, r) => {
110
+ const start = new Date(r.createdAt).getTime();
111
+ const end = r.completedAt ? new Date(r.completedAt).getTime() : Date.now();
112
+ return sum + (end - start);
113
+ }, 0);
114
+ avgDuration = totalMs / completed.length / 1000 / 60;
115
+ }
116
+
117
+ lines.push("## Overview");
118
+ lines.push("");
119
+ lines.push(`| Metric | Value |`);
120
+ lines.push(`|--------|-------|`);
121
+ lines.push(`| Total runs | ${runs.length} |`);
122
+ lines.push(`| Completed | ${completed.length} |`);
123
+ lines.push(`| Failed/Cancelled | ${failed.length} |`);
124
+ if (avgDuration > 0) {
125
+ lines.push(`| Avg duration | ${avgDuration.toFixed(1)} min |`);
126
+ }
127
+ lines.push("");
128
+
129
+ // Past runs table
130
+ if (runs.length > 0) {
131
+ lines.push("## Past Runs");
132
+ lines.push("");
133
+ lines.push("| Run | Goal | Duration | Status |");
134
+ lines.push("|-----|------|----------|--------|");
135
+
136
+ for (const run of runs) {
137
+ const duration = formatDuration(run.createdAt, run.completedAt);
138
+ const goalPreview = run.goal ? run.goal.slice(0, 40) : "N/A";
139
+ const statusIcon = run.status === "completed" ? "✅" : run.status === "failed" ? "❌" : "⚠️";
140
+ lines.push(`| \`${run.runId.slice(-8)}\` | ${goalPreview}${run.goal.length > 40 ? "..." : ""} | ${duration} | ${statusIcon} ${run.status} |`);
141
+ }
142
+ lines.push("");
143
+ }
144
+
145
+ // How to run
146
+ lines.push("## How to Run");
147
+ lines.push("");
148
+ lines.push("```bash");
149
+ lines.push(`team action='run' team='${team}' goal="<your goal>"`);
150
+ lines.push("```");
151
+ lines.push("");
152
+ lines.push("See `team action='help'` for more options.");
153
+ lines.push("");
154
+
155
+ // Teams
156
+ lines.push("## Available Teams");
157
+ lines.push("");
158
+ try {
159
+ const discovery = discoverTeams(cwd);
160
+ const teams = allTeams(discovery);
161
+ for (const t of teams) {
162
+ lines.push(`- \`${t.name}\` — ${t.description ?? "No description"}`);
163
+ }
164
+ } catch {
165
+ lines.push("- *(team discovery unavailable)*");
166
+ }
167
+ lines.push("");
168
+
169
+ // Footer
170
+ lines.push("---");
171
+ lines.push("*Generated by pi-crew. For up-to-date info, check recent run history.*");
172
+
173
+ return lines.join("\n");
174
+ }
@@ -0,0 +1,268 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { toolResult } from "../tool-result.ts";
4
+ import { loadRunManifestById } from "../../state/state-store.ts";
5
+ import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
6
+
7
+ /**
8
+ * Local wrapper matching the planned `result` API used by handleExplain.
9
+ */
10
+ function result(text: string, details: Record<string, unknown>, isError: boolean): { isError: boolean; text: string } {
11
+ return { isError, text };
12
+ }
13
+
14
+ export interface TaskExplainContext {
15
+ taskId: string;
16
+ role: string;
17
+ status: string;
18
+ phase?: string;
19
+ why: string;
20
+ what: string;
21
+ filesTouched: string[];
22
+ connectedTasks: Array<{ taskId: string; status: string; relation: string }>;
23
+ layer: string;
24
+ complexity: "simple" | "moderate" | "complex";
25
+ usage?: { inputTokens?: number; outputTokens?: number };
26
+ duration?: number;
27
+ }
28
+
29
+ /**
30
+ * Build explain context for a specific task in a run.
31
+ */
32
+ export function buildTaskExplainContext(
33
+ manifest: TeamRunManifest,
34
+ tasks: TeamTaskState[],
35
+ taskId: string,
36
+ ): TaskExplainContext {
37
+ const task = tasks.find((t) => t.id === taskId);
38
+ if (!task) {
39
+ throw new Error(`Task ${taskId} not found in run ${manifest.runId}`);
40
+ }
41
+
42
+ const dependsOn = task.dependsOn ?? [];
43
+ const dependents = tasks.filter((t) => (t.dependsOn ?? []).includes(taskId));
44
+
45
+ // Layer from phase
46
+ const layerMap: Record<string, string> = {
47
+ explore: "exploration",
48
+ plan: "planning",
49
+ assess: "assessment",
50
+ execute: "execution",
51
+ verify: "verification",
52
+ analyze: "analysis",
53
+ write: "documentation",
54
+ "": "unknown",
55
+ };
56
+ const layer = layerMap[task.adaptive?.phase ?? ""] ?? "unknown";
57
+
58
+ // Why it exists
59
+ let why = `Part of ${manifest.team ?? "unknown"} team workflow.`;
60
+ if (dependsOn.length > 0) {
61
+ const depTasks = dependsOn.map((d) => {
62
+ const dep = tasks.find((t) => t.id === d);
63
+ return dep ? `\`${d}\` (${dep.status})` : `\`${d}\``;
64
+ }).join(", ");
65
+ why += ` Depends on ${depTasks}.`;
66
+ }
67
+ if (dependents.length > 0) {
68
+ why += ` ${dependents.length} task(s) depend on this.`;
69
+ }
70
+
71
+ // What it did
72
+ let what = `Ran agent: ${task.role}`;
73
+ if (task.model) {
74
+ what += ` (${task.model})`;
75
+ }
76
+ if (task.usage) {
77
+ const inputTokens = task.usage.input ?? 0;
78
+ const outputTokens = task.usage.output ?? 0;
79
+ what += `. Usage: input=${inputTokens}, output=${outputTokens}`;
80
+ }
81
+ if (task.status === "failed") {
82
+ what += `. Status: FAILED`;
83
+ if (task.error) {
84
+ what += ` — ${task.error}`;
85
+ }
86
+ }
87
+
88
+ // Files from artifacts
89
+ const artifactsPath = manifest.artifactsRoot;
90
+ const filesTouched: string[] = [];
91
+ if (fs.existsSync(artifactsPath)) {
92
+ try {
93
+ const entries = fs.readdirSync(artifactsPath);
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(artifactsPath, entry);
96
+ try {
97
+ if (fs.statSync(fullPath).isFile()) {
98
+ filesTouched.push(entry);
99
+ }
100
+ } catch { /* ignore */ }
101
+ }
102
+ } catch { /* ignore */ }
103
+ }
104
+
105
+ // Duration
106
+ let duration: number | undefined;
107
+ if (task.startedAt && task.finishedAt) {
108
+ duration = (new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime()) / 1000;
109
+ }
110
+
111
+ // Complexity
112
+ const complexity = tasks.length <= 3 ? "simple" : tasks.length <= 8 ? "moderate" : "complex";
113
+
114
+ return {
115
+ taskId,
116
+ role: task.role,
117
+ status: task.status,
118
+ phase: task.adaptive?.phase,
119
+ why,
120
+ what,
121
+ filesTouched,
122
+ connectedTasks: [
123
+ ...dependsOn.map((d) => {
124
+ const dep = tasks.find((t) => t.id === d);
125
+ return {
126
+ taskId: d,
127
+ status: dep?.status ?? "unknown",
128
+ relation: "depends on",
129
+ };
130
+ }),
131
+ ...dependents.map((d) => ({
132
+ taskId: d.id,
133
+ status: d.status,
134
+ relation: "depended on by",
135
+ })),
136
+ ],
137
+ layer,
138
+ complexity,
139
+ usage: task.usage ? { inputTokens: task.usage.input, outputTokens: task.usage.output } : undefined,
140
+ duration,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Format task explain context as markdown.
146
+ */
147
+ export function formatTaskExplain(ctx: TaskExplainContext): string {
148
+ const lines: string[] = [];
149
+
150
+ lines.push(`# Task: ${ctx.taskId} (${ctx.role})`);
151
+ lines.push("");
152
+ lines.push(`| | |`);
153
+ lines.push(`|---|---|`);
154
+ lines.push(`| **Status** | ${ctx.status} |`);
155
+ if (ctx.phase) lines.push(`| **Phase** | ${ctx.phase} |`);
156
+ lines.push(`| **Layer** | ${ctx.layer} |`);
157
+ lines.push(`| **Complexity** | ${ctx.complexity} |`);
158
+
159
+ if (ctx.duration) {
160
+ const minutes = Math.round(ctx.duration / 60);
161
+ lines.push(`| **Duration** | ${minutes} min |`);
162
+ }
163
+
164
+ if (ctx.usage) {
165
+ const input = ctx.usage.inputTokens ?? 0;
166
+ const output = ctx.usage.outputTokens ?? 0;
167
+ lines.push(`| **Usage** | ↑${input} ↓${output} tokens |`);
168
+ }
169
+
170
+ lines.push("");
171
+ lines.push(`## Why it exists`);
172
+ lines.push("");
173
+ lines.push(ctx.why);
174
+ lines.push("");
175
+ lines.push(`## What it did`);
176
+ lines.push("");
177
+ lines.push(ctx.what);
178
+ lines.push("");
179
+
180
+ if (ctx.filesTouched.length > 0) {
181
+ lines.push("## Files produced");
182
+ lines.push("");
183
+ for (const file of ctx.filesTouched) {
184
+ lines.push(`- \`${file}\``);
185
+ }
186
+ lines.push("");
187
+ }
188
+
189
+ if (ctx.connectedTasks.length > 0) {
190
+ lines.push("## Connected tasks");
191
+ lines.push("");
192
+ for (const conn of ctx.connectedTasks) {
193
+ lines.push(`- ${conn.relation} \`${conn.taskId}\` (${conn.status})`);
194
+ }
195
+ lines.push("");
196
+ }
197
+
198
+ return lines.join("\n");
199
+ }
200
+
201
+ /**
202
+ * Handle team action='explain'.
203
+ */
204
+ export function handleExplain(params: {
205
+ runId?: string;
206
+ taskId?: string;
207
+ cwd?: string;
208
+ }, cwd: string): { isError: boolean; text: string } {
209
+ if (!params.runId) {
210
+ return result("explain requires runId", { action: "explain", status: "error" }, true);
211
+ }
212
+
213
+ const loaded = loadRunManifestById(cwd, params.runId);
214
+ if (!loaded) {
215
+ return result(`Run '${params.runId}' not found.`, { action: "explain", status: "error" }, true);
216
+ }
217
+
218
+ const { manifest, tasks } = loaded;
219
+
220
+ if (params.taskId) {
221
+ try {
222
+ const ctx = buildTaskExplainContext(manifest, tasks, params.taskId);
223
+ const output = formatTaskExplain(ctx);
224
+ return result(output, { action: "explain", runId: params.runId }, false);
225
+ } catch (err) {
226
+ return result(`Task '${params.taskId}' not found: ${err instanceof Error ? err.message : String(err)}`, { action: "explain", status: "error" }, true);
227
+ }
228
+ }
229
+
230
+ // Explain entire run
231
+ const lines: string[] = [];
232
+ lines.push(`# Run: ${params.runId}`);
233
+ lines.push("");
234
+ lines.push(`| | |`);
235
+ lines.push(`|---|---|`);
236
+
237
+ const start = new Date(manifest.createdAt).getTime();
238
+ const end = manifest.updatedAt ? new Date(manifest.updatedAt).getTime() : Date.now();
239
+ const duration = Math.round((end - start) / 1000 / 60);
240
+
241
+ lines.push(`| **Team** | ${manifest.team} |`);
242
+ lines.push(`| **Workflow** | ${manifest.workflow ?? "default"} |`);
243
+ lines.push(`| **Status** | ${manifest.status} |`);
244
+ lines.push(`| **Duration** | ${duration} min |`);
245
+ lines.push(`| **Tasks** | ${tasks.length} |`);
246
+ lines.push("");
247
+
248
+ lines.push("## Tasks");
249
+ lines.push("");
250
+ lines.push("| Task | Role | Status | Layer |");
251
+ lines.push("|------|------|--------|-------|");
252
+
253
+ for (const task of tasks) {
254
+ const layerMap: Record<string, string> = {
255
+ explore: "exploration", plan: "planning", assess: "assessment",
256
+ execute: "execution", verify: "verification", analyze: "analysis", write: "documentation",
257
+ };
258
+ const layer = layerMap[task.adaptive?.phase ?? ""] ?? "unknown";
259
+ const statusIcon = task.status === "completed" ? "✅" : task.status === "failed" ? "❌" : "⏳";
260
+ lines.push(`| \`${task.id}\` | ${task.role} | ${statusIcon} ${task.status} | ${layer} |`);
261
+ }
262
+ lines.push("");
263
+
264
+ lines.push("---");
265
+ lines.push(`*Use \`team action='explain' runId=${params.runId} taskId=<taskId>\` for task detail.*`);
266
+
267
+ return result(lines.join("\n"), { action: "explain", runId: params.runId }, false);
268
+ }
@@ -1,4 +1,5 @@
1
1
  import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
2
+ import { ensureCrewDirectory } from "../../state/crew-init.ts";
2
3
  import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
3
4
  import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
4
5
  import { loadConfig } from "../../config/config.ts";
@@ -74,6 +75,9 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
74
75
  if (!goal) return result("Run requires goal or task.", { action: "run", status: "error" }, true);
75
76
  const intentPrefix = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
76
77
 
78
+ // P0: Ensure .crew directory structure exists before creating any manifests.
79
+ await ensureCrewDirectory(ctx.cwd);
80
+
77
81
  const teams = allTeams(discoverTeams(ctx.cwd));
78
82
  const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
79
83
  const agents = allAgents(discoverAgents(ctx.cwd));
@@ -129,12 +129,15 @@ async function handleRun(
129
129
  import { waitForRun } from "../runtime/run-tracker.ts";
130
130
  import { normalizeSkillOverride } from "../runtime/skill-instructions.ts";
131
131
  import { logInternalError } from "../utils/internal-error.ts";
132
+ import { searchAgents, searchTeams } from "../utils/bm25-search.ts";
133
+ import { projectCrewRoot } from "../utils/paths.ts";
132
134
  import {
133
135
  type CacheControlDeps,
134
136
  invalidateSnapshot,
135
137
  } from "./team-tool/cache-control.ts";
136
138
  import { handleCancel, handleRetry } from "./team-tool/cancel.ts";
137
139
  import { handleDoctor } from "./team-tool/doctor.ts";
140
+ import { handleExplain } from "./team-tool/explain.ts";
138
141
  import { handleHealthMonitor } from "./team-tool/health-monitor.ts";
139
142
  import {
140
143
  handleArtifacts,
@@ -150,6 +153,17 @@ import {
150
153
  handlePrune,
151
154
  handleWorktrees,
152
155
  } from "./team-tool/lifecycle-actions.ts";
156
+ import {
157
+ getCachedRun,
158
+ computeRunCacheKey,
159
+ getCacheStats,
160
+ } from "../state/run-cache.ts";
161
+ import {
162
+ loadRunGraph,
163
+ listRunGraphs,
164
+ } from "../state/run-graph.ts";
165
+ import { FileCheckpointStore } from "../runtime/checkpoint.ts";
166
+ import { buildTeamOnboarding } from "./team-onboard.ts";
153
167
  import { handleParallel } from "./team-tool/parallel-dispatch.ts";
154
168
  import { handlePlan } from "./team-tool/plan.ts";
155
169
  import { handleRespond } from "./team-tool/respond.ts";
@@ -1089,6 +1103,97 @@ export async function handleTeamTool(
1089
1103
  return handleHealthMonitor(ctx, params);
1090
1104
  case "wait":
1091
1105
  return handleWait(params, ctx);
1106
+ case "graph": {
1107
+ if (params.runId) {
1108
+ const graph = loadRunGraph(ctx.cwd, params.runId);
1109
+ return result(
1110
+ graph ? JSON.stringify(graph, null, 2) : "No graph found for this run.",
1111
+ { action: "graph", status: graph ? "ok" : "error" },
1112
+ !graph,
1113
+ );
1114
+ }
1115
+ const graphs = listRunGraphs(ctx.cwd);
1116
+ return result(
1117
+ graphs.length ? `Available graphs:\n${graphs.join("\n")}` : "No graphs available.",
1118
+ { action: "graph", status: "ok" },
1119
+ );
1120
+ }
1121
+ case "search": {
1122
+ const query = params.goal ?? params.task ?? "";
1123
+ if (!query) {
1124
+ return result("Search requires goal or task query.", { action: "search", status: "error" }, true);
1125
+ }
1126
+ try {
1127
+ const [agentResults, teamResults] = await Promise.all([
1128
+ searchAgents(query, { limit: 5 }),
1129
+ searchTeams(query, { limit: 3 }),
1130
+ ]);
1131
+ const lines: string[] = [];
1132
+ if (teamResults.length) {
1133
+ lines.push("## Teams");
1134
+ for (const r of teamResults) {
1135
+ lines.push(`- [${r.team.name}] score=${r.score.toFixed(2)}: ${r.team.description ?? "(no description)"}`);
1136
+ }
1137
+ }
1138
+ if (agentResults.length) {
1139
+ lines.push("## Agents");
1140
+ for (const r of agentResults) {
1141
+ lines.push(`- [${r.agent.name}] score=${r.score.toFixed(2)}: ${r.agent.description ?? "(no description)"}`);
1142
+ }
1143
+ }
1144
+ return result(lines.length ? lines.join("\n") : "No results found.", { action: "search", status: "ok" });
1145
+ } catch (err) {
1146
+ const msg = err instanceof Error ? err.message : String(err);
1147
+ return result(`Search failed: ${msg}`, { action: "search", status: "error" }, true);
1148
+ }
1149
+ }
1150
+ case "onboard": {
1151
+ const team = params.team ?? "default";
1152
+ const onboarding = buildTeamOnboarding(team, ctx.cwd);
1153
+ return result(onboarding, { action: "onboard", status: "ok" });
1154
+ }
1155
+ case "explain": {
1156
+ const explainResult = handleExplain(params, ctx.cwd);
1157
+ return result(explainResult.text, { action: "explain", status: explainResult.isError ? "error" : "ok" }, explainResult.isError);
1158
+ }
1159
+ case "cache": {
1160
+ if (params.goal) {
1161
+ const key = computeRunCacheKey(
1162
+ params.goal,
1163
+ params.team ?? "default",
1164
+ params.workflow ?? "default",
1165
+ ctx.cwd,
1166
+ );
1167
+ const cached = getCachedRun(ctx.cwd, key);
1168
+ if (cached) {
1169
+ return result(
1170
+ `Cached run found (${new Date(cached.cachedAt).toISOString()}): runId=${cached.runId}, status=${cached.status}, ${cached.tasks.length} tasks`,
1171
+ { action: "cache", status: "ok", data: { cacheKey: key, cacheHit: true, runId: cached.runId, status: cached.status, taskCount: cached.tasks.length } },
1172
+ );
1173
+ }
1174
+ return result(`No cached result for key: ${key}`, { action: "cache", status: "ok", data: { cacheKey: key, cacheHit: false } });
1175
+ }
1176
+ const stats = getCacheStats(ctx.cwd);
1177
+ return result(
1178
+ `Cache stats: ${stats.entries} entries, ${stats.sizeBytes} bytes`,
1179
+ { action: "cache", status: "ok" },
1180
+ );
1181
+ }
1182
+ case "checkpoint": {
1183
+ if (!params.runId || !params.taskId) {
1184
+ return result("Checkpoint requires runId and taskId.", { action: "checkpoint", status: "error" }, true);
1185
+ }
1186
+ const stateRoot = path.join(projectCrewRoot(ctx.cwd), "state", "runs", params.runId);
1187
+ const store = new FileCheckpointStore(stateRoot);
1188
+ const checkpoint = store.load(params.runId, params.taskId);
1189
+ if (!checkpoint) {
1190
+ return result("No checkpoint found.", { action: "checkpoint", status: "error" }, true);
1191
+ }
1192
+ return result(
1193
+ `Checkpoint: step=${checkpoint.step}, progress=${checkpoint.progress}, savedAt=${new Date(checkpoint.savedAt).toISOString()}`,
1194
+ { action: "checkpoint", status: "ok", data: { checkpoint } },
1195
+ );
1196
+ }
1092
1197
  default:
1093
1198
  return result(
1094
1199
  `Unknown action: ${action}`,