@os-eco/overstory-cli 0.6.1
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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: overstory status [--json] [--watch]
|
|
3
|
+
*
|
|
4
|
+
* Shows active agents, worktree status, beads summary, mail queue depth,
|
|
5
|
+
* and merge queue state. --watch mode uses polling for live updates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { loadConfig } from "../config.ts";
|
|
10
|
+
import { ValidationError } from "../errors.ts";
|
|
11
|
+
import { createMailStore } from "../mail/store.ts";
|
|
12
|
+
import { createMergeQueue } from "../merge/queue.ts";
|
|
13
|
+
import { createMetricsStore } from "../metrics/store.ts";
|
|
14
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
15
|
+
import type { AgentSession } from "../types.ts";
|
|
16
|
+
import { listWorktrees } from "../worktree/manager.ts";
|
|
17
|
+
import { listSessions } from "../worktree/tmux.ts";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Subprocess result cache (TTL-based, module-level)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface CacheEntry<T> {
|
|
24
|
+
data: T;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let worktreeCache: CacheEntry<Array<{ path: string; branch: string; head: string }>> | null = null;
|
|
29
|
+
let tmuxCache: CacheEntry<Array<{ name: string; pid: number }>> | null = null;
|
|
30
|
+
|
|
31
|
+
const DEFAULT_CACHE_TTL_MS = 10_000; // 10 seconds
|
|
32
|
+
|
|
33
|
+
export function invalidateStatusCache(): void {
|
|
34
|
+
worktreeCache = null;
|
|
35
|
+
tmuxCache = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getCachedWorktrees(
|
|
39
|
+
root: string,
|
|
40
|
+
ttlMs: number = DEFAULT_CACHE_TTL_MS,
|
|
41
|
+
): Promise<Array<{ path: string; branch: string; head: string }>> {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
if (worktreeCache && now - worktreeCache.timestamp < ttlMs) {
|
|
44
|
+
return worktreeCache.data;
|
|
45
|
+
}
|
|
46
|
+
const data = await listWorktrees(root);
|
|
47
|
+
worktreeCache = { data, timestamp: now };
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getCachedTmuxSessions(
|
|
52
|
+
ttlMs: number = DEFAULT_CACHE_TTL_MS,
|
|
53
|
+
): Promise<Array<{ name: string; pid: number }>> {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (tmuxCache && now - tmuxCache.timestamp < ttlMs) {
|
|
56
|
+
return tmuxCache.data;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const data = await listSessions();
|
|
60
|
+
tmuxCache = { data, timestamp: now };
|
|
61
|
+
return data;
|
|
62
|
+
} catch {
|
|
63
|
+
return tmuxCache?.data ?? [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a named flag value from args.
|
|
69
|
+
*/
|
|
70
|
+
function getFlag(args: string[], flag: string): string | undefined {
|
|
71
|
+
const idx = args.indexOf(flag);
|
|
72
|
+
if (idx === -1 || idx + 1 >= args.length) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return args[idx + 1];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasFlag(args: string[], flag: string): boolean {
|
|
79
|
+
return args.includes(flag);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Format a duration in ms to a human-readable string.
|
|
84
|
+
*/
|
|
85
|
+
function formatDuration(ms: number): string {
|
|
86
|
+
const seconds = Math.floor(ms / 1000);
|
|
87
|
+
if (seconds < 60) return `${seconds}s`;
|
|
88
|
+
const minutes = Math.floor(seconds / 60);
|
|
89
|
+
const remainSec = seconds % 60;
|
|
90
|
+
if (minutes < 60) return `${minutes}m ${remainSec}s`;
|
|
91
|
+
const hours = Math.floor(minutes / 60);
|
|
92
|
+
const remainMin = minutes % 60;
|
|
93
|
+
return `${hours}h ${remainMin}m`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface VerboseAgentDetail {
|
|
97
|
+
worktreePath: string;
|
|
98
|
+
logsDir: string;
|
|
99
|
+
lastMailSent: string | null;
|
|
100
|
+
lastMailReceived: string | null;
|
|
101
|
+
capability: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface StatusData {
|
|
105
|
+
currentRunId?: string | null;
|
|
106
|
+
agents: AgentSession[];
|
|
107
|
+
worktrees: Array<{ path: string; branch: string; head: string }>;
|
|
108
|
+
tmuxSessions: Array<{ name: string; pid: number }>;
|
|
109
|
+
unreadMailCount: number;
|
|
110
|
+
mergeQueueCount: number;
|
|
111
|
+
recentMetricsCount: number;
|
|
112
|
+
verboseDetails?: Record<string, VerboseAgentDetail>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readCurrentRunId(overstoryDir: string): Promise<string | null> {
|
|
116
|
+
const path = join(overstoryDir, "current-run.txt");
|
|
117
|
+
const file = Bun.file(path);
|
|
118
|
+
if (!(await file.exists())) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const text = await file.text();
|
|
122
|
+
const trimmed = text.trim();
|
|
123
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gather all status data.
|
|
128
|
+
* @param agentName - Which agent's perspective for unread mail count (default "orchestrator")
|
|
129
|
+
* @param verbose - When true, collect extra per-agent detail (worktree path, logs dir, last mail)
|
|
130
|
+
* @param runId - When provided, only sessions for that run are returned; null/undefined shows all
|
|
131
|
+
*/
|
|
132
|
+
export async function gatherStatus(
|
|
133
|
+
root: string,
|
|
134
|
+
agentName = "orchestrator",
|
|
135
|
+
verbose = false,
|
|
136
|
+
runId?: string | null,
|
|
137
|
+
): Promise<StatusData> {
|
|
138
|
+
const overstoryDir = join(root, ".overstory");
|
|
139
|
+
const { store } = openSessionStore(overstoryDir);
|
|
140
|
+
|
|
141
|
+
let sessions: AgentSession[];
|
|
142
|
+
try {
|
|
143
|
+
// When run-scoped, also include sessions with null runId (e.g. coordinator)
|
|
144
|
+
// because SQL WHERE run_id = $run_id never matches NULL rows.
|
|
145
|
+
sessions = runId
|
|
146
|
+
? [...store.getByRun(runId), ...store.getAll().filter((s) => s.runId === null)]
|
|
147
|
+
: store.getAll();
|
|
148
|
+
|
|
149
|
+
const worktrees = await getCachedWorktrees(root);
|
|
150
|
+
|
|
151
|
+
const tmuxSessions = await getCachedTmuxSessions();
|
|
152
|
+
|
|
153
|
+
// Reconcile agent states: if tmux session is dead but agent state
|
|
154
|
+
// indicates it should be alive, mark it as zombie
|
|
155
|
+
const tmuxSessionNames = new Set(tmuxSessions.map((s) => s.name));
|
|
156
|
+
for (const session of sessions) {
|
|
157
|
+
if (
|
|
158
|
+
session.state === "booting" ||
|
|
159
|
+
session.state === "working" ||
|
|
160
|
+
session.state === "stalled"
|
|
161
|
+
) {
|
|
162
|
+
const tmuxAlive = tmuxSessionNames.has(session.tmuxSession);
|
|
163
|
+
if (!tmuxAlive) {
|
|
164
|
+
try {
|
|
165
|
+
store.updateState(session.agentName, "zombie");
|
|
166
|
+
session.state = "zombie";
|
|
167
|
+
} catch {
|
|
168
|
+
// Best effort: don't fail status display if update fails
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let unreadMailCount = 0;
|
|
175
|
+
let mailStore: ReturnType<typeof createMailStore> | null = null;
|
|
176
|
+
try {
|
|
177
|
+
const mailDbPath = join(root, ".overstory", "mail.db");
|
|
178
|
+
const mailFile = Bun.file(mailDbPath);
|
|
179
|
+
if (await mailFile.exists()) {
|
|
180
|
+
mailStore = createMailStore(mailDbPath);
|
|
181
|
+
const unread = mailStore.getAll({ to: agentName, unread: true });
|
|
182
|
+
unreadMailCount = unread.length;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// mail db might not exist
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let mergeQueueCount = 0;
|
|
189
|
+
try {
|
|
190
|
+
const queuePath = join(root, ".overstory", "merge-queue.db");
|
|
191
|
+
const queue = createMergeQueue(queuePath);
|
|
192
|
+
mergeQueueCount = queue.list("pending").length;
|
|
193
|
+
queue.close();
|
|
194
|
+
} catch {
|
|
195
|
+
// queue might not exist
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let recentMetricsCount = 0;
|
|
199
|
+
try {
|
|
200
|
+
const metricsDbPath = join(root, ".overstory", "metrics.db");
|
|
201
|
+
const metricsFile = Bun.file(metricsDbPath);
|
|
202
|
+
if (await metricsFile.exists()) {
|
|
203
|
+
const metricsStore = createMetricsStore(metricsDbPath);
|
|
204
|
+
recentMetricsCount = metricsStore.getRecentSessions(100).length;
|
|
205
|
+
metricsStore.close();
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// metrics db might not exist
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let verboseDetails: Record<string, VerboseAgentDetail> | undefined;
|
|
212
|
+
if (verbose && sessions.length > 0) {
|
|
213
|
+
verboseDetails = {};
|
|
214
|
+
for (const session of sessions) {
|
|
215
|
+
const logsDir = join(root, ".overstory", "logs", session.agentName);
|
|
216
|
+
|
|
217
|
+
let lastMailSent: string | null = null;
|
|
218
|
+
let lastMailReceived: string | null = null;
|
|
219
|
+
if (mailStore) {
|
|
220
|
+
try {
|
|
221
|
+
const sent = mailStore.getAll({ from: session.agentName });
|
|
222
|
+
if (sent.length > 0 && sent[0]) {
|
|
223
|
+
lastMailSent = sent[0].createdAt;
|
|
224
|
+
}
|
|
225
|
+
const received = mailStore.getAll({ to: session.agentName });
|
|
226
|
+
if (received.length > 0 && received[0]) {
|
|
227
|
+
lastMailReceived = received[0].createdAt;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// Best effort
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
verboseDetails[session.agentName] = {
|
|
235
|
+
worktreePath: session.worktreePath,
|
|
236
|
+
logsDir,
|
|
237
|
+
lastMailSent,
|
|
238
|
+
lastMailReceived,
|
|
239
|
+
capability: session.capability,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (mailStore) {
|
|
245
|
+
mailStore.close();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
currentRunId: runId,
|
|
250
|
+
agents: sessions,
|
|
251
|
+
worktrees,
|
|
252
|
+
tmuxSessions,
|
|
253
|
+
unreadMailCount,
|
|
254
|
+
mergeQueueCount,
|
|
255
|
+
recentMetricsCount,
|
|
256
|
+
verboseDetails,
|
|
257
|
+
};
|
|
258
|
+
} finally {
|
|
259
|
+
store.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Print status in human-readable format.
|
|
265
|
+
*/
|
|
266
|
+
export function printStatus(data: StatusData): void {
|
|
267
|
+
const now = Date.now();
|
|
268
|
+
const w = process.stdout.write.bind(process.stdout);
|
|
269
|
+
|
|
270
|
+
w("📊 Overstory Status\n");
|
|
271
|
+
w(`${"═".repeat(60)}\n\n`);
|
|
272
|
+
if (data.currentRunId) {
|
|
273
|
+
w(`🏃 Run: ${data.currentRunId}\n`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Active agents
|
|
277
|
+
const active = data.agents.filter((a) => a.state !== "zombie" && a.state !== "completed");
|
|
278
|
+
w(`🤖 Agents: ${active.length} active\n`);
|
|
279
|
+
if (active.length > 0) {
|
|
280
|
+
const tmuxSessionNames = new Set(data.tmuxSessions.map((s) => s.name));
|
|
281
|
+
for (const agent of active) {
|
|
282
|
+
const endTime =
|
|
283
|
+
agent.state === "completed" || agent.state === "zombie"
|
|
284
|
+
? new Date(agent.lastActivity).getTime()
|
|
285
|
+
: now;
|
|
286
|
+
const duration = formatDuration(endTime - new Date(agent.startedAt).getTime());
|
|
287
|
+
const tmuxAlive = tmuxSessionNames.has(agent.tmuxSession);
|
|
288
|
+
const aliveMarker = tmuxAlive ? "●" : "○";
|
|
289
|
+
w(` ${aliveMarker} ${agent.agentName} [${agent.capability}] `);
|
|
290
|
+
w(`${agent.state} | ${agent.beadId} | ${duration}\n`);
|
|
291
|
+
|
|
292
|
+
const detail = data.verboseDetails?.[agent.agentName];
|
|
293
|
+
if (detail) {
|
|
294
|
+
w(` Worktree: ${detail.worktreePath}\n`);
|
|
295
|
+
w(` Logs: ${detail.logsDir}\n`);
|
|
296
|
+
w(` Mail sent: ${detail.lastMailSent ?? "none"}`);
|
|
297
|
+
w(` | received: ${detail.lastMailReceived ?? "none"}\n`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
w(" No active agents\n");
|
|
302
|
+
}
|
|
303
|
+
w("\n");
|
|
304
|
+
|
|
305
|
+
// Worktrees
|
|
306
|
+
const overstoryWts = data.worktrees.filter((wt) => wt.branch.startsWith("overstory/"));
|
|
307
|
+
w(`🌳 Worktrees: ${overstoryWts.length}\n`);
|
|
308
|
+
for (const wt of overstoryWts) {
|
|
309
|
+
w(` ${wt.branch}\n`);
|
|
310
|
+
}
|
|
311
|
+
if (overstoryWts.length === 0) {
|
|
312
|
+
w(" No agent worktrees\n");
|
|
313
|
+
}
|
|
314
|
+
w("\n");
|
|
315
|
+
|
|
316
|
+
// Mail
|
|
317
|
+
w(`📬 Mail: ${data.unreadMailCount} unread\n`);
|
|
318
|
+
|
|
319
|
+
// Merge queue
|
|
320
|
+
w(`🔀 Merge queue: ${data.mergeQueueCount} pending\n`);
|
|
321
|
+
|
|
322
|
+
// Metrics
|
|
323
|
+
w(`📈 Sessions recorded: ${data.recentMetricsCount}\n`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Entry point for `overstory status [--json] [--watch]`.
|
|
328
|
+
*/
|
|
329
|
+
const STATUS_HELP = `overstory status — Show all active agents and project state
|
|
330
|
+
|
|
331
|
+
Usage: overstory status [--json] [--verbose] [--agent <name>] [--all]
|
|
332
|
+
|
|
333
|
+
Options:
|
|
334
|
+
--json Output as JSON
|
|
335
|
+
--verbose Show extra detail per agent (worktree, logs, mail timestamps)
|
|
336
|
+
--agent <name> Show unread mail for this agent (default: orchestrator)
|
|
337
|
+
--all Show sessions from all runs (default: current run only)
|
|
338
|
+
--watch (deprecated) Use 'overstory dashboard' for live monitoring
|
|
339
|
+
--interval <ms> Poll interval for --watch in milliseconds (default: 3000)
|
|
340
|
+
--help, -h Show this help`;
|
|
341
|
+
|
|
342
|
+
export async function statusCommand(args: string[]): Promise<void> {
|
|
343
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
344
|
+
process.stdout.write(`${STATUS_HELP}\n`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const json = hasFlag(args, "--json");
|
|
349
|
+
const watch = hasFlag(args, "--watch");
|
|
350
|
+
const verbose = hasFlag(args, "--verbose");
|
|
351
|
+
const all = hasFlag(args, "--all");
|
|
352
|
+
const intervalStr = getFlag(args, "--interval");
|
|
353
|
+
const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 3000;
|
|
354
|
+
|
|
355
|
+
if (Number.isNaN(interval) || interval < 500) {
|
|
356
|
+
throw new ValidationError("--interval must be a number >= 500 (milliseconds)", {
|
|
357
|
+
field: "interval",
|
|
358
|
+
value: intervalStr,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const agentName = getFlag(args, "--agent") ?? "orchestrator";
|
|
363
|
+
|
|
364
|
+
const cwd = process.cwd();
|
|
365
|
+
const config = await loadConfig(cwd);
|
|
366
|
+
const root = config.project.root;
|
|
367
|
+
|
|
368
|
+
let runId: string | null | undefined;
|
|
369
|
+
if (!all) {
|
|
370
|
+
const overstoryDir = join(root, ".overstory");
|
|
371
|
+
runId = await readCurrentRunId(overstoryDir);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (watch) {
|
|
375
|
+
process.stderr.write(
|
|
376
|
+
"⚠️ --watch is deprecated. Use 'overstory dashboard' for live monitoring.\n\n",
|
|
377
|
+
);
|
|
378
|
+
// Polling loop (kept for one release cycle)
|
|
379
|
+
while (true) {
|
|
380
|
+
// Clear screen
|
|
381
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
382
|
+
const data = await gatherStatus(root, agentName, verbose, runId);
|
|
383
|
+
if (json) {
|
|
384
|
+
process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
|
|
385
|
+
} else {
|
|
386
|
+
printStatus(data);
|
|
387
|
+
}
|
|
388
|
+
await Bun.sleep(interval);
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const data = await gatherStatus(root, agentName, verbose, runId);
|
|
392
|
+
if (json) {
|
|
393
|
+
process.stdout.write(`${JSON.stringify(data, null, "\t")}\n`);
|
|
394
|
+
} else {
|
|
395
|
+
printStatus(data);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|