pi-crew 0.1.43 → 0.1.45

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 (158) hide show
  1. package/CHANGELOG.md +27 -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/refactor-tasks-phase3.md +394 -394
  13. package/docs/refactor-tasks-phase4.md +564 -564
  14. package/docs/refactor-tasks-phase5.md +402 -402
  15. package/docs/refactor-tasks-phase6.md +662 -662
  16. package/docs/research-extension-examples.md +297 -297
  17. package/docs/research-extension-system.md +324 -324
  18. package/docs/research-optimization-plan.md +548 -548
  19. package/docs/research-phase10-distillation.md +199 -0
  20. package/docs/research-phase11-distillation.md +201 -0
  21. package/docs/research-pi-coding-agent.md +357 -357
  22. package/docs/research-source-pi-crew-reference.md +174 -174
  23. package/docs/runtime-flow.md +148 -148
  24. package/docs/source-runtime-refactor-map.md +83 -83
  25. package/index.ts +6 -6
  26. package/package.json +1 -1
  27. package/src/agents/agent-serializer.ts +34 -34
  28. package/src/agents/discover-agents.ts +1 -0
  29. package/src/config/config.ts +19 -0
  30. package/src/extension/cross-extension-rpc.ts +82 -82
  31. package/src/extension/register.ts +134 -8
  32. package/src/extension/registration/commands.ts +18 -2
  33. package/src/extension/registration/compaction-guard.ts +125 -125
  34. package/src/extension/registration/subagent-tools.ts +148 -148
  35. package/src/extension/registration/team-tool.ts +27 -8
  36. package/src/extension/run-bundle-schema.ts +89 -89
  37. package/src/extension/run-index.ts +19 -0
  38. package/src/extension/run-maintenance.ts +43 -43
  39. package/src/extension/team-tool/api.ts +1 -1
  40. package/src/extension/team-tool/cancel.ts +79 -4
  41. package/src/extension/team-tool/context.ts +2 -0
  42. package/src/extension/team-tool/handle-settings.ts +188 -188
  43. package/src/extension/team-tool/inspect.ts +41 -41
  44. package/src/extension/team-tool/lifecycle-actions.ts +79 -79
  45. package/src/extension/team-tool/plan.ts +19 -19
  46. package/src/extension/team-tool/respond.ts +84 -0
  47. package/src/extension/team-tool/run.ts +3 -2
  48. package/src/extension/team-tool/status.ts +7 -1
  49. package/src/extension/team-tool-types.ts +4 -0
  50. package/src/extension/team-tool.ts +2 -0
  51. package/src/i18n.ts +184 -184
  52. package/src/observability/event-to-metric.ts +6 -0
  53. package/src/observability/exporters/otlp-exporter.ts +77 -77
  54. package/src/prompt/prompt-runtime.ts +72 -72
  55. package/src/runtime/agent-control.ts +63 -63
  56. package/src/runtime/agent-memory.ts +72 -72
  57. package/src/runtime/agent-observability.ts +114 -114
  58. package/src/runtime/async-marker.ts +26 -26
  59. package/src/runtime/attention-events.ts +28 -28
  60. package/src/runtime/background-runner.ts +53 -53
  61. package/src/runtime/child-pi.ts +444 -444
  62. package/src/runtime/completion-guard.ts +87 -0
  63. package/src/runtime/crash-recovery.ts +30 -0
  64. package/src/runtime/crew-agent-records.ts +8 -0
  65. package/src/runtime/crew-agent-runtime.ts +2 -1
  66. package/src/runtime/delivery-coordinator.ts +154 -0
  67. package/src/runtime/direct-run.ts +35 -35
  68. package/src/runtime/foreground-control.ts +82 -82
  69. package/src/runtime/green-contract.ts +46 -46
  70. package/src/runtime/group-join.ts +106 -106
  71. package/src/runtime/heartbeat-gradient.ts +28 -28
  72. package/src/runtime/heartbeat-watcher.ts +124 -124
  73. package/src/runtime/live-agent-control.ts +87 -87
  74. package/src/runtime/live-agent-manager.ts +85 -85
  75. package/src/runtime/live-control-realtime.ts +36 -36
  76. package/src/runtime/live-session-runtime.ts +305 -305
  77. package/src/runtime/model-fallback.ts +5 -2
  78. package/src/runtime/overflow-recovery.ts +176 -0
  79. package/src/runtime/parallel-research.ts +44 -44
  80. package/src/runtime/pi-json-output.ts +111 -111
  81. package/src/runtime/policy-engine.ts +79 -79
  82. package/src/runtime/process-status.ts +1 -1
  83. package/src/runtime/progress-event-coalescer.ts +43 -43
  84. package/src/runtime/recovery-recipes.ts +74 -74
  85. package/src/runtime/retry-executor.ts +64 -64
  86. package/src/runtime/role-permission.ts +39 -39
  87. package/src/runtime/session-resources.ts +25 -0
  88. package/src/runtime/session-snapshot.ts +59 -0
  89. package/src/runtime/session-usage.ts +79 -79
  90. package/src/runtime/sidechain-output.ts +29 -29
  91. package/src/runtime/stale-reconciler.ts +199 -0
  92. package/src/runtime/supervisor-contact.ts +59 -0
  93. package/src/runtime/task-display.ts +38 -38
  94. package/src/runtime/task-output-context.ts +127 -127
  95. package/src/runtime/task-runner/live-executor.ts +101 -101
  96. package/src/runtime/task-runner/progress.ts +119 -119
  97. package/src/runtime/task-runner/result-utils.ts +14 -14
  98. package/src/runtime/task-runner/state-helpers.ts +22 -22
  99. package/src/runtime/task-runner.ts +14 -0
  100. package/src/runtime/team-runner.ts +19 -8
  101. package/src/runtime/worker-heartbeat.ts +21 -21
  102. package/src/runtime/worker-startup.ts +57 -57
  103. package/src/schema/config-schema.ts +1 -0
  104. package/src/schema/team-tool-schema.ts +6 -1
  105. package/src/state/contracts.ts +6 -2
  106. package/src/state/state-store.ts +43 -0
  107. package/src/state/task-claims.ts +44 -44
  108. package/src/state/types.ts +2 -0
  109. package/src/state/usage.ts +29 -29
  110. package/src/subagents/async-entry.ts +1 -1
  111. package/src/subagents/index.ts +3 -3
  112. package/src/subagents/live/control.ts +1 -1
  113. package/src/subagents/live/manager.ts +1 -1
  114. package/src/subagents/live/realtime.ts +1 -1
  115. package/src/subagents/live/session-runtime.ts +1 -1
  116. package/src/subagents/manager.ts +1 -1
  117. package/src/subagents/spawn.ts +1 -1
  118. package/src/teams/team-serializer.ts +38 -38
  119. package/src/types/diff.d.ts +18 -18
  120. package/src/ui/crew-footer.ts +101 -101
  121. package/src/ui/crew-select-list.ts +111 -111
  122. package/src/ui/crew-widget.ts +10 -5
  123. package/src/ui/dashboard-panes/mailbox-pane.ts +2 -1
  124. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  125. package/src/ui/dynamic-border.ts +25 -25
  126. package/src/ui/layout-primitives.ts +106 -106
  127. package/src/ui/loaders.ts +158 -158
  128. package/src/ui/powerbar-publisher.ts +4 -4
  129. package/src/ui/render-diff.ts +119 -119
  130. package/src/ui/render-scheduler.ts +143 -143
  131. package/src/ui/run-snapshot-cache.ts +310 -17
  132. package/src/ui/snapshot-types.ts +5 -0
  133. package/src/ui/spinner.ts +17 -17
  134. package/src/ui/status-colors.ts +58 -54
  135. package/src/ui/syntax-highlight.ts +116 -116
  136. package/src/utils/atomic-write.ts +33 -0
  137. package/src/utils/completion-dedupe.ts +63 -63
  138. package/src/utils/frontmatter.ts +68 -68
  139. package/src/utils/git.ts +262 -262
  140. package/src/utils/ids.ts +12 -12
  141. package/src/utils/names.ts +27 -27
  142. package/src/utils/redaction.ts +44 -44
  143. package/src/utils/safe-paths.ts +47 -47
  144. package/src/utils/sleep.ts +32 -32
  145. package/src/workflows/validate-workflow.ts +40 -40
  146. package/src/worktree/branch-freshness.ts +45 -45
  147. package/teams/default.team.md +12 -12
  148. package/teams/fast-fix.team.md +11 -11
  149. package/teams/implementation.team.md +18 -18
  150. package/teams/parallel-research.team.md +14 -14
  151. package/teams/research.team.md +11 -11
  152. package/teams/review.team.md +12 -12
  153. package/workflows/default.workflow.md +29 -29
  154. package/workflows/fast-fix.workflow.md +22 -22
  155. package/workflows/implementation.workflow.md +38 -38
  156. package/workflows/parallel-research.workflow.md +46 -46
  157. package/workflows/research.workflow.md +22 -22
  158. package/workflows/review.workflow.md +30 -30
