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 +71 -0
- package/package.json +1 -1
- package/src/extension/team-onboard.ts +174 -0
- package/src/extension/team-tool/explain.ts +268 -0
- package/src/extension/team-tool/run.ts +4 -0
- package/src/extension/team-tool.ts +105 -0
- package/src/runtime/checkpoint.ts +232 -0
- package/src/schema/team-tool-schema.ts +13 -1
- package/src/state/crew-init.ts +121 -0
- package/src/state/gitignore-manager.ts +51 -0
- package/src/state/run-cache.ts +176 -0
- package/src/state/run-graph.ts +180 -0
- package/src/utils/bm25-search.ts +207 -0
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
|
@@ -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}`,
|