@automagik/genie 0.260202.1607 → 0.260202.1833
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/dist/claudio.js +5 -5
- package/dist/genie.js +6 -6
- package/dist/term.js +115 -53
- package/package.json +1 -1
- package/src/lib/orchestrator/completion.ts +392 -0
- package/src/lib/orchestrator/event-monitor.ts +442 -0
- package/src/lib/orchestrator/index.ts +12 -0
- package/src/lib/orchestrator/patterns.ts +277 -0
- package/src/lib/orchestrator/state-detector.ts +339 -0
- package/src/lib/version.ts +1 -1
- package/src/lib/worker-registry.ts +229 -0
- package/src/term-commands/close.ts +221 -0
- package/src/term-commands/kill.ts +143 -0
- package/src/term-commands/orchestrate.ts +844 -0
- package/src/term-commands/work.ts +415 -0
- package/src/term-commands/workers.ts +264 -0
- package/src/term.ts +189 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Registry - Manages worker state persistence
|
|
3
|
+
*
|
|
4
|
+
* Tracks Claude workers bound to beads issues, storing state in
|
|
5
|
+
* ~/.config/term/workers.json
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export type WorkerState =
|
|
17
|
+
| 'spawning' // Worker being created
|
|
18
|
+
| 'working' // Actively producing output
|
|
19
|
+
| 'idle' // At prompt, waiting for input
|
|
20
|
+
| 'permission' // Waiting for permission approval
|
|
21
|
+
| 'question' // Waiting for question answer
|
|
22
|
+
| 'done' // Task completed, ready for close
|
|
23
|
+
| 'error'; // Encountered error
|
|
24
|
+
|
|
25
|
+
export interface Worker {
|
|
26
|
+
/** Unique worker ID (usually matches taskId, e.g., "bd-42") */
|
|
27
|
+
id: string;
|
|
28
|
+
/** tmux pane ID (e.g., "%16") */
|
|
29
|
+
paneId: string;
|
|
30
|
+
/** tmux session name */
|
|
31
|
+
session: string;
|
|
32
|
+
/** Path to git worktree, null if using shared repo */
|
|
33
|
+
worktree: string | null;
|
|
34
|
+
/** Beads issue ID this worker is bound to */
|
|
35
|
+
taskId: string;
|
|
36
|
+
/** Task title from beads */
|
|
37
|
+
taskTitle?: string;
|
|
38
|
+
/** Associated wish slug (if from decompose) */
|
|
39
|
+
wishSlug?: string;
|
|
40
|
+
/** Execution group number within wish */
|
|
41
|
+
groupNumber?: number;
|
|
42
|
+
/** ISO timestamp when worker was started */
|
|
43
|
+
startedAt: string;
|
|
44
|
+
/** Current worker state */
|
|
45
|
+
state: WorkerState;
|
|
46
|
+
/** Last state change timestamp */
|
|
47
|
+
lastStateChange: string;
|
|
48
|
+
/** Repository path where worker operates */
|
|
49
|
+
repoPath: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkerRegistry {
|
|
53
|
+
workers: Record<string, Worker>;
|
|
54
|
+
lastUpdated: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Configuration
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
const CONFIG_DIR = join(homedir(), '.config', 'term');
|
|
62
|
+
const REGISTRY_FILE = join(CONFIG_DIR, 'workers.json');
|
|
63
|
+
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Private Functions
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
async function ensureConfigDir(): Promise<void> {
|
|
69
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function loadRegistry(): Promise<WorkerRegistry> {
|
|
73
|
+
try {
|
|
74
|
+
const content = await readFile(REGISTRY_FILE, 'utf-8');
|
|
75
|
+
return JSON.parse(content);
|
|
76
|
+
} catch {
|
|
77
|
+
return { workers: {}, lastUpdated: new Date().toISOString() };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function saveRegistry(registry: WorkerRegistry): Promise<void> {
|
|
82
|
+
await ensureConfigDir();
|
|
83
|
+
registry.lastUpdated = new Date().toISOString();
|
|
84
|
+
await writeFile(REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Public API
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register a new worker in the registry
|
|
93
|
+
*/
|
|
94
|
+
export async function register(worker: Worker): Promise<void> {
|
|
95
|
+
const registry = await loadRegistry();
|
|
96
|
+
registry.workers[worker.id] = worker;
|
|
97
|
+
await saveRegistry(registry);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Unregister (remove) a worker from the registry
|
|
102
|
+
*/
|
|
103
|
+
export async function unregister(id: string): Promise<void> {
|
|
104
|
+
const registry = await loadRegistry();
|
|
105
|
+
delete registry.workers[id];
|
|
106
|
+
await saveRegistry(registry);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a worker by ID
|
|
111
|
+
*/
|
|
112
|
+
export async function get(id: string): Promise<Worker | null> {
|
|
113
|
+
const registry = await loadRegistry();
|
|
114
|
+
return registry.workers[id] || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* List all workers
|
|
119
|
+
*/
|
|
120
|
+
export async function list(): Promise<Worker[]> {
|
|
121
|
+
const registry = await loadRegistry();
|
|
122
|
+
return Object.values(registry.workers);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Update a worker's state
|
|
127
|
+
*/
|
|
128
|
+
export async function updateState(id: string, state: WorkerState): Promise<void> {
|
|
129
|
+
const registry = await loadRegistry();
|
|
130
|
+
const worker = registry.workers[id];
|
|
131
|
+
if (worker) {
|
|
132
|
+
worker.state = state;
|
|
133
|
+
worker.lastStateChange = new Date().toISOString();
|
|
134
|
+
await saveRegistry(registry);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update multiple worker fields
|
|
140
|
+
*/
|
|
141
|
+
export async function update(id: string, updates: Partial<Worker>): Promise<void> {
|
|
142
|
+
const registry = await loadRegistry();
|
|
143
|
+
const worker = registry.workers[id];
|
|
144
|
+
if (worker) {
|
|
145
|
+
Object.assign(worker, updates);
|
|
146
|
+
if (updates.state) {
|
|
147
|
+
worker.lastStateChange = new Date().toISOString();
|
|
148
|
+
}
|
|
149
|
+
await saveRegistry(registry);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Find worker by tmux pane ID
|
|
155
|
+
*/
|
|
156
|
+
export async function findByPane(paneId: string): Promise<Worker | null> {
|
|
157
|
+
const workers = await list();
|
|
158
|
+
// Normalize pane ID (with or without % prefix)
|
|
159
|
+
const normalizedPaneId = paneId.startsWith('%') ? paneId : `%${paneId}`;
|
|
160
|
+
return workers.find(w => w.paneId === normalizedPaneId) || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Find worker by beads task ID
|
|
165
|
+
*/
|
|
166
|
+
export async function findByTask(taskId: string): Promise<Worker | null> {
|
|
167
|
+
const workers = await list();
|
|
168
|
+
return workers.find(w => w.taskId === taskId) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Find workers by wish slug
|
|
173
|
+
*/
|
|
174
|
+
export async function findByWish(wishSlug: string): Promise<Worker[]> {
|
|
175
|
+
const workers = await list();
|
|
176
|
+
return workers.filter(w => w.wishSlug === wishSlug);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Check if a worker exists for a given task
|
|
181
|
+
*/
|
|
182
|
+
export async function hasWorkerForTask(taskId: string): Promise<boolean> {
|
|
183
|
+
const worker = await findByTask(taskId);
|
|
184
|
+
return worker !== null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get workers in a specific state
|
|
189
|
+
*/
|
|
190
|
+
export async function getByState(state: WorkerState): Promise<Worker[]> {
|
|
191
|
+
const workers = await list();
|
|
192
|
+
return workers.filter(w => w.state === state);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Calculate elapsed time for a worker
|
|
197
|
+
*/
|
|
198
|
+
export function getElapsedTime(worker: Worker): { ms: number; formatted: string } {
|
|
199
|
+
const startTime = new Date(worker.startedAt).getTime();
|
|
200
|
+
const ms = Date.now() - startTime;
|
|
201
|
+
|
|
202
|
+
const minutes = Math.floor(ms / 60000);
|
|
203
|
+
const hours = Math.floor(minutes / 60);
|
|
204
|
+
|
|
205
|
+
let formatted: string;
|
|
206
|
+
if (hours > 0) {
|
|
207
|
+
formatted = `${hours}h ${minutes % 60}m`;
|
|
208
|
+
} else if (minutes > 0) {
|
|
209
|
+
formatted = `${minutes}m`;
|
|
210
|
+
} else {
|
|
211
|
+
formatted = '<1m';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { ms, formatted };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the config directory path
|
|
219
|
+
*/
|
|
220
|
+
export function getConfigDir(): string {
|
|
221
|
+
return CONFIG_DIR;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get the registry file path
|
|
226
|
+
*/
|
|
227
|
+
export function getRegistryPath(): string {
|
|
228
|
+
return REGISTRY_FILE;
|
|
229
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close command - Close beads issue and cleanup worker
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* term close <bd-id> - Close issue, cleanup worktree, kill worker
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* --no-sync - Skip bd sync
|
|
9
|
+
* --keep-worktree - Don't remove the worktree
|
|
10
|
+
* --merge - Merge worktree changes to main branch
|
|
11
|
+
* -y, --yes - Skip confirmation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { $ } from 'bun';
|
|
15
|
+
import { confirm } from '@inquirer/prompts';
|
|
16
|
+
import * as tmux from '../lib/tmux.js';
|
|
17
|
+
import * as registry from '../lib/worker-registry.js';
|
|
18
|
+
import { WorktreeManager } from '../lib/worktree.js';
|
|
19
|
+
import { join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
export interface CloseOptions {
|
|
27
|
+
noSync?: boolean;
|
|
28
|
+
keepWorktree?: boolean;
|
|
29
|
+
merge?: boolean;
|
|
30
|
+
yes?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Configuration
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Helper Functions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run bd command
|
|
45
|
+
*/
|
|
46
|
+
async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
|
|
47
|
+
try {
|
|
48
|
+
const result = await $`bd ${args}`.quiet();
|
|
49
|
+
return { stdout: result.stdout.toString().trim(), exitCode: 0 };
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
return { stdout: error.stdout?.toString().trim() || '', exitCode: error.exitCode || 1 };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Close beads issue
|
|
57
|
+
*/
|
|
58
|
+
async function closeBeadsIssue(taskId: string): Promise<boolean> {
|
|
59
|
+
const { exitCode } = await runBd(['close', taskId]);
|
|
60
|
+
return exitCode === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync beads to git
|
|
65
|
+
*/
|
|
66
|
+
async function syncBeads(): Promise<boolean> {
|
|
67
|
+
const { exitCode } = await runBd(['sync']);
|
|
68
|
+
return exitCode === 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Merge worktree branch to main
|
|
73
|
+
*/
|
|
74
|
+
async function mergeToMain(
|
|
75
|
+
repoPath: string,
|
|
76
|
+
branchName: string
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
try {
|
|
79
|
+
// Get current branch
|
|
80
|
+
const currentResult = await $`git -C ${repoPath} branch --show-current`.quiet();
|
|
81
|
+
const currentBranch = currentResult.stdout.toString().trim();
|
|
82
|
+
|
|
83
|
+
if (currentBranch === branchName) {
|
|
84
|
+
console.log(`⚠️ Already on branch ${branchName}. Skipping merge.`);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Checkout main and merge
|
|
89
|
+
console.log(` Switching to ${currentBranch}...`);
|
|
90
|
+
await $`git -C ${repoPath} checkout ${currentBranch}`.quiet();
|
|
91
|
+
|
|
92
|
+
console.log(` Merging ${branchName}...`);
|
|
93
|
+
await $`git -C ${repoPath} merge ${branchName} --no-edit`.quiet();
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
console.error(`⚠️ Merge failed: ${error.message}`);
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Remove worktree
|
|
104
|
+
*/
|
|
105
|
+
async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
|
|
106
|
+
try {
|
|
107
|
+
const manager = new WorktreeManager({
|
|
108
|
+
baseDir: WORKTREE_BASE,
|
|
109
|
+
repoPath,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (await manager.worktreeExists(taskId)) {
|
|
113
|
+
await manager.removeWorktree(taskId);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return true; // Already doesn't exist
|
|
117
|
+
} catch (error: any) {
|
|
118
|
+
console.error(`⚠️ Failed to remove worktree: ${error.message}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Kill worker pane
|
|
125
|
+
*/
|
|
126
|
+
async function killWorkerPane(paneId: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
await tmux.killPane(paneId);
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false; // Pane may already be gone
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// Main Command
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
export async function closeCommand(
|
|
140
|
+
taskId: string,
|
|
141
|
+
options: CloseOptions = {}
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
// Find worker in registry
|
|
145
|
+
const worker = await registry.findByTask(taskId);
|
|
146
|
+
|
|
147
|
+
if (!worker) {
|
|
148
|
+
console.log(`ℹ️ No active worker for ${taskId}. Closing issue only.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Confirm with user
|
|
152
|
+
if (!options.yes) {
|
|
153
|
+
const confirmed = await confirm({
|
|
154
|
+
message: `Close ${taskId}${worker ? ` and kill worker (pane ${worker.paneId})` : ''}?`,
|
|
155
|
+
default: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!confirmed) {
|
|
159
|
+
console.log('Cancelled.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 1. Close beads issue
|
|
165
|
+
console.log(`📝 Closing ${taskId}...`);
|
|
166
|
+
const closed = await closeBeadsIssue(taskId);
|
|
167
|
+
if (!closed) {
|
|
168
|
+
console.error(`❌ Failed to close ${taskId}. Check \`bd show ${taskId}\`.`);
|
|
169
|
+
// Continue with cleanup anyway
|
|
170
|
+
} else {
|
|
171
|
+
console.log(` ✅ Issue closed`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 2. Sync beads (unless --no-sync)
|
|
175
|
+
if (!options.noSync) {
|
|
176
|
+
console.log(`🔄 Syncing beads...`);
|
|
177
|
+
const synced = await syncBeads();
|
|
178
|
+
if (synced) {
|
|
179
|
+
console.log(` ✅ Synced to git`);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(` ⚠️ Sync failed (non-fatal)`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. Handle worktree
|
|
186
|
+
if (worker?.worktree && !options.keepWorktree) {
|
|
187
|
+
// Merge if requested
|
|
188
|
+
if (options.merge) {
|
|
189
|
+
console.log(`🔀 Merging changes...`);
|
|
190
|
+
const merged = await mergeToMain(worker.repoPath, taskId);
|
|
191
|
+
if (merged) {
|
|
192
|
+
console.log(` ✅ Merged to main`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Remove worktree
|
|
197
|
+
console.log(`🌳 Removing worktree...`);
|
|
198
|
+
const removed = await removeWorktree(taskId, worker.repoPath);
|
|
199
|
+
if (removed) {
|
|
200
|
+
console.log(` ✅ Worktree removed`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 4. Kill worker pane
|
|
205
|
+
if (worker) {
|
|
206
|
+
console.log(`💀 Killing worker pane...`);
|
|
207
|
+
await killWorkerPane(worker.paneId);
|
|
208
|
+
console.log(` ✅ Pane killed`);
|
|
209
|
+
|
|
210
|
+
// 5. Unregister worker
|
|
211
|
+
await registry.unregister(worker.id);
|
|
212
|
+
console.log(` ✅ Worker unregistered`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log(`\n✅ ${taskId} closed successfully`);
|
|
216
|
+
|
|
217
|
+
} catch (error: any) {
|
|
218
|
+
console.error(`❌ Error: ${error.message}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kill command - Force kill a worker
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* term kill <worker> - Kill worker by ID or pane
|
|
6
|
+
*
|
|
7
|
+
* Options:
|
|
8
|
+
* -y, --yes - Skip confirmation
|
|
9
|
+
* --keep-worktree - Don't remove the worktree
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { confirm } from '@inquirer/prompts';
|
|
13
|
+
import * as tmux from '../lib/tmux.js';
|
|
14
|
+
import * as registry from '../lib/worker-registry.js';
|
|
15
|
+
import { WorktreeManager } from '../lib/worktree.js';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export interface KillOptions {
|
|
24
|
+
yes?: boolean;
|
|
25
|
+
keepWorktree?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Configuration
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Helper Functions
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Kill worker pane
|
|
40
|
+
*/
|
|
41
|
+
async function killWorkerPane(paneId: string): Promise<boolean> {
|
|
42
|
+
try {
|
|
43
|
+
await tmux.killPane(paneId);
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false; // Pane may already be gone
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove worktree
|
|
52
|
+
*/
|
|
53
|
+
async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
|
|
54
|
+
try {
|
|
55
|
+
const manager = new WorktreeManager({
|
|
56
|
+
baseDir: WORKTREE_BASE,
|
|
57
|
+
repoPath,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (await manager.worktreeExists(taskId)) {
|
|
61
|
+
await manager.removeWorktree(taskId);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return true; // Already doesn't exist
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
console.error(`⚠️ Failed to remove worktree: ${error.message}`);
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Main Command
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export async function killCommand(
|
|
76
|
+
target: string,
|
|
77
|
+
options: KillOptions = {}
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
// Find worker by ID or pane
|
|
81
|
+
let worker = await registry.get(target);
|
|
82
|
+
|
|
83
|
+
if (!worker) {
|
|
84
|
+
// Try finding by pane ID
|
|
85
|
+
worker = await registry.findByPane(target);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!worker) {
|
|
89
|
+
// Try finding by task ID
|
|
90
|
+
worker = await registry.findByTask(target);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!worker) {
|
|
94
|
+
console.error(`❌ Worker "${target}" not found.`);
|
|
95
|
+
console.log(` Run \`term workers\` to see active workers.`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Confirm with user
|
|
100
|
+
if (!options.yes) {
|
|
101
|
+
const confirmed = await confirm({
|
|
102
|
+
message: `Kill worker ${worker.id} (pane ${worker.paneId})?`,
|
|
103
|
+
default: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!confirmed) {
|
|
107
|
+
console.log('Cancelled.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 1. Kill worker pane
|
|
113
|
+
console.log(`💀 Killing worker pane ${worker.paneId}...`);
|
|
114
|
+
const killed = await killWorkerPane(worker.paneId);
|
|
115
|
+
if (killed) {
|
|
116
|
+
console.log(` ✅ Pane killed`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(` ℹ️ Pane already gone`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 2. Remove worktree (unless --keep-worktree)
|
|
122
|
+
if (worker.worktree && !options.keepWorktree) {
|
|
123
|
+
console.log(`🌳 Removing worktree...`);
|
|
124
|
+
const removed = await removeWorktree(worker.taskId, worker.repoPath);
|
|
125
|
+
if (removed) {
|
|
126
|
+
console.log(` ✅ Worktree removed`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Unregister worker
|
|
131
|
+
await registry.unregister(worker.id);
|
|
132
|
+
console.log(` ✅ Worker unregistered`);
|
|
133
|
+
|
|
134
|
+
// 4. Note about task status
|
|
135
|
+
console.log(`\n⚠️ Task ${worker.taskId} is still in_progress in beads.`);
|
|
136
|
+
console.log(` Run \`bd update ${worker.taskId} --status open\` to reopen,`);
|
|
137
|
+
console.log(` or \`term work ${worker.taskId}\` to start a new worker.`);
|
|
138
|
+
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
console.error(`❌ Error: ${error.message}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|