@@ -1,14 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { readCrewAgents, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
4
+ import { readCrewAgents, readCrewAgentsAsync, agentsPath, agentOutputPath } from "../runtime/crew-agent-records.ts";
5
5
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
6
  import { isActiveRunStatus } from "../runtime/process-status.ts";
7
7
  import type { TeamEvent } from "../state/event-log.ts";
8
8
  import type { MailboxMessageStatus } from "../state/mailbox.ts";
9
- import { loadRunManifestById } from "../state/state-store.ts";
9
+ import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/state-store.ts";
10
10
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
11
- import type { RunSnapshotCache, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
11
+ import type { RunSnapshotCache as RunSnapshotCacheBase, RunUiGroupJoin, RunUiMailbox, RunUiProgress, RunUiSnapshot, RunUiUsage } from "./snapshot-types.ts";
12
+
13
+ export interface RunSnapshotCache extends RunSnapshotCacheBase {
14
+ preloadStale(runId: string): Promise<RunUiSnapshot | undefined>;
15
+ preloadAllStale(runIds: string[]): Promise<void>;
16
+ }
12
17
 
13
18
  const DEFAULT_TTL_MS = 500;
14
19
  const DEFAULT_MAX_ENTRIES = 24;
@@ -60,6 +65,16 @@ function stampFile(filePath: string | undefined): FileStamp {
60
65
  }
61
66
  }
62
67
 
68
+ async function stampFileAsync(filePath: string | undefined): Promise<FileStamp> {
69
+ if (!filePath) return zeroStamp();
70
+ try {
71
+ const stat = await fs.promises.stat(filePath);
72
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
73
+ } catch {
74
+ return zeroStamp();
75
+ }
76
+ }
77
+
63
78
  function combineStamps(stamps: FileStamp[]): FileStamp {
64
79
  return stamps.reduce((acc, stamp) => ({ mtimeMs: Math.max(acc.mtimeMs, stamp.mtimeMs), size: acc.size + stamp.size }), zeroStamp());
65
80
  }
@@ -84,6 +99,26 @@ function mailboxStamp(manifest: TeamRunManifest): FileStamp {
84
99
  return combineStamps(stamps);
85
100
  }
86
101
 
102
+ async function mailboxStampAsync(manifest: TeamRunManifest): Promise<FileStamp> {
103
+ const root = path.join(manifest.stateRoot, "mailbox");
104
+ const stamps: FileStamp[] = [
105
+ await stampFileAsync(path.join(root, "inbox.jsonl")),
106
+ await stampFileAsync(path.join(root, "outbox.jsonl")),
107
+ await stampFileAsync(path.join(root, "delivery.json")),
108
+ ];
109
+ const tasksRoot = path.join(root, "tasks");
110
+ try {
111
+ for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
112
+ if (!entry.isDirectory()) continue;
113
+ stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "inbox.jsonl")));
114
+ stamps.push(await stampFileAsync(path.join(tasksRoot, entry.name, "outbox.jsonl")));
115
+ }
116
+ } catch {
117
+ // No task mailbox yet.
118
+ }
119
+ return combineStamps(stamps);
120
+ }
121
+
87
122
  function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord): string | undefined {
88
123
  try {
89
124
  return agentOutputPath(manifest, agent.taskId);
@@ -96,6 +131,10 @@ function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): File
96
131
  return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
97
132
  }
