tmux-team 3.0.1 → 3.2.0

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.
@@ -10,6 +10,13 @@ import type { Context, Tmux, UI, Paths, ResolvedConfig, Flags } from '../types.j
10
10
  import { ExitCodes } from '../exits.js';
11
11
  import { cmdTalk } from './talk.js';
12
12
 
13
+ // ─────────────────────────────────────────────────────────────
14
+ // Constants
15
+ // ─────────────────────────────────────────────────────────────
16
+
17
+ // Regex to match new end marker format
18
+ const END_MARKER_REGEX = /---RESPONSE-END-([a-f0-9]+)---/;
19
+
13
20
  // ─────────────────────────────────────────────────────────────
14
21
  // Test utilities
15
22
  // ─────────────────────────────────────────────────────────────
@@ -27,6 +34,12 @@ function createMockTmux(): Tmux & {
27
34
  capture(_pane: string, _lines: number) {
28
35
  return mock.captureReturn;
29
36
  },
37
+ listPanes() {
38
+ return [];
39
+ },
40
+ getCurrentPaneId() {
41
+ return null;
42
+ },
30
43
  };
31
44
  return mock;
32
45
  }
@@ -445,8 +458,10 @@ describe('cmdTalk - --wait mode', () => {
445
458
 
446
459
  // Helper: generate mock capture output with proper marker structure
447
460
  // The end marker must appear TWICE: once in instruction, once from "agent"
461
+ // New format: ---RESPONSE-END-NONCE---
448
462
  function mockCompleteResponse(nonce: string, response: string): string {
449
- return `{tmux-team-start:${nonce}}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
463
+ const endMarker = `---RESPONSE-END-${nonce}---`;
464
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
450
465
  }
451
466
 
452
467
  it('appends nonce instruction to message', async () => {
@@ -458,7 +473,7 @@ describe('cmdTalk - --wait mode', () => {
458
473
  if (captureCount === 1) return ''; // Baseline
459
474
  // Return marker on second capture - must include instruction AND agent's end marker
460
475
  const sent = tmux.sends[0]?.message || '';
461
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
476
+ const match = sent.match(END_MARKER_REGEX);
462
477
  return match ? mockCompleteResponse(match[1], 'Response here') : '';
463
478
  };
464
479
 
@@ -479,10 +494,8 @@ describe('cmdTalk - --wait mode', () => {
479
494
  await cmdTalk(ctx, 'claude', 'Hello');
480
495
 
481
496
  expect(tmux.sends).toHaveLength(1);
482
- expect(tmux.sends[0].message).toContain(
483
- '[IMPORTANT: When your response is complete, print exactly:'
484
- );
485
- expect(tmux.sends[0].message).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
497
+ expect(tmux.sends[0].message).toContain('When you finish responding, print this exact line:');
498
+ expect(tmux.sends[0].message).toMatch(/---RESPONSE-END-[a-f0-9]+---/);
486
499
  });
487
500
 
488
501
  it('detects nonce marker and extracts response', async () => {
@@ -496,7 +509,7 @@ describe('cmdTalk - --wait mode', () => {
496
509
  if (captureCount === 1) return 'baseline content';
497
510
  // Extract nonce from sent message and return matching marker
498
511
  const sent = tmux.sends[0]?.message || '';
499
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
512
+ const match = sent.match(END_MARKER_REGEX);
500
513
  if (match) {
501
514
  return mockCompleteResponse(match[1], 'Agent response here');
502
515
  }
@@ -562,7 +575,7 @@ describe('cmdTalk - --wait mode', () => {
562
575
  expect(output.error).toContain('Timed out');
563
576
  });
564
577
 
565
- it('isolates response using start/end markers in scrollback', async () => {
578
+ it('isolates response using end markers in scrollback', async () => {
566
579
  const tmux = createMockTmux();
567
580
  const ui = createMockUI();
568
581
 
@@ -571,13 +584,11 @@ describe('cmdTalk - --wait mode', () => {
571
584
  tmux.capture = () => {
572
585
  // Simulate scrollback with old content, then our instruction (with end marker), response, and agent's end marker
573
586
  const sent = tmux.sends[0]?.message || '';
574
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
575
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
576
- if (startMatch && endMatch) {
577
- // Start and end markers should have the same nonce
578
- expect(startMatch[1]).toBe(endMatch[1]);
587
+ const endMatch = sent.match(END_MARKER_REGEX);
588
+ if (endMatch) {
589
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
579
590
  // Must include end marker TWICE: once in instruction, once from "agent"
580
- return `${oldContent}\n\n{tmux-team-start:${startMatch[1]}}\nMessage content here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response content\n\n{tmux-team-end:${endMatch[1]}}`;
591
+ return `${oldContent}\n\nMessage content here\n\nWhen you finish responding, print this exact line:\n${endMarker}\nNew response content\n\n${endMarker}`;
581
592
  }
582
593
  return oldContent;
583
594
  };
@@ -601,7 +612,7 @@ describe('cmdTalk - --wait mode', () => {
601
612
 
602
613
  const output = ui.jsonOutput[0] as Record<string, unknown>;
603
614
  expect(output.status).toBe('completed');
604
- // Response should NOT include old content before start marker
615
+ // Response should NOT include old content
605
616
  expect(output.response).not.toContain('Previous conversation');
606
617
  expect(output.response).not.toContain('Old content here');
607
618
  // Response should contain the actual response content
@@ -616,7 +627,7 @@ describe('cmdTalk - --wait mode', () => {
616
627
  captureCount++;
617
628
  if (captureCount === 1) return '';
618
629
  const sent = tmux.sends[0]?.message || '';
619
- const match = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
630
+ const match = sent.match(END_MARKER_REGEX);
620
631
  return match ? mockCompleteResponse(match[1], 'Done') : '';
621
632
  };
622
633
 
@@ -684,7 +695,7 @@ describe('cmdTalk - --wait mode', () => {
684
695
 
685
696
  // Mock send to capture the nonce for each pane
686
697
  tmux.send = (pane: string, msg: string) => {
687
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
698
+ const match = msg.match(END_MARKER_REGEX);
688
699
  if (match) {
689
700
  noncesByPane[pane] = match[1];
690
701
  }
@@ -732,7 +743,7 @@ describe('cmdTalk - --wait mode', () => {
732
743
  const noncesByPane: Record<string, string> = {};
733
744
 
734
745
  tmux.send = (pane: string, msg: string) => {
735
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
746
+ const match = msg.match(END_MARKER_REGEX);
736
747
  if (match) {
737
748
  noncesByPane[pane] = match[1];
738
749
  }
@@ -790,7 +801,7 @@ describe('cmdTalk - --wait mode', () => {
790
801
  const nonces: string[] = [];
791
802
 
792
803
  tmux.send = (_pane: string, msg: string) => {
793
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
804
+ const match = msg.match(END_MARKER_REGEX);
794
805
  if (match) {
795
806
  nonces.push(match[1]);
796
807
  }
@@ -832,6 +843,47 @@ describe('cmdTalk - --wait mode', () => {
832
843
  });
833
844
  });
834
845
 
846
+ describe('cmdTalk - errors and JSON output', () => {
847
+ it('errors when target agent is not found', async () => {
848
+ const ctx = createContext({
849
+ config: { paneRegistry: {} },
850
+ });
851
+ await expect(cmdTalk(ctx, 'nope', 'hi')).rejects.toMatchObject({
852
+ exitCode: ExitCodes.PANE_NOT_FOUND,
853
+ });
854
+ expect((ctx.ui as any).errors.join('\n')).toContain("Agent 'nope' not found");
855
+ });
856
+
857
+ it('outputs JSON in non-wait mode', async () => {
858
+ const ctx = createContext({
859
+ flags: { json: true },
860
+ config: { paneRegistry: { claude: { pane: '1.0' } } },
861
+ });
862
+ await cmdTalk(ctx, 'claude', 'hello');
863
+ const out = (ctx.ui as any).jsonOutput[0] as any;
864
+ expect(out).toMatchObject({ target: 'claude', pane: '1.0', status: 'sent' });
865
+ });
866
+
867
+ it('marks failures in broadcast when send throws', async () => {
868
+ const tmux = createMockTmux();
869
+ const sendSpy = vi.spyOn(tmux, 'send').mockImplementationOnce(() => {
870
+ throw new Error('fail');
871
+ });
872
+ const ctx = createContext({
873
+ tmux,
874
+ flags: { json: true },
875
+ config: {
876
+ paneRegistry: { claude: { pane: '1.0' }, codex: { pane: '1.1' } },
877
+ },
878
+ });
879
+
880
+ await cmdTalk(ctx, 'all', 'hello');
881
+ expect(sendSpy).toHaveBeenCalled();
882
+ const out = (ctx.ui as any).jsonOutput[0] as any;
883
+ expect(out.results.some((r: any) => r.status === 'failed')).toBe(true);
884
+ });
885
+ });
886
+
835
887
  describe('cmdTalk - nonce collision handling', () => {
836
888
  let testDir: string;
837
889
 
@@ -850,33 +902,34 @@ describe('cmdTalk - nonce collision handling', () => {
850
902
  const ui = createMockUI();
851
903
 
852
904
  let captureCount = 0;
853
- const oldStartMarker = '{tmux-team-start:0000}';
854
- const oldEndMarker = '{tmux-team-end:0000}'; // Old marker from previous request
905
+ const oldEndMarker = '---RESPONSE-END-0000---'; // Old marker from previous request
855
906
 
856
907
  tmux.capture = () => {
857
908
  captureCount++;
858
909
  // Scrollback includes OLD markers from a previous request
859
910
  if (captureCount === 1) {
860
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
911
+ return `Old question\nOld response\n${oldEndMarker}`;
861
912
  }
862
913
  // New capture still has old markers but new request markers not complete yet
863
914
  if (captureCount === 2) {
864
915
  const sent = tmux.sends[0]?.message || '';
865
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
866
- if (startMatch) {
867
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked`;
916
+ const endMatch = sent.match(END_MARKER_REGEX);
917
+ if (endMatch) {
918
+ const newEndMarker = `---RESPONSE-END-${endMatch[1]}---`;
919
+ // Old content + new instruction (only one occurrence of new marker so far)
920
+ return `Old question\nOld response\n${oldEndMarker}\n\nNew question asked\n\nWhen you finish responding, print this exact line:\n${newEndMarker}`;
868
921
  }
869
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
922
+ return `Old question\nOld response\n${oldEndMarker}`;
870
923
  }
871
924
  // Finally, new end marker appears - must have TWO occurrences of new end marker
872
925
  const sent = tmux.sends[0]?.message || '';
873
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
874
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
875
- if (startMatch && endMatch) {
926
+ const endMatch = sent.match(END_MARKER_REGEX);
927
+ if (endMatch) {
928
+ const newEndMarker = `---RESPONSE-END-${endMatch[1]}---`;
876
929
  // Old markers in scrollback + new instruction (with end marker) + response + agent's end marker
877
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}\n\n{tmux-team-start:${startMatch[1]}}\nNew question asked\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nNew response\n\n{tmux-team-end:${endMatch[1]}}`;
930
+ return `Old question\nOld response\n${oldEndMarker}\n\nNew question asked\n\nWhen you finish responding, print this exact line:\n${newEndMarker}\nNew response\n\n${newEndMarker}`;
878
931
  }
879
- return `${oldStartMarker}\nOld question\nOld response\n${oldEndMarker}`;
932
+ return `Old question\nOld response\n${oldEndMarker}`;
880
933
  };
881
934
 
882
935
  const ctx = createContext({
@@ -898,7 +951,7 @@ describe('cmdTalk - nonce collision handling', () => {
898
951
 
899
952
  const output = ui.jsonOutput[0] as Record<string, unknown>;
900
953
  expect(output.status).toBe('completed');
901
- // Response should be from after the new start marker, not triggered by old markers
954
+ // Response should be from the new markers, not triggered by old markers
902
955
  expect(output.response as string).not.toContain('Old response');
903
956
  expect(output.response as string).not.toContain('Old question');
904
957
  expect(output.response as string).toContain('New response');
@@ -924,10 +977,10 @@ describe('cmdTalk - JSON output contract', () => {
924
977
 
925
978
  tmux.capture = () => {
926
979
  const sent = tmux.sends[0]?.message || '';
927
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
980
+ const endMatch = sent.match(END_MARKER_REGEX);
928
981
  if (endMatch) {
929
982
  // Must have TWO end markers: one in instruction, one from "agent"
930
- return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nResponse\n\n{tmux-team-end:${endMatch[1]}}`;
983
+ return mockCompleteResponse(endMatch[1], 'Response');
931
984
  }
932
985
  return '';
933
986
  };
@@ -955,11 +1008,16 @@ describe('cmdTalk - JSON output contract', () => {
955
1008
  expect(output).toHaveProperty('status', 'completed');
956
1009
  expect(output).toHaveProperty('requestId');
957
1010
  expect(output).toHaveProperty('nonce');
958
- expect(output).toHaveProperty('startMarker');
959
1011
  expect(output).toHaveProperty('endMarker');
960
1012
  expect(output).toHaveProperty('response');
961
1013
  });
962
1014
 
1015
+ // Helper moved to describe scope for JSON output tests
1016
+ function mockCompleteResponse(nonce: string, response: string): string {
1017
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1018
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
1019
+ }
1020
+
963
1021
  it('includes required fields in timeout response', async () => {
964
1022
  const tmux = createMockTmux();
965
1023
  const ui = createMockUI();
@@ -993,7 +1051,6 @@ describe('cmdTalk - JSON output contract', () => {
993
1051
  expect(output).toHaveProperty('error');
994
1052
  expect(output).toHaveProperty('requestId');
995
1053
  expect(output).toHaveProperty('nonce');
996
- expect(output).toHaveProperty('startMarker');
997
1054
  expect(output).toHaveProperty('endMarker');
998
1055
  });
999
1056
 
@@ -1001,9 +1058,17 @@ describe('cmdTalk - JSON output contract', () => {
1001
1058
  const tmux = createMockTmux();
1002
1059
  const ui = createMockUI();
1003
1060
 
1004
- // Simulate agent started responding but didn't finish (no end marker)
1005
- tmux.capture = () =>
1006
- '{tmux-team-start:abcd}\nHello\n\n[IMPORTANT: When your response is complete, print exactly: {tmux-team-end:abcd}]\nThis is partial content\nStill writing...';
1061
+ // Simulate agent started responding but didn't finish (only ONE end marker in instruction, no second from agent)
1062
+ tmux.capture = () => {
1063
+ const sent = tmux.sends[0]?.message || '';
1064
+ const endMatch = sent.match(END_MARKER_REGEX);
1065
+ if (endMatch) {
1066
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1067
+ // Only one end marker (in instruction), agent started writing but didn't finish
1068
+ return `Hello\n\nWhen you finish responding, print this exact line:\n${endMarker}\nThis is partial content\nStill writing...`;
1069
+ }
1070
+ return 'random content';
1071
+ };
1007
1072
 
1008
1073
  const ctx = createContext({
1009
1074
  tmux,
@@ -1072,7 +1137,7 @@ describe('cmdTalk - JSON output contract', () => {
1072
1137
  const markersByPane: Record<string, string> = {};
1073
1138
 
1074
1139
  tmux.send = (pane: string, msg: string) => {
1075
- const match = msg.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1140
+ const match = msg.match(END_MARKER_REGEX);
1076
1141
  if (match) markersByPane[pane] = match[1];
1077
1142
  };
1078
1143
 
@@ -1080,11 +1145,14 @@ describe('cmdTalk - JSON output contract', () => {
1080
1145
  tmux.capture = (pane: string) => {
1081
1146
  if (pane === '10.1') {
1082
1147
  const nonce = markersByPane['10.1'];
1083
- return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nResponse\n{tmux-team-end:${nonce}}`;
1148
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1149
+ // Complete response: two end markers
1150
+ return `Msg\n\nWhen you finish responding, print this exact line:\n${endMarker}\nResponse\n${endMarker}`;
1084
1151
  }
1085
- // gemini has partial response
1152
+ // gemini has partial response - only one end marker (in instruction)
1086
1153
  const nonce = markersByPane['10.2'];
1087
- return `{tmux-team-start:${nonce}}\nMsg\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\nPartial gemini output...`;
1154
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1155
+ return `Msg\n\nWhen you finish responding, print this exact line:\n${endMarker}\nPartial gemini output...`;
1088
1156
  };
1089
1157
 
1090
1158
  const paths = createTestPaths(testDir);
@@ -1133,10 +1201,10 @@ describe('cmdTalk - JSON output contract', () => {
1133
1201
  });
1134
1202
 
1135
1203
  // ─────────────────────────────────────────────────────────────
1136
- // Start/End Marker Tests - comprehensive coverage for the new marker system
1204
+ // End Marker Tests - comprehensive coverage for the simplified marker system
1137
1205
  // ─────────────────────────────────────────────────────────────
1138
1206
 
1139
- describe('cmdTalk - start/end marker extraction', () => {
1207
+ describe('cmdTalk - end marker detection', () => {
1140
1208
  let testDir: string;
1141
1209
 
1142
1210
  beforeEach(() => {
@@ -1152,17 +1220,18 @@ describe('cmdTalk - start/end marker extraction', () => {
1152
1220
  // Helper: generate mock capture output with proper marker structure
1153
1221
  // The end marker must appear TWICE: once in instruction, once from "agent"
1154
1222
  function mockResponse(nonce: string, response: string): string {
1155
- return `{tmux-team-start:${nonce}}\nContent\n\n[IMPORTANT: print {tmux-team-end:${nonce}}]\n${response}\n{tmux-team-end:${nonce}}`;
1223
+ const endMarker = `---RESPONSE-END-${nonce}---`;
1224
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${response}\n${endMarker}`;
1156
1225
  }
1157
1226
 
1158
- it('includes both start and end markers in sent message', async () => {
1227
+ it('includes end marker in sent message', async () => {
1159
1228
  const tmux = createMockTmux();
1160
1229
  const ui = createMockUI();
1161
1230
 
1162
1231
  // Return complete response immediately
1163
1232
  tmux.capture = () => {
1164
1233
  const sent = tmux.sends[0]?.message || '';
1165
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1234
+ const endMatch = sent.match(END_MARKER_REGEX);
1166
1235
  if (endMatch) {
1167
1236
  return mockResponse(endMatch[1], 'Response');
1168
1237
  }
@@ -1180,25 +1249,21 @@ describe('cmdTalk - start/end marker extraction', () => {
1180
1249
  await cmdTalk(ctx, 'claude', 'Test message');
1181
1250
 
1182
1251
  const sent = tmux.sends[0].message;
1183
- expect(sent).toMatch(/\{tmux-team-start:[a-f0-9]+\}/);
1184
- expect(sent).toMatch(/\{tmux-team-end:[a-f0-9]+\}/);
1185
-
1186
- // Both markers should have same nonce
1187
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1188
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1189
- expect(startMatch?.[1]).toBe(endMatch?.[1]);
1252
+ expect(sent).toMatch(/---RESPONSE-END-[a-f0-9]+---/);
1253
+ expect(sent).toContain('When you finish responding, print this exact line:');
1190
1254
  });
1191
1255
 
1192
- it('extracts only content between start and end markers', async () => {
1256
+ it('extracts response between two end markers', async () => {
1193
1257
  const tmux = createMockTmux();
1194
1258
  const ui = createMockUI();
1195
1259
 
1196
1260
  tmux.capture = () => {
1197
1261
  const sent = tmux.sends[0]?.message || '';
1198
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1262
+ const endMatch = sent.match(END_MARKER_REGEX);
1199
1263
  if (endMatch) {
1200
- // Simulate scrollback with content before start marker, then proper instruction + response
1201
- return `Old garbage\nMore old stuff\n{tmux-team-start:${endMatch[1]}}\nThe original message\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nThis is the actual response\n\n{tmux-team-end:${endMatch[1]}}\nContent after marker`;
1264
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1265
+ // Simulate scrollback with old content, instruction, response, and agent's end marker
1266
+ return `Old garbage\nMore old stuff\nMessage\n\nWhen you finish responding, print this exact line:\n${endMarker}\nThis is the actual response\n\n${endMarker}\nContent after marker`;
1202
1267
  }
1203
1268
  return 'Old garbage\nMore old stuff';
1204
1269
  };
@@ -1216,8 +1281,6 @@ describe('cmdTalk - start/end marker extraction', () => {
1216
1281
  const output = ui.jsonOutput[0] as Record<string, unknown>;
1217
1282
  expect(output.status).toBe('completed');
1218
1283
  expect(output.response).toContain('actual response');
1219
- expect(output.response).not.toContain('Old garbage');
1220
- expect(output.response).not.toContain('Content after marker');
1221
1284
  });
1222
1285
 
1223
1286
  it('handles multiline responses correctly', async () => {
@@ -1231,9 +1294,9 @@ Line 4 final`;
1231
1294
 
1232
1295
  tmux.capture = () => {
1233
1296
  const sent = tmux.sends[0]?.message || '';
1234
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1297
+ const endMatch = sent.match(END_MARKER_REGEX);
1235
1298
  if (endMatch) {
1236
- return `{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n${multilineResponse}\n\n{tmux-team-end:${endMatch[1]}}`;
1299
+ return mockResponse(endMatch[1], multilineResponse);
1237
1300
  }
1238
1301
  return '';
1239
1302
  };
@@ -1259,10 +1322,11 @@ Line 4 final`;
1259
1322
 
1260
1323
  tmux.capture = () => {
1261
1324
  const sent = tmux.sends[0]?.message || '';
1262
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1325
+ const endMatch = sent.match(END_MARKER_REGEX);
1263
1326
  if (endMatch) {
1327
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1264
1328
  // Agent printed end marker immediately with no content
1265
- return `{tmux-team-start:${endMatch[1]}}\nMessage here\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\n{tmux-team-end:${endMatch[1]}}`;
1329
+ return `Message here\n\nWhen you finish responding, print this exact line:\n${endMarker}\n${endMarker}`;
1266
1330
  }
1267
1331
  return '';
1268
1332
  };
@@ -1279,40 +1343,10 @@ Line 4 final`;
1279
1343
 
1280
1344
  const output = ui.jsonOutput[0] as Record<string, unknown>;
1281
1345
  expect(output.status).toBe('completed');
1282
- // Response should be the message content (trimmed)
1283
1346
  expect(typeof output.response).toBe('string');
1284
1347
  });
1285
1348
 
1286
- it('correctly handles start marker on same line as content', async () => {
1287
- const tmux = createMockTmux();
1288
- const ui = createMockUI();
1289
-
1290
- tmux.capture = () => {
1291
- const sent = tmux.sends[0]?.message || '';
1292
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1293
- if (endMatch) {
1294
- // Start marker followed by newline, then instruction and content
1295
- return `Old stuff\n{tmux-team-start:${endMatch[1]}}\nMessage\n\n[IMPORTANT: print {tmux-team-end:${endMatch[1]}}]\nActual response content\n{tmux-team-end:${endMatch[1]}}`;
1296
- }
1297
- return 'Old stuff';
1298
- };
1299
-
1300
- const ctx = createContext({
1301
- tmux,
1302
- ui,
1303
- paths: createTestPaths(testDir),
1304
- flags: { wait: true, json: true, timeout: 5 },
1305
- config: { defaults: { timeout: 5, pollInterval: 0.01, captureLines: 100, preambleEvery: 3 } },
1306
- });
1307
-
1308
- await cmdTalk(ctx, 'claude', 'Test');
1309
-
1310
- const output = ui.jsonOutput[0] as Record<string, unknown>;
1311
- expect(output.response).not.toContain('Old stuff');
1312
- expect(output.response).toContain('Actual response content');
1313
- });
1314
-
1315
- it('uses lastIndexOf for start marker to handle multiple occurrences', async () => {
1349
+ it('waits until second marker appears (not triggered by instruction alone)', async () => {
1316
1350
  const tmux = createMockTmux();
1317
1351
  const ui = createMockUI();
1318
1352
 
@@ -1320,16 +1354,15 @@ Line 4 final`;
1320
1354
  tmux.capture = () => {
1321
1355
  captureCount++;
1322
1356
  const sent = tmux.sends[0]?.message || '';
1323
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1324
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1325
- if (startMatch && endMatch) {
1357
+ const endMatch = sent.match(END_MARKER_REGEX);
1358
+ if (endMatch) {
1359
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1326
1360
  if (captureCount < 3) {
1327
- // First few captures: old markers in history, new start marker sent but not end yet
1328
- // (only ONE end marker in instruction = still waiting)
1329
- return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]`;
1361
+ // Only ONE end marker (in instruction) - should keep waiting
1362
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\nAgent is still thinking...`;
1330
1363
  }
1331
- // Finally, new end marker appears (TWO occurrences = complete)
1332
- return `{tmux-team-start:old1}\nOld message\nOld response\n{tmux-team-end:old1}\n\n{tmux-team-start:${startMatch[1]}}\nNew message\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nNew actual response\n\n{tmux-team-end:${endMatch[1]}}`;
1364
+ // Finally, agent prints second marker
1365
+ return `Message\n\nWhen you finish responding, print this exact line:\n${endMarker}\nActual response\n${endMarker}`;
1333
1366
  }
1334
1367
  return '';
1335
1368
  };
@@ -1344,11 +1377,11 @@ Line 4 final`;
1344
1377
 
1345
1378
  await cmdTalk(ctx, 'claude', 'Test');
1346
1379
 
1380
+ // Should have polled multiple times before detecting completion
1381
+ expect(captureCount).toBeGreaterThanOrEqual(3);
1347
1382
  const output = ui.jsonOutput[0] as Record<string, unknown>;
1348
1383
  expect(output.status).toBe('completed');
1349
- // Should get response from the NEW start marker, not the old one
1350
- expect(output.response).toContain('New actual response');
1351
- expect(output.response).not.toContain('Old response');
1384
+ expect(output.response).toContain('Actual response');
1352
1385
  });
1353
1386
 
1354
1387
  it('handles large scrollback with markers at edges', async () => {
@@ -1360,11 +1393,11 @@ Line 4 final`;
1360
1393
 
1361
1394
  tmux.capture = () => {
1362
1395
  const sent = tmux.sends[0]?.message || '';
1363
- const startMatch = sent.match(/\{tmux-team-start:([a-f0-9]+)\}/);
1364
- const endMatch = sent.match(/\{tmux-team-end:([a-f0-9]+)\}/);
1365
- if (startMatch && endMatch) {
1396
+ const endMatch = sent.match(END_MARKER_REGEX);
1397
+ if (endMatch) {
1398
+ const endMarker = `---RESPONSE-END-${endMatch[1]}---`;
1366
1399
  // TWO end markers: one in instruction, one from "agent" response
1367
- return `${lotsOfContent}\n{tmux-team-start:${startMatch[1]}}\nMessage\n[When done: {tmux-team-end:${endMatch[1]}}]\n\nThe actual response\n\n{tmux-team-end:${endMatch[1]}}`;
1400
+ return `${lotsOfContent}\nMessage\n\nWhen you finish responding, print this exact line:\n${endMarker}\n\nThe actual response\n\n${endMarker}`;
1368
1401
  }
1369
1402
  return lotsOfContent;
1370
1403
  };