@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.
- package/.beads/README.md +81 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +9 -0
- package/.beads/metadata.json +4 -0
- package/.claude/skills/brainstorm/SKILL.md +53 -0
- package/.claude/skills/genie-base/SKILL.md +66 -0
- package/.claude/skills/genie-base/assets/workspace/AGENTS.md +191 -0
- package/.claude/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
- package/.claude/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
- package/.claude/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
- package/.claude/skills/genie-base/assets/workspace/MEMORY.md +16 -0
- package/.claude/skills/genie-base/assets/workspace/ROLE.md +14 -0
- package/.claude/skills/genie-base/assets/workspace/SOUL.md +36 -0
- package/.claude/skills/genie-base/assets/workspace/TOOLS.md +25 -0
- package/.claude/skills/genie-base/assets/workspace/USER.md +13 -0
- package/.claude/skills/genie-base/assets/workspace/memory/2026-01-30.md +6 -0
- package/.claude/skills/genie-base/assets/workspace/memory/2026-01-31.md +16 -0
- package/.claude/skills/genie-base/assets/workspace/memory/882c22be-9710-41c1-91f8-ed82947ef6ce.txt +1 -0
- package/.claude/skills/genie-base/scripts/install-workspace.sh +107 -0
- package/.claude/skills/genie-base/scripts/sanity-sweep.sh +60 -0
- package/.claude/skills/genie-blank-init/SKILL.md +37 -0
- package/.claude/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
- package/.claude/skills/genie-blank-init/assets/IDENTITY.md +9 -0
- package/.claude/skills/genie-blank-init/assets/SOUL.md +10 -0
- package/.claude/skills/genie-blank-init/assets/USER.md +9 -0
- package/.claude/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
- package/.claude/skills/genie-forge/SKILL.md +171 -0
- package/.claude/skills/genie-plan-review/CLAUDE.md +11 -0
- package/.claude/skills/genie-plan-review/SKILL.md +53 -0
- package/.claude/skills/genie-review/SKILL.md +171 -0
- package/.claude/skills/genie-wish/SKILL.md +141 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.genie/.gitkeep +3 -0
- package/.genie/backlog/hooks-v2.md +82 -0
- package/.genie/wishes/upgrade-brainstorm-handoff/wish.md +124 -0
- package/.gitattributes +3 -0
- package/AGENTS.md +75 -0
- package/bun.lock +55 -0
- package/dist/claudio.js +1 -1
- package/dist/genie.js +1 -1
- package/dist/term.js +123 -99
- package/docs/CO-ORCHESTRATION-GUIDE.md +368 -0
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/README.md +120 -0
- package/plugin/agents/implementor.md +92 -0
- package/plugin/agents/quality-reviewer.md +113 -0
- package/plugin/agents/spec-reviewer.md +90 -0
- package/plugin/hooks/hooks.json +3 -0
- package/plugin/references/review-criteria.md +72 -0
- package/plugin/references/wish-template.md +92 -0
- package/plugin/scripts/genie.cjs +141 -0
- package/plugin/scripts/smart-install.js +308 -0
- package/plugin/scripts/src/install-genie-cli.sh +120 -0
- package/plugin/scripts/src/validate-completion.ts +142 -0
- package/plugin/scripts/src/validate-wish.ts +137 -0
- package/plugin/scripts/term.cjs +229 -0
- package/plugin/scripts/validate-completion.cjs +16 -0
- package/plugin/scripts/validate-wish.cjs +17 -0
- package/plugin/scripts/worker-service.cjs +28 -0
- package/plugin/skills/brainstorm/SKILL.md +106 -0
- package/plugin/skills/forge/SKILL.md +171 -0
- package/plugin/skills/genie-base/SKILL.md +99 -0
- package/plugin/skills/genie-base/assets/workspace/AGENTS.md +191 -0
- package/plugin/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
- package/plugin/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
- package/plugin/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
- package/plugin/skills/genie-base/assets/workspace/MEMORY.md +16 -0
- package/plugin/skills/genie-base/assets/workspace/ROLE.md +14 -0
- package/plugin/skills/genie-base/assets/workspace/SOUL.md +36 -0
- package/plugin/skills/genie-base/assets/workspace/TOOLS.md +25 -0
- package/plugin/skills/genie-base/assets/workspace/USER.md +13 -0
- package/plugin/skills/genie-base/scripts/install-workspace.sh +107 -0
- package/plugin/skills/genie-base/scripts/sanity-sweep.sh +60 -0
- package/plugin/skills/genie-blank-init/SKILL.md +73 -0
- package/plugin/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
- package/plugin/skills/genie-blank-init/assets/IDENTITY.md +9 -0
- package/plugin/skills/genie-blank-init/assets/SOUL.md +10 -0
- package/plugin/skills/genie-blank-init/assets/USER.md +9 -0
- package/plugin/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
- package/plugin/skills/genie-cli-dev/CLAUDE.md +19 -0
- package/plugin/skills/genie-cli-dev/SKILL.md +292 -0
- package/plugin/skills/plan-review/SKILL.md +101 -0
- package/plugin/skills/review/SKILL.md +221 -0
- package/plugin/skills/wish/SKILL.md +110 -0
- package/plugin/skills/work-orchestration/SKILL.md +110 -0
- package/scripts/build.js +132 -0
- package/scripts/smart-install.js +308 -0
- package/scripts/sync.js +134 -0
- package/src/lib/beads-registry.ts +595 -0
- package/src/lib/orchestrator/event-monitor.ts +2 -0
- package/src/lib/skill-loader.ts +215 -0
- package/src/lib/tmux.ts +30 -11
- package/src/lib/version.ts +1 -1
- package/src/lib/worker-registry.ts +10 -0
- package/src/services/worker-service.ts +351 -0
- package/src/term-commands/close.ts +48 -3
- package/src/term-commands/create.ts +95 -0
- package/src/term-commands/daemon.ts +176 -0
- package/src/term-commands/kill.ts +56 -2
- package/src/term-commands/orchestrate.ts +3 -2
- package/src/term-commands/send.ts +43 -15
- package/src/term-commands/spawn.ts +446 -0
- package/src/term-commands/split.ts +20 -8
- package/src/term-commands/work.ts +279 -37
- package/src/term-commands/workers.ts +36 -2
- 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:
|
|
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
|
|
19
|
-
import { EventMonitor
|
|
21
|
+
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
22
|
+
import { EventMonitor } from '../lib/orchestrator/index.js';
|
|
20
23
|
import { join } from 'path';
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
164
|
+
await fs.mkdir(worktreeDir, { recursive: true });
|
|
165
|
+
} catch {
|
|
166
|
+
// Directory may already exist
|
|
167
|
+
}
|
|
148
168
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
//
|
|
377
|
-
await
|
|
601
|
+
// Also register in JSON registry (parallel operation during transition)
|
|
602
|
+
await registry.register(worker);
|
|
378
603
|
|
|
379
|
-
// 9.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 (
|
|
395
|
-
if (options.focus
|
|
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
|
-
|
|
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
|
|
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';
|