@os-eco/overstory-cli 0.9.2 → 0.9.3

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.
@@ -102,13 +102,13 @@ describe("createSession", () => {
102
102
  const tmuxCallArgs = spawnSpy.mock.calls[1] as unknown[];
103
103
  const cmd = tmuxCallArgs[0] as string[];
104
104
  expect(cmd[0]).toBe("tmux");
105
- expect(cmd[1]).toBe("new-session");
106
- expect(cmd[3]).toBe("-s");
107
- expect(cmd[4]).toBe("my-session");
108
- expect(cmd[5]).toBe("-c");
109
- expect(cmd[6]).toBe("/work/dir");
105
+ expect(cmd[3]).toBe("new-session");
106
+ expect(cmd[5]).toBe("-s");
107
+ expect(cmd[6]).toBe("my-session");
108
+ expect(cmd[7]).toBe("-c");
109
+ expect(cmd[8]).toBe("/work/dir");
110
110
  // The command should be wrapped with PATH export
111
- const wrappedCmd = cmd[7] as string;
111
+ const wrappedCmd = cmd[9] as string;
112
112
  expect(wrappedCmd).toContain("echo hello");
113
113
  expect(wrappedCmd).toContain("export PATH=");
114
114
 
@@ -136,7 +136,16 @@ describe("createSession", () => {
136
136
  expect(spawnSpy).toHaveBeenCalledTimes(3);
137
137
  const thirdCallArgs = spawnSpy.mock.calls[2] as unknown[];
138
138
  const cmd = thirdCallArgs[0] as string[];
139
- expect(cmd).toEqual(["tmux", "list-panes", "-t", "test-agent", "-F", "#{pane_pid}"]);
139
+ expect(cmd).toEqual([
140
+ "tmux",
141
+ "-L",
142
+ "overstory",
143
+ "list-panes",
144
+ "-t",
145
+ "test-agent",
146
+ "-F",
147
+ "#{pane_pid}",
148
+ ]);
140
149
  });
141
150
 
142
151
  test("throws AgentError if session creation fails", async () => {
@@ -239,7 +248,7 @@ describe("createSession", () => {
239
248
  // Call 0: which ov, Call 1: which overstory, Call 2: tmux new-session
240
249
  const tmuxCallArgs = spawnSpy.mock.calls[2] as unknown[];
241
250
  const cmd = tmuxCallArgs[0] as string[];
242
- const tmuxCmd = cmd[7] as string;
251
+ const tmuxCmd = cmd[9] as string;
243
252
  expect(tmuxCmd).toContain("echo test");
244
253
  });
245
254
 
@@ -360,7 +369,14 @@ describe("listSessions", () => {
360
369
  expect(spawnSpy).toHaveBeenCalledTimes(1);
361
370
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
362
371
  const cmd = callArgs[0] as string[];
363
- expect(cmd).toEqual(["tmux", "list-sessions", "-F", "#{session_name}:#{pid}"]);
372
+ expect(cmd).toEqual([
373
+ "tmux",
374
+ "-L",
375
+ "overstory",
376
+ "list-sessions",
377
+ "-F",
378
+ "#{session_name}:#{pid}",
379
+ ]);
364
380
  });
365
381
  });
366
382
 
@@ -383,7 +399,16 @@ describe("getPanePid", () => {
383
399
  expect(pid).toBe(42);
384
400
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
385
401
  const cmd = callArgs[0] as string[];
386
- expect(cmd).toEqual(["tmux", "display-message", "-p", "-t", "overstory-auth", "#{pane_pid}"]);
402
+ expect(cmd).toEqual([
403
+ "tmux",
404
+ "-L",
405
+ "overstory",
406
+ "display-message",
407
+ "-p",
408
+ "-t",
409
+ "overstory-auth",
410
+ "#{pane_pid}",
411
+ ]);
387
412
  });
388
413
 
389
414
  test("returns null when session does not exist", async () => {
@@ -679,7 +704,7 @@ describe("killSession", () => {
679
704
  const cmd = args[0] as string[];
680
705
  cmds.push(cmd);
681
706
 
682
- if (cmd[0] === "tmux" && cmd[1] === "display-message") {
707
+ if (cmd[0] === "tmux" && cmd[3] === "display-message") {
683
708
  // getPanePid → returns PID 500
684
709
  return mockSpawnResult("500\n", "", 0);
685
710
  }
@@ -687,7 +712,7 @@ describe("killSession", () => {
687
712
  // getDescendantPids → no children
688
713
  return mockSpawnResult("", "", 1);
689
714
  }
690
- if (cmd[0] === "tmux" && cmd[1] === "kill-session") {
715
+ if (cmd[0] === "tmux" && cmd[3] === "kill-session") {
691
716
  return mockSpawnResult("", "", 0);
692
717
  }
693
718
  return mockSpawnResult("", "", 0);
@@ -700,6 +725,8 @@ describe("killSession", () => {
700
725
  // Should have called: tmux display-message, pgrep, tmux kill-session
701
726
  expect(cmds[0]).toEqual([
702
727
  "tmux",
728
+ "-L",
729
+ "overstory",
703
730
  "display-message",
704
731
  "-p",
705
732
  "-t",
@@ -708,7 +735,7 @@ describe("killSession", () => {
708
735
  ]);
709
736
  expect(cmds[1]).toEqual(["pgrep", "-P", "500"]);
710
737
  const lastCmd = cmds[cmds.length - 1];
711
- expect(lastCmd).toEqual(["tmux", "kill-session", "-t", "overstory-auth"]);
738
+ expect(lastCmd).toEqual(["tmux", "-L", "overstory", "kill-session", "-t", "overstory-auth"]);
712
739
 
713
740
  // Should have sent SIGTERM to root PID 500
714
741
  expect(killSpy).toHaveBeenCalledWith(500, "SIGTERM");
@@ -720,11 +747,11 @@ describe("killSession", () => {
720
747
  const cmd = args[0] as string[];
721
748
  cmds.push(cmd);
722
749
 
723
- if (cmd[0] === "tmux" && cmd[1] === "display-message") {
750
+ if (cmd[0] === "tmux" && cmd[3] === "display-message") {
724
751
  // getPanePid → session not found
725
752
  return mockSpawnResult("", "can't find session", 1);
726
753
  }
727
- if (cmd[0] === "tmux" && cmd[1] === "kill-session") {
754
+ if (cmd[0] === "tmux" && cmd[3] === "kill-session") {
728
755
  return mockSpawnResult("", "", 0);
729
756
  }
730
757
  return mockSpawnResult("", "", 0);
@@ -734,8 +761,8 @@ describe("killSession", () => {
734
761
 
735
762
  // Should go straight to tmux kill-session (no pgrep calls)
736
763
  expect(cmds).toHaveLength(2);
737
- expect(cmds[0]?.[1]).toBe("display-message");
738
- expect(cmds[1]?.[1]).toBe("kill-session");
764
+ expect(cmds[0]?.[3]).toBe("display-message");
765
+ expect(cmds[1]?.[3]).toBe("kill-session");
739
766
  // No process.kill calls since we had no PID
740
767
  expect(killSpy).not.toHaveBeenCalled();
741
768
  });
@@ -743,13 +770,13 @@ describe("killSession", () => {
743
770
  test("succeeds silently when session is already gone after process cleanup", async () => {
744
771
  spawnSpy.mockImplementation((...args: unknown[]) => {
745
772
  const cmd = args[0] as string[];
746
- if (cmd[0] === "tmux" && cmd[1] === "display-message") {
773
+ if (cmd[0] === "tmux" && cmd[3] === "display-message") {
747
774
  return mockSpawnResult("500\n", "", 0);
748
775
  }
749
776
  if (cmd[0] === "pgrep") {
750
777
  return mockSpawnResult("", "", 1);
751
778
  }
752
- if (cmd[0] === "tmux" && cmd[1] === "kill-session") {
779
+ if (cmd[0] === "tmux" && cmd[3] === "kill-session") {
753
780
  // Session already gone after process cleanup
754
781
  return mockSpawnResult("", "can't find session: overstory-auth", 1);
755
782
  }
@@ -765,10 +792,10 @@ describe("killSession", () => {
765
792
  test("throws AgentError on unexpected tmux kill-session failure", async () => {
766
793
  spawnSpy.mockImplementation((...args: unknown[]) => {
767
794
  const cmd = args[0] as string[];
768
- if (cmd[0] === "tmux" && cmd[1] === "display-message") {
795
+ if (cmd[0] === "tmux" && cmd[3] === "display-message") {
769
796
  return mockSpawnResult("", "can't find session", 1);
770
797
  }
771
- if (cmd[0] === "tmux" && cmd[1] === "kill-session") {
798
+ if (cmd[0] === "tmux" && cmd[3] === "kill-session") {
772
799
  return mockSpawnResult("", "server exited unexpectedly", 1);
773
800
  }
774
801
  return mockSpawnResult("", "", 0);
@@ -780,10 +807,10 @@ describe("killSession", () => {
780
807
  test("AgentError contains session name on failure", async () => {
781
808
  spawnSpy.mockImplementation((...args: unknown[]) => {
782
809
  const cmd = args[0] as string[];
783
- if (cmd[0] === "tmux" && cmd[1] === "display-message") {
810
+ if (cmd[0] === "tmux" && cmd[3] === "display-message") {
784
811
  return mockSpawnResult("", "error", 1);
785
812
  }
786
- if (cmd[0] === "tmux" && cmd[1] === "kill-session") {
813
+ if (cmd[0] === "tmux" && cmd[3] === "kill-session") {
787
814
  return mockSpawnResult("", "server exited unexpectedly", 1);
788
815
  }
789
816
  return mockSpawnResult("", "", 0);
@@ -836,7 +863,7 @@ describe("isSessionAlive", () => {
836
863
  expect(spawnSpy).toHaveBeenCalledTimes(1);
837
864
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
838
865
  const cmd = callArgs[0] as string[];
839
- expect(cmd).toEqual(["tmux", "has-session", "-t", "my-agent"]);
866
+ expect(cmd).toEqual(["tmux", "-L", "overstory", "has-session", "-t", "my-agent"]);
840
867
  });
841
868
  });
842
869
 
@@ -907,7 +934,16 @@ describe("sendKeys", () => {
907
934
  expect(spawnSpy).toHaveBeenCalledTimes(1);
908
935
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
909
936
  const cmd = callArgs[0] as string[];
910
- expect(cmd).toEqual(["tmux", "send-keys", "-t", "overstory-auth", "echo hello world", "Enter"]);
937
+ expect(cmd).toEqual([
938
+ "tmux",
939
+ "-L",
940
+ "overstory",
941
+ "send-keys",
942
+ "-t",
943
+ "overstory-auth",
944
+ "echo hello world",
945
+ "Enter",
946
+ ]);
911
947
  });
912
948
 
913
949
  test("flattens newlines in keys to spaces", async () => {
@@ -920,6 +956,8 @@ describe("sendKeys", () => {
920
956
  const cmd = callArgs[0] as string[];
921
957
  expect(cmd).toEqual([
922
958
  "tmux",
959
+ "-L",
960
+ "overstory",
923
961
  "send-keys",
924
962
  "-t",
925
963
  "overstory-agent",
@@ -956,7 +994,16 @@ describe("sendKeys", () => {
956
994
  expect(spawnSpy).toHaveBeenCalledTimes(1);
957
995
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
958
996
  const cmd = callArgs[0] as string[];
959
- expect(cmd).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "", "Enter"]);
997
+ expect(cmd).toEqual([
998
+ "tmux",
999
+ "-L",
1000
+ "overstory",
1001
+ "send-keys",
1002
+ "-t",
1003
+ "overstory-agent",
1004
+ "",
1005
+ "Enter",
1006
+ ]);
960
1007
  });
961
1008
 
962
1009
  test("throws descriptive error when tmux server is not running", async () => {
@@ -1039,7 +1086,17 @@ describe("capturePaneContent", () => {
1039
1086
 
1040
1087
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
1041
1088
  const cmd = callArgs[0] as string[];
1042
- expect(cmd).toEqual(["tmux", "capture-pane", "-t", "my-session", "-p", "-S", "-100"]);
1089
+ expect(cmd).toEqual([
1090
+ "tmux",
1091
+ "-L",
1092
+ "overstory",
1093
+ "capture-pane",
1094
+ "-t",
1095
+ "my-session",
1096
+ "-p",
1097
+ "-S",
1098
+ "-100",
1099
+ ]);
1043
1100
  });
1044
1101
 
1045
1102
  test("uses default 50 lines when not specified", async () => {
@@ -1049,7 +1106,7 @@ describe("capturePaneContent", () => {
1049
1106
 
1050
1107
  const callArgs = spawnSpy.mock.calls[0] as unknown[];
1051
1108
  const cmd = callArgs[0] as string[];
1052
- expect(cmd[6]).toBe("-50");
1109
+ expect(cmd[8]).toBe("-50");
1053
1110
  });
1054
1111
 
1055
1112
  test("returns null when capture-pane fails", async () => {
@@ -1122,7 +1179,7 @@ describe("waitForTuiReady", () => {
1122
1179
  let captureCallCount = 0;
1123
1180
  spawnSpy.mockImplementation((...args: unknown[]) => {
1124
1181
  const cmd = args[0] as string[];
1125
- if (cmd[1] === "capture-pane") {
1182
+ if (cmd[3] === "capture-pane") {
1126
1183
  captureCallCount++;
1127
1184
  if (captureCallCount <= 3) {
1128
1185
  // First 3 capture-pane polls: empty pane (TUI still loading)
@@ -1174,7 +1231,7 @@ describe("waitForTuiReady", () => {
1174
1231
  // capture-pane fails (session dead), has-session also fails (session dead)
1175
1232
  spawnSpy.mockImplementation((...args: unknown[]) => {
1176
1233
  const cmd = args[0] as string[];
1177
- if (cmd[1] === "capture-pane") {
1234
+ if (cmd[3] === "capture-pane") {
1178
1235
  return mockSpawnResult("", "can't find session", 1);
1179
1236
  }
1180
1237
  // has-session: session is dead
@@ -1192,7 +1249,7 @@ describe("waitForTuiReady", () => {
1192
1249
  let captureCallCount = 0;
1193
1250
  spawnSpy.mockImplementation((...args: unknown[]) => {
1194
1251
  const cmd = args[0] as string[];
1195
- if (cmd[1] === "capture-pane") {
1252
+ if (cmd[3] === "capture-pane") {
1196
1253
  captureCallCount++;
1197
1254
  // Pane stays empty for all polls (session alive but TUI not rendered yet)
1198
1255
  return mockSpawnResult("", "", 0);
@@ -1214,7 +1271,7 @@ describe("waitForTuiReady", () => {
1214
1271
  // Pane always shows prompt indicator but never shows status bar text
1215
1272
  spawnSpy.mockImplementation((...args: unknown[]) => {
1216
1273
  const cmd = args[0] as string[];
1217
- if (cmd[1] === "capture-pane") {
1274
+ if (cmd[3] === "capture-pane") {
1218
1275
  return mockSpawnResult("Welcome to Claude Code!\n\u276f", "", 0);
1219
1276
  }
1220
1277
  // has-session: session is alive
@@ -1230,7 +1287,7 @@ describe("waitForTuiReady", () => {
1230
1287
  // Pane always shows status bar but never shows prompt indicator
1231
1288
  spawnSpy.mockImplementation((...args: unknown[]) => {
1232
1289
  const cmd = args[0] as string[];
1233
- if (cmd[1] === "capture-pane") {
1290
+ if (cmd[3] === "capture-pane") {
1234
1291
  return mockSpawnResult("bypass permissions", "", 0);
1235
1292
  }
1236
1293
  // has-session: session is alive
@@ -1246,7 +1303,7 @@ describe("waitForTuiReady", () => {
1246
1303
  let captureCallCount = 0;
1247
1304
  spawnSpy.mockImplementation((...args: unknown[]) => {
1248
1305
  const cmd = args[0] as string[];
1249
- if (cmd[1] === "capture-pane") {
1306
+ if (cmd[3] === "capture-pane") {
1250
1307
  captureCallCount++;
1251
1308
  if (captureCallCount <= 2) {
1252
1309
  // First 2 polls: only prompt indicator visible (phase 1 only)
@@ -1271,7 +1328,7 @@ describe("waitForTuiReady", () => {
1271
1328
  let captureCallCount = 0;
1272
1329
  spawnSpy.mockImplementation((...args: unknown[]) => {
1273
1330
  const cmd = args[0] as string[];
1274
- if (cmd[1] === "capture-pane") {
1331
+ if (cmd[3] === "capture-pane") {
1275
1332
  captureCallCount++;
1276
1333
  if (captureCallCount === 1) {
1277
1334
  // First poll: trust dialog is showing
@@ -1280,7 +1337,7 @@ describe("waitForTuiReady", () => {
1280
1337
  // Subsequent polls: trust confirmed, real TUI with both indicators
1281
1338
  return mockSpawnResult('Try "help"\nshift+tab', "", 0);
1282
1339
  }
1283
- if (cmd[1] === "send-keys") {
1340
+ if (cmd[3] === "send-keys") {
1284
1341
  sendKeysCalls.push(cmd);
1285
1342
  return mockSpawnResult("", "", 0);
1286
1343
  }
@@ -1294,7 +1351,16 @@ describe("waitForTuiReady", () => {
1294
1351
  // sendKeys should have been called once to confirm the trust dialog
1295
1352
  expect(sendKeysCalls).toHaveLength(1);
1296
1353
  const trustCall = sendKeysCalls[0];
1297
- expect(trustCall).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "", "Enter"]);
1354
+ expect(trustCall).toEqual([
1355
+ "tmux",
1356
+ "-L",
1357
+ "overstory",
1358
+ "send-keys",
1359
+ "-t",
1360
+ "overstory-agent",
1361
+ "",
1362
+ "Enter",
1363
+ ]);
1298
1364
  });
1299
1365
 
1300
1366
  test("detects bypass permissions dialog and types 2 before Enter", async () => {
@@ -1302,7 +1368,7 @@ describe("waitForTuiReady", () => {
1302
1368
  let captureCallCount = 0;
1303
1369
  spawnSpy.mockImplementation((...args: unknown[]) => {
1304
1370
  const cmd = args[0] as string[];
1305
- if (cmd[1] === "capture-pane") {
1371
+ if (cmd[3] === "capture-pane") {
1306
1372
  captureCallCount++;
1307
1373
  if (captureCallCount === 1) {
1308
1374
  return mockSpawnResult(
@@ -1313,7 +1379,7 @@ describe("waitForTuiReady", () => {
1313
1379
  }
1314
1380
  return mockSpawnResult('Try "help"\nshift+tab', "", 0);
1315
1381
  }
1316
- if (cmd[1] === "send-keys") {
1382
+ if (cmd[3] === "send-keys") {
1317
1383
  sendKeysCalls.push(cmd);
1318
1384
  return mockSpawnResult("", "", 0);
1319
1385
  }
@@ -1324,8 +1390,25 @@ describe("waitForTuiReady", () => {
1324
1390
 
1325
1391
  expect(ready).toBe(true);
1326
1392
  expect(sendKeysCalls).toHaveLength(2);
1327
- expect(sendKeysCalls[0]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "2"]);
1328
- expect(sendKeysCalls[1]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "", "Enter"]);
1393
+ expect(sendKeysCalls[0]).toEqual([
1394
+ "tmux",
1395
+ "-L",
1396
+ "overstory",
1397
+ "send-keys",
1398
+ "-t",
1399
+ "overstory-agent",
1400
+ "2",
1401
+ ]);
1402
+ expect(sendKeysCalls[1]).toEqual([
1403
+ "tmux",
1404
+ "-L",
1405
+ "overstory",
1406
+ "send-keys",
1407
+ "-t",
1408
+ "overstory-agent",
1409
+ "",
1410
+ "Enter",
1411
+ ]);
1329
1412
  });
1330
1413
 
1331
1414
  test("retries typed bypass dialog action when the same dialog persists", async () => {
@@ -1333,7 +1416,7 @@ describe("waitForTuiReady", () => {
1333
1416
  let captureCallCount = 0;
1334
1417
  spawnSpy.mockImplementation((...args: unknown[]) => {
1335
1418
  const cmd = args[0] as string[];
1336
- if (cmd[1] === "capture-pane") {
1419
+ if (cmd[3] === "capture-pane") {
1337
1420
  captureCallCount++;
1338
1421
  if (captureCallCount <= 3) {
1339
1422
  return mockSpawnResult(
@@ -1344,7 +1427,7 @@ describe("waitForTuiReady", () => {
1344
1427
  }
1345
1428
  return mockSpawnResult('Try "help"\nshift+tab', "", 0);
1346
1429
  }
1347
- if (cmd[1] === "send-keys") {
1430
+ if (cmd[3] === "send-keys") {
1348
1431
  sendKeysCalls.push(cmd);
1349
1432
  return mockSpawnResult("", "", 0);
1350
1433
  }
@@ -1355,10 +1438,44 @@ describe("waitForTuiReady", () => {
1355
1438
 
1356
1439
  expect(ready).toBe(true);
1357
1440
  expect(sendKeysCalls).toHaveLength(4);
1358
- expect(sendKeysCalls[0]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "2"]);
1359
- expect(sendKeysCalls[1]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "", "Enter"]);
1360
- expect(sendKeysCalls[2]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "2"]);
1361
- expect(sendKeysCalls[3]).toEqual(["tmux", "send-keys", "-t", "overstory-agent", "", "Enter"]);
1441
+ expect(sendKeysCalls[0]).toEqual([
1442
+ "tmux",
1443
+ "-L",
1444
+ "overstory",
1445
+ "send-keys",
1446
+ "-t",
1447
+ "overstory-agent",
1448
+ "2",
1449
+ ]);
1450
+ expect(sendKeysCalls[1]).toEqual([
1451
+ "tmux",
1452
+ "-L",
1453
+ "overstory",
1454
+ "send-keys",
1455
+ "-t",
1456
+ "overstory-agent",
1457
+ "",
1458
+ "Enter",
1459
+ ]);
1460
+ expect(sendKeysCalls[2]).toEqual([
1461
+ "tmux",
1462
+ "-L",
1463
+ "overstory",
1464
+ "send-keys",
1465
+ "-t",
1466
+ "overstory-agent",
1467
+ "2",
1468
+ ]);
1469
+ expect(sendKeysCalls[3]).toEqual([
1470
+ "tmux",
1471
+ "-L",
1472
+ "overstory",
1473
+ "send-keys",
1474
+ "-t",
1475
+ "overstory-agent",
1476
+ "",
1477
+ "Enter",
1478
+ ]);
1362
1479
  });
1363
1480
 
1364
1481
  test("handles trust dialog only once (trustHandled flag)", async () => {
@@ -1366,7 +1483,7 @@ describe("waitForTuiReady", () => {
1366
1483
  let captureCallCount = 0;
1367
1484
  spawnSpy.mockImplementation((...args: unknown[]) => {
1368
1485
  const cmd = args[0] as string[];
1369
- if (cmd[1] === "capture-pane") {
1486
+ if (cmd[3] === "capture-pane") {
1370
1487
  captureCallCount++;
1371
1488
  if (captureCallCount <= 3) {
1372
1489
  // Multiple polls still show trust dialog (slow dialog dismissal)
@@ -1375,7 +1492,7 @@ describe("waitForTuiReady", () => {
1375
1492
  // Eventually TUI loads with both indicators
1376
1493
  return mockSpawnResult('Try "help"\nbypass permissions', "", 0);
1377
1494
  }
1378
- if (cmd[1] === "send-keys") {
1495
+ if (cmd[3] === "send-keys") {
1379
1496
  sendKeysCalls.push(cmd);
1380
1497
  return mockSpawnResult("", "", 0);
1381
1498
  }
@@ -11,6 +11,24 @@ import { dirname, resolve } from "node:path";
11
11
  import { AgentError } from "../errors.ts";
12
12
  import type { ReadyState } from "../runtimes/types.ts";
13
13
 
14
+ /**
15
+ * Dedicated tmux server socket name for agent session isolation.
16
+ *
17
+ * All overstory agent sessions use `tmux -L overstory` so they run on a
18
+ * separate server from the user's personal tmux. This prevents user tmux
19
+ * config (themes, plugins, keybindings) from interfering with agent spawn.
20
+ * See GitHub #93.
21
+ */
22
+ export const TMUX_SOCKET = "overstory";
23
+
24
+ /**
25
+ * Build a tmux command array with the dedicated server socket.
26
+ * All agent session operations should use this to ensure isolation.
27
+ */
28
+ function tmuxCmd(...args: string[]): string[] {
29
+ return ["tmux", "-L", TMUX_SOCKET, ...args];
30
+ }
31
+
14
32
  /**
15
33
  * Detect the directory containing the overstory binary.
16
34
  *
@@ -123,7 +141,7 @@ export async function createSession(
123
141
  exports.length > 0 ? `/bin/bash -c '${startupScript.replace(/'/g, "'\\''")}'` : command;
124
142
 
125
143
  const { exitCode, stderr } = await runCommand(
126
- ["tmux", "new-session", "-d", "-s", name, "-c", cwd, wrappedCommand],
144
+ tmuxCmd("new-session", "-d", "-s", name, "-c", cwd, wrappedCommand),
127
145
  cwd,
128
146
  );
129
147
 
@@ -138,7 +156,7 @@ export async function createSession(
138
156
  // the session exists but the pane hasn't been registered yet (#73).
139
157
  let pidResult: { stdout: string; stderr: string; exitCode: number } | undefined;
140
158
  for (let attempt = 0; attempt < maxRetries; attempt++) {
141
- pidResult = await runCommand(["tmux", "list-panes", "-t", name, "-F", "#{pane_pid}"]);
159
+ pidResult = await runCommand(tmuxCmd("list-panes", "-t", name, "-F", "#{pane_pid}"));
142
160
  if (pidResult.exitCode === 0) break;
143
161
  await Bun.sleep(250 * (attempt + 1));
144
162
  }
@@ -170,12 +188,9 @@ export async function createSession(
170
188
  * @throws AgentError if tmux is not installed
171
189
  */
172
190
  export async function listSessions(): Promise<Array<{ name: string; pid: number }>> {
173
- const { exitCode, stdout, stderr } = await runCommand([
174
- "tmux",
175
- "list-sessions",
176
- "-F",
177
- "#{session_name}:#{pid}",
178
- ]);
191
+ const { exitCode, stdout, stderr } = await runCommand(
192
+ tmuxCmd("list-sessions", "-F", "#{session_name}:#{pid}"),
193
+ );
179
194
 
180
195
  // Exit code 1 with "no server running" means no sessions exist — not an error
181
196
  if (exitCode !== 0) {
@@ -219,14 +234,9 @@ const KILL_GRACE_PERIOD_MS = 2000;
219
234
  * the session doesn't exist or the PID can't be determined
220
235
  */
221
236
  export async function getPanePid(name: string): Promise<number | null> {
222
- const { exitCode, stdout } = await runCommand([
223
- "tmux",
224
- "display-message",
225
- "-p",
226
- "-t",
227
- name,
228
- "#{pane_pid}",
229
- ]);
237
+ const { exitCode, stdout } = await runCommand(
238
+ tmuxCmd("display-message", "-p", "-t", name, "#{pane_pid}"),
239
+ );
230
240
 
231
241
  if (exitCode !== 0) {
232
242
  return null;
@@ -383,7 +393,7 @@ export async function killSession(name: string): Promise<void> {
383
393
  }
384
394
 
385
395
  // Step 3: Kill the tmux session itself
386
- const { exitCode, stderr } = await runCommand(["tmux", "kill-session", "-t", name]);
396
+ const { exitCode, stderr } = await runCommand(tmuxCmd("kill-session", "-t", name));
387
397
 
388
398
  if (exitCode !== 0) {
389
399
  // If the session is already gone (e.g., died during process cleanup), that's fine
@@ -427,7 +437,7 @@ export async function getCurrentSessionName(): Promise<string | null> {
427
437
  * @returns true if the session exists, false otherwise
428
438
  */
429
439
  export async function isSessionAlive(name: string): Promise<boolean> {
430
- const { exitCode } = await runCommand(["tmux", "has-session", "-t", name]);
440
+ const { exitCode } = await runCommand(tmuxCmd("has-session", "-t", name));
431
441
  return exitCode === 0;
432
442
  }
433
443
 
@@ -456,7 +466,7 @@ export type SessionState = "alive" | "dead" | "no_server";
456
466
  * @returns The session state
457
467
  */
458
468
  export async function checkSessionState(name: string): Promise<SessionState> {
459
- const { exitCode, stderr } = await runCommand(["tmux", "has-session", "-t", name]);
469
+ const { exitCode, stderr } = await runCommand(tmuxCmd("has-session", "-t", name));
460
470
  if (exitCode === 0) return "alive";
461
471
  if (stderr.includes("no server running") || stderr.includes("no sessions")) {
462
472
  return "no_server";
@@ -472,15 +482,9 @@ export async function checkSessionState(name: string): Promise<SessionState> {
472
482
  * @returns The trimmed pane content, or null if capture fails
473
483
  */
474
484
  export async function capturePaneContent(name: string, lines = 50): Promise<string | null> {
475
- const { exitCode, stdout } = await runCommand([
476
- "tmux",
477
- "capture-pane",
478
- "-t",
479
- name,
480
- "-p",
481
- "-S",
482
- `-${lines}`,
483
- ]);
485
+ const { exitCode, stdout } = await runCommand(
486
+ tmuxCmd("capture-pane", "-t", name, "-p", "-S", `-${lines}`),
487
+ );
484
488
  if (exitCode !== 0) {
485
489
  return null;
486
490
  }
@@ -584,14 +588,9 @@ export async function sendKeys(name: string, keys: string, maxRetries = 3): Prom
584
588
  const flatKeys = keys.replace(/\n/g, " ");
585
589
 
586
590
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
587
- const { exitCode, stderr } = await runCommand([
588
- "tmux",
589
- "send-keys",
590
- "-t",
591
- name,
592
- flatKeys,
593
- "Enter",
594
- ]);
591
+ const { exitCode, stderr } = await runCommand(
592
+ tmuxCmd("send-keys", "-t", name, flatKeys, "Enter"),
593
+ );
595
594
 
596
595
  if (exitCode === 0) {
597
596
  return;
@@ -640,7 +639,7 @@ export async function sendKeys(name: string, keys: string, maxRetries = 3): Prom
640
639
 
641
640
  async function sendRawKeys(name: string, keys: string): Promise<void> {
642
641
  const flatKeys = keys.replace(/\n/g, " ");
643
- const { exitCode, stderr } = await runCommand(["tmux", "send-keys", "-t", name, flatKeys]);
642
+ const { exitCode, stderr } = await runCommand(tmuxCmd("send-keys", "-t", name, flatKeys));
644
643
 
645
644
  if (exitCode !== 0) {
646
645
  const trimmedStderr = stderr.trim();
@@ -0,0 +1,13 @@
1
+ {
2
+ "_comment": "Copilot CLI lifecycle hooks — schema reverse-engineered from copilot CLI behavior. Key differences from Claude Code: event names are camelCase (onSessionStart, onToolUse, onError), no matcher field, hook entries are { command } only (no type field).",
3
+ "hooks": {
4
+ "onSessionStart": [
5
+ {
6
+ "command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov prime --agent {{AGENT_NAME}}"
7
+ },
8
+ {
9
+ "command": "[ -z \"$OVERSTORY_AGENT_NAME\" ] && exit 0; ov mail check --inject --agent {{AGENT_NAME}}"
10
+ }
11
+ ]
12
+ }
13
+ }