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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -1
  3. package/docs/actions-reference.md +87 -0
  4. package/docs/commands-reference.md +5 -0
  5. package/docs/pi-crew-bugs.md +6 -0
  6. package/index.ts +1 -1
  7. package/package.json +18 -16
  8. package/src/benchmark/benchmark-runner.ts +245 -0
  9. package/src/benchmark/feedback-loop.ts +66 -0
  10. package/src/extension/async-notifier.ts +1 -1
  11. package/src/extension/autonomous-policy.ts +1 -1
  12. package/src/extension/cross-extension-rpc.ts +1 -1
  13. package/src/extension/plan-orchestrate.ts +322 -0
  14. package/src/extension/register.ts +31 -41
  15. package/src/extension/registration/command-utils.ts +1 -1
  16. package/src/extension/registration/commands.ts +1 -1
  17. package/src/extension/registration/compaction-guard.ts +1 -1
  18. package/src/extension/registration/subagent-helpers.ts +1 -1
  19. package/src/extension/registration/subagent-tools.ts +1 -1
  20. package/src/extension/registration/team-tool.ts +1 -1
  21. package/src/extension/registration/viewers.ts +1 -1
  22. package/src/extension/session-summary.ts +1 -1
  23. package/src/extension/team-manager-command.ts +1 -1
  24. package/src/extension/team-onboard.ts +1 -3
  25. package/src/extension/team-tool/context.ts +1 -1
  26. package/src/extension/team-tool/handle-schedule.ts +183 -0
  27. package/src/extension/team-tool/orchestrate.ts +102 -0
  28. package/src/extension/team-tool/run.ts +215 -28
  29. package/src/extension/team-tool.ts +115 -0
  30. package/src/extension/tool-result.ts +1 -1
  31. package/src/i18n.ts +1 -1
  32. package/src/observability/event-to-metric.ts +1 -1
  33. package/src/prompt/prompt-runtime.ts +1 -1
  34. package/src/runtime/background-runner.ts +27 -5
  35. package/src/runtime/crash-recovery.ts +1 -1
  36. package/src/runtime/crew-hooks.ts +240 -0
  37. package/src/runtime/custom-tools/irc-tool.ts +1 -1
  38. package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
  39. package/src/runtime/diagnostic-export.ts +38 -2
  40. package/src/runtime/foreground-watchdog.ts +1 -1
  41. package/src/runtime/live-session-runtime.ts +1 -1
  42. package/src/runtime/mcp-proxy.ts +1 -1
  43. package/src/runtime/pi-spawn.ts +20 -4
  44. package/src/runtime/process-status.ts +15 -2
  45. package/src/runtime/runtime-resolver.ts +1 -1
  46. package/src/runtime/session-resources.ts +1 -1
  47. package/src/runtime/task-runner.ts +31 -1
  48. package/src/runtime/team-runner.ts +6 -0
  49. package/src/schema/team-tool-schema.ts +36 -1
  50. package/src/state/crew-init.ts +56 -38
  51. package/src/state/decision-ledger.ts +295 -0
  52. package/src/state/hook-instinct-bridge.ts +90 -0
  53. package/src/state/hook-integrations.ts +51 -0
  54. package/src/state/instinct-store.ts +249 -0
  55. package/src/state/run-graph.ts +5 -24
  56. package/src/state/run-metrics.ts +135 -0
  57. package/src/state/tiered-eval.ts +471 -0
  58. package/src/state/types-eval.ts +58 -0
  59. package/src/state/types.ts +3 -0
  60. package/src/tools/safe-bash-extension.ts +5 -5
  61. package/src/ui/crew-widget.ts +1 -1
  62. package/src/ui/pi-ui-compat.ts +1 -1
  63. package/src/ui/run-action-dispatcher.ts +1 -1
  64. package/src/ui/tool-render.ts +2 -2
  65. package/src/utils/bm25-search.ts +0 -2
  66. package/src/utils/project-detector.ts +160 -0
  67. package/test-bugs-all.mjs +1 -1
  68. package/skills/.gitkeep +0 -0
  69. 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 "@mariozechner/pi-agent-core";
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 "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  type Params = Record<string, string | number>;
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { MetricRegistry } from "./metric-registry.ts";
3
3
 
4
4
  function recordValue(value: unknown): Record<string, unknown> {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
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 request — exiting." });
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
- }, 3_000);
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 "@mariozechner/pi-coding-agent";
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 "@mariozechner/pi-coding-agent";
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 "@mariozechner/pi-coding-agent";
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 "@mariozechner/pi-coding-agent";
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(buildSnapshot(loaded.manifest, loaded.tasks)),
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 "@mariozechner/pi-coding-agent";
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("@mariozechner/pi-coding-agent") as LiveSessionModule;
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;
@@ -16,7 +16,7 @@
16
16
  * when proxying from the parent.
17
17
  */
18
18
 
19
- import { defineTool, type ToolDefinition } from "@mariozechner/pi-coding-agent";
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 {