98
133
 
134
+ async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
135
+ return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
136
+ }
137
+
99
138
  function sameStamp(a: FileStamp, b: FileStamp): boolean {
100
139
  return a.mtimeMs === b.mtimeMs && a.size === b.size;
101
140
  }
@@ -141,6 +180,29 @@ function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string
141
180
  }
142
181
  }
143
182
 
183
+ /** Async tail-read JSONL lines from a file, returning parsed objects (limited). */
184
+ async function tailJsonlLinesAsync<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): Promise<T[]> {
185
+ if (limit <= 0) return [];
186
+ try {
187
+ const stat = await fs.promises.stat(filePath);
188
+ const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
189
+ const handle = await fs.promises.open(filePath, "r");
190
+ try {
191
+ const buffer = Buffer.alloc(bytesToRead);
192
+ await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
193
+ const lines = buffer.toString("utf-8").split(/\r?\n/).filter(Boolean);
194
+ return lines.flatMap((line) => {
195
+ const item = parse(line);
196
+ return item ? [item] : [];
197
+ }).slice(-limit);
198
+ } finally {
199
+ await handle.close();
200
+ }
201
+ } catch {
202
+ return [];
203
+ }
204
+ }
205
+
144
206
  function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
145
207
  return tailJsonlLines(eventsPath, limit, (line) => {
146
208
  try {
@@ -152,6 +214,17 @@ function safeRecentEvents(eventsPath: string, limit: number): TeamEvent[] {
152
214
  });
153
215
  }
154
216
 
217
+ async function safeRecentEventsAsync(eventsPath: string, limit: number): Promise<TeamEvent[]> {
218
+ return tailJsonlLinesAsync(eventsPath, limit, (line) => {
219
+ try {
220
+ const parsed = JSON.parse(line) as unknown;
221
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as TeamEvent) : undefined;
222
+ } catch {
223
+ return undefined;
224
+ }
225
+ });
226
+ }
227
+
155
228
  function tailLines(filePath: string, limit: number): string[] {
156
229
  if (limit <= 0) return [];
157
230
  try {
@@ -170,6 +243,24 @@ function tailLines(filePath: string, limit: number): string[] {
170
243
  }
171
244
  }
172
245
 
246
+ async function tailLinesAsync(filePath: string, limit: number): Promise<string[]> {
247
+ if (limit <= 0) return [];
248
+ try {
249
+ const stat = await fs.promises.stat(filePath);
250
+ const bytesToRead = Math.min(stat.size, MAX_TAIL_BYTES);
251
+ const handle = await fs.promises.open(filePath, "r");
252
+ try {
253
+ const buffer = Buffer.alloc(bytesToRead);
254
+ await handle.read(buffer, 0, bytesToRead, stat.size - bytesToRead);
255
+ return buffer.toString("utf-8").split(/\r?\n/).filter(Boolean).slice(-limit);
256
+ } finally {
257
+ await handle.close();
258
+ }
259
+ } catch {
260
+ return [];
261
+ }
262
+ }
263
+
173
264
  function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): string[] {
174
265
  const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
175
266
  const fromFiles = agents.flatMap((agent) => {
@@ -179,14 +270,28 @@ function recentOutputLines(manifest: TeamRunManifest, agents: CrewAgentRecord[],
179
270
  return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
180
271
  }
181
272
 
273
+ async function recentOutputLinesAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[], limit: number): Promise<string[]> {
274
+ const fromProgress = agents.flatMap((agent) => agent.progress?.recentOutput ?? []);
275
+ const fromFilesArrays = await Promise.all(agents.map((agent) => {
276
+ const outputPath = safeAgentOutputPath(manifest, agent);
277
+ return outputPath ? tailLinesAsync(outputPath, limit) : Promise.resolve([]);
278
+ }));
279
+ const fromFiles = fromFilesArrays.flat();
280
+ return [...fromProgress, ...fromFiles].map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean).slice(-limit);
281
+ }
282
+
182
283
  function progressFromTasks(tasks: TeamTaskState[]): RunUiProgress {
183
- return {
184
- total: tasks.length,
185
- completed: tasks.filter((task) => task.status === "completed").length,
186
- running: tasks.filter((task) => task.status === "running").length,
187
- failed: tasks.filter((task) => task.status === "failed").length,
188
- queued: tasks.filter((task) => task.status === "queued").length,
189
- };
284
+ const progress: RunUiProgress = { total: tasks.length, completed: 0, running: 0, failed: 0, queued: 0, waiting: 0, cancelled: 0, skipped: 0 };
285
+ for (const task of tasks) {
286
+ if (task.status === "completed") progress.completed += 1;
287
+ else if (task.status === "running") progress.running += 1;
288
+ else if (task.status === "failed") progress.failed += 1;
289
+ else if (task.status === "queued") progress.queued += 1;
290
+ else if (task.status === "waiting") progress.waiting = (progress.waiting ?? 0) + 1;
291
+ else if (task.status === "cancelled") progress.cancelled = (progress.cancelled ?? 0) + 1;
292
+ else if (task.status === "skipped") progress.skipped = (progress.skipped ?? 0) + 1;
293
+ }
294
+ return progress;
190
295
  }
191
296
 
192
297
  function usageFrom(tasks: TeamTaskState[], agents: CrewAgentRecord[]): RunUiUsage {
@@ -223,6 +328,21 @@ function readDeliveryMessages(filePath: string): Record<string, MailboxMessageSt
223
328
  }
224
329
  }
