@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
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * CLI command: overstory mail send/check/list/read/reply
3
3
  *
4
- * Parses CLI args and delegates to the mail client.
4
+ * Parses CLI args via Commander.js and delegates to the mail client.
5
5
  * Supports --inject for hook context injection, --json for machine output,
6
6
  * and various filters for listing messages.
7
7
  */
8
8
 
9
9
  import { join } from "node:path";
10
+ import { Command } from "commander";
10
11
  import { resolveProjectRoot } from "../config.ts";
11
- import { MailError, ValidationError } from "../errors.ts";
12
+ import { ValidationError } from "../errors.ts";
12
13
  import { createEventStore } from "../events/store.ts";
13
14
  import { isGroupAddress, resolveGroupAddress } from "../mail/broadcast.ts";
14
15
  import { createMailClient } from "../mail/client.ts";
@@ -29,54 +30,6 @@ const AUTO_NUDGE_TYPES: ReadonlySet<MailMessageType> = new Set([
29
30
  "merge_failed",
30
31
  ]);
31
32
 
32
- /**
33
- * Parse a named flag value from an args array.
34
- * Returns the value after the flag, or undefined if not present.
35
- */
36
- function getFlag(args: string[], flag: string): string | undefined {
37
- const idx = args.indexOf(flag);
38
- if (idx === -1 || idx + 1 >= args.length) {
39
- return undefined;
40
- }
41
- return args[idx + 1];
42
- }
43
-
44
- /** Check if a boolean flag is present in the args. */
45
- function hasFlag(args: string[], flag: string): boolean {
46
- return args.includes(flag);
47
- }
48
-
49
- /** Boolean flags that do NOT consume the next arg as a value. */
50
- const BOOLEAN_FLAGS = new Set(["--json", "--inject", "--unread", "--all", "--help", "-h"]);
51
-
52
- /**
53
- * Extract positional arguments from an args array, skipping flag-value pairs.
54
- *
55
- * Iterates through args, skipping `--flag value` pairs for value-bearing flags
56
- * and lone boolean flags. Everything else is a positional arg.
57
- */
58
- function getPositionalArgs(args: string[]): string[] {
59
- const positional: string[] = [];
60
- let i = 0;
61
- while (i < args.length) {
62
- const arg = args[i];
63
- if (arg?.startsWith("-")) {
64
- // It's a flag. If it's boolean, skip just it; otherwise skip it + its value.
65
- if (BOOLEAN_FLAGS.has(arg)) {
66
- i += 1;
67
- } else {
68
- i += 2; // skip flag + its value
69
- }
70
- } else {
71
- if (arg !== undefined) {
72
- positional.push(arg);
73
- }
74
- i += 1;
75
- }
76
- }
77
- return positional;
78
- }
79
-
80
33
  /** Format a single message for human-readable output. */
