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.
- package/README.md +21 -191
- package/package.json +1 -1
- package/src/cli.ts +0 -5
- package/src/commands/config.ts +2 -44
- package/src/commands/help.ts +0 -2
- package/src/commands/talk.test.ts +296 -46
- package/src/commands/talk.ts +69 -63
- package/src/config.test.ts +0 -1
- package/src/config.ts +0 -1
- package/src/identity.ts +89 -0
- package/src/types.ts +2 -2
- package/src/version.ts +1 -1
- package/src/pm/commands.test.ts +0 -1462
- package/src/pm/commands.ts +0 -1011
- package/src/pm/manager.test.ts +0 -377
- package/src/pm/manager.ts +0 -146
- package/src/pm/permissions.test.ts +0 -444
- package/src/pm/permissions.ts +0 -293
- package/src/pm/storage/adapter.ts +0 -57
- package/src/pm/storage/fs.test.ts +0 -512
- package/src/pm/storage/fs.ts +0 -290
- package/src/pm/storage/github.ts +0 -842
- package/src/pm/types.ts +0 -91
package/src/commands/talk.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
setActiveRequest,
|
|
14
14
|
incrementPreambleCounter,
|
|
15
15
|
} from '../state.js';
|
|
16
|
-
import { resolveActor } from '../
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
215
|
+
const startMarker = `{tmux-team-start:${nonce}}`;
|
|
216
|
+
const endMarker = `{tmux-team-end:${nonce}}`;
|
|
216
217
|
|
|
217
|
-
// Build message with preamble, then
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
384
|
+
const startMarker = `{tmux-team-start:${nonce}}`;
|
|
385
|
+
const endMarker = `{tmux-team-end:${nonce}}`;
|
|
375
386
|
|
|
376
|
-
|
|
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: ${
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
428
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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(
|
|
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
|
-
|
|
596
|
-
|
|
601
|
+
startMarker: s.startMarker,
|
|
602
|
+
endMarker: s.endMarker,
|
|
597
603
|
status: s.status,
|
|
598
604
|
response: s.response,
|
|
599
605
|
error: s.error,
|
package/src/config.test.ts
CHANGED
|
@@ -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
package/src/identity.ts
ADDED
|
@@ -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
|
-
|
|
88
|
+
startMarker: string;
|
|
89
|
+
endMarker: string;
|
|
90
90
|
response: string;
|
|
91
91
|
}
|
|
92
92
|
|