@automagik/genie 0.260202.1833 → 0.260203.43

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.
Files changed (108) hide show
  1. package/.beads/README.md +81 -0
  2. package/.beads/config.yaml +67 -0
  3. package/.beads/interactions.jsonl +0 -0
  4. package/.beads/issues.jsonl +9 -0
  5. package/.beads/metadata.json +4 -0
  6. package/.claude/skills/brainstorm/SKILL.md +53 -0
  7. package/.claude/skills/genie-base/SKILL.md +66 -0
  8. package/.claude/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  9. package/.claude/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  10. package/.claude/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  11. package/.claude/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  12. package/.claude/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  13. package/.claude/skills/genie-base/assets/workspace/ROLE.md +14 -0
  14. package/.claude/skills/genie-base/assets/workspace/SOUL.md +36 -0
  15. package/.claude/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  16. package/.claude/skills/genie-base/assets/workspace/USER.md +13 -0
  17. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-30.md +6 -0
  18. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-31.md +16 -0
  19. package/.claude/skills/genie-base/assets/workspace/memory/882c22be-9710-41c1-91f8-ed82947ef6ce.txt +1 -0
  20. package/.claude/skills/genie-base/scripts/install-workspace.sh +107 -0
  21. package/.claude/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  22. package/.claude/skills/genie-blank-init/SKILL.md +37 -0
  23. package/.claude/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  24. package/.claude/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  25. package/.claude/skills/genie-blank-init/assets/SOUL.md +10 -0
  26. package/.claude/skills/genie-blank-init/assets/USER.md +9 -0
  27. package/.claude/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  28. package/.claude/skills/genie-forge/SKILL.md +171 -0
  29. package/.claude/skills/genie-plan-review/CLAUDE.md +11 -0
  30. package/.claude/skills/genie-plan-review/SKILL.md +53 -0
  31. package/.claude/skills/genie-review/SKILL.md +171 -0
  32. package/.claude/skills/genie-wish/SKILL.md +141 -0
  33. package/.claude-plugin/marketplace.json +18 -0
  34. package/.genie/.gitkeep +3 -0
  35. package/.genie/backlog/hooks-v2.md +82 -0
  36. package/.genie/wishes/upgrade-brainstorm-handoff/wish.md +124 -0
  37. package/.gitattributes +3 -0
  38. package/AGENTS.md +75 -0
  39. package/bun.lock +55 -0
  40. package/dist/claudio.js +1 -1
  41. package/dist/genie.js +1 -1
  42. package/dist/term.js +123 -99
  43. package/docs/CO-ORCHESTRATION-GUIDE.md +368 -0
  44. package/package.json +5 -1
  45. package/plugin/.claude-plugin/plugin.json +18 -0
  46. package/plugin/README.md +120 -0
  47. package/plugin/agents/implementor.md +92 -0
  48. package/plugin/agents/quality-reviewer.md +113 -0
  49. package/plugin/agents/spec-reviewer.md +90 -0
  50. package/plugin/hooks/hooks.json +3 -0
  51. package/plugin/references/review-criteria.md +72 -0
  52. package/plugin/references/wish-template.md +92 -0
  53. package/plugin/scripts/genie.cjs +141 -0
  54. package/plugin/scripts/smart-install.js +308 -0
  55. package/plugin/scripts/src/install-genie-cli.sh +120 -0
  56. package/plugin/scripts/src/validate-completion.ts +142 -0
  57. package/plugin/scripts/src/validate-wish.ts +137 -0
  58. package/plugin/scripts/term.cjs +229 -0
  59. package/plugin/scripts/validate-completion.cjs +16 -0
  60. package/plugin/scripts/validate-wish.cjs +17 -0
  61. package/plugin/scripts/worker-service.cjs +28 -0
  62. package/plugin/skills/brainstorm/SKILL.md +106 -0
  63. package/plugin/skills/forge/SKILL.md +171 -0
  64. package/plugin/skills/genie-base/SKILL.md +99 -0
  65. package/plugin/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  66. package/plugin/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  67. package/plugin/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  68. package/plugin/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  69. package/plugin/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  70. package/plugin/skills/genie-base/assets/workspace/ROLE.md +14 -0
  71. package/plugin/skills/genie-base/assets/workspace/SOUL.md +36 -0
  72. package/plugin/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  73. package/plugin/skills/genie-base/assets/workspace/USER.md +13 -0
  74. package/plugin/skills/genie-base/scripts/install-workspace.sh +107 -0
  75. package/plugin/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  76. package/plugin/skills/genie-blank-init/SKILL.md +73 -0
  77. package/plugin/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  78. package/plugin/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  79. package/plugin/skills/genie-blank-init/assets/SOUL.md +10 -0
  80. package/plugin/skills/genie-blank-init/assets/USER.md +9 -0
  81. package/plugin/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  82. package/plugin/skills/genie-cli-dev/CLAUDE.md +19 -0
  83. package/plugin/skills/genie-cli-dev/SKILL.md +292 -0
  84. package/plugin/skills/plan-review/SKILL.md +101 -0
  85. package/plugin/skills/review/SKILL.md +221 -0
  86. package/plugin/skills/wish/SKILL.md +110 -0
  87. package/plugin/skills/work-orchestration/SKILL.md +110 -0
  88. package/scripts/build.js +132 -0
  89. package/scripts/smart-install.js +308 -0
  90. package/scripts/sync.js +134 -0
  91. package/src/lib/beads-registry.ts +595 -0
  92. package/src/lib/orchestrator/event-monitor.ts +2 -0
  93. package/src/lib/skill-loader.ts +215 -0
  94. package/src/lib/tmux.ts +30 -11
  95. package/src/lib/version.ts +1 -1
  96. package/src/lib/worker-registry.ts +10 -0
  97. package/src/services/worker-service.ts +351 -0
  98. package/src/term-commands/close.ts +48 -3
  99. package/src/term-commands/create.ts +95 -0
  100. package/src/term-commands/daemon.ts +176 -0
  101. package/src/term-commands/kill.ts +56 -2
  102. package/src/term-commands/orchestrate.ts +3 -2
  103. package/src/term-commands/send.ts +43 -15
  104. package/src/term-commands/spawn.ts +446 -0
  105. package/src/term-commands/split.ts +20 -8
  106. package/src/term-commands/work.ts +279 -37
  107. package/src/term-commands/workers.ts +36 -2
  108. package/src/term.ts +120 -7
