@os-eco/overstory-cli 0.6.1 → 0.6.4

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 (80) hide show
  1. package/README.md +7 -6
  2. package/package.json +12 -4
  3. package/src/agents/hooks-deployer.test.ts +94 -16
  4. package/src/agents/hooks-deployer.ts +18 -0
  5. package/src/agents/manifest.test.ts +86 -0
  6. package/src/commands/agents.test.ts +3 -3
  7. package/src/commands/agents.ts +59 -88
  8. package/src/commands/clean.test.ts +31 -46
  9. package/src/commands/clean.ts +28 -49
  10. package/src/commands/completions.ts +14 -0
  11. package/src/commands/coordinator.test.ts +131 -24
  12. package/src/commands/coordinator.ts +100 -63
  13. package/src/commands/costs.test.ts +2 -2
  14. package/src/commands/costs.ts +96 -75
  15. package/src/commands/dashboard.test.ts +2 -2
  16. package/src/commands/dashboard.ts +73 -93
  17. package/src/commands/doctor.test.ts +2 -2
  18. package/src/commands/doctor.ts +92 -79
  19. package/src/commands/errors.test.ts +2 -2
  20. package/src/commands/errors.ts +56 -50
  21. package/src/commands/feed.test.ts +2 -2
  22. package/src/commands/feed.ts +86 -83
  23. package/src/commands/group.ts +167 -177
  24. package/src/commands/hooks.test.ts +2 -2
  25. package/src/commands/hooks.ts +52 -42
  26. package/src/commands/init.test.ts +19 -19
  27. package/src/commands/init.ts +7 -16
  28. package/src/commands/inspect.test.ts +2 -2
  29. package/src/commands/inspect.ts +54 -57
  30. package/src/commands/log.test.ts +5 -10
  31. package/src/commands/log.ts +90 -84
  32. package/src/commands/logs.test.ts +1 -1
  33. package/src/commands/logs.ts +101 -104
  34. package/src/commands/mail.ts +157 -169
  35. package/src/commands/merge.test.ts +20 -58
  36. package/src/commands/merge.ts +13 -43
  37. package/src/commands/metrics.test.ts +2 -2
  38. package/src/commands/metrics.ts +33 -34
  39. package/src/commands/monitor.test.ts +3 -3
  40. package/src/commands/monitor.ts +56 -61
  41. package/src/commands/nudge.ts +41 -89
  42. package/src/commands/prime.test.ts +15 -47
  43. package/src/commands/prime.ts +7 -44
  44. package/src/commands/replay.test.ts +2 -2
  45. package/src/commands/replay.ts +79 -86
  46. package/src/commands/run.ts +97 -77
  47. package/src/commands/sling.test.ts +196 -0
  48. package/src/commands/sling.ts +24 -54
  49. package/src/commands/spec.test.ts +13 -39
  50. package/src/commands/spec.ts +30 -99
  51. package/src/commands/status.ts +46 -42
  52. package/src/commands/stop.test.ts +21 -39
  53. package/src/commands/stop.ts +18 -33
  54. package/src/commands/supervisor.test.ts +3 -5
  55. package/src/commands/supervisor.ts +136 -157
  56. package/src/commands/trace.test.ts +9 -9
  57. package/src/commands/trace.ts +54 -77
  58. package/src/commands/watch.test.ts +2 -2
  59. package/src/commands/watch.ts +38 -45
  60. package/src/commands/worktree.test.ts +8 -8
  61. package/src/commands/worktree.ts +63 -46
  62. package/src/config.test.ts +96 -0
  63. package/src/doctor/databases.test.ts +22 -2
  64. package/src/doctor/databases.ts +16 -0
  65. package/src/doctor/dependencies.test.ts +55 -1
  66. package/src/doctor/dependencies.ts +113 -18
  67. package/src/e2e/init-sling-lifecycle.test.ts +6 -6
  68. package/src/index.ts +223 -213
  69. package/src/logging/color.test.ts +74 -91
  70. package/src/logging/color.ts +52 -46
  71. package/src/logging/reporter.test.ts +10 -10
  72. package/src/logging/reporter.ts +6 -5
  73. package/src/merge/queue.test.ts +66 -0
  74. package/src/merge/queue.ts +15 -0
  75. package/src/schema-consistency.test.ts +239 -0
  76. package/src/sessions/compat.ts +1 -1
  77. package/src/sessions/store.test.ts +37 -0
  78. package/src/sessions/store.ts +11 -0
  79. package/src/worktree/tmux.test.ts +98 -9
  80. package/src/worktree/tmux.ts +18 -0
