tmux-team 2.2.0 → 3.0.0-alpha.1

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.
@@ -13,7 +13,7 @@ import {
13
13
  setActiveRequest,
14
14
  incrementPreambleCounter,
15
15
  } from '../state.js';
16
- import { resolveActor } from '../pm/permissions.js';
16
+ import { resolveActor } from '../identity.js';
17
17
 
18
18
  function sleepMs(ms: number): Promise<void> {
19
19
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -41,8 +41,8 @@ interface AgentWaitState {
41
41
  pane: string;
42
42
  requestId: string;
43
43
  nonce: string;
44
- marker: string;
45
- baseline: string;
44
+ startMarker: string;
45
+ endMarker: string;
46
46
  status: 'pending' | 'completed' | 'timeout' | 'error';
47
47
  response?: string;
48
48
  error?: string;
@@ -212,11 +212,12 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
212
212
 
213
213
  const requestId = makeRequestId();
214
214
  const nonce = makeNonce();
215
- const marker = `{tmux-team-end:${nonce}}`;
215
+ const startMarker = `{tmux-team-start:${nonce}}`;
216
+ const endMarker = `{tmux-team-end:${nonce}}`;
216
217
 
217
- // Build message with preamble, then append nonce instruction
218
+ // Build message with preamble, then wrap with start/end markers
218
219
  const messageWithPreamble = buildMessage(message, target, ctx);
219
- const fullMessage = `${messageWithPreamble}\n\n[IMPORTANT: When your response is complete, print exactly: ${marker}]`;
220
+ const fullMessage = `${startMarker}\n${messageWithPreamble}\n\n[IMPORTANT: When your response is complete, print exactly: ${endMarker}]`;
220
221
 
221
222
  // Best-effort cleanup and soft-lock warning
222
223
  const state = cleanupState(ctx.paths, 60 * 60); // 1 hour TTL
@@ -227,14 +228,6 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
227
228
  );
228
229
  }
229
230
 
230
- let baseline = '';
231
- try {
232
- baseline = tmux.capture(pane, captureLines);
233
- } catch {
234
- ui.error(`Failed to capture pane ${pane}. Is tmux running?`);
235
- exit(ExitCodes.ERROR);
236
- }
237
-
238
231
  setActiveRequest(ctx.paths, target, { id: requestId, nonce, pane, startedAtMs: Date.now() });
239
232
 
240
233
  const startedAt = Date.now();
@@ -267,7 +260,8 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
267
260
  error: `Timed out waiting for ${target} after ${Math.floor(timeoutSeconds)}s`,
268
261
  requestId,
269
262
  nonce,
270
- marker,
263
+ startMarker,
264
+ endMarker,
271
265
  });
272
266
  exit(ExitCodes.TIMEOUT);
273
267
  }
@@ -303,16 +297,32 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
303
297
  exit(ExitCodes.ERROR);
304
298
  }
305
299
 
