pi-crew 0.9.26 → 0.9.28
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 +102 -0
- package/NOTICE.md +14 -0
- package/assets/crew-vibes.ttf +0 -0
- package/assets/runner-spritesheet.png +0 -0
- package/dist/build-meta.json +245 -48
- package/dist/index.mjs +1611 -243
- package/dist/index.mjs.map +4 -4
- package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
- package/package.json +9 -2
- package/scripts/bench-check.mjs +96 -0
- package/scripts/bench-cold-start.mjs +216 -0
- package/scripts/build-bundle.mjs +84 -0
- package/scripts/build-crew-vibes-font.py +328 -0
- package/scripts/check-all-skills.ts +294 -0
- package/scripts/check-bundle-staleness.mjs +97 -0
- package/scripts/check-conflict-markers.mjs +91 -0
- package/scripts/check-lazy-imports.mjs +28 -0
- package/scripts/install-crew-vibes-font.mjs +91 -0
- package/scripts/postinstall.mjs +47 -0
- package/scripts/profile-startup.mjs +125 -0
- package/scripts/release-smoke.mjs +74 -0
- package/scripts/run-bench.mjs +47 -0
- package/scripts/run-real-chain.ts +41 -0
- package/scripts/test-issue-29-crash.ts +111 -0
- package/scripts/test-issue-29-e2e.ts +183 -0
- package/scripts/test-issue-29-real-runtime.ts +330 -0
- package/scripts/test-issue-29-real-tasks.ts +387 -0
- package/scripts/test-issue-29-team-tool.ts +105 -0
- package/scripts/test-runner.mjs +74 -0
- package/scripts/verify-flicker-fix.ts +109 -0
- package/scripts/verify-skill.ts +550 -0
- package/scripts/watch-bundle.mjs +259 -0
- package/scripts/watchdog-harness.ts +119 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/discover-agents.ts +10 -1
- package/src/config/defaults.ts +6 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +94 -0
- package/src/extension/crew-vibes/font-detect.ts +58 -0
- package/src/extension/crew-vibes/index.ts +385 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/knowledge-injection.ts +12 -3
- package/src/extension/management.ts +23 -3
- package/src/extension/register.ts +7 -0
- package/src/extension/team-tool.ts +11 -0
- package/src/prompt/prompt-runtime.ts +65 -0
- package/src/runtime/child-pi.ts +123 -10
- package/src/runtime/crew-agent-records.ts +10 -3
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/retry-executor.ts +4 -1
- package/src/runtime/task-runner/state-helpers.ts +9 -30
- package/src/runtime/task-runner.ts +1 -0
- package/src/runtime/team-runner.ts +5 -3
- package/src/state/atomic-write.ts +153 -49
- package/src/state/event-log.ts +16 -10
- package/src/state/locks.ts +7 -8
- package/src/state/mailbox.ts +15 -4
- package/src/state/state-store.ts +39 -10
- package/src/teams/discover-teams.ts +56 -1
- package/src/utils/safe-paths.ts +2 -1
- package/src/workflows/discover-workflows.ts +72 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* REAL-RUNTIME E2E test for issue #29.
|
|
4
|
+
*
|
|
5
|
+
* This is the most realistic test possible without invoking the full
|
|
6
|
+
* `pi` agent loop. It:
|
|
7
|
+
* 1. Sets up a .pi/-only project (no .crew/).
|
|
8
|
+
* 2. Creates a real team run manifest via createRunManifest().
|
|
9
|
+
* 3. Calls spawnBackgroundTeamRun() which spawns a REAL detached
|
|
10
|
+
* background-runner.ts process (the same path used by `team action='run'`).
|
|
11
|
+
* 4. Waits for the background runner to write background.log and exit.
|
|
12
|
+
* 5. Verifies EVERY path the fix touched:
|
|
13
|
+
* - logPath is at .pi/teams/state/runs/<runId>/background.log
|
|
14
|
+
* - exit-code.txt is at .pi/teams/state/runs/<runId>/exit-code.txt
|
|
15
|
+
* - manifest.json is at .pi/teams/state/runs/<runId>/manifest.json
|
|
16
|
+
* - tasks.json is at .pi/teams/state/runs/<runId>/tasks.json
|
|
17
|
+
* - events.jsonl is at .pi/teams/state/runs/<runId>/events.jsonl
|
|
18
|
+
* 6. Verifies NO files leak to .crew/state/runs/...
|
|
19
|
+
* 7. Captures any uncaughtException/unhandledRejection from the test
|
|
20
|
+
* process (which would indicate the bug fires).
|
|
21
|
+
*
|
|
22
|
+
* The test uses PI_CREW_EXECUTE_WORKERS=false so the team-runner uses
|
|
23
|
+
* "scaffold" runtime (no child Pi agents spawned), but the background-
|
|
24
|
+
* runner process itself is REAL — it actually runs, writes to disk, and
|
|
25
|
+
* exits. This exercises all 11 fixed sites:
|
|
26
|
+
* - background-runner.ts:139 (log redirect path)
|
|
27
|
+
* - background-runner.ts:172 (exit-code path)
|
|
28
|
+
* - run-tracker.ts:82 (waitForRun slow path early-exit)
|
|
29
|
+
* - checkpoint.ts:166,177,188,199,209 (5 sites)
|
|
30
|
+
* - skill-effectiveness.ts:115,125 (when tasks complete)
|
|
31
|
+
* - decision-ledger.ts:29 (when rollouts happen)
|
|
32
|
+
*
|
|
33
|
+
* Run with: npx tsx scripts/test-issue-29-real-runtime.ts
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import * as fs from "node:fs";
|
|
37
|
+
import * as os from "node:os";
|
|
38
|
+
import * as path from "node:path";
|
|
39
|
+
import { fileURLToPath } from "node:url";
|
|
40
|
+
|
|
41
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-crew-issue-29-runtime-"));
|
|
42
|
+
fs.mkdirSync(path.join(tmpDir, ".pi"), { recursive: true });
|
|
43
|
+
// CRITICALLY: no .crew/
|
|
44
|
+
|
|
45
|
+
console.log("=".repeat(72));
|
|
46
|
+
console.log("Issue #29 REAL-RUNTIME E2E test");
|
|
47
|
+
console.log(" (spawns REAL detached background-runner.ts process)");
|
|
48
|
+
console.log("=".repeat(72));
|
|
49
|
+
console.log(`Project: ${tmpDir}`);
|
|
50
|
+
console.log(` Has .pi/ : ${fs.existsSync(path.join(tmpDir, ".pi"))}`);
|
|
51
|
+
console.log(` Has .crew/: ${fs.existsSync(path.join(tmpDir, ".crew"))} (should be false)`);
|
|
52
|
+
console.log();
|
|
53
|
+
|
|
54
|
+
// ── Crash detection ──────────────────────────────────────────────────────
|
|
55
|
+
let crashed = false;
|
|
56
|
+
let crashError: Error | undefined;
|
|
57
|
+
process.on("uncaughtException", (error) => {
|
|
58
|
+
crashed = true;
|
|
59
|
+
crashError = error;
|
|
60
|
+
console.error(`[CRASH] uncaughtException: ${error.message}`);
|
|
61
|
+
});
|
|
62
|
+
process.on("unhandledRejection", (reason) => {
|
|
63
|
+
crashed = true;
|
|
64
|
+
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
65
|
+
crashError = err;
|
|
66
|
+
console.error(`[CRASH] unhandledRejection: ${err.message}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Force placeholder workers in the background runner (no LLM needed).
|
|
70
|
+
// The runtime resolver checks for "0" (string) to disable child worker execution.
|
|
71
|
+
process.env.PI_CREW_EXECUTE_WORKERS = "0";
|
|
72
|
+
process.env.PI_TEAMS_EXECUTE_WORKERS = "0";
|
|
73
|
+
// We also need to set runConfig.runtime.mode = "scaffold" in the manifest
|
|
74
|
+
// because the background-runner explicitly blocks PI_CREW_EXECUTE_WORKERS
|
|
75
|
+
// from leaking to the child (security feature).
|
|
76
|
+
|
|
77
|
+
const piCrewRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
78
|
+
let pass = 0;
|
|
79
|
+
let fail = 0;
|
|
80
|
+
const failures: string[] = [];
|
|
81
|
+
|
|
82
|
+
function check(label: string, condition: boolean, detail = ""): void {
|
|
83
|
+
if (condition) {
|
|
84
|
+
pass++;
|
|
85
|
+
console.log(` ✓ ${label}`);
|
|
86
|
+
} else {
|
|
87
|
+
fail++;
|
|
88
|
+
failures.push(label);
|
|
89
|
+
console.error(` ✗ ${label}${detail ? ` — ${detail}` : ""}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function sleep(ms: number): Promise<void> {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function waitForFile(filePath: string, timeoutMs: number): Promise<boolean> {
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
while (Date.now() - start < timeoutMs) {
|
|
100
|
+
if (fs.existsSync(filePath)) return true;
|
|
101
|
+
await sleep(100);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main(): Promise<void> {
|
|
107
|
+
const { createRunManifest } = await import(path.join(piCrewRoot, "src/state/state-store.ts"));
|
|
108
|
+
const { discoverTeams, allTeams } = await import(path.join(piCrewRoot, "src/teams/discover-teams.ts"));
|
|
109
|
+
const { discoverWorkflows, allWorkflows } = await import(path.join(piCrewRoot, "src/workflows/discover-workflows.ts"));
|
|
110
|
+
const { spawnBackgroundTeamRun } = await import(path.join(piCrewRoot, "src/runtime/async-runner.ts"));
|
|
111
|
+
const { projectCrewRoot, clearProjectRootCache } = await import(path.join(piCrewRoot, "src/utils/paths.ts"));
|
|
112
|
+
const { loadRunManifestById } = await import(path.join(piCrewRoot, "src/state/state-store.ts"));
|
|
113
|
+
|
|
114
|
+
clearProjectRootCache();
|
|
115
|
+
const root = projectCrewRoot(tmpDir);
|
|
116
|
+
check(`projectCrewRoot returns .pi/teams/`, root === path.join(tmpDir, ".pi", "teams"));
|
|
117
|
+
|
|
118
|
+
// ── Set up a team run ─────────────────────────────────────────────
|
|
119
|
+
console.log();
|
|
120
|
+
console.log("Step 1: Create team run manifest");
|
|
121
|
+
console.log("-".repeat(72));
|
|
122
|
+
|
|
123
|
+
const teams = allTeams(discoverTeams(tmpDir));
|
|
124
|
+
const workflows = allWorkflows(discoverWorkflows(tmpDir));
|
|
125
|
+
const team = teams.find((t) => t.name === "fast-fix");
|
|
126
|
+
const workflow = workflows.find((w) => w.name === "fast-fix");
|
|
127
|
+
check("fast-fix team discovered", Boolean(team));
|
|
128
|
+
check("fast-fix workflow discovered", Boolean(workflow));
|
|
129
|
+
|
|
130
|
+
if (!team || !workflow) {
|
|
131
|
+
console.error("Cannot continue without team/workflow");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { manifest } = createRunManifest({
|
|
136
|
+
cwd: tmpDir,
|
|
137
|
+
team,
|
|
138
|
+
workflow,
|
|
139
|
+
goal: "REAL RUNTIME test: spawn background runner, verify paths land in .pi/teams/state/runs/",
|
|
140
|
+
workspaceMode: "single",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Force scaffold mode so no child Pi workers are spawned (no LLM needed).
|
|
144
|
+
// This is what the background-runner will use.
|
|
145
|
+
const scaffoldRuntimeResolution = {
|
|
146
|
+
kind: "scaffold" as const,
|
|
147
|
+
requestedMode: "scaffold" as const,
|
|
148
|
+
available: true,
|
|
149
|
+
reason: "Test: forced scaffold mode for E2E without LLM",
|
|
150
|
+
resolvedAt: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
const scaffoldRunConfig = {
|
|
153
|
+
...((manifest as { runConfig?: unknown }).runConfig ?? {}),
|
|
154
|
+
executeWorkers: false,
|
|
155
|
+
runtime: {
|
|
156
|
+
...(((manifest as { runConfig?: { runtime?: unknown } }).runConfig?.runtime ?? {}) as Record<string, unknown>),
|
|
157
|
+
mode: "scaffold",
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const manifestWithRuntime = {
|
|
161
|
+
...manifest,
|
|
162
|
+
runtimeResolution: scaffoldRuntimeResolution,
|
|
163
|
+
runConfig: scaffoldRunConfig,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Override status to queued (createRunManifest sets it, but let's be explicit)
|
|
167
|
+
const queuedManifest = {
|
|
168
|
+
...manifestWithRuntime,
|
|
169
|
+
status: "queued" as const,
|
|
170
|
+
};
|
|
171
|
+
console.log(` Created run: ${queuedManifest.runId}`);
|
|
172
|
+
console.log(` stateRoot: ${queuedManifest.stateRoot}`);
|
|
173
|
+
console.log(` runtime: ${queuedManifest.runtimeResolution?.kind ?? "(default)"}`);
|
|
174
|
+
|
|
175
|
+
check(
|
|
176
|
+
`manifest.stateRoot is under .pi/teams/state/runs/`,
|
|
177
|
+
queuedManifest.stateRoot === path.join(tmpDir, ".pi", "teams", "state", "runs", queuedManifest.runId),
|
|
178
|
+
`got: ${queuedManifest.stateRoot}`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// ── Step 2: Spawn REAL background runner ──────────────────────────
|
|
182
|
+
console.log();
|
|
183
|
+
console.log("Step 2: spawnBackgroundTeamRun() — spawn REAL detached process");
|
|
184
|
+
console.log("-".repeat(72));
|
|
185
|
+
|
|
186
|
+
// Pass the queued manifest (background runner expects this format)
|
|
187
|
+
// We also need to write the manifest to disk so the runner can read it.
|
|
188
|
+
const stateDir = queuedManifest.stateRoot;
|
|
189
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
190
|
+
fs.writeFileSync(path.join(stateDir, "manifest.json"), JSON.stringify(queuedManifest, null, 2));
|
|
191
|
+
|
|
192
|
+
const tStart = Date.now();
|
|
193
|
+
const spawnResult = await spawnBackgroundTeamRun(queuedManifest);
|
|
194
|
+
console.log(` Spawned background runner`);
|
|
195
|
+
console.log(` PID: ${spawnResult.pid ?? "(unknown)"}`);
|
|
196
|
+
console.log(` logPath: ${spawnResult.logPath}`);
|
|
197
|
+
console.log();
|
|
198
|
+
|
|
199
|
+
// ── Step 3: Verify the logPath is in the CORRECT location ─────────
|
|
200
|
+
// This is the path the background-runner.ts writes to (line 139).
|
|
201
|
+
check(
|
|
202
|
+
`logPath is under .pi/teams/state/runs/<runId>/background.log`,
|
|
203
|
+
spawnResult.logPath === path.join(stateDir, "background.log"),
|
|
204
|
+
`got: ${spawnResult.logPath}`,
|
|
205
|
+
);
|
|
206
|
+
check(`logPath is NOT under .crew/state/runs/...`, !spawnResult.logPath.includes(".crew/state/runs"));
|
|
207
|
+
|
|
208
|
+
// ── Step 4: Wait for the background runner to finish ──────────────
|
|
209
|
+
console.log();
|
|
210
|
+
console.log("Step 3: Wait for background runner to write logs and exit");
|
|
211
|
+
console.log("-".repeat(72));
|
|
212
|
+
|
|
213
|
+
// Wait for background.log to exist
|
|
214
|
+
const logWritten = await waitForFile(spawnResult.logPath, 30_000);
|
|
215
|
+
if (!logWritten) {
|
|
216
|
+
fail++;
|
|
217
|
+
failures.push("background.log was created within 30s");
|
|
218
|
+
console.error(" ✗ background.log was not created within 30s");
|
|
219
|
+
} else {
|
|
220
|
+
console.log(` ✓ background.log created at ${spawnResult.logPath}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Wait for exit-code.txt (background-runner.ts:172 writes this on exit)
|
|
224
|
+
const exitCodePath = path.join(stateDir, "exit-code.txt");
|
|
225
|
+
const exitCodeWritten = await waitForFile(exitCodePath, 30_000);
|
|
226
|
+
if (!exitCodeWritten) {
|
|
227
|
+
fail++;
|
|
228
|
+
failures.push("exit-code.txt was created within 30s");
|
|
229
|
+
console.error(" ✗ exit-code.txt was not created within 30s");
|
|
230
|
+
} else {
|
|
231
|
+
const elapsed = Date.now() - tStart;
|
|
232
|
+
console.log(` ✓ exit-code.txt created in ${elapsed}ms`);
|
|
233
|
+
const exitCode = fs.readFileSync(exitCodePath, "utf-8").trim();
|
|
234
|
+
console.log(` Contents: ${JSON.stringify(exitCode.slice(0, 200))}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Wait for the process to actually exit
|
|
238
|
+
let processExited = false;
|
|
239
|
+
for (let i = 0; i < 50; i++) {
|
|
240
|
+
if (spawnResult.pid !== undefined) {
|
|
241
|
+
try {
|
|
242
|
+
process.kill(spawnResult.pid, 0); // Check if alive
|
|
243
|
+
await sleep(200);
|
|
244
|
+
} catch {
|
|
245
|
+
processExited = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// No PID — assume the runner already exited (synchronously)
|
|
250
|
+
processExited = true;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
check(`background runner process exited (PID ${spawnResult.pid})`, processExited);
|
|
255
|
+
|
|
256
|
+
// ── Step 5: Verify NO files leaked to .crew/ ──────────────────────
|
|
257
|
+
console.log();
|
|
258
|
+
console.log("Step 4: Verify no .crew/ files were created");
|
|
259
|
+
console.log("-".repeat(72));
|
|
260
|
+
|
|
261
|
+
const crewDir = path.join(tmpDir, ".crew");
|
|
262
|
+
check(`no .crew/ directory was created (would indicate fallback)`, !fs.existsSync(crewDir), `found at: ${crewDir}`);
|
|
263
|
+
|
|
264
|
+
// Also check that all the expected files are under .pi/teams/
|
|
265
|
+
const expectedFiles = ["manifest.json", "tasks.json", "events.jsonl", "background.log", "exit-code.txt"];
|
|
266
|
+
for (const f of expectedFiles) {
|
|
267
|
+
const p = path.join(stateDir, f);
|
|
268
|
+
check(`${f} exists at .pi/teams/state/runs/<runId>/${f}`, fs.existsSync(p));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Step 6: Verify the runner wrote meaningful content ───────────
|
|
272
|
+
console.log();
|
|
273
|
+
console.log("Step 5: Verify background runner output");
|
|
274
|
+
console.log("-".repeat(72));
|
|
275
|
+
|
|
276
|
+
if (logWritten) {
|
|
277
|
+
const logContent = fs.readFileSync(spawnResult.logPath, "utf-8");
|
|
278
|
+
const lineCount = logContent.split("\n").filter((l) => l.trim()).length;
|
|
279
|
+
check(`background.log has content (${lineCount} non-empty lines)`, lineCount > 0);
|
|
280
|
+
console.log(` First 200 chars: ${JSON.stringify(logContent.slice(0, 200))}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Verify the manifest was updated by the runner
|
|
284
|
+
const reloaded = loadRunManifestById(tmpDir, queuedManifest.runId);
|
|
285
|
+
check(`manifest can be reloaded after runner completed`, Boolean(reloaded));
|
|
286
|
+
if (reloaded) {
|
|
287
|
+
console.log(` Final status: ${reloaded.manifest.status}`);
|
|
288
|
+
check(
|
|
289
|
+
`final status is completed/failed/cancelled (not 'running')`,
|
|
290
|
+
["completed", "failed", "cancelled"].includes(reloaded.manifest.status),
|
|
291
|
+
`got: ${reloaded.manifest.status}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Final verdict ───────────────────────────────────────────────────
|
|
296
|
+
console.log();
|
|
297
|
+
console.log("=".repeat(72));
|
|
298
|
+
console.log(`Results: ${pass} passed, ${fail} failed, ${crashed ? "1" : "0"} crashed`);
|
|
299
|
+
if (failures.length > 0) {
|
|
300
|
+
console.error("Failures:");
|
|
301
|
+
for (const f of failures) console.error(` - ${f}`);
|
|
302
|
+
}
|
|
303
|
+
if (crashed) {
|
|
304
|
+
console.error(`Crashed: ${crashError?.message}`);
|
|
305
|
+
}
|
|
306
|
+
console.log("=".repeat(72));
|
|
307
|
+
|
|
308
|
+
// Cleanup
|
|
309
|
+
try {
|
|
310
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
311
|
+
} catch {
|
|
312
|
+
/* ignore */
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (crashed || fail > 0) {
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main().catch((error) => {
|
|
322
|
+
console.error("Test script itself crashed:");
|
|
323
|
+
console.error(error);
|
|
324
|
+
try {
|
|
325
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
326
|
+
} catch {
|
|
327
|
+
/* ignore */
|
|
328
|
+
}
|
|
329
|
+
process.exit(1);
|
|
330
|
+
});
|