@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
@@ -2,19 +2,21 @@
2
2
  * CLI command: overstory trace <target> [--json] [--since <ts>] [--until <ts>] [--limit <n>]
3
3
  *
4
4
  * Shows a chronological timeline of events for an agent or bead task.
5
- * Target can be an agent name or a bead ID (resolved to agent name via SessionStore).
5
+ * Target can be an agent name or a task ID (resolved to agent name via SessionStore).
6
6
  */
7
7
 
8
8
  import { join } from "node:path";
9
+ import { Command } from "commander";
9
10
  import { loadConfig } from "../config.ts";
10
11
  import { ValidationError } from "../errors.ts";
11
12
  import { createEventStore } from "../events/store.ts";
13
+ import type { ColorFn } from "../logging/color.ts";
12
14
  import { color } from "../logging/color.ts";
13
15
  import { openSessionStore } from "../sessions/compat.ts";
14
16
  import type { EventType, StoredEvent } from "../types.ts";
15
17
 
16
18
  /** Labels and colors for each event type. */
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 START", color: color.blue },
19
21
  tool_end: { label: "TOOL END ", color: color.blue },
20
22
  session_start: { label: "SESSION +", color: color.green },
@@ -27,22 +29,7 @@ const EVENT_LABELS: Record<EventType, { label: string; color: string }> = {
27
29
  };
28
30
 
29
31
  /**
30
- * Parse a named flag value from args.
31
- */
32
- function getFlag(args: string[], flag: string): string | undefined {
33
- const idx = args.indexOf(flag);
34
- if (idx === -1 || idx + 1 >= args.length) {
35
- return undefined;
36
- }
37
- return args[idx + 1];
38
- }
39
-
40
- function hasFlag(args: string[], flag: string): boolean {
41
- return args.includes(flag);
42
- }
43
-
44
- /**
45
- * Detect whether a target string looks like a bead ID.
32
+ * Detect whether a target string looks like a task ID.
46
33
  * Bead IDs follow the pattern: word-alphanumeric (e.g., "overstory-rj1k", "myproject-abc1").
47
34
  */
