@automagik/genie 0.260202.530 → 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.
Files changed (39) hide show
  1. package/dist/claudio.js +44 -45
  2. package/dist/genie.js +58 -135
  3. package/dist/term.js +134 -67
  4. package/install.sh +43 -7
  5. package/package.json +1 -1
  6. package/src/claudio.ts +31 -21
  7. package/src/commands/launch.ts +12 -68
  8. package/src/genie-commands/doctor.ts +327 -0
  9. package/src/genie-commands/setup.ts +317 -199
  10. package/src/genie-commands/uninstall.ts +176 -0
  11. package/src/genie.ts +24 -44
  12. package/src/lib/claude-settings.ts +22 -64
  13. package/src/lib/genie-config.ts +169 -57
  14. package/src/lib/orchestrator/completion.ts +392 -0
  15. package/src/lib/orchestrator/event-monitor.ts +442 -0
  16. package/src/lib/orchestrator/index.ts +12 -0
  17. package/src/lib/orchestrator/patterns.ts +277 -0
  18. package/src/lib/orchestrator/state-detector.ts +339 -0
  19. package/src/lib/version.ts +1 -1
  20. package/src/lib/worker-registry.ts +229 -0
  21. package/src/term-commands/close.ts +221 -0
  22. package/src/term-commands/exec.ts +28 -6
  23. package/src/term-commands/kill.ts +143 -0
  24. package/src/term-commands/orchestrate.ts +844 -0
  25. package/src/term-commands/read.ts +6 -1
  26. package/src/term-commands/shortcuts.ts +14 -14
  27. package/src/term-commands/work.ts +415 -0
  28. package/src/term-commands/workers.ts +264 -0
  29. package/src/term.ts +201 -3
  30. package/src/types/genie-config.ts +49 -81
  31. package/src/genie-commands/hooks.ts +0 -317
  32. package/src/lib/hook-script.ts +0 -263
  33. package/src/lib/hooks/compose.ts +0 -72
  34. package/src/lib/hooks/index.ts +0 -163
  35. package/src/lib/hooks/presets/audited.ts +0 -191
  36. package/src/lib/hooks/presets/collaborative.ts +0 -143
  37. package/src/lib/hooks/presets/sandboxed.ts +0 -153
  38. package/src/lib/hooks/presets/supervised.ts +0 -66
  39. package/src/lib/hooks/utils/escape.ts +0 -46
@@ -1,4 +1,5 @@
1
1
  import * as logReader from '../lib/log-reader.js';
2
+ import { getTerminalConfig } from '../lib/genie-config.js';
2
3
 
3
4
  export interface ReadOptions {
4
5
  lines?: string;
@@ -15,9 +16,13 @@ export interface ReadOptions {
15
16
 
16
17
  export async function readSessionLogs(sessionName: string, options: ReadOptions): Promise<void> {
17
18
  try {
19
+ // Use config default if no lines specified
20
+ const termConfig = getTerminalConfig();
21
+ const defaultLines = termConfig.readLines;
22
+
18
23
  // Parse options
19
24
  const readOptions: logReader.ReadOptions = {
20
- lines: options.lines ? parseInt(options.lines, 10) : 100,
25
+ lines: options.lines ? parseInt(options.lines, 10) : defaultLines,
21
26
  from: options.from ? parseInt(options.from, 10) : undefined,
22
27
  to: options.to ? parseInt(options.to, 10) : undefined,
23
28
  range: options.range,
@@ -15,13 +15,13 @@ export function generateTmuxConfig(): string {
15
15
  # To use: add to ~/.tmux.conf or source this file
16
16
 
17
17
  # Ctrl+T: New window (tab) in current session
18
- bind-key -n C-t new-window
18
+ bind-key -n C-t new-window -c "#{pane_current_path}"
19
19
 
20
20
  # Ctrl+S: Vertical split (requires stty -ixon in shell rc)
21
- bind-key -n C-s split-window -v
21
+ bind-key -n C-s split-window -v -c "#{pane_current_path}"
22
22
 
23
- # Ctrl+H: Horizontal split
24
- bind-key -n C-h split-window -h
23
+ # Ctrl+Shift+S: Horizontal split
24
+ bind-key -n C-S split-window -h -c "#{pane_current_path}"
25
25
  `;
26
26
  }
27
27
 
@@ -49,7 +49,7 @@ genie-new-tab() {
49
49
  local session
50
50
  session=$(tmux display-message -p '#S' 2>/dev/null)
51
51
  if [ -n "$session" ]; then
52
- tmux new-window
52
+ tmux new-window -c "#{pane_current_path}"
53
53
  else
54
54
  echo "Not in a tmux session"
55
55
  fi
@@ -57,7 +57,7 @@ genie-new-tab() {
57
57
 
58
58
  genie-vsplit() {
59
59
  if tmux display-message -p '#S' >/dev/null 2>&1; then
60
- tmux split-window -v
60
+ tmux split-window -v -c "#{pane_current_path}"
61
61
  else
62
62
  echo "Not in a tmux session"
63
63
  fi
@@ -65,7 +65,7 @@ genie-vsplit() {
65
65
 
66
66
  genie-hsplit() {
67
67
  if tmux display-message -p '#S' >/dev/null 2>&1; then
68
- tmux split-window -h
68
+ tmux split-window -h -c "#{pane_current_path}"
69
69
  else
70
70
  echo "Not in a tmux session"
71
71
  fi
@@ -83,13 +83,13 @@ export function displayShortcuts(): void {
83
83
  console.log(`
84
84
  Warp-like Terminal Shortcuts for tmux + Termux
85
85
 
86
- ┌────────────┬────────────────────────────────────────┐
87
- │ Shortcut │ Action │
88
- ├────────────┼────────────────────────────────────────┤
89
- │ Ctrl+T │ New tab (window) in current session │
90
- │ Ctrl+S │ Vertical split in current session │
91
- │ Ctrl+H │ Horizontal split in current session │
92
- └────────────┴────────────────────────────────────────┘
86
+ ┌──────────────────┬────────────────────────────────────────┐
87
+ │ Shortcut │ Action │
88
+ ├──────────────────┼────────────────────────────────────────┤
89
+ │ Ctrl+T │ New tab (window) in current session │
90
+ │ Ctrl+S │ Vertical split in current session │
91
+ │ Ctrl+Shift+S │ Horizontal split in current session │
92
+ └──────────────────┴────────────────────────────────────────┘
93
93
 
94
94
  Termux Extra Keys (F1-F3):
95
95
  F1 → New tab F2 → Vertical split
@@ -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
+ }