@os-eco/overstory-cli 0.6.1 → 0.6.5

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 (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
@@ -7,27 +7,13 @@
7
7
  */
8
8
 
9
9
  import { join } from "node:path";
10
+ import { Command } from "commander";
10
11
  import { loadConfig } from "../config.ts";
11
12
  import { ValidationError } from "../errors.ts";
12
13
  import { createEventStore } from "../events/store.ts";
13
14
  import { color } from "../logging/color.ts";
14
15
  import type { StoredEvent } from "../types.ts";
15
16
 
16
- /**
17
- * Parse a named flag value from args.
18
- */
19
- function getFlag(args: string[], flag: string): string | undefined {
20
- const idx = args.indexOf(flag);
21
- if (idx === -1 || idx + 1 >= args.length) {
22
- return undefined;
23
- }
24
- return args[idx + 1];
25
- }
26
-
27
- function hasFlag(args: string[], flag: string): boolean {
28
- return args.includes(flag);
29
- }
30
-
31
17
  /**
32
18
  * Format an absolute time from an ISO timestamp.
33
19
  * Returns "HH:MM:SS" portion.
@@ -109,15 +95,15 @@ function groupByAgent(events: StoredEvent[]): Map<string, StoredEvent[]> {
109
95
  function printErrors(events: StoredEvent[]): void {
110
96
  const w = process.stdout.write.bind(process.stdout);
111
97
 
112
- w(`${color.bold}${color.red}Errors${color.reset}\n`);
98
+ w(`${color.bold(color.red("Errors"))}\n`);
113
99
  w(`${"=".repeat(70)}\n`);
114
100
 
115
101
  if (events.length === 0) {
116
- w(`${color.dim}No errors found.${color.reset}\n`);
102
+ w(`${color.dim("No errors found.")}\n`);
117
103
  return;
118
104
  }
119
105
 
120
- w(`${color.dim}${events.length} error${events.length === 1 ? "" : "s"}${color.reset}\n\n`);
106
+ w(`${color.dim(`${events.length} error${events.length === 1 ? "" : "s"}`)}\n\n`);
121
107
 
122
108
  const grouped = groupByAgent(events);
123
109
 
@@ -129,7 +115,7 @@ function printErrors(events: StoredEvent[]): void {
129
115
  firstGroup = false;
130
116
 
131
117
  w(
132
- `${color.bold}${agentName}${color.reset} ${color.dim}(${agentEvents.length} error${agentEvents.length === 1 ? "" : "s"})${color.reset}\n`,
118
+ `${color.bold(agentName)} ${color.dim(`(${agentEvents.length} error${agentEvents.length === 1 ? "" : "s"})`)}\n`,
133
119
  );
134
120
 
135
121
  for (const event of agentEvents) {
@@ -138,43 +124,29 @@ function printErrors(events: StoredEvent[]): void {
138
124
  const timestamp = date ? `${date} ${time}` : time;
139
125
 
140
126
  const detail = buildErrorDetail(event);
141
- const detailSuffix = detail ? ` ${color.dim}${detail}${color.reset}` : "";
127
+ const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
142
128
 
143
- w(
144
- ` ${color.dim}${timestamp}${color.reset} ${color.red}${color.bold}ERROR${color.reset}${detailSuffix}\n`,
145
- );
129
+ w(` ${color.dim(timestamp)} ${color.red(color.bold("ERROR"))}${detailSuffix}\n`);
146
130
  }
147
131
  }
148
132
  }
149
133
 
150
- const ERRORS_HELP = `overstory errors -- Aggregated error view across agents
151
-
152
- Usage: overstory errors [options]
153
-
154
- Options:
155
- --agent <name> Filter errors by agent name
156
- --run <id> Filter errors by run ID
157
- --since <timestamp> Start time filter (ISO 8601)
158
- --until <timestamp> End time filter (ISO 8601)
159
- --limit <n> Max errors to show (default: 100)
160
- --json Output as JSON array of StoredEvent objects
161
- --help, -h Show this help`;
162
-
163
- /**
164
- * Entry point for `overstory errors [--agent <name>] [--run <id>] [--json] [--since] [--until] [--limit]`.
165
- */
166
- export async function errorsCommand(args: string[]): Promise<void> {
167
- if (args.includes("--help") || args.includes("-h")) {
168
- process.stdout.write(`${ERRORS_HELP}\n`);
169
- return;
170
- }
134
+ interface ErrorsOpts {
135
+ agent?: string;
136
+ run?: string;
137
+ since?: string;
138
+ until?: string;
139
+ limit?: string;
140
+ json?: boolean;
141
+ }
171
142
 
172
- const json = hasFlag(args, "--json");
173
- const agentName = getFlag(args, "--agent");
174
- const runId = getFlag(args, "--run");
175
- const sinceStr = getFlag(args, "--since");
176
- const untilStr = getFlag(args, "--until");
177
- const limitStr = getFlag(args, "--limit");
143
+ async function executeErrors(opts: ErrorsOpts): Promise<void> {
144
+ const json = opts.json ?? false;
145
+ const agentName = opts.agent;
146
+ const runId = opts.run;
147
+ const sinceStr = opts.since;
148
+ const untilStr = opts.until;
149
+ const limitStr = opts.limit;
178
150
  const limit = limitStr ? Number.parseInt(limitStr, 10) : 100;
179
151
 
180
152
  if (Number.isNaN(limit) || limit < 1) {
@@ -246,3 +218,37 @@ export async function errorsCommand(args: string[]): Promise<void> {
246
218
  eventStore.close();
247
219
  }
248
220
  }
221
+
222
+ export function createErrorsCommand(): Command {
223
+ return new Command("errors")
224
+ .description("Aggregated error view across agents")
225
+ .option("--agent <name>", "Filter errors by agent name")
226
+ .option("--run <id>", "Filter errors by run ID")
227
+ .option("--since <timestamp>", "Start time filter (ISO 8601)")
228
+ .option("--until <timestamp>", "End time filter (ISO 8601)")
229
+ .option("--limit <n>", "Max errors to show (default: 100)")
230
+ .option("--json", "Output as JSON array of StoredEvent objects")
231
+ .action(async (opts: ErrorsOpts) => {
232
+ await executeErrors(opts);
233
+ });
234
+ }
235
+
236
+ export async function errorsCommand(args: string[]): Promise<void> {
237
+ const cmd = createErrorsCommand();
238
+ cmd.exitOverride();
239
+ try {
240
+ await cmd.parseAsync(args, { from: "user" });
241
+ } catch (err: unknown) {
242
+ if (err && typeof err === "object" && "code" in err) {
243
+ const code = (err as { code: string }).code;
244
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
245
+ return;
246
+ }
247
+ if (code.startsWith("commander.")) {
248
+ const message = err instanceof Error ? err.message : String(err);
249
+ throw new ValidationError(message, { field: "args" });
250
+ }
251
+ }
252
+ throw err;
253
+ }
254
+ }
@@ -78,7 +78,7 @@ describe("feedCommand", () => {
78
78
  await feedCommand(["--help"]);
79
79
  const out = output();
80
80
 
81
- expect(out).toContain("overstory feed");
81
+ expect(out).toContain("feed");
82
82
  expect(out).toContain("--follow");
83
83
  expect(out).toContain("--agent");
84
84
  expect(out).toContain("--run");
@@ -92,7 +92,7 @@ describe("feedCommand", () => {
92
92
  await feedCommand(["-h"]);
93
93
  const out = output();
94
94
 
95
- expect(out).toContain("overstory feed");
95
+ expect(out).toContain("feed");
96
96
  });
97
97
  });
98
98
 
@@ -7,14 +7,16 @@
7
7
  */
8
8
 
9
9
  import { join } from "node:path";
10
+ import { Command } from "commander";
10
11
  import { loadConfig } from "../config.ts";
11
12
  import { ValidationError } from "../errors.ts";
12
13
  import { createEventStore } from "../events/store.ts";
14
+ import type { ColorFn } from "../logging/color.ts";
13
15
  import { color } from "../logging/color.ts";
14
16
  import type { EventType, StoredEvent } from "../types.ts";
15
17
 
16
18
  /** Compact 5-char labels for feed output. */
17
- const EVENT_LABELS: Record<EventType, { label: string; color: string }> = {
19
+ const EVENT_LABELS: Record<EventType, { label: string; color: ColorFn }> = {
18
20
  tool_start: { label: "TOOL+", color: color.blue },
19
21
  tool_end: { label: "TOOL-", color: color.blue },
20
22
  session_start: { label: "SESS+", color: color.green },
@@ -26,41 +28,14 @@ const EVENT_LABELS: Record<EventType, { label: string; color: string }> = {
26
28
  custom: { label: "CUSTM", color: color.gray },
27
29
  };
28
30
 
29
- /** Colors assigned to agents in order of first appearance. */
30
- const AGENT_COLORS = [color.blue, color.green, color.yellow, color.cyan, color.magenta] as const;
31
-
32
- /**
33
- * Parse a named flag value from args.
34
- */
35
- function getFlag(args: string[], flag: string): string | undefined {
36
- const idx = args.indexOf(flag);
37
- if (idx === -1 || idx + 1 >= args.length) {
38
- return undefined;
39
- }
40
- return args[idx + 1];
41
- }
42
-
43
- /**
44
- * Parse all occurrences of a named flag from args.
45
- * Returns an array of values (e.g., --agent a --agent b => ["a", "b"]).
46
- */
47
- function getAllFlags(args: string[], flag: string): string[] {
48
- const values: string[] = [];
49
- for (let i = 0; i < args.length; i++) {
50
- if (args[i] === flag && i + 1 < args.length) {
51
- const value = args[i + 1];
52
- if (value !== undefined) {
53
- values.push(value);
54
- }
55
- i++; // skip the value
56
- }
57
- }
58
- return values;
59
- }
60
-
61
- function hasFlag(args: string[], flag: string): boolean {
62
- return args.includes(flag);
63
- }
31
+ /** Color functions assigned to agents in order of first appearance. */
32
+ const AGENT_COLORS: readonly ColorFn[] = [
33
+ color.blue,
34
+ color.green,
35
+ color.yellow,
36
+ color.cyan,
37
+ color.magenta,
38
+ ];
64
39
 
65
40
  /**
66
41
  * Format an absolute time from an ISO timestamp.
@@ -114,16 +89,16 @@ function buildEventDetail(event: StoredEvent): string {
114
89
  }
115
90
 
116
91
  /**
117
- * Assign a stable color to each agent based on order of first appearance.
92
+ * Assign a stable color function to each agent based on order of first appearance.
118
93
  */
119
- function buildAgentColorMap(events: StoredEvent[]): Map<string, string> {
120
- const colorMap = new Map<string, string>();
94
+ function buildAgentColorMap(events: StoredEvent[]): Map<string, ColorFn> {
95
+ const colorMap = new Map<string, ColorFn>();
121
96
  for (const event of events) {
122
97
  if (!colorMap.has(event.agentName)) {
123
98
  const colorIndex = colorMap.size % AGENT_COLORS.length;
124
- const agentColor = AGENT_COLORS[colorIndex];
125
- if (agentColor !== undefined) {
126
- colorMap.set(event.agentName, agentColor);
99
+ const agentColorFn = AGENT_COLORS[colorIndex];
100
+ if (agentColorFn !== undefined) {
101
+ colorMap.set(event.agentName, agentColorFn);
127
102
  }
128
103
  }
129
104
  }
@@ -134,7 +109,7 @@ function buildAgentColorMap(events: StoredEvent[]): Map<string, string> {
134
109
  * Print a single event in compact feed format:
135
110
  * HH:MM:SS LABEL agentname detail
136
111
  */
137
- function printEvent(event: StoredEvent, colorMap: Map<string, string>): void {
112
+ function printEvent(event: StoredEvent, colorMap: Map<string, ColorFn>): void {
138
113
  const w = process.stdout.write.bind(process.stdout);
139
114
 
140
115
  const timeStr = formatAbsoluteTime(event.createdAt);
@@ -144,55 +119,43 @@ function printEvent(event: StoredEvent, colorMap: Map<string, string>): void {
144
119
  color: color.gray,
145
120
  };
146
121
 
147
- const levelColor =
148
- event.level === "error" ? color.red : event.level === "warn" ? color.yellow : "";
149
- const levelReset = levelColor ? color.reset : "";
122
+ const levelColorFn =
123
+ event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
124
+ const applyLevel = (text: string) => (levelColorFn ? levelColorFn(text) : text);
150
125
 
151
126
  const detail = buildEventDetail(event);
152
- const detailSuffix = detail ? ` ${color.dim}${detail}${color.reset}` : "";
127
+ const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
153
128
 
154
- const agentColor = colorMap.get(event.agentName) ?? color.gray;
155
- const agentLabel = ` ${agentColor}${event.agentName.padEnd(15)}${color.reset}`;
129
+ const agentColorFn = colorMap.get(event.agentName) ?? color.gray;
130
+ const agentLabel = ` ${agentColorFn(event.agentName.padEnd(15))}`;
156
131
 
157
132
  w(
158
- `${color.dim}${timeStr}${color.reset} ` +
159
- `${levelColor}${eventInfo.color}${color.bold}${eventInfo.label}${color.reset}${levelReset}` +
133
+ `${color.dim(timeStr)} ` +
134
+ `${applyLevel(eventInfo.color(color.bold(eventInfo.label)))}` +
160
135
  `${agentLabel}${detailSuffix}\n`,
161
136
  );
162
137
  }
163
138
 
164
- const FEED_HELP = `overstory feed -- Unified real-time event stream across all agents
165
-
166
- Usage: overstory feed [options]
167
-
168
- Options:
169
- --follow, -f Continuously poll for new events (like tail -f)
170
- --interval <ms> Polling interval for --follow (default: 1000, min: 200)
171
- --agent <name> Filter by agent name (can appear multiple times)
172
- --run <id> Filter events by run ID
173
- --since <timestamp> Start time (ISO 8601, default: 5 minutes ago)
174
- --limit <n> Max initial events to show (default: 50)
175
- --json Output events as JSON (one per line in follow mode)
176
- --help, -h Show this help`;
177
-
178
- /**
179
- * Entry point for `overstory feed [--follow] [--agent <name>...] [--run <id>] [--json]`.
180
- */
181
- export async function feedCommand(args: string[]): Promise<void> {
182
- if (args.includes("--help") || args.includes("-h")) {
183
- process.stdout.write(`${FEED_HELP}\n`);
184
- return;
185
- }
139
+ interface FeedOpts {
140
+ follow?: boolean;
141
+ interval?: string;
142
+ agent: string[]; // repeatable
143
+ run?: string;
144
+ since?: string;
145
+ limit?: string;
146
+ json?: boolean;
147
+ }
186
148
 
187
- const json = hasFlag(args, "--json");
188
- const follow = hasFlag(args, "--follow") || hasFlag(args, "-f");
189
- const runId = getFlag(args, "--run");
190
- const agentNames = getAllFlags(args, "--agent");
191
- const sinceStr = getFlag(args, "--since");
192
- const limitStr = getFlag(args, "--limit");
149
+ async function executeFeed(opts: FeedOpts): Promise<void> {
150
+ const json = opts.json ?? false;
151
+ const follow = opts.follow ?? false;
152
+ const runId = opts.run;
153
+ const agentNames = opts.agent;
154
+ const sinceStr = opts.since;
155
+ const limitStr = opts.limit;
193
156
  const limit = limitStr ? Number.parseInt(limitStr, 10) : 50;
194
157
 
195
- const intervalStr = getFlag(args, "--interval");
158
+ const intervalStr = opts.interval;
196
159
  const interval = intervalStr ? Number.parseInt(intervalStr, 10) : 1000;
197
160
 
198
161
  if (Number.isNaN(limit) || limit < 1) {
@@ -330,9 +293,9 @@ export async function feedCommand(args: string[]): Promise<void> {
330
293
  for (const event of newEvents) {
331
294
  if (!globalColorMap.has(event.agentName)) {
332
295
  const colorIndex = globalColorMap.size % AGENT_COLORS.length;
333
- const agentColor = AGENT_COLORS[colorIndex];
334
- if (agentColor !== undefined) {
335
- globalColorMap.set(event.agentName, agentColor);
296
+ const agentColorFn = AGENT_COLORS[colorIndex];
297
+ if (agentColorFn !== undefined) {
298
+ globalColorMap.set(event.agentName, agentColorFn);
336
299
  }
337
300
  }
338
301
  }
@@ -359,3 +322,43 @@ export async function feedCommand(args: string[]): Promise<void> {
359
322
  eventStore.close();
360
323
  }
361
324
  }
325
+
326
+ export function createFeedCommand(): Command {
327
+ return new Command("feed")
328
+ .description("Unified real-time event stream across all agents")
329
+ .option("-f, --follow", "Continuously poll for new events (like tail -f)")
330
+ .option("--interval <ms>", "Polling interval for --follow (default: 1000, min: 200)")
331
+ .option(
332
+ "--agent <name>",
333
+ "Filter by agent name (can appear multiple times)",
334
+ (val: string, prev: string[]) => [...prev, val],
335
+ [] as string[],
336
+ )
337
+ .option("--run <id>", "Filter events by run ID")
338
+ .option("--since <timestamp>", "Start time (ISO 8601, default: 5 minutes ago)")
339
+ .option("--limit <n>", "Max initial events to show (default: 50)")
340
+ .option("--json", "Output events as JSON (one per line in follow mode)")
341
+ .action(async (opts: FeedOpts) => {
342
+ await executeFeed(opts);
343
+ });
344
+ }
345
+
346
+ export async function feedCommand(args: string[]): Promise<void> {
347
+ const cmd = createFeedCommand();
348
+ cmd.exitOverride();
349
+ try {
350
+ await cmd.parseAsync(args, { from: "user" });
351
+ } catch (err: unknown) {
352
+ if (err && typeof err === "object" && "code" in err) {
353
+ const code = (err as { code: string }).code;
354
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
355
+ return;
356
+ }
357
+ if (code.startsWith("commander.")) {
358
+ const message = err instanceof Error ? err.message : String(err);
359
+ throw new ValidationError(message, { field: "args" });
360
+ }
361
+ }
362
+ throw err;
363
+ }
364
+ }