225
330
 
331
+ async function readDeliveryMessagesAsync(filePath: string): Promise<Record<string, MailboxMessageStatus>> {
332
+ try {
333
+ const content = await fs.promises.readFile(filePath, "utf-8");
334
+ const parsed = JSON.parse(content) as unknown;
335
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
336
+ const messages = (parsed as { messages?: unknown }).messages;
337
+ if (!messages || typeof messages !== "object" || Array.isArray(messages)) return {};
338
+ const output: Record<string, MailboxMessageStatus> = {};
339
+ for (const [id, status] of Object.entries(messages)) if (isMailboxStatus(status)) output[id] = status;
340
+ return output;
341
+ } catch {
342
+ return {};
343
+ }
344
+ }
345
+
226
346
  function readGroupJoinMailbox(filePath: string, delivery: Record<string, MailboxMessageStatus>): RunUiGroupJoin[] {
227
347
  return tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
228
348
  try {
@@ -238,7 +358,43 @@ function readGroupJoinMailbox(filePath: string, delivery: Record<string, Mailbox
238
358
  });
239
359
  }
240
360
 
241
- function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): number {
361
+ async function readGroupJoinMailboxAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<RunUiGroupJoin[]> {
362
+ return tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
363
+ try {
364
+ const parsed = JSON.parse(line) as unknown;
365
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
366
+ const message = parsed as { id?: unknown; data?: unknown };
367
+ const data = message.data && typeof message.data === "object" && !Array.isArray(message.data) ? message.data as Record<string, unknown> : undefined;
368
+ if (typeof message.id !== "string" || data?.kind !== "group_join" || typeof data.requestId !== "string") return undefined;
369
+ return { requestId: data.requestId, messageId: message.id, partial: data.partial === true, ack: delivery[message.id] === "acknowledged" ? "acknowledged" as const : "pending" as const };
370
+ } catch {
371
+ return undefined;
372
+ }
373
+ });
374
+ }
375
+
376
+ interface MailboxCount {
377
+ count: number;
378
+ approximate: boolean;
379
+ }
380
+
381
+ function tailApproximate(filePath: string): boolean {
382
+ try {
383
+ return fs.statSync(filePath).size > MAX_TAIL_BYTES;
384
+ } catch {
385
+ return false;
386
+ }
387
+ }
388
+
389
+ async function tailApproximateAsync(filePath: string): Promise<boolean> {
390
+ try {
391
+ return (await fs.promises.stat(filePath)).size > MAX_TAIL_BYTES;
392
+ } catch {
393
+ return false;
394
+ }
395
+ }
396
+
397
+ function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMessageStatus>): MailboxCount {
242
398
  const items = tailJsonlLines(filePath, MAX_TAIL_LINES, (line) => {
243
399
  try {
244
400
  const parsed = JSON.parse(line) as unknown;
@@ -250,7 +406,22 @@ function readMailboxCounts(filePath: string, delivery: Record<string, MailboxMes
250
406
  return 0;
251
407
  }
252
408
  }) as number[];
253
- return items.reduce((sum, val) => sum + val, 0);
409
+ return { count: items.reduce((sum, val) => sum + val, 0), approximate: tailApproximate(filePath) };
410
+ }
411
+
412
+ async function readMailboxCountsAsync(filePath: string, delivery: Record<string, MailboxMessageStatus>): Promise<MailboxCount> {
413
+ const items = await tailJsonlLinesAsync(filePath, MAX_TAIL_LINES, (line) => {
414
+ try {
415
+ const parsed = JSON.parse(line) as unknown;
416
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return 0;
417
+ const message = parsed as { id?: unknown; status?: unknown };
418
+ if (typeof message.id !== "string" || !isMailboxStatus(message.status)) return 0;
419
+ return message.status !== "acknowledged" && delivery[message.id] !== "acknowledged" ? 1 : 0;
420
+ } catch {
421
+ return 0;
422
+ }
423
+ }) as number[];
424
+ return { count: items.reduce((sum, val) => sum + val, 0), approximate: await tailApproximateAsync(filePath) };
254
425
  }
255
426
 
256
427
  function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
@@ -259,23 +430,52 @@ function groupJoinsFrom(manifest: TeamRunManifest): RunUiGroupJoin[] {
259
430
  return readGroupJoinMailbox(path.join(root, "outbox.jsonl"), delivery).slice(-5);
260
431
  }
261
432
 
433
+ async function groupJoinsFromAsync(manifest: TeamRunManifest): Promise<RunUiGroupJoin[]> {
434
+ const root = path.join(manifest.stateRoot, "mailbox");
435
+ const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
436
+ return (await readGroupJoinMailboxAsync(path.join(root, "outbox.jsonl"), delivery)).slice(-5);
437
+ }
438
+
262
439
  function mailboxFrom(manifest: TeamRunManifest, agents: CrewAgentRecord[]): RunUiMailbox {
263
440
  const root = path.join(manifest.stateRoot, "mailbox");
264
441
  const delivery = readDeliveryMessages(path.join(root, "delivery.json"));
265
- let inboxUnread = readMailboxCounts(path.join(root, "inbox.jsonl"), delivery);
266
- let outboxPending = readMailboxCounts(path.join(root, "outbox.jsonl"), delivery);
442
+ let inbox = readMailboxCounts(path.join(root, "inbox.jsonl"), delivery);
443
+ let outbox = readMailboxCounts(path.join(root, "outbox.jsonl"), delivery);
267
444
  const tasksRoot = path.join(root, "tasks");
268
445
  try {
269
446
  for (const entry of fs.readdirSync(tasksRoot, { withFileTypes: true })) {
270
447
  if (!entry.isDirectory()) continue;
271
- inboxUnread += readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
272
- outboxPending += readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
448
+ const taskInbox = readMailboxCounts(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
449
+ const taskOutbox = readMailboxCounts(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
450
+ inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
451
+ outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
273
452
  }
274
453
  } catch {
275
454
  // No task mailboxes yet.
276
455
  }
277
456
  const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
278
- return { inboxUnread, outboxPending, needsAttention: inboxUnread + attentionAgents };
457
+ return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
458
+ }
459
+
460
+ async function mailboxFromAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<RunUiMailbox> {
461
+ const root = path.join(manifest.stateRoot, "mailbox");
462
+ const delivery = await readDeliveryMessagesAsync(path.join(root, "delivery.json"));
463
+ let inbox = await readMailboxCountsAsync(path.join(root, "inbox.jsonl"), delivery);
464
+ let outbox = await readMailboxCountsAsync(path.join(root, "outbox.jsonl"), delivery);
465
+ const tasksRoot = path.join(root, "tasks");
466
+ try {
467
+ for (const entry of await fs.promises.readdir(tasksRoot, { withFileTypes: true })) {
468
+ if (!entry.isDirectory()) continue;
469
+ const taskInbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "inbox.jsonl"), delivery);
470
+ const taskOutbox = await readMailboxCountsAsync(path.join(tasksRoot, entry.name, "outbox.jsonl"), delivery);
471
+ inbox = { count: inbox.count + taskInbox.count, approximate: inbox.approximate || taskInbox.approximate };
472
+ outbox = { count: outbox.count + taskOutbox.count, approximate: outbox.approximate || taskOutbox.approximate };
473
+ }
474
+ } catch {
475
+ // No task mailboxes yet.
476
+ }
477
+ const attentionAgents = agents.filter((agent) => agent.progress?.activityState === "needs_attention").length;
478
+ return { inboxUnread: inbox.count, outboxPending: outbox.count, needsAttention: inbox.count + attentionAgents, approximate: inbox.approximate || outbox.approximate };
279
479
  }
280
480
 
281
481
  function signatureFor(input: Omit<RunUiSnapshot, "signature" | "fetchedAt">, stamps: SnapshotStamps): string {
@@ -311,6 +511,18 @@ function stampsFor(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Snapsh
311
511
  };
312
512
  }
313
513
 
514
+ async function stampsForAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<SnapshotStamps> {
515
+ const [manifestStamp, tasksStamp, agentsStamp, eventsStamp, mailbox, output] = await Promise.all([
516
+ stampFileAsync(path.join(manifest.stateRoot, "manifest.json")),
517
+ stampFileAsync(manifest.tasksPath),
518
+ stampFileAsync(agentsPath(manifest)),
519
+ stampFileAsync(manifest.eventsPath),
520
+ mailboxStampAsync(manifest),
521
+ outputStampAsync(manifest, agents),
522
+ ]);
523
+ return { manifest: manifestStamp, tasks: tasksStamp, agents: agentsStamp, events: eventsStamp, mailbox, output };
524
+ }
525
+
314
526
  export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOptions = {}): RunSnapshotCache {
315
527
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
316
528
  const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
@@ -377,6 +589,51 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
377
589
  return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
378
590
  }
