@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
@@ -9,16 +9,21 @@
9
9
  * Options:
10
10
  * --no-worktree - Use shared repo instead of worktree
11
11
  * -s, --session <name> - Target tmux session
12
- * --focus - Focus the worker pane (default: true)
12
+ * --focus - Focus the worker pane (default: false)
13
+ * --resume - Resume previous Claude session if available (default: true)
14
+ * --no-resume - Start fresh session even if previous exists
13
15
  */
14
16
 
15
17
  import { $ } from 'bun';
18
+ import { randomUUID } from 'crypto';
16
19
  import * as tmux from '../lib/tmux.js';
17
20
  import * as registry from '../lib/worker-registry.js';
18
- import { WorktreeManager } from '../lib/worktree.js';
19
- import { EventMonitor, detectState } from '../lib/orchestrator/index.js';
21
+ import * as beadsRegistry from '../lib/beads-registry.js';
22
+ import { EventMonitor } from '../lib/orchestrator/index.js';
20
23
  import { join } from 'path';
21
- import { homedir } from 'os';
24
+
25
+ // Use beads registry when enabled
26
+ const useBeads = beadsRegistry.isBeadsRegistryEnabled();
22
27
 
23
28
  // ============================================================================
24
29
  // Types
@@ -29,12 +34,15 @@ export interface WorkOptions {
29
34
  session?: string;
30
35
  focus?: boolean;
31
36
  prompt?: string;
37
+ /** Resume previous Claude session if available */
38
+ resume?: boolean;
32
39
  }
33
40
 
34
41
  interface BeadsIssue {
35
42
  id: string;
36
43
  title: string;
37
44
  status: string;
45
+ description?: string;
38
46
  blockedBy?: string[];
39
47
  }
40
48
 
@@ -42,7 +50,8 @@ interface BeadsIssue {
42
50
  // Configuration
43
51
  // ============================================================================
44
52
 
45
- const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
53
+ // Worktrees are created inside the project at .genie/worktrees/<taskId>
54
+ const WORKTREE_DIR_NAME = '.genie/worktrees';
46
55
 
47
56
  // ============================================================================
48
57
  // Helper Functions
@@ -68,11 +77,15 @@ async function getBeadsIssue(id: string): Promise<BeadsIssue | null> {
68
77
  if (exitCode !== 0 || !stdout) return null;
69
78
 
70
79
  try {
71
- const issue = JSON.parse(stdout);
80
+ const parsed = JSON.parse(stdout);
81
+ // bd show --json returns an array with single element
82
+ const issue = Array.isArray(parsed) ? parsed[0] : parsed;
83
+ if (!issue) return null;
72
84
  return {
73
85
  id: issue.id,
74
86
  title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
75
87
  status: issue.status,
88
+ description: issue.description,
76
89
  blockedBy: issue.blockedBy || [],
77
90
  };
78
91
  } catch {
@@ -95,6 +108,7 @@ async function getNextReadyIssue(): Promise<BeadsIssue | null> {
95
108
  id: issue.id,
96
109
  title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
97
110
  status: issue.status,
111
+ description: issue.description,
98
112
  blockedBy: issue.blockedBy || [],
99
113
  };
100
114
  }
@@ -134,27 +148,54 @@ async function getCurrentSession(): Promise<string | null> {
134
148
  }
135
149
 
136
150
  /**
137
- * Create worktree for worker
151
+ * Create worktree for worker in .genie/worktrees/<taskId>
152
+ * Creates a .genie redirect file so bd commands work in the worktree
138
153
  */
139
154
  async function createWorktree(
140
155
  taskId: string,
141
156
  repoPath: string
142
157
  ): Promise<string | null> {
158
+ const fs = await import('fs/promises');
159
+ const worktreeDir = join(repoPath, WORKTREE_DIR_NAME);
160
+ const worktreePath = join(worktreeDir, taskId);
161
+
162
+ // Ensure .genie/worktrees exists
143
163
  try {
144
- const manager = new WorktreeManager({
145
- baseDir: WORKTREE_BASE,
146
- repoPath,
147
- });
164
+ await fs.mkdir(worktreeDir, { recursive: true });
165
+ } catch {
166
+ // Directory may already exist
167
+ }
148
168
 
149
- // Check if worktree already exists
150
- if (await manager.worktreeExists(taskId)) {
169
+ // Check if worktree already exists
170
+ try {
171
+ const stat = await fs.stat(worktreePath);
172
+ if (stat.isDirectory()) {
151
173
  console.log(`ℹ️ Worktree for ${taskId} already exists`);
152
- return manager.getWorktreePath(taskId);
174
+ return worktreePath;
175
+ }
176
+ } catch {
177
+ // Doesn't exist, will create
178
+ }
179
+
180
+ // Create worktree using git directly (with branch)
181
+ const branchName = `work/${taskId}`;
182
+ try {
183
+ // Create branch if it doesn't exist (ignore error if it already exists)
184
+ try {
185
+ await $`git -C ${repoPath} branch ${branchName}`.quiet();
186
+ } catch {
187
+ // Branch may already exist, that's ok
153
188
  }
154
189
 
155
- // Create new worktree with branch
156
- const info = await manager.createWorktree(taskId, true);
157
- return info.path;
190
+ // Create worktree
191
+ await $`git -C ${repoPath} worktree add ${worktreePath} ${branchName}`.quiet();
192
+
193
+ // Set up .genie redirect so bd commands work in the worktree
194
+ const genieRedirect = join(worktreePath, '.genie');
195
+ await fs.mkdir(genieRedirect, { recursive: true });
196
+ await fs.writeFile(join(genieRedirect, 'redirect'), join(repoPath, '.genie'));
197
+
198
+ return worktreePath;
158
199
  } catch (error: any) {
159
200
  console.error(`⚠️ Failed to create worktree: ${error.message}`);
160
201
  return null;
@@ -162,14 +203,71 @@ async function createWorktree(
162
203
  }
163
204
 
164
205
  /**
165
- * Spawn Claude worker in new pane
206
+ * Remove worktree for a worker
207
+ */
208
+ async function removeWorktree(taskId: string, repoPath: string): Promise<void> {
209
+ const worktreePath = join(repoPath, WORKTREE_DIR_NAME, taskId);
210
+
211
+ try {
212
+ // Remove worktree
213
+ await $`git -C ${repoPath} worktree remove ${worktreePath} --force`.quiet();
214
+ } catch {
215
+ // Ignore errors - worktree may already be removed
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Wait for Claude CLI to be ready to accept input
221
+ * Polls pane content looking for Claude's input prompt indicator
222
+ */
223
+ async function waitForClaudeReady(
224
+ paneId: string,
225
+ timeoutMs: number = 30000,
226
+ pollIntervalMs: number = 500
227
+ ): Promise<boolean> {
228
+ const startTime = Date.now();
229
+
230
+ while (Date.now() - startTime < timeoutMs) {
231
+ try {
232
+ const content = await tmux.capturePaneContent(paneId, 50);
233
+
234
+ // Claude CLI shows ">" prompt when ready for input
235
+ // Also check for the input area indicator
236
+ // The prompt appears at the end of output when Claude is waiting for input
237
+ const lines = content.split('\n').filter(l => l.trim());
238
+ if (lines.length > 0) {
239
+ const lastFewLines = lines.slice(-5).join('\n');
240
+ // Claude shows "❯" prompt when ready for input
241
+ // Also detect welcome messages or input hints
242
+ if (
243
+ lastFewLines.includes('❯') ||
244
+ lastFewLines.includes('? for shortcuts') ||
245
+ lastFewLines.includes('What would you like') ||
246
+ lastFewLines.includes('How can I help')
247
+ ) {
248
+ return true;
249
+ }
250
+ }
251
+ } catch {
252
+ // Pane may not exist yet, continue polling
253
+ }
254
+
255
+ await new Promise(r => setTimeout(r, pollIntervalMs));
256
+ }
257
+
258
+ // Timeout - return false but don't fail (caller can decide)
259
+ return false;
260
+ }
261
+
262
+ /**
263
+ * Spawn Claude worker in new pane (splits the CURRENT active pane)
166
264
  */
167
265
  async function spawnWorkerPane(
168
266
  session: string,
169
267
  workingDir: string
170
268
  ): Promise<{ paneId: string } | null> {
171
269
  try {
172
- // Find current window
270
+ // Find session
173
271
  const sessionObj = await tmux.findSessionByName(session);
174
272
  if (!sessionObj) {
175
273
  console.error(`❌ Session "${session}" not found`);
@@ -182,15 +280,21 @@ async function spawnWorkerPane(
182
280
  return null;
183
281
  }
184
282
 
185
- const panes = await tmux.listPanes(windows[0].id);
283
+ // Find the ACTIVE window (not first window)
284
+ const activeWindow = windows.find(w => w.active) || windows[0];
285
+
286
+ const panes = await tmux.listPanes(activeWindow.id);
186
287
  if (!panes || panes.length === 0) {
187
- console.error(`❌ No panes in session "${session}"`);
288
+ console.error(`❌ No panes in window "${activeWindow.name}"`);
188
289
  return null;
189
290
  }
190
291
 
292
+ // Find the ACTIVE pane (not first pane)
293
+ const activePane = panes.find(p => p.active) || panes[0];
294
+
191
295
  // Split current pane horizontally (side by side)
192
296
  const newPane = await tmux.splitPane(
193
- panes[0].id,
297
+ activePane.id,
194
298
  'horizontal',
195
299
  50,
196
300
  workingDir
@@ -210,6 +314,7 @@ async function spawnWorkerPane(
210
314
 
211
315
  /**
212
316
  * Start monitoring worker state and update registry
317
+ * Updates both beads and JSON registry during transition
213
318
  */
214
319
  function startWorkerMonitoring(
215
320
  workerId: string,
@@ -250,6 +355,10 @@ function startWorkerMonitoring(
250
355
  }
251
356
 
252
357
  try {
358
+ // Update both registries during transition
359
+ if (useBeads) {
360
+ await beadsRegistry.updateState(workerId, newState);
361
+ }
253
362
  await registry.updateState(workerId, newState);
254
363
  } catch {
255
364
  // Ignore errors in background monitoring
@@ -258,6 +367,9 @@ function startWorkerMonitoring(
258
367
 
259
368
  monitor.on('poll_error', () => {
260
369
  // Pane may have been killed - unregister worker
370
+ if (useBeads) {
371
+ beadsRegistry.unregister(workerId).catch(() => {});
372
+ }
261
373
  registry.unregister(workerId).catch(() => {});
262
374
  monitor.stop();
263
375
  });
@@ -282,6 +394,20 @@ export async function workCommand(
282
394
  // Get current working directory as repo path
283
395
  const repoPath = process.cwd();
284
396
 
397
+ // Ensure beads daemon is running for auto-sync
398
+ if (useBeads) {
399
+ const daemonStatus = await beadsRegistry.checkDaemonStatus();
400
+ if (!daemonStatus.running) {
401
+ console.log('🔄 Starting beads daemon for auto-sync...');
402
+ const started = await beadsRegistry.startDaemon({ autoCommit: true });
403
+ if (started) {
404
+ console.log(' ✅ Daemon started');
405
+ } else {
406
+ console.log(' ⚠️ Daemon failed to start (non-fatal)');
407
+ }
408
+ }
409
+ }
410
+
285
411
  // 1. Resolve target
286
412
  let issue: BeadsIssue | null = null;
287
413
 
@@ -307,9 +433,86 @@ export async function workCommand(
307
433
 
308
434
  const taskId = issue.id;
309
435
 
310
- // 2. Check not already assigned
311
- const existingWorker = await registry.findByTask(taskId);
436
+ // 2. Check not already assigned (check both registries)
437
+ let existingWorker = useBeads
438
+ ? await beadsRegistry.findByTask(taskId)
439
+ : null;
440
+ if (!existingWorker) {
441
+ existingWorker = await registry.findByTask(taskId);
442
+ }
312
443
  if (existingWorker) {
444
+ // If worker exists and has a session ID, offer to resume
445
+ if (existingWorker.claudeSessionId && options.resume !== false) {
446
+ console.log(`📋 Found existing worker for ${taskId} with resumable session`);
447
+ console.log(` Session ID: ${existingWorker.claudeSessionId}`);
448
+ console.log(` Resuming previous Claude session...`);
449
+
450
+ // Get session
451
+ const session = options.session || await getCurrentSession();
452
+ if (!session) {
453
+ console.error('❌ Not in a tmux session. Attach to a session first or use --session.');
454
+ process.exit(1);
455
+ }
456
+
457
+ // Spawn a new pane for the resumed session
458
+ const workingDir = existingWorker.worktree || existingWorker.repoPath;
459
+ console.log(`🚀 Spawning worker pane...`);
460
+ const paneResult = await spawnWorkerPane(session, workingDir);
461
+ if (!paneResult) {
462
+ process.exit(1);
463
+ }
464
+
465
+ const { paneId } = paneResult;
466
+
467
+ // Update worker with new pane ID
468
+ await registry.update(existingWorker.id, {
469
+ paneId,
470
+ session,
471
+ state: 'spawning',
472
+ lastStateChange: new Date().toISOString(),
473
+ });
474
+ if (useBeads) {
475
+ await beadsRegistry.setAgentState(existingWorker.id, 'spawning').catch(() => {});
476
+ }
477
+
478
+ // Set BEADS_DIR so bd commands work in the worktree
479
+ const beadsDir = join(existingWorker.repoPath, '.genie');
480
+ const escapedWorkingDir = workingDir.replace(/'/g, "'\\''");
481
+
482
+ // Resume Claude with the stored session ID
483
+ await tmux.executeCommand(
484
+ paneId,
485
+ `cd '${escapedWorkingDir}' && BEADS_DIR='${beadsDir}' claude --resume '${existingWorker.claudeSessionId}'`,
486
+ true,
487
+ false
488
+ );
489
+
490
+ // Update state to working
491
+ if (useBeads) {
492
+ await beadsRegistry.setAgentState(existingWorker.id, 'working').catch(() => {});
493
+ }
494
+ await registry.updateState(existingWorker.id, 'working');
495
+
496
+ // Start monitoring
497
+ startWorkerMonitoring(existingWorker.id, session, paneId);
498
+
499
+ // Focus pane (only if explicitly requested)
500
+ if (options.focus === true) {
501
+ await tmux.executeTmux(`select-pane -t '${paneId}'`);
502
+ }
503
+
504
+ console.log(`\n✅ Resumed worker for ${taskId}`);
505
+ console.log(` Pane: ${paneId}`);
506
+ console.log(` Session: ${session}`);
507
+ console.log(` Claude Session: ${existingWorker.claudeSessionId}`);
508
+ console.log(`\nCommands:`);
509
+ console.log(` term workers - Check worker status`);
510
+ console.log(` term approve ${taskId} - Approve permissions`);
511
+ console.log(` term close ${taskId} - Close issue when done`);
512
+ console.log(` term kill ${taskId} - Force kill worker`);
513
+ return;
514
+ }
515
+
313
516
  console.error(`❌ ${taskId} already has a worker (pane ${existingWorker.paneId})`);
314
517
  console.log(` Run \`term kill ${existingWorker.id}\` first, or work on a different issue.`);
315
518
  process.exit(1);
@@ -354,7 +557,10 @@ export async function workCommand(
354
557
 
355
558
  const { paneId } = paneResult;
356
559
 
357
- // 7. Register worker
560
+ // 7. Generate Claude session ID for resume capability
561
+ const claudeSessionId = randomUUID();
562
+
563
+ // 8. Register worker (write to both registries during transition)
358
564
  const worker: registry.Worker = {
359
565
  id: taskId,
360
566
  paneId,
@@ -366,33 +572,69 @@ export async function workCommand(
366
572
  state: 'spawning',
367
573
  lastStateChange: new Date().toISOString(),
368
574
  repoPath,
575
+ claudeSessionId,
369
576
  };
370
577
 
371
- await registry.register(worker);
372
-
373
- // 8. Start Claude in pane
374
- await tmux.executeCommand(paneId, 'claude', false, false);
578
+ // Register in beads (creates agent bead)
579
+ if (useBeads) {
580
+ try {
581
+ const agentId = await beadsRegistry.ensureAgent(taskId, {
582
+ paneId,
583
+ session,
584
+ worktree: worktreePath,
585
+ repoPath,
586
+ taskId,
587
+ taskTitle: issue.title,
588
+ claudeSessionId,
589
+ });
590
+
591
+ // Bind work to agent
592
+ await beadsRegistry.bindWork(taskId, taskId);
593
+
594
+ // Set initial state
595
+ await beadsRegistry.setAgentState(taskId, 'spawning');
596
+ } catch (error: any) {
597
+ console.log(`⚠️ Beads registration failed: ${error.message} (non-fatal)`);
598
+ }
599
+ }
375
600
 
376
- // Wait a bit for Claude to start
377
- await new Promise(r => setTimeout(r, 2000));
601
+ // Also register in JSON registry (parallel operation during transition)
602
+ await registry.register(worker);
378
603
 
379
- // 9. Send initial prompt
604
+ // 9. Build prompt and start Claude with it as argument
380
605
  const prompt = options.prompt || `Work on beads issue ${taskId}: "${issue.title}"
381
606
 
382
- Read the issue details with: bd show ${taskId}
607
+ ## Description
608
+ ${issue.description || 'No description provided.'}
383
609
 
384
610
  When you're done, commit your changes and let me know.`;
385
611
 
386
- await tmux.executeCommand(paneId, prompt, false, false);
612
+ // Escape the prompt for shell (single quotes)
613
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
614
+
615
+ // Set BEADS_DIR so bd commands work in the worktree
616
+ const beadsDir = join(repoPath, '.genie');
617
+
618
+ // Escape workingDir for shell
619
+ const escapedWorkingDir = workingDir.replace(/'/g, "'\\''");
387
620
 
388
- // 10. Update state to working
621
+ // Start Claude with session ID for resume capability
622
+ // First cd to correct directory (shell rc files may have overridden tmux -c)
623
+ await tmux.executeCommand(paneId, `cd '${escapedWorkingDir}' && BEADS_DIR='${beadsDir}' claude --session-id '${claudeSessionId}' '${escapedPrompt}'`, true, false);
624
+
625
+ console.log(` Session ID: ${claudeSessionId}`);
626
+
627
+ // 10. Update state to working (both registries)
628
+ if (useBeads) {
629
+ await beadsRegistry.setAgentState(taskId, 'working').catch(() => {});
630
+ }
389
631
  await registry.updateState(taskId, 'working');
390
632
 
391
633
  // 11. Start monitoring
392
634
  startWorkerMonitoring(taskId, session, paneId);
393
635
 
394
- // 12. Focus pane (unless disabled)
395
- if (options.focus !== false) {
636
+ // 12. Focus pane (only if explicitly requested)
637
+ if (options.focus === true) {
396
638
  await tmux.executeTmux(`select-pane -t '${paneId}'`);
397
639
  }
398
640
 
@@ -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';