pi-crew 0.5.1 → 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 (66) hide show
  1. package/CHANGELOG.md +28 -0
  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-tool/context.ts +1 -1
  25. package/src/extension/team-tool/handle-schedule.ts +183 -0
  26. package/src/extension/team-tool/orchestrate.ts +102 -0
  27. package/src/extension/team-tool/run.ts +215 -28
  28. package/src/extension/team-tool.ts +10 -0
  29. package/src/extension/tool-result.ts +1 -1
  30. package/src/i18n.ts +1 -1
  31. package/src/observability/event-to-metric.ts +1 -1
  32. package/src/prompt/prompt-runtime.ts +1 -1
  33. package/src/runtime/background-runner.ts +27 -5
  34. package/src/runtime/crash-recovery.ts +1 -1
  35. package/src/runtime/crew-hooks.ts +240 -0
  36. package/src/runtime/custom-tools/irc-tool.ts +1 -1
  37. package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
  38. package/src/runtime/diagnostic-export.ts +38 -2
  39. package/src/runtime/foreground-watchdog.ts +1 -1
  40. package/src/runtime/live-session-runtime.ts +1 -1
  41. package/src/runtime/mcp-proxy.ts +1 -1
  42. package/src/runtime/pi-spawn.ts +20 -4
  43. package/src/runtime/process-status.ts +15 -2
  44. package/src/runtime/runtime-resolver.ts +1 -1
  45. package/src/runtime/session-resources.ts +1 -1
  46. package/src/runtime/task-runner.ts +31 -1
  47. package/src/runtime/team-runner.ts +6 -0
  48. package/src/schema/team-tool-schema.ts +24 -1
  49. package/src/state/crew-init.ts +56 -38
  50. package/src/state/decision-ledger.ts +295 -0
  51. package/src/state/hook-instinct-bridge.ts +90 -0
  52. package/src/state/hook-integrations.ts +51 -0
  53. package/src/state/instinct-store.ts +249 -0
  54. package/src/state/run-metrics.ts +135 -0
  55. package/src/state/tiered-eval.ts +471 -0
  56. package/src/state/types-eval.ts +58 -0
  57. package/src/state/types.ts +3 -0
  58. package/src/tools/safe-bash-extension.ts +5 -5
  59. package/src/ui/crew-widget.ts +1 -1
  60. package/src/ui/pi-ui-compat.ts +1 -1
  61. package/src/ui/run-action-dispatcher.ts +1 -1
  62. package/src/ui/tool-render.ts +2 -2
  63. package/src/utils/project-detector.ts +160 -0
  64. package/test-bugs-all.mjs +1 -1
  65. package/skills/.gitkeep +0 -0
  66. package/skills/REFERENCE.md +0 -136
