pi-crew 0.1.46 → 0.1.49

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 (253) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/agents/analyst.md +11 -11
  3. package/agents/critic.md +11 -11
  4. package/agents/executor.md +11 -11
  5. package/agents/explorer.md +11 -11
  6. package/agents/planner.md +11 -11
  7. package/agents/reviewer.md +11 -11
  8. package/agents/security-reviewer.md +11 -11
  9. package/agents/test-engineer.md +11 -11
  10. package/agents/verifier.md +11 -11
  11. package/agents/writer.md +11 -11
  12. package/docs/next-upgrade-roadmap.md +117 -42
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research/AGENT-EXECUTION-ARCHITECTURE.md +261 -0
  18. package/docs/research/AGENT-LIFECYCLE-COMPARISON.md +111 -0
  19. package/docs/research/AUDIT_OH_MY_PI.md +261 -0
  20. package/docs/research/AUDIT_PI_CREW.md +457 -0
  21. package/docs/research/CAVEMAN-DEEP-RESEARCH.md +281 -0
  22. package/docs/research/COMPARISON_OH_MY_PI_VS_PI_CREW.md +264 -0
  23. package/docs/research/DEEP-RESEARCH-PI-POWERBAR.md +343 -0
  24. package/docs/research/DEEP_RESEARCH_SUBAGENT_ARCHITECTURE.md +480 -0
  25. package/docs/research/GAP_CLOSURE_IMPLEMENTATION_PLAN.md +354 -0
  26. package/docs/research/IMPLEMENTATION_PLAN.md +385 -0
  27. package/docs/research/LIVE-SESSION-PRODUCTION-READY-PLAN.md +502 -0
  28. package/docs/research/OH-MY-PI-DEEP-RESEARCH-v14.7.6.md +266 -0
  29. package/docs/research/REMAINING-GAPS-PLAN.md +363 -0
  30. package/docs/research/SESSION-SUMMARY-2026-05-08.md +146 -0
  31. package/docs/research/UI-RESPONSIVENESS-AUDIT.md +173 -0
  32. package/docs/research-awesome-agent-skills-distillation.md +100 -100
  33. package/docs/research-extension-examples.md +297 -297
  34. package/docs/research-extension-system.md +324 -324
  35. package/docs/research-oh-my-pi-distillation.md +56 -9
  36. package/docs/research-optimization-plan.md +548 -548
  37. package/docs/research-phase10-distillation.md +198 -198
  38. package/docs/research-phase11-distillation.md +201 -201
  39. package/docs/research-pi-coding-agent.md +357 -357
  40. package/docs/research-source-pi-crew-reference.md +174 -174
  41. package/docs/runtime-flow.md +148 -148
  42. package/docs/source-runtime-refactor-map.md +107 -107
  43. package/index.ts +6 -6
  44. package/package.json +99 -98
  45. package/schema.json +8 -0
  46. package/skills/async-worker-recovery/SKILL.md +42 -42
  47. package/skills/context-artifact-hygiene/SKILL.md +52 -52
  48. package/skills/delegation-patterns/SKILL.md +54 -54
  49. package/skills/mailbox-interactive/SKILL.md +40 -40
  50. package/skills/model-routing-context/SKILL.md +39 -39
  51. package/skills/multi-perspective-review/SKILL.md +58 -58
  52. package/skills/observability-reliability/SKILL.md +41 -41
  53. package/skills/orchestration/SKILL.md +157 -0
  54. package/skills/ownership-session-security/SKILL.md +41 -41
  55. package/skills/pi-extension-lifecycle/SKILL.md +39 -39
  56. package/skills/requirements-to-task-packet/SKILL.md +63 -63
  57. package/skills/resource-discovery-config/SKILL.md +41 -41
  58. package/skills/runtime-state-reader/SKILL.md +44 -44
  59. package/skills/secure-agent-orchestration-review/SKILL.md +45 -45
  60. package/skills/state-mutation-locking/SKILL.md +42 -42
  61. package/skills/systematic-debugging/SKILL.md +67 -67
  62. package/skills/ui-render-performance/SKILL.md +39 -39
  63. package/skills/verification-before-done/SKILL.md +57 -57
  64. package/skills/worktree-isolation/SKILL.md +39 -39
  65. package/src/agents/agent-config.ts +6 -0
  66. package/src/agents/agent-search.ts +98 -0
  67. package/src/agents/agent-serializer.ts +4 -0
  68. package/src/agents/discover-agents.ts +17 -4
  69. package/src/config/config.ts +24 -0
  70. package/src/config/defaults.ts +11 -0
  71. package/src/extension/autonomous-policy.ts +26 -33
  72. package/src/extension/cross-extension-rpc.ts +82 -82
  73. package/src/extension/help.ts +1 -0
  74. package/src/extension/management.ts +5 -0
  75. package/src/extension/register.ts +58 -13
  76. package/src/extension/registration/commands.ts +33 -1
  77. package/src/extension/registration/compaction-guard.ts +125 -125
  78. package/src/extension/registration/team-tool.ts +6 -4
  79. package/src/extension/run-bundle-schema.ts +89 -89
  80. package/src/extension/run-index.ts +24 -18
  81. package/src/extension/run-maintenance.ts +68 -62
  82. package/src/extension/team-tool/api.ts +23 -2
  83. package/src/extension/team-tool/cancel.ts +86 -11
  84. package/src/extension/team-tool/context.ts +3 -0
  85. package/src/extension/team-tool/handle-settings.ts +188 -188
  86. package/src/extension/team-tool/inspect.ts +41 -41
  87. package/src/extension/team-tool/intent-policy.ts +42 -0
  88. package/src/extension/team-tool/lifecycle-actions.ts +47 -18
  89. package/src/extension/team-tool/parallel-dispatch.ts +156 -0
  90. package/src/extension/team-tool/plan.ts +19 -19
  91. package/src/extension/team-tool/respond.ts +10 -2
  92. package/src/extension/team-tool/run.ts +3 -2
  93. package/src/extension/team-tool/status.ts +1 -1
  94. package/src/extension/team-tool-types.ts +1 -0
  95. package/src/extension/team-tool.ts +13 -3
  96. package/src/hooks/registry.ts +61 -0
  97. package/src/hooks/types.ts +41 -0
  98. package/src/i18n.ts +184 -184
  99. package/src/observability/exporters/otlp-exporter.ts +77 -77
  100. package/src/prompt/prompt-runtime.ts +72 -72
  101. package/src/runtime/agent-control.ts +108 -2
  102. package/src/runtime/agent-memory.ts +72 -72
  103. package/src/runtime/agent-observability.ts +114 -114
  104. package/src/runtime/async-marker.ts +26 -26
  105. package/src/runtime/async-runner.ts +3 -1
  106. package/src/runtime/attention-events.ts +28 -28
  107. package/src/runtime/background-runner.ts +19 -0
  108. package/src/runtime/cancellation-token.ts +89 -0
  109. package/src/runtime/cancellation.ts +61 -51
  110. package/src/runtime/capability-inventory.ts +116 -0
  111. package/src/runtime/child-pi.ts +2 -1
  112. package/src/runtime/code-summary.ts +247 -0
  113. package/src/runtime/completion-guard.ts +190 -190
  114. package/src/runtime/crash-recovery.ts +181 -0
  115. package/src/runtime/crew-agent-records.ts +35 -7
  116. package/src/runtime/crew-agent-runtime.ts +1 -0
  117. package/src/runtime/custom-tools/irc-tool.ts +201 -0
  118. package/src/runtime/custom-tools/submit-result-tool.ts +90 -0
  119. package/src/runtime/delivery-coordinator.ts +3 -1
  120. package/src/runtime/direct-run.ts +35 -35
  121. package/src/runtime/effectiveness.ts +81 -76
  122. package/src/runtime/event-stream-bridge.ts +90 -0
  123. package/src/runtime/foreground-control.ts +82 -82
  124. package/src/runtime/green-contract.ts +46 -46
  125. package/src/runtime/group-join.ts +106 -106
  126. package/src/runtime/heartbeat-gradient.ts +28 -28
  127. package/src/runtime/heartbeat-watcher.ts +124 -124
  128. package/src/runtime/live-agent-control.ts +88 -88
  129. package/src/runtime/live-agent-manager.ts +78 -2
  130. package/src/runtime/live-control-realtime.ts +36 -36
  131. package/src/runtime/live-extension-bridge.ts +150 -0
  132. package/src/runtime/live-irc.ts +92 -0
  133. package/src/runtime/live-session-health.ts +100 -0
  134. package/src/runtime/live-session-runtime.ts +297 -7
  135. package/src/runtime/mcp-proxy.ts +113 -0
  136. package/src/runtime/notebook-helpers.ts +90 -0
  137. package/src/runtime/orphan-sentinel.ts +7 -0
  138. package/src/runtime/output-validator.ts +187 -0
  139. package/src/runtime/parallel-research.ts +44 -44
  140. package/src/runtime/parallel-utils.ts +57 -0
  141. package/src/runtime/parent-guard.ts +80 -0
  142. package/src/runtime/pi-json-output.ts +111 -111
  143. package/src/runtime/policy-engine.ts +79 -79
  144. package/src/runtime/progress-event-coalescer.ts +43 -43
  145. package/src/runtime/prose-compressor.ts +164 -0
  146. package/src/runtime/recovery-recipes.ts +74 -74
  147. package/src/runtime/result-extractor.ts +121 -0
  148. package/src/runtime/role-permission.ts +39 -39
  149. package/src/runtime/runtime-resolver.ts +1 -4
  150. package/src/runtime/semaphore.ts +131 -0
  151. package/src/runtime/sensitive-paths.ts +92 -0
  152. package/src/runtime/session-resources.ts +25 -25
  153. package/src/runtime/session-snapshot.ts +59 -59
  154. package/src/runtime/session-usage.ts +79 -79
  155. package/src/runtime/sidechain-output.ts +29 -29
  156. package/src/runtime/stream-preview.ts +177 -0
  157. package/src/runtime/subagent-manager.ts +3 -2
  158. package/src/runtime/subprocess-tool-registry.ts +67 -0
  159. package/src/runtime/supervisor-contact.ts +59 -59
  160. package/src/runtime/task-display.ts +38 -38
  161. package/src/runtime/task-output-context.ts +59 -9
  162. package/src/runtime/task-runner/capabilities.ts +78 -78
  163. package/src/runtime/task-runner/live-executor.ts +2 -0
  164. package/src/runtime/task-runner/progress.ts +119 -119
  165. package/src/runtime/task-runner/prompt-builder.ts +70 -8
  166. package/src/runtime/task-runner/prompt-pipeline.ts +64 -64
  167. package/src/runtime/task-runner/result-utils.ts +14 -14
  168. package/src/runtime/task-runner/run-projection.ts +104 -0
  169. package/src/runtime/task-runner/state-helpers.ts +22 -22
  170. package/src/runtime/task-runner.ts +75 -4
  171. package/src/runtime/team-runner.ts +60 -8
  172. package/src/runtime/worker-heartbeat.ts +21 -21
  173. package/src/runtime/worker-startup.ts +57 -57
  174. package/src/runtime/workspace-tree.ts +298 -0
  175. package/src/runtime/yield-handler.ts +189 -0
  176. package/src/schema/config-schema.ts +6 -0
  177. package/src/schema/team-tool-schema.ts +11 -1
  178. package/src/skills/discover-skills.ts +67 -0
  179. package/src/state/active-run-registry.ts +4 -2
  180. package/src/state/artifact-store.ts +4 -1
  181. package/src/state/atomic-write.ts +50 -1
  182. package/src/state/blob-store.ts +117 -0
  183. package/src/state/contracts.ts +1 -0
  184. package/src/state/event-log-rotation.ts +158 -0
  185. package/src/state/event-log.ts +52 -2
  186. package/src/state/mailbox.ts +87 -7
  187. package/src/state/state-store.ts +24 -4
  188. package/src/state/task-claims.ts +44 -44
  189. package/src/state/types.ts +20 -0
  190. package/src/state/usage.ts +29 -29
  191. package/src/subagents/async-entry.ts +1 -1
  192. package/src/subagents/index.ts +3 -3
  193. package/src/subagents/live/control.ts +1 -1
  194. package/src/subagents/live/manager.ts +1 -1
  195. package/src/subagents/live/realtime.ts +1 -1
  196. package/src/subagents/live/session-runtime.ts +1 -1
  197. package/src/subagents/manager.ts +1 -1
  198. package/src/subagents/spawn.ts +1 -1
  199. package/src/teams/team-serializer.ts +38 -38
  200. package/src/types/diff.d.ts +18 -18
  201. package/src/ui/agent-management-overlay.ts +144 -0
  202. package/src/ui/crew-footer.ts +101 -101
  203. package/src/ui/crew-select-list.ts +111 -111
  204. package/src/ui/crew-widget.ts +11 -2
  205. package/src/ui/dashboard-panes/cancellation-pane.ts +43 -0
  206. package/src/ui/dashboard-panes/capability-pane.ts +60 -0
  207. package/src/ui/dashboard-panes/mailbox-pane.ts +35 -11
  208. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  209. package/src/ui/dynamic-border.ts +25 -25
  210. package/src/ui/layout-primitives.ts +106 -106
  211. package/src/ui/live-run-sidebar.ts +4 -0
  212. package/src/ui/loaders.ts +158 -158
  213. package/src/ui/powerbar-publisher.ts +77 -15
  214. package/src/ui/render-coalescer.ts +51 -0
  215. package/src/ui/render-diff.ts +119 -119
  216. package/src/ui/render-scheduler.ts +143 -143
  217. package/src/ui/run-dashboard.ts +4 -0
  218. package/src/ui/run-event-bus.ts +209 -0
  219. package/src/ui/run-snapshot-cache.ts +68 -16
  220. package/src/ui/snapshot-types.ts +8 -0
  221. package/src/ui/spinner.ts +17 -17
  222. package/src/ui/status-colors.ts +58 -58
  223. package/src/ui/syntax-highlight.ts +116 -116
  224. package/src/ui/transcript-entries.ts +258 -0
  225. package/src/utils/atomic-write.ts +33 -33
  226. package/src/utils/completion-dedupe.ts +63 -63
  227. package/src/utils/frontmatter.ts +68 -68
  228. package/src/utils/git.ts +262 -262
  229. package/src/utils/ids.ts +17 -12
  230. package/src/utils/incremental-reader.ts +104 -0
  231. package/src/utils/names.ts +27 -27
  232. package/src/utils/redaction.ts +44 -44
  233. package/src/utils/safe-paths.ts +47 -47
  234. package/src/utils/scan-cache.ts +137 -0
  235. package/src/utils/sleep.ts +32 -32
  236. package/src/utils/sse-parser.ts +134 -0
  237. package/src/utils/task-name-generator.ts +337 -0
  238. package/src/utils/visual.ts +33 -2
  239. package/src/workflows/validate-workflow.ts +40 -40
  240. package/src/worktree/branch-freshness.ts +45 -45
  241. package/src/worktree/cleanup.ts +2 -1
  242. package/teams/default.team.md +12 -12
  243. package/teams/fast-fix.team.md +11 -11
  244. package/teams/implementation.team.md +18 -18
  245. package/teams/parallel-research.team.md +14 -14
  246. package/teams/research.team.md +11 -11
  247. package/teams/review.team.md +12 -12
  248. package/workflows/default.workflow.md +29 -29
  249. package/workflows/fast-fix.workflow.md +22 -22
  250. package/workflows/implementation.workflow.md +38 -38
  251. package/workflows/parallel-research.workflow.md +46 -46
  252. package/workflows/research.workflow.md +22 -22
  253. package/workflows/review.workflow.md +30 -30
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import * as fs from "node:fs";
2
3
  import type { MetricRegistry } from "../observability/metric-registry.ts";
