pi-crew 0.5.0 → 0.5.2
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 +51 -1
- package/README.md +1 -1
- package/docs/actions-reference.md +87 -0
- package/docs/commands-reference.md +5 -0
- package/docs/pi-crew-bugs.md +6 -0
- package/index.ts +1 -1
- package/package.json +18 -16
- package/src/benchmark/benchmark-runner.ts +245 -0
- package/src/benchmark/feedback-loop.ts +66 -0
- package/src/extension/async-notifier.ts +1 -1
- package/src/extension/autonomous-policy.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/plan-orchestrate.ts +322 -0
- package/src/extension/register.ts +31 -41
- package/src/extension/registration/command-utils.ts +1 -1
- package/src/extension/registration/commands.ts +1 -1
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +1 -1
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/registration/viewers.ts +1 -1
- package/src/extension/session-summary.ts +1 -1
- package/src/extension/team-manager-command.ts +1 -1
- package/src/extension/team-onboard.ts +1 -3
- package/src/extension/team-tool/context.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +183 -0
- package/src/extension/team-tool/orchestrate.ts +102 -0
- package/src/extension/team-tool/run.ts +215 -28
- package/src/extension/team-tool.ts +115 -0
- package/src/extension/tool-result.ts +1 -1
- package/src/i18n.ts +1 -1
- package/src/observability/event-to-metric.ts +1 -1
- package/src/prompt/prompt-runtime.ts +1 -1
- package/src/runtime/background-runner.ts +27 -5
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-hooks.ts +240 -0
- package/src/runtime/custom-tools/irc-tool.ts +1 -1
- package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
- package/src/runtime/diagnostic-export.ts +38 -2
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/live-session-runtime.ts +1 -1
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/pi-spawn.ts +20 -4
- package/src/runtime/process-status.ts +15 -2
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/task-runner.ts +31 -1
- package/src/runtime/team-runner.ts +6 -0
- package/src/schema/team-tool-schema.ts +36 -1
- package/src/state/crew-init.ts +56 -38
- package/src/state/decision-ledger.ts +295 -0
- package/src/state/hook-instinct-bridge.ts +90 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +249 -0
- package/src/state/run-graph.ts +5 -24
- package/src/state/run-metrics.ts +135 -0
- package/src/state/tiered-eval.ts +471 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +3 -0
- package/src/tools/safe-bash-extension.ts +5 -5
- package/src/ui/crew-widget.ts +1 -1
- package/src/ui/pi-ui-compat.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +1 -1
- package/src/ui/tool-render.ts +2 -2
- package/src/utils/bm25-search.ts +0 -2
- package/src/utils/project-detector.ts +160 -0
- package/test-bugs-all.mjs +1 -1
- package/skills/.gitkeep +0 -0
- package/skills/REFERENCE.md +0 -136
|
@@ -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,8 +153,21 @@ 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";
|
|
168
|
+
import { handleSchedule, handleListScheduled } from "./team-tool/handle-schedule.ts";
|
|
154
169
|
import { handlePlan } from "./team-tool/plan.ts";
|
|
170
|
+
import { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
155
171
|
import { handleRespond } from "./team-tool/respond.ts";
|
|
156
172
|
import { handleStatus } from "./team-tool/status.ts";
|
|
157
173
|
|
|
@@ -173,10 +189,12 @@ export {
|
|
|
173
189
|
handlePrune,
|
|
174
190
|
handleWorktrees,
|
|
175
191
|
} from "./team-tool/lifecycle-actions.ts";
|
|
192
|
+
export { handleSchedule } from "./team-tool/handle-schedule.ts";
|
|
176
193
|
export { handlePlan } from "./team-tool/plan.ts";
|
|
177
194
|
export { handleStatus } from "./team-tool/status.ts";
|
|
178
195
|
export type { TeamToolDetails } from "./team-tool-types.ts";
|
|
179
196
|
export { handleRun };
|
|
197
|
+
export { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
180
198
|
|
|
181
199
|
export function handleList(
|
|
182
200
|
params: TeamToolParamsValue,
|
|
@@ -1075,6 +1093,8 @@ export async function handleTeamTool(
|
|
|
1075
1093
|
return await handleParallel(params, ctx);
|
|
1076
1094
|
case "plan":
|
|
1077
1095
|
return handlePlan(params, ctx);
|
|
1096
|
+
case "orchestrate":
|
|
1097
|
+
return handleOrchestrate(params, ctx);
|
|
1078
1098
|
case "resume":
|
|
1079
1099
|
return handleResume(params, ctx);
|
|
1080
1100
|
case "create":
|
|
@@ -1089,6 +1109,101 @@ export async function handleTeamTool(
|
|
|
1089
1109
|
return handleHealthMonitor(ctx, params);
|
|
1090
1110
|
case "wait":
|
|
1091
1111
|
return handleWait(params, ctx);
|
|
1112
|
+
case "graph": {
|
|
1113
|
+
if (params.runId) {
|
|
1114
|
+
const graph = loadRunGraph(ctx.cwd, params.runId);
|
|
1115
|
+
return result(
|
|
1116
|
+
graph ? JSON.stringify(graph, null, 2) : "No graph found for this run.",
|
|
1117
|
+
{ action: "graph", status: graph ? "ok" : "error" },
|
|
1118
|
+
!graph,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const graphs = listRunGraphs(ctx.cwd);
|
|
1122
|
+
return result(
|
|
1123
|
+
graphs.length ? `Available graphs:\n${graphs.join("\n")}` : "No graphs available.",
|
|
1124
|
+
{ action: "graph", status: "ok" },
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
case "search": {
|
|
1128
|
+
const query = params.goal ?? params.task ?? "";
|
|
1129
|
+
if (!query) {
|
|
1130
|
+
return result("Search requires goal or task query.", { action: "search", status: "error" }, true);
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
const [agentResults, teamResults] = await Promise.all([
|
|
1134
|
+
searchAgents(query, { limit: 5 }),
|
|
1135
|
+
searchTeams(query, { limit: 3 }),
|
|
1136
|
+
]);
|
|
1137
|
+
const lines: string[] = [];
|
|
1138
|
+
if (teamResults.length) {
|
|
1139
|
+
lines.push("## Teams");
|
|
1140
|
+
for (const r of teamResults) {
|
|
1141
|
+
lines.push(`- [${r.team.name}] score=${r.score.toFixed(2)}: ${r.team.description ?? "(no description)"}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (agentResults.length) {
|
|
1145
|
+
lines.push("## Agents");
|
|
1146
|
+
for (const r of agentResults) {
|
|
1147
|
+
lines.push(`- [${r.agent.name}] score=${r.score.toFixed(2)}: ${r.agent.description ?? "(no description)"}`);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return result(lines.length ? lines.join("\n") : "No results found.", { action: "search", status: "ok" });
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1153
|
+
return result(`Search failed: ${msg}`, { action: "search", status: "error" }, true);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
case "schedule":
|
|
1157
|
+
return handleSchedule(params, ctx);
|
|
1158
|
+
case "scheduled":
|
|
1159
|
+
return handleListScheduled(params, ctx);
|
|
1160
|
+
case "onboard": {
|
|
1161
|
+
const team = params.team ?? "default";
|
|
1162
|
+
const onboarding = buildTeamOnboarding(team, ctx.cwd);
|
|
1163
|
+
return result(onboarding, { action: "onboard", status: "ok" });
|
|
1164
|
+
}
|
|
1165
|
+
case "explain": {
|
|
1166
|
+
const explainResult = handleExplain(params, ctx.cwd);
|
|
1167
|
+
return result(explainResult.text, { action: "explain", status: explainResult.isError ? "error" : "ok" }, explainResult.isError);
|
|
1168
|
+
}
|
|
1169
|
+
case "cache": {
|
|
1170
|
+
if (params.goal) {
|
|
1171
|
+
const key = computeRunCacheKey(
|
|
1172
|
+
params.goal,
|
|
1173
|
+
params.team ?? "default",
|
|
1174
|
+
params.workflow ?? "default",
|
|
1175
|
+
ctx.cwd,
|
|
1176
|
+
);
|
|
1177
|
+
const cached = getCachedRun(ctx.cwd, key);
|
|
1178
|
+
if (cached) {
|
|
1179
|
+
return result(
|
|
1180
|
+
`Cached run found (${new Date(cached.cachedAt).toISOString()}): runId=${cached.runId}, status=${cached.status}, ${cached.tasks.length} tasks`,
|
|
1181
|
+
{ action: "cache", status: "ok", data: { cacheKey: key, cacheHit: true, runId: cached.runId, status: cached.status, taskCount: cached.tasks.length } },
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
return result(`No cached result for key: ${key}`, { action: "cache", status: "ok", data: { cacheKey: key, cacheHit: false } });
|
|
1185
|
+
}
|
|
1186
|
+
const stats = getCacheStats(ctx.cwd);
|
|
1187
|
+
return result(
|
|
1188
|
+
`Cache stats: ${stats.entries} entries, ${stats.sizeBytes} bytes`,
|
|
1189
|
+
{ action: "cache", status: "ok" },
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
case "checkpoint": {
|
|
1193
|
+
if (!params.runId || !params.taskId) {
|
|
1194
|
+
return result("Checkpoint requires runId and taskId.", { action: "checkpoint", status: "error" }, true);
|
|
1195
|
+
}
|
|
1196
|
+
const stateRoot = path.join(projectCrewRoot(ctx.cwd), "state", "runs", params.runId);
|
|
1197
|
+
const store = new FileCheckpointStore(stateRoot);
|
|
1198
|
+
const checkpoint = store.load(params.runId, params.taskId);
|
|
1199
|
+
if (!checkpoint) {
|
|
1200
|
+
return result("No checkpoint found.", { action: "checkpoint", status: "error" }, true);
|
|
1201
|
+
}
|
|
1202
|
+
return result(
|
|
1203
|
+
`Checkpoint: step=${checkpoint.step}, progress=${checkpoint.progress}, savedAt=${new Date(checkpoint.savedAt).toISOString()}`,
|
|
1204
|
+
{ action: "checkpoint", status: "ok", data: { checkpoint } },
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1092
1207
|
default:
|
|
1093
1208
|
return result(
|
|
1094
1209
|
`Unknown action: ${action}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
3
3
|
|
|
4
4
|
export type PiTeamsToolResult<TDetails = TeamToolDetails> = AgentToolResult<TDetails> & { isError?: boolean };
|
package/src/i18n.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
4
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
@@ -19,6 +19,7 @@ async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Pro
|
|
|
19
19
|
return _cachedExecuteTeamRun(...args);
|
|
20
20
|
}
|
|
21
21
|
import { resolveCrewRuntime, runtimeResolutionState } from "./runtime-resolver.ts";
|
|
22
|
+
import { terminateActiveChildPiProcesses } from "./child-pi.ts";
|
|
22
23
|
import { directTeamAndWorkflowFromRun } from "./direct-run.ts";
|
|
23
24
|
import { expandParallelResearchWorkflow } from "./parallel-research.ts";
|
|
24
25
|
import { writeAsyncStartMarker } from "./async-marker.ts";
|
|
@@ -67,7 +68,7 @@ function argValue(name: string): string | undefined {
|
|
|
67
68
|
return process.argv[index + 1];
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function startInterruptGuard(manifest: { runId: string; stateRoot: string; eventsPath: string }): () => void {
|
|
71
|
+
function startInterruptGuard(manifest: { runId: string; stateRoot: string; eventsPath: string }, abortController: AbortController): () => void {
|
|
71
72
|
const controlPath = path.join(manifest.stateRoot, "foreground-control.json");
|
|
72
73
|
const interval = setInterval(() => {
|
|
73
74
|
try {
|
|
@@ -75,13 +76,21 @@ function startInterruptGuard(manifest: { runId: string; stateRoot: string; event
|
|
|
75
76
|
const parsed = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { requests?: Array<{ type: string; acknowledged?: boolean }> };
|
|
76
77
|
const last = parsed.requests?.at(-1);
|
|
77
78
|
if (last?.type === "interrupt" && last?.acknowledged !== true) {
|
|
78
|
-
appendEvent(manifest.eventsPath, { type: "async.interrupt_detected", runId: manifest.runId, message: "Background runner detected foreground interrupt
|
|
79
|
+
appendEvent(manifest.eventsPath, { type: "async.interrupt_detected", runId: manifest.runId, message: "Background runner detected foreground interrupt — killing child processes and exiting." });
|
|
80
|
+
// FIX: Terminate ALL child-pi processes IMMEDIATELY before exiting.
|
|
81
|
+
// Previously this was missing, causing orphaned child processes to run forever
|
|
82
|
+
// after the background-runner exited. terminateActiveChildPiProcesses sends
|
|
83
|
+
// SIGTERM then SIGKILL (after HARD_KILL_MS=3s) to every active child.
|
|
84
|
+
const killed = terminateActiveChildPiProcesses();
|
|
85
|
+
console.log(`[background-runner] interrupt: killed ${killed} child processes`);
|
|
86
|
+
// Also abort the run signal so executeTeamRun exits quickly via its signal check.
|
|
87
|
+
abortController.abort();
|
|
79
88
|
process.exit(130);
|
|
80
89
|
}
|
|
81
90
|
} catch {
|
|
82
91
|
/* ignore read/parse errors */
|
|
83
92
|
}
|
|
84
|
-
},
|
|
93
|
+
}, 500); // FIX: Reduced from 3000ms to 500ms for faster cancel response
|
|
85
94
|
interval.unref();
|
|
86
95
|
return () => clearInterval(interval);
|
|
87
96
|
}
|
|
@@ -238,8 +247,13 @@ async function main(): Promise<void> {
|
|
|
238
247
|
appendEvent(manifest.eventsPath, { type: "async.started", runId: manifest.runId, data: { pid: process.pid } });
|
|
239
248
|
console.log(`[background-runner] DEBUG: async.started written, pid=${process.pid}`);
|
|
240
249
|
writeAsyncStartMarker(manifest, { pid: process.pid, startedAt: new Date().toISOString() });
|
|
250
|
+
// FIX: Create AbortController EARLY so interrupt guard can use it.
|
|
251
|
+
// abortController.signal flows through: executeTeamRun → runTeamTask → runChildPi.
|
|
252
|
+
// When interrupt guard detects cancel, abortController.abort() fires the abort
|
|
253
|
+
// handler in runChildPi which kills child processes immediately.
|
|
254
|
+
const abortController = new AbortController();
|
|
241
255
|
const stopHeartbeat = startHeartbeat(manifest.stateRoot, manifest.eventsPath, manifest.runId);
|
|
242
|
-
const stopInterruptGuard = startInterruptGuard(manifest);
|
|
256
|
+
const stopInterruptGuard = startInterruptGuard(manifest, abortController);
|
|
243
257
|
console.log(`[background-runner] DEBUG: heartbeat+interrupt guard started`);
|
|
244
258
|
// BUG #17: Keep-alive interval prevents event loop from exiting during
|
|
245
259
|
// jiti compilation. Pure empty interval (no I/O to avoid io_uring issues).
|
|
@@ -278,10 +292,13 @@ async function main(): Promise<void> {
|
|
|
278
292
|
// BUG #17: Keep-alive interval (NOT unref'd) prevents event loop from exiting
|
|
279
293
|
// during jiti compilation of team-runner.ts. Without this, the event loop
|
|
280
294
|
// can drain when import() blocks, causing the process to exit prematurely.
|
|
295
|
+
// NOTE: abortController is already created above (before heartbeat/interrupt guard start)
|
|
296
|
+
// so it is available here and its signal is passed through to executeTeamRun → child-pi.
|
|
297
|
+
|
|
281
298
|
console.log(`[background-runner] DEBUG: calling executeTeamRun`);
|
|
282
299
|
let result;
|
|
283
300
|
try {
|
|
284
|
-
result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: runConfig.limits, runtime, runtimeConfig: runConfig.runtime, skillOverride: manifest.skillOverride, reliability: runConfig.reliability, workspaceId: manifest.ownerSessionId ?? manifest.cwd });
|
|
301
|
+
result = await executeTeamRun({ manifest, tasks, team, workflow, agents, executeWorkers, limits: runConfig.limits, runtime, runtimeConfig: runConfig.runtime, skillOverride: manifest.skillOverride, reliability: runConfig.reliability, workspaceId: manifest.ownerSessionId ?? manifest.cwd, signal: abortController.signal });
|
|
285
302
|
console.log(`[background-runner] DEBUG: executeTeamRun returned, status=${result.manifest.status}`);
|
|
286
303
|
} catch (execError) {
|
|
287
304
|
console.log(`[background-runner] DEBUG: executeTeamRun THREW: ${execError instanceof Error ? execError.message : String(execError)}`);
|
|
@@ -314,6 +331,11 @@ async function main(): Promise<void> {
|
|
|
314
331
|
stopParentGuard();
|
|
315
332
|
stopHeartbeat();
|
|
316
333
|
clearInterval(keepAlive);
|
|
334
|
+
// FIX: Always kill child processes on exit. executeTeamRun's terminateLiveAgentsForRun
|
|
335
|
+
// only handles live-session agents, not child-pi processes. Without this, child-pi
|
|
336
|
+
// processes can become orphaned if executeTeamRun throws before completing.
|
|
337
|
+
const killed = terminateActiveChildPiProcesses();
|
|
338
|
+
console.log(`[background-runner] finally: killed ${killed} child processes`);
|
|
317
339
|
}
|
|
318
340
|
}
|
|
319
341
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
4
4
|
import { appendEvent, scanSequence } from "../state/event-log.ts";
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew Hook System — a hook-based observation system for pi-crew runtime.
|
|
3
|
+
*
|
|
4
|
+
* Provides a reliable, fire-and-forget event system for observing crew lifecycle events.
|
|
5
|
+
* Hooks are executed synchronously without blocking the emitter.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { crewHooks } from './runtime/crew-hooks.ts';
|
|
10
|
+
*
|
|
11
|
+
* // Register a hook
|
|
12
|
+
* const myHook = (event) => {
|
|
13
|
+
* console.log(`Event: ${event.type}`, event);
|
|
14
|
+
* };
|
|
15
|
+
* crewHooks.register('task_started', myHook);
|
|
16
|
+
*
|
|
17
|
+
* // Emit an event
|
|
18
|
+
* crewHooks.emit({ type: 'task_started', timestamp: new Date().toISOString(), runId: 'run-123', taskId: 'task-1' });
|
|
19
|
+
*
|
|
20
|
+
* // Unregister when done
|
|
21
|
+
* crewHooks.unregister('task_started', myHook);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Valid hook event types in the crew lifecycle. */
|
|
26
|
+
export type CrewHookEventType =
|
|
27
|
+
| 'task_started'
|
|
28
|
+
| 'task_completed'
|
|
29
|
+
| 'task_failed'
|
|
30
|
+
| 'run_completed'
|
|
31
|
+
| 'run_failed';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A hook event emitted by the crew runtime.
|
|
35
|
+
*/
|
|
36
|
+
export interface CrewHookEvent {
|
|
37
|
+
/** The type of event being emitted. */
|
|
38
|
+
type: CrewHookEventType;
|
|
39
|
+
/** ISO timestamp of when the event occurred. */
|
|
40
|
+
timestamp: string;
|
|
41
|
+
/** The unique identifier of the run that generated this event. */
|
|
42
|
+
runId: string;
|
|
43
|
+
/** Optional task identifier (present for task-scoped events). */
|
|
44
|
+
taskId?: string;
|
|
45
|
+
/** Optional additional event data. */
|
|
46
|
+
data?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A hook function that can be registered to receive crew events.
|
|
51
|
+
* May be synchronous or return a Promise (async hooks are fire-and-forget).
|
|
52
|
+
*/
|
|
53
|
+
export type CrewHook = (event: CrewHookEvent) => void | Promise<void>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Type guard to check if a value is a valid CrewHookEventType.
|
|
57
|
+
*/
|
|
58
|
+
export function isValidEventType(type: string): type is CrewHookEventType {
|
|
59
|
+
return (
|
|
60
|
+
type === 'task_started' ||
|
|
61
|
+
type === 'task_completed' ||
|
|
62
|
+
type === 'task_failed' ||
|
|
63
|
+
type === 'run_completed' ||
|
|
64
|
+
type === 'run_failed'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type guard to check if an object is a valid CrewHookEvent.
|
|
70
|
+
*/
|
|
71
|
+
export function isHookEvent(obj: unknown): obj is CrewHookEvent {
|
|
72
|
+
if (typeof obj !== 'object' || obj === null) return false;
|
|
73
|
+
const event = obj as Record<string, unknown>;
|
|
74
|
+
return (
|
|
75
|
+
typeof event.type === 'string' &&
|
|
76
|
+
isValidEventType(event.type) &&
|
|
77
|
+
typeof event.timestamp === 'string' &&
|
|
78
|
+
typeof event.runId === 'string' &&
|
|
79
|
+
(event.taskId === undefined || typeof event.taskId === 'string') &&
|
|
80
|
+
(event.data === undefined || typeof event.data === 'object')
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Registry for managing and emitting crew lifecycle hooks.
|
|
86
|
+
*
|
|
87
|
+
* Hooks are stored in Sets for efficient insertion, deletion, and iteration.
|
|
88
|
+
* The emit() method executes all registered hooks synchronously without awaiting
|
|
89
|
+
* async completions, ensuring 100% reliable event firing without blocking.
|
|
90
|
+
*/
|
|
91
|
+
export class HookRegistry {
|
|
92
|
+
private readonly hooks: Map<CrewHookEventType, Set<CrewHook>>;
|
|
93
|
+
|
|
94
|
+
constructor() {
|
|
95
|
+
this.hooks = new Map();
|
|
96
|
+
// Initialize with empty Sets for all event types
|
|
97
|
+
const eventTypes: CrewHookEventType[] = [
|
|
98
|
+
'task_started',
|
|
99
|
+
'task_completed',
|
|
100
|
+
'task_failed',
|
|
101
|
+
'run_completed',
|
|
102
|
+
'run_failed',
|
|
103
|
+
];
|
|
104
|
+
for (const type of eventTypes) {
|
|
105
|
+
this.hooks.set(type, new Set());
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register a hook to be called when the specified event type is emitted.
|
|
111
|
+
*
|
|
112
|
+
* @param eventType - The type of event to listen for
|
|
113
|
+
* @param hook - The hook function to register
|
|
114
|
+
*/
|
|
115
|
+
register(eventType: CrewHookEventType, hook: CrewHook): void {
|
|
116
|
+
const hooksForType = this.hooks.get(eventType);
|
|
117
|
+
if (hooksForType) {
|
|
118
|
+
hooksForType.add(hook);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Unregister a previously registered hook.
|
|
124
|
+
*
|
|
125
|
+
* @param eventType - The type of event the hook was registered for
|
|
126
|
+
* @param hook - The hook function to remove
|
|
127
|
+
*/
|
|
128
|
+
unregister(eventType: CrewHookEventType, hook: CrewHook): void {
|
|
129
|
+
const hooksForType = this.hooks.get(eventType);
|
|
130
|
+
if (hooksForType) {
|
|
131
|
+
hooksForType.delete(hook);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Emit an event to all registered hooks for that event type.
|
|
137
|
+
*
|
|
138
|
+
* This method executes all hooks synchronously and does not await async hooks.
|
|
139
|
+
* Errors thrown by hooks are caught and logged but do not prevent other hooks
|
|
140
|
+
* from executing or block the caller.
|
|
141
|
+
*
|
|
142
|
+
* @param event - The event to emit
|
|
143
|
+
*/
|
|
144
|
+
emit(event: CrewHookEvent): void {
|
|
145
|
+
// Validate event type using type guard
|
|
146
|
+
if (!isValidEventType(event.type)) {
|
|
147
|
+
console.warn(`[crew-hooks] Unknown event type: ${event.type}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hooksForType = this.hooks.get(event.type);
|
|
152
|
+
if (!hooksForType || hooksForType.size === 0) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Execute all hooks - fire-and-forget pattern
|
|
157
|
+
// We iterate over a snapshot to allow safe modification during iteration
|
|
158
|
+
const hooksSnapshot = Array.from(hooksForType);
|
|
159
|
+
for (const hook of hooksSnapshot) {
|
|
160
|
+
try {
|
|
161
|
+
const result = hook(event);
|
|
162
|
+
// If the hook returns a Promise, we intentionally do NOT await it.
|
|
163
|
+
// This is the "fire-and-forget" pattern - async hooks run in background.
|
|
164
|
+
if (result instanceof Promise) {
|
|
165
|
+
// Attach a silent catch to prevent unhandled rejection warnings
|
|
166
|
+
result.catch((err) => {
|
|
167
|
+
console.error(`[crew-hooks] Async hook error for ${event.type}:`, err);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
// Catch synchronous errors but don't let them block other hooks
|
|
172
|
+
console.error(`[crew-hooks] Hook error for ${event.type}:`, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get all hooks registered for a specific event type.
|
|
179
|
+
*
|
|
180
|
+
* Returns a snapshot of the current hooks. The returned array is a new copy,
|
|
181
|
+
* so modifications to it won't affect the registry.
|
|
182
|
+
*
|
|
183
|
+
* @param eventType - The event type to query
|
|
184
|
+
* @returns Array of registered hooks (may be empty)
|
|
185
|
+
*/
|
|
186
|
+
hooksFor(eventType: CrewHookEventType): CrewHook[] {
|
|
187
|
+
const hooksForType = this.hooks.get(eventType);
|
|
188
|
+
if (!hooksForType) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
return Array.from(hooksForType);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get the count of hooks registered for a specific event type.
|
|
196
|
+
*
|
|
197
|
+
* @param eventType - The event type to query
|
|
198
|
+
* @returns Number of registered hooks
|
|
199
|
+
*/
|
|
200
|
+
count(eventType: CrewHookEventType): number {
|
|
201
|
+
const hooksForType = this.hooks.get(eventType);
|
|
202
|
+
return hooksForType?.size ?? 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove all hooks for a specific event type.
|
|
207
|
+
*
|
|
208
|
+
* @param eventType - The event type to clear
|
|
209
|
+
*/
|
|
210
|
+
clear(eventType: CrewHookEventType): void {
|
|
211
|
+
const hooksForType = this.hooks.get(eventType);
|
|
212
|
+
if (hooksForType) {
|
|
213
|
+
hooksForType.clear();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Remove all registered hooks across all event types.
|
|
219
|
+
*/
|
|
220
|
+
clearAll(): void {
|
|
221
|
+
for (const hooksForType of this.hooks.values()) {
|
|
222
|
+
hooksForType.clear();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Global singleton instance of HookRegistry for use throughout pi-crew.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* import { crewHooks } from './runtime/crew-hooks.ts';
|
|
233
|
+
*
|
|
234
|
+
* // Simple logging hook
|
|
235
|
+
* crewHooks.register('task_completed', (event) => {
|
|
236
|
+
* console.log(`Task ${event.taskId} completed in run ${event.runId}`);
|
|
237
|
+
* });
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export const crewHooks = new HookRegistry();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* for routing messages between in-process workers.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { defineTool, type ToolDefinition } from "@
|
|
15
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import { Type, type Static } from "@sinclair/typebox";
|
|
17
17
|
import { listLiveAgents, sendIrcMessage, broadcastIrcMessage } from "../live-agent-manager.ts";
|
|
18
18
|
import type { IrcMessage } from "../live-irc.ts";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* and TypeBox schemas for validation.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { defineTool, type ToolDefinition } from "@
|
|
12
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, type Static } from "@sinclair/typebox";
|
|
14
14
|
import type { YieldResult } from "../yield-handler.ts";
|
|
15
15
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
3
3
|
import type { MetricSnapshot } from "../observability/metrics-primitives.ts";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -10,6 +10,7 @@ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
|
10
10
|
import { summarizeHeartbeats, type HeartbeatSummary } from "../ui/heartbeat-aggregator.ts";
|
|
11
11
|
import type { RunUiSnapshot } from "../ui/snapshot-types.ts";
|
|
12
12
|
import { redactSecrets } from "../utils/redaction.ts";
|
|
13
|
+
import { buildRecoveryLedger, type RecoveryLedgerEntry } from "./recovery-recipes.ts";
|
|
13
14
|
export { redactSecrets } from "../utils/redaction.ts";
|
|
14
15
|
|
|
15
16
|
export interface DiagnosticReport {
|
|
@@ -23,6 +24,17 @@ export interface DiagnosticReport {
|
|
|
23
24
|
agents: unknown[];
|
|
24
25
|
envRedacted: Record<string, string>;
|
|
25
26
|
metricsSnapshot?: MetricSnapshot[];
|
|
27
|
+
// Layer 8: task diagnostics
|
|
28
|
+
taskDiagnostics: Record<string, Record<string, unknown>>;
|
|
29
|
+
// Layer 9: terminal evidence
|
|
30
|
+
terminalEvidence: Record<string, TeamTaskState["terminalEvidence"]>;
|
|
31
|
+
// Layer 10: model attempts and routing
|
|
32
|
+
modelAttempts: { taskId: string; attempts: TeamTaskState["modelAttempts"]; routing: TeamTaskState["modelRouting"] }[];
|
|
33
|
+
// Layer 11: pending mailbox
|
|
34
|
+
pendingMailbox: { taskId: string; pendingSteers: TeamTaskState["pendingSteers"] }[];
|
|
35
|
+
runMailboxUnread: RunUiSnapshot["mailbox"];
|
|
36
|
+
// Layer 12: recovery ledger
|
|
37
|
+
recoveryLedger: RecoveryLedgerEntry[];
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
const SECRET_KEY_PATTERN = /(token|key|password|secret|credential|auth)/i;
|
|
@@ -69,6 +81,24 @@ export async function exportDiagnostic(ctx: Pick<ExtensionContext, "cwd">, runId
|
|
|
69
81
|
const safeTimestamp = exportedAt.replace(/[:.]/g, "-");
|
|
70
82
|
const recentEvents = readEvents(loaded.manifest.eventsPath).slice(-200);
|
|
71
83
|
const metricsSnapshot = options.registry?.snapshot();
|
|
84
|
+
const taskDiagnostics: Record<string, Record<string, unknown>> = {};
|
|
85
|
+
const terminalEvidence: Record<string, TeamTaskState["terminalEvidence"]> = {};
|
|
86
|
+
const modelAttempts: { taskId: string; attempts: TeamTaskState["modelAttempts"]; routing: TeamTaskState["modelRouting"] }[] = [];
|
|
87
|
+
const pendingMailbox: { taskId: string; pendingSteers: TeamTaskState["pendingSteers"] }[] = [];
|
|
88
|
+
for (const task of loaded.tasks) {
|
|
89
|
+
if (task.diagnostics) taskDiagnostics[task.id] = task.diagnostics;
|
|
90
|
+
if (task.terminalEvidence) terminalEvidence[task.id] = task.terminalEvidence;
|
|
91
|
+
if (task.modelAttempts || task.modelRouting) {
|
|
92
|
+
modelAttempts.push({ taskId: task.id, attempts: task.modelAttempts, routing: task.modelRouting });
|
|
93
|
+
}
|
|
94
|
+
if (task.pendingSteers) {
|
|
95
|
+
pendingMailbox.push({ taskId: task.id, pendingSteers: task.pendingSteers });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const recoveryLedger = loaded.manifest.policyDecisions
|
|
99
|
+
? buildRecoveryLedger(loaded.manifest.policyDecisions).entries
|
|
100
|
+
: [];
|
|
101
|
+
const snapshot = buildSnapshot(loaded.manifest, loaded.tasks);
|
|
72
102
|
const report: DiagnosticReport = {
|
|
73
103
|
...(metricsSnapshot ? { schemaVersion: 2 } : {}),
|
|
74
104
|
runId,
|
|
@@ -76,10 +106,16 @@ export async function exportDiagnostic(ctx: Pick<ExtensionContext, "cwd">, runId
|
|
|
76
106
|
manifest: redactSecrets(loaded.manifest) as TeamRunManifest,
|
|
77
107
|
tasks: redactSecrets(loaded.tasks) as TeamTaskState[],
|
|
78
108
|
recentEvents: redactSecrets(recentEvents) as TeamEvent[],
|
|
79
|
-
heartbeat: summarizeHeartbeats(
|
|
109
|
+
heartbeat: summarizeHeartbeats(snapshot),
|
|
80
110
|
agents: redactSecrets(readCrewAgents(loaded.manifest)) as unknown[],
|
|
81
111
|
envRedacted: envRedacted(),
|
|
82
112
|
...(metricsSnapshot ? { metricsSnapshot: redactSecrets(metricsSnapshot) as MetricSnapshot[] } : {}),
|
|
113
|
+
taskDiagnostics,
|
|
114
|
+
terminalEvidence,
|
|
115
|
+
modelAttempts,
|
|
116
|
+
pendingMailbox,
|
|
117
|
+
runMailboxUnread: snapshot.mailbox,
|
|
118
|
+
recoveryLedger,
|
|
83
119
|
};
|
|
84
120
|
const dir = path.join(loaded.manifest.artifactsRoot, "diagnostic");
|
|
85
121
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is automatically notified — no manual sleep+check needed.
|
|
12
12
|
* 3. Cleans up after itself when the run completes or the session ends.
|
|
13
13
|
*/
|
|
14
|
-
import type { ExtensionAPI } from "@
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
16
16
|
import { readCrewAgents } from "./crew-agent-records.ts";
|
|
17
17
|
import { isActiveRunStatus, isLikelyOrphanedActiveRun } from "./process-status.ts";
|
|
@@ -336,7 +336,7 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
336
336
|
const availability = await isLiveSessionRuntimeAvailable();
|
|
337
337
|
if (!availability.available) return { available: true, exitCode: 1, stdout: "", stderr: availability.reason ?? "Live-session runtime unavailable.", jsonEvents: 0, error: availability.reason };
|
|
338
338
|
// LAZY: optional peer dependency — only loaded when live-session runtime is chosen.
|
|
339
|
-
const mod = await import("@
|
|
339
|
+
const mod = await import("@earendil-works/pi-coding-agent") as unknown as LiveSessionModule;
|
|
340
340
|
if (typeof mod.createAgentSession !== "function") return { available: true, exitCode: 1, stdout: "", stderr: "createAgentSession export is unavailable.", jsonEvents: 0, error: "createAgentSession export is unavailable." };
|
|
341
341
|
let session: LiveSessionLike | undefined;
|
|
342
342
|
let unsubscribe: (() => void) | undefined;
|
package/src/runtime/mcp-proxy.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* when proxying from the parent.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { defineTool, type ToolDefinition } from "@
|
|
19
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { Type, type Static, type TSchema } from "@sinclair/typebox";
|
|
21
21
|
|
|
22
22
|
export interface McpProxyConfig {
|