379
591
 
592
+ async function buildAsync(runId: string, previous?: CacheEntry): Promise<CacheEntry> {
593
+ let loaded: Awaited<ReturnType<typeof loadRunManifestByIdAsync>>;
594
+ try {
595
+ loaded = await loadRunManifestByIdAsync(cwd, runId);
596
+ } catch {
597
+ if (previous) return previous;
598
+ throw new Error(`Run '${runId}' could not be parsed.`);
599
+ }
600
+ if (!loaded) {
601
+ if (previous) return previous;
602
+ throw new Error(`Run '${runId}' not found.`);
603
+ }
604
+ let tasks: TeamTaskState[];
605
+ let agents: CrewAgentRecord[];
606
+ try {
607
+ tasks = loaded.tasks;
608
+ agents = await readCrewAgentsAsync(loaded.manifest);
609
+ } catch {
610
+ if (previous) return previous;
611
+ throw new Error(`Run '${runId}' could not be parsed.`);
612
+ }
613
+ const [mailbox, groupJoins, recentEvents, recentOutput] = await Promise.all([
614
+ mailboxFromAsync(loaded.manifest, agents),
615
+ groupJoinsFromAsync(loaded.manifest),
616
+ safeRecentEventsAsync(loaded.manifest.eventsPath, recentEventsLimit),
617
+ recentOutputLinesAsync(loaded.manifest, agents, recentOutputLimit),
618
+ ]);
619
+ const base = {
620
+ runId: loaded.manifest.runId,
621
+ cwd: loaded.manifest.cwd,
622
+ manifest: loaded.manifest,
623
+ tasks,
624
+ agents,
625
+ progress: progressFromTasks(tasks),
626
+ usage: usageFrom(tasks, agents),
627
+ mailbox,
628
+ groupJoins,
629
+ recentEvents,
630
+ recentOutputLines: recentOutput,
631
+ };
632
+ const stamps = await stampsForAsync(loaded.manifest, agents);
633
+ const snapshot: RunUiSnapshot = { ...base, fetchedAt: Date.now(), signature: signatureFor(base, stamps) };
634
+ return { snapshot, stamps, loadedAtMs: snapshot.fetchedAt, lastAccessMs: snapshot.fetchedAt };
635
+ }
636
+
380
637
  function currentStamps(previous: CacheEntry): SnapshotStamps {
381
638
  const manifest = previous.snapshot.manifest;
382
639
  return {
@@ -389,6 +646,40 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
389
646
  };
390
647
  }