@@ -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 {
@@ -8,6 +8,11 @@ export interface PiSpawnCommand {
8
8
  args: string[];
9
9
  }
10
10
 
11
+ const PI_PACKAGE_NAMES = [
12
+ "@earendil-works/pi-coding-agent",
13
+ "@mariozechner/pi-coding-agent",
14
+ ];
15
+
11
16
  function isRunnableNodeScript(filePath: string): boolean {
12
17
  return fs.existsSync(filePath) && /\.(?:mjs|cjs|js)$/i.test(filePath);
13
18
  }
@@ -26,6 +31,7 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
26
31
  try {
27
32
  const execDir = path.dirname(fs.realpathSync.native(process.execPath));
28
33
  allowedPrefixes.push(execDir.toLowerCase());
34
+ allowedPrefixes.push(path.join(path.dirname(execDir), "lib", "node_modules").toLowerCase());
29
35
  } catch { /* ignore */ }
30
36
 
31
37
  // npm global bin via APPDATA
@@ -33,6 +39,12 @@ function isWithinAllowedPrefixes(resolvedPath: string): boolean {
33
39
  allowedPrefixes.push(path.join(process.env.APPDATA, "npm").toLowerCase());
34
40
  }
35
41
 
42
+ const npmPrefix = process.env.npm_config_prefix ?? process.env.NPM_CONFIG_PREFIX;
43
+ if (npmPrefix) {
44
+ allowedPrefixes.push(path.resolve(npmPrefix).toLowerCase());
45
+ allowedPrefixes.push(path.join(path.resolve(npmPrefix), "lib", "node_modules").toLowerCase());
46
+ }
47
+
36
48
  // Project-local node_modules/.bin
37
49
  try {
38
50
  const projectBin = path.resolve("node_modules", ".bin");
@@ -62,7 +74,7 @@ function resolvePiPackageRoot(): string | undefined {
62
74
  while (dir !== path.dirname(dir)) {
63
75
  try {
64
76
  const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")) as { name?: string };
65
- if (pkg.name === "@mariozechner/pi-coding-agent") return dir;
77
+ if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return dir;
66
78
  } catch {
67
79
  // Continue walking upward.
68
80
  }
@@ -92,12 +104,15 @@ function findPiPackageJsonFrom(startDir: string): string | undefined {
92
104
  const direct = path.join(dir, "package.json");
93
105
  try {
94
106
  const pkg = JSON.parse(fs.readFileSync(direct, "utf-8")) as { name?: string };
95
- if (pkg.name === "@mariozechner/pi-coding-agent") return direct;
107
+ if (pkg.name && PI_PACKAGE_NAMES.includes(pkg.name)) return direct;
96
108
  } catch {
97
109
  // Continue searching upward and in node_modules.
98
110
  }
99
- const dependency = path.join(dir, "node_modules", "@mariozechner", "pi-coding-agent", "package.json");
100
- if (fs.existsSync(dependency)) return dependency;
111
+ for (const pkgName of PI_PACKAGE_NAMES) {
112
+ const [scope, name] = pkgName.replace("@", "").split("/");
113
+ const dependency = path.join(dir, "node_modules", `@${scope}`, name, "package.json");
114
+ if (fs.existsSync(dependency)) return dependency;
115
+ }
101
116
  dir = path.dirname(dir);
102
117
  }
103
118
  return undefined;
@@ -112,6 +127,7 @@ function resolvePiCliScript(): string | undefined {
112
127
 
113
128
  const roots = [
114
129
  resolvePiPackageRoot(),
130
+ process.env.APPDATA ? path.join(process.env.APPDATA, "npm", "node_modules", "@earendil-works", "pi-coding-agent") : undefined,
115
131
  process.env.APPDATA ? path.join(process.env.APPDATA, "npm", "node_modules", "@mariozechner", "pi-coding-agent") : undefined,
116
132
  path.dirname(fileURLToPath(import.meta.url)),
117
133
  process.cwd(),
@@ -77,8 +77,21 @@ export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgen
77
77
  }
78
78
 
79
79
  function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {
80
- if (agent.status !== "running" && agent.status !== "queued") return false;
81
- return Boolean(agent.statusPath || agent.eventsPath || agent.outputPath || agent.progress || agent.toolUses || agent.jsonEvents);
80
+ if (agent.status === "running") {
81
+ // Running agents are actively executing trust them.
82
+ // Activity evidence is only required for queued agents (zombie prevention).
83
+ return true;
84
+ }
85
+ if (agent.status === "queued") {
86
+ // Queued agents need actual activity evidence to distinguish from zombies:
87
+ // spawned-but-never-executed agents should not appear as active.
88
+ return Boolean(
89
+ (agent.progress && (agent.progress.toolCount > 0 || agent.progress.recentOutput.length > 0)) ||
90
+ (agent.jsonEvents && agent.jsonEvents > 0) ||
91
+ (agent.toolUses && agent.toolUses > 0),
92
+ );
93
+ }
94
+ return false;
82
95
  }
83
96
 
84
97
  export function hasStaleAsyncProcess(run: TeamRunManifest, now = Date.now()): boolean {
@@ -38,7 +38,7 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
38
38
  const probe = async (): Promise<{ available: boolean; reason?: string }> => {
39
39
  try {
40
40
  // LAZY: optional peer dependency — probe at runtime to avoid hard dependency.
41
- const mod = await import("@mariozechner/pi-coding-agent");
41
+ const mod = await import("@earendil-works/pi-coding-agent");
42
42
  const api = mod as Record<string, unknown>;
43
43
  const required = ["createAgentSession", "DefaultResourceLoader", "SessionManager", "SettingsManager"];
44
44
  const missing = required.filter((name) => typeof api[name] === "undefined");
@@ -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 { logInternalError } from "../utils/internal-error.ts";
3
3
 
4
4
  /**
@@ -40,6 +40,7 @@ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
40
40
  import { createVerificationEvidence } from "./green-contract.ts";
41
41
  import { createStartupEvidence } from "./worker-startup.ts";
42
42
  import { permissionForRole } from "./role-permission.ts";
43
+ import { crewHooks } from "./crew-hooks.ts";
43
44
  import {
44
45
  collectDependencyOutputContext,
45
46
  renderDependencyOutputContext,
@@ -401,6 +402,7 @@ export async function runTeamTask(
401
402
  modelAttempts: [...modelAttempts, pendingAttempt],
402
403
  };
403
404
  tasks = updateTask(tasks, task);
405
+ crewHooks.emit({ type: "task_started", timestamp: new Date().toISOString(), runId: manifest.runId, taskId: task.id, data: { role: task.role, model: model ?? "default" } });
404
406
  upsertCrewAgent(
405
407
  manifest,
406
408
  recordFromTask(manifest, task, "child-process"),
@@ -808,7 +810,22 @@ export async function runTeamTask(
808
810
  exitCode = live.exitCode;
809
811
  error = live.error;
810
812
  parsedOutput = live.parsedOutput;
811
- resultArtifact = live.resultArtifact;
813
+ // Bug #21 fix: live-session may not produce structured output via submit_result,
814
+ // leaving finalText empty. Re-write resultArtifact with parsedOutput.finalText
815
+ // so downstream tasks that depend on this task can read meaningful output.
816
+ const liveText = cleanResultText(parsedOutput?.finalText);
817
+ if (liveText) {
818
+ // Re-write the artifact with the captured stdout — this is the content
819
+ // downstream tasks will read via task.resultArtifact.path.
820
+ resultArtifact = writeArtifact(manifest.artifactsRoot, {
821
+ kind: "result",
822
+ relativePath: `results/${task.id}.txt`,
823
+ content: liveText,
824
+ producer: task.id,
825
+ });
826
+ } else {
827
+ resultArtifact = live.resultArtifact;
828
+ }
812
829
  logArtifact = live.logArtifact;
813
830
  transcriptArtifact = live.transcriptArtifact;
814
831
  } else {
@@ -855,6 +872,8 @@ export async function runTeamTask(
855
872
  data: {
856
873
  activityState: "needs_attention",
857
874
  reason: "no_yield",
875
+ // Bug #21 fix: include result path so downstream tasks can read the output
876
+ resultPath: resultArtifact?.path,
858
877
  },
859
878
  });
860
879
  }
@@ -1004,6 +1023,17 @@ export async function runTeamTask(
1004
1023
  ...(transcriptArtifact ? { transcriptArtifact } : {}),
1005
1024
  };
1006
1025
  tasks = updateTask(tasks, task);
1026
+
1027
+ // Emit task completion hooks (100% reliable, fire-and-forget)
1028
+ const hookType = task.status === "completed" ? "task_completed" : task.status === "failed" ? "task_failed" : "task_started";
1029
+ crewHooks.emit({
1030
+ type: hookType,
1031
+ timestamp: task.finishedAt ?? new Date().toISOString(),
1032
+ runId: manifest.runId,
1033
+ taskId: task.id,
1034
+ data: { status: task.status, role: task.role, error: task.error, exitCode: task.exitCode, usage: task.usage },
1035
+ });
1036
+
1007
1037
  const packetArtifact = writeArtifact(manifest.artifactsRoot, {
1008
1038
  kind: "metadata",
1009
1039
  relativePath: `metadata/${task.id}.task-packet.json`,
@@ -28,6 +28,7 @@ import { executeWithRetry, DEFAULT_RETRY_POLICY, type RetryPolicy } from "./retr
28
28
  import { appendDeadletter } from "./deadletter.ts";
29
29
  import type { MetricRegistry } from "../observability/metric-registry.ts";
30
30
  import { childCorrelation, withCorrelation } from "../observability/correlation.ts";
31
+ import { crewHooks } from "./crew-hooks.ts";
31
32
  import { resolveBatchConcurrency } from "./concurrency.ts";
32
33
  import { mapConcurrent } from "./parallel-utils.ts";
33
34
  import { permissionForRole } from "./role-permission.ts";
@@ -279,6 +280,10 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
279
280
  cleanupUsage();
280
281
  // Terminate live agents for this run — agents are done when the run ends.
281
282
  void terminateLiveAgentsForRun(manifest.runId, "completed", appendEvent, manifest.eventsPath).catch(() => {});
283
+
284
+ // Emit run completion hook (100% reliable, fire-and-forget)
285
+ crewHooks.emit({ type: "run_completed", timestamp: new Date().toISOString(), runId: manifest.runId, data: { status: result.manifest.status, taskCount: result.tasks.length } });
286
+
282
287
  return result;
283
288
  } catch (error) {
284
289
  // P1: Catch unhandled errors — ensure manifest/tasks/agents are terminal so they don't stay "running" forever.
@@ -310,6 +315,7 @@ export async function executeTeamRun(input: ExecuteTeamRunInput): Promise<{ mani
310
315
  }
311
316
  const result = { manifest, tasks };
312
317
  rejectRunPromise(manifest.runId, error instanceof Error ? error : new Error(message));
318
+ crewHooks.emit({ type: "run_failed", timestamp: new Date().toISOString(), runId: manifest.runId, data: { status: manifest.status, error: message } });
313
319
  cleanupUsage();
314
320
  return result;
315
321
  }