pi-crew 0.3.9 → 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 +72 -0
- package/package.json +1 -1
- package/skills/REFERENCE.md +136 -0
- package/skills/delegation-patterns/SKILL.md +1 -1
- package/skills/event-log-tracing/SKILL.md +1 -1
- package/skills/multi-perspective-review/SKILL.md +17 -1
- package/skills/orchestration/SKILL.md +1 -1
- package/skills/post-mortem/SKILL.md +90 -0
- package/skills/safe-bash/SKILL.md +1 -1
- package/skills/scrutinize/SKILL.md +67 -0
- package/skills/systematic-debugging/SKILL.md +60 -5
- package/skills/verification-before-done/SKILL.md +1 -1
- package/skills/workspace-isolation/SKILL.md +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/test-integration-check.ts +114 -0
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
tags: (agent.tags ?? []).join(" "),
|
|
144
|
+
},
|
|
145
|
+
agent,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const engine = new BM25Search(docs, {
|
|
149
|
+
name: 3.0,
|
|
150
|
+
description: 1.5,
|
|
151
|
+
skills: 1.0,
|
|
152
|
+
tags: 1.0,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const results = engine.search(query, {
|
|
156
|
+
limit: options?.limit ?? 10,
|
|
157
|
+
minScore: 0.1,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return results.map((r) => ({
|
|
161
|
+
agent: r.item.agent,
|
|
162
|
+
score: r.score,
|
|
163
|
+
matchedOn: r.matchedOn,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Team search interface
|
|
168
|
+
interface TeamSearchResult {
|
|
169
|
+
team: TeamConfig;
|
|
170
|
+
score: number;
|
|
171
|
+
matchedOn: string[];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Search teams using BM25 ranking.
|
|
176
|
+
* Uses dynamic import to avoid ESM/CJS issues at module load time.
|
|
177
|
+
*/
|
|
178
|
+
export async function searchTeams(query: string, options?: { limit?: number }): Promise<TeamSearchResult[]> {
|
|
179
|
+
const { discoverTeams, allTeams } = await import("../teams/discover-teams.ts");
|
|
180
|
+
const discovery = discoverTeams(process.cwd());
|
|
181
|
+
const all = allTeams(discovery);
|
|
182
|
+
|
|
183
|
+
const docs: (SearchDocument & { team: TeamConfig })[] = all.map((team: TeamConfig) => ({
|
|
184
|
+
id: team.name,
|
|
185
|
+
fields: {
|
|
186
|
+
name: team.name,
|
|
187
|
+
description: team.description ?? "",
|
|
188
|
+
roles: (team.roles ?? []).map((r: { name: string }) => r.name).join(" "),
|
|
189
|
+
},
|
|
190
|
+
team,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
const engine = new BM25Search(docs, {
|
|
194
|
+
name: 2.0,
|
|
195
|
+
description: 1.5,
|
|
196
|
+
roles: 1.0,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const results = engine.search(query, {
|
|
200
|
+
limit: options?.limit ?? 5,
|
|
201
|
+
minScore: 0.1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return results.map((r) => ({
|
|
205
|
+
team: r.item.team,
|
|
206
|
+
score: r.score,
|
|
207
|
+
matchedOn: r.matchedOn,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration check: validates pi-crew core discovery and team-run functionality.
|
|
3
|
+
* Run with: node --experimental-strip-types --test test-integration-check.ts
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
|
|
11
|
+
import { discoverAgents, allAgents } from "./src/agents/discover-agents.ts";
|
|
12
|
+
import { discoverTeams, allTeams } from "./src/teams/discover-teams.ts";
|
|
13
|
+
import { discoverWorkflows, allWorkflows } from "./src/workflows/discover-workflows.ts";
|
|
14
|
+
import { handleTeamTool } from "./src/extension/team-tool.ts";
|
|
15
|
+
import { loadRunManifestById } from "./src/state/state-store.ts";
|
|
16
|
+
|
|
17
|
+
const pkgRoot = path.resolve(import.meta.dirname ?? ".");
|
|
18
|
+
|
|
19
|
+
// ── Discovery tests ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
test("discovers builtin agents", () => {
|
|
22
|
+
const discovery = discoverAgents(pkgRoot);
|
|
23
|
+
assert.ok(discovery, "discoverAgents should return a result");
|
|
24
|
+
assert.ok(
|
|
25
|
+
discovery.builtin.length >= 10,
|
|
26
|
+
`Expected ≥10 builtin agents, got ${discovery.builtin.length}`,
|
|
27
|
+
);
|
|
28
|
+
const all = allAgents(discovery);
|
|
29
|
+
const names = all.map((a) => a.name);
|
|
30
|
+
assert.ok(names.includes("executor"), `Missing "executor" agent. Got: ${names.join(", ")}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("discovers builtin teams", () => {
|
|
34
|
+
const discovery = discoverTeams(pkgRoot);
|
|
35
|
+
assert.ok(discovery, "discoverTeams should return a result");
|
|
36
|
+
assert.ok(
|
|
37
|
+
discovery.builtin.length >= 6,
|
|
38
|
+
`Expected ≥6 builtin teams, got ${discovery.builtin.length}`,
|
|
39
|
+
);
|
|
40
|
+
const all = allTeams(discovery);
|
|
41
|
+
const names = all.map((t) => t.name);
|
|
42
|
+
assert.ok(names.includes("fast-fix"), `Missing "fast-fix" team. Got: ${names.join(", ")}`);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("discovers builtin workflows", () => {
|
|
46
|
+
const discovery = discoverWorkflows(pkgRoot);
|
|
47
|
+
assert.ok(discovery, "discoverWorkflows should return a result");
|
|
48
|
+
assert.ok(
|
|
49
|
+
discovery.builtin.length >= 6,
|
|
50
|
+
`Expected ≥6 builtin workflows, got ${discovery.builtin.length}`,
|
|
51
|
+
);
|
|
52
|
+
const all = allWorkflows(discovery);
|
|
53
|
+
const names = all.map((w) => w.name);
|
|
54
|
+
assert.ok(
|
|
55
|
+
names.includes("fast-fix"),
|
|
56
|
+
`Missing "fast-fix" workflow. Got: ${names.join(", ")}`,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ── Team run test ─────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
test("fast-fix team run completes successfully with mock child Pi", async () => {
|
|
63
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-int-check-"));
|
|
64
|
+
fs.mkdirSync(path.join(cwd, ".crew"), { recursive: true });
|
|
65
|
+
|
|
66
|
+
const prevExec = process.env.PI_TEAMS_EXECUTE_WORKERS;
|
|
67
|
+
const prevMock = process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
68
|
+
process.env.PI_TEAMS_EXECUTE_WORKERS = "1";
|
|
69
|
+
process.env.PI_TEAMS_MOCK_CHILD_PI = "success";
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const run = await handleTeamTool(
|
|
73
|
+
{ action: "run", team: "fast-fix", goal: "create a hello.txt file" },
|
|
74
|
+
{ cwd },
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// run result is not an error
|
|
78
|
+
assert.equal(run.isError, false, `handleTeamTool returned error: ${JSON.stringify(run)}`);
|
|
79
|
+
|
|
80
|
+
const runId = run.details.runId;
|
|
81
|
+
assert.ok(runId, "Expected a runId in details");
|
|
82
|
+
|
|
83
|
+
// manifest should be persisted and completed
|
|
84
|
+
const loaded = loadRunManifestById(cwd, runId!);
|
|
85
|
+
assert.ok(loaded, "loadRunManifestById should return data");
|
|
86
|
+
assert.equal(
|
|
87
|
+
loaded!.manifest.status,
|
|
88
|
+
"completed",
|
|
89
|
+
`Expected manifest status "completed", got "${loaded!.manifest.status}"`,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// all tasks should be completed
|
|
93
|
+
const taskStatuses = loaded!.tasks.map((t) => t.status);
|
|
94
|
+
assert.ok(
|
|
95
|
+
taskStatuses.every((s) => s === "completed"),
|
|
96
|
+
`Not all tasks completed: ${JSON.stringify(taskStatuses)}`,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// artifacts directory should exist
|
|
100
|
+
const artifactsDir = path.join(cwd, ".crew", "artifacts", runId!);
|
|
101
|
+
assert.ok(
|
|
102
|
+
fs.existsSync(artifactsDir),
|
|
103
|
+
`Artifacts directory should exist: ${artifactsDir}`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
console.log(`✅ fast-fix run ${runId} completed successfully with ${loaded!.tasks.length} tasks`);
|
|
107
|
+
} finally {
|
|
108
|
+
if (prevExec === undefined) delete process.env.PI_TEAMS_EXECUTE_WORKERS;
|
|
109
|
+
else process.env.PI_TEAMS_EXECUTE_WORKERS = prevExec;
|
|
110
|
+
if (prevMock === undefined) delete process.env.PI_TEAMS_MOCK_CHILD_PI;
|
|
111
|
+
else process.env.PI_TEAMS_MOCK_CHILD_PI = prevMock;
|
|
112
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|