@automagik/genie 0.260202.1901 → 0.260203.135
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/issues.jsonl +9 -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 +1 -1
- package/AGENTS.md +35 -0
- package/README.md +10 -5
- package/bun.lock +55 -0
- package/dist/claudio.js +1 -1
- package/dist/genie.js +1 -1
- package/dist/term.js +108 -85
- package/docs/CO-ORCHESTRATION-GUIDE.md +375 -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/hooks/postInstall.sh +10 -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 +231 -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 +295 -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 +116 -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 +49 -0
- package/src/lib/orchestrator/event-monitor.ts +2 -0
- package/src/lib/skill-loader.ts +215 -0
- package/src/lib/tmux.ts +19 -14
- 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 +14 -4
- package/src/term-commands/create.ts +95 -0
- package/src/term-commands/kill.ts +15 -4
- 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 +14 -3
- package/src/term-commands/work.ts +217 -57
- package/src/term.ts +81 -6
|
@@ -9,17 +9,18 @@
|
|
|
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
21
|
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
19
|
-
import {
|
|
20
|
-
import { EventMonitor, detectState } from '../lib/orchestrator/index.js';
|
|
22
|
+
import { EventMonitor } from '../lib/orchestrator/index.js';
|
|
21
23
|
import { join } from 'path';
|
|
22
|
-
import { homedir } from 'os';
|
|
23
24
|
|
|
24
25
|
// Use beads registry when enabled
|
|
25
26
|
const useBeads = beadsRegistry.isBeadsRegistryEnabled();
|
|
@@ -33,12 +34,15 @@ export interface WorkOptions {
|
|
|
33
34
|
session?: string;
|
|
34
35
|
focus?: boolean;
|
|
35
36
|
prompt?: string;
|
|
37
|
+
/** Resume previous Claude session if available */
|
|
38
|
+
resume?: boolean;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
interface BeadsIssue {
|
|
39
42
|
id: string;
|
|
40
43
|
title: string;
|
|
41
44
|
status: string;
|
|
45
|
+
description?: string;
|
|
42
46
|
blockedBy?: string[];
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -46,7 +50,8 @@ interface BeadsIssue {
|
|
|
46
50
|
// Configuration
|
|
47
51
|
// ============================================================================
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
// Worktrees are created inside the project at .genie/worktrees/<taskId>
|
|
54
|
+
const WORKTREE_DIR_NAME = '.genie/worktrees';
|
|
50
55
|
|
|
51
56
|
// ============================================================================
|
|
52
57
|
// Helper Functions
|
|
@@ -72,11 +77,15 @@ async function getBeadsIssue(id: string): Promise<BeadsIssue | null> {
|
|
|
72
77
|
if (exitCode !== 0 || !stdout) return null;
|
|
73
78
|
|
|
74
79
|
try {
|
|
75
|
-
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;
|
|
76
84
|
return {
|
|
77
85
|
id: issue.id,
|
|
78
86
|
title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
|
|
79
87
|
status: issue.status,
|
|
88
|
+
description: issue.description,
|
|
80
89
|
blockedBy: issue.blockedBy || [],
|
|
81
90
|
};
|
|
82
91
|
} catch {
|
|
@@ -99,6 +108,7 @@ async function getNextReadyIssue(): Promise<BeadsIssue | null> {
|
|
|
99
108
|
id: issue.id,
|
|
100
109
|
title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
|
|
101
110
|
status: issue.status,
|
|
111
|
+
description: issue.description,
|
|
102
112
|
blockedBy: issue.blockedBy || [],
|
|
103
113
|
};
|
|
104
114
|
}
|
|
@@ -138,52 +148,54 @@ async function getCurrentSession(): Promise<string | null> {
|
|
|
138
148
|
}
|
|
139
149
|
|
|
140
150
|
/**
|
|
141
|
-
* Create worktree for worker
|
|
142
|
-
*
|
|
143
|
-
* Falls back to WorktreeManager otherwise
|
|
151
|
+
* Create worktree for worker in .genie/worktrees/<taskId>
|
|
152
|
+
* Creates a .genie redirect file so bd commands work in the worktree
|
|
144
153
|
*/
|
|
145
154
|
async function createWorktree(
|
|
146
155
|
taskId: string,
|
|
147
156
|
repoPath: string
|
|
148
157
|
): Promise<string | null> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
const fs = await import('fs/promises');
|
|
159
|
+
const worktreeDir = join(repoPath, WORKTREE_DIR_NAME);
|
|
160
|
+
const worktreePath = join(worktreeDir, taskId);
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
}
|
|
162
|
+
// Ensure .genie/worktrees exists
|
|
163
|
+
try {
|
|
164
|
+
await fs.mkdir(worktreeDir, { recursive: true });
|
|
165
|
+
} catch {
|
|
166
|
+
// Directory may already exist
|
|
169
167
|
}
|
|
170
168
|
|
|
171
|
-
//
|
|
169
|
+
// Check if worktree already exists
|
|
172
170
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
repoPath,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// Check if worktree already exists
|
|
179
|
-
if (await manager.worktreeExists(taskId)) {
|
|
171
|
+
const stat = await fs.stat(worktreePath);
|
|
172
|
+
if (stat.isDirectory()) {
|
|
180
173
|
console.log(`ℹ️ Worktree for ${taskId} already exists`);
|
|
181
|
-
return
|
|
174
|
+
return worktreePath;
|
|
182
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
|
|
188
|
+
}
|
|
189
|
+
|
|
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'));
|
|
183
197
|
|
|
184
|
-
|
|
185
|
-
const info = await manager.createWorktree(taskId, true);
|
|
186
|
-
return info.path;
|
|
198
|
+
return worktreePath;
|
|
187
199
|
} catch (error: any) {
|
|
188
200
|
console.error(`⚠️ Failed to create worktree: ${error.message}`);
|
|
189
201
|
return null;
|
|
@@ -191,14 +203,71 @@ async function createWorktree(
|
|
|
191
203
|
}
|
|
192
204
|
|
|
193
205
|
/**
|
|
194
|
-
*
|
|
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)
|
|
195
264
|
*/
|
|
196
265
|
async function spawnWorkerPane(
|
|
197
266
|
session: string,
|
|
198
267
|
workingDir: string
|
|
199
268
|
): Promise<{ paneId: string } | null> {
|
|
200
269
|
try {
|
|
201
|
-
// Find
|
|
270
|
+
// Find session
|
|
202
271
|
const sessionObj = await tmux.findSessionByName(session);
|
|
203
272
|
if (!sessionObj) {
|
|
204
273
|
console.error(`❌ Session "${session}" not found`);
|
|
@@ -211,15 +280,21 @@ async function spawnWorkerPane(
|
|
|
211
280
|
return null;
|
|
212
281
|
}
|
|
213
282
|
|
|
214
|
-
|
|
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);
|
|
215
287
|
if (!panes || panes.length === 0) {
|
|
216
|
-
console.error(`❌ No panes in
|
|
288
|
+
console.error(`❌ No panes in window "${activeWindow.name}"`);
|
|
217
289
|
return null;
|
|
218
290
|
}
|
|
219
291
|
|
|
292
|
+
// Find the ACTIVE pane (not first pane)
|
|
293
|
+
const activePane = panes.find(p => p.active) || panes[0];
|
|
294
|
+
|
|
220
295
|
// Split current pane horizontally (side by side)
|
|
221
296
|
const newPane = await tmux.splitPane(
|
|
222
|
-
|
|
297
|
+
activePane.id,
|
|
223
298
|
'horizontal',
|
|
224
299
|
50,
|
|
225
300
|
workingDir
|
|
@@ -366,6 +441,78 @@ export async function workCommand(
|
|
|
366
441
|
existingWorker = await registry.findByTask(taskId);
|
|
367
442
|
}
|
|
368
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
|
+
|
|
369
516
|
console.error(`❌ ${taskId} already has a worker (pane ${existingWorker.paneId})`);
|
|
370
517
|
console.log(` Run \`term kill ${existingWorker.id}\` first, or work on a different issue.`);
|
|
371
518
|
process.exit(1);
|
|
@@ -410,7 +557,10 @@ export async function workCommand(
|
|
|
410
557
|
|
|
411
558
|
const { paneId } = paneResult;
|
|
412
559
|
|
|
413
|
-
// 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)
|
|
414
564
|
const worker: registry.Worker = {
|
|
415
565
|
id: taskId,
|
|
416
566
|
paneId,
|
|
@@ -422,6 +572,7 @@ export async function workCommand(
|
|
|
422
572
|
state: 'spawning',
|
|
423
573
|
lastStateChange: new Date().toISOString(),
|
|
424
574
|
repoPath,
|
|
575
|
+
claudeSessionId,
|
|
425
576
|
};
|
|
426
577
|
|
|
427
578
|
// Register in beads (creates agent bead)
|
|
@@ -434,6 +585,7 @@ export async function workCommand(
|
|
|
434
585
|
repoPath,
|
|
435
586
|
taskId,
|
|
436
587
|
taskTitle: issue.title,
|
|
588
|
+
claudeSessionId,
|
|
437
589
|
});
|
|
438
590
|
|
|
439
591
|
// Bind work to agent
|
|
@@ -449,20 +601,28 @@ export async function workCommand(
|
|
|
449
601
|
// Also register in JSON registry (parallel operation during transition)
|
|
450
602
|
await registry.register(worker);
|
|
451
603
|
|
|
452
|
-
//
|
|
453
|
-
await tmux.executeCommand(paneId, 'claude', false, false);
|
|
454
|
-
|
|
455
|
-
// Wait a bit for Claude to start
|
|
456
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
457
|
-
|
|
458
|
-
// 9. Send initial prompt
|
|
604
|
+
// 9. Build prompt and start Claude with it as argument
|
|
459
605
|
const prompt = options.prompt || `Work on beads issue ${taskId}: "${issue.title}"
|
|
460
606
|
|
|
461
|
-
|
|
607
|
+
## Description
|
|
608
|
+
${issue.description || 'No description provided.'}
|
|
462
609
|
|
|
463
610
|
When you're done, commit your changes and let me know.`;
|
|
464
611
|
|
|
465
|
-
|
|
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, "'\\''");
|
|
620
|
+
|
|
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}`);
|
|
466
626
|
|
|
467
627
|
// 10. Update state to working (both registries)
|
|
468
628
|
if (useBeads) {
|
|
@@ -473,8 +633,8 @@ When you're done, commit your changes and let me know.`;
|
|
|
473
633
|
// 11. Start monitoring
|
|
474
634
|
startWorkerMonitoring(taskId, session, paneId);
|
|
475
635
|
|
|
476
|
-
// 12. Focus pane (
|
|
477
|
-
if (options.focus
|
|
636
|
+
// 12. Focus pane (only if explicitly requested)
|
|
637
|
+
if (options.focus === true) {
|
|
478
638
|
await tmux.executeTmux(`select-pane -t '${paneId}'`);
|
|
479
639
|
}
|
|
480
640
|
|
package/src/term.ts
CHANGED
|
@@ -21,6 +21,8 @@ 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
23
|
import * as daemonCmd from './term-commands/daemon.js';
|
|
24
|
+
import * as spawnCmd from './term-commands/spawn.js';
|
|
25
|
+
import * as createCmd from './term-commands/create.js';
|
|
24
26
|
|
|
25
27
|
const program = new Command();
|
|
26
28
|
|
|
@@ -36,6 +38,12 @@ Collaborative Usage:
|
|
|
36
38
|
Workflow: new → exec → read → rm
|
|
37
39
|
Full control: window new/ls/rm, pane ls/rm, split, status
|
|
38
40
|
|
|
41
|
+
Skill-Based Spawning:
|
|
42
|
+
term spawn - Pick skill interactively
|
|
43
|
+
term spawn <skill> - Spawn Claude with skill loaded
|
|
44
|
+
term skills - List available skills
|
|
45
|
+
term create <title> - Create beads issue
|
|
46
|
+
|
|
39
47
|
Worker Orchestration:
|
|
40
48
|
term work <bd-id> - Spawn worker bound to beads issue
|
|
41
49
|
term work next - Work on next ready issue
|
|
@@ -111,9 +119,11 @@ program
|
|
|
111
119
|
|
|
112
120
|
program
|
|
113
121
|
.command('send <session> <keys>')
|
|
114
|
-
.description('Send
|
|
115
|
-
.
|
|
116
|
-
|
|
122
|
+
.description('Send keys to a tmux session (appends Enter by default)')
|
|
123
|
+
.option('--no-enter', 'Send raw keys without appending Enter')
|
|
124
|
+
.option('-p, --pane <id>', 'Target specific pane ID (e.g., %16)')
|
|
125
|
+
.action(async (session: string, keys: string, options: { enter?: boolean; pane?: string }) => {
|
|
126
|
+
await sendCmd.sendKeysToSession(session, keys, options);
|
|
117
127
|
});
|
|
118
128
|
|
|
119
129
|
// Pane splitting
|
|
@@ -126,9 +136,9 @@ program
|
|
|
126
136
|
await splitCmd.splitSessionPane(session, direction, options);
|
|
127
137
|
});
|
|
128
138
|
|
|
129
|
-
//
|
|
139
|
+
// Info command (renamed from status)
|
|
130
140
|
program
|
|
131
|
-
.command('
|
|
141
|
+
.command('info <session>')
|
|
132
142
|
.description('Check session state (idle/busy, pane count)')
|
|
133
143
|
.option('--command <id>', 'Check specific command status')
|
|
134
144
|
.option('--json', 'Output as JSON')
|
|
@@ -214,14 +224,79 @@ program
|
|
|
214
224
|
await shortcutsCmd.handleShortcuts(options);
|
|
215
225
|
});
|
|
216
226
|
|
|
227
|
+
// Skill-based spawning
|
|
228
|
+
program
|
|
229
|
+
.command('spawn [skill]')
|
|
230
|
+
.description('Spawn Claude with a skill (interactive picker if no skill specified)')
|
|
231
|
+
.option('-s, --session <name>', 'Target tmux session')
|
|
232
|
+
.option('--no-worktree', 'Skip worktree creation when taskId provided')
|
|
233
|
+
.option('--no-focus', 'Don\'t focus the new pane')
|
|
234
|
+
.option('-p, --prompt <message>', 'Additional context for the skill')
|
|
235
|
+
.option('-t, --task-id <id>', 'Bind to beads issue')
|
|
236
|
+
.action(async (skill: string | undefined, options: spawnCmd.SpawnOptions) => {
|
|
237
|
+
await spawnCmd.spawnCommand(skill, options);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
program
|
|
241
|
+
.command('skills')
|
|
242
|
+
.description('List available skills')
|
|
243
|
+
.action(async () => {
|
|
244
|
+
await spawnCmd.listSkillsCommand();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
program
|
|
248
|
+
.command('brainstorm')
|
|
249
|
+
.description('Spawn Claude with brainstorm skill (idea → design → spec)')
|
|
250
|
+
.option('-s, --session <name>', 'Target tmux session')
|
|
251
|
+
.option('--no-focus', 'Don\'t focus the new pane')
|
|
252
|
+
.option('-p, --prompt <message>', 'Additional context')
|
|
253
|
+
.action(async (options: spawnCmd.SpawnOptions) => {
|
|
254
|
+
await spawnCmd.spawnCommand('brainstorm', options);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Watch session events (promoted from orc watch)
|
|
258
|
+
program
|
|
259
|
+
.command('watch <session>')
|
|
260
|
+
.description('Watch session events in real-time')
|
|
261
|
+
.option('-p, --pane <id>', 'Target specific pane ID (e.g., %16)')
|
|
262
|
+
.option('--json', 'Output events as JSON')
|
|
263
|
+
.option('--poll <ms>', 'Poll interval in milliseconds')
|
|
264
|
+
.action(async (session: string, options: orchestrateCmd.WatchOptions) => {
|
|
265
|
+
await orchestrateCmd.watchSession(session, options);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Run task with monitoring (promoted from orc run)
|
|
269
|
+
program
|
|
270
|
+
.command('run <session> <message>')
|
|
271
|
+
.description('Send task and auto-approve until idle (fire-and-forget)')
|
|
272
|
+
.option('-p, --pane <id>', 'Target specific pane ID (e.g., %16)')
|
|
273
|
+
.option('-a, --auto-approve', 'Auto-approve permissions and plans')
|
|
274
|
+
.option('-t, --timeout <ms>', 'Timeout in milliseconds (default: 300000)')
|
|
275
|
+
.option('--json', 'Output final state as JSON')
|
|
276
|
+
.action(async (session: string, message: string, options: orchestrateCmd.RunOptions) => {
|
|
277
|
+
await orchestrateCmd.runTask(session, message, options);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Create beads issue command
|
|
281
|
+
program
|
|
282
|
+
.command('create <title>')
|
|
283
|
+
.description('Create a new beads issue')
|
|
284
|
+
.option('-d, --description <text>', 'Issue description')
|
|
285
|
+
.option('-p, --parent <id>', 'Parent issue ID (creates dependency)')
|
|
286
|
+
.option('--json', 'Output as JSON')
|
|
287
|
+
.action(async (title: string, options: createCmd.CreateOptions) => {
|
|
288
|
+
await createCmd.createCommand(title, options);
|
|
289
|
+
});
|
|
290
|
+
|
|
217
291
|
// Worker management commands (beads + Claude orchestration)
|
|
218
292
|
program
|
|
219
293
|
.command('work <target>')
|
|
220
294
|
.description('Spawn worker bound to beads issue (target: bd-id, "next", or "wish")')
|
|
221
295
|
.option('--no-worktree', 'Use shared repo instead of worktree')
|
|
222
296
|
.option('-s, --session <name>', 'Target tmux session')
|
|
223
|
-
.option('--
|
|
297
|
+
.option('--focus', 'Focus the worker pane after spawning')
|
|
224
298
|
.option('-p, --prompt <message>', 'Custom initial prompt')
|
|
299
|
+
.option('--no-resume', 'Start fresh session even if previous exists')
|
|
225
300
|
.action(async (target: string, options: workCmd.WorkOptions) => {
|
|
226
301
|
await workCmd.workCommand(target, options);
|
|
227
302
|
});
|