@@ -0,0 +1,595 @@
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
+ claudeSessionId?: string;
89
+ }
90
+ ): Promise<string> {
91
+ // Create agent bead with metadata
92
+ const title = `Worker: ${workerId}`;
93
+ const { stdout, exitCode } = await runBd([
94
+ 'create',
95
+ `--title=${title}`,
96
+ '--type=agent',
97
+ `--label=${AGENT_LABEL}`,
98
+ `--label=worker:${workerId}`,
99
+ '--json',
100
+ ]);
101
+
102
+ if (exitCode !== 0) {
103
+ throw new Error(`Failed to create agent bead: ${stdout}`);
104
+ }
105
+
106
+ const created = parseJson<{ id: string }>(stdout);
107
+ if (!created?.id) {
108
+ throw new Error('Failed to parse created agent bead');
109
+ }
110
+
111
+ // Store worker metadata in agent
112
+ const metadataJson = JSON.stringify({
113
+ paneId: metadata.paneId,
114
+ session: metadata.session,
115
+ worktree: metadata.worktree,
116
+ repoPath: metadata.repoPath,
117
+ taskId: metadata.taskId,
118
+ taskTitle: metadata.taskTitle,
119
+ claudeSessionId: metadata.claudeSessionId,
120
+ startedAt: new Date().toISOString(),
121
+ });
122
+
123
+ await runBd(['update', created.id, `--metadata=${metadataJson}`]);
124
+
125
+ return created.id;
126
+ }
127
+
128
+ /**
129
+ * Ensure an agent bead exists for a worker, creating if needed
130
+ */
131
+ export async function ensureAgent(
132
+ workerId: string,
133
+ metadata: {
134
+ paneId: string;
135
+ session: string;
136
+ worktree: string | null;
137
+ repoPath: string;
138
+ taskId: string;
139
+ taskTitle?: string;
140
+ claudeSessionId?: string;
141
+ }
142
+ ): Promise<string> {
143
+ // Check if agent already exists
144
+ const existing = await findAgentByWorkerId(workerId);
145
+ if (existing) {
146
+ return existing.id;
147
+ }
148
+
149
+ return createAgent(workerId, metadata);
150
+ }
151
+
152
+ /**
153
+ * Find agent bead by worker ID
154
+ */
155
+ async function findAgentByWorkerId(workerId: string): Promise<AgentBead | null> {
156
+ const { stdout, exitCode } = await runBd([
157
+ 'list',
158
+ `--label=${AGENT_LABEL}`,
159
+ `--label=worker:${workerId}`,
160
+ '--json',
161
+ ]);
162
+
163
+ if (exitCode !== 0 || !stdout) return null;
164
+
165
+ const agents = parseJson<AgentBead[]>(stdout);
166
+ return agents?.[0] || null;
167
+ }
168
+
169
+ /**
170
+ * Delete an agent bead
171
+ */
172
+ export async function deleteAgent(agentIdOrWorkerId: string): Promise<boolean> {
173
+ // Try direct ID first
174
+ let { exitCode } = await runBd(['delete', agentIdOrWorkerId]);
175
+ if (exitCode === 0) return true;
176
+
177
+ // Try finding by worker ID
178
+ const agent = await findAgentByWorkerId(agentIdOrWorkerId);
179
+ if (agent) {
180
+ ({ exitCode } = await runBd(['delete', agent.id]));
181
+ return exitCode === 0;
182
+ }
183
+
184
+ return false;
185
+ }
186
+
187
+ // ============================================================================
188
+ // Agent State Management
189
+ // ============================================================================
190
+
191
+ /**
192
+ * Map our WorkerState to beads agent states
193
+ */
194
+ function mapToBeadsState(state: WorkerState): string {
195
+ switch (state) {
196
+ case 'spawning':
197
+ return 'spawning';
198
+ case 'working':
199
+ return 'working';
200
+ case 'idle':
201
+ return 'idle';
202
+ case 'permission':
203
+ return 'blocked';
204
+ case 'question':
205
+ return 'blocked';
206
+ case 'done':
207
+ return 'done';
208
+ case 'error':
209
+ return 'error';
210
+ default:
211
+ return 'unknown';
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Map beads agent state to WorkerState
217
+ */
218
+ function mapFromBeadsState(beadsState: string): WorkerState {
219
+ switch (beadsState) {
220
+ case 'spawning':
221
+ return 'spawning';
222
+ case 'working':
223
+ return 'working';
224
+ case 'idle':
225
+ return 'idle';
226
+ case 'blocked':
227
+ return 'permission'; // Could be permission or question
228
+ case 'done':
229
+ return 'done';
230
+ case 'error':
231
+ return 'error';
232
+ default:
233
+ return 'idle';
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Set agent state
239
+ */
240
+ export async function setAgentState(workerId: string, state: WorkerState): Promise<void> {
241
+ const agent = await findAgentByWorkerId(workerId);
242
+ if (!agent) {
243
+ throw new Error(`Agent not found for worker ${workerId}`);
244
+ }
245
+
246
+ const beadsState = mapToBeadsState(state);
247
+ const { exitCode, stderr } = await runBd(['agent', 'state', agent.id, beadsState]);
248
+
249
+ if (exitCode !== 0) {
250
+ throw new Error(`Failed to set agent state: ${stderr}`);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Send heartbeat for agent
256
+ */
257
+ export async function heartbeat(workerId: string): Promise<void> {
258
+ const agent = await findAgentByWorkerId(workerId);
259
+ if (!agent) return; // Silently ignore if agent doesn't exist
260
+
261
+ await runBd(['agent', 'heartbeat', agent.id]);
262
+ }
263
+
264
+ // ============================================================================
265
+ // Work Binding (Slots)
266
+ // ============================================================================
267
+
268
+ /**
269
+ * Bind work (task) to an agent
270
+ */
271
+ export async function bindWork(workerId: string, taskId: string): Promise<void> {
272
+ const agent = await findAgentByWorkerId(workerId);
273
+ if (!agent) {
274
+ throw new Error(`Agent not found for worker ${workerId}`);
275
+ }
276
+
277
+ const { exitCode, stderr } = await runBd(['slot', 'set', agent.id, 'hook', taskId]);
278
+
279
+ if (exitCode !== 0) {
280
+ throw new Error(`Failed to bind work: ${stderr}`);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Unbind work from an agent
286
+ */
287
+ export async function unbindWork(workerId: string): Promise<void> {
288
+ const agent = await findAgentByWorkerId(workerId);
289
+ if (!agent) return; // Silently ignore if agent doesn't exist
290
+
291
+ await runBd(['slot', 'clear', agent.id, 'hook']);
292
+ }
293
+
294
+ // ============================================================================
295
+ // Worker Query Functions
296
+ // ============================================================================
297
+
298
+ interface AgentMetadata {
299
+ paneId: string;
300
+ session: string;
301
+ worktree: string | null;
302
+ repoPath: string;
303
+ taskId: string;
304
+ taskTitle?: string;
305
+ startedAt: string;
306
+ wishSlug?: string;
307
+ groupNumber?: number;
308
+ claudeSessionId?: string;
309
+ }
310
+
311
+ /**
312
+ * Convert agent bead to Worker interface
313
+ */
314
+ function agentToWorker(agent: AgentBead, metadata: AgentMetadata): Worker {
315
+ return {
316
+ id: metadata.taskId, // Worker ID matches task ID
317
+ paneId: metadata.paneId,
318
+ session: metadata.session,
319
+ worktree: metadata.worktree,
320
+ taskId: metadata.taskId,
321
+ taskTitle: metadata.taskTitle,
322
+ wishSlug: metadata.wishSlug,
323
+ groupNumber: metadata.groupNumber,
324
+ startedAt: metadata.startedAt,
325
+ state: mapFromBeadsState(agent.state || 'idle'),
326
+ lastStateChange: new Date().toISOString(), // Would need to track this in beads
327
+ repoPath: metadata.repoPath,
328
+ claudeSessionId: metadata.claudeSessionId,
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Get a worker by ID
334
+ */
335
+ export async function getWorker(workerId: string): Promise<Worker | null> {
336
+ const agent = await findAgentByWorkerId(workerId);
337
+ if (!agent) return null;
338
+
339
+ // Get full agent details with metadata
340
+ const { stdout, exitCode } = await runBd(['show', agent.id, '--json']);
341
+ if (exitCode !== 0 || !stdout) return null;
342
+
343
+ const fullAgent = parseJson<AgentBead & { metadata?: AgentMetadata }>(stdout);
344
+ if (!fullAgent?.metadata) return null;
345
+
346
+ return agentToWorker(fullAgent, fullAgent.metadata);
347
+ }
348
+
349
+ /**
350
+ * List all workers
351
+ */
352
+ export async function listWorkers(): Promise<Worker[]> {
353
+ const { stdout, exitCode } = await runBd([
354
+ 'list',
355
+ `--label=${AGENT_LABEL}`,
356
+ '--json',
357
+ ]);
358
+
359
+ if (exitCode !== 0 || !stdout) return [];
360
+
361
+ const agents = parseJson<Array<AgentBead & { metadata?: AgentMetadata }>>(stdout);
362
+ if (!agents) return [];
363
+
364
+ const workers: Worker[] = [];
365
+ for (const agent of agents) {
366
+ if (agent.metadata) {
367
+ workers.push(agentToWorker(agent, agent.metadata));
368
+ }
369
+ }
370
+
371
+ return workers;
372
+ }
373
+
374
+ /**
375
+ * Find worker by pane ID
376
+ */
377
+ export async function findByPane(paneId: string): Promise<Worker | null> {
378
+ const workers = await listWorkers();
379
+ const normalizedPaneId = paneId.startsWith('%') ? paneId : `%${paneId}`;
380
+ return workers.find(w => w.paneId === normalizedPaneId) || null;
381
+ }
382
+
383
+ /**
384
+ * Find worker by task ID
385
+ */
386
+ export async function findByTask(taskId: string): Promise<Worker | null> {
387
+ // Worker ID typically matches task ID
388
+ return getWorker(taskId);
389
+ }
390
+
391
+ /**
392
+ * Check if a worker exists for a task
393
+ */
394
+ export async function hasWorkerForTask(taskId: string): Promise<boolean> {
395
+ const worker = await findByTask(taskId);
396
+ return worker !== null;
397
+ }
398
+
399
+ /**
400
+ * Find worker by Claude session ID
401
+ */
402
+ export async function findBySessionId(sessionId: string): Promise<Worker | null> {
403
+ const workers = await listWorkers();
404
+ return workers.find(w => w.claudeSessionId === sessionId) || null;
405
+ }
406
+
407
+ // ============================================================================
408
+ // Daemon Management
409
+ // ============================================================================
410
+
411
+ interface DaemonStatus {
412
+ running: boolean;
413
+ pid?: number;
414
+ lastSync?: string;
415
+ autoCommit?: boolean;
416
+ autoPush?: boolean;
417
+ }
418
+
419
+ /**
420
+ * Check if beads daemon is running
421
+ */
422
+ export async function checkDaemonStatus(): Promise<DaemonStatus> {
423
+ const { stdout, exitCode } = await runBd(['daemon', 'status', '--json']);
424
+
425
+ if (exitCode !== 0) {
426
+ return { running: false };
427
+ }
428
+
429
+ const status = parseJson<DaemonStatus>(stdout);
430
+ return status || { running: false };
431
+ }
432
+
433
+ /**
434
+ * Start beads daemon
435
+ */
436
+ export async function startDaemon(options?: { autoCommit?: boolean; autoPush?: boolean }): Promise<boolean> {
437
+ const args = ['daemon', 'start'];
438
+ if (options?.autoCommit) args.push('--auto-commit');
439
+ if (options?.autoPush) args.push('--auto-push');
440
+
441
+ const { exitCode } = await runBd(args);
442
+ return exitCode === 0;
443
+ }
444
+
445
+ /**
446
+ * Stop beads daemon
447
+ */
448
+ export async function stopDaemon(): Promise<boolean> {
449
+ const { exitCode } = await runBd(['daemon', 'stop']);
450
+ return exitCode === 0;
451
+ }
452
+
453
+ /**
454
+ * Ensure daemon is running, start if not
455
+ */
456
+ export async function ensureDaemon(options?: { autoCommit?: boolean }): Promise<boolean> {
457
+ const status = await checkDaemonStatus();
458
+ if (status.running) return true;
459
+
460
+ return startDaemon(options);
461
+ }
462
+
463
+ // ============================================================================
464
+ // Worktree Management (via bd worktree)
465
+ // ============================================================================
466
+
467
+ export interface BeadsWorktreeInfo {
468
+ path: string;
469
+ branch: string;
470
+ name: string;
471
+ }
472
+
473
+ /**
474
+ * Create worktree via beads
475
+ */
476
+ export async function createWorktree(name: string): Promise<BeadsWorktreeInfo | null> {
477
+ const { stdout, exitCode, stderr } = await runBd(['worktree', 'create', name, '--json']);
478
+
479
+ if (exitCode !== 0) {
480
+ console.error(`bd worktree create failed: ${stderr}`);
481
+ return null;
482
+ }
483
+
484
+ const info = parseJson<BeadsWorktreeInfo>(stdout);
485
+ return info;
486
+ }
487
+
488
+ /**
489
+ * Remove worktree via beads
490
+ */
491
+ export async function removeWorktree(name: string): Promise<boolean> {
492
+ const { exitCode } = await runBd(['worktree', 'remove', name]);
493
+ return exitCode === 0;
494
+ }
495
+
496
+ /**
497
+ * List worktrees via beads
498
+ */
499
+ export async function listWorktrees(): Promise<BeadsWorktreeInfo[]> {
500
+ const { stdout, exitCode } = await runBd(['worktree', 'list', '--json']);
501
+
502
+ if (exitCode !== 0 || !stdout) return [];
503
+
504
+ const worktrees = parseJson<BeadsWorktreeInfo[]>(stdout);
505
+ return worktrees || [];
506
+ }
507
+
508
+ /**
509
+ * Check if worktree exists via beads
510
+ */
511
+ export async function worktreeExists(name: string): Promise<boolean> {
512
+ const worktrees = await listWorktrees();
513
+ return worktrees.some(wt => wt.name === name || wt.branch === name);
514
+ }
515
+
516
+ /**
517
+ * Get worktree info via beads
518
+ */
519
+ export async function getWorktree(name: string): Promise<BeadsWorktreeInfo | null> {
520
+ const worktrees = await listWorktrees();
521
+ return worktrees.find(wt => wt.name === name || wt.branch === name) || null;
522
+ }
523
+
524
+ // ============================================================================
525
+ // Migration/Compatibility
526
+ // ============================================================================
527
+
528
+ /**
529
+ * Register a worker (compatibility with worker-registry interface)
530
+ * Writes to both beads and JSON registry during migration
531
+ */
532
+ export async function register(worker: Worker): Promise<void> {
533
+ await ensureAgent(worker.id, {
534
+ paneId: worker.paneId,
535
+ session: worker.session,
536
+ worktree: worker.worktree,
537
+ repoPath: worker.repoPath,
538
+ taskId: worker.taskId,
539
+ taskTitle: worker.taskTitle,
540
+ claudeSessionId: worker.claudeSessionId,
541
+ });
542
+
543
+ await setAgentState(worker.id, worker.state);
544
+ }
545
+
546
+ /**
547
+ * Unregister a worker (compatibility with worker-registry interface)
548
+ */
549
+ export async function unregister(workerId: string): Promise<void> {
550
+ await unbindWork(workerId);
551
+ await deleteAgent(workerId);
552
+ }
553
+
554
+ /**
555
+ * Update worker state (compatibility with worker-registry interface)
556
+ */
557
+ export async function updateState(workerId: string, state: WorkerState): Promise<void> {
558
+ await setAgentState(workerId, state);
559
+ await heartbeat(workerId);
560
+ }
561
+
562
+ /**
563
+ * Update Claude session ID for a worker
564
+ */
565
+ export async function updateSessionId(workerId: string, sessionId: string): Promise<void> {
566
+ const agent = await findAgentByWorkerId(workerId);
567
+ if (!agent) {
568
+ throw new Error(`Agent not found for worker ${workerId}`);
569
+ }
570
+
571
+ // Get current metadata
572
+ const { stdout, exitCode } = await runBd(['show', agent.id, '--json']);
573
+ if (exitCode !== 0 || !stdout) {
574
+ throw new Error(`Failed to get agent metadata for ${workerId}`);
575
+ }
576
+
577
+ const fullAgent = parseJson<AgentBead & { metadata?: AgentMetadata }>(stdout);
578
+ if (!fullAgent?.metadata) {
579
+ throw new Error(`Agent ${workerId} has no metadata`);
580
+ }
581
+
582
+ // Update metadata with new session ID
583
+ const updatedMetadata = { ...fullAgent.metadata, claudeSessionId: sessionId };
584
+ const metadataJson = JSON.stringify(updatedMetadata);
585
+
586
+ const { exitCode: updateExitCode, stderr } = await runBd([
587
+ 'update',
588
+ agent.id,
589
+ `--metadata=${metadataJson}`,
590
+ ]);
591
+
592
+ if (updateExitCode !== 0) {
593
+ throw new Error(`Failed to update session ID: ${stderr}`);
594
+ }
595
+ }
@@ -96,7 +96,9 @@ export class EventMonitor extends EventEmitter {
96
96
  await this.poll();
97
97
 
98
98
  // Start polling
99
+ // Use unref() so the timer doesn't prevent process exit
99
100
  this.pollTimer = setInterval(() => this.poll(), this.options.pollIntervalMs);
101
+ this.pollTimer.unref();
100
102
 
101
103
  this.emit('started', { sessionName: this.sessionName, paneId: this.paneId });
102
104
  }