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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { TeamRunManifest, TeamTaskState } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export interface RunGraphNode {
|
|
6
|
+
id: string;
|
|
7
|
+
type: "run" | "task" | "agent" | "artifact" | "file";
|
|
8
|
+
name: string;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RunGraphEdge {
|
|
13
|
+
source: string;
|
|
14
|
+
target: string;
|
|
15
|
+
type: "dependsOn" | "produces" | "runs" | "contains";
|
|
16
|
+
weight?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RunGraphLayer {
|
|
20
|
+
name: string;
|
|
21
|
+
nodeIds: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunGraph {
|
|
25
|
+
version: "1.0.0";
|
|
26
|
+
runId: string;
|
|
27
|
+
team: string;
|
|
28
|
+
workflow: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
completedAt?: string;
|
|
31
|
+
status: string;
|
|
32
|
+
nodes: RunGraphNode[];
|
|
33
|
+
edges: RunGraphEdge[];
|
|
34
|
+
layers: RunGraphLayer[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a unified run graph from manifest + tasks.
|
|
39
|
+
* Consolidates state into a single graph JSON for dashboard/API use.
|
|
40
|
+
*/
|
|
41
|
+
export function buildRunGraph(
|
|
42
|
+
manifest: TeamRunManifest,
|
|
43
|
+
tasks: TeamTaskState[],
|
|
44
|
+
): RunGraph {
|
|
45
|
+
const nodes: RunGraphNode[] = [];
|
|
46
|
+
const edges: RunGraphEdge[] = [];
|
|
47
|
+
const nodeIds = new Set<string>();
|
|
48
|
+
|
|
49
|
+
// Add run node
|
|
50
|
+
const runId = manifest.runId;
|
|
51
|
+
nodes.push({
|
|
52
|
+
id: `run:${runId}`,
|
|
53
|
+
type: "run",
|
|
54
|
+
name: manifest.goal ?? runId,
|
|
55
|
+
metadata: {
|
|
56
|
+
team: manifest.team,
|
|
57
|
+
workflow: manifest.workflow,
|
|
58
|
+
status: manifest.status,
|
|
59
|
+
createdAt: manifest.createdAt,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
nodeIds.add(`run:${runId}`);
|
|
63
|
+
|
|
64
|
+
// Add task nodes
|
|
65
|
+
for (const task of tasks) {
|
|
66
|
+
const taskId = `task:${task.id}`;
|
|
67
|
+
if (nodeIds.has(taskId)) continue;
|
|
68
|
+
nodeIds.add(taskId);
|
|
69
|
+
|
|
70
|
+
nodes.push({
|
|
71
|
+
id: taskId,
|
|
72
|
+
type: "task",
|
|
73
|
+
name: task.role,
|
|
74
|
+
metadata: {
|
|
75
|
+
status: task.status,
|
|
76
|
+
startedAt: task.startedAt,
|
|
77
|
+
finishedAt: task.finishedAt,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Edge from run to task
|
|
82
|
+
edges.push({
|
|
83
|
+
source: `run:${runId}`,
|
|
84
|
+
target: taskId,
|
|
85
|
+
type: "contains",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Edges from dependencies
|
|
89
|
+
for (const dep of task.dependsOn ?? []) {
|
|
90
|
+
edges.push({
|
|
91
|
+
source: `task:${dep}`,
|
|
92
|
+
target: taskId,
|
|
93
|
+
type: "dependsOn",
|
|
94
|
+
weight: 1.0,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Group by layer (based on phase or role)
|
|
101
|
+
const layerMap = new Map<string, string[]>();
|
|
102
|
+
for (const task of tasks) {
|
|
103
|
+
const layerName = task.adaptive?.phase ?? task.role;
|
|
104
|
+
if (!layerMap.has(layerName)) layerMap.set(layerName, []);
|
|
105
|
+
layerMap.get(layerName)!.push(`task:${task.id}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const layers: RunGraphLayer[] = [...layerMap.entries()].map(([name, nodeIdList]) => ({
|
|
109
|
+
name,
|
|
110
|
+
nodeIds: nodeIdList,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
version: "1.0.0",
|
|
115
|
+
runId,
|
|
116
|
+
team: manifest.team ?? "unknown",
|
|
117
|
+
workflow: manifest.workflow ?? "unknown",
|
|
118
|
+
createdAt: manifest.createdAt,
|
|
119
|
+
completedAt: manifest.updatedAt,
|
|
120
|
+
status: manifest.status,
|
|
121
|
+
nodes,
|
|
122
|
+
edges,
|
|
123
|
+
layers,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Save run graph to disk in .crew/graphs/
|
|
129
|
+
*/
|
|
130
|
+
export function saveRunGraph(graph: RunGraph, cwd: string): string {
|
|
131
|
+
const crewRoot = path.join(cwd, ".crew");
|
|
132
|
+
const graphsDir = path.join(crewRoot, "graphs");
|
|
133
|
+
|
|
134
|
+
if (!fs.existsSync(graphsDir)) {
|
|
135
|
+
fs.mkdirSync(graphsDir, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const graphPath = path.join(graphsDir, `${graph.runId}.json`);
|
|
139
|
+
fs.writeFileSync(graphPath, JSON.stringify(graph, null, 2), "utf-8");
|
|
140
|
+
|
|
141
|
+
return graphPath;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Load run graph from disk.
|
|
146
|
+
*/
|
|
147
|
+
export function loadRunGraph(cwd: string, runId: string): RunGraph | null {
|
|
148
|
+
const graphPath = path.join(cwd, ".crew", "graphs", `${runId}.json`);
|
|
149
|
+
if (!fs.existsSync(graphPath)) return null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(graphPath, "utf-8")) as RunGraph;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* List all archived run graphs.
|
|
160
|
+
*/
|
|
161
|
+
export function listRunGraphs(cwd: string): string[] {
|
|
162
|
+
const graphsDir = path.join(cwd, ".crew", "graphs");
|
|
163
|
+
if (!fs.existsSync(graphsDir)) return [];
|
|
164
|
+
|
|
165
|
+
return fs.readdirSync(graphsDir)
|
|
166
|
+
.filter((f) => f.endsWith(".json"))
|
|
167
|
+
.map((f) => f.replace(/\.json$/, ""));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build and save run graph from manifest + tasks.
|
|
172
|
+
*/
|
|
173
|
+
export function buildAndSaveRunGraph(
|
|
174
|
+
manifest: TeamRunManifest,
|
|
175
|
+
tasks: TeamTaskState[],
|
|
176
|
+
cwd: string,
|
|
177
|
+
): string {
|
|
178
|
+
const graph = buildRunGraph(manifest, tasks);
|
|
179
|
+
return saveRunGraph(graph, cwd);
|
|
180
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
|
+
import type { TeamConfig } from "../teams/team-config.ts";
|
|
3
|
+
|
|
4
|
+
interface SearchDocument {
|
|
5
|
+
id: string;
|
|
6
|
+
fields: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SearchResult<T> {
|
|
10
|
+
item: T;
|
|
11
|
+
score: number;
|
|
12
|
+
matchedOn: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BM25Config {
|
|
16
|
+
k1?: number;
|
|
17
|
+
b?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BM25Search<T extends SearchDocument> {
|
|
21
|
+
private readonly documents: T[];
|
|
22
|
+
private readonly fieldWeights: Record<string, number>;
|
|
23
|
+
private readonly avgDocLen: number;
|
|
24
|
+
private readonly k1: number;
|
|
25
|
+
private readonly b: number;
|
|
26
|
+
private readonly docLenMap: Map<string, number>;
|
|
27
|
+
private readonly N: number;
|
|
28
|
+
|
|
29
|
+
constructor(documents: T[], fieldWeights: Record<string, number> = {}, config: BM25Config = {}) {
|
|
30
|
+
this.documents = documents;
|
|
31
|
+
this.fieldWeights = fieldWeights;
|
|
32
|
+
this.k1 = config.k1 ?? 1.5;
|
|
33
|
+
this.b = config.b ?? 0.75;
|
|
34
|
+
this.N = documents.length;
|
|
35
|
+
|
|
36
|
+
this.docLenMap = new Map();
|
|
37
|
+
|
|
38
|
+
for (const doc of documents) {
|
|
39
|
+
const fieldValues = Object.values(doc.fields).join(" ");
|
|
40
|
+
const len = fieldValues.split(/\s+/).filter(Boolean).length;
|
|
41
|
+
this.docLenMap.set(doc.id, len);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const totalLen = [...this.docLenMap.values()].reduce((a, b) => a + b, 0);
|
|
45
|
+
this.avgDocLen = totalLen / this.N || 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute document frequency for a query term using substring matching,
|
|
50
|
+
* consistent with the regex-based tf computation in search().
|
|
51
|
+
*/
|
|
52
|
+
private df(term: string): number {
|
|
53
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
54
|
+
const re = new RegExp(escaped, "g");
|
|
55
|
+
let count = 0;
|
|
56
|
+
for (const doc of this.documents) {
|
|
57
|
+
for (const field of Object.keys(this.fieldWeights)) {
|
|
58
|
+
const text = (doc.fields[field] ?? "").toLowerCase();
|
|
59
|
+
if (re.test(text)) {
|
|
60
|
+
count++;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return count;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
search(query: string, options?: { limit?: number; minScore?: number }): SearchResult<T>[] {
|
|
69
|
+
const queryTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
70
|
+
if (queryTerms.length === 0) return [];
|
|
71
|
+
|
|
72
|
+
const results: SearchResult<T>[] = [];
|
|
73
|
+
|
|
74
|
+
for (const doc of this.documents) {
|
|
75
|
+
let totalScore = 0;
|
|
76
|
+
const matchedOn: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const [field, weight] of Object.entries(this.fieldWeights)) {
|
|
79
|
+
const text = doc.fields[field] ?? "";
|
|
80
|
+
const textLower = text.toLowerCase();
|
|
81
|
+
let fieldScore = 0;
|
|
82
|
+
|
|
83
|
+
for (const term of queryTerms) {
|
|
84
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
85
|
+
const tf = (textLower.match(new RegExp(escaped, "g")) ?? []).length;
|
|
86
|
+
if (tf === 0) continue;
|
|
87
|
+
|
|
88
|
+
const df = this.df(term);
|
|
89
|
+
if (df === 0) continue;
|
|
90
|
+
|
|
91
|
+
const idf = Math.log((this.N - df + 0.5) / (df + 0.5) + 1);
|
|
92
|
+
const docLen = this.docLenMap.get(doc.id) ?? this.avgDocLen;
|
|
93
|
+
const numerator = tf * (this.k1 + 1);
|
|
94
|
+
const denominator = tf + this.k1 * (1 - this.b + this.b * docLen / this.avgDocLen);
|
|
95
|
+
fieldScore += idf * (numerator / denominator);
|
|
96
|
+
|
|
97
|
+
matchedOn.push(field);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
totalScore += fieldScore * (weight || 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (totalScore > 0) {
|
|
104
|
+
results.push({
|
|
105
|
+
item: doc,
|
|
106
|
+
score: totalScore,
|
|
107
|
+
matchedOn: [...new Set(matchedOn)],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
results.sort((a, b) => b.score - a.score);
|
|
113
|
+
|
|
114
|
+
const limit = options?.limit ?? 10;
|
|
115
|
+
const minScore = options?.minScore ?? 0.01;
|
|
116
|
+
|
|
117
|
+
return results.filter((r) => r.score >= minScore).slice(0, limit);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Agent search interface
|
|
122
|
+
interface AgentSearchResult {
|
|
123
|
+
agent: AgentConfig;
|
|
124
|
+
score: number;
|
|
125
|
+
matchedOn: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Search agents using BM25 ranking.
|
|
130
|
+
* Uses dynamic import to avoid ESM/CJS issues at module load time.
|
|
131
|
+
*/
|
|
132
|
+
export async function searchAgents(query: string, options?: { limit?: number }): Promise<AgentSearchResult[]> {
|
|
133
|
+
const { discoverAgents, allAgents } = await import("../agents/discover-agents.ts");
|
|
134
|
+
const discovery = discoverAgents(process.cwd());
|
|
135
|
+
const all = allAgents(discovery);
|
|
136
|
+
|
|
137
|
+
const docs: (SearchDocument & { agent: AgentConfig })[] = all.map((agent: AgentConfig) => ({
|
|
138
|
+
id: agent.name,
|
|
139
|
+
fields: {
|
|
140
|
+
name: agent.name,
|
|
141
|
+
description: agent.description ?? "",
|
|
142
|
+
skills: (agent.skills ?? []).join(" "),
|
|
143
|
+
},
|
|
144
|
+
agent,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
const engine = new BM25Search(docs, {
|
|
148
|
+
name: 3.0,
|
|
149
|
+
description: 1.5,
|
|
150
|
+
skills: 1.0,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const results = engine.search(query, {
|
|
154
|
+
limit: options?.limit ?? 10,
|
|
155
|
+
minScore: 0.1,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return results.map((r) => ({
|
|
159
|
+
agent: r.item.agent,
|
|
160
|
+
score: r.score,
|
|
161
|
+
matchedOn: r.matchedOn,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Team search interface
|
|
166
|
+
interface TeamSearchResult {
|
|
167
|
+
team: TeamConfig;
|
|
168
|
+
score: number;
|
|
169
|
+
matchedOn: string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Search teams using BM25 ranking.
|
|
174
|
+
* Uses dynamic import to avoid ESM/CJS issues at module load time.
|
|
175
|
+
*/
|
|
176
|
+
export async function searchTeams(query: string, options?: { limit?: number }): Promise<TeamSearchResult[]> {
|
|
177
|
+
const { discoverTeams, allTeams } = await import("../teams/discover-teams.ts");
|
|
178
|
+
const discovery = discoverTeams(process.cwd());
|
|
179
|
+
const all = allTeams(discovery);
|
|
180
|
+
|
|
181
|
+
const docs: (SearchDocument & { team: TeamConfig })[] = all.map((team: TeamConfig) => ({
|
|
182
|
+
id: team.name,
|
|
183
|
+
fields: {
|
|
184
|
+
name: team.name,
|
|
185
|
+
description: team.description ?? "",
|
|
186
|
+
roles: (team.roles ?? []).map((r: { name: string }) => r.name).join(" "),
|
|
187
|
+
},
|
|
188
|
+
team,
|
|
189
|
+
}));
|
|
190
|
+
|
|
191
|
+
const engine = new BM25Search(docs, {
|
|
192
|
+
name: 2.0,
|
|
193
|
+
description: 1.5,
|
|
194
|
+
roles: 1.0,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const results = engine.search(query, {
|
|
198
|
+
limit: options?.limit ?? 5,
|
|
199
|
+
minScore: 0.1,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return results.map((r) => ({
|
|
203
|
+
team: r.item.team,
|
|
204
|
+
score: r.score,
|
|
205
|
+
matchedOn: r.matchedOn,
|
|
206
|
+
}));
|
|
207
|
+
}
|