306
- const markerIndex = output.indexOf(marker);
307
- if (markerIndex === -1) continue;
308
-
309
- let startIndex = 0;
310
- const baselineIndex = baseline ? output.lastIndexOf(baseline) : -1;
311
- if (baselineIndex !== -1) {
312
- startIndex = baselineIndex + baseline.length;
300
+ // Find end marker (use lastIndexOf because the marker appears in the instruction AND agent's response)
301
+ const endMarkerIndex = output.lastIndexOf(endMarker);
302
+ if (endMarkerIndex === -1) continue;
303
+
304
+ // Find the end of our instruction by looking for `}]` pattern (the instruction ends with `{end}]`)
305
+ // This is more reliable than looking for newline after start marker because
306
+ // the message may be word-wrapped across multiple visual lines
307
+ let responseStart = 0;
308
+ const instructionEndPattern = '}]';
309
+ const instructionEndIndex = output.lastIndexOf(instructionEndPattern, endMarkerIndex);
310
+ if (instructionEndIndex !== -1) {
311
+ // Find the first newline after the instruction's closing `}]`
312
+ responseStart = output.indexOf('\n', instructionEndIndex + 2);
313
+ if (responseStart !== -1) responseStart += 1;
314
+ else responseStart = instructionEndIndex + 2;
315
+ } else {
316
+ // Fallback: if no `}]` found, try to find newline after start marker
317
+ const startMarkerIndex = output.lastIndexOf(startMarker);
318
+ if (startMarkerIndex !== -1) {
319
+ responseStart = output.indexOf('\n', startMarkerIndex);
320
+ if (responseStart !== -1) responseStart += 1;
321
+ else responseStart = startMarkerIndex + startMarker.length;
322
+ }
313
323
  }
314
324
 
315
- const response = output.slice(startIndex, markerIndex).trim();
325
+ const response = output.slice(responseStart, endMarkerIndex).trim();
316
326
 
317
327
  if (!flags.json && isTTY) {
318
328
  process.stdout.write('\r' + ' '.repeat(80) + '\r');
@@ -323,7 +333,7 @@ export async function cmdTalk(ctx: Context, target: string, message: string): Pr
323
333
 
324
334
  clearActiveRequest(ctx.paths, target, requestId);
325
335
 
326
- const result: WaitResult = { requestId, nonce, marker, response };
336
+ const result: WaitResult = { requestId, nonce, startMarker, endMarker, response };
327
337
  if (flags.json) {
328
338
  ui.json({ target, pane, status: 'completed', ...result });
329
339
  } else {
@@ -367,35 +377,16 @@ async function cmdTalkAllWait(
367
377
  );
368
378
  }
369
379
 
370
- // Phase 1: Send messages to all agents and capture baselines
380
+ // Phase 1: Send messages to all agents with start/end markers
371
381
  for (const [name, data] of targetAgents) {
372
382
  const requestId = makeRequestId();
373
383
  const nonce = makeNonce(); // Unique nonce per agent (#19)
374
- const marker = `{tmux-team-end:${nonce}}`;
384
+ const startMarker = `{tmux-team-start:${nonce}}`;
385
+ const endMarker = `{tmux-team-end:${nonce}}`;
375
386
 
376
- let baseline = '';
377
- try {
378
- baseline = tmux.capture(data.pane, captureLines);
379
- } catch {
380
- agentStates.push({
381
- agent: name,
382
- pane: data.pane,
383
- requestId,
384
- nonce,
385
- marker,
386
- baseline: '',
387
- status: 'error',
388
- error: `Failed to capture pane ${data.pane}`,
389
- });
390
- if (!flags.json) {
391
- ui.warn(`Failed to capture ${name} (${data.pane})`);
392
- }
393
- continue;
394
- }
395
-
396
- // Build and send message
387
+ // Build and send message with start/end markers
397
388
  const messageWithPreamble = buildMessage(message, name, ctx);
398
- const fullMessage = `${messageWithPreamble}\n\n[IMPORTANT: When your response is complete, print exactly: ${marker}]`;
389
+ const fullMessage = `${startMarker}\n${messageWithPreamble}\n\n[IMPORTANT: When your response is complete, print exactly: ${endMarker}]`;
399
390
  const msg = name === 'gemini' ? fullMessage.replace(/!/g, '') : fullMessage;
400
391
 
401
392
  try {
@@ -411,8 +402,8 @@ async function cmdTalkAllWait(
411
402
  pane: data.pane,
412
403
  requestId,
413
404
  nonce,
414
- marker,
415
- baseline,
405
+ startMarker,
406
+ endMarker,
416
407
  status: 'pending',
417
408
  });
418
409
  if (!flags.json) {
@@ -424,8 +415,8 @@ async function cmdTalkAllWait(
424
415
  pane: data.pane,
425
416
  requestId,
426
417
  nonce,
427
- marker,
428
- baseline,
418
+ startMarker,
419
+ endMarker,
429
420
  status: 'error',
430
421
  error: `Failed to send to pane ${data.pane}`,
431
422
  });
@@ -520,17 +511,32 @@ async function cmdTalkAllWait(
520
511
  continue;
521
512
  }
522
513
 
523
- const markerIndex = output.indexOf(state.marker);
524
- if (markerIndex === -1) continue;
525
-
526
- // Found marker - extract response
527
- let startIndex = 0;
528
- const baselineIndex = state.baseline ? output.lastIndexOf(state.baseline) : -1;
529
- if (baselineIndex !== -1) {
530
- startIndex = baselineIndex + state.baseline.length;
514
+ // Find end marker (use lastIndexOf because the marker appears in the instruction AND agent's response)
515
+ const endMarkerIndex = output.lastIndexOf(state.endMarker);
516
+ if (endMarkerIndex === -1) continue;
517
+
518
+ // Find the end of our instruction by looking for `}]` pattern (the instruction ends with `{end}]`)
519
+ // This is more reliable than looking for newline after start marker because
520
+ // the message may be word-wrapped across multiple visual lines
521
+ let responseStart = 0;
522
+ const instructionEndPattern = '}]';
523
+ const instructionEndIndex = output.lastIndexOf(instructionEndPattern, endMarkerIndex);
524
+ if (instructionEndIndex !== -1) {
525
+ // Find the first newline after the instruction's closing `}]`
526
+ responseStart = output.indexOf('\n', instructionEndIndex + 2);
527
+ if (responseStart !== -1) responseStart += 1;
528
+ else responseStart = instructionEndIndex + 2;
529
+ } else {
530
+ // Fallback: if no `}]` found, try to find newline after start marker
531
+ const startMarkerIndex = output.lastIndexOf(state.startMarker);
532
+ if (startMarkerIndex !== -1) {
533
+ responseStart = output.indexOf('\n', startMarkerIndex);
534
+ if (responseStart !== -1) responseStart += 1;
535
+ else responseStart = startMarkerIndex + state.startMarker.length;
536
+ }
531
537
  }
532
538
 
533
- state.response = output.slice(startIndex, markerIndex).trim();
539
+ state.response = output.slice(responseStart, endMarkerIndex).trim();
534
540
  state.status = 'completed';
535
541
  state.elapsedMs = Date.now() - startedAt;
536
542
  clearActiveRequest(paths, state.agent, state.requestId);
@@ -592,8 +598,8 @@ function outputBroadcastResults(
592
598
  pane: s.pane,
593
599
  requestId: s.requestId,
594
600
  nonce: s.nonce,
595
- marker: s.marker,
596
- baseline: '', // Don't include baseline in output
601
+ startMarker: s.startMarker,
602
+ endMarker: s.endMarker,
597
603
  status: s.status,
598
604
  response: s.response,
599
605
  error: s.error,
@@ -161,7 +161,6 @@ describe('loadConfig', () => {
161
161
  expect(config.defaults.timeout).toBe(180);
162
162
  expect(config.defaults.pollInterval).toBe(1);
163
163
  expect(config.defaults.captureLines).toBe(100);
164
- expect(config.defaults.hideOrphanTasks).toBe(false);
165
164
  expect(config.agents).toEqual({});
166
165
  expect(config.paneRegistry).toEqual({});
167
166
  });
package/src/config.ts CHANGED
@@ -27,7 +27,6 @@ const DEFAULT_CONFIG: GlobalConfig = {
27
27
  pollInterval: 1,
28
28
  captureLines: 100,
29
29
  preambleEvery: 3, // inject preamble every 3 messages
30
- hideOrphanTasks: false, // hide tasks without milestone in list
31
30
  },
32
31
  };
33
32
 
@@ -0,0 +1,89 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // Identity resolution - determine current agent from tmux pane
3
+ // ─────────────────────────────────────────────────────────────
4
+
5
+ import { execSync } from 'child_process';
6
+ import type { PaneEntry } from './types.js';
7
+
8
+ export interface ActorResolution {
9
+ actor: string;
10
+ source: 'pane' | 'env' | 'default';
11
+ warning?: string;
12
+ }
13
+
14
+ /**
15
+ * Get current tmux pane ID (e.g., "1.0").
16
+ */
17
+ function getCurrentPane(): string | null {
18
+ if (!process.env.TMUX) {
19
+ return null;
20
+ }
21
+
22
+ const tmuxPane = process.env.TMUX_PANE;
23
+ if (!tmuxPane) {
24
+ return null;
25
+ }
26
+
27
+ try {
28
+ const result = execSync(
29
+ `tmux display-message -p -t "${tmuxPane}" '#{window_index}.#{pane_index}'`,
30
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
31
+ );
32
+ return result.trim();
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Find agent name by pane ID.
40
+ */
41
+ function findAgentByPane(paneRegistry: Record<string, PaneEntry>, paneId: string): string | null {
42
+ for (const [agentName, entry] of Object.entries(paneRegistry)) {
43
+ if (entry.pane === paneId) {
44
+ return agentName;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Resolve current actor using pane registry as primary source.
52
+ */
53
+ export function resolveActor(paneRegistry: Record<string, PaneEntry>): ActorResolution {
54
+ const envActor = process.env.TMT_AGENT_NAME || process.env.TMUX_TEAM_ACTOR;
55
+ const currentPane = getCurrentPane();
56
+
57
+ // Not in tmux - use env var or default to human
58
+ if (!currentPane) {
59
+ if (envActor) {
60
+ return { actor: envActor, source: 'env' };
61
+ }
62
+ return { actor: 'human', source: 'default' };
63
+ }
64
+
65
+ // In tmux - look up pane in registry
66
+ const paneAgent = findAgentByPane(paneRegistry, currentPane);
67
+
68
+ if (paneAgent) {
69
+ if (envActor && envActor !== paneAgent) {
70
+ return {
71
+ actor: paneAgent,
72
+ source: 'pane',
73
+ warning: `⚠️ Identity mismatch: TMT_AGENT_NAME="${envActor}" but pane ${currentPane} is registered to "${paneAgent}". Using pane identity.`,
74
+ };
75
+ }
76
+ return { actor: paneAgent, source: 'pane' };
77
+ }
78
+
79
+ // Pane not in registry
80
+ if (envActor) {
81
+ return {
82
+ actor: envActor,
83
+ source: 'env',
84
+ warning: `⚠️ Unregistered pane: pane ${currentPane} is not in registry. Using TMT_AGENT_NAME="${envActor}".`,
85
+ };
86
+ }
87
+
88
+ return { actor: 'human', source: 'default' };
89
+ }
package/src/types.ts CHANGED
@@ -19,7 +19,6 @@ export interface ConfigDefaults {
19
19
  pollInterval: number; // seconds
20
20
  captureLines: number;
21
21
  preambleEvery: number; // inject preamble every N messages (default: 3)
22
- hideOrphanTasks: boolean; // hide tasks without milestone in list (default: false)
23
22
  }
24
23
 
25
24
  export interface GlobalConfig {
@@ -86,7 +85,8 @@ export interface Tmux {
86
85
  export interface WaitResult {
87
86
  requestId: string;
88
87
  nonce: string;
89
- marker: string;
88
+ startMarker: string;
89
+ endMarker: string;
90
90
  response: string;
91
91
  }
92
92
 
package/src/version.ts CHANGED
@@ -14,7 +14,7 @@ function getVersion(): string {
14
14
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
15
15
  return pkg.version;
16
16
  } catch {
17
- return '2.0.0';
17
+ return '3.0.0';
18
18
  }
19
19
  }
20
20