81
34
  function formatMessage(msg: MailMessage): string {
82
35
  const readMarker = msg.read ? " " : "*";
@@ -255,17 +208,58 @@ function openClient(cwd: string) {
255
208
  return client;
256
209
  }
257
210
 
211
+ // === Typed option interfaces for each subcommand ===
212
+
213
+ interface SendOpts {
214
+ to: string;
215
+ subject: string;
216
+ body: string;
217
+ from?: string;
218
+ agent?: string;
219
+ type?: string;
220
+ priority?: string;
221
+ payload?: string;
222
+ json?: boolean;
223
+ }
224
+
225
+ interface CheckOpts {
226
+ agent?: string;
227
+ inject?: boolean;
228
+ json?: boolean;
229
+ debounce?: string;
230
+ }
231
+
232
+ interface ListOpts {
233
+ from?: string;
234
+ to?: string;
235
+ agent?: string;
236
+ unread?: boolean;
237
+ json?: boolean;
238
+ }
239
+
240
+ interface ReplyOpts {
241
+ body: string;
242
+ from?: string;
243
+ agent?: string;
244
+ json?: boolean;
245
+ }
246
+
247
+ interface PurgeOpts {
248
+ all?: boolean;
249
+ days?: string;
250
+ agent?: string;
251
+ json?: boolean;
252
+ }
253
+
258
254
  /** overstory mail send */
259
- async function handleSend(args: string[], cwd: string): Promise<void> {
260
- const to = getFlag(args, "--to");
261
- const subject = getFlag(args, "--subject");
262
- const body = getFlag(args, "--body");
263
- const from = getFlag(args, "--agent") ?? getFlag(args, "--from") ?? "orchestrator";
264
- const rawPayload = getFlag(args, "--payload");
255
+ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
256
+ const { to, subject, body } = opts;
257
+ const from = opts.agent ?? opts.from ?? "orchestrator";
258
+ const rawPayload = opts.payload;
265
259
  const VALID_PRIORITIES = ["low", "normal", "high", "urgent"] as const;
266
260
 
267
- const rawType = getFlag(args, "--type") ?? "status";
268
- const rawPriority = getFlag(args, "--priority") ?? "normal";
261
+ const rawType = opts.type ?? "status";
262
+ const rawPriority = opts.priority ?? "normal";
269
263
 
270
264
  if (!MAIL_MESSAGE_TYPES.includes(rawType as MailMessage["type"])) {
271
265
  throw new ValidationError(
@@ -297,16 +291,6 @@ async function handleSend(args: string[], cwd: string): Promise<void> {
297
291
  }
298
292
  }
299
293
 
300
- if (!to) {
301
- throw new ValidationError("--to is required for mail send", { field: "to" });
302
- }
303
- if (!subject) {
304
- throw new ValidationError("--subject is required for mail send", { field: "subject" });
305
- }
306
- if (!body) {
307
- throw new ValidationError("--body is required for mail send", { field: "body" });
308
- }
309
-
310
294
  // Handle broadcast messages (group addresses)
311
295
  if (isGroupAddress(to)) {
312
296
  const overstoryDir = join(cwd, ".overstory");
@@ -383,7 +367,7 @@ async function handleSend(args: string[], cwd: string): Promise<void> {
383
367
  }
384
368
 
385
369
  // Output broadcast summary
386
- if (hasFlag(args, "--json")) {
370
+ if (opts.json) {
387
371
  process.stdout.write(
388
372
  `${JSON.stringify({ messageIds, recipientCount: recipients.length })}\n`,
389
373
  );
@@ -442,7 +426,7 @@ async function handleSend(args: string[], cwd: string): Promise<void> {
442
426
  // Event recording failure is non-fatal
443
427
  }
444
428
 
445
- if (hasFlag(args, "--json")) {
429
+ if (opts.json) {
446
430
  process.stdout.write(`${JSON.stringify({ id })}\n`);
447
431
  } else {
448
432
  process.stdout.write(`✉️ Sent message ${id} to ${to}\n`);
@@ -463,7 +447,7 @@ async function handleSend(args: string[], cwd: string): Promise<void> {
463
447
  subject,
464
448
  messageId: id,
465
449
  });
466
- if (!hasFlag(args, "--json")) {
450
+ if (!opts.json) {
467
451
  process.stdout.write(
468
452
  `📢 Queued nudge for "${to}" (${nudgeReason}, delivered on next prompt)\n`,
469
453
  );
@@ -507,11 +491,11 @@ async function handleSend(args: string[], cwd: string): Promise<void> {
507
491
  }
508
492
 
509
493
  /** overstory mail check */
510
- async function handleCheck(args: string[], cwd: string): Promise<void> {
511
- const agent = getFlag(args, "--agent") ?? "orchestrator";
512
- const inject = hasFlag(args, "--inject");
513
- const json = hasFlag(args, "--json");
514
- const debounceFlag = getFlag(args, "--debounce");
494
+ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
495
+ const agent = opts.agent ?? "orchestrator";
496
+ const inject = opts.inject ?? false;
497
+ const json = opts.json ?? false;
498
+ const debounceFlag = opts.debounce;
515
499
 
516
500
  // Parse debounce interval if provided
517
501
  let debounceMs: number | undefined;
@@ -578,12 +562,12 @@ async function handleCheck(args: string[], cwd: string): Promise<void> {
578
562
  }
579
563
 
580
564
  /** overstory mail list */
581
- function handleList(args: string[], cwd: string): void {
582
- const from = getFlag(args, "--from");
583
- // --agent is an alias for --to, providing agent-scoped perspective (like mail check)
584
- const to = getFlag(args, "--to") ?? getFlag(args, "--agent");
585
- const unread = hasFlag(args, "--unread") ? true : undefined;
586
- const json = hasFlag(args, "--json");
565
+ function handleList(opts: ListOpts, cwd: string): void {
566
+ const from = opts.from;
567
+ // --to takes precedence over --agent (agent is an alias for recipient filtering)
568
+ const to = opts.to ?? opts.agent;
569
+ const unread = opts.unread ? true : undefined;
570
+ const json = opts.json ?? false;
587
571
 
588
572
  const client = openClient(cwd);
589
573
  try {
@@ -607,13 +591,7 @@ function handleList(args: string[], cwd: string): void {
607
591
  }
608
592
 
609
593
  /** overstory mail read */
610
- function handleRead(args: string[], cwd: string): void {
611
- const positional = getPositionalArgs(args);
612
- const id = positional[0];
613
- if (!id) {
614
- throw new ValidationError("Message ID is required for mail read", { field: "id" });
615
- }
616
-
594
+ function handleRead(id: string, cwd: string): void {
617
595
  const client = openClient(cwd);
618
596
  try {
619
597
  const { alreadyRead } = client.markRead(id);
@@ -628,24 +606,15 @@ function handleRead(args: string[], cwd: string): void {
628
606
  }
629
607
 
630
608
  /** overstory mail reply */
631
- function handleReply(args: string[], cwd: string): void {
632
- const positional = getPositionalArgs(args);
633
- const id = positional[0];
634
- const body = getFlag(args, "--body");
635
- const from = getFlag(args, "--agent") ?? getFlag(args, "--from") ?? "orchestrator";
636
-
637
- if (!id) {
638
- throw new ValidationError("Message ID is required for mail reply", { field: "id" });
639
- }
640
- if (!body) {
641
- throw new ValidationError("--body is required for mail reply", { field: "body" });
642
- }
609
+ function handleReply(id: string, opts: ReplyOpts, cwd: string): void {
610
+ const body = opts.body;
611
+ const from = opts.agent ?? opts.from ?? "orchestrator";
643
612
 
644
613
  const client = openClient(cwd);
645
614
  try {
646
615
  const replyId = client.reply(id, body, from);
647
616
 
648
- if (hasFlag(args, "--json")) {
617
+ if (opts.json) {
649
618
  process.stdout.write(`${JSON.stringify({ id: replyId })}\n`);
650
619
  } else {
651
620
  process.stdout.write(`✉️ Reply sent: ${replyId}\n`);
@@ -656,11 +625,11 @@ function handleReply(args: string[], cwd: string): void {
656
625
  }
657
626
 
658
627
  /** overstory mail purge */
659
- function handlePurge(args: string[], cwd: string): void {
660
- const all = hasFlag(args, "--all");
661
- const daysStr = getFlag(args, "--days");
662
- const agent = getFlag(args, "--agent");
663
- const json = hasFlag(args, "--json");
628
+ function handlePurge(opts: PurgeOpts, cwd: string): void {
629
+ const all = opts.all ?? false;
630
+ const daysStr = opts.days;
631
+ const agent = opts.agent;
632
+ const json = opts.json ?? false;
664
633
 
665
634
  if (!all && daysStr === undefined && agent === undefined) {
666
635
  throw new ValidationError(
@@ -699,73 +668,92 @@ function handlePurge(args: string[], cwd: string): void {
699
668
  * Entry point for `overstory mail <subcommand> [args...]`.
700
669
  *
701
670
  * Subcommands: send, check, list, read, reply, purge.
671
+ * Uses Commander.js for subcommand routing and option parsing.
702
672
  */
703
- const MAIL_HELP = `overstory mail — Agent messaging system
704
-
705
- Usage: overstory mail <subcommand> [args...]
706
-
707
- Subcommands:
708
- send Send a message
709
- --to <agent> --subject <text> --body <text>
710
- [--from <name>] [--agent <name> (alias for --from)]
711
- [--type <type>] [--priority <low|normal|high|urgent>]
712
- [--payload <json>] [--json]
713
- Types: status, question, result, error (semantic)
714
- worker_done, merge_ready, merged, merge_failed,
715
- escalation, health_check, dispatch, assign (protocol)
716
- check Check inbox (unread messages)
717
- [--agent <name>] [--inject] [--json]
718
- list List messages with filters
719
- [--from <name>] [--to <name>] [--agent <name> (alias for --to)]
720
- [--unread] [--json]
721
- read Mark a message as read
722
- <message-id>
723
- reply Reply to a message
724
- <message-id> --body <text> [--from <name>]
725
- [--agent <name> (alias for --from)] [--json]
726
- purge Delete old messages
727
- --all | --days <n> | --agent <name>
728
- [--json]
729
-
730
- Options:
731
- --help, -h Show this help`;
732
-
733
673
  export async function mailCommand(args: string[]): Promise<void> {
734
- if (args.includes("--help") || args.includes("-h")) {
735
- process.stdout.write(`${MAIL_HELP}\n`);
736
- return;
737
- }
738
-
739
- const subcommand = args[0];
740
- const subArgs = args.slice(1);
741
-
742
674
  // Resolve the actual project root (handles git worktrees).
743
675
  // Mail commands may run from agent worktrees via hooks, so we must
744
676
  // resolve up to the main project root where .overstory/mail.db lives.
745
677
  const root = await resolveProjectRoot(process.cwd());
746
678
 
747
- switch (subcommand) {
748
- case "send":
749
- await handleSend(subArgs, root);
750
- break;
751
- case "check":
752
- await handleCheck(subArgs, root);
753
- break;
754
- case "list":
755
- handleList(subArgs, root);
756
- break;
757
- case "read":
758
- handleRead(subArgs, root);
759
- break;
760
- case "reply":
761
- handleReply(subArgs, root);
762
- break;
763
- case "purge":
764
- handlePurge(subArgs, root);
765
- break;
766
- default:
767
- throw new MailError(
768
- `Unknown mail subcommand: ${subcommand ?? "(none)"}. Use: send, check, list, read, reply, purge`,
769
- );
770
- }
679
+ const program = new Command();
680
+ program.name("overstory mail").description("Agent messaging system").exitOverride();
681
+
682
+ program
683
+ .command("send")
684
+ .description("Send a message")
685
+ .requiredOption("--to <agent>", "Recipient agent name")
686
+ .requiredOption("--subject <text>", "Message subject")
687
+ .requiredOption("--body <text>", "Message body")
688
+ .option("--from <name>", "Sender name")
689
+ .option("--agent <name>", "Alias for --from")
690
+ .option("--type <type>", "Message type", "status")
691
+ .option("--priority <level>", "Priority level", "normal")
692
+ .option("--payload <json>", "Structured JSON payload")
693
+ .option("--json", "Output as JSON")
694
+ .exitOverride()
695
+ .action(async (opts: SendOpts) => {
696
+ await handleSend(opts, root);
697
+ });
698
+
699
+ program
700
+ .command("check")
701
+ .description("Check inbox (unread messages)")
702
+ .option("--agent <name>", "Agent name")
703
+ .option("--inject", "Inject format for hook context")
704
+ .option("--json", "Output as JSON")
705
+ .option("--debounce <ms>", "Debounce interval in milliseconds")
706
+ .exitOverride()
707
+ .action(async (opts: CheckOpts) => {
708
+ await handleCheck(opts, root);
709
+ });
710
+
711
+ program
712
+ .command("list")
713
+ .description("List messages with filters")
714
+ .option("--from <name>", "Filter by sender")
715
+ .option("--to <name>", "Filter by recipient")
716
+ .option("--agent <name>", "Alias for --to (filter by recipient)")
717
+ .option("--unread", "Show only unread messages")
718
+ .option("--json", "Output as JSON")
719
+ .exitOverride()
720
+ .action((opts: ListOpts) => {
721
+ handleList(opts, root);
722
+ });
723
+
724
+ program
725
+ .command("read")
726
+ .description("Mark a message as read")
727
+ .argument("<message-id>", "Message ID")
728
+ .exitOverride()
729
+ .action((id: string) => {
730
+ handleRead(id, root);
731
+ });
732
+
733
+ program
734
+ .command("reply")
735
+ .description("Reply to a message")
736
+ .argument("<message-id>", "Message ID to reply to")
737
+ .requiredOption("--body <text>", "Reply body")
738
+ .option("--from <name>", "Sender name")
739
+ .option("--agent <name>", "Alias for --from")
740
+ .option("--json", "Output as JSON")
741
+ .exitOverride()
742
+ .action((id: string, opts: ReplyOpts) => {
743
+ handleReply(id, opts, root);
744
+ });
745
+
746
+ program
747
+ .command("purge")
748
+ .description("Delete old messages")
749
+ .option("--all", "Purge all messages")
750
+ .option("--days <n>", "Purge messages older than N days")
751
+ .option("--agent <name>", "Purge messages for specific agent")
752
+ .option("--json", "Output as JSON")
753
+ .exitOverride()
754
+ .action((opts: PurgeOpts) => {
755
+ handlePurge(opts, root);
756
+ });
757
+
758
+ await program.parseAsync(["node", "overstory-mail", ...args]);
771
759
  }
@@ -63,50 +63,12 @@ merge:
63
63
  await runGitInDir(dir, ["checkout", defaultBranch]);
64
64
  }
65
65
 
66
- describe("help and validation", () => {
67
- test("--help prints help containing 'overstory merge', '--branch', '--all', '--dry-run', '--into'", async () => {
68
- let output = "";
69
- const originalWrite = process.stdout.write.bind(process.stdout);
70
- process.stdout.write = (chunk: unknown): boolean => {
71
- output += String(chunk);
72
- return true;
73
- };
74
-
75
- try {
76
- await mergeCommand(["--help"]);
77
- } finally {
78
- process.stdout.write = originalWrite;
79
- }
80
-
81
- expect(output).toContain("overstory merge");
82
- expect(output).toContain("--branch");
83
- expect(output).toContain("--all");
84
- expect(output).toContain("--dry-run");
85
- expect(output).toContain("--into");
86
- });
87
-
88
- test("-h prints help", async () => {
89
- let output = "";
90
- const originalWrite = process.stdout.write.bind(process.stdout);
91
- process.stdout.write = (chunk: unknown): boolean => {
92
- output += String(chunk);
93
- return true;
94
- };
95
-
96
- try {
97
- await mergeCommand(["-h"]);
98
- } finally {
99
- process.stdout.write = originalWrite;
100
- }
101
-
102
- expect(output).toContain("overstory merge");
103
- });
104
-
105
- test("no flags throws ValidationError mentioning '--branch' and '--all'", async () => {
66
+ describe("validation", () => {
67
+ test("no branch/all throws ValidationError mentioning '--branch' and '--all'", async () => {
106
68
  await setupProject(repoDir, defaultBranch);
107
69
 
108
70
  try {
109
- await mergeCommand([]);
71
+ await mergeCommand({});
110
72
  expect(true).toBe(false); // Should not reach here
111
73
  } catch (err: unknown) {
112
74
  expect(err).toBeInstanceOf(ValidationError);
@@ -122,7 +84,7 @@ merge:
122
84
  await setupProject(repoDir, defaultBranch);
123
85
 
124
86
  try {
125
- await mergeCommand(["--branch", "nonexistent-branch"]);
87
+ await mergeCommand({ branch: "nonexistent-branch" });
126
88
  expect(true).toBe(false); // Should not reach here
127
89
  } catch (err: unknown) {
128
90
  expect(err).toBeInstanceOf(ValidationError);
@@ -144,7 +106,7 @@ merge:
144
106
  };
145
107
 
146
108
  try {
147
- await mergeCommand(["--branch", branchName, "--dry-run"]);
109
+ await mergeCommand({ branch: branchName, dryRun: true });
148
110
  } finally {
149
111
  process.stdout.write = originalWrite;
150
112
  }
@@ -170,7 +132,7 @@ merge:
170
132
  };
171
133
 
172
134
  try {
173
- await mergeCommand(["--branch", branchName, "--dry-run", "--json"]);
135
+ await mergeCommand({ branch: branchName, dryRun: true, json: true });
174
136
  } finally {
175
137
  process.stdout.write = originalWrite;
176
138
  }
@@ -191,7 +153,7 @@ merge:
191
153
  };
192
154
 
193
155
  try {
194
- await mergeCommand(["--branch", branchName]);
156
+ await mergeCommand({ branch: branchName });
195
157
  } finally {
196
158
  process.stdout.write = originalWrite;
197
159
  }
@@ -215,7 +177,7 @@ merge:
215
177
  };
216
178
 
217
179
  try {
218
- await mergeCommand(["--branch", branchName, "--json"]);
180
+ await mergeCommand({ branch: branchName, json: true });
219
181
  } finally {
220
182
  process.stdout.write = originalWrite;
221
183
  }
@@ -238,14 +200,14 @@ merge:
238
200
  };
239
201
 
240
202
  try {
241
- await mergeCommand(["--branch", branchName, "--dry-run", "--json"]);
203
+ await mergeCommand({ branch: branchName, dryRun: true, json: true });
242
204
  } finally {
243
205
  process.stdout.write = originalWrite;
244
206
  }
245
207
 
246
208
  const parsed = JSON.parse(output);
247
209
  expect(parsed.agentName).toBe("my-builder");
248
- expect(parsed.beadId).toBe("bead-xyz");
210
+ expect(parsed.taskId).toBe("bead-xyz");
249
211
  });
250
212
  });
251
213
 
@@ -261,7 +223,7 @@ merge:
261
223
  };
262
224
 
263
225
  try {
264
- await mergeCommand(["--all"]);
226
+ await mergeCommand({ all: true });
265
227
  } finally {
266
228
  process.stdout.write = originalWrite;
267
229
  }
@@ -280,7 +242,7 @@ merge:
280
242
  };
281
243
 
282
244
  try {
283
- await mergeCommand(["--all", "--json"]);
245
+ await mergeCommand({ all: true, json: true });
284
246
  } finally {
285
247
  process.stdout.write = originalWrite;
286
248
  }
@@ -302,13 +264,13 @@ merge:
302
264
  const queue = createMergeQueue(queuePath);
303
265
  queue.enqueue({
304
266
  branchName: branch1,
305
- beadId: "bead-001",
267
+ taskId: "bead-001",
306
268
  agentName: "agent1",
307
269
  filesModified: [`src/${branch1}.ts`],
308
270
  });
309
271
  queue.enqueue({
310
272
  branchName: branch2,
311
- beadId: "bead-002",
273
+ taskId: "bead-002",
312
274
  agentName: "agent2",
313
275
  filesModified: [`src/${branch2}.ts`],
314
276
  });
@@ -322,7 +284,7 @@ merge:
322
284
  };
323
285
 
324
286
  try {
325
- await mergeCommand(["--all", "--dry-run"]);
287
+ await mergeCommand({ all: true, dryRun: true });
326
288
  } finally {
327
289
  process.stdout.write = originalWrite;
328
290
  }
@@ -344,13 +306,13 @@ merge:
344
306
  const queue = createMergeQueue(queuePath);
345
307
  queue.enqueue({
346
308
  branchName: branch1,
347
- beadId: "bead-100",
309
+ taskId: "bead-100",
348
310
  agentName: "builder1",
349
311
  filesModified: [`src/${branch1}.ts`],
350
312
  });
351
313
  queue.enqueue({
352
314
  branchName: branch2,
353
- beadId: "bead-200",
315
+ taskId: "bead-200",
354
316
  agentName: "builder2",
355
317
  filesModified: [`src/${branch2}.ts`],
356
318
  });
@@ -364,7 +326,7 @@ merge:
364
326
  };
365
327
 
366
328
  try {
367
- await mergeCommand(["--all"]);
329
+ await mergeCommand({ all: true });
368
330
  } finally {
369
331
  process.stdout.write = originalWrite;
370
332
  }
@@ -389,7 +351,7 @@ merge:
389
351
  const queue = createMergeQueue(queuePath);
390
352
  queue.enqueue({
391
353
  branchName: branch1,
392
- beadId: "bead-300",
354
+ taskId: "bead-300",
393
355
  agentName: "builder3",
394
356
  filesModified: [`src/${branch1}.ts`],
395
357
  });
@@ -403,7 +365,7 @@ merge:
403
365
  };
404
366
 
405
367
  try {
406
- await mergeCommand(["--all", "--json"]);
368
+ await mergeCommand({ all: true, json: true });
407
369
  } finally {
408
370
  process.stdout.write = originalWrite;
409
371
  }
@@ -439,7 +401,7 @@ merge:
439
401
  };
440
402
 
441
403
  try {
442
- await mergeCommand(["--branch", branchName, "--into", "develop", "--json"]);
404
+ await mergeCommand({ branch: branchName, into: "develop", json: true });
443
405
  } finally {
444
406
  process.stdout.write = originalWrite;
445
407
  }
@@ -487,13 +449,13 @@ merge:
487
449
  const queue = createMergeQueue(queuePath);
488
450
  queue.enqueue({
489
451
  branchName: branch1,
490
- beadId: "bead-into-all-1",
452
+ taskId: "bead-into-all-1",
491
453
  agentName: "agent1",
492
454
  filesModified: [`src/${branch1}.ts`],
493
455
  });
494
456
  queue.enqueue({
495
457
  branchName: branch2,
496
- beadId: "bead-into-all-2",
458
+ taskId: "bead-into-all-2",
497
459
  agentName: "agent2",
498
460
  filesModified: [`src/${branch2}.ts`],
499
461
  });
@@ -507,7 +469,7 @@ merge:
507
469
  };
508
470
 
509
471
  try {
510
- await mergeCommand(["--all", "--into", "staging", "--json"]);
472
+ await mergeCommand({ all: true, into: "staging", json: true });
511
473
  } finally {
512
474
  process.stdout.write = originalWrite;
513
475
  }
@@ -534,7 +496,7 @@ merge:
534
496
  };
535
497
 
536
498
  try {
537
- await mergeCommand(["--branch", branchName, "--json"]);
499
+ await mergeCommand({ branch: branchName, json: true });
538
500
  } finally {
539
501
  process.stdout.write = originalWrite;
540
502
  }
@@ -574,7 +536,7 @@ merge:
574
536
 
575
537
  try {
576
538
  // No --into flag — should read session-branch.txt
577
- await mergeCommand(["--branch", branchName, "--json"]);
539
+ await mergeCommand({ branch: branchName, json: true });
578
540
  } finally {
579
541
  process.stdout.write = originalWrite;
580
542
  }
@@ -621,7 +583,7 @@ merge:
621
583
 
622
584
  try {
623
585
  // --into overrides session-branch.txt
624
- await mergeCommand(["--branch", branchName, "--into", "explicit-target", "--json"]);
586
+ await mergeCommand({ branch: branchName, into: "explicit-target", json: true });
625
587
  } finally {
626
588
  process.stdout.write = originalWrite;
627
589
  }
@@ -657,7 +619,7 @@ merge:
657
619
  };
658
620
 
659
621
  try {
660
- await mergeCommand(["--branch", branchName]);
622
+ await mergeCommand({ branch: branchName });
661
623
  } finally {
662
624
  process.stdout.write = originalWrite;
663
625
  }