@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.
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Work command - Spawn worker bound to beads issue
3
+ *
4
+ * Usage:
5
+ * term work <bd-id> - Work on specific beads issue
6
+ * term work next - Work on next ready issue
7
+ * term work wish - Create a new wish (deferred)
8
+ *
9
+ * Options:
10
+ * --no-worktree - Use shared repo instead of worktree
11
+ * -s, --session <name> - Target tmux session
12
+ * --focus - Focus the worker pane (default: true)
13
+ */
14
+
15
+ import { $ } from 'bun';
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 { EventMonitor, detectState } from '../lib/orchestrator/index.js';
20
+ import { join } from 'path';
21
+ import { homedir } from 'os';
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ export interface WorkOptions {
28
+ noWorktree?: boolean;
29
+ session?: string;
30
+ focus?: boolean;
31
+ prompt?: string;
32
+ }
33
+
34
+ interface BeadsIssue {
35
+ id: string;
36
+ title: string;
37
+ status: string;
38
+ blockedBy?: string[];
39
+ }
40
+
41
+ // ============================================================================
42
+ // Configuration
43
+ // ============================================================================
44
+
45
+ const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
46
+
47
+ // ============================================================================
48
+ // Helper Functions
49
+ // ============================================================================
50
+
51
+ /**
52
+ * Run bd command and parse output
53
+ */
54
+ async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
55
+ try {
56
+ const result = await $`bd ${args}`.quiet();
57
+ return { stdout: result.stdout.toString().trim(), exitCode: 0 };
58
+ } catch (error: any) {
59
+ return { stdout: error.stdout?.toString().trim() || '', exitCode: error.exitCode || 1 };
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get a beads issue by ID
65
+ */
66
+ async function getBeadsIssue(id: string): Promise<BeadsIssue | null> {
67
+ const { stdout, exitCode } = await runBd(['show', id, '--json']);
68
+ if (exitCode !== 0 || !stdout) return null;
69
+
70
+ try {
71
+ const issue = JSON.parse(stdout);
72
+ return {
73
+ id: issue.id,
74
+ title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
75
+ status: issue.status,
76
+ blockedBy: issue.blockedBy || [],
77
+ };
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get next ready beads issue
85
+ */
86
+ async function getNextReadyIssue(): Promise<BeadsIssue | null> {
87
+ const { stdout, exitCode } = await runBd(['ready', '--json']);
88
+ if (exitCode !== 0 || !stdout) return null;
89
+
90
+ try {
91
+ const issues = JSON.parse(stdout);
92
+ if (Array.isArray(issues) && issues.length > 0) {
93
+ const issue = issues[0];
94
+ return {
95
+ id: issue.id,
96
+ title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
97
+ status: issue.status,
98
+ blockedBy: issue.blockedBy || [],
99
+ };
100
+ }
101
+ return null;
102
+ } catch {
103
+ // Try parsing as single issue or line-based format
104
+ const lines = stdout.split('\n').filter(l => l.trim());
105
+ if (lines.length > 0) {
106
+ // Extract ID from first line (format: "bd-1: title" or just "bd-1")
107
+ const match = lines[0].match(/^(bd-\d+)/);
108
+ if (match) {
109
+ return getBeadsIssue(match[1]);
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Mark beads issue as in_progress
118
+ */
119
+ async function claimIssue(id: string): Promise<boolean> {
120
+ const { exitCode } = await runBd(['update', id, '--status', 'in_progress']);
121
+ return exitCode === 0;
122
+ }
123
+
124
+ /**
125
+ * Get current tmux session name
126
+ */
127
+ async function getCurrentSession(): Promise<string | null> {
128
+ try {
129
+ const result = await tmux.executeTmux(`display-message -p '#{session_name}'`);
130
+ return result.trim() || null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Create worktree for worker
138
+ */
139
+ async function createWorktree(
140
+ taskId: string,
141
+ repoPath: string
142
+ ): Promise<string | null> {
143
+ try {
144
+ const manager = new WorktreeManager({
145
+ baseDir: WORKTREE_BASE,
146
+ repoPath,
147
+ });
148
+
149
+ // Check if worktree already exists
150
+ if (await manager.worktreeExists(taskId)) {
151
+ console.log(`ℹ️ Worktree for ${taskId} already exists`);
152
+ return manager.getWorktreePath(taskId);
153
+ }
154
+
155
+ // Create new worktree with branch
156
+ const info = await manager.createWorktree(taskId, true);
157
+ return info.path;
158
+ } catch (error: any) {
159
+ console.error(`⚠️ Failed to create worktree: ${error.message}`);
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Spawn Claude worker in new pane
166
+ */
167
+ async function spawnWorkerPane(
168
+ session: string,
169
+ workingDir: string
170
+ ): Promise<{ paneId: string } | null> {
171
+ try {
172
+ // Find current window
173
+ const sessionObj = await tmux.findSessionByName(session);
174
+ if (!sessionObj) {
175
+ console.error(`❌ Session "${session}" not found`);
176
+ return null;
177
+ }
178
+
179
+ const windows = await tmux.listWindows(sessionObj.id);
180
+ if (!windows || windows.length === 0) {
181
+ console.error(`❌ No windows in session "${session}"`);
182
+ return null;
183
+ }
184
+
185
+ const panes = await tmux.listPanes(windows[0].id);
186
+ if (!panes || panes.length === 0) {
187
+ console.error(`❌ No panes in session "${session}"`);
188
+ return null;
189
+ }
190
+
191
+ // Split current pane horizontally (side by side)
192
+ const newPane = await tmux.splitPane(
193
+ panes[0].id,
194
+ 'horizontal',
195
+ 50,
196
+ workingDir
197
+ );
198
+
199
+ if (!newPane) {
200
+ console.error(`❌ Failed to create new pane`);
201
+ return null;
202
+ }
203
+
204
+ return { paneId: newPane.id };
205
+ } catch (error: any) {
206
+ console.error(`❌ Error spawning worker pane: ${error.message}`);
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Start monitoring worker state and update registry
213
+ */
214
+ function startWorkerMonitoring(
215
+ workerId: string,
216
+ session: string,
217
+ paneId: string
218
+ ): void {
219
+ const monitor = new EventMonitor(session, {
220
+ pollIntervalMs: 1000,
221
+ paneId,
222
+ });
223
+
224
+ monitor.on('state_change', async (event) => {
225
+ if (!event.state) return;
226
+
227
+ let newState: registry.WorkerState;
228
+ switch (event.state.type) {
229
+ case 'working':
230
+ case 'tool_use':
231
+ newState = 'working';
232
+ break;
233
+ case 'idle':
234
+ newState = 'idle';
235
+ break;
236
+ case 'permission':
237
+ newState = 'permission';
238
+ break;
239
+ case 'question':
240
+ newState = 'question';
241
+ break;
242
+ case 'error':
243
+ newState = 'error';
244
+ break;
245
+ case 'complete':
246
+ newState = 'done';
247
+ break;
248
+ default:
249
+ return; // Don't update for unknown states
250
+ }
251
+
252
+ try {
253
+ await registry.updateState(workerId, newState);
254
+ } catch {
255
+ // Ignore errors in background monitoring
256
+ }
257
+ });
258
+
259
+ monitor.on('poll_error', () => {
260
+ // Pane may have been killed - unregister worker
261
+ registry.unregister(workerId).catch(() => {});
262
+ monitor.stop();
263
+ });
264
+
265
+ monitor.start().catch(() => {
266
+ // Session/pane not found - ignore
267
+ });
268
+
269
+ // Store monitor reference for cleanup (could be enhanced)
270
+ // For now, monitoring is fire-and-forget
271
+ }
272
+
273
+ // ============================================================================
274
+ // Main Command
275
+ // ============================================================================
276
+
277
+ export async function workCommand(
278
+ target: string,
279
+ options: WorkOptions = {}
280
+ ): Promise<void> {
281
+ try {
282
+ // Get current working directory as repo path
283
+ const repoPath = process.cwd();
284
+
285
+ // 1. Resolve target
286
+ let issue: BeadsIssue | null = null;
287
+
288
+ if (target === 'next') {
289
+ console.log('πŸ” Finding next ready issue...');
290
+ issue = await getNextReadyIssue();
291
+ if (!issue) {
292
+ console.log('ℹ️ No ready issues. Run `bd ready` to see the queue.');
293
+ return;
294
+ }
295
+ console.log(`πŸ“‹ Found: ${issue.id} - "${issue.title}"`);
296
+ } else if (target === 'wish') {
297
+ console.error('❌ `term work wish` is not yet implemented. Coming in Phase 1.5.');
298
+ process.exit(1);
299
+ } else {
300
+ // Validate bd-id exists
301
+ issue = await getBeadsIssue(target);
302
+ if (!issue) {
303
+ console.error(`❌ Issue "${target}" not found. Run \`bd list\` to see issues.`);
304
+ process.exit(1);
305
+ }
306
+ }
307
+
308
+ const taskId = issue.id;
309
+
310
+ // 2. Check not already assigned
311
+ const existingWorker = await registry.findByTask(taskId);
312
+ if (existingWorker) {
313
+ console.error(`❌ ${taskId} already has a worker (pane ${existingWorker.paneId})`);
314
+ console.log(` Run \`term kill ${existingWorker.id}\` first, or work on a different issue.`);
315
+ process.exit(1);
316
+ }
317
+
318
+ // 3. Get session
319
+ const session = options.session || await getCurrentSession();
320
+ if (!session) {
321
+ console.error('❌ Not in a tmux session. Attach to a session first or use --session.');
322
+ process.exit(1);
323
+ }
324
+
325
+ // 4. Claim in beads
326
+ console.log(`πŸ“ Claiming ${taskId}...`);
327
+ const claimed = await claimIssue(taskId);
328
+ if (!claimed) {
329
+ console.error(`❌ Failed to claim ${taskId}. Check \`bd show ${taskId}\`.`);
330
+ process.exit(1);
331
+ }
332
+
333
+ // 5. Create worktree (unless --no-worktree)
334
+ let workingDir = repoPath;
335
+ let worktreePath: string | null = null;
336
+
337
+ if (!options.noWorktree) {
338
+ console.log(`🌳 Creating worktree for ${taskId}...`);
339
+ worktreePath = await createWorktree(taskId, repoPath);
340
+ if (worktreePath) {
341
+ workingDir = worktreePath;
342
+ console.log(` Created: ${worktreePath}`);
343
+ } else {
344
+ console.log(`⚠️ Worktree creation failed. Using shared repo.`);
345
+ }
346
+ }
347
+
348
+ // 6. Spawn Claude pane
349
+ console.log(`πŸš€ Spawning worker pane...`);
350
+ const paneResult = await spawnWorkerPane(session, workingDir);
351
+ if (!paneResult) {
352
+ process.exit(1);
353
+ }
354
+
355
+ const { paneId } = paneResult;
356
+
357
+ // 7. Register worker
358
+ const worker: registry.Worker = {
359
+ id: taskId,
360
+ paneId,
361
+ session,
362
+ worktree: worktreePath,
363
+ taskId,
364
+ taskTitle: issue.title,
365
+ startedAt: new Date().toISOString(),
366
+ state: 'spawning',
367
+ lastStateChange: new Date().toISOString(),
368
+ repoPath,
369
+ };
370
+
371
+ await registry.register(worker);
372
+
373
+ // 8. Start Claude in pane
374
+ await tmux.executeCommand(paneId, 'claude', false, false);
375
+
376
+ // Wait a bit for Claude to start
377
+ await new Promise(r => setTimeout(r, 2000));
378
+
379
+ // 9. Send initial prompt
380
+ const prompt = options.prompt || `Work on beads issue ${taskId}: "${issue.title}"
381
+
382
+ Read the issue details with: bd show ${taskId}
383
+
384
+ When you're done, commit your changes and let me know.`;
385
+
386
+ await tmux.executeCommand(paneId, prompt, false, false);
387
+
388
+ // 10. Update state to working
389
+ await registry.updateState(taskId, 'working');
390
+
391
+ // 11. Start monitoring
392
+ startWorkerMonitoring(taskId, session, paneId);
393
+
394
+ // 12. Focus pane (unless disabled)
395
+ if (options.focus !== false) {
396
+ await tmux.executeTmux(`select-pane -t '${paneId}'`);
397
+ }
398
+
399
+ console.log(`\nβœ… Worker started for ${taskId}`);
400
+ console.log(` Pane: ${paneId}`);
401
+ console.log(` Session: ${session}`);
402
+ if (worktreePath) {
403
+ console.log(` Worktree: ${worktreePath}`);
404
+ }
405
+ console.log(`\nCommands:`);
406
+ console.log(` term workers - Check worker status`);
407
+ console.log(` term approve ${taskId} - Approve permissions`);
408
+ console.log(` term close ${taskId} - Close issue when done`);
409
+ console.log(` term kill ${taskId} - Force kill worker`);
410
+
411
+ } catch (error: any) {
412
+ console.error(`❌ Error: ${error.message}`);
413
+ process.exit(1);
414
+ }
415
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Workers command - Show worker status
3
+ *
4
+ * Usage:
5
+ * term workers - List all workers and their states
6
+ * term workers --json - Output as JSON
7
+ * term workers --watch - Live updates (TODO: Phase 1.5)
8
+ */
9
+
10
+ import { $ } from 'bun';
11
+ import * as tmux from '../lib/tmux.js';
12
+ import * as registry from '../lib/worker-registry.js';
13
+ import { detectState, stripAnsi } from '../lib/orchestrator/index.js';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ export interface WorkersOptions {
20
+ json?: boolean;
21
+ watch?: boolean;
22
+ }
23
+
24
+ interface WorkerDisplay {
25
+ name: string;
26
+ pane: string;
27
+ task: string;
28
+ state: string;
29
+ time: string;
30
+ alive: boolean;
31
+ }
32
+
33
+ // ============================================================================
34
+ // Helper Functions
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Check if a pane still exists
39
+ */
40
+ async function isPaneAlive(paneId: string): Promise<boolean> {
41
+ try {
42
+ await tmux.capturePaneContent(paneId, 1);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get current state from pane output
51
+ */
52
+ async function getCurrentState(paneId: string): Promise<string> {
53
+ try {
54
+ const output = await tmux.capturePaneContent(paneId, 30);
55
+ const state = detectState(output);
56
+
57
+ // Map to display format
58
+ switch (state.type) {
59
+ case 'working':
60
+ case 'tool_use':
61
+ return 'working';
62
+ case 'idle':
63
+ return 'idle';
64
+ case 'permission':
65
+ return '⚠️ perm';
66
+ case 'question':
67
+ return '⚠️ question';
68
+ case 'error':
69
+ return '❌ error';
70
+ case 'complete':
71
+ return 'βœ… done';
72
+ default:
73
+ return state.type;
74
+ }
75
+ } catch {
76
+ return 'unknown';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get beads queue status
82
+ */
83
+ async function getQueueStatus(): Promise<{ ready: string[]; blocked: string[] }> {
84
+ const ready: string[] = [];
85
+ const blocked: string[] = [];
86
+
87
+ try {
88
+ // Get ready issues
89
+ const readyResult = await $`bd ready --json`.quiet();
90
+ const readyOutput = readyResult.stdout.toString().trim();
91
+ if (readyOutput) {
92
+ try {
93
+ const issues = JSON.parse(readyOutput);
94
+ for (const issue of issues) {
95
+ ready.push(`${issue.id}`);
96
+ }
97
+ } catch {
98
+ // Parse line-based format
99
+ const lines = readyOutput.split('\n').filter(l => l.trim());
100
+ for (const line of lines) {
101
+ const match = line.match(/^(bd-\d+)/);
102
+ if (match) ready.push(match[1]);
103
+ }
104
+ }
105
+ }
106
+ } catch {
107
+ // Ignore bd errors
108
+ }
109
+
110
+ try {
111
+ // Get blocked issues
112
+ const listResult = await $`bd list --json`.quiet();
113
+ const listOutput = listResult.stdout.toString().trim();
114
+ if (listOutput) {
115
+ try {
116
+ const issues = JSON.parse(listOutput);
117
+ for (const issue of issues) {
118
+ if (issue.blockedBy && issue.blockedBy.length > 0) {
119
+ blocked.push(`${issue.id} (blocked by ${issue.blockedBy.join(', ')})`);
120
+ }
121
+ }
122
+ } catch {
123
+ // Ignore parse errors
124
+ }
125
+ }
126
+ } catch {
127
+ // Ignore bd errors
128
+ }
129
+
130
+ return { ready, blocked };
131
+ }
132
+
133
+ /**
134
+ * Format time elapsed
135
+ */
136
+ function formatElapsed(startedAt: string): string {
137
+ const startTime = new Date(startedAt).getTime();
138
+ const ms = Date.now() - startTime;
139
+ const minutes = Math.floor(ms / 60000);
140
+ const hours = Math.floor(minutes / 60);
141
+
142
+ if (hours > 0) {
143
+ return `${hours}h ${minutes % 60}m`;
144
+ } else if (minutes > 0) {
145
+ return `${minutes}m`;
146
+ }
147
+ return '<1m';
148
+ }
149
+
150
+ // ============================================================================
151
+ // Main Command
152
+ // ============================================================================
153
+
154
+ export async function workersCommand(options: WorkersOptions = {}): Promise<void> {
155
+ try {
156
+ const workers = await registry.list();
157
+
158
+ // Gather display data for each worker
159
+ const displayData: WorkerDisplay[] = [];
160
+
161
+ for (const worker of workers) {
162
+ const alive = await isPaneAlive(worker.paneId);
163
+ let currentState = worker.state;
164
+
165
+ if (alive) {
166
+ // Get live state from pane
167
+ currentState = await getCurrentState(worker.paneId);
168
+
169
+ // Update registry if state differs
170
+ const mappedState = mapDisplayStateToRegistry(currentState);
171
+ if (mappedState && mappedState !== worker.state) {
172
+ await registry.updateState(worker.id, mappedState);
173
+ }
174
+ } else {
175
+ currentState = 'πŸ’€ dead';
176
+ }
177
+
178
+ displayData.push({
179
+ name: worker.id,
180
+ pane: worker.paneId,
181
+ task: worker.taskTitle
182
+ ? `"${worker.taskTitle.substring(0, 25)}${worker.taskTitle.length > 25 ? '...' : ''}"`
183
+ : worker.taskId,
184
+ state: currentState,
185
+ time: formatElapsed(worker.startedAt),
186
+ alive,
187
+ });
188
+ }
189
+
190
+ // Get queue status
191
+ const queue = await getQueueStatus();
192
+
193
+ // Filter out dead workers from ready count
194
+ const activeTaskIds = workers.filter(w => displayData.find(d => d.name === w.id && d.alive)).map(w => w.taskId);
195
+ const actuallyReady = queue.ready.filter(id => !activeTaskIds.includes(id));
196
+
197
+ if (options.json) {
198
+ console.log(JSON.stringify({
199
+ workers: displayData,
200
+ queue: {
201
+ ready: actuallyReady,
202
+ blocked: queue.blocked,
203
+ },
204
+ }, null, 2));
205
+ return;
206
+ }
207
+
208
+ // Display workers table
209
+ console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”');
210
+ console.log('β”‚ WORKERS β”‚');
211
+ console.log('β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€');
212
+ console.log('β”‚ Name β”‚ Pane β”‚ Task β”‚ State β”‚Timeβ”‚');
213
+ console.log('β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€');
214
+
215
+ if (displayData.length === 0) {
216
+ console.log('β”‚ (no workers) β”‚');
217
+ } else {
218
+ for (const w of displayData) {
219
+ const name = w.name.padEnd(8).substring(0, 8);
220
+ const pane = w.pane.padEnd(8).substring(0, 8);
221
+ const task = w.task.padEnd(25).substring(0, 25);
222
+ const state = w.state.padEnd(8).substring(0, 8);
223
+ const time = w.time.padStart(4).substring(0, 4);
224
+ console.log(`β”‚ ${name} β”‚ ${pane} β”‚ ${task} β”‚ ${state} β”‚${time}β”‚`);
225
+ }
226
+ }
227
+
228
+ console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”˜');
229
+
230
+ // Display queue
231
+ if (queue.blocked.length > 0) {
232
+ console.log(`\nBlocked: ${queue.blocked.slice(0, 5).join(', ')}${queue.blocked.length > 5 ? '...' : ''}`);
233
+ }
234
+
235
+ if (actuallyReady.length > 0) {
236
+ console.log(`Ready: ${actuallyReady.slice(0, 5).join(', ')}${actuallyReady.length > 5 ? '...' : ''}`);
237
+ } else if (displayData.length > 0) {
238
+ console.log(`\nReady: (none - all assigned or blocked)`);
239
+ }
240
+
241
+ // Cleanup dead workers (optional - could prompt)
242
+ const deadWorkers = displayData.filter(w => !w.alive);
243
+ if (deadWorkers.length > 0) {
244
+ console.log(`\n⚠️ ${deadWorkers.length} dead worker(s) detected. Run \`term kill <name>\` to clean up.`);
245
+ }
246
+
247
+ } catch (error: any) {
248
+ console.error(`❌ Error: ${error.message}`);
249
+ process.exit(1);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Map display state to registry state
255
+ */
256
+ function mapDisplayStateToRegistry(displayState: string): registry.WorkerState | null {
257
+ if (displayState === 'working') return 'working';
258
+ if (displayState === 'idle') return 'idle';
259
+ if (displayState === '⚠️ perm') return 'permission';
260
+ if (displayState === '⚠️ question') return 'question';
261
+ if (displayState === '❌ error') return 'error';
262
+ if (displayState === 'βœ… done') return 'done';
263
+ return null;
264
+ }