@@ -19,21 +19,12 @@ import { createMergeResolver } from "../merge/resolver.ts";
19
19
  import { createMulchClient } from "../mulch/client.ts";
20
20
  import type { MergeEntry, MergeResult } from "../types.ts";
21
21
 
22
- /**
23
- * Parse a named flag value from an args array.
24
- * Returns the value after the flag, or undefined if not present.
25
- */
26
- function getFlag(args: string[], flag: string): string | undefined {
27
- const idx = args.indexOf(flag);
28
- if (idx === -1 || idx + 1 >= args.length) {
29
- return undefined;
30
- }
31
- return args[idx + 1];
32
- }
33
-
34
- /** Check if a boolean flag is present in the args. */
35
- function hasFlag(args: string[], flag: string): boolean {
36
- return args.includes(flag);
22
+ export interface MergeOptions {
23
+ branch?: string;
24
+ all?: boolean;
25
+ into?: string;
26
+ dryRun?: boolean;
27
+ json?: boolean;
37
28
  }
38
29
 
39
30
  /**
@@ -135,35 +126,14 @@ function formatDryRun(entry: MergeEntry): string {
135
126
  /**
136
127
  * Entry point for `overstory merge [flags]`.
137
128
  *
138
- * Flags:
139
- * --branch <name> Merge a specific branch
140
- * --all Merge all pending branches in the queue
141
- * --dry-run Check for conflicts without actually merging
142
- * --json Output results as JSON
129
+ * @param opts - Command options
143
130
  */
144
- const MERGE_HELP = `overstory merge Merge agent branches into canonical
145
-
146
- Usage: overstory merge --branch <name> | --all [--into <branch>] [--dry-run] [--json]
147
-
148
- Options:
149
- --branch <name> Merge a specific branch
150
- --all Merge all pending branches in the queue
151
- --into <branch> Target branch to merge into (default: config canonicalBranch)
152
- --dry-run Check for conflicts without actually merging
153
- --json Output results as JSON
154
- --help, -h Show this help`;
155
-
156
- export async function mergeCommand(args: string[]): Promise<void> {
157
- if (args.includes("--help") || args.includes("-h")) {
158
- process.stdout.write(`${MERGE_HELP}\n`);
159
- return;
160
- }
161
-
162
- const branchName = getFlag(args, "--branch");
163
- const all = hasFlag(args, "--all");
164
- const into = getFlag(args, "--into");
165
- const dryRun = hasFlag(args, "--dry-run");
166
- const json = hasFlag(args, "--json");
131
+ export async function mergeCommand(opts: MergeOptions): Promise<void> {
132
+ const branchName = opts.branch;
133
+ const all = opts.all ?? false;
134
+ const into = opts.into;
135
+ const dryRun = opts.dryRun ?? false;
136
+ const json = opts.json ?? false;
167
137
 
168
138
  if (!branchName && !all) {
169
139
  throw new ValidationError("Either --branch <name> or --all is required for overstory merge", {
@@ -77,7 +77,7 @@ describe("metricsCommand", () => {
77
77
  await metricsCommand(["--help"]);
78
78
  const out = output();
79
79
 
80
- expect(out).toContain("overstory metrics");
80
+ expect(out).toContain("metrics");
81
81
  expect(out).toContain("--last <n>");
82
82
  expect(out).toContain("--json");
83
83
  expect(out).toContain("--help");
@@ -87,7 +87,7 @@ describe("metricsCommand", () => {
87
87
  await metricsCommand(["-h"]);
88
88
  const out = output();
89
89
 
90
- expect(out).toContain("overstory metrics");
90
+ expect(out).toContain("metrics");
91
91
  expect(out).toContain("--last <n>");
92
92
  });
93
93
 
@@ -6,22 +6,13 @@
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 { createMetricsStore } from "../metrics/store.ts";
11
12
 
12
- /**
13
- * Parse a named flag value from args.
14
- */
15
- function getFlag(args: string[], flag: string): string | undefined {
16
- const idx = args.indexOf(flag);
17
- if (idx === -1 || idx + 1 >= args.length) {
18
- return undefined;
19
- }
20
- return args[idx + 1];
21
- }
22
-
23
- function hasFlag(args: string[], flag: string): boolean {
24
- return args.includes(flag);
13
+ interface MetricsOpts {
14
+ last?: string;
15
+ json?: boolean;
25
16
  }
26
17
 
27
18
  /**
@@ -39,27 +30,9 @@ function formatDuration(ms: number): string {
39
30
  return `${hours}h ${remainMin}m`;
40
31
  }
41
32
 
42
- /**
43
- * Entry point for `overstory metrics [--last <n>] [--json]`.
44
- */
45
- const METRICS_HELP = `overstory metrics — Show session metrics
46
-
47
- Usage: overstory metrics [--last <n>] [--json]
48
-
49
- Options:
50
- --last <n> Number of recent sessions to show (default: 20)
51
- --json Output as JSON
52
- --help, -h Show this help`;
53
-
54
- export async function metricsCommand(args: string[]): Promise<void> {
55
- if (args.includes("--help") || args.includes("-h")) {
56
- process.stdout.write(`${METRICS_HELP}\n`);
57
- return;
58
- }
59
-
60
- const lastStr = getFlag(args, "--last");
61
- const limit = lastStr ? Number.parseInt(lastStr, 10) : 20;
62
- const json = hasFlag(args, "--json");
33
+ async function executeMetrics(opts: MetricsOpts): Promise<void> {
34
+ const limit = opts.last ? Number.parseInt(opts.last, 10) : 20;
35
+ const json = opts.json ?? false;
63
36
 
64
37
  const cwd = process.cwd();
65
38
  const config = await loadConfig(cwd);
@@ -141,3 +114,29 @@ export async function metricsCommand(args: string[]): Promise<void> {
141
114
  store.close();
142
115
  }
143
116
  }
117
+
118
+ export function createMetricsCommand(): Command {
119
+ return new Command("metrics")
120
+ .description("Show session metrics")
121
+ .option("--last <n>", "Number of recent sessions to show (default: 20)")
122
+ .option("--json", "Output as JSON")
123
+ .action(async (opts: MetricsOpts) => {
124
+ await executeMetrics(opts);
125
+ });
126
+ }
127
+
128
+ export async function metricsCommand(args: string[]): Promise<void> {
129
+ const cmd = createMetricsCommand();
130
+ cmd.exitOverride();
131
+ try {
132
+ await cmd.parseAsync(args, { from: "user" });
133
+ } catch (err: unknown) {
134
+ if (err && typeof err === "object" && "code" in err) {
135
+ const code = (err as { code: string }).code;
136
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
137
+ return;
138
+ }
139
+ }
140
+ throw err;
141
+ }
142
+ }
@@ -89,7 +89,7 @@ describe("monitorCommand", () => {
89
89
  test("--help prints help text containing 'overstory monitor'", async () => {
90
90
  await monitorCommand(["--help"]);
91
91
  const output = stdoutWrites.join("");
92
- expect(output).toContain("overstory monitor");
92
+ expect(output).toContain("monitor");
93
93
  });
94
94
 
95
95
  test("--help prints help text containing 'start'", async () => {
@@ -113,13 +113,13 @@ describe("monitorCommand", () => {
113
113
  test("-h prints help text", async () => {
114
114
  await monitorCommand(["-h"]);
115
115
  const output = stdoutWrites.join("");
116
- expect(output).toContain("overstory monitor");
116
+ expect(output).toContain("monitor");
117
117
  });
118
118
 
119
119
  test("empty args [] shows help (same as --help)", async () => {
120
120
  await monitorCommand([]);
121
121
  const output = stdoutWrites.join("");
122
- expect(output).toContain("overstory monitor");
122
+ expect(output).toContain("monitor");
123
123
  });
124
124
 
125
125
  test("unknown subcommand 'restart' throws ValidationError", async () => {
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { mkdir } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
+ import { Command } from "commander";
18
19
  import { deployHooks } from "../agents/hooks-deployer.ts";
19
20
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
20
21
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
@@ -50,15 +51,6 @@ export function buildMonitorBeacon(): string {
50
51
  return parts.join(" — ");
51
52
  }
52
53
 
53
- /**
54
- * Determine whether to auto-attach to the tmux session after starting.
55
- */
56
- function resolveAttach(args: string[], isTTY: boolean): boolean {
57
- if (args.includes("--attach")) return true;
58
- if (args.includes("--no-attach")) return false;
59
- return isTTY;
60
- }
61
-
62
54
  /**
63
55
  * Start the monitor agent.
64
56
  *
@@ -70,9 +62,8 @@ function resolveAttach(args: string[], isTTY: boolean): boolean {
70
62
  * 6. Send startup beacon
71
63
  * 7. Record session in SessionStore (sessions.db)
72
64
  */
73
- async function startMonitor(args: string[]): Promise<void> {
74
- const json = args.includes("--json");
75
- const shouldAttach = resolveAttach(args, !!process.stdout.isTTY);
65
+ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<void> {
66
+ const { json, attach: shouldAttach } = opts;
76
67
 
77
68
  if (isRunningAsRoot()) {
78
69
  throw new AgentError(
@@ -118,8 +109,6 @@ async function startMonitor(args: string[]): Promise<void> {
118
109
  }
119
110
 
120
111
  // Deploy monitor-specific hooks to the project root's .claude/ directory.
121
- // The monitor gets the same structural enforcement as other non-implementation
122
- // agents (Write/Edit/NotebookEdit blocked, dangerous bash commands blocked).
123
112
  await deployHooks(projectRoot, MONITOR_NAME, "monitor");
124
113
 
125
114
  // Create monitor identity if first run
@@ -146,7 +135,6 @@ async function startMonitor(args: string[]): Promise<void> {
146
135
  const { model, env } = resolveModel(config, manifest, "monitor", "sonnet");
147
136
 
148
137
  // Spawn tmux session at project root with Claude Code (interactive mode).
149
- // Inject the monitor base definition via --append-system-prompt.
150
138
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
151
139
  const agentDefFile = Bun.file(agentDefPath);
152
140
  let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
@@ -226,8 +214,8 @@ async function startMonitor(args: string[]): Promise<void> {
226
214
  * 2. Kill the tmux session (with process tree cleanup)
227
215
  * 3. Mark session as completed in SessionStore
228
216
  */
229
- async function stopMonitor(args: string[]): Promise<void> {
230
- const json = args.includes("--json");
217
+ async function stopMonitor(opts: { json: boolean }): Promise<void> {
218
+ const { json } = opts;
231
219
  const cwd = process.cwd();
232
220
  const config = await loadConfig(cwd);
233
221
  const projectRoot = config.project.root;
@@ -273,8 +261,8 @@ async function stopMonitor(args: string[]): Promise<void> {
273
261
  *
274
262
  * Checks session registry and tmux liveness to report actual state.
275
263
  */
276
- async function statusMonitor(args: string[]): Promise<void> {
277
- const json = args.includes("--json");
264
+ async function statusMonitor(opts: { json: boolean }): Promise<void> {
265
+ const { json } = opts;
278
266
  const cwd = process.cwd();
279
267
  const config = await loadConfig(cwd);
280
268
  const projectRoot = config.project.root;
@@ -301,8 +289,6 @@ async function statusMonitor(args: string[]): Promise<void> {
301
289
  const alive = await isSessionAlive(session.tmuxSession);
302
290
 
303
291
  // Reconcile state: if session says active but tmux is dead, update.
304
- // We already filtered out completed/zombie states above, so if tmux is dead
305
- // this session needs to be marked as zombie.
306
292
  if (!alive) {
307
293
  store.updateState(MONITOR_NAME, "zombie");
308
294
  store.updateLastActivity(MONITOR_NAME);
@@ -335,56 +321,65 @@ async function statusMonitor(args: string[]): Promise<void> {
335
321
  }
336
322
  }
337
323
 
338
- const MONITOR_HELP = `overstory monitor — Manage the persistent Tier 2 monitor agent
339
-
340
- Usage: overstory monitor <subcommand> [flags]
341
-
342
- Subcommands:
343
- start Start the monitor (spawns Claude Code at project root)
344
- stop Stop the monitor (kills tmux session)
345
- status Show monitor state
324
+ export function createMonitorCommand(): Command {
325
+ const cmd = new Command("monitor").description("Manage the persistent Tier 2 monitor agent");
326
+
327
+ cmd
328
+ .command("start")
329
+ .description("Start the monitor (spawns Claude Code at project root)")
330
+ .option("--attach", "Always attach to tmux session after start")
331
+ .option("--no-attach", "Never attach to tmux session after start")
332
+ .option("--json", "Output as JSON")
333
+ .action(async (opts: { attach?: boolean; json?: boolean }) => {
334
+ // opts.attach = true if --attach, false if --no-attach, undefined if neither
335
+ const shouldAttach = opts.attach !== undefined ? opts.attach : !!process.stdout.isTTY;
336
+ await startMonitor({ json: opts.json ?? false, attach: shouldAttach });
337
+ });
346
338
 
347
- Start options:
348
- --attach Always attach to tmux session after start
349
- --no-attach Never attach to tmux session after start
350
- Default: attach when running in an interactive TTY
339
+ cmd
340
+ .command("stop")
341
+ .description("Stop the monitor (kills tmux session)")
342
+ .option("--json", "Output as JSON")
343
+ .action(async (opts: { json?: boolean }) => {
344
+ await stopMonitor({ json: opts.json ?? false });
345
+ });
351
346
 
352
- General options:
353
- --json Output as JSON
354
- --help, -h Show this help
347
+ cmd
348
+ .command("status")
349
+ .description("Show monitor state")
350
+ .option("--json", "Output as JSON")
351
+ .action(async (opts: { json?: boolean }) => {
352
+ await statusMonitor({ json: opts.json ?? false });
353
+ });
355
354
 
356
- The monitor agent (Tier 2) continuously patrols the agent fleet by:
357
- - Checking agent health via overstory status
358
- - Sending progressive nudges to stalled agents
359
- - Escalating unresponsive agents to the coordinator
360
- - Producing periodic health summaries`;
355
+ return cmd;
356
+ }
361
357
 
362
358
  /**
363
359
  * Entry point for `overstory monitor <subcommand>`.
364
360
  */
365
361
  export async function monitorCommand(args: string[]): Promise<void> {
366
- if (args.includes("--help") || args.includes("-h") || args.length === 0) {
367
- process.stdout.write(`${MONITOR_HELP}\n`);
362
+ const cmd = createMonitorCommand();
363
+ cmd.exitOverride();
364
+
365
+ if (args.length === 0) {
366
+ process.stdout.write(cmd.helpInformation());
368
367
  return;
369
368
  }
370
369
 
371
- const subcommand = args[0];
372
- const subArgs = args.slice(1);
373
-
374
- switch (subcommand) {
375
- case "start":
376
- await startMonitor(subArgs);
377
- break;
378
- case "stop":
379
- await stopMonitor(subArgs);
380
- break;
381
- case "status":
382
- await statusMonitor(subArgs);
383
- break;
384
- default:
385
- throw new ValidationError(
386
- `Unknown monitor subcommand: ${subcommand}. Run 'overstory monitor --help' for usage.`,
387
- { field: "subcommand", value: subcommand },
388
- );
370
+ try {
371
+ await cmd.parseAsync(args, { from: "user" });
372
+ } catch (err: unknown) {
373
+ if (err && typeof err === "object" && "code" in err) {
374
+ const code = (err as { code: string }).code;
375
+ if (code === "commander.helpDisplayed" || code === "commander.version") {
376
+ return;
377
+ }
378
+ if (code === "commander.unknownCommand") {
379
+ const message = err instanceof Error ? err.message : String(err);
380
+ throw new ValidationError(message, { field: "subcommand" });
381
+ }
382
+ }
383
+ throw err;
389
384
  }
390
385
  }
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { join } from "node:path";
13
- import { AgentError, ValidationError } from "../errors.ts";
13
+ import { Command } from "commander";
14
+ import { AgentError } from "../errors.ts";
14
15
  import { createEventStore } from "../events/store.ts";
15
16
  import { openSessionStore } from "../sessions/compat.ts";
16
17
  import type { EventStore } from "../types.ts";
@@ -21,44 +22,6 @@ const MAX_RETRIES = 3;
21
22
  const RETRY_DELAY_MS = 500;
22
23
  const DEBOUNCE_MS = 500;
23
24
 
24
- /**
25
- * Parse a named flag value from args.
26
- */
27
- function getFlag(args: string[], flag: string): string | undefined {
28
- const idx = args.indexOf(flag);
29
- if (idx === -1 || idx + 1 >= args.length) {
30
- return undefined;
31
- }
32
- return args[idx + 1];
33
- }
34
-
35
- /** Boolean flags that do NOT consume the next arg. */
36
- const BOOLEAN_FLAGS = new Set(["--json", "--force", "--help", "-h"]);
37
-
38
- /**
39
- * Extract positional arguments, skipping flag-value pairs.
40
- */
41
- function getPositionalArgs(args: string[]): string[] {
42
- const positional: string[] = [];
43
- let i = 0;
44
- while (i < args.length) {
45
- const arg = args[i];
46
- if (arg?.startsWith("-")) {
47
- if (BOOLEAN_FLAGS.has(arg)) {
48
- i += 1;
49
- } else {
50
- i += 2;
51
- }
52
- } else {
53
- if (arg !== undefined) {
54
- positional.push(arg);
55
- }
56
- i += 1;
57
- }
58
- }
59
- return positional;
60
- }
61
-
62
25
  /**
63
26
  * Load the orchestrator's registered tmux session name.
64
27
  *
@@ -317,56 +280,45 @@ export async function nudgeAgent(
317
280
  /**
318
281
  * Entry point for `overstory nudge <agent-name> [message]`.
319
282
  */
320
- const NUDGE_HELP = `overstory nudge — Send a text nudge to an agent
321
-
322
- Usage: overstory nudge <agent-name> [message]
323
-
324
- Arguments:
325
- <agent-name> Name of the agent to nudge
326
- [message] Text to send (default: "${DEFAULT_MESSAGE}")
327
-
328
- Options:
329
- --from <name> Sender name for the nudge prefix (default: orchestrator)
330
- --force Skip debounce check
331
- --json Output result as JSON
332
- --help, -h Show this help`;
333
-
334
283
  export async function nudgeCommand(args: string[]): Promise<void> {
335
- if (args.includes("--help") || args.includes("-h")) {
336
- process.stdout.write(`${NUDGE_HELP}\n`);
337
- return;
338
- }
339
-
340
- const positional = getPositionalArgs(args);
341
- const agentName = positional[0];
342
- if (!agentName || agentName.trim().length === 0) {
343
- throw new ValidationError("Agent name is required: overstory nudge <agent-name> [message]", {
344
- field: "agentName",
345
- });
346
- }
347
-
348
- const from = getFlag(args, "--from") ?? "orchestrator";
349
- const force = args.includes("--force");
350
- const json = args.includes("--json");
351
-
352
- // Build the nudge message: prefix with sender, use custom or default text
353
- const customMessage = positional.slice(1).join(" ");
354
- const rawMessage = customMessage.length > 0 ? customMessage : DEFAULT_MESSAGE;
355
- const message = `[NUDGE from ${from}] ${rawMessage}`;
356
-
357
- // Resolve project root
358
- const { resolveProjectRoot } = await import("../config.ts");
359
- const projectRoot = await resolveProjectRoot(process.cwd());
360
-
361
- const result = await nudgeAgent(projectRoot, agentName, message, force);
362
-
363
- if (json) {
364
- process.stdout.write(
365
- `${JSON.stringify({ agentName, delivered: result.delivered, reason: result.reason })}\n`,
284
+ const program = new Command();
285
+ program
286
+ .name("overstory nudge")
287
+ .description("Send a text nudge to an agent")
288
+ .argument("<agent-name>", "Name of the agent to nudge")
289
+ .argument("[message...]", "Text to send (default: check mail prompt)")
290
+ .option("--from <name>", "Sender name", "orchestrator")
291
+ .option("--force", "Skip debounce check")
292
+ .option("--json", "Output result as JSON")
293
+ .exitOverride()
294
+ .action(
295
+ async (
296
+ agentName: string,
297
+ messageParts: string[],
298
+ opts: { from: string; force?: boolean; json?: boolean },
299
+ ) => {
300
+ // Build the nudge message: prefix with sender, use custom or default text
301
+ const customMessage = messageParts.join(" ");
302
+ const rawMessage = customMessage.length > 0 ? customMessage : DEFAULT_MESSAGE;
303
+ const message = `[NUDGE from ${opts.from}] ${rawMessage}`;
304
+
305
+ // Resolve project root
306
+ const { resolveProjectRoot } = await import("../config.ts");
307
+ const projectRoot = await resolveProjectRoot(process.cwd());
308
+
309
+ const result = await nudgeAgent(projectRoot, agentName, message, opts.force ?? false);
310
+
311
+ if (opts.json) {
312
+ process.stdout.write(
313
+ `${JSON.stringify({ agentName, delivered: result.delivered, reason: result.reason })}\n`,
314
+ );
315
+ } else if (result.delivered) {
316
+ process.stdout.write(`📢 Nudged "${agentName}"\n`);
317
+ } else {
318
+ throw new AgentError(`Nudge failed: ${result.reason}`, { agentName });
319
+ }
320
+ },
366
321
  );
367
- } else if (result.delivered) {
368
- process.stdout.write(`📢 Nudged "${agentName}"\n`);
369
- } else {
370
- throw new AgentError(`Nudge failed: ${result.reason}`, { agentName });
371
- }
322
+
323
+ await program.parseAsync(["node", "overstory-nudge", ...args]);
372
324
  }