tmux-ide 1.2.1 → 2.0.0

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 (205) hide show
  1. package/bin/cli.js +4 -216
  2. package/bin/cli.ts +536 -0
  3. package/package.json +33 -14
  4. package/src/__tests__/support.ts +186 -0
  5. package/src/{attach.js → attach.ts} +7 -4
  6. package/src/cli.test.ts +314 -0
  7. package/src/command-center/discovery.test.ts +265 -0
  8. package/src/command-center/discovery.ts +296 -0
  9. package/src/command-center/index.ts +31 -0
  10. package/src/command-center/schemas.ts +57 -0
  11. package/src/command-center/server.test.ts +818 -0
  12. package/src/command-center/server.ts +1426 -0
  13. package/src/config-cli.test.ts +86 -0
  14. package/src/config.test.ts +205 -0
  15. package/src/config.ts +313 -0
  16. package/src/detect.test.ts +142 -0
  17. package/src/{detect.js → detect.ts} +31 -15
  18. package/src/dispatch.ts +102 -0
  19. package/src/doctor.test.ts +119 -0
  20. package/src/doctor.ts +167 -0
  21. package/src/init.test.ts +101 -0
  22. package/src/init.ts +144 -0
  23. package/src/inspect.test.ts +67 -0
  24. package/src/{inspect.js → inspect.ts} +95 -10
  25. package/src/integration.test.ts.skip +276 -0
  26. package/src/js-yaml.d.ts +10 -0
  27. package/src/launch.test.ts +222 -0
  28. package/src/launch.ts +494 -0
  29. package/src/lib/auth/auth-service.test.ts +178 -0
  30. package/src/lib/auth/auth-service.ts +227 -0
  31. package/src/lib/auth/middleware.test.ts +119 -0
  32. package/src/lib/auth/middleware.ts +56 -0
  33. package/src/lib/auth/types.ts +13 -0
  34. package/src/lib/authorship.test.ts +242 -0
  35. package/src/lib/authorship.ts +280 -0
  36. package/src/lib/cast/recorder.test.ts +127 -0
  37. package/src/lib/cast/recorder.ts +148 -0
  38. package/src/lib/daemon-watchdog.ts +102 -0
  39. package/src/lib/daemon.ts +417 -0
  40. package/src/lib/dot-path.test.ts +66 -0
  41. package/src/lib/dot-path.ts +17 -0
  42. package/src/lib/errors.ts +44 -0
  43. package/src/lib/event-log.test.ts +189 -0
  44. package/src/lib/event-log.ts +237 -0
  45. package/src/lib/github-pr.ts +182 -0
  46. package/src/lib/hq/client.test.ts +154 -0
  47. package/src/lib/hq/client.ts +142 -0
  48. package/src/lib/hq/mdns.test.ts +54 -0
  49. package/src/lib/hq/mdns.ts +88 -0
  50. package/src/lib/hq/registry.test.ts +237 -0
  51. package/src/lib/hq/registry.ts +145 -0
  52. package/src/lib/hq/types.ts +44 -0
  53. package/src/lib/ipc/socket-protocol.test.ts +126 -0
  54. package/src/lib/ipc/socket-protocol.ts +324 -0
  55. package/src/lib/launch-plan.test.ts +119 -0
  56. package/src/lib/launch-plan.ts +90 -0
  57. package/src/lib/log.test.ts +39 -0
  58. package/src/lib/log.ts +71 -0
  59. package/src/lib/metrics.test.ts +213 -0
  60. package/src/lib/metrics.ts +345 -0
  61. package/src/lib/orchestrator.test.ts +2358 -0
  62. package/src/lib/orchestrator.ts +1792 -0
  63. package/src/lib/{output.js → output.ts} +18 -10
  64. package/src/lib/plan-store.test.ts +148 -0
  65. package/src/lib/plan-store.ts +131 -0
  66. package/src/lib/research.test.ts +208 -0
  67. package/src/lib/research.ts +475 -0
  68. package/src/lib/round-trip.test.ts +341 -0
  69. package/src/lib/session-monitor.test.ts +152 -0
  70. package/src/lib/session-monitor.ts +282 -0
  71. package/src/lib/session-options.test.ts +146 -0
  72. package/src/lib/{session-options.js → session-options.ts} +11 -6
  73. package/src/lib/shell.test.ts +20 -0
  74. package/src/lib/shell.ts +8 -0
  75. package/src/lib/sizes.test.ts +73 -0
  76. package/src/lib/{sizes.js → sizes.ts} +4 -4
  77. package/src/lib/skill-registry.test.ts +134 -0
  78. package/src/lib/skill-registry.ts +86 -0
  79. package/src/lib/slugify.test.ts +35 -0
  80. package/src/lib/slugify.ts +10 -0
  81. package/src/lib/task-store.ts +352 -0
  82. package/src/lib/tmux.test.ts +500 -0
  83. package/src/lib/tmux.ts +372 -0
  84. package/src/lib/token-tracker.test.ts +102 -0
  85. package/src/lib/token-tracker.ts +70 -0
  86. package/src/lib/tunnels/cloudflare.test.ts +73 -0
  87. package/src/lib/tunnels/cloudflare.ts +254 -0
  88. package/src/lib/tunnels/manager.test.ts +63 -0
  89. package/src/lib/tunnels/manager.ts +132 -0
  90. package/src/lib/tunnels/ngrok.test.ts +94 -0
  91. package/src/lib/tunnels/ngrok.ts +261 -0
  92. package/src/lib/tunnels/tailscale.test.ts +103 -0
  93. package/src/lib/tunnels/tailscale.ts +1048 -0
  94. package/src/lib/tunnels/types.ts +44 -0
  95. package/src/lib/validation.test.ts +289 -0
  96. package/src/lib/validation.ts +123 -0
  97. package/src/lib/webhook.ts +51 -0
  98. package/src/lib/workflow-store.ts +209 -0
  99. package/src/lib/ws-v3/protocol.test.ts +127 -0
  100. package/src/lib/ws-v3/protocol.ts +160 -0
  101. package/src/lib/yaml-io.test.ts +125 -0
  102. package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
  103. package/src/ls.test.ts +76 -0
  104. package/src/{ls.js → ls.ts} +4 -4
  105. package/src/metrics-cli.ts +177 -0
  106. package/src/notify.ts +85 -0
  107. package/src/orchestrator-status.ts +228 -0
  108. package/src/plan.ts +117 -0
  109. package/src/postinstall.test.ts +79 -0
  110. package/src/remote.test.ts +115 -0
  111. package/src/remote.ts +148 -0
  112. package/src/restart.ts +24 -0
  113. package/src/schemas/domain.ts +422 -0
  114. package/src/schemas/ide-config.ts +148 -0
  115. package/src/schemas/index.ts +53 -0
  116. package/src/send.test.ts +70 -0
  117. package/src/send.ts +181 -0
  118. package/src/skill.test.ts +114 -0
  119. package/src/skill.ts +138 -0
  120. package/src/status.ts +66 -0
  121. package/src/stop.test.ts +176 -0
  122. package/src/stop.ts +47 -0
  123. package/src/task.test.ts +1466 -0
  124. package/src/task.ts +1143 -0
  125. package/src/tunnel.test.ts +80 -0
  126. package/src/tunnel.ts +133 -0
  127. package/src/types.ts +15 -0
  128. package/src/ui/index.ts +32 -0
  129. package/src/ui/terminal/index.ts +9 -0
  130. package/src/ui/types.ts +91 -0
  131. package/src/ui/web/base.css +80 -0
  132. package/src/ui/web/components/Box.tsx +59 -0
  133. package/src/ui/web/components/Input.tsx +32 -0
  134. package/src/ui/web/components/ScrollBox.tsx +60 -0
  135. package/src/ui/web/components/Text.tsx +28 -0
  136. package/src/ui/web/hooks.ts +106 -0
  137. package/src/ui/web/index.ts +27 -0
  138. package/src/ui/web/render.ts +77 -0
  139. package/src/ui/web/utils/color.test.ts +93 -0
  140. package/src/ui/web/utils/color.ts +27 -0
  141. package/src/validate.test.ts +385 -0
  142. package/src/validate.ts +217 -0
  143. package/src/widgets/changes/index.tsx +576 -0
  144. package/src/widgets/config/index.tsx +440 -0
  145. package/src/widgets/costs/index.tsx +216 -0
  146. package/src/widgets/explorer/breadcrumbs.tsx +77 -0
  147. package/src/widgets/explorer/footer.tsx +20 -0
  148. package/src/widgets/explorer/header.tsx +23 -0
  149. package/src/widgets/explorer/index.tsx +408 -0
  150. package/src/widgets/explorer/tree-model.test.ts +214 -0
  151. package/src/widgets/explorer/tree-model.ts +103 -0
  152. package/src/widgets/explorer/tree.tsx +156 -0
  153. package/src/widgets/lib/config-model.ts +116 -0
  154. package/src/widgets/lib/files.test.ts +103 -0
  155. package/src/widgets/lib/files.ts +88 -0
  156. package/src/widgets/lib/git.test.ts +151 -0
  157. package/src/widgets/lib/git.ts +88 -0
  158. package/src/widgets/lib/pane-comms.test.ts +178 -0
  159. package/src/widgets/lib/pane-comms.ts +192 -0
  160. package/src/widgets/lib/theme.ts +151 -0
  161. package/src/widgets/lib/watcher.ts +50 -0
  162. package/src/widgets/mission-control/activity-feed.tsx +59 -0
  163. package/src/widgets/mission-control/agent-panel.tsx +132 -0
  164. package/src/widgets/mission-control/command-bar.tsx +63 -0
  165. package/src/widgets/mission-control/index.tsx +751 -0
  166. package/src/widgets/mission-control/task-panel.tsx +238 -0
  167. package/src/widgets/preview/index.tsx +415 -0
  168. package/src/widgets/resolve.ts +40 -0
  169. package/src/widgets/setup/agent-naming.tsx +112 -0
  170. package/src/widgets/setup/config-tree.tsx +238 -0
  171. package/src/widgets/setup/detect-panel.tsx +72 -0
  172. package/src/widgets/setup/field-editor.tsx +265 -0
  173. package/src/widgets/setup/footer.tsx +107 -0
  174. package/src/widgets/setup/index.tsx +340 -0
  175. package/src/widgets/setup/layout-picker.tsx +96 -0
  176. package/src/widgets/setup/orchestrator-panel.tsx +200 -0
  177. package/src/widgets/setup/review-panel.tsx +140 -0
  178. package/src/widgets/setup/setup-model.test.ts +303 -0
  179. package/src/widgets/setup/setup-model.ts +188 -0
  180. package/src/widgets/tasks/index.tsx +129 -0
  181. package/src/widgets/tasks/task-detail.tsx +309 -0
  182. package/src/widgets/tasks/task-form.tsx +193 -0
  183. package/src/widgets/tasks/task-list.tsx +205 -0
  184. package/src/widgets/tasks/task-model.test.ts +473 -0
  185. package/src/widgets/tasks/task-model.ts +157 -0
  186. package/templates/AGENTS.md +20 -0
  187. package/templates/missions.yml +41 -0
  188. package/templates/skills/backend.md +30 -0
  189. package/templates/skills/frontend.md +30 -0
  190. package/templates/skills/general-worker.md +30 -0
  191. package/templates/skills/researcher.md +69 -0
  192. package/templates/skills/reviewer.md +30 -0
  193. package/src/config.js +0 -341
  194. package/src/doctor.js +0 -91
  195. package/src/init.js +0 -73
  196. package/src/launch.js +0 -245
  197. package/src/lib/dot-path.js +0 -16
  198. package/src/lib/errors.js +0 -35
  199. package/src/lib/launch-plan.js +0 -49
  200. package/src/lib/session-monitor.js +0 -179
  201. package/src/lib/tmux.js +0 -237
  202. package/src/restart.js +0 -16
  203. package/src/status.js +0 -35
  204. package/src/stop.js +0 -25
  205. package/src/validate.js +0 -154
