@automagik/genie 0.260202.1607 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "0.260202.1607",
3
+ "version": "0.260202.1901",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }