@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.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Daemon command - Manage beads daemon
3
+ *
4
+ * Usage:
5
+ * term daemon start - Start beads daemon (auto-commit, auto-sync)
6
+ * term daemon stop - Stop beads daemon
7
+ * term daemon status - Show daemon status
8
+ * term daemon restart - Restart daemon
9
+ *
10
+ * Options:
11
+ * --auto-commit - Enable auto-commit (default: true for start)
12
+ * --auto-push - Enable auto-push to remote
13
+ * --json - Output as JSON
14
+ */
15
+
16
+ import * as beadsRegistry from '../lib/beads-registry.js';
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ export interface DaemonStartOptions {
23
+ autoCommit?: boolean;
24
+ autoPush?: boolean;
25
+ }
26
+
27
+ export interface DaemonStatusOptions {
28
+ json?: boolean;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Commands
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Start the beads daemon
37
+ */
38
+ export async function startCommand(options: DaemonStartOptions = {}): Promise<void> {
39
+ try {
40
+ // Check if already running
41
+ const status = await beadsRegistry.checkDaemonStatus();
42
+ if (status.running) {
43
+ console.log('ℹ️ Daemon is already running');
44
+ if (status.pid) {
45
+ console.log(` PID: ${status.pid}`);
46
+ }
47
+ return;
48
+ }
49
+
50
+ console.log('🚀 Starting beads daemon...');
51
+ const started = await beadsRegistry.startDaemon({
52
+ autoCommit: options.autoCommit !== false, // Default to true
53
+ autoPush: options.autoPush,
54
+ });
55
+
56
+ if (started) {
57
+ console.log(' ✅ Daemon started');
58
+
59
+ // Show updated status
60
+ const newStatus = await beadsRegistry.checkDaemonStatus();
61
+ if (newStatus.pid) {
62
+ console.log(` PID: ${newStatus.pid}`);
63
+ }
64
+ if (newStatus.autoCommit) {
65
+ console.log(' Auto-commit: enabled');
66
+ }
67
+ if (newStatus.autoPush) {
68
+ console.log(' Auto-push: enabled');
69
+ }
70
+ } else {
71
+ console.error('❌ Failed to start daemon');
72
+ console.log(' Check `bd daemon status` for details');
73
+ process.exit(1);
74
+ }
75
+ } catch (error: any) {
76
+ console.error(`❌ Error: ${error.message}`);
77
+ process.exit(1);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Stop the beads daemon
83
+ */
84
+ export async function stopCommand(): Promise<void> {
85
+ try {
86
+ // Check if running
87
+ const status = await beadsRegistry.checkDaemonStatus();
88
+ if (!status.running) {
89
+ console.log('ℹ️ Daemon is not running');
90
+ return;
91
+ }
92
+
93
+ console.log('🛑 Stopping beads daemon...');
94
+ const stopped = await beadsRegistry.stopDaemon();
95
+
96
+ if (stopped) {
97
+ console.log(' ✅ Daemon stopped');
98
+ } else {
99
+ console.error('❌ Failed to stop daemon');
100
+ process.exit(1);
101
+ }
102
+ } catch (error: any) {
103
+ console.error(`❌ Error: ${error.message}`);
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Show daemon status
110
+ */
111
+ export async function statusCommand(options: DaemonStatusOptions = {}): Promise<void> {
112
+ try {
113
+ const status = await beadsRegistry.checkDaemonStatus();
114
+
115
+ if (options.json) {
116
+ console.log(JSON.stringify(status, null, 2));
117
+ return;
118
+ }
119
+
120
+ console.log('Beads Daemon Status');
121
+ console.log('───────────────────');
122
+ console.log(`Running: ${status.running ? '✅ yes' : '❌ no'}`);
123
+
124
+ if (status.running) {
125
+ if (status.pid) {
126
+ console.log(`PID: ${status.pid}`);
127
+ }
128
+ if (status.lastSync) {
129
+ console.log(`Last sync: ${status.lastSync}`);
130
+ }
131
+ if (status.autoCommit !== undefined) {
132
+ console.log(`Auto-commit: ${status.autoCommit ? 'enabled' : 'disabled'}`);
133
+ }
134
+ if (status.autoPush !== undefined) {
135
+ console.log(`Auto-push: ${status.autoPush ? 'enabled' : 'disabled'}`);
136
+ }
137
+ } else {
138
+ console.log('\nRun `term daemon start` to start the daemon');
139
+ }
140
+ } catch (error: any) {
141
+ console.error(`❌ Error: ${error.message}`);
142
+ process.exit(1);
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Restart the beads daemon
148
+ */
149
+ export async function restartCommand(options: DaemonStartOptions = {}): Promise<void> {
150
+ try {
151
+ // Check if running and stop
152
+ const status = await beadsRegistry.checkDaemonStatus();
153
+ if (status.running) {
154
+ console.log('🛑 Stopping beads daemon...');
155
+ await beadsRegistry.stopDaemon();
156
+ console.log(' ✅ Stopped');
157
+ }
158
+
159
+ // Start with new options
160
+ console.log('🚀 Starting beads daemon...');
161
+ const started = await beadsRegistry.startDaemon({
162
+ autoCommit: options.autoCommit !== false,
163
+ autoPush: options.autoPush,
164
+ });
165
+
166
+ if (started) {
167
+ console.log(' ✅ Daemon restarted');
168
+ } else {
169
+ console.error('❌ Failed to restart daemon');
170
+ process.exit(1);
171
+ }
172
+ } catch (error: any) {
173
+ console.error(`❌ Error: ${error.message}`);
174
+ process.exit(1);
175
+ }
176
+ }
@@ -12,10 +12,14 @@
12
12
  import { confirm } from '@inquirer/prompts';
13
13
  import * as tmux from '../lib/tmux.js';
14
14
  import * as registry from '../lib/worker-registry.js';
15
+ import * as beadsRegistry from '../lib/beads-registry.js';
15
16
  import { WorktreeManager } from '../lib/worktree.js';
16
17
  import { join } from 'path';
17
18
  import { homedir } from 'os';
18
19
 
20
+ // Use beads registry when enabled
21
+ const useBeads = beadsRegistry.isBeadsRegistryEnabled();
22
+
19
23
  // ============================================================================
20
24
  // Types
21
25
  // ============================================================================
@@ -49,8 +53,22 @@ async function killWorkerPane(paneId: string): Promise<boolean> {
49
53
 
50
54
  /**
51
55
  * Remove worktree
56
+ * Uses bd worktree when beads registry is enabled
57
+ * Falls back to WorktreeManager otherwise
52
58
  */
53
59
  async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
60
+ // Try bd worktree first when beads is enabled
61
+ if (useBeads) {
62
+ try {
63
+ const removed = await beadsRegistry.removeWorktree(taskId);
64
+ if (removed) return true;
65
+ // Fall through to WorktreeManager if bd worktree fails
66
+ } catch {
67
+ // Fall through
68
+ }
69
+ }
70
+
71
+ // Fallback to WorktreeManager
54
72
  try {
55
73
  const manager = new WorktreeManager({
56
74
  baseDir: WORKTREE_BASE,
@@ -77,19 +95,32 @@ export async function killCommand(
77
95
  options: KillOptions = {}
78
96
  ): Promise<void> {
79
97
  try {
80
- // Find worker by ID or pane
98
+ // Find worker by ID or pane (check both registries during transition)
81
99
  let worker = await registry.get(target);
82
100
 
101
+ if (!worker && useBeads) {
102
+ // Try beads registry
103
+ worker = await beadsRegistry.getWorker(target);
104
+ }
105
+
83
106
  if (!worker) {
84
107
  // Try finding by pane ID
85
108
  worker = await registry.findByPane(target);
86
109
  }
87
110
 
111
+ if (!worker && useBeads) {
112
+ worker = await beadsRegistry.findByPane(target);
113
+ }
114
+
88
115
  if (!worker) {
89
116
  // Try finding by task ID
90
117
  worker = await registry.findByTask(target);
91
118
  }
92
119
 
120
+ if (!worker && useBeads) {
121
+ worker = await beadsRegistry.findByTask(target);
122
+ }
123
+
93
124
  if (!worker) {
94
125
  console.error(`❌ Worker "${target}" not found.`);
95
126
  console.log(` Run \`term workers\` to see active workers.`);
@@ -127,7 +158,19 @@ export async function killCommand(
127
158
  }
128
159
  }
129
160
 
130
- // 3. Unregister worker
161
+ // 3. Unregister worker from both registries
162
+ if (useBeads) {
163
+ try {
164
+ // Unbind work from agent
165
+ await beadsRegistry.unbindWork(worker.id);
166
+ // Set agent state to error (killed, not done)
167
+ await beadsRegistry.setAgentState(worker.id, 'error');
168
+ // Delete agent bead
169
+ await beadsRegistry.deleteAgent(worker.id);
170
+ } catch {
171
+ // Non-fatal if beads cleanup fails
172
+ }
173
+ }
131
174
  await registry.unregister(worker.id);
132
175
  console.log(` ✅ Worker unregistered`);
133
176
 
@@ -38,6 +38,9 @@ export async function splitSessionPane(
38
38
  // Determine direction
39
39
  const splitDirection = direction === 'h' ? 'horizontal' : 'vertical';
40
40
 
41
+ // Get source pane's current working directory
42
+ const sourcePath = await tmux.executeTmux(`display-message -p -t '${paneId}' '#{pane_current_path}'`);
43
+
41
44
  // Handle workspace and worktree options
42
45
  let workingDir: string | undefined;
43
46
 
@@ -62,20 +65,18 @@ export async function splitSessionPane(
62
65
  workingDir = manager.getWorktreePath(options.worktree);
63
66
  } else if (options.workspace) {
64
67
  workingDir = options.workspace;
68
+ } else {
69
+ // Default to source pane's current directory
70
+ workingDir = sourcePath.trim() || undefined;
65
71
  }
66
72
 
67
- // Split pane
68
- const newPane = await tmux.splitPane(paneId, splitDirection);
73
+ // Split pane with working directory (tmux -c flag handles this natively)
74
+ const newPane = await tmux.splitPane(paneId, splitDirection, undefined, workingDir);
69
75
  if (!newPane) {
70
76
  console.error('❌ Failed to split pane');
71
77
  process.exit(1);
72
78
  }
73
79
 
74
- // Change to working directory if specified
75
- if (workingDir && newPane) {
76
- await tmux.executeTmux(`send-keys -t '${newPane.id}' 'cd ${workingDir}' Enter`);
77
- }
78
-
79
80
  console.log(`✅ Pane split ${splitDirection}ly in session "${sessionName}"`);
80
81
  if (workingDir) {
81
82
  console.log(` Working directory: ${workingDir}`);
@@ -15,11 +15,15 @@
15
15
  import { $ } from 'bun';
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 { EventMonitor, detectState } from '../lib/orchestrator/index.js';
20
21
  import { join } from 'path';
21
22
  import { homedir } from 'os';
22
23
 
24
+ // Use beads registry when enabled
25
+ const useBeads = beadsRegistry.isBeadsRegistryEnabled();
26
+
23
27
  // ============================================================================
24
28
  // Types
25
29
  // ============================================================================
@@ -135,11 +139,36 @@ async function getCurrentSession(): Promise<string | null> {
135
139
 
136
140
  /**
137
141
  * Create worktree for worker
142
+ * Uses bd worktree when beads registry is enabled (auto .beads redirect)
143
+ * Falls back to WorktreeManager otherwise
138
144
  */
139
145
  async function createWorktree(
140
146
  taskId: string,
141
147
  repoPath: string
142
148
  ): Promise<string | null> {
149
+ // Try bd worktree first when beads is enabled
150
+ if (useBeads) {
151
+ try {
152
+ // Check if worktree exists via beads
153
+ const existing = await beadsRegistry.getWorktree(taskId);
154
+ if (existing) {
155
+ console.log(`ℹ️ Worktree for ${taskId} already exists`);
156
+ return existing.path;
157
+ }
158
+
159
+ // Create via bd worktree (includes .beads redirect)
160
+ const info = await beadsRegistry.createWorktree(taskId);
161
+ if (info) {
162
+ return info.path;
163
+ }
164
+ // Fall through to WorktreeManager if bd worktree fails
165
+ console.log(`⚠️ bd worktree failed, falling back to git worktree`);
166
+ } catch (error: any) {
167
+ console.log(`⚠️ bd worktree error: ${error.message}, falling back`);
168
+ }
169
+ }
170
+
171
+ // Fallback to WorktreeManager
143
172
  try {
144
173
  const manager = new WorktreeManager({
145
174
  baseDir: WORKTREE_BASE,
@@ -210,6 +239,7 @@ async function spawnWorkerPane(
210
239
 
211
240
  /**
212
241
  * Start monitoring worker state and update registry
242
+ * Updates both beads and JSON registry during transition
213
243
  */
214
244
  function startWorkerMonitoring(
215
245
  workerId: string,
@@ -250,6 +280,10 @@ function startWorkerMonitoring(
250
280
  }
251
281
 
252
282
  try {
283
+ // Update both registries during transition
284
+ if (useBeads) {
285
+ await beadsRegistry.updateState(workerId, newState);
286
+ }
253
287
  await registry.updateState(workerId, newState);
254
288
  } catch {
255
289
  // Ignore errors in background monitoring
@@ -258,6 +292,9 @@ function startWorkerMonitoring(
258
292
 
259
293
  monitor.on('poll_error', () => {
260
294
  // Pane may have been killed - unregister worker
295
+ if (useBeads) {
296
+ beadsRegistry.unregister(workerId).catch(() => {});
297
+ }
261
298
  registry.unregister(workerId).catch(() => {});
262
299
  monitor.stop();
263
300
  });
@@ -282,6 +319,20 @@ export async function workCommand(
282
319
  // Get current working directory as repo path
283
320
  const repoPath = process.cwd();
284
321
 
322
+ // Ensure beads daemon is running for auto-sync
323
+ if (useBeads) {
324
+ const daemonStatus = await beadsRegistry.checkDaemonStatus();
325
+ if (!daemonStatus.running) {
326
+ console.log('🔄 Starting beads daemon for auto-sync...');
327
+ const started = await beadsRegistry.startDaemon({ autoCommit: true });
328
+ if (started) {
329
+ console.log(' ✅ Daemon started');
330
+ } else {
331
+ console.log(' ⚠️ Daemon failed to start (non-fatal)');
332
+ }
333
+ }
334
+ }
335
+
285
336
  // 1. Resolve target
286
337
  let issue: BeadsIssue | null = null;
287
338
 
@@ -307,8 +358,13 @@ export async function workCommand(
307
358
 
308
359
  const taskId = issue.id;
309
360
 
310
- // 2. Check not already assigned
311
- const existingWorker = await registry.findByTask(taskId);
361
+ // 2. Check not already assigned (check both registries)
362
+ let existingWorker = useBeads
363
+ ? await beadsRegistry.findByTask(taskId)
364
+ : null;
365
+ if (!existingWorker) {
366
+ existingWorker = await registry.findByTask(taskId);
367
+ }
312
368
  if (existingWorker) {
313
369
  console.error(`❌ ${taskId} already has a worker (pane ${existingWorker.paneId})`);
314
370
  console.log(` Run \`term kill ${existingWorker.id}\` first, or work on a different issue.`);
@@ -354,7 +410,7 @@ export async function workCommand(
354
410
 
355
411
  const { paneId } = paneResult;
356
412
 
357
- // 7. Register worker
413
+ // 7. Register worker (write to both registries during transition)
358
414
  const worker: registry.Worker = {
359
415
  id: taskId,
360
416
  paneId,
@@ -368,6 +424,29 @@ export async function workCommand(
368
424
  repoPath,
369
425
  };
370
426
 
427
+ // Register in beads (creates agent bead)
428
+ if (useBeads) {
429
+ try {
430
+ const agentId = await beadsRegistry.ensureAgent(taskId, {
431
+ paneId,
432
+ session,
433
+ worktree: worktreePath,
434
+ repoPath,
435
+ taskId,
436
+ taskTitle: issue.title,
437
+ });
438
+
439
+ // Bind work to agent
440
+ await beadsRegistry.bindWork(taskId, taskId);
441
+
442
+ // Set initial state
443
+ await beadsRegistry.setAgentState(taskId, 'spawning');
444
+ } catch (error: any) {
445
+ console.log(`⚠️ Beads registration failed: ${error.message} (non-fatal)`);
446
+ }
447
+ }
448
+
449
+ // Also register in JSON registry (parallel operation during transition)
371
450
  await registry.register(worker);
372
451
 
373
452
  // 8. Start Claude in pane
@@ -385,7 +464,10 @@ When you're done, commit your changes and let me know.`;
385
464
 
386
465
  await tmux.executeCommand(paneId, prompt, false, false);
387
466
 
388
- // 10. Update state to working
467
+ // 10. Update state to working (both registries)
468
+ if (useBeads) {
469
+ await beadsRegistry.setAgentState(taskId, 'working').catch(() => {});
470
+ }
389
471
  await registry.updateState(taskId, 'working');
390
472
 
391
473
  // 11. Start monitoring
@@ -10,8 +10,12 @@
10
10
  import { $ } from 'bun';
11
11
  import * as tmux from '../lib/tmux.js';
12
12
  import * as registry from '../lib/worker-registry.js';
13
+ import * as beadsRegistry from '../lib/beads-registry.js';
13
14
  import { detectState, stripAnsi } from '../lib/orchestrator/index.js';
14
15
 
16
+ // Use beads registry when enabled
17
+ const useBeads = beadsRegistry.isBeadsRegistryEnabled();
18
+
15
19
  // ============================================================================
16
20
  // Types
17
21
  // ============================================================================
@@ -153,7 +157,30 @@ function formatElapsed(startedAt: string): string {
153
157
 
154
158
  export async function workersCommand(options: WorkersOptions = {}): Promise<void> {
155
159
  try {
156
- const workers = await registry.list();
160
+ // Get workers from beads or JSON registry
161
+ // During transition, merge results from both
162
+ let workers: registry.Worker[] = [];
163
+
164
+ if (useBeads) {
165
+ try {
166
+ const beadsWorkers = await beadsRegistry.listWorkers();
167
+ workers = beadsWorkers;
168
+ } catch {
169
+ // Fallback to JSON registry
170
+ workers = await registry.list();
171
+ }
172
+ } else {
173
+ workers = await registry.list();
174
+ }
175
+
176
+ // Also check JSON registry for any workers not in beads
177
+ const jsonWorkers = await registry.list();
178
+ const beadsIds = new Set(workers.map(w => w.id));
179
+ for (const jw of jsonWorkers) {
180
+ if (!beadsIds.has(jw.id)) {
181
+ workers.push(jw);
182
+ }
183
+ }
157
184
 
158
185
  // Gather display data for each worker
159
186
  const displayData: WorkerDisplay[] = [];
@@ -166,10 +193,17 @@ export async function workersCommand(options: WorkersOptions = {}): Promise<void
166
193
  // Get live state from pane
167
194
  currentState = await getCurrentState(worker.paneId);
168
195
 
169
- // Update registry if state differs
196
+ // Update both registries if state differs
170
197
  const mappedState = mapDisplayStateToRegistry(currentState);
171
198
  if (mappedState && mappedState !== worker.state) {
199
+ if (useBeads) {
200
+ // Update beads and send heartbeat
201
+ await beadsRegistry.updateState(worker.id, mappedState).catch(() => {});
202
+ }
172
203
  await registry.updateState(worker.id, mappedState);
204
+ } else if (useBeads) {
205
+ // Just send heartbeat even if state unchanged
206
+ await beadsRegistry.heartbeat(worker.id).catch(() => {});
173
207
  }
174
208
  } else {
175
209
  currentState = '💀 dead';
package/src/term.ts CHANGED
@@ -20,6 +20,7 @@ import * as workCmd from './term-commands/work.js';
20
20
  import * as workersCmd from './term-commands/workers.js';
21
21
  import * as closeCmd from './term-commands/close.js';
22
22
  import * as killCmd from './term-commands/kill.js';
23
+ import * as daemonCmd from './term-commands/daemon.js';
23
24
 
24
25
  const program = new Command();
25
26
 
@@ -40,7 +41,8 @@ Worker Orchestration:
40
41
  term work next - Work on next ready issue
41
42
  term workers - List all workers and states
42
43
  term close <bd-id> - Close issue, cleanup worker
43
- term kill <worker> - Force kill a stuck worker`)
44
+ term kill <worker> - Force kill a stuck worker
45
+ term daemon start - Start beads daemon for auto-sync`)
44
46
  .version(VERSION);
45
47
 
46
48
  // Session management
@@ -297,6 +299,42 @@ program
297
299
  });
298
300
  });
299
301
 
302
+ // Daemon management (beads auto-sync)
303
+ const daemonProgram = program.command('daemon').description('Manage beads daemon for auto-sync');
304
+
305
+ daemonProgram
306
+ .command('start')
307
+ .description('Start beads daemon (auto-commit, auto-sync)')
308
+ .option('--no-auto-commit', 'Disable auto-commit')
309
+ .option('--auto-push', 'Enable auto-push to remote')
310
+ .action(async (options: daemonCmd.DaemonStartOptions) => {
311
+ await daemonCmd.startCommand(options);
312
+ });
313
+
314
+ daemonProgram
315
+ .command('stop')
316
+ .description('Stop beads daemon')
317
+ .action(async () => {
318
+ await daemonCmd.stopCommand();
319
+ });
320
+
321
+ daemonProgram
322
+ .command('status')
323
+ .description('Show daemon status')
324
+ .option('--json', 'Output as JSON')
325
+ .action(async (options: daemonCmd.DaemonStatusOptions) => {
326
+ await daemonCmd.statusCommand(options);
327
+ });
328
+
329
+ daemonProgram
330
+ .command('restart')
331
+ .description('Restart beads daemon')
332
+ .option('--no-auto-commit', 'Disable auto-commit')
333
+ .option('--auto-push', 'Enable auto-push to remote')
334
+ .action(async (options: daemonCmd.DaemonStartOptions) => {
335
+ await daemonCmd.restartCommand(options);
336
+ });
337
+
300
338
  // Orchestration commands (Claude Code automation)
301
339
  const orcProgram = program.command('orc').description('Orchestrate Claude Code sessions');
302
340