@@ -0,0 +1,345 @@
1
+ import { join } from "node:path";
2
+ import {
3
+ existsSync,
4
+ readFileSync,
5
+ writeFileSync,
6
+ appendFileSync,
7
+ renameSync,
8
+ mkdirSync,
9
+ } from "node:fs";
10
+ import { loadTasks, loadMission, type Task, type Mission } from "./task-store.ts";
11
+ import { readEvents } from "./event-log.ts";
12
+
13
+ // --- Interfaces ---
14
+
15
+ export interface AgentMetrics {
16
+ name: string;
17
+ totalTimeMs: number;
18
+ activeTimeMs: number;
19
+ idleTimeMs: number;
20
+ taskCount: number;
21
+ retryCount: number;
22
+ utilization: number;
23
+ specialties: string[];
24
+ }
25
+
26
+ export interface MilestoneMetrics {
27
+ id: string;
28
+ title: string;
29
+ status: string;
30
+ taskCount: number;
31
+ completedCount: number;
32
+ durationMs: number;
33
+ }
34
+
35
+ export interface TimelineEntry {
36
+ timestamp: string;
37
+ completedTasks: number;
38
+ activeTasks: number;
39
+ busyAgents: number;
40
+ idleAgents: number;
41
+ }
42
+
43
+ export interface MetricsSnapshot {
44
+ session: {
45
+ startedAt: string | null;
46
+ durationMs: number;
47
+ status: string;
48
+ agentCount: number;
49
+ };
50
+ tasks: {
51
+ total: number;
52
+ completed: number;
53
+ failed: number;
54
+ retried: number;
55
+ completionRate: number;
56
+ retryRate: number;
57
+ avgDurationMs: number;
58
+ medianDurationMs: number;
59
+ p90DurationMs: number;
60
+ byMilestone: MilestoneMetrics[];
61
+ };
62
+ agents: AgentMetrics[];
63
+ mission: {
64
+ title: string | null;
65
+ status: string | null;
66
+ milestonesCompleted: number;
67
+ validationPassRate: number;
68
+ wallClockMs: number;
69
+ };
70
+ timeline: TimelineEntry[];
71
+ }
72
+
73
+ export interface MissionSummary {
74
+ title: string;
75
+ status: string;
76
+ completedAt: string;
77
+ wallClockMs: number;
78
+ taskCount: number;
79
+ completionRate: number;
80
+ agentCount: number;
81
+ milestonesCompleted: number;
82
+ }
83
+
84
+ // --- Helpers ---
85
+
86
+ function percentile(sorted: number[], p: number): number {
87
+ if (sorted.length === 0) return 0;
88
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
89
+ return sorted[Math.max(0, idx)]!;
90
+ }
91
+
92
+ function median(sorted: number[]): number {
93
+ return percentile(sorted, 50);
94
+ }
95
+
96
+ // --- Core computation ---
97
+
98
+ export function computeMetrics(dir: string): MetricsSnapshot {
99
+ const events = readEvents(dir);
100
+ const tasks = loadTasks(dir);
101
+ const mission = loadMission(dir);
102
+
103
+ // Session info
104
+ const sessionStart = events.find((e) => e.type === "session_start");
105
+ const startedAt = sessionStart?.timestamp ?? events[0]?.timestamp ?? null;
106
+ const now = Date.now();
107
+ const durationMs = startedAt ? now - new Date(startedAt).getTime() : 0;
108
+
109
+ // Task metrics
110
+ const completed = tasks.filter((t) => t.status === "done").length;
111
+ const failed = tasks.filter((t) => t.status === "review" && t.lastError).length;
112
+ const retried = tasks.filter((t) => t.retryCount > 0).length;
113
+ const completionRate = tasks.length > 0 ? completed / tasks.length : 0;
114
+ const retryRate = tasks.length > 0 ? retried / tasks.length : 0;
115
+
116
+ // Task durations from completion events with durationMs
117
+ const durations: number[] = [];
118
+ for (const evt of events) {
119
+ const durationMs = (evt as unknown as { durationMs?: unknown }).durationMs;
120
+ if (evt.type === "completion" && typeof durationMs === "number") {
121
+ durations.push(durationMs);
122
+ }
123
+ }
124
+ durations.sort((a, b) => a - b);
125
+
126
+ // Per-milestone metrics
127
+ const byMilestone: MilestoneMetrics[] = (mission?.milestones ?? []).map((m) => {
128
+ const mTasks = tasks.filter((t) => t.milestone === m.id);
129
+ const mCompleted = mTasks.filter((t) => t.status === "done").length;
130
+ // Estimate milestone duration from task timestamps
131
+ const timestamps = mTasks
132
+ .filter((t) => t.status === "done")
133
+ .map((t) => new Date(t.updated).getTime());
134
+ const mDuration = timestamps.length > 0 ? Math.max(...timestamps) - Math.min(...timestamps) : 0;
135
+ return {
136
+ id: m.id,
137
+ title: m.title,
138
+ status: m.status,
139
+ taskCount: mTasks.length,
140
+ completedCount: mCompleted,
141
+ durationMs: mDuration,
142
+ };
143
+ });
144
+
145
+ // Agent metrics from heartbeat events
146
+ const agentBusySamples = new Map<string, { busy: number; idle: number; total: number }>();
147
+ const agentTaskCounts = new Map<string, number>();
148
+ const agentRetryCounts = new Map<string, number>();
149
+ const agentSpecialties = new Map<string, Set<string>>();
150
+
151
+ for (const evt of events) {
152
+ if (evt.type === "agent_heartbeat" && evt.message) {
153
+ // Parse heartbeat: "agents: Name1=busy, Name2=idle"
154
+ const parts = evt.message.replace("agents: ", "").split(", ");
155
+ for (const part of parts) {
156
+ const [name, status] = part.split("=");
157
+ if (!name || !status) continue;
158
+ const entry = agentBusySamples.get(name) ?? { busy: 0, idle: 0, total: 0 };
159
+ entry.total++;
160
+ if (status === "busy") entry.busy++;
161
+ else entry.idle++;
162
+ agentBusySamples.set(name, entry);
163
+ }
164
+ }
165
+ }
166
+
167
+ // Count tasks and retries per agent
168
+ for (const task of tasks) {
169
+ if (task.assignee) {
170
+ agentTaskCounts.set(task.assignee, (agentTaskCounts.get(task.assignee) ?? 0) + 1);
171
+ if (task.retryCount > 0) {
172
+ agentRetryCounts.set(
173
+ task.assignee,
174
+ (agentRetryCounts.get(task.assignee) ?? 0) + task.retryCount,
175
+ );
176
+ }
177
+ if (task.specialty) {
178
+ const specs = agentSpecialties.get(task.assignee) ?? new Set();
179
+ specs.add(task.specialty);
180
+ agentSpecialties.set(task.assignee, specs);
181
+ }
182
+ }
183
+ }
184
+
185
+ const agentNames = new Set([...agentBusySamples.keys(), ...agentTaskCounts.keys()]);
186
+ const agents: AgentMetrics[] = [...agentNames].map((name) => {
187
+ const samples = agentBusySamples.get(name) ?? { busy: 0, idle: 0, total: 0 };
188
+ const utilization = samples.total > 0 ? samples.busy / samples.total : 0;
189
+ const estimatedTotalMs = durationMs;
190
+ return {
191
+ name,
192
+ totalTimeMs: estimatedTotalMs,
193
+ activeTimeMs: Math.round(estimatedTotalMs * utilization),
194
+ idleTimeMs: Math.round(estimatedTotalMs * (1 - utilization)),
195
+ taskCount: agentTaskCounts.get(name) ?? 0,
196
+ retryCount: agentRetryCounts.get(name) ?? 0,
197
+ utilization: Math.round(utilization * 100) / 100,
198
+ specialties: [...(agentSpecialties.get(name) ?? [])],
199
+ };
200
+ });
201
+
202
+ // Mission metrics
203
+ const milestonesCompleted = (mission?.milestones ?? []).filter((m) => m.status === "done").length;
204
+ const missionWallClock = mission?.created ? now - new Date(mission.created).getTime() : 0;
205
+
206
+ // Validation pass rate (simple estimate from completion events)
207
+ const validationEvents = events.filter(
208
+ (e) => e.type === "milestone_complete" || e.type === "validation_failed",
209
+ );
210
+ const passEvents = validationEvents.filter((e) => e.type === "milestone_complete").length;
211
+ const validationPassRate = validationEvents.length > 0 ? passEvents / validationEvents.length : 1;
212
+
213
+ // Timeline: sample at 5-minute intervals
214
+ const timeline: TimelineEntry[] = [];
215
+ if (events.length > 0) {
216
+ const firstTs = new Date(events[0]!.timestamp).getTime();
217
+ const interval = 5 * 60 * 1000;
218
+ for (let t = firstTs; t <= now; t += interval) {
219
+ const cutoff = new Date(t).toISOString();
220
+ const completedByNow = events.filter(
221
+ (e) => e.type === "completion" && e.timestamp <= cutoff,
222
+ ).length;
223
+ const activeByNow =
224
+ events.filter((e) => e.type === "dispatch" && e.timestamp <= cutoff).length -
225
+ completedByNow;
226
+
227
+ // Last heartbeat before this timestamp
228
+ const heartbeat = events
229
+ .filter((e) => e.type === "agent_heartbeat" && e.timestamp <= cutoff)
230
+ .pop();
231
+ let busyAgents = 0;
232
+ let idleAgents = 0;
233
+ if (heartbeat?.message) {
234
+ const parts = heartbeat.message.replace("agents: ", "").split(", ");
235
+ for (const p of parts) {
236
+ if (p.includes("=busy")) busyAgents++;
237
+ else if (p.includes("=idle")) idleAgents++;
238
+ }
239
+ }
240
+
241
+ timeline.push({
242
+ timestamp: cutoff,
243
+ completedTasks: completedByNow,
244
+ activeTasks: Math.max(0, activeByNow),
245
+ busyAgents,
246
+ idleAgents,
247
+ });
248
+
249
+ if (timeline.length >= 200) break;
250
+ }
251
+ }
252
+
253
+ return {
254
+ session: {
255
+ startedAt,
256
+ durationMs,
257
+ status: mission?.status ?? "unknown",
258
+ agentCount: agents.length,
259
+ },
260
+ tasks: {
261
+ total: tasks.length,
262
+ completed,
263
+ failed,
264
+ retried,
265
+ completionRate: Math.round(completionRate * 100) / 100,
266
+ retryRate: Math.round(retryRate * 100) / 100,
267
+ avgDurationMs:
268
+ durations.length > 0
269
+ ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
270
+ : 0,
271
+ medianDurationMs: median(durations),
272
+ p90DurationMs: percentile(durations, 90),
273
+ byMilestone,
274
+ },
275
+ agents,
276
+ mission: {
277
+ title: mission?.title ?? null,
278
+ status: mission?.status ?? null,
279
+ milestonesCompleted,
280
+ validationPassRate: Math.round(validationPassRate * 100) / 100,
281
+ wallClockMs: missionWallClock,
282
+ },
283
+ timeline,
284
+ };
285
+ }
286
+
287
+ // --- Persistence ---
288
+
289
+ export function loadMetrics(dir: string): MetricsSnapshot | null {
290
+ const path = join(dir, ".tasks", "metrics.json");
291
+ if (!existsSync(path)) return null;
292
+ try {
293
+ return JSON.parse(readFileSync(path, "utf-8")) as MetricsSnapshot;
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+
299
+ export function saveMetrics(dir: string, snapshot: MetricsSnapshot): void {
300
+ const tasksDir = join(dir, ".tasks");
301
+ if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
302
+ const path = join(tasksDir, "metrics.json");
303
+ const tmpPath = path + ".tmp";
304
+ writeFileSync(tmpPath, JSON.stringify(snapshot, null, 2) + "\n");
305
+ renameSync(tmpPath, path);
306
+ }
307
+
308
+ export function computeAndSaveMetrics(dir: string): MetricsSnapshot {
309
+ const snapshot = computeMetrics(dir);
310
+ saveMetrics(dir, snapshot);
311
+ return snapshot;
312
+ }
313
+
314
+ export function appendMissionHistory(dir: string, mission: Mission, tasks: Task[]): void {
315
+ const tasksDir = join(dir, ".tasks");
316
+ if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
317
+ const path = join(tasksDir, "metrics-history.jsonl");
318
+ const completed = tasks.filter((t) => t.status === "done").length;
319
+ const agentNames = new Set(tasks.filter((t) => t.assignee).map((t) => t.assignee!));
320
+ const summary: MissionSummary = {
321
+ title: mission.title,
322
+ status: mission.status,
323
+ completedAt: new Date().toISOString(),
324
+ wallClockMs: mission.created ? Date.now() - new Date(mission.created).getTime() : 0,
325
+ taskCount: tasks.length,
326
+ completionRate: tasks.length > 0 ? Math.round((completed / tasks.length) * 100) / 100 : 0,
327
+ agentCount: agentNames.size,
328
+ milestonesCompleted: mission.milestones.filter((m) => m.status === "done").length,
329
+ };
330
+ appendFileSync(path, JSON.stringify(summary) + "\n");
331
+ }
332
+
333
+ export function loadMissionHistory(dir: string): MissionSummary[] {
334
+ const path = join(dir, ".tasks", "metrics-history.jsonl");
335
+ if (!existsSync(path)) return [];
336
+ try {
337
+ return readFileSync(path, "utf-8")
338
+ .trim()
339
+ .split("\n")
340
+ .filter(Boolean)
341
+ .map((line) => JSON.parse(line) as MissionSummary);
342
+ } catch {
343
+ return [];
344
+ }
345
+ }