3
4
  import { appendEvent, scanSequence } from "../state/event-log.ts";
4
5
  import { withRunLockSync } from "../state/locks.ts";
@@ -8,6 +9,8 @@ import { isWorkerHeartbeatStale } from "./worker-heartbeat.ts";
8
9
  import type { ManifestCache } from "./manifest-cache.ts";
9
10
  import { checkProcessLiveness } from "./process-status.ts";
10
11
  import { reconcileStaleRun, type ReconcileResult } from "./stale-reconciler.ts";
12
+ import { executeHook, appendHookEvent } from "../hooks/registry.ts";
13
+ import { activeRunEntries, unregisterActiveRun, readActiveRunRegistry } from "../state/active-run-registry.ts";
11
14
 
12
15
  export interface RecoveryPlan {
13
16
  runId: string;
@@ -43,6 +46,14 @@ export function detectInterruptedRuns(cwd: string, manifestCache: ManifestCache,
43
46
  export async function applyRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionContext, "cwd">, registry?: MetricRegistry): Promise<void> {
44
47
  const loaded = loadRunManifestById(ctx.cwd, plan.runId);
45
48
  if (!loaded) throw new Error(`Run '${plan.runId}' not found.`);
49
+
50
+ const hookReport = await executeHook("run_recovery", { runId: plan.runId, cwd: ctx.cwd });
51
+ appendHookEvent(loaded.manifest, hookReport);
52
+ if (hookReport.outcome === "block") {
53
+ appendEvent(loaded.manifest.eventsPath, { type: "crew.run.recovery_blocked", runId: plan.runId, message: `Recovery blocked by hook: ${hookReport.reason ?? "run_recovery hook blocked the operation."}`, data: { hookOutcome: "block", reason: hookReport.reason } });
54
+ return;
55
+ }
56
+
46
57
  const reset = new Set(plan.resumableTasks);
47
58
  const tasks = loaded.tasks.map((task) => reset.has(task.id) ? { ...task, status: "queued" as const, startedAt: undefined, finishedAt: undefined, error: undefined, heartbeat: undefined } : task);
48
59
  saveRunTasks(loaded.manifest, tasks);
@@ -62,6 +73,176 @@ export function declineRecoveryPlan(plan: RecoveryPlan, ctx: Pick<ExtensionConte
62
73
  * Run 3-phase stale reconciliation on all active runs.
63
74
  * Returns results for each reconciled run.
64
75
  */
76
+ /**
77
+ * Auto-cancel orphaned runs whose owner session no longer exists.
78
+ *
79
+ * When a Pi session dies (crash, force-close, Ctrl+C), `session_shutdown`
80
+ * does not fire and child workers are not terminated. The next Pi session
81
+ * must detect these orphaned runs and cancel them.
82
+ *
83
+ * Criteria for orphan detection:
84
+ * 1. Manifest status is "running"
85
+ * 2. Manifest has an `ownerSessionId` that is NOT the current session
86
+ * 3. The owner session's process is no longer alive (PID check)
87
+ * 4. No recent heartbeat activity (task heartbeat or agent progress within threshold)
88
+ *
89
+ * Returns the number of runs cancelled.
90
+ */
91
+ export function cancelOrphanedRuns(
92
+ cwd: string,
93
+ manifestCache: ManifestCache,
94
+ currentSessionId: string,
95
+ staleThresholdMs = 300_000,
96
+ now = Date.now(),
97
+ ): { cancelled: string[]; skipped: string[] } {
98
+ const cancelled: string[] = [];
99
+ const skipped: string[] = [];
100
+
101
+ // Phase 1: Scan project-level manifests via manifestCache
102
+ for (const manifest of manifestCache.list(50)) {
103
+ if (manifest.status !== "running") continue;
104
+
105
+ // Only consider runs owned by a different session
106
+ const ownerId = manifest.ownerSessionId;
107
+ if (!ownerId || ownerId === currentSessionId) continue;
108
+
109
+ // Check if the owner process is still alive
110
+ const ownerPid = manifest.async?.pid;
111
+ if (ownerPid !== undefined && checkProcessLiveness(ownerPid).alive) {
112
+ skipped.push(manifest.runId);
113
+ continue;
114
+ }
115
+
116
+ // Check for recent heartbeat activity
117
+ const loaded = loadRunManifestById(cwd, manifest.runId);
118
+ if (!loaded) continue;
119
+
120
+ const hasRecentActivity = loaded.tasks.some((task) => {
121
+ if (task.status !== "running" && task.status !== "waiting") return false;
122
+ const heartbeatAt = task.heartbeat?.lastSeenAt ? new Date(task.heartbeat.lastSeenAt).getTime() : Number.NaN;
123
+ if (task.heartbeat?.alive !== false && Number.isFinite(heartbeatAt) && now - heartbeatAt <= staleThresholdMs) return true;
124
+ const activityAt = task.agentProgress?.lastActivityAt ? new Date(task.agentProgress.lastActivityAt).getTime() : Number.NaN;
125
+ return Number.isFinite(activityAt) && now - activityAt <= staleThresholdMs;
126
+ });
127
+
128
+ if (hasRecentActivity) {
129
+ skipped.push(manifest.runId);
130
+ continue;
131
+ }
132
+
133
+ // Orphan confirmed — cancel all running tasks
134
+ withRunLockSync(loaded.manifest, () => {
135
+ const fresh = loadRunManifestById(cwd, manifest.runId);
136
+ if (!fresh || fresh.manifest.status !== "running") return;
137
+
138
+ const now_iso = new Date(now).toISOString();
139
+ const repairedTasks = fresh.tasks.map((task) => {
140
+ if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
141
+ return { ...task, status: "cancelled" as const, finishedAt: now_iso, error: `Orphaned run: owner session ${ownerId} no longer exists` };
142
+ }
143
+ return task;
144
+ });
145
+
146
+ saveRunTasks(fresh.manifest, repairedTasks);
147
+ updateRunStatus(fresh.manifest, "cancelled", `Orphaned run: owner session ${ownerId} no longer exists`);
148
+ appendEvent(fresh.manifest.eventsPath, { type: "crew.run.orphan_cancelled", runId: manifest.runId, message: `Auto-cancelled orphaned run (owner: ${ownerId})`, data: { ownerSessionId: ownerId, cancelledTasks: repairedTasks.filter((t) => t.status === "cancelled").length } });
149
+ cancelled.push(manifest.runId);
150
+ });
151
+ }
152
+
153
+ return { cancelled, skipped };
154
+ }
155
+
156
+ /**
157
+ * Purge the global active-run-index of entries whose manifest is no longer active.
158
+ *
159
+ * This scans every entry in active-run-index.json and removes any whose:
160
+ * - manifest file no longer exists, OR
161
+ * - manifest status is terminal (completed/failed/cancelled/blocked), OR
162
+ * - manifest cwd directory no longer exists (e.g. temp test dirs)
163
+ *
164
+ * Also removes entries where the manifest is still "running" but:
165
+ * - The cwd has been deleted (temp dir cleanup)
166
+ * - The async worker PID is dead AND no heartbeat for > threshold
167
+ *
168
+ * This is the **global** cleanup that cancelOrphanedRuns (project-scoped)
169
+ * cannot reach.
170
+ */
171
+ export function purgeStaleActiveRunIndex(staleThresholdMs = 300_000, now = Date.now()): { purged: string[]; kept: string[] } {
172
+ const purged: string[] = [];
173
+ const kept: string[] = [];
174
+ const entries = readActiveRunRegistry();
175
+
176
+ for (const entry of entries) {
177
+ // 1. Manifest file gone → definitely stale
178
+ if (!fs.existsSync(entry.manifestPath)) {
179
+ unregisterActiveRun(entry.runId);
180
+ purged.push(entry.runId);
181
+ continue;
182
+ }
183
+
184
+ // 2. CWD gone → temp dir cleaned up
185
+ if (!fs.existsSync(entry.cwd)) {
186
+ unregisterActiveRun(entry.runId);
187
+ purged.push(entry.runId);
188
+ continue;
189
+ }
190
+
191
+ // 3. Read manifest status
192
+ let manifest: { status?: string; async?: { pid?: number }; ownerSessionId?: string } | undefined;
193
+ try {
194
+ manifest = JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8"));
195
+ } catch {
196
+ unregisterActiveRun(entry.runId);
197
+ purged.push(entry.runId);
198
+ continue;
199
+ }
200
+
201
+ // 4. Terminal status → no longer active
202
+ const terminalStatuses = new Set(["completed", "failed", "cancelled", "blocked"]);
203
+ if (manifest && terminalStatuses.has(manifest.status ?? "")) {
204
+ unregisterActiveRun(entry.runId);
205
+ purged.push(entry.runId);
206
+ continue;
207
+ }
208
+
209
+ // 5. Still "running" — check if worker PID is dead and no heartbeat
210
+ if (manifest?.status === "running" && manifest.async?.pid !== undefined) {
211
+ const pidAlive = checkProcessLiveness(manifest.async.pid).alive;
212
+ if (!pidAlive) {
213
+ // Check age — if manifest hasn't been updated in > threshold, it's stale
214
+ const updatedAt = new Date(entry.updatedAt).getTime();
215
+ if (Number.isFinite(updatedAt) && now - updatedAt > staleThresholdMs) {
216
+ // Dead PID + stale update → cancel the manifest and unregister
217
+ try {
218
+ const fullLoaded = loadRunManifestById(entry.cwd, entry.runId);
219
+ if (fullLoaded) {
220
+ const now_iso = new Date(now).toISOString();
221
+ const repairedTasks = fullLoaded.tasks.map((task) => {
222
+ if (task.status === "running" || task.status === "queued" || task.status === "waiting") {
223
+ return { ...task, status: "cancelled" as const, finishedAt: now_iso, error: "Orphaned run: worker process dead and no recent activity" };
224
+ }
225
+ return task;
226
+ });
227
+ saveRunTasks(fullLoaded.manifest, repairedTasks);
228
+ updateRunStatus(fullLoaded.manifest, "cancelled", "Orphaned run: worker process dead and no recent activity");
229
+ }
230
+ } catch {
231
+ // Best-effort manifest cleanup
232
+ }
233
+ unregisterActiveRun(entry.runId);
234
+ purged.push(entry.runId);
235
+ continue;
236
+ }
237
+ }
238
+ }
239
+
240
+ kept.push(entry.runId);
241
+ }
242
+
243
+ return { purged, kept };
244
+ }
245
+
65
246
  export function reconcileAllStaleRuns(cwd: string, manifestCache: ManifestCache, now = Date.now()): ReconcileResult[] {
66
247
  const results: ReconcileResult[] = [];
67
248
  for (const manifest of manifestCache.list(50)) {
@@ -81,7 +81,20 @@ function setAsyncAgentReaderCache(filePath: string, entry: { expiresAt: number;
81
81
 
82
82
  export function readCrewAgents(manifest: TeamRunManifest): CrewAgentRecord[] {
83
83
  try {
84
- return readJsonFileCoalesced(agentsPath(manifest), AGENT_READER_TTL_MS, () => readJsonFile<CrewAgentRecord[]>(agentsPath(manifest)) ?? []);
84
+ const records = readJsonFileCoalesced(agentsPath(manifest), AGENT_READER_TTL_MS, () => readJsonFile<CrewAgentRecord[]>(agentsPath(manifest)) ?? []);
85
+ // Validate schema and deduplicate by id to handle concurrent write conflicts
86
+ const seen = new Set<string>();
87
+ const deduped = records.filter((r) => {
88
+ if (!r || typeof r.id !== "string" || typeof r.taskId !== "string") return false;
89
+ if (seen.has(r.id)) return false;
90
+ seen.add(r.id);
91
+ return true;
92
+ });
93
+ if (deduped.length !== records.length) {
94
+ // Schema mismatch or duplicates detected — save corrected state
95
+ saveCrewAgents(manifest, deduped);
96
+ }
97
+ return deduped;
85
98
  } catch {
86
99
  return [];
87
100
  }
@@ -96,9 +109,20 @@ export async function readCrewAgentsAsync(manifest: TeamRunManifest): Promise<Cr
96
109
  const inFlight = (async (): Promise<CrewAgentRecord[]> => {
97
110
  try {
98
111
  const parsed = JSON.parse(await fs.promises.readFile(filePath, "utf-8")) as unknown;
99
- const records = Array.isArray(parsed) ? redactSecrets(parsed) as CrewAgentRecord[] : [];
100
- setAsyncAgentReaderCache(filePath, { expiresAt: Date.now() + AGENT_READER_TTL_MS, records });
101
- return records;
112
+ const raw = Array.isArray(parsed) ? redactSecrets(parsed) as CrewAgentRecord[] : [];
113
+ // Deduplicate by id to handle concurrent write conflicts
114
+ const seen = new Set<string>();
115
+ const deduped = raw.filter((r) => {
116
+ if (!r || typeof r.id !== "string" || typeof r.taskId !== "string") return false;
117
+ if (seen.has(r.id)) return false;
118
+ seen.add(r.id);
119
+ return true;
120
+ });
121
+ if (deduped.length !== raw.length) {
122
+ try { saveCrewAgents(manifest, deduped); } catch { /* best-effort */ }
123
+ }
124
+ setAsyncAgentReaderCache(filePath, { expiresAt: Date.now() + AGENT_READER_TTL_MS, records: deduped });
125
+ return deduped;
102
126
  } catch {
103
127
  setAsyncAgentReaderCache(filePath, { expiresAt: Date.now() + AGENT_READER_TTL_MS, records: [] });
104
128
  return [];
@@ -117,9 +141,13 @@ export function saveCrewAgents(manifest: TeamRunManifest, records: CrewAgentReco
117
141
  }
118
142
 
119
143
  export function upsertCrewAgent(manifest: TeamRunManifest, record: CrewAgentRecord): void {
120
- const records = readCrewAgents(manifest).filter((item) => item.id !== record.id);
121
- records.push(record);
122
- saveCrewAgents(manifest, records);
144
+ // Read current state
145
+ const existing = readCrewAgents(manifest);
146
+ // Deduplicate by id: keep newer record when same id appears
147
+ const idIndex = new Map(existing.map((item, i) => [item.id, i]));
148
+ const merged: CrewAgentRecord[] = existing.map((item) => item.id === record.id ? record : item);
149
+ if (!idIndex.has(record.id)) merged.push(record);
150
+ saveCrewAgents(manifest, merged);
123
151
  writeCrewAgentStatus(manifest, record);
124
152
  }
125
153
 
@@ -23,6 +23,7 @@ export interface CrewAgentProgress {
23
23
  lastActivityAt?: string;
24
24
  activityState?: CrewActivityState;
25
25
  failedTool?: string;
26
+ consecutiveFailures?: number;
26
27
  }
27
28
 
28
29
  export interface CrewAgentRecord {
@@ -0,0 +1,201 @@
1
+ /**
2
+ * G1: Custom tool — irc.
3
+ *
4
+ * Registers a real `irc` tool in the Pi SDK session so that
5
+ * live-session workers can send messages to other live agents.
6
+ *
7
+ * Operations:
8
+ * - `list`: List currently visible peer agents
9
+ * - `send`: Send a message to a specific agent or broadcast to all
10
+ *
11
+ * Adapted from oh-my-pi's `IrcTool` pattern. Uses the live-agent-manager
12
+ * for routing messages between in-process workers.
13
+ */
14
+
15
+ import { defineTool, type ToolDefinition } from "@mariozechner/pi-coding-agent";
16
+ import { Type, type Static } from "@sinclair/typebox";
17
+ import { listLiveAgents, sendIrcMessage, broadcastIrcMessage } from "../live-agent-manager.ts";
18
+ import type { IrcMessage } from "../live-irc.ts";
19
+
20
+ const IrcParams = Type.Object({
21
+ op: Type.Union(
22
+ [
23
+ Type.Literal("send", { description: "Send a message to one peer or to all peers." }),
24
+ Type.Literal("list", { description: "List currently visible peers." }),
25
+ ],
26
+ { description: "IRC operation." },
27
+ ),
28
+ to: Type.Optional(
29
+ Type.String({
30
+ description: 'Recipient agent ID or "all" to broadcast.',
31
+ }),
32
+ ),
33
+ message: Type.Optional(
34
+ Type.String({
35
+ description: "Message body to deliver.",
36
+ }),
37
+ ),
38
+ awaitReply: Type.Optional(
39
+ Type.Boolean({
40
+ description: "Wait for a reply (default: true for DM, false for broadcast). Not yet supported — messages are fire-and-forget.",
41
+ }),
42
+ ),
43
+ });
44
+
45
+ type IrcParams = Static<typeof IrcParams>;
46
+
47
+ interface IrcDetails {
48
+ op: "send" | "list";
49
+ from?: string;
50
+ to?: string;
51
+ delivered?: string[];
52
+ notFound?: string[];
53
+ peers?: Array<{ id: string; status: string }>;
54
+ error?: string;
55
+ }
56
+
57
+ /**
58
+ * Create an `irc` tool definition for a specific agent.
59
+ *
60
+ * @param selfId — This agent's ID (runId:taskId format)
61
+ */
62
+ export function createIrcTool(
63
+ selfId: string,
64
+ ): ToolDefinition<typeof IrcParams, IrcDetails> {
65
+ return defineTool({
66
+ name: "irc",
67
+ label: "IRC",
68
+ description:
69
+ "Send messages to other live agents in same team. " +
70
+ 'Use `op: "list"` to see peers, `op: "send"` with `to` (agent ID or "all") and `message` to communicate.',
71
+ parameters: IrcParams,
72
+ promptSnippet: "Send messages to other live agents via the irc tool",
73
+ promptGuidelines: [
74
+ "Use irc to coordinate with other agents when you need information or want to share findings.",
75
+ 'Use `op: "list"` first to discover available peers.',
76
+ ],
77
+ async execute(
78
+ _toolCallId: string,
79
+ params: IrcParams,
80
+ _signal: AbortSignal | undefined,
81
+ _onUpdate: unknown,
82
+ _ctx: unknown,
83
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: IrcDetails }> {
84
+ if (params.op === "list") {
85
+ return executeList(selfId);
86
+ }
87
+ if (params.op === "send") {
88
+ return executeSend(selfId, params);
89
+ }
90
+ return {
91
+ content: [{ type: "text", text: "Unknown irc op." }],
92
+ details: { op: params.op, from: selfId, error: "Unknown operation." },
93
+ };
94
+ },
95
+ });
96
+ }
97
+
98
+ function executeList(selfId: string): { content: Array<{ type: "text"; text: string }>; details: IrcDetails } {
99
+ const agents = listLiveAgents();
100
+ const peers = agents
101
+ .filter((a) => a.agentId !== selfId && (a.status === "running" || a.status === "queued"))
102
+ .map((a) => ({ id: a.agentId, status: a.status }));
103
+
104
+ const lines: string[] = [];
105
+ if (peers.length === 0) {
106
+ lines.push("No other live agents.");
107
+ } else {
108
+ lines.push(`${peers.length} peer(s):`);
109
+ for (const peer of peers) {
110
+ lines.push(`- ${peer.id} (${peer.status})`);
111
+ }
112
+ }
113
+
114
+ return {
115
+ content: [{ type: "text", text: lines.join("\n") }],
116
+ details: { op: "list", from: selfId, peers },
117
+ };
118
+ }
119
+
120
+ function executeSend(
121
+ selfId: string,
122
+ params: IrcParams,
123
+ ): { content: Array<{ type: "text"; text: string }>; details: IrcDetails } {
124
+ const to = params.to?.trim();
125
+ const message = params.message?.trim();
126
+
127
+ if (!to) {
128
+ return {
129
+ content: [{ type: "text", text: '`to` is required for op="send".' }],
130
+ details: { op: "send", from: selfId, error: "Missing 'to' field." },
131
+ };
132
+ }
133
+ if (!message) {
134
+ return {
135
+ content: [{ type: "text", text: '`message` is required for op="send".' }],
136
+ details: { op: "send", from: selfId, to, error: "Missing 'message' field." },
137
+ };
138
+ }
139
+ if (to === selfId) {
140
+ return {
141
+ content: [{ type: "text", text: "Cannot send a message to yourself." }],
142
+ details: { op: "send", from: selfId, to, error: "Self-message not allowed." },
143
+ };
144
+ }
145
+
146
+ const ircMessage: IrcMessage = {
147
+ from: selfId,
148
+ to,
149
+ content: message,
150
+ timestamp: new Date().toISOString(),
151
+ awaitReply: params.awaitReply,
152
+ };
153
+
154
+ const notFound: string[] = [];
155
+ const delivered: string[] = [];
156
+
157
+ try {
158
+ if (to === "all") {
159
+ const recipients = broadcastIrcMessage(selfId, ircMessage);
160
+ delivered.push(...recipients);
161
+ } else {
162
+ // DM to specific agent
163
+ const agents = listLiveAgents();
164
+ const target = agents.find((a) => a.agentId === to);
165
+ if (!target || (target.status !== "running" && target.status !== "queued")) {
166
+ notFound.push(to);
167
+ } else {
168
+ try {
169
+ sendIrcMessage(to, ircMessage);
170
+ delivered.push(to);
171
+ } catch {
172
+ notFound.push(to);
173
+ }
174
+ }
175
+ }
176
+ } catch {
177
+ // Agent deregistered during routing — treat as not found
178
+ notFound.push(to);
179
+ }
180
+
181
+ const lines: string[] = [];
182
+ if (delivered.length > 0) {
183
+ lines.push(`Delivered to ${delivered.length} peer(s): ${delivered.join(", ")}`);
184
+ } else {
185
+ lines.push("No recipients received the message.");
186
+ }
187
+ if (notFound.length > 0) {
188
+ lines.push(`Unknown / unavailable peers: ${notFound.join(", ")}`);
189
+ }
190
+
191
+ return {
192
+ content: [{ type: "text", text: lines.join("\n") }],
193
+ details: {
194
+ op: "send",
195
+ from: selfId,
196
+ to,
197
+ delivered: delivered.length > 0 ? delivered : undefined,
198
+ notFound: notFound.length > 0 ? notFound : undefined,
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * G1: Custom tool — submit_result.
3
+ *
4
+ * Registers a real `submit_result` tool in the Pi SDK session so that
5
+ * live-session workers can yield their result by calling a tool (instead of
6
+ * relying solely on prompt-based reminders).
7
+ *
8
+ * Adapted from oh-my-pi's `YieldTool` pattern. Uses Pi SDK's `defineTool()`
9
+ * and TypeBox schemas for validation.
10
+ */
11
+
12
+ import { defineTool, type ToolDefinition } from "@mariozechner/pi-coding-agent";
13
+ import { Type, type Static } from "@sinclair/typebox";
14
+ import type { YieldResult } from "../yield-handler.ts";
15
+
16
+ const SubmitResultParams = Type.Object({
17
+ summary: Type.String({ description: "Summary of completed work." }),
18
+ artifacts: Type.Optional(
19
+ Type.Record(Type.String(), Type.String(), {
20
+ description: "Key-value map of artifact labels to file paths or content.",
21
+ }),
22
+ ),
23
+ structuredData: Type.Optional(
24
+ Type.Record(Type.String(), Type.Unknown(), {
25
+ description: "Structured key-value data to pass back to the orchestrator.",
26
+ }),
27
+ ),
28
+ });
29
+
30
+ type SubmitResultParams = Static<typeof SubmitResultParams>;
31
+
32
+ interface SubmitResultDetails {
33
+ summary: string;
34
+ artifacts?: Record<string, string>;
35
+ structuredData?: Record<string, unknown>;
36
+ }
37
+
38
+ /**
39
+ * Create a `submit_result` tool definition that calls `onYield` when invoked.
40
+ *
41
+ * The tool is injected into the session via `createAgentSession({ customTools: [...] })`.
42
+ * When the model calls it, the result is captured via the `onYield` callback
43
+ * and the yield enforcement loop terminates.
44
+ */
45
+ export function createSubmitResultTool(
46
+ onYield: (result: YieldResult) => void,
47
+ ): ToolDefinition<typeof SubmitResultParams, SubmitResultDetails> {
48
+ return defineTool({
49
+ name: "submit_result",
50
+ label: "Submit Result",
51
+ description:
52
+ "Submit final task result. Call when task complete. " +
53
+ "Provide summary, optional artifacts (file paths/content), optional structured data.",
54
+ parameters: SubmitResultParams,
55
+ promptSnippet: "Submit your task result when done using submit_result",
56
+ promptGuidelines: [
57
+ "Always call submit_result when your task is complete, even if you were unable to finish.",
58
+ "Include a clear summary of what was accomplished.",
59
+ ],
60
+ async execute(
61
+ toolCallId: string,
62
+ params: SubmitResultParams,
63
+ _signal: AbortSignal | undefined,
64
+ _onUpdate: unknown,
65
+ _ctx: unknown,
66
+ ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SubmitResultDetails }> {
67
+ const result: YieldResult = {
68
+ summary: params.summary,
69
+ toolCallId,
70
+ ...(params.artifacts ? { artifacts: params.artifacts } : {}),
71
+ ...(params.structuredData ? { structuredData: params.structuredData } : {}),
72
+ };
73
+ // Build response first so the model always gets confirmation
74
+ const response: { content: Array<{ type: "text"; text: string }>; details: SubmitResultDetails } = {
75
+ content: [{ type: "text", text: "Result submitted successfully. Thank you." }],
76
+ details: {
77
+ summary: params.summary,
78
+ artifacts: params.artifacts,
79
+ structuredData: params.structuredData,
80
+ },
81
+ };
82
+ try {
83
+ onYield(result);
84
+ } catch {
85
+ // Yield handler failure should not prevent tool response
86
+ }
87
+ return response;
88
+ },
89
+ });
90
+ }
@@ -102,8 +102,10 @@ export class DeliveryCoordinator {
102
102
 
103
103
  flushQueuedResults(): void {
104
104
  if (!this.active || this.pending.length === 0) return;
105
- const batch = this.pending.splice(0);
105
+ // H7: Set flushing BEFORE splice to prevent re-entrancy
106
+ if (this.flushing) return;
106
107
  this.flushing = true;
108
+ const batch = this.pending.splice(0);
107
109
  try {
108
110
  const retryLater: PendingDelivery[] = [];
109
111
  for (const delivery of batch) {
@@ -1,35 +1,35 @@
1
- import type { AgentConfig } from "../agents/agent-config.ts";
2
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
- import type { TeamConfig } from "../teams/team-config.ts";
4
- import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
-
6
- export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
- return manifest.workflow === "direct-agent";
8
- }
9
-
10
- export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
- if (!isDirectRun(manifest)) return undefined;
12
- const firstTask = tasks[0];
13
- const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
- const agent = agents.find((candidate) => candidate.name === agentName);
15
- const role = firstTask?.role ?? "agent";
16
- const stepId = firstTask?.stepId ?? "01_agent";
17
- return {
18
- team: {
19
- name: manifest.team,
20
- description: `Direct subagent run for ${agentName}`,
21
- source: "builtin",
22
- filePath: "<generated>",
23
- roles: [{ name: role, agent: agentName, description: agent?.description }],
24
- defaultWorkflow: "direct-agent",
25
- workspaceMode: manifest.workspaceMode,
26
- },
27
- workflow: {
28
- name: manifest.workflow ?? "direct-agent",
29
- description: `Direct task for ${agentName}`,
30
- source: "builtin",
31
- filePath: "<generated>",
32
- steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
- },
34
- };
35
- }
1
+ import type { AgentConfig } from "../agents/agent-config.ts";
2
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { TeamConfig } from "../teams/team-config.ts";
4
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
5
+
6
+ export function isDirectRun(manifest: Pick<TeamRunManifest, "team" | "workflow">): boolean {
7
+ return manifest.workflow === "direct-agent";
8
+ }
9
+
10
+ export function directTeamAndWorkflowFromRun(manifest: TeamRunManifest, tasks: TeamTaskState[], agents: AgentConfig[]): { team: TeamConfig; workflow: WorkflowConfig } | undefined {
11
+ if (!isDirectRun(manifest)) return undefined;
12
+ const firstTask = tasks[0];
13
+ const agentName = firstTask?.agent ?? (manifest.team.replace(/^direct-/, "") || "executor");
14
+ const agent = agents.find((candidate) => candidate.name === agentName);
15
+ const role = firstTask?.role ?? "agent";
16
+ const stepId = firstTask?.stepId ?? "01_agent";
17
+ return {
18
+ team: {
19
+ name: manifest.team,
20
+ description: `Direct subagent run for ${agentName}`,
21
+ source: "builtin",
22
+ filePath: "<generated>",
23
+ roles: [{ name: role, agent: agentName, description: agent?.description }],
24
+ defaultWorkflow: "direct-agent",
25
+ workspaceMode: manifest.workspaceMode,
26
+ },
27
+ workflow: {
28
+ name: manifest.workflow ?? "direct-agent",
29
+ description: `Direct task for ${agentName}`,
30
+ source: "builtin",
31
+ filePath: "<generated>",
32
+ steps: [{ id: stepId, role, task: "{goal}", model: firstTask?.model }],
33
+ },
34
+ };
35
+ }