391
648
 
649
+ async function currentStampsAsync(previous: CacheEntry): Promise<SnapshotStamps> {
650
+ return stampsForAsync(previous.snapshot.manifest, previous.snapshot.agents);
651
+ }
652
+
653
+ async function preloadStale(runId: string): Promise<RunUiSnapshot | undefined> {
654
+ const previous = entries.get(runId);
655
+ const now = Date.now();
656
+ // Fresh enough? Return immediately
657
+ if (previous && now - previous.loadedAtMs < ttlMs) {
658
+ return touch(runId, previous);
659
+ }
660
+ // Check stamps async
661
+ if (previous) {
662
+ const stamps = await currentStampsAsync(previous);
663
+ if (sameStamps(stamps, previous.stamps)) {
664
+ previous.loadedAtMs = now;
665
+ return touch(runId, previous);
666
+ }
667
+ }
668
+ // Full async build
669
+ const entry = await buildAsync(runId, previous);
670
+ entries.set(runId, entry);
671
+ evictIfNeeded();
672
+ return entry.snapshot;
673
+ }
674
+
675
+ async function preloadAllStale(runIds: string[]): Promise<void> {
676
+ const batchSize = 4;
677
+ for (let i = 0; i < runIds.length; i += batchSize) {
678
+ const batch = runIds.slice(i, i + batchSize);
679
+ await Promise.all(batch.map((id) => preloadStale(id)));
680
+ }
681
+ }
682
+
392
683
  return {
393
684
  get(runId: string): RunUiSnapshot | undefined {
394
685
  const entry = entries.get(runId);
@@ -410,6 +701,8 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
410
701
  if (sameStamps(stamps, previous.stamps)) return touch(runId, previous);
411
702
  return this.refresh(runId);
412
703
  },
704
+ preloadStale,
705
+ preloadAllStale,
413
706
  invalidate(runId?: string): void {
414
707
  if (runId) entries.delete(runId);
415
708
  else entries.clear();
@@ -8,6 +8,9 @@ export interface RunUiProgress {
8
8
  running: number;
9
9
  failed: number;
10
10
  queued: number;
11
+ waiting?: number;
12
+ cancelled?: number;
13
+ skipped?: number;
11
14
  }
12
15
 
13
16
  export interface RunUiUsage {
@@ -20,6 +23,8 @@ export interface RunUiMailbox {
20
23
  inboxUnread: number;
21
24
  outboxPending: number;
22
25
  needsAttention: number;
26
+ /** True when counts come from bounded tail reads and older messages may be omitted. */
27
+ approximate?: boolean;
23
28
  }
24
29
 
25
30
  export interface RunUiGroupJoin {
package/src/ui/spinner.ts CHANGED
@@ -1,17 +1,17 @@
1
- export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
- export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
-
4
- export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
- return Math.floor(now / Math.max(1, frameMs));
6
- }
7
-
8
- function hashKey(key: string): number {
9
- let hash = 0;
10
- for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
- return hash;
12
- }
13
-
14
- export function spinnerFrame(key = "", now = Date.now()): string {
15
- const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
- return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
- }
1
+ export const SUBAGENT_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
2
+ export const SUBAGENT_SPINNER_FRAME_MS = 160;
3
+
4
+ export function spinnerBucket(now = Date.now(), frameMs = SUBAGENT_SPINNER_FRAME_MS): number {
5
+ return Math.floor(now / Math.max(1, frameMs));
6
+ }
7
+
8
+ function hashKey(key: string): number {
9
+ let hash = 0;
10
+ for (let index = 0; index < key.length; index += 1) hash = (hash * 31 + key.charCodeAt(index)) >>> 0;
11
+ return hash;
12
+ }
13
+
14
+ export function spinnerFrame(key = "", now = Date.now()): string {
15
+ const offset = key ? hashKey(key) % SUBAGENT_SPINNER_FRAMES.length : 0;
16
+ return SUBAGENT_SPINNER_FRAMES[(spinnerBucket(now) + offset) % SUBAGENT_SPINNER_FRAMES.length] ?? SUBAGENT_SPINNER_FRAMES[0];
17
+ }
@@ -1,54 +1,58 @@
1
- import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
-
3
- export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
-
5
- export function colorForStatus(status: RunStatus): CrewThemeColor {
6
- switch (status) {
7
- case "running":
8
- return "accent";
9
- case "completed":
10
- return "success";
11
- case "failed":
12
- case "stale":
13
- return "error";
14
- case "cancelled":
15
- case "blocked":
16
- case "stopped":
17
- return "warning";
18
- case "queued":
19
- default:
20
- return "dim";
21
- }
22
- }
23
-
24
- export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
25
- const glyph = options?.runningGlyph ?? "▶";
26
- switch (status) {
27
- case "completed":
28
- return "✓";
29
- case "failed":
30
- case "stale":
31
- return "";
32
- case "cancelled":
33
- case "stopped":
34
- return "";
35
- case "running":
36
- return glyph;
37
- case "queued":
38
- return "◦";
39
- case "blocked":
40
- return "";
41
- default:
42
- return "·";
43
- }
44
- }
45
-
46
- export function colorForActivity(activityState: string | undefined): CrewThemeColor {
47
- if (activityState === "needs_attention") return "warning";
48
- if (activityState === "stale") return "error";
49
- return "dim";
50
- }
51
-
52
- export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
53
- return theme.fg(colorForStatus(status), text);
54
- }
1
+ import type { CrewTheme, CrewThemeColor } from "./theme-adapter.ts";
2
+
3
+ export type RunStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped" | "blocked" | (string & {});
4
+
5
+ export function colorForStatus(status: RunStatus): CrewThemeColor {
6
+ switch (status) {
7
+ case "running":
8
+ return "accent";
9
+ case "waiting":
10
+ return "muted";
11
+ case "completed":
12
+ return "success";
13
+ case "failed":
14
+ case "stale":
15
+ return "error";
16
+ case "cancelled":
17
+ case "blocked":
18
+ case "stopped":
19
+ return "warning";
20
+ case "queued":
21
+ default:
22
+ return "dim";
23
+ }
24
+ }
25
+
26
+ export function iconForStatus(status: RunStatus, options?: { runningGlyph?: string }): string {
27
+ const glyph = options?.runningGlyph ?? "";
28
+ switch (status) {
29
+ case "completed":
30
+ return "";
31
+ case "failed":
32
+ case "stale":
33
+ return "";
34
+ case "cancelled":
35
+ case "stopped":
36
+ return "■";
37
+ case "running":
38
+ return glyph;
39
+ case "waiting":
40
+ return "";
41
+ case "queued":
42
+ return "";
43
+ case "blocked":
44
+ return "⏸";
45
+ default:
46
+ return "·";
47
+ }
48
+ }
49
+
50
+ export function colorForActivity(activityState: string | undefined): CrewThemeColor {
51
+ if (activityState === "needs_attention") return "warning";
52
+ if (activityState === "stale") return "error";
53
+ return "dim";
54
+ }
55
+
56
+ export function applyStatusColor(theme: CrewTheme, status: RunStatus, text: string): string {
57
+ return theme.fg(colorForStatus(status), text);
58
+ }