48
35
  function looksLikeBeadId(target: string): boolean {
@@ -142,15 +129,15 @@ function buildEventDetail(event: StoredEvent): string {
142
129
  function printTimeline(events: StoredEvent[], agentName: string, useAbsoluteTime: boolean): void {
143
130
  const w = process.stdout.write.bind(process.stdout);
144
131
 
145
- w(`${color.bold}Timeline for ${agentName}${color.reset}\n`);
132
+ w(`${color.bold(`Timeline for ${agentName}`)}\n`);
146
133
  w(`${"=".repeat(70)}\n`);
147
134
 
148
135
  if (events.length === 0) {
149
- w(`${color.dim}No events found.${color.reset}\n`);
136
+ w(`${color.dim("No events found.")}\n`);
150
137
  return;
151
138
  }
152
139
 
153
- w(`${color.dim}${events.length} event${events.length === 1 ? "" : "s"}${color.reset}\n\n`);
140
+ w(`${color.dim(`${events.length} event${events.length === 1 ? "" : "s"}`)}\n\n`);
154
141
 
155
142
  let lastDate = "";
156
143
 
@@ -161,7 +148,7 @@ function printTimeline(events: StoredEvent[], agentName: string, useAbsoluteTime
161
148
  if (lastDate !== "") {
162
149
  w("\n");
163
150
  }
164
- w(`${color.dim}--- ${date} ---${color.reset}\n`);
151
+ w(`${color.dim(`--- ${date} ---`)}\n`);
165
152
  lastDate = date;
166
153
  }
167
154
 
@@ -174,74 +161,35 @@ function printTimeline(events: StoredEvent[], agentName: string, useAbsoluteTime
174
161
  color: color.gray,
175
162
  };
176
163
 
177
- const levelColor =
178
- event.level === "error" ? color.red : event.level === "warn" ? color.yellow : "";
179
- const levelReset = levelColor ? color.reset : "";
164
+ const levelColorFn =
165
+ event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
166
+ const applyLevel = (text: string) => (levelColorFn ? levelColorFn(text) : text);
180
167
 
181
168
  const detail = buildEventDetail(event);
182
- const detailSuffix = detail ? ` ${color.dim}${detail}${color.reset}` : "";
169
+ const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
183
170
 
184
- const agentLabel =
185
- event.agentName !== agentName ? ` ${color.dim}[${event.agentName}]${color.reset}` : "";
171
+ const agentLabel = event.agentName !== agentName ? ` ${color.dim(`[${event.agentName}]`)}` : "";
186
172
 
187
173
  w(
188
- `${color.dim}${timeStr.padStart(10)}${color.reset} ` +
189
- `${levelColor}${eventInfo.color}${color.bold}${eventInfo.label}${color.reset}${levelReset}` +
174
+ `${color.dim(timeStr.padStart(10))} ` +
175
+ `${applyLevel(eventInfo.color(color.bold(eventInfo.label)))}` +
190
176
  `${agentLabel}${detailSuffix}\n`,
191
177
  );
192
178
  }
193
179
  }
194
180
 
195
- const TRACE_HELP = `overstory trace -- Show chronological timeline for an agent or bead
196
-
197
- Usage: overstory trace <target> [options]
198
-
199
- Arguments:
200
- <target> Agent name or bead ID
201
-
202
- Options:
203
- --json Output as JSON array of StoredEvent objects
204
- --since <timestamp> Start time filter (ISO 8601)
205
- --until <timestamp> End time filter (ISO 8601)
206
- --limit <n> Max events to show (default: 100)
207
- --help, -h Show this help`;
208
-
209
- /**
210
- * Entry point for `overstory trace <target> [--json] [--since] [--until] [--limit]`.
211
- */
212
- export async function traceCommand(args: string[]): Promise<void> {
213
- if (args.includes("--help") || args.includes("-h")) {
214
- process.stdout.write(`${TRACE_HELP}\n`);
215
- return;
216
- }
217
-
218
- // Extract positional target: first arg that is not a flag or flag value
219
- const flagsWithValues = new Set(["--since", "--until", "--limit"]);
220
- const booleanFlags = new Set(["--json", "--help", "-h"]);
221
- let target: string | undefined;
222
- for (let i = 0; i < args.length; i++) {
223
- const arg = args[i];
224
- if (arg === undefined) continue;
225
- if (booleanFlags.has(arg)) continue;
226
- if (flagsWithValues.has(arg)) {
227
- i++; // skip the value
228
- continue;
229
- }
230
- if (arg.startsWith("-")) continue;
231
- target = arg;
232
- break;
233
- }
234
-
235
- if (!target) {
236
- throw new ValidationError("Missing target. Usage: overstory trace <agent-name|bead-id>", {
237
- field: "target",
238
- });
239
- }
181
+ interface TraceOpts {
182
+ json?: boolean;
183
+ since?: string;
184
+ until?: string;
185
+ limit?: string;
186
+ }
240
187
 
241
- const json = hasFlag(args, "--json");
242
- const sinceStr = getFlag(args, "--since");
243
- const untilStr = getFlag(args, "--until");
244
- const limitStr = getFlag(args, "--limit");
188
+ async function executeTrace(target: string, opts: TraceOpts): Promise<void> {
189
+ const json = opts.json ?? false;
190
+ const sinceStr = opts.since;
191
+ const untilStr = opts.until;
192
+ const limitStr = opts.limit;
245
193
  const limit = limitStr ? Number.parseInt(limitStr, 10) : 100;
246
194
 
247
195
  if (Number.isNaN(limit) || limit < 1) {
@@ -273,15 +221,15 @@ export async function traceCommand(args: string[]): Promise<void> {
273
221
  let agentName = target;
274
222
 
275
223
  if (looksLikeBeadId(target)) {
276
- // Try to resolve bead ID to agent name via SessionStore
224
+ // Try to resolve task ID to agent name via SessionStore
277
225
  const { store: sessionStore } = openSessionStore(overstoryDir);
278
226
  try {
279
227
  const allSessions = sessionStore.getAll();
280
- const matchingSession = allSessions.find((s) => s.beadId === target);
228
+ const matchingSession = allSessions.find((s) => s.taskId === target);
281
229
  if (matchingSession) {
282
230
  agentName = matchingSession.agentName;
283
231
  } else {
284
- // No session found for this bead ID; treat it as an agent name anyway
232
+ // No session found for this task ID; treat it as an agent name anyway
285
233
  // (the event query will return empty results if no events match)
286
234
  agentName = target;
287
235
  }
@@ -323,3 +271,32 @@ export async function traceCommand(args: string[]): Promise<void> {
323
271
  eventStore.close();
324
272
  }
325
273
  }
274
+
275
+ export function createTraceCommand(): Command {
276
+ return new Command("trace")
277
+ .description("Chronological event timeline for agent/bead")
278
+ .argument("<target>", "Agent name or task ID")
279
+ .option("--json", "Output as JSON array of StoredEvent objects")
280
+ .option("--since <timestamp>", "Start time filter (ISO 8601)")
281
+ .option("--until <timestamp>", "End time filter (ISO 8601)")
282
+ .option("--limit <n>", "Max events to show (default: 100)")
283
+ .action(async (target: string, opts: TraceOpts) => {
284
+ await executeTrace(target, opts);
285
+ });
286
+ }
287
+
288
+ export async function traceCommand(args: string[]): Promise<void> {
289
+ const cmd = createTraceCommand();
290
+ cmd.exitOverride();
291
+ try {
292
+ await cmd.parseAsync(args, { from: "user" });
293
+ } catch (err: unknown) {
294
+ if (err && typeof err === "object" && "code" in err) {
295
+ const code = (err as { code: string }).code;
296
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
297
+ return;
298
+ }
299
+ }
300
+ throw err;
301
+ }
302
+ }
@@ -81,7 +81,7 @@ describe("watchCommand", () => {
81
81
  await watchCommand(["--help"]);
82
82
  const out = output();
83
83
 
84
- expect(out).toContain("overstory watch");
84
+ expect(out).toContain("watch");
85
85
  expect(out).toContain("--interval");
86
86
  expect(out).toContain("--background");
87
87
  expect(out).toContain("Tier 0");
@@ -91,7 +91,7 @@ describe("watchCommand", () => {
91
91
  await watchCommand(["-h"]);
92
92
  const out = output();
93
93
 
94
- expect(out).toContain("overstory watch");
94
+ expect(out).toContain("watch");
95
95
  expect(out).toContain("Tier 0");
96
96
  });
97
97
 
@@ -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 { OverstoryError } from "../errors.ts";
12
13
  import type { HealthCheck } from "../types.ts";
13
14
  import { startDaemon } from "../watchdog/daemon.ts";
14
15
  import { isProcessRunning } from "../watchdog/health.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 a health check for display.
33
19
  */
@@ -126,44 +112,21 @@ async function resolveOverstoryBin(): Promise<string> {
126
112
  }
127
113
 
128
114
  /**
129
- * Entry point for `overstory watch [--interval <ms>] [--background]`.
115
+ * Core implementation for the watch command.
130
116
  */
131
- const WATCH_HELP = `overstory watch Start Tier 0 mechanical watchdog daemon
132
-
133
- Usage: overstory watch [--interval <ms>] [--background]
134
-
135
- Tier numbering:
136
- Tier 0 Mechanical daemon (heartbeat, tmux/pid liveness) — this command
137
- Tier 1 Triage agent (ephemeral AI analysis of stalled agents)
138
- Tier 2 Monitor agent (continuous patrol — not yet implemented)
139
- Tier 3 Supervisor monitors (per-project)
140
-
141
- Options:
142
- --interval <ms> Health check interval in milliseconds (default: from config)
143
- --background Daemonize (run in background)
144
- --help, -h Show this help`;
145
-
146
- export async function watchCommand(args: string[]): Promise<void> {
147
- if (args.includes("--help") || args.includes("-h")) {
148
- process.stdout.write(`${WATCH_HELP}\n`);
149
- return;
150
- }
151
-
152
- const intervalStr = getFlag(args, "--interval");
153
- const background = hasFlag(args, "--background");
154
-
117
+ async function runWatch(opts: { interval?: string; background?: boolean }): Promise<void> {
155
118
  const cwd = process.cwd();
156
119
  const config = await loadConfig(cwd);
157
120
 
158
- const intervalMs = intervalStr
159
- ? Number.parseInt(intervalStr, 10)
121
+ const intervalMs = opts.interval
122
+ ? Number.parseInt(opts.interval, 10)
160
123
  : config.watchdog.tier0IntervalMs;
161
124
 
162
125
  const staleThresholdMs = config.watchdog.staleThresholdMs;
163
126
  const zombieThresholdMs = config.watchdog.zombieThresholdMs;
164
127
  const pidFilePath = join(config.project.root, ".overstory", "watchdog.pid");
165
128
 
166
- if (background) {
129
+ if (opts.background) {
167
130
  // Check if a watchdog is already running
168
131
  const existingPid = await readPidFile(pidFilePath);
169
132
  if (existingPid !== null && isProcessRunning(existingPid)) {
@@ -182,8 +145,8 @@ export async function watchCommand(args: string[]): Promise<void> {
182
145
 
183
146
  // Build the args for the child process, forwarding --interval but not --background
184
147
  const childArgs: string[] = ["watch"];
185
- if (intervalStr) {
186
- childArgs.push("--interval", intervalStr);
148
+ if (opts.interval) {
149
+ childArgs.push("--interval", opts.interval);
187
150
  }
188
151
 
189
152
  // Resolve the overstory binary path
@@ -245,3 +208,33 @@ export async function watchCommand(args: string[]): Promise<void> {
245
208
  // Block forever
246
209
  await new Promise(() => {});
247
210
  }
211
+
212
+ export function createWatchCommand(): Command {
213
+ return new Command("watch")
214
+ .description("Start Tier 0 mechanical watchdog daemon")
215
+ .option("--interval <ms>", "Health check interval in milliseconds")
216
+ .option("--background", "Daemonize (run in background)")
217
+ .action(async (opts: { interval?: string; background?: boolean }) => {
218
+ await runWatch(opts);
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Entry point for `overstory watch [--interval <ms>] [--background]`.
224
+ */
225
+ export async function watchCommand(args: string[]): Promise<void> {
226
+ const cmd = createWatchCommand();
227
+ cmd.exitOverride();
228
+
229
+ try {
230
+ await cmd.parseAsync(args, { from: "user" });
231
+ } catch (err: unknown) {
232
+ if (err && typeof err === "object" && "code" in err) {
233
+ const code = (err as { code: string }).code;
234
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
235
+ return;
236
+ }
237
+ }
238
+ throw err;
239
+ }
240
+ }