@os-eco/overstory-cli 0.6.11 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -13
- package/agents/builder.md +1 -1
- package/agents/coordinator.md +12 -11
- package/agents/lead.md +25 -24
- package/agents/monitor.md +4 -4
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +5 -5
- package/agents/supervisor.md +36 -32
- package/package.json +5 -3
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/hooks-deployer.ts +7 -90
- package/src/agents/overlay.test.ts +30 -7
- package/src/agents/overlay.ts +10 -9
- package/src/commands/agents.test.ts +5 -0
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/coordinator.test.ts +1 -0
- package/src/commands/coordinator.ts +34 -18
- package/src/commands/costs.test.ts +6 -1
- package/src/commands/costs.ts +13 -20
- package/src/commands/dashboard.ts +38 -138
- package/src/commands/doctor.test.ts +1 -1
- package/src/commands/doctor.ts +2 -2
- package/src/commands/ecosystem.ts +2 -1
- package/src/commands/errors.test.ts +4 -5
- package/src/commands/errors.ts +4 -62
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +12 -106
- package/src/commands/init.test.ts +1 -2
- package/src/commands/init.ts +1 -8
- package/src/commands/inspect.test.ts +14 -0
- package/src/commands/inspect.ts +10 -44
- package/src/commands/log.test.ts +14 -0
- package/src/commands/log.ts +39 -0
- package/src/commands/logs.ts +7 -63
- package/src/commands/mail.test.ts +5 -0
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +3 -17
- package/src/commands/monitor.ts +30 -16
- package/src/commands/nudge.test.ts +1 -0
- package/src/commands/prime.test.ts +2 -0
- package/src/commands/prime.ts +6 -2
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +12 -135
- package/src/commands/run.test.ts +1 -0
- package/src/commands/run.ts +7 -23
- package/src/commands/sling.test.ts +68 -1
- package/src/commands/sling.ts +62 -24
- package/src/commands/status.test.ts +1 -0
- package/src/commands/status.ts +4 -17
- package/src/commands/stop.test.ts +1 -0
- package/src/commands/supervisor.ts +35 -18
- package/src/commands/trace.test.ts +6 -6
- package/src/commands/trace.ts +11 -109
- package/src/commands/worktree.test.ts +9 -0
- package/src/config.ts +39 -0
- package/src/doctor/consistency.test.ts +14 -0
- package/src/e2e/init-sling-lifecycle.test.ts +3 -5
- package/src/index.ts +2 -1
- package/src/logging/format.ts +214 -0
- package/src/logging/theme.ts +132 -0
- package/src/mail/broadcast.test.ts +1 -0
- package/src/merge/resolver.ts +23 -4
- package/src/metrics/store.test.ts +46 -0
- package/src/metrics/store.ts +11 -0
- package/src/mulch/client.test.ts +20 -0
- package/src/mulch/client.ts +312 -45
- package/src/runtimes/claude.test.ts +616 -0
- package/src/runtimes/claude.ts +218 -0
- package/src/runtimes/pi-guards.test.ts +433 -0
- package/src/runtimes/pi-guards.ts +349 -0
- package/src/runtimes/pi.test.ts +620 -0
- package/src/runtimes/pi.ts +244 -0
- package/src/runtimes/registry.test.ts +86 -0
- package/src/runtimes/registry.ts +46 -0
- package/src/runtimes/types.ts +188 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/compat.ts +1 -0
- package/src/sessions/store.test.ts +31 -0
- package/src/sessions/store.ts +37 -4
- package/src/types.ts +21 -0
- package/src/watchdog/daemon.test.ts +7 -4
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -0
- package/src/watchdog/triage.ts +14 -4
- package/src/worktree/tmux.test.ts +28 -13
- package/src/worktree/tmux.ts +14 -28
package/src/commands/costs.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { loadConfig } from "../config.ts";
|
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
14
|
import { jsonError, jsonOutput } from "../json.ts";
|
|
15
15
|
import { color } from "../logging/color.ts";
|
|
16
|
+
import { renderHeader, separator } from "../logging/theme.ts";
|
|
16
17
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
17
18
|
import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
|
|
18
19
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
@@ -141,10 +142,8 @@ function groupByCapability(sessions: SessionMetrics[]): CapabilityGroup[] {
|
|
|
141
142
|
/** Print the standard per-agent cost summary table. */
|
|
142
143
|
function printCostSummary(sessions: SessionMetrics[]): void {
|
|
143
144
|
const w = process.stdout.write.bind(process.stdout);
|
|
144
|
-
const separator = "\u2500".repeat(70);
|
|
145
145
|
|
|
146
|
-
w(`${
|
|
147
|
-
w(`${"=".repeat(70)}\n`);
|
|
146
|
+
w(`${renderHeader("Cost Summary")}\n`);
|
|
148
147
|
|
|
149
148
|
if (sessions.length === 0) {
|
|
150
149
|
w(`${color.dim("No session data found.")}\n`);
|
|
@@ -156,7 +155,7 @@ function printCostSummary(sessions: SessionMetrics[]): void {
|
|
|
156
155
|
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
157
156
|
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
158
157
|
);
|
|
159
|
-
w(`${color.dim(separator)}\n`);
|
|
158
|
+
w(`${color.dim(separator())}\n`);
|
|
160
159
|
|
|
161
160
|
for (const s of sessions) {
|
|
162
161
|
const cacheTotal = s.cacheReadTokens + s.cacheCreationTokens;
|
|
@@ -170,7 +169,7 @@ function printCostSummary(sessions: SessionMetrics[]): void {
|
|
|
170
169
|
}
|
|
171
170
|
|
|
172
171
|
const totals = computeTotals(sessions);
|
|
173
|
-
w(`${color.dim(separator)}\n`);
|
|
172
|
+
w(`${color.dim(separator())}\n`);
|
|
174
173
|
w(
|
|
175
174
|
`${color.green(
|
|
176
175
|
color.bold(
|
|
@@ -187,10 +186,8 @@ function printCostSummary(sessions: SessionMetrics[]): void {
|
|
|
187
186
|
/** Print the capability-grouped cost table. */
|
|
188
187
|
function printByCapability(sessions: SessionMetrics[]): void {
|
|
189
188
|
const w = process.stdout.write.bind(process.stdout);
|
|
190
|
-
const separator = "\u2500".repeat(70);
|
|
191
189
|
|
|
192
|
-
w(`${
|
|
193
|
-
w(`${"=".repeat(70)}\n`);
|
|
190
|
+
w(`${renderHeader("Cost by Capability")}\n`);
|
|
194
191
|
|
|
195
192
|
if (sessions.length === 0) {
|
|
196
193
|
w(`${color.dim("No session data found.")}\n`);
|
|
@@ -202,7 +199,7 @@ function printByCapability(sessions: SessionMetrics[]): void {
|
|
|
202
199
|
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
203
200
|
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
204
201
|
);
|
|
205
|
-
w(`${color.dim(separator)}\n`);
|
|
202
|
+
w(`${color.dim(separator())}\n`);
|
|
206
203
|
|
|
207
204
|
const groups = groupByCapability(sessions);
|
|
208
205
|
|
|
@@ -218,7 +215,7 @@ function printByCapability(sessions: SessionMetrics[]): void {
|
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
const totals = computeTotals(sessions);
|
|
221
|
-
w(`${color.dim(separator)}\n`);
|
|
218
|
+
w(`${color.dim(separator())}\n`);
|
|
222
219
|
w(
|
|
223
220
|
`${color.green(
|
|
224
221
|
color.bold(
|
|
@@ -299,17 +296,15 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
299
296
|
});
|
|
300
297
|
} else {
|
|
301
298
|
const w = process.stdout.write.bind(process.stdout);
|
|
302
|
-
const separator = "\u2500".repeat(70);
|
|
303
299
|
|
|
304
|
-
w(`${
|
|
305
|
-
w(`${"=".repeat(70)}\n`);
|
|
300
|
+
w(`${renderHeader("Orchestrator Session Cost")}\n`);
|
|
306
301
|
w(`${padRight("Model:", 12)}${usage.modelUsed ?? "unknown"}\n`);
|
|
307
302
|
w(`${padRight("Transcript:", 12)}${transcriptPath}\n`);
|
|
308
|
-
w(`${color.dim(separator)}\n`);
|
|
303
|
+
w(`${color.dim(separator())}\n`);
|
|
309
304
|
w(`${padRight("Input tokens:", 22)}${padLeft(formatNumber(usage.inputTokens), 12)}\n`);
|
|
310
305
|
w(`${padRight("Output tokens:", 22)}${padLeft(formatNumber(usage.outputTokens), 12)}\n`);
|
|
311
306
|
w(`${padRight("Cache tokens:", 22)}${padLeft(formatNumber(cacheTotal), 12)}\n`);
|
|
312
|
-
w(`${color.dim(separator)}\n`);
|
|
307
|
+
w(`${color.dim(separator())}\n`);
|
|
313
308
|
w(
|
|
314
309
|
`${color.green(color.bold(padRight("Estimated cost:", 22) + padLeft(formatCost(cost), 12)))}\n`,
|
|
315
310
|
);
|
|
@@ -453,16 +448,14 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
453
448
|
});
|
|
454
449
|
} else {
|
|
455
450
|
const w = process.stdout.write.bind(process.stdout);
|
|
456
|
-
const separator = "\u2500".repeat(70);
|
|
457
451
|
|
|
458
|
-
w(`${
|
|
459
|
-
w(`${"=".repeat(70)}\n`);
|
|
452
|
+
w(`${renderHeader(`Live Token Usage (${agentData.length} active agents)`)}\n`);
|
|
460
453
|
w(
|
|
461
454
|
`${padRight("Agent", 19)}${padRight("Capability", 12)}` +
|
|
462
455
|
`${padLeft("Input", 10)}${padLeft("Output", 10)}` +
|
|
463
456
|
`${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
|
|
464
457
|
);
|
|
465
|
-
w(`${color.dim(separator)}\n`);
|
|
458
|
+
w(`${color.dim(separator())}\n`);
|
|
466
459
|
|
|
467
460
|
for (const agent of agentData) {
|
|
468
461
|
const cacheTotal = agent.cacheReadTokens + agent.cacheCreationTokens;
|
|
@@ -475,7 +468,7 @@ async function executeCosts(opts: CostsOpts): Promise<void> {
|
|
|
475
468
|
);
|
|
476
469
|
}
|
|
477
470
|
|
|
478
|
-
w(`${color.dim(separator)}\n`);
|
|
471
|
+
w(`${color.dim(separator())}\n`);
|
|
479
472
|
w(
|
|
480
473
|
`${color.green(
|
|
481
474
|
color.bold(
|
|
@@ -14,8 +14,14 @@ import { join, resolve } from "node:path";
|
|
|
14
14
|
import { Command } from "commander";
|
|
15
15
|
import { loadConfig } from "../config.ts";
|
|
16
16
|
import { ValidationError } from "../errors.ts";
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
17
|
+
import { accent, brand, color, visibleLength } from "../logging/color.ts";
|
|
18
|
+
import {
|
|
19
|
+
formatDuration,
|
|
20
|
+
formatRelativeTime,
|
|
21
|
+
mergeStatusColor,
|
|
22
|
+
priorityColor,
|
|
23
|
+
} from "../logging/format.ts";
|
|
24
|
+
import { stateColor, stateIcon } from "../logging/theme.ts";
|
|
19
25
|
import { createMailStore, type MailStore } from "../mail/store.ts";
|
|
20
26
|
import { createMergeQueue, type MergeQueue } from "../merge/queue.ts";
|
|
21
27
|
import { createMetricsStore, type MetricsStore } from "../metrics/store.ts";
|
|
@@ -54,38 +60,6 @@ const BOX = {
|
|
|
54
60
|
cross: "┼",
|
|
55
61
|
};
|
|
56
62
|
|
|
57
|
-
/**
|
|
58
|
-
* Format a duration in ms to a human-readable string.
|
|
59
|
-
*/
|
|
60
|
-
function formatDuration(ms: number): string {
|
|
61
|
-
const seconds = Math.floor(ms / 1000);
|
|
62
|
-
if (seconds < 60) return `${seconds}s`;
|
|
63
|
-
const minutes = Math.floor(seconds / 60);
|
|
64
|
-
const remainSec = seconds % 60;
|
|
65
|
-
if (minutes < 60) return `${minutes}m ${remainSec}s`;
|
|
66
|
-
const hours = Math.floor(minutes / 60);
|
|
67
|
-
const remainMin = minutes % 60;
|
|
68
|
-
return `${hours}h ${remainMin}m`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Format a timestamp to "time ago" format.
|
|
73
|
-
*/
|
|
74
|
-
function timeAgo(timestamp: string): string {
|
|
75
|
-
const now = Date.now();
|
|
76
|
-
const then = new Date(timestamp).getTime();
|
|
77
|
-
const diffMs = now - then;
|
|
78
|
-
const diffSec = Math.floor(diffMs / 1000);
|
|
79
|
-
|
|
80
|
-
if (diffSec < 60) return `${diffSec}s ago`;
|
|
81
|
-
const diffMin = Math.floor(diffSec / 60);
|
|
82
|
-
if (diffMin < 60) return `${diffMin}m ago`;
|
|
83
|
-
const diffHr = Math.floor(diffMin / 60);
|
|
84
|
-
if (diffHr < 24) return `${diffHr}h ago`;
|
|
85
|
-
const diffDay = Math.floor(diffHr / 24);
|
|
86
|
-
return `${diffDay}d ago`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
63
|
/**
|
|
90
64
|
* Truncate a string to fit within maxLen characters, adding ellipsis if needed.
|
|
91
65
|
*/
|
|
@@ -297,7 +271,7 @@ async function loadDashboardData(
|
|
|
297
271
|
let recentMetricsCount = 0;
|
|
298
272
|
if (stores.metricsStore) {
|
|
299
273
|
try {
|
|
300
|
-
recentMetricsCount = stores.metricsStore.
|
|
274
|
+
recentMetricsCount = stores.metricsStore.countSessions();
|
|
301
275
|
} catch {
|
|
302
276
|
// best effort
|
|
303
277
|
}
|
|
@@ -357,32 +331,34 @@ async function loadDashboardData(
|
|
|
357
331
|
const byCapability: Record<string, number> = {};
|
|
358
332
|
if (stores.metricsStore) {
|
|
359
333
|
try {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const agentNames = new Set(filteredAgents.map((a) => a.agentName));
|
|
366
|
-
return sessions.filter((s) => agentNames.has(s.agentName));
|
|
367
|
-
})()
|
|
368
|
-
: sessions;
|
|
334
|
+
if (runId && filteredAgents.length > 0) {
|
|
335
|
+
// Run-scoped: filter sessions by agent names, compute all values from the filtered set
|
|
336
|
+
const agentNames = new Set(filteredAgents.map((a) => a.agentName));
|
|
337
|
+
const sessions = stores.metricsStore.getRecentSessions(100);
|
|
338
|
+
const filtered = sessions.filter((s) => agentNames.has(s.agentName));
|
|
369
339
|
|
|
370
|
-
|
|
340
|
+
totalSessions = filtered.length;
|
|
371
341
|
|
|
372
|
-
// When run-scoped, compute avg duration from filtered sessions manually
|
|
373
|
-
if (runId && filteredAgents.length > 0) {
|
|
374
342
|
const completedSessions = filtered.filter((s) => s.completedAt !== null);
|
|
375
343
|
if (completedSessions.length > 0) {
|
|
376
344
|
avgDuration =
|
|
377
345
|
completedSessions.reduce((sum, s) => sum + s.durationMs, 0) / completedSessions.length;
|
|
378
346
|
}
|
|
347
|
+
|
|
348
|
+
for (const session of filtered) {
|
|
349
|
+
const cap = session.capability;
|
|
350
|
+
byCapability[cap] = (byCapability[cap] ?? 0) + 1;
|
|
351
|
+
}
|
|
379
352
|
} else {
|
|
353
|
+
// All-runs view: use countSessions() to get accurate total (not capped at 100)
|
|
354
|
+
totalSessions = stores.metricsStore.countSessions();
|
|
380
355
|
avgDuration = stores.metricsStore.getAverageDuration();
|
|
381
|
-
}
|
|
382
356
|
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
357
|
+
const sessions = stores.metricsStore.getRecentSessions(100);
|
|
358
|
+
for (const session of sessions) {
|
|
359
|
+
const cap = session.capability;
|
|
360
|
+
byCapability[cap] = (byCapability[cap] ?? 0) + 1;
|
|
361
|
+
}
|
|
386
362
|
}
|
|
387
363
|
} catch {
|
|
388
364
|
// best effort
|
|
@@ -402,7 +378,7 @@ async function loadDashboardData(
|
|
|
402
378
|
* Render the header bar (line 1).
|
|
403
379
|
*/
|
|
404
380
|
function renderHeader(width: number, interval: number, currentRunId?: string | null): string {
|
|
405
|
-
const left =
|
|
381
|
+
const left = brand.bold(`ov dashboard v${PKG_VERSION}`);
|
|
406
382
|
const now = new Date().toLocaleTimeString();
|
|
407
383
|
const scope = currentRunId ? ` [run: ${accent(currentRunId.slice(0, 8))}]` : " [all runs]";
|
|
408
384
|
const right = `${now}${scope} | refresh: ${interval}ms`;
|
|
@@ -412,46 +388,6 @@ function renderHeader(width: number, interval: number, currentRunId?: string | n
|
|
|
412
388
|
return `${line}\n${separator}`;
|
|
413
389
|
}
|
|
414
390
|
|
|
415
|
-
/**
|
|
416
|
-
* Get color function for agent state.
|
|
417
|
-
*/
|
|
418
|
-
function getStateColor(state: string): ColorFn {
|
|
419
|
-
switch (state) {
|
|
420
|
-
case "working":
|
|
421
|
-
return color.green;
|
|
422
|
-
case "booting":
|
|
423
|
-
return color.yellow;
|
|
424
|
-
case "stalled":
|
|
425
|
-
return color.red;
|
|
426
|
-
case "zombie":
|
|
427
|
-
return color.dim;
|
|
428
|
-
case "completed":
|
|
429
|
-
return color.cyan;
|
|
430
|
-
default:
|
|
431
|
-
return noColor;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Get status icon for agent state.
|
|
437
|
-
*/
|
|
438
|
-
function getStateIcon(state: string): string {
|
|
439
|
-
switch (state) {
|
|
440
|
-
case "working":
|
|
441
|
-
return ">";
|
|
442
|
-
case "booting":
|
|
443
|
-
return "-";
|
|
444
|
-
case "stalled":
|
|
445
|
-
return "!";
|
|
446
|
-
case "zombie":
|
|
447
|
-
return "x";
|
|
448
|
-
case "completed":
|
|
449
|
-
return "x";
|
|
450
|
-
default:
|
|
451
|
-
return "?";
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
391
|
/**
|
|
456
392
|
* Render the agent panel (top ~40% of screen).
|
|
457
393
|
*/
|
|
@@ -465,7 +401,7 @@ function renderAgentPanel(
|
|
|
465
401
|
let output = "";
|
|
466
402
|
|
|
467
403
|
// Panel header
|
|
468
|
-
const headerLine = `${BOX.vertical} ${
|
|
404
|
+
const headerLine = `${BOX.vertical} ${brand.bold("Agents")} (${data.status.agents.length})`;
|
|
469
405
|
const headerPadding = " ".repeat(Math.max(0, width - visibleLength(headerLine) - 1));
|
|
470
406
|
output += `${CURSOR.cursorTo(startRow, 1)}${headerLine}${headerPadding}${BOX.vertical}\n`;
|
|
471
407
|
|
|
@@ -495,8 +431,8 @@ function renderAgentPanel(
|
|
|
495
431
|
const agent = visibleAgents[i];
|
|
496
432
|
if (!agent) continue;
|
|
497
433
|
|
|
498
|
-
const icon =
|
|
499
|
-
const
|
|
434
|
+
const icon = stateIcon(agent.state);
|
|
435
|
+
const stateColorFn = stateColor(agent.state);
|
|
500
436
|
const name = accent(pad(truncate(agent.agentName, 15), 15));
|
|
501
437
|
const capability = pad(truncate(agent.capability, 12), 12);
|
|
502
438
|
const state = pad(agent.state, 10);
|
|
@@ -510,7 +446,7 @@ function renderAgentPanel(
|
|
|
510
446
|
const tmuxAlive = data.status.tmuxSessions.some((s) => s.name === agent.tmuxSession);
|
|
511
447
|
const tmuxDot = tmuxAlive ? color.green(">") : color.red("x");
|
|
512
448
|
|
|
513
|
-
const line = `${BOX.vertical} ${
|
|
449
|
+
const line = `${BOX.vertical} ${stateColorFn(icon)} ${name} ${capability} ${stateColorFn(state)} ${taskId} ${durationPadded} ${tmuxDot} ${BOX.vertical}`;
|
|
514
450
|
output += `${CURSOR.cursorTo(startRow + 3 + i, 1)}${line}\n`;
|
|
515
451
|
}
|
|
516
452
|
|
|
@@ -527,24 +463,6 @@ function renderAgentPanel(
|
|
|
527
463
|
return output;
|
|
528
464
|
}
|
|
529
465
|
|
|
530
|
-
/**
|
|
531
|
-
* Get color function for mail priority.
|
|
532
|
-
*/
|
|
533
|
-
function getPriorityColor(priority: string): ColorFn {
|
|
534
|
-
switch (priority) {
|
|
535
|
-
case "urgent":
|
|
536
|
-
return color.red;
|
|
537
|
-
case "high":
|
|
538
|
-
return color.yellow;
|
|
539
|
-
case "normal":
|
|
540
|
-
return noColor;
|
|
541
|
-
case "low":
|
|
542
|
-
return color.dim;
|
|
543
|
-
default:
|
|
544
|
-
return noColor;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
466
|
/**
|
|
549
467
|
* Render the mail panel (middle-left ~30% height, ~60% width).
|
|
550
468
|
*/
|
|
@@ -559,7 +477,7 @@ function renderMailPanel(
|
|
|
559
477
|
let output = "";
|
|
560
478
|
|
|
561
479
|
const unreadCount = data.status.unreadMailCount;
|
|
562
|
-
const headerLine = `${BOX.vertical} ${
|
|
480
|
+
const headerLine = `${BOX.vertical} ${brand.bold("Mail")} (${unreadCount} unread)`;
|
|
563
481
|
const headerPadding = " ".repeat(Math.max(0, panelWidth - visibleLength(headerLine) - 1));
|
|
564
482
|
output += `${CURSOR.cursorTo(startRow, 1)}${headerLine}${headerPadding}${BOX.vertical}\n`;
|
|
565
483
|
|
|
@@ -573,12 +491,12 @@ function renderMailPanel(
|
|
|
573
491
|
const msg = messages[i];
|
|
574
492
|
if (!msg) continue;
|
|
575
493
|
|
|
576
|
-
const priorityColorFn =
|
|
494
|
+
const priorityColorFn = priorityColor(msg.priority);
|
|
577
495
|
const priority = msg.priority === "normal" ? "" : `[${msg.priority}] `;
|
|
578
496
|
const from = accent(truncate(msg.from, 12));
|
|
579
497
|
const to = accent(truncate(msg.to, 12));
|
|
580
498
|
const subject = truncate(msg.subject, panelWidth - 40);
|
|
581
|
-
const time =
|
|
499
|
+
const time = formatRelativeTime(msg.createdAt);
|
|
582
500
|
|
|
583
501
|
const coloredPriority = priority ? priorityColorFn(priority) : "";
|
|
584
502
|
const line = `${BOX.vertical} ${coloredPriority}${from} → ${to}: ${subject} (${time})`;
|
|
@@ -595,24 +513,6 @@ function renderMailPanel(
|
|
|
595
513
|
return output;
|
|
596
514
|
}
|
|
597
515
|
|
|
598
|
-
/**
|
|
599
|
-
* Get color function for merge queue status.
|
|
600
|
-
*/
|
|
601
|
-
function getMergeStatusColor(status: string): ColorFn {
|
|
602
|
-
switch (status) {
|
|
603
|
-
case "pending":
|
|
604
|
-
return color.yellow;
|
|
605
|
-
case "merging":
|
|
606
|
-
return color.blue;
|
|
607
|
-
case "conflict":
|
|
608
|
-
return color.red;
|
|
609
|
-
case "merged":
|
|
610
|
-
return color.green;
|
|
611
|
-
default:
|
|
612
|
-
return noColor;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
516
|
/**
|
|
617
517
|
* Render the merge queue panel (middle-right ~30% height, ~40% width).
|
|
618
518
|
*/
|
|
@@ -627,7 +527,7 @@ function renderMergeQueuePanel(
|
|
|
627
527
|
const panelWidth = width - startCol + 1;
|
|
628
528
|
let output = "";
|
|
629
529
|
|
|
630
|
-
const headerLine = `${BOX.vertical} ${
|
|
530
|
+
const headerLine = `${BOX.vertical} ${brand.bold("Merge Queue")} (${data.mergeQueue.length})`;
|
|
631
531
|
const headerPadding = " ".repeat(Math.max(0, panelWidth - visibleLength(headerLine) - 1));
|
|
632
532
|
output += `${CURSOR.cursorTo(startRow, startCol)}${headerLine}${headerPadding}${BOX.vertical}\n`;
|
|
633
533
|
|
|
@@ -641,7 +541,7 @@ function renderMergeQueuePanel(
|
|
|
641
541
|
const entry = entries[i];
|
|
642
542
|
if (!entry) continue;
|
|
643
543
|
|
|
644
|
-
const statusColorFn =
|
|
544
|
+
const statusColorFn = mergeStatusColor(entry.status);
|
|
645
545
|
const status = pad(entry.status, 10);
|
|
646
546
|
const agent = accent(truncate(entry.agentName, 15));
|
|
647
547
|
const branch = truncate(entry.branchName, panelWidth - 30);
|
|
@@ -674,7 +574,7 @@ function renderMetricsPanel(
|
|
|
674
574
|
const separator = horizontalLine(width, BOX.tee, BOX.horizontal, BOX.teeRight);
|
|
675
575
|
output += `${CURSOR.cursorTo(startRow, 1)}${separator}\n`;
|
|
676
576
|
|
|
677
|
-
const headerLine = `${BOX.vertical} ${
|
|
577
|
+
const headerLine = `${BOX.vertical} ${brand.bold("Metrics")}`;
|
|
678
578
|
const headerPadding = " ".repeat(Math.max(0, width - visibleLength(headerLine) - 1));
|
|
679
579
|
output += `${CURSOR.cursorTo(startRow + 1, 1)}${headerLine}${headerPadding}${BOX.vertical}\n`;
|
|
680
580
|
|
|
@@ -127,7 +127,7 @@ describe("doctorCommand", () => {
|
|
|
127
127
|
const out = output();
|
|
128
128
|
|
|
129
129
|
expect(out).toContain("Overstory Doctor");
|
|
130
|
-
expect(out).toContain("
|
|
130
|
+
expect(out).toContain("────────────────");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
test("shows summary line with zero counts", async () => {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { checkVersion } from "../doctor/version.ts";
|
|
|
21
21
|
import { ValidationError } from "../errors.ts";
|
|
22
22
|
import { jsonOutput } from "../json.ts";
|
|
23
23
|
import { color } from "../logging/color.ts";
|
|
24
|
+
import { renderHeader } from "../logging/theme.ts";
|
|
24
25
|
|
|
25
26
|
/** Registry of all check modules in execution order. */
|
|
26
27
|
const ALL_CHECKS: Array<{ category: DoctorCategory; fn: DoctorCheckFn }> = [
|
|
@@ -63,8 +64,7 @@ function printHumanReadable(
|
|
|
63
64
|
): void {
|
|
64
65
|
const w = process.stdout.write.bind(process.stdout);
|
|
65
66
|
|
|
66
|
-
w(`${
|
|
67
|
-
w("================\n\n");
|
|
67
|
+
w(`${renderHeader("Overstory Doctor")}\n\n`);
|
|
68
68
|
|
|
69
69
|
// Group checks by category
|
|
70
70
|
const byCategory = new Map<DoctorCategory, DoctorCheck[]>();
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { jsonError, jsonOutput } from "../json.ts";
|
|
10
10
|
import { accent, brand, color, muted } from "../logging/color.ts";
|
|
11
|
+
import { thickSeparator } from "../logging/theme.ts";
|
|
11
12
|
|
|
12
13
|
const TOOLS = [
|
|
13
14
|
{ name: "overstory", cli: "ov", npm: "@os-eco/overstory-cli" },
|
|
@@ -167,7 +168,7 @@ function formatDoctorLine(summary: DoctorSummary): string {
|
|
|
167
168
|
|
|
168
169
|
function printHumanOutput(results: ToolResult[]): void {
|
|
169
170
|
process.stdout.write(`${brand.bold("os-eco Ecosystem")}\n`);
|
|
170
|
-
process.stdout.write(`${
|
|
171
|
+
process.stdout.write(`${thickSeparator()}\n`);
|
|
171
172
|
process.stdout.write("\n");
|
|
172
173
|
|
|
173
174
|
for (const tool of results) {
|
|
@@ -226,7 +226,7 @@ describe("errorsCommand", () => {
|
|
|
226
226
|
const out = output();
|
|
227
227
|
|
|
228
228
|
expect(out).toContain("Errors");
|
|
229
|
-
expect(out).toContain("
|
|
229
|
+
expect(out).toContain("─".repeat(70));
|
|
230
230
|
});
|
|
231
231
|
|
|
232
232
|
test("shows error count", async () => {
|
|
@@ -344,8 +344,7 @@ describe("errorsCommand", () => {
|
|
|
344
344
|
await errorsCommand([]);
|
|
345
345
|
const out = output();
|
|
346
346
|
|
|
347
|
-
expect(out).toContain("reason
|
|
348
|
-
expect(out).toContain("code=500");
|
|
347
|
+
expect(out).toContain('data={"reason":"disk full","code":500}');
|
|
349
348
|
});
|
|
350
349
|
|
|
351
350
|
test("long data values are truncated", async () => {
|
|
@@ -364,8 +363,8 @@ describe("errorsCommand", () => {
|
|
|
364
363
|
|
|
365
364
|
// The full 200-char value should not appear
|
|
366
365
|
expect(out).not.toContain(longValue);
|
|
367
|
-
// But a truncated version with "
|
|
368
|
-
expect(out).toContain("
|
|
366
|
+
// But a truncated version with "…" should
|
|
367
|
+
expect(out).toContain("…");
|
|
369
368
|
});
|
|
370
369
|
|
|
371
370
|
test("non-JSON data is shown raw if short", async () => {
|
package/src/commands/errors.ts
CHANGED
|
@@ -13,67 +13,10 @@ import { ValidationError } from "../errors.ts";
|
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
14
|
import { jsonOutput } from "../json.ts";
|
|
15
15
|
import { accent, color } from "../logging/color.ts";
|
|
16
|
+
import { buildEventDetail, formatAbsoluteTime, formatDate } from "../logging/format.ts";
|
|
17
|
+
import { separator } from "../logging/theme.ts";
|
|
16
18
|
import type { StoredEvent } from "../types.ts";
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* Format an absolute time from an ISO timestamp.
|
|
20
|
-
* Returns "HH:MM:SS" portion.
|
|
21
|
-
*/
|
|
22
|
-
function formatAbsoluteTime(timestamp: string): string {
|
|
23
|
-
const match = /T(\d{2}:\d{2}:\d{2})/.exec(timestamp);
|
|
24
|
-
if (match?.[1]) {
|
|
25
|
-
return match[1];
|
|
26
|
-
}
|
|
27
|
-
return timestamp;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Format the date portion of an ISO timestamp.
|
|
32
|
-
* Returns "YYYY-MM-DD".
|
|
33
|
-
*/
|
|
34
|
-
function formatDate(timestamp: string): string {
|
|
35
|
-
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
36
|
-
if (match?.[1]) {
|
|
37
|
-
return match[1];
|
|
38
|
-
}
|
|
39
|
-
return "";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Build a detail string for an error event based on its fields.
|
|
44
|
-
*/
|
|
45
|
-
function buildErrorDetail(event: StoredEvent): string {
|
|
46
|
-
const parts: string[] = [];
|
|
47
|
-
|
|
48
|
-
if (event.toolName) {
|
|
49
|
-
parts.push(`tool=${event.toolName}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (event.data) {
|
|
53
|
-
try {
|
|
54
|
-
const parsed: unknown = JSON.parse(event.data);
|
|
55
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
56
|
-
const data = parsed as Record<string, unknown>;
|
|
57
|
-
for (const [key, value] of Object.entries(data)) {
|
|
58
|
-
if (value !== null && value !== undefined) {
|
|
59
|
-
const strValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
60
|
-
// Truncate long values
|
|
61
|
-
const truncated = strValue.length > 80 ? `${strValue.slice(0, 77)}...` : strValue;
|
|
62
|
-
parts.push(`${key}=${truncated}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// data is not valid JSON; show it raw if short enough
|
|
68
|
-
if (event.data.length <= 80) {
|
|
69
|
-
parts.push(event.data);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return parts.join(" ");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
20
|
/**
|
|
78
21
|
* Group errors by agent name, preserving insertion order.
|
|
79
22
|
*/
|
|
@@ -96,8 +39,7 @@ function groupByAgent(events: StoredEvent[]): Map<string, StoredEvent[]> {
|
|
|
96
39
|
function printErrors(events: StoredEvent[]): void {
|
|
97
40
|
const w = process.stdout.write.bind(process.stdout);
|
|
98
41
|
|
|
99
|
-
w(`${color.bold(color.red("Errors"))}\n`);
|
|
100
|
-
w(`${"=".repeat(70)}\n`);
|
|
42
|
+
w(`${color.bold(color.red("Errors"))}\n${separator()}\n`);
|
|
101
43
|
|
|
102
44
|
if (events.length === 0) {
|
|
103
45
|
w(`${color.dim("No errors found.")}\n`);
|
|
@@ -124,7 +66,7 @@ function printErrors(events: StoredEvent[]): void {
|
|
|
124
66
|
const time = formatAbsoluteTime(event.createdAt);
|
|
125
67
|
const timestamp = date ? `${date} ${time}` : time;
|
|
126
68
|
|
|
127
|
-
const detail =
|
|
69
|
+
const detail = buildEventDetail(event);
|
|
128
70
|
const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
|
|
129
71
|
|
|
130
72
|
w(` ${color.dim(timestamp)} ${color.red(color.bold("ERROR"))}${detailSuffix}\n`);
|
|
@@ -561,8 +561,8 @@ describe("feedCommand", () => {
|
|
|
561
561
|
|
|
562
562
|
// The full 200-char value should not appear
|
|
563
563
|
expect(out).not.toContain(longValue);
|
|
564
|
-
// But a truncated version with "
|
|
565
|
-
expect(out).toContain("
|
|
564
|
+
// But a truncated version with "…" should
|
|
565
|
+
expect(out).toContain("…");
|
|
566
566
|
});
|
|
567
567
|
|
|
568
568
|
test("agent color assignment is stable", async () => {
|