@automagik/genie 0.260202.1833 → 0.260202.1901
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/.beads/README.md +81 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +0 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/AGENTS.md +40 -0
- package/dist/claudio.js +1 -1
- package/dist/genie.js +1 -1
- package/dist/term.js +98 -97
- package/package.json +1 -1
- package/src/lib/beads-registry.ts +546 -0
- package/src/lib/tmux.ts +15 -1
- package/src/lib/version.ts +1 -1
- package/src/term-commands/close.ts +38 -3
- package/src/term-commands/daemon.ts +176 -0
- package/src/term-commands/kill.ts +45 -2
- package/src/term-commands/split.ts +8 -7
- package/src/term-commands/work.ts +86 -4
- package/src/term-commands/workers.ts +36 -2
- package/src/term.ts +39 -1
package/package.json
CHANGED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beads Registry - Worker state management via beads native commands
|
|
3
|
+
*
|
|
4
|
+
* Replaces JSON file-based worker-registry.ts with beads agent/slot system.
|
|
5
|
+
* Provides unified state tracking with Witness support and built-in cardinality.
|
|
6
|
+
*
|
|
7
|
+
* Environment:
|
|
8
|
+
* TERM_USE_BEADS_REGISTRY=false - Disable beads registry (fallback to JSON)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { $ } from 'bun';
|
|
12
|
+
import type { Worker, WorkerState } from './worker-registry.js';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Configuration
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const AGENT_LABEL = 'gt:agent';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if beads registry is enabled (default: true)
|
|
22
|
+
*/
|
|
23
|
+
export function isBeadsRegistryEnabled(): boolean {
|
|
24
|
+
return process.env.TERM_USE_BEADS_REGISTRY !== 'false';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Helper Functions
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run bd command and parse output
|
|
33
|
+
*/
|
|
34
|
+
async function runBd(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
35
|
+
try {
|
|
36
|
+
const result = await $`bd ${args}`.quiet();
|
|
37
|
+
return {
|
|
38
|
+
stdout: result.stdout.toString().trim(),
|
|
39
|
+
stderr: result.stderr.toString().trim(),
|
|
40
|
+
exitCode: 0,
|
|
41
|
+
};
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
return {
|
|
44
|
+
stdout: error.stdout?.toString().trim() || '',
|
|
45
|
+
stderr: error.stderr?.toString().trim() || '',
|
|
46
|
+
exitCode: error.exitCode || 1,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse JSON from bd command, handling potential errors
|
|
53
|
+
*/
|
|
54
|
+
function parseJson<T>(output: string): T | null {
|
|
55
|
+
if (!output) return null;
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(output);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Agent Bead Management
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
interface AgentBead {
|
|
68
|
+
id: string;
|
|
69
|
+
title: string;
|
|
70
|
+
type: string;
|
|
71
|
+
labels: string[];
|
|
72
|
+
state?: string;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a new agent bead for a worker
|
|
78
|
+
*/
|
|
79
|
+
export async function createAgent(
|
|
80
|
+
workerId: string,
|
|
81
|
+
metadata: {
|
|
82
|
+
paneId: string;
|
|
83
|
+
session: string;
|
|
84
|
+
worktree: string | null;
|
|
85
|
+
repoPath: string;
|
|
86
|
+
taskId: string;
|
|
87
|
+
taskTitle?: string;
|
|
88
|
+
}
|
|
89
|
+
): Promise<string> {
|
|
90
|
+
// Create agent bead with metadata
|
|
91
|
+
const title = `Worker: ${workerId}`;
|
|
92
|
+
const { stdout, exitCode } = await runBd([
|
|
93
|
+
'create',
|
|
94
|
+
`--title=${title}`,
|
|
95
|
+
'--type=agent',
|
|
96
|
+
`--label=${AGENT_LABEL}`,
|
|
97
|
+
`--label=worker:${workerId}`,
|
|
98
|
+
'--json',
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
if (exitCode !== 0) {
|
|
102
|
+
throw new Error(`Failed to create agent bead: ${stdout}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const created = parseJson<{ id: string }>(stdout);
|
|
106
|
+
if (!created?.id) {
|
|
107
|
+
throw new Error('Failed to parse created agent bead');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Store worker metadata in agent
|
|
111
|
+
const metadataJson = JSON.stringify({
|
|
112
|
+
paneId: metadata.paneId,
|
|
113
|
+
session: metadata.session,
|
|
114
|
+
worktree: metadata.worktree,
|
|
115
|
+
repoPath: metadata.repoPath,
|
|
116
|
+
taskId: metadata.taskId,
|
|
117
|
+
taskTitle: metadata.taskTitle,
|
|
118
|
+
startedAt: new Date().toISOString(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await runBd(['update', created.id, `--metadata=${metadataJson}`]);
|
|
122
|
+
|
|
123
|
+
return created.id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Ensure an agent bead exists for a worker, creating if needed
|
|
128
|
+
*/
|
|
129
|
+
export async function ensureAgent(
|
|
130
|
+
workerId: string,
|
|
131
|
+
metadata: {
|
|
132
|
+
paneId: string;
|
|
133
|
+
session: string;
|
|
134
|
+
worktree: string | null;
|
|
135
|
+
repoPath: string;
|
|
136
|
+
taskId: string;
|
|
137
|
+
taskTitle?: string;
|
|
138
|
+
}
|
|
139
|
+
): Promise<string> {
|
|
140
|
+
// Check if agent already exists
|
|
141
|
+
const existing = await findAgentByWorkerId(workerId);
|
|
142
|
+
if (existing) {
|
|
143
|
+
return existing.id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return createAgent(workerId, metadata);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find agent bead by worker ID
|
|
151
|
+
*/
|
|
152
|
+
async function findAgentByWorkerId(workerId: string): Promise<AgentBead | null> {
|
|
153
|
+
const { stdout, exitCode } = await runBd([
|
|
154
|
+
'list',
|
|
155
|
+
`--label=${AGENT_LABEL}`,
|
|
156
|
+
`--label=worker:${workerId}`,
|
|
157
|
+
'--json',
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
if (exitCode !== 0 || !stdout) return null;
|
|
161
|
+
|
|
162
|
+
const agents = parseJson<AgentBead[]>(stdout);
|
|
163
|
+
return agents?.[0] || null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete an agent bead
|
|
168
|
+
*/
|
|
169
|
+
export async function deleteAgent(agentIdOrWorkerId: string): Promise<boolean> {
|
|
170
|
+
// Try direct ID first
|
|
171
|
+
let { exitCode } = await runBd(['delete', agentIdOrWorkerId]);
|
|
172
|
+
if (exitCode === 0) return true;
|
|
173
|
+
|
|
174
|
+
// Try finding by worker ID
|
|
175
|
+
const agent = await findAgentByWorkerId(agentIdOrWorkerId);
|
|
176
|
+
if (agent) {
|
|
177
|
+
({ exitCode } = await runBd(['delete', agent.id]));
|
|
178
|
+
return exitCode === 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// Agent State Management
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Map our WorkerState to beads agent states
|
|
190
|
+
*/
|
|
191
|
+
function mapToBeadsState(state: WorkerState): string {
|
|
192
|
+
switch (state) {
|
|
193
|
+
case 'spawning':
|
|
194
|
+
return 'spawning';
|
|
195
|
+
case 'working':
|
|
196
|
+
return 'working';
|
|
197
|
+
case 'idle':
|
|
198
|
+
return 'idle';
|
|
199
|
+
case 'permission':
|
|
200
|
+
return 'blocked';
|
|
201
|
+
case 'question':
|
|
202
|
+
return 'blocked';
|
|
203
|
+
case 'done':
|
|
204
|
+
return 'done';
|
|
205
|
+
case 'error':
|
|
206
|
+
return 'error';
|
|
207
|
+
default:
|
|
208
|
+
return 'unknown';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Map beads agent state to WorkerState
|
|
214
|
+
*/
|
|
215
|
+
function mapFromBeadsState(beadsState: string): WorkerState {
|
|
216
|
+
switch (beadsState) {
|
|
217
|
+
case 'spawning':
|
|
218
|
+
return 'spawning';
|
|
219
|
+
case 'working':
|
|
220
|
+
return 'working';
|
|
221
|
+
case 'idle':
|
|
222
|
+
return 'idle';
|
|
223
|
+
case 'blocked':
|
|
224
|
+
return 'permission'; // Could be permission or question
|
|
225
|
+
case 'done':
|
|
226
|
+
return 'done';
|
|
227
|
+
case 'error':
|
|
228
|
+
return 'error';
|
|
229
|
+
default:
|
|
230
|
+
return 'idle';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Set agent state
|
|
236
|
+
*/
|
|
237
|
+
export async function setAgentState(workerId: string, state: WorkerState): Promise<void> {
|
|
238
|
+
const agent = await findAgentByWorkerId(workerId);
|
|
239
|
+
if (!agent) {
|
|
240
|
+
throw new Error(`Agent not found for worker ${workerId}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const beadsState = mapToBeadsState(state);
|
|
244
|
+
const { exitCode, stderr } = await runBd(['agent', 'state', agent.id, beadsState]);
|
|
245
|
+
|
|
246
|
+
if (exitCode !== 0) {
|
|
247
|
+
throw new Error(`Failed to set agent state: ${stderr}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Send heartbeat for agent
|
|
253
|
+
*/
|
|
254
|
+
export async function heartbeat(workerId: string): Promise<void> {
|
|
255
|
+
const agent = await findAgentByWorkerId(workerId);
|
|
256
|
+
if (!agent) return; // Silently ignore if agent doesn't exist
|
|
257
|
+
|
|
258
|
+
await runBd(['agent', 'heartbeat', agent.id]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// Work Binding (Slots)
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Bind work (task) to an agent
|
|
267
|
+
*/
|
|
268
|
+
export async function bindWork(workerId: string, taskId: string): Promise<void> {
|
|
269
|
+
const agent = await findAgentByWorkerId(workerId);
|
|
270
|
+
if (!agent) {
|
|
271
|
+
throw new Error(`Agent not found for worker ${workerId}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { exitCode, stderr } = await runBd(['slot', 'set', agent.id, 'hook', taskId]);
|
|
275
|
+
|
|
276
|
+
if (exitCode !== 0) {
|
|
277
|
+
throw new Error(`Failed to bind work: ${stderr}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Unbind work from an agent
|
|
283
|
+
*/
|
|
284
|
+
export async function unbindWork(workerId: string): Promise<void> {
|
|
285
|
+
const agent = await findAgentByWorkerId(workerId);
|
|
286
|
+
if (!agent) return; // Silently ignore if agent doesn't exist
|
|
287
|
+
|
|
288
|
+
await runBd(['slot', 'clear', agent.id, 'hook']);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Worker Query Functions
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
interface AgentMetadata {
|
|
296
|
+
paneId: string;
|
|
297
|
+
session: string;
|
|
298
|
+
worktree: string | null;
|
|
299
|
+
repoPath: string;
|
|
300
|
+
taskId: string;
|
|
301
|
+
taskTitle?: string;
|
|
302
|
+
startedAt: string;
|
|
303
|
+
wishSlug?: string;
|
|
304
|
+
groupNumber?: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Convert agent bead to Worker interface
|
|
309
|
+
*/
|
|
310
|
+
function agentToWorker(agent: AgentBead, metadata: AgentMetadata): Worker {
|
|
311
|
+
return {
|
|
312
|
+
id: metadata.taskId, // Worker ID matches task ID
|
|
313
|
+
paneId: metadata.paneId,
|
|
314
|
+
session: metadata.session,
|
|
315
|
+
worktree: metadata.worktree,
|
|
316
|
+
taskId: metadata.taskId,
|
|
317
|
+
taskTitle: metadata.taskTitle,
|
|
318
|
+
wishSlug: metadata.wishSlug,
|
|
319
|
+
groupNumber: metadata.groupNumber,
|
|
320
|
+
startedAt: metadata.startedAt,
|
|
321
|
+
state: mapFromBeadsState(agent.state || 'idle'),
|
|
322
|
+
lastStateChange: new Date().toISOString(), // Would need to track this in beads
|
|
323
|
+
repoPath: metadata.repoPath,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get a worker by ID
|
|
329
|
+
*/
|
|
330
|
+
export async function getWorker(workerId: string): Promise<Worker | null> {
|
|
331
|
+
const agent = await findAgentByWorkerId(workerId);
|
|
332
|
+
if (!agent) return null;
|
|
333
|
+
|
|
334
|
+
// Get full agent details with metadata
|
|
335
|
+
const { stdout, exitCode } = await runBd(['show', agent.id, '--json']);
|
|
336
|
+
if (exitCode !== 0 || !stdout) return null;
|
|
337
|
+
|
|
338
|
+
const fullAgent = parseJson<AgentBead & { metadata?: AgentMetadata }>(stdout);
|
|
339
|
+
if (!fullAgent?.metadata) return null;
|
|
340
|
+
|
|
341
|
+
return agentToWorker(fullAgent, fullAgent.metadata);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* List all workers
|
|
346
|
+
*/
|
|
347
|
+
export async function listWorkers(): Promise<Worker[]> {
|
|
348
|
+
const { stdout, exitCode } = await runBd([
|
|
349
|
+
'list',
|
|
350
|
+
`--label=${AGENT_LABEL}`,
|
|
351
|
+
'--json',
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
if (exitCode !== 0 || !stdout) return [];
|
|
355
|
+
|
|
356
|
+
const agents = parseJson<Array<AgentBead & { metadata?: AgentMetadata }>>(stdout);
|
|
357
|
+
if (!agents) return [];
|
|
358
|
+
|
|
359
|
+
const workers: Worker[] = [];
|
|
360
|
+
for (const agent of agents) {
|
|
361
|
+
if (agent.metadata) {
|
|
362
|
+
workers.push(agentToWorker(agent, agent.metadata));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return workers;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Find worker by pane ID
|
|
371
|
+
*/
|
|
372
|
+
export async function findByPane(paneId: string): Promise<Worker | null> {
|
|
373
|
+
const workers = await listWorkers();
|
|
374
|
+
const normalizedPaneId = paneId.startsWith('%') ? paneId : `%${paneId}`;
|
|
375
|
+
return workers.find(w => w.paneId === normalizedPaneId) || null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Find worker by task ID
|
|
380
|
+
*/
|
|
381
|
+
export async function findByTask(taskId: string): Promise<Worker | null> {
|
|
382
|
+
// Worker ID typically matches task ID
|
|
383
|
+
return getWorker(taskId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Check if a worker exists for a task
|
|
388
|
+
*/
|
|
389
|
+
export async function hasWorkerForTask(taskId: string): Promise<boolean> {
|
|
390
|
+
const worker = await findByTask(taskId);
|
|
391
|
+
return worker !== null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// Daemon Management
|
|
396
|
+
// ============================================================================
|
|
397
|
+
|
|
398
|
+
interface DaemonStatus {
|
|
399
|
+
running: boolean;
|
|
400
|
+
pid?: number;
|
|
401
|
+
lastSync?: string;
|
|
402
|
+
autoCommit?: boolean;
|
|
403
|
+
autoPush?: boolean;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if beads daemon is running
|
|
408
|
+
*/
|
|
409
|
+
export async function checkDaemonStatus(): Promise<DaemonStatus> {
|
|
410
|
+
const { stdout, exitCode } = await runBd(['daemon', 'status', '--json']);
|
|
411
|
+
|
|
412
|
+
if (exitCode !== 0) {
|
|
413
|
+
return { running: false };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const status = parseJson<DaemonStatus>(stdout);
|
|
417
|
+
return status || { running: false };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Start beads daemon
|
|
422
|
+
*/
|
|
423
|
+
export async function startDaemon(options?: { autoCommit?: boolean; autoPush?: boolean }): Promise<boolean> {
|
|
424
|
+
const args = ['daemon', 'start'];
|
|
425
|
+
if (options?.autoCommit) args.push('--auto-commit');
|
|
426
|
+
if (options?.autoPush) args.push('--auto-push');
|
|
427
|
+
|
|
428
|
+
const { exitCode } = await runBd(args);
|
|
429
|
+
return exitCode === 0;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Stop beads daemon
|
|
434
|
+
*/
|
|
435
|
+
export async function stopDaemon(): Promise<boolean> {
|
|
436
|
+
const { exitCode } = await runBd(['daemon', 'stop']);
|
|
437
|
+
return exitCode === 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Ensure daemon is running, start if not
|
|
442
|
+
*/
|
|
443
|
+
export async function ensureDaemon(options?: { autoCommit?: boolean }): Promise<boolean> {
|
|
444
|
+
const status = await checkDaemonStatus();
|
|
445
|
+
if (status.running) return true;
|
|
446
|
+
|
|
447
|
+
return startDaemon(options);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// Worktree Management (via bd worktree)
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
export interface BeadsWorktreeInfo {
|
|
455
|
+
path: string;
|
|
456
|
+
branch: string;
|
|
457
|
+
name: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Create worktree via beads
|
|
462
|
+
*/
|
|
463
|
+
export async function createWorktree(name: string): Promise<BeadsWorktreeInfo | null> {
|
|
464
|
+
const { stdout, exitCode, stderr } = await runBd(['worktree', 'create', name, '--json']);
|
|
465
|
+
|
|
466
|
+
if (exitCode !== 0) {
|
|
467
|
+
console.error(`bd worktree create failed: ${stderr}`);
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const info = parseJson<BeadsWorktreeInfo>(stdout);
|
|
472
|
+
return info;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Remove worktree via beads
|
|
477
|
+
*/
|
|
478
|
+
export async function removeWorktree(name: string): Promise<boolean> {
|
|
479
|
+
const { exitCode } = await runBd(['worktree', 'remove', name]);
|
|
480
|
+
return exitCode === 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* List worktrees via beads
|
|
485
|
+
*/
|
|
486
|
+
export async function listWorktrees(): Promise<BeadsWorktreeInfo[]> {
|
|
487
|
+
const { stdout, exitCode } = await runBd(['worktree', 'list', '--json']);
|
|
488
|
+
|
|
489
|
+
if (exitCode !== 0 || !stdout) return [];
|
|
490
|
+
|
|
491
|
+
const worktrees = parseJson<BeadsWorktreeInfo[]>(stdout);
|
|
492
|
+
return worktrees || [];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Check if worktree exists via beads
|
|
497
|
+
*/
|
|
498
|
+
export async function worktreeExists(name: string): Promise<boolean> {
|
|
499
|
+
const worktrees = await listWorktrees();
|
|
500
|
+
return worktrees.some(wt => wt.name === name || wt.branch === name);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Get worktree info via beads
|
|
505
|
+
*/
|
|
506
|
+
export async function getWorktree(name: string): Promise<BeadsWorktreeInfo | null> {
|
|
507
|
+
const worktrees = await listWorktrees();
|
|
508
|
+
return worktrees.find(wt => wt.name === name || wt.branch === name) || null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ============================================================================
|
|
512
|
+
// Migration/Compatibility
|
|
513
|
+
// ============================================================================
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Register a worker (compatibility with worker-registry interface)
|
|
517
|
+
* Writes to both beads and JSON registry during migration
|
|
518
|
+
*/
|
|
519
|
+
export async function register(worker: Worker): Promise<void> {
|
|
520
|
+
await ensureAgent(worker.id, {
|
|
521
|
+
paneId: worker.paneId,
|
|
522
|
+
session: worker.session,
|
|
523
|
+
worktree: worker.worktree,
|
|
524
|
+
repoPath: worker.repoPath,
|
|
525
|
+
taskId: worker.taskId,
|
|
526
|
+
taskTitle: worker.taskTitle,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await setAgentState(worker.id, worker.state);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Unregister a worker (compatibility with worker-registry interface)
|
|
534
|
+
*/
|
|
535
|
+
export async function unregister(workerId: string): Promise<void> {
|
|
536
|
+
await unbindWork(workerId);
|
|
537
|
+
await deleteAgent(workerId);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Update worker state (compatibility with worker-registry interface)
|
|
542
|
+
*/
|
|
543
|
+
export async function updateState(workerId: string, state: WorkerState): Promise<void> {
|
|
544
|
+
await setAgentState(workerId, state);
|
|
545
|
+
await heartbeat(workerId);
|
|
546
|
+
}
|
package/src/lib/tmux.ts
CHANGED
|
@@ -222,13 +222,22 @@ export async function killPane(paneId: string): Promise<void> {
|
|
|
222
222
|
await executeTmux(`kill-pane -t '${paneId}'`);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Escape a string for safe use in shell single quotes.
|
|
227
|
+
* Replaces ' with '\'' (end quote, escaped quote, start quote).
|
|
228
|
+
*/
|
|
229
|
+
function escapeShellPath(path: string): string {
|
|
230
|
+
return path.replace(/'/g, "'\\''");
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
/**
|
|
226
234
|
* Split a tmux pane horizontally or vertically
|
|
227
235
|
*/
|
|
228
236
|
export async function splitPane(
|
|
229
237
|
targetPaneId: string,
|
|
230
238
|
direction: 'horizontal' | 'vertical' = 'vertical',
|
|
231
|
-
size?: number
|
|
239
|
+
size?: number,
|
|
240
|
+
workingDir?: string
|
|
232
241
|
): Promise<TmuxPane | null> {
|
|
233
242
|
// Build the split-window command
|
|
234
243
|
let splitCommand = 'split-window';
|
|
@@ -248,6 +257,11 @@ export async function splitPane(
|
|
|
248
257
|
splitCommand += ` -p ${size}`;
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
// Add working directory if specified
|
|
261
|
+
if (workingDir) {
|
|
262
|
+
splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
|
|
263
|
+
}
|
|
264
|
+
|
|
251
265
|
// Execute the split command
|
|
252
266
|
await executeTmux(splitCommand);
|
|
253
267
|
|
package/src/lib/version.ts
CHANGED
|
@@ -15,10 +15,14 @@ import { $ } from 'bun';
|
|
|
15
15
|
import { confirm } from '@inquirer/prompts';
|
|
16
16
|
import * as tmux from '../lib/tmux.js';
|
|
17
17
|
import * as registry from '../lib/worker-registry.js';
|
|
18
|
+
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
18
19
|
import { WorktreeManager } from '../lib/worktree.js';
|
|
19
20
|
import { join } from 'path';
|
|
20
21
|
import { homedir } from 'os';
|
|
21
22
|
|
|
23
|
+
// Use beads registry when enabled
|
|
24
|
+
const useBeads = beadsRegistry.isBeadsRegistryEnabled();
|
|
25
|
+
|
|
22
26
|
// ============================================================================
|
|
23
27
|
// Types
|
|
24
28
|
// ============================================================================
|
|
@@ -101,8 +105,22 @@ async function mergeToMain(
|
|
|
101
105
|
|
|
102
106
|
/**
|
|
103
107
|
* Remove worktree
|
|
108
|
+
* Uses bd worktree when beads registry is enabled
|
|
109
|
+
* Falls back to WorktreeManager otherwise
|
|
104
110
|
*/
|
|
105
111
|
async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
|
|
112
|
+
// Try bd worktree first when beads is enabled
|
|
113
|
+
if (useBeads) {
|
|
114
|
+
try {
|
|
115
|
+
const removed = await beadsRegistry.removeWorktree(taskId);
|
|
116
|
+
if (removed) return true;
|
|
117
|
+
// Fall through to WorktreeManager if bd worktree fails
|
|
118
|
+
} catch {
|
|
119
|
+
// Fall through
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback to WorktreeManager
|
|
106
124
|
try {
|
|
107
125
|
const manager = new WorktreeManager({
|
|
108
126
|
baseDir: WORKTREE_BASE,
|
|
@@ -141,8 +159,13 @@ export async function closeCommand(
|
|
|
141
159
|
options: CloseOptions = {}
|
|
142
160
|
): Promise<void> {
|
|
143
161
|
try {
|
|
144
|
-
// Find worker in registry
|
|
145
|
-
|
|
162
|
+
// Find worker in registry (check both during transition)
|
|
163
|
+
let worker = useBeads
|
|
164
|
+
? await beadsRegistry.findByTask(taskId)
|
|
165
|
+
: null;
|
|
166
|
+
if (!worker) {
|
|
167
|
+
worker = await registry.findByTask(taskId);
|
|
168
|
+
}
|
|
146
169
|
|
|
147
170
|
if (!worker) {
|
|
148
171
|
console.log(`ℹ️ No active worker for ${taskId}. Closing issue only.`);
|
|
@@ -207,7 +230,19 @@ export async function closeCommand(
|
|
|
207
230
|
await killWorkerPane(worker.paneId);
|
|
208
231
|
console.log(` ✅ Pane killed`);
|
|
209
232
|
|
|
210
|
-
// 5. Unregister worker
|
|
233
|
+
// 5. Unregister worker from both registries
|
|
234
|
+
if (useBeads) {
|
|
235
|
+
try {
|
|
236
|
+
// Unbind work from agent
|
|
237
|
+
await beadsRegistry.unbindWork(worker.id);
|
|
238
|
+
// Set agent state to done
|
|
239
|
+
await beadsRegistry.setAgentState(worker.id, 'done');
|
|
240
|
+
// Delete agent bead
|
|
241
|
+
await beadsRegistry.deleteAgent(worker.id);
|
|
242
|
+
} catch {
|
|
243
|
+
// Non-fatal if beads cleanup fails
|
|
244
|
+
}
|
|
245
|
+
}
|
|
211
246
|
await registry.unregister(worker.id);
|
|
212
247
|
console.log(` ✅ Worker unregistered`);
|
|
213
248
|
}
|