pi-crew 0.4.0 → 0.5.0
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 +49 -0
- package/package.json +1 -1
- package/src/extension/team-onboard.ts +176 -0
- package/src/extension/team-tool/explain.ts +268 -0
- package/src/extension/team-tool/run.ts +4 -0
- package/src/runtime/checkpoint.ts +232 -0
- 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 +199 -0
- package/src/utils/bm25-search.ts +209 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] — Understand-Anything Patterns & New Features (2026-05-26)
|
|
4
|
+
|
|
5
|
+
### New Features: P0-P6 from Understand-Anything Research
|
|
6
|
+
|
|
7
|
+
#### P0: Auto-Setup .crew Directory
|
|
8
|
+
- `ensureCrewDirectory()` creates full directory structure on first run
|
|
9
|
+
- `gitignore-manager.ts` auto-updates `.gitignore` with `.crew/` entries
|
|
10
|
+
- Creates: `state/runs`, `state/subagents`, `artifacts`, `cache`, `graphs`, `audit`
|
|
11
|
+
- README.md explains `.crew` directory purpose
|
|
12
|
+
|
|
13
|
+
#### P1: BM25 Agent/Team Search
|
|
14
|
+
- `BM25Search` class with configurable k1/b parameters
|
|
15
|
+
- `searchAgents(query)` — ranked agent search by name/description/skills
|
|
16
|
+
- `searchTeams(query)` — ranked team search by name/description/roles
|
|
17
|
+
|
|
18
|
+
#### P2: Team Onboarding Generator
|
|
19
|
+
- `buildTeamOnboarding()` generates markdown from run history
|
|
20
|
+
- Shows: past runs, stats, usage examples, available teams
|
|
21
|
+
- `loadRunSummaries()` helper for run history loading
|
|
22
|
+
|
|
23
|
+
#### P3: Task Explain Context
|
|
24
|
+
- `handleExplain(runId, taskId)` — full run or individual task explanation
|
|
25
|
+
- `buildTaskExplainContext()` — causal chain, layers, files produced
|
|
26
|
+
- `formatTaskExplain()` — markdown output with why/what/connections
|
|
27
|
+
|
|
28
|
+
#### P4: Unified Run Graph
|
|
29
|
+
- `buildRunGraph()` — consolidates manifest + tasks into single graph
|
|
30
|
+
- `saveRunGraph()` / `loadRunGraph()` — persist to `.crew/graphs/`
|
|
31
|
+
- `listRunGraphs()` — enumerate archived graphs
|
|
32
|
+
|
|
33
|
+
#### P5: Run Result Caching
|
|
34
|
+
- `computeRunCacheKey()` — SHA-256 hash of goal+team+workflow
|
|
35
|
+
- `getCachedRun()` / `saveRunToCache()` — TTL-based cache (default 1h)
|
|
36
|
+
- `clearCache()` / `getCacheStats()` — cache management
|
|
37
|
+
|
|
38
|
+
#### P6: Agent Checkpointing
|
|
39
|
+
- `FileCheckpointStore` — checkpoints in `.crew/state/runs/<runId>/checkpoints/`
|
|
40
|
+
- `saveCheckpoint()` / `loadCheckpoint()` / `clearCheckpoint()`
|
|
41
|
+
- `hasCheckpoint()` / `listCheckpoints()` for recovery
|
|
42
|
+
|
|
43
|
+
### Tests
|
|
44
|
+
- 56 new unit tests (all passing)
|
|
45
|
+
- Total: 1796 unit tests + 45 integration tests passing
|
|
46
|
+
|
|
47
|
+
### Bug Fixes
|
|
48
|
+
- Worktree test teardown: clean `.crew/` before git checks for clean repository
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
3
52
|
## [0.4.0] — 9arm-skills Enforcement Patterns & Integration Tests (2026-05-26)
|
|
4
53
|
|
|
5
54
|
### Features
|
package/package.json
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
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: (raw as Record<string, unknown>).tasks != null
|
|
68
|
+
? ((raw as Record<string, unknown>).tasks as unknown[]).length
|
|
69
|
+
: 0,
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return summaries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format duration in minutes.
|
|
81
|
+
*/
|
|
82
|
+
function formatDuration(createdAt: string, completedAt?: string): string {
|
|
83
|
+
const start = new Date(createdAt).getTime();
|
|
84
|
+
const end = completedAt ? new Date(completedAt).getTime() : Date.now();
|
|
85
|
+
const minutes = Math.round((end - start) / 1000 / 60);
|
|
86
|
+
if (minutes < 1) return "<1m";
|
|
87
|
+
if (minutes >= 60) return `${Math.round(minutes / 60)}h`;
|
|
88
|
+
return `${minutes}m`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build markdown onboarding guide for a team.
|
|
93
|
+
*/
|
|
94
|
+
export function buildTeamOnboarding(team: string, cwd: string, options: OnboardingOptions = {}): string {
|
|
95
|
+
const runs = loadRunSummaries(cwd, { ...options, team });
|
|
96
|
+
|
|
97
|
+
const lines: string[] = [];
|
|
98
|
+
|
|
99
|
+
// Header
|
|
100
|
+
lines.push(`# Team: ${team}`);
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`> Multi-agent ${team} team for pi-crew.`);
|
|
103
|
+
lines.push("");
|
|
104
|
+
|
|
105
|
+
// Overview stats
|
|
106
|
+
const completed = runs.filter((r) => r.status === "completed");
|
|
107
|
+
const failed = runs.filter((r) => r.status === "failed" || r.status === "cancelled");
|
|
108
|
+
|
|
109
|
+
let avgDuration = 0;
|
|
110
|
+
if (completed.length > 0) {
|
|
111
|
+
const totalMs = completed.reduce((sum, r) => {
|
|
112
|
+
const start = new Date(r.createdAt).getTime();
|
|
113
|
+
const end = r.completedAt ? new Date(r.completedAt).getTime() : Date.now();
|
|
114
|
+
return sum + (end - start);
|
|
115
|
+
}, 0);
|
|
116
|
+
avgDuration = totalMs / completed.length / 1000 / 60;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
lines.push("## Overview");
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push(`| Metric | Value |`);
|
|
122
|
+
lines.push(`|--------|-------|`);
|
|
123
|
+
lines.push(`| Total runs | ${runs.length} |`);
|
|
124
|
+
lines.push(`| Completed | ${completed.length} |`);
|
|
125
|
+
lines.push(`| Failed/Cancelled | ${failed.length} |`);
|
|
126
|
+
if (avgDuration > 0) {
|
|
127
|
+
lines.push(`| Avg duration | ${avgDuration.toFixed(1)} min |`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
|
|
131
|
+
// Past runs table
|
|
132
|
+
if (runs.length > 0) {
|
|
133
|
+
lines.push("## Past Runs");
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push("| Run | Goal | Duration | Status |");
|
|
136
|
+
lines.push("|-----|------|----------|--------|");
|
|
137
|
+
|
|
138
|
+
for (const run of runs) {
|
|
139
|
+
const duration = formatDuration(run.createdAt, run.completedAt);
|
|
140
|
+
const goalPreview = run.goal ? run.goal.slice(0, 40) : "N/A";
|
|
141
|
+
const statusIcon = run.status === "completed" ? "✅" : run.status === "failed" ? "❌" : "⚠️";
|
|
142
|
+
lines.push(`| \`${run.runId.slice(-8)}\` | ${goalPreview}${run.goal.length > 40 ? "..." : ""} | ${duration} | ${statusIcon} ${run.status} |`);
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// How to run
|
|
148
|
+
lines.push("## How to Run");
|
|
149
|
+
lines.push("");
|
|
150
|
+
lines.push("```bash");
|
|
151
|
+
lines.push(`team action='run' team='${team}' goal="<your goal>"`);
|
|
152
|
+
lines.push("```");
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push("See `team action='help'` for more options.");
|
|
155
|
+
lines.push("");
|
|
156
|
+
|
|
157
|
+
// Teams
|
|
158
|
+
lines.push("## Available Teams");
|
|
159
|
+
lines.push("");
|
|
160
|
+
try {
|
|
161
|
+
const discovery = discoverTeams(cwd);
|
|
162
|
+
const teams = allTeams(discovery);
|
|
163
|
+
for (const t of teams) {
|
|
164
|
+
lines.push(`- \`${t.name}\` — ${t.description ?? "No description"}`);
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
lines.push("- *(team discovery unavailable)*");
|
|
168
|
+
}
|
|
169
|
+
lines.push("");
|
|
170
|
+
|
|
171
|
+
// Footer
|
|
172
|
+
lines.push("---");
|
|
173
|
+
lines.push("*Generated by pi-crew. For up-to-date info, check recent run history.*");
|
|
174
|
+
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
@@ -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));
|