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.
- package/bin/cli.js +4 -216
- package/bin/cli.ts +536 -0
- package/package.json +33 -14
- package/src/__tests__/support.ts +186 -0
- package/src/{attach.js → attach.ts} +7 -4
- package/src/cli.test.ts +314 -0
- package/src/command-center/discovery.test.ts +265 -0
- package/src/command-center/discovery.ts +296 -0
- package/src/command-center/index.ts +31 -0
- package/src/command-center/schemas.ts +57 -0
- package/src/command-center/server.test.ts +818 -0
- package/src/command-center/server.ts +1426 -0
- package/src/config-cli.test.ts +86 -0
- package/src/config.test.ts +205 -0
- package/src/config.ts +313 -0
- package/src/detect.test.ts +142 -0
- package/src/{detect.js → detect.ts} +31 -15
- package/src/dispatch.ts +102 -0
- package/src/doctor.test.ts +119 -0
- package/src/doctor.ts +167 -0
- package/src/init.test.ts +101 -0
- package/src/init.ts +144 -0
- package/src/inspect.test.ts +67 -0
- package/src/{inspect.js → inspect.ts} +95 -10
- package/src/integration.test.ts.skip +276 -0
- package/src/js-yaml.d.ts +10 -0
- package/src/launch.test.ts +222 -0
- package/src/launch.ts +494 -0
- package/src/lib/auth/auth-service.test.ts +178 -0
- package/src/lib/auth/auth-service.ts +227 -0
- package/src/lib/auth/middleware.test.ts +119 -0
- package/src/lib/auth/middleware.ts +56 -0
- package/src/lib/auth/types.ts +13 -0
- package/src/lib/authorship.test.ts +242 -0
- package/src/lib/authorship.ts +280 -0
- package/src/lib/cast/recorder.test.ts +127 -0
- package/src/lib/cast/recorder.ts +148 -0
- package/src/lib/daemon-watchdog.ts +102 -0
- package/src/lib/daemon.ts +417 -0
- package/src/lib/dot-path.test.ts +66 -0
- package/src/lib/dot-path.ts +17 -0
- package/src/lib/errors.ts +44 -0
- package/src/lib/event-log.test.ts +189 -0
- package/src/lib/event-log.ts +237 -0
- package/src/lib/github-pr.ts +182 -0
- package/src/lib/hq/client.test.ts +154 -0
- package/src/lib/hq/client.ts +142 -0
- package/src/lib/hq/mdns.test.ts +54 -0
- package/src/lib/hq/mdns.ts +88 -0
- package/src/lib/hq/registry.test.ts +237 -0
- package/src/lib/hq/registry.ts +145 -0
- package/src/lib/hq/types.ts +44 -0
- package/src/lib/ipc/socket-protocol.test.ts +126 -0
- package/src/lib/ipc/socket-protocol.ts +324 -0
- package/src/lib/launch-plan.test.ts +119 -0
- package/src/lib/launch-plan.ts +90 -0
- package/src/lib/log.test.ts +39 -0
- package/src/lib/log.ts +71 -0
- package/src/lib/metrics.test.ts +213 -0
- package/src/lib/metrics.ts +345 -0
- package/src/lib/orchestrator.test.ts +2358 -0
- package/src/lib/orchestrator.ts +1792 -0
- package/src/lib/{output.js → output.ts} +18 -10
- package/src/lib/plan-store.test.ts +148 -0
- package/src/lib/plan-store.ts +131 -0
- package/src/lib/research.test.ts +208 -0
- package/src/lib/research.ts +475 -0
- package/src/lib/round-trip.test.ts +341 -0
- package/src/lib/session-monitor.test.ts +152 -0
- package/src/lib/session-monitor.ts +282 -0
- package/src/lib/session-options.test.ts +146 -0
- package/src/lib/{session-options.js → session-options.ts} +11 -6
- package/src/lib/shell.test.ts +20 -0
- package/src/lib/shell.ts +8 -0
- package/src/lib/sizes.test.ts +73 -0
- package/src/lib/{sizes.js → sizes.ts} +4 -4
- package/src/lib/skill-registry.test.ts +134 -0
- package/src/lib/skill-registry.ts +86 -0
- package/src/lib/slugify.test.ts +35 -0
- package/src/lib/slugify.ts +10 -0
- package/src/lib/task-store.ts +352 -0
- package/src/lib/tmux.test.ts +500 -0
- package/src/lib/tmux.ts +372 -0
- package/src/lib/token-tracker.test.ts +102 -0
- package/src/lib/token-tracker.ts +70 -0
- package/src/lib/tunnels/cloudflare.test.ts +73 -0
- package/src/lib/tunnels/cloudflare.ts +254 -0
- package/src/lib/tunnels/manager.test.ts +63 -0
- package/src/lib/tunnels/manager.ts +132 -0
- package/src/lib/tunnels/ngrok.test.ts +94 -0
- package/src/lib/tunnels/ngrok.ts +261 -0
- package/src/lib/tunnels/tailscale.test.ts +103 -0
- package/src/lib/tunnels/tailscale.ts +1048 -0
- package/src/lib/tunnels/types.ts +44 -0
- package/src/lib/validation.test.ts +289 -0
- package/src/lib/validation.ts +123 -0
- package/src/lib/webhook.ts +51 -0
- package/src/lib/workflow-store.ts +209 -0
- package/src/lib/ws-v3/protocol.test.ts +127 -0
- package/src/lib/ws-v3/protocol.ts +160 -0
- package/src/lib/yaml-io.test.ts +125 -0
- package/src/lib/{yaml-io.js → yaml-io.ts} +5 -4
- package/src/ls.test.ts +76 -0
- package/src/{ls.js → ls.ts} +4 -4
- package/src/metrics-cli.ts +177 -0
- package/src/notify.ts +85 -0
- package/src/orchestrator-status.ts +228 -0
- package/src/plan.ts +117 -0
- package/src/postinstall.test.ts +79 -0
- package/src/remote.test.ts +115 -0
- package/src/remote.ts +148 -0
- package/src/restart.ts +24 -0
- package/src/schemas/domain.ts +422 -0
- package/src/schemas/ide-config.ts +148 -0
- package/src/schemas/index.ts +53 -0
- package/src/send.test.ts +70 -0
- package/src/send.ts +181 -0
- package/src/skill.test.ts +114 -0
- package/src/skill.ts +138 -0
- package/src/status.ts +66 -0
- package/src/stop.test.ts +176 -0
- package/src/stop.ts +47 -0
- package/src/task.test.ts +1466 -0
- package/src/task.ts +1143 -0
- package/src/tunnel.test.ts +80 -0
- package/src/tunnel.ts +133 -0
- package/src/types.ts +15 -0
- package/src/ui/index.ts +32 -0
- package/src/ui/terminal/index.ts +9 -0
- package/src/ui/types.ts +91 -0
- package/src/ui/web/base.css +80 -0
- package/src/ui/web/components/Box.tsx +59 -0
- package/src/ui/web/components/Input.tsx +32 -0
- package/src/ui/web/components/ScrollBox.tsx +60 -0
- package/src/ui/web/components/Text.tsx +28 -0
- package/src/ui/web/hooks.ts +106 -0
- package/src/ui/web/index.ts +27 -0
- package/src/ui/web/render.ts +77 -0
- package/src/ui/web/utils/color.test.ts +93 -0
- package/src/ui/web/utils/color.ts +27 -0
- package/src/validate.test.ts +385 -0
- package/src/validate.ts +217 -0
- package/src/widgets/changes/index.tsx +576 -0
- package/src/widgets/config/index.tsx +440 -0
- package/src/widgets/costs/index.tsx +216 -0
- package/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/src/widgets/explorer/footer.tsx +20 -0
- package/src/widgets/explorer/header.tsx +23 -0
- package/src/widgets/explorer/index.tsx +408 -0
- package/src/widgets/explorer/tree-model.test.ts +214 -0
- package/src/widgets/explorer/tree-model.ts +103 -0
- package/src/widgets/explorer/tree.tsx +156 -0
- package/src/widgets/lib/config-model.ts +116 -0
- package/src/widgets/lib/files.test.ts +103 -0
- package/src/widgets/lib/files.ts +88 -0
- package/src/widgets/lib/git.test.ts +151 -0
- package/src/widgets/lib/git.ts +88 -0
- package/src/widgets/lib/pane-comms.test.ts +178 -0
- package/src/widgets/lib/pane-comms.ts +192 -0
- package/src/widgets/lib/theme.ts +151 -0
- package/src/widgets/lib/watcher.ts +50 -0
- package/src/widgets/mission-control/activity-feed.tsx +59 -0
- package/src/widgets/mission-control/agent-panel.tsx +132 -0
- package/src/widgets/mission-control/command-bar.tsx +63 -0
- package/src/widgets/mission-control/index.tsx +751 -0
- package/src/widgets/mission-control/task-panel.tsx +238 -0
- package/src/widgets/preview/index.tsx +415 -0
- package/src/widgets/resolve.ts +40 -0
- package/src/widgets/setup/agent-naming.tsx +112 -0
- package/src/widgets/setup/config-tree.tsx +238 -0
- package/src/widgets/setup/detect-panel.tsx +72 -0
- package/src/widgets/setup/field-editor.tsx +265 -0
- package/src/widgets/setup/footer.tsx +107 -0
- package/src/widgets/setup/index.tsx +340 -0
- package/src/widgets/setup/layout-picker.tsx +96 -0
- package/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/src/widgets/setup/review-panel.tsx +140 -0
- package/src/widgets/setup/setup-model.test.ts +303 -0
- package/src/widgets/setup/setup-model.ts +188 -0
- package/src/widgets/tasks/index.tsx +129 -0
- package/src/widgets/tasks/task-detail.tsx +309 -0
- package/src/widgets/tasks/task-form.tsx +193 -0
- package/src/widgets/tasks/task-list.tsx +205 -0
- package/src/widgets/tasks/task-model.test.ts +473 -0
- package/src/widgets/tasks/task-model.ts +157 -0
- package/templates/AGENTS.md +20 -0
- package/templates/missions.yml +41 -0
- package/templates/skills/backend.md +30 -0
- package/templates/skills/frontend.md +30 -0
- package/templates/skills/general-worker.md +30 -0
- package/templates/skills/researcher.md +69 -0
- package/templates/skills/reviewer.md +30 -0
- package/src/config.js +0 -341
- package/src/doctor.js +0 -91
- package/src/init.js +0 -73
- package/src/launch.js +0 -245
- package/src/lib/dot-path.js +0 -16
- package/src/lib/errors.js +0 -35
- package/src/lib/launch-plan.js +0 -49
- package/src/lib/session-monitor.js +0 -179
- package/src/lib/tmux.js +0 -237
- package/src/restart.js +0 -16
- package/src/status.js +0 -35
- package/src/stop.js +0 -25
- 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
|
+
}
|