@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.
Files changed (104) hide show
  1. package/.beads/issues.jsonl +9 -0
  2. package/.claude/skills/brainstorm/SKILL.md +53 -0
  3. package/.claude/skills/genie-base/SKILL.md +66 -0
  4. package/.claude/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  5. package/.claude/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  6. package/.claude/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  7. package/.claude/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  8. package/.claude/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  9. package/.claude/skills/genie-base/assets/workspace/ROLE.md +14 -0
  10. package/.claude/skills/genie-base/assets/workspace/SOUL.md +36 -0
  11. package/.claude/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  12. package/.claude/skills/genie-base/assets/workspace/USER.md +13 -0
  13. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-30.md +6 -0
  14. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-31.md +16 -0
  15. package/.claude/skills/genie-base/assets/workspace/memory/882c22be-9710-41c1-91f8-ed82947ef6ce.txt +1 -0
  16. package/.claude/skills/genie-base/scripts/install-workspace.sh +107 -0
  17. package/.claude/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  18. package/.claude/skills/genie-blank-init/SKILL.md +37 -0
  19. package/.claude/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  20. package/.claude/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  21. package/.claude/skills/genie-blank-init/assets/SOUL.md +10 -0
  22. package/.claude/skills/genie-blank-init/assets/USER.md +9 -0
  23. package/.claude/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  24. package/.claude/skills/genie-forge/SKILL.md +171 -0
  25. package/.claude/skills/genie-plan-review/CLAUDE.md +11 -0
  26. package/.claude/skills/genie-plan-review/SKILL.md +53 -0
  27. package/.claude/skills/genie-review/SKILL.md +171 -0
  28. package/.claude/skills/genie-wish/SKILL.md +141 -0
  29. package/.claude-plugin/marketplace.json +18 -0
  30. package/.genie/.gitkeep +3 -0
  31. package/.genie/backlog/hooks-v2.md +82 -0
  32. package/.genie/wishes/upgrade-brainstorm-handoff/wish.md +124 -0
  33. package/.gitattributes +1 -1
  34. package/AGENTS.md +35 -0
  35. package/README.md +10 -5
  36. package/bun.lock +55 -0
  37. package/dist/claudio.js +1 -1
  38. package/dist/genie.js +1 -1
  39. package/dist/term.js +108 -85
  40. package/docs/CO-ORCHESTRATION-GUIDE.md +375 -0
  41. package/package.json +5 -1
  42. package/plugin/.claude-plugin/plugin.json +18 -0
  43. package/plugin/README.md +120 -0
  44. package/plugin/agents/implementor.md +92 -0
  45. package/plugin/agents/quality-reviewer.md +113 -0
  46. package/plugin/agents/spec-reviewer.md +90 -0
  47. package/plugin/hooks/hooks.json +3 -0
  48. package/plugin/hooks/postInstall.sh +10 -0
  49. package/plugin/references/review-criteria.md +72 -0
  50. package/plugin/references/wish-template.md +92 -0
  51. package/plugin/scripts/genie.cjs +141 -0
  52. package/plugin/scripts/smart-install.js +308 -0
  53. package/plugin/scripts/src/install-genie-cli.sh +120 -0
  54. package/plugin/scripts/src/validate-completion.ts +142 -0
  55. package/plugin/scripts/src/validate-wish.ts +137 -0
  56. package/plugin/scripts/term.cjs +231 -0
  57. package/plugin/scripts/validate-completion.cjs +16 -0
  58. package/plugin/scripts/validate-wish.cjs +17 -0
  59. package/plugin/scripts/worker-service.cjs +28 -0
  60. package/plugin/skills/brainstorm/SKILL.md +106 -0
  61. package/plugin/skills/forge/SKILL.md +171 -0
  62. package/plugin/skills/genie-base/SKILL.md +99 -0
  63. package/plugin/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  64. package/plugin/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  65. package/plugin/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  66. package/plugin/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  67. package/plugin/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  68. package/plugin/skills/genie-base/assets/workspace/ROLE.md +14 -0
  69. package/plugin/skills/genie-base/assets/workspace/SOUL.md +36 -0
  70. package/plugin/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  71. package/plugin/skills/genie-base/assets/workspace/USER.md +13 -0
  72. package/plugin/skills/genie-base/scripts/install-workspace.sh +107 -0
  73. package/plugin/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  74. package/plugin/skills/genie-blank-init/SKILL.md +73 -0
  75. package/plugin/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  76. package/plugin/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  77. package/plugin/skills/genie-blank-init/assets/SOUL.md +10 -0
  78. package/plugin/skills/genie-blank-init/assets/USER.md +9 -0
  79. package/plugin/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  80. package/plugin/skills/genie-cli-dev/CLAUDE.md +19 -0
  81. package/plugin/skills/genie-cli-dev/SKILL.md +295 -0
  82. package/plugin/skills/plan-review/SKILL.md +101 -0
  83. package/plugin/skills/review/SKILL.md +221 -0
  84. package/plugin/skills/wish/SKILL.md +110 -0
  85. package/plugin/skills/work-orchestration/SKILL.md +116 -0
  86. package/scripts/build.js +132 -0
  87. package/scripts/smart-install.js +308 -0
  88. package/scripts/sync.js +134 -0
  89. package/src/lib/beads-registry.ts +49 -0
  90. package/src/lib/orchestrator/event-monitor.ts +2 -0
  91. package/src/lib/skill-loader.ts +215 -0
  92. package/src/lib/tmux.ts +19 -14
  93. package/src/lib/version.ts +1 -1
  94. package/src/lib/worker-registry.ts +10 -0
  95. package/src/services/worker-service.ts +351 -0
  96. package/src/term-commands/close.ts +14 -4
  97. package/src/term-commands/create.ts +95 -0
  98. package/src/term-commands/kill.ts +15 -4
  99. package/src/term-commands/orchestrate.ts +3 -2
  100. package/src/term-commands/send.ts +43 -15
  101. package/src/term-commands/spawn.ts +446 -0
  102. package/src/term-commands/split.ts +14 -3
  103. package/src/term-commands/work.ts +217 -57
  104. package/src/term.ts +81 -6
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Worker Service for automagik-genie
4
+ *
5
+ * Background HTTP service for workflow state management.
6
+ * Port: 48888 (avoids collision with claude-mem's 37777)
7
+ */
8
+
9
+ import { createServer, IncomingMessage, ServerResponse } from 'http';
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { homedir } from 'os';
13
+ import { spawn, execSync } from 'child_process';
14
+
15
+ const PORT = 48888;
16
+ const GENIE_DIR = join(homedir(), '.genie');
17
+ const PID_FILE = join(GENIE_DIR, 'worker.pid');
18
+ const STATE_FILE = join(GENIE_DIR, 'workflow-state.json');
19
+
20
+ // Ensure .genie directory exists
21
+ if (!existsSync(GENIE_DIR)) {
22
+ mkdirSync(GENIE_DIR, { recursive: true });
23
+ }
24
+
25
+ interface WorkflowState {
26
+ activeWish?: string;
27
+ activeForge?: {
28
+ wishSlug: string;
29
+ currentTask?: string;
30
+ completedTasks: string[];
31
+ failedTasks: string[];
32
+ };
33
+ lastUpdate: string;
34
+ }
35
+
36
+ function loadState(): WorkflowState {
37
+ try {
38
+ if (existsSync(STATE_FILE)) {
39
+ return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
40
+ }
41
+ } catch {
42
+ // Ignore parse errors
43
+ }
44
+ return { lastUpdate: new Date().toISOString() };
45
+ }
46
+
47
+ function saveState(state: WorkflowState): void {
48
+ state.lastUpdate = new Date().toISOString();
49
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
50
+ }
51
+
52
+ function json(res: ServerResponse, data: unknown, status = 200): void {
53
+ res.writeHead(status, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify(data));
55
+ }
56
+
57
+ function parseBody(req: IncomingMessage): Promise<unknown> {
58
+ return new Promise((resolve, reject) => {
59
+ let body = '';
60
+ req.on('data', chunk => body += chunk);
61
+ req.on('end', () => {
62
+ try {
63
+ resolve(body ? JSON.parse(body) : {});
64
+ } catch {
65
+ reject(new Error('Invalid JSON'));
66
+ }
67
+ });
68
+ });
69
+ }
70
+
71
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
72
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
73
+ const path = url.pathname;
74
+ const method = req.method || 'GET';
75
+
76
+ // CORS headers
77
+ res.setHeader('Access-Control-Allow-Origin', '*');
78
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
79
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
80
+
81
+ if (method === 'OPTIONS') {
82
+ res.writeHead(204);
83
+ res.end();
84
+ return;
85
+ }
86
+
87
+ // Health check
88
+ if (path === '/health' || path === '/') {
89
+ json(res, {
90
+ status: 'ok',
91
+ service: 'automagik-genie',
92
+ version: process.env.GENIE_VERSION || 'dev',
93
+ port: PORT,
94
+ uptime: process.uptime()
95
+ });
96
+ return;
97
+ }
98
+
99
+ // Workflow status
100
+ if (path === '/api/workflow/status' && method === 'GET') {
101
+ const state = loadState();
102
+ json(res, state);
103
+ return;
104
+ }
105
+
106
+ // Update workflow state
107
+ if (path === '/api/workflow/update' && method === 'POST') {
108
+ try {
109
+ const body = await parseBody(req) as Partial<WorkflowState>;
110
+ const state = loadState();
111
+ Object.assign(state, body);
112
+ saveState(state);
113
+ json(res, { success: true, state });
114
+ } catch (error) {
115
+ json(res, { error: 'Invalid request body' }, 400);
116
+ }
117
+ return;
118
+ }
119
+
120
+ // Start wish tracking
121
+ if (path === '/api/workflow/wish/start' && method === 'POST') {
122
+ try {
123
+ const body = await parseBody(req) as { slug: string };
124
+ const state = loadState();
125
+ state.activeWish = body.slug;
126
+ saveState(state);
127
+ json(res, { success: true, wish: body.slug });
128
+ } catch {
129
+ json(res, { error: 'Invalid request' }, 400);
130
+ }
131
+ return;
132
+ }
133
+
134
+ // Start forge session
135
+ if (path === '/api/workflow/forge/start' && method === 'POST') {
136
+ try {
137
+ const body = await parseBody(req) as { wishSlug: string };
138
+ const state = loadState();
139
+ state.activeForge = {
140
+ wishSlug: body.wishSlug,
141
+ completedTasks: [],
142
+ failedTasks: []
143
+ };
144
+ saveState(state);
145
+ json(res, { success: true, forge: state.activeForge });
146
+ } catch {
147
+ json(res, { error: 'Invalid request' }, 400);
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Update forge task status
153
+ if (path === '/api/workflow/forge/task' && method === 'POST') {
154
+ try {
155
+ const body = await parseBody(req) as { task: string; status: 'started' | 'completed' | 'failed' };
156
+ const state = loadState();
157
+ if (!state.activeForge) {
158
+ json(res, { error: 'No active forge session' }, 400);
159
+ return;
160
+ }
161
+ if (body.status === 'started') {
162
+ state.activeForge.currentTask = body.task;
163
+ } else if (body.status === 'completed') {
164
+ state.activeForge.completedTasks.push(body.task);
165
+ state.activeForge.currentTask = undefined;
166
+ } else if (body.status === 'failed') {
167
+ state.activeForge.failedTasks.push(body.task);
168
+ state.activeForge.currentTask = undefined;
169
+ }
170
+ saveState(state);
171
+ json(res, { success: true, forge: state.activeForge });
172
+ } catch {
173
+ json(res, { error: 'Invalid request' }, 400);
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Context hook - inject active workflow into Claude session
179
+ if (path === '/api/hook/context' && method === 'GET') {
180
+ const state = loadState();
181
+ let context = '';
182
+
183
+ if (state.activeWish) {
184
+ context += `Active Wish: ${state.activeWish}\n`;
185
+ }
186
+ if (state.activeForge) {
187
+ context += `Active Forge: ${state.activeForge.wishSlug}\n`;
188
+ if (state.activeForge.currentTask) {
189
+ context += ` Current Task: ${state.activeForge.currentTask}\n`;
190
+ }
191
+ context += ` Completed: ${state.activeForge.completedTasks.length} tasks\n`;
192
+ if (state.activeForge.failedTasks.length > 0) {
193
+ context += ` Failed: ${state.activeForge.failedTasks.length} tasks\n`;
194
+ }
195
+ }
196
+
197
+ if (context) {
198
+ json(res, { context });
199
+ } else {
200
+ json(res, { context: null });
201
+ }
202
+ return;
203
+ }
204
+
205
+ // Admin restart endpoint
206
+ if (path === '/api/admin/restart' && method === 'POST') {
207
+ json(res, { success: true, message: 'Worker restarting...' });
208
+ // Schedule restart after response is sent
209
+ setTimeout(() => {
210
+ process.exit(0);
211
+ }, 100);
212
+ return;
213
+ }
214
+
215
+ // 404 for unknown routes
216
+ json(res, { error: 'Not found', path }, 404);
217
+ }
218
+
219
+ // Check if another instance is running
220
+ function isAlreadyRunning(): boolean {
221
+ try {
222
+ if (existsSync(PID_FILE)) {
223
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
224
+ // Check if process exists
225
+ process.kill(pid, 0);
226
+ return true;
227
+ }
228
+ } catch {
229
+ // Process doesn't exist or PID file invalid
230
+ }
231
+ return false;
232
+ }
233
+
234
+ // Write PID file
235
+ function writePidFile(): void {
236
+ writeFileSync(PID_FILE, String(process.pid));
237
+ }
238
+
239
+ // CLI commands
240
+ const command = process.argv[2];
241
+
242
+ if (command === 'start') {
243
+ if (isAlreadyRunning()) {
244
+ console.log('Worker already running');
245
+ process.exit(0);
246
+ }
247
+
248
+ // Start as daemon (background process)
249
+ if (process.argv[3] !== '--foreground') {
250
+ const child = spawn(process.argv[0], [process.argv[1], 'start', '--foreground'], {
251
+ detached: true,
252
+ stdio: 'ignore'
253
+ });
254
+ child.unref();
255
+ console.log(`Worker started (PID: ${child.pid})`);
256
+ process.exit(0);
257
+ }
258
+
259
+ // Running in foreground
260
+ const server = createServer((req, res) => {
261
+ handleRequest(req, res).catch(err => {
262
+ console.error('Request error:', err);
263
+ json(res, { error: 'Internal server error' }, 500);
264
+ });
265
+ });
266
+
267
+ server.listen(PORT, '127.0.0.1', () => {
268
+ writePidFile();
269
+ console.log(`automagik-genie worker listening on http://127.0.0.1:${PORT}`);
270
+ });
271
+
272
+ // Graceful shutdown
273
+ process.on('SIGTERM', () => {
274
+ server.close();
275
+ process.exit(0);
276
+ });
277
+ process.on('SIGINT', () => {
278
+ server.close();
279
+ process.exit(0);
280
+ });
281
+
282
+ } else if (command === 'stop') {
283
+ try {
284
+ if (existsSync(PID_FILE)) {
285
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
286
+ process.kill(pid, 'SIGTERM');
287
+ console.log('Worker stopped');
288
+ } else {
289
+ console.log('Worker not running');
290
+ }
291
+ } catch {
292
+ console.log('Worker not running');
293
+ }
294
+ process.exit(0);
295
+
296
+ } else if (command === 'status') {
297
+ if (isAlreadyRunning()) {
298
+ const pid = readFileSync(PID_FILE, 'utf-8').trim();
299
+ console.log(`Worker running (PID: ${pid})`);
300
+ // Try to get health status
301
+ try {
302
+ execSync(`curl -s http://127.0.0.1:${PORT}/health`, { encoding: 'utf-8' });
303
+ console.log('Health: OK');
304
+ } catch {
305
+ console.log('Health: Unable to connect');
306
+ }
307
+ } else {
308
+ console.log('Worker not running');
309
+ }
310
+ process.exit(0);
311
+
312
+ } else if (command === 'hook') {
313
+ // Hook subcommand for lifecycle hooks
314
+ const hookType = process.argv[3];
315
+
316
+ if (hookType === 'context') {
317
+ // Inject workflow context - called by SessionStart hook
318
+ try {
319
+ const response = execSync(`curl -s http://127.0.0.1:${PORT}/api/hook/context`, { encoding: 'utf-8' });
320
+ const data = JSON.parse(response);
321
+ if (data.context) {
322
+ console.log(`\n<genie-workflow>\n${data.context}</genie-workflow>\n`);
323
+ }
324
+ } catch {
325
+ // Worker not running, no context to inject
326
+ }
327
+ }
328
+ process.exit(0);
329
+
330
+ } else {
331
+ console.log(`
332
+ automagik-genie worker service
333
+
334
+ Usage:
335
+ worker-service start Start the worker (daemonized)
336
+ worker-service stop Stop the worker
337
+ worker-service status Check worker status
338
+ worker-service hook <type> Run hook command
339
+
340
+ Endpoints:
341
+ GET /health Health check
342
+ GET /api/workflow/status Get workflow state
343
+ POST /api/workflow/update Update workflow state
344
+ POST /api/workflow/wish/start Start tracking a wish
345
+ POST /api/workflow/forge/start Start forge session
346
+ POST /api/workflow/forge/task Update forge task status
347
+ GET /api/hook/context Get context for injection
348
+ POST /api/admin/restart Restart worker
349
+ `);
350
+ process.exit(0);
351
+ }
@@ -39,6 +39,8 @@ export interface CloseOptions {
39
39
  // ============================================================================
40
40
 
41
41
  const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
42
+ // Worktrees are created inside the project at .genie/worktrees/<taskId>
43
+ const WORKTREE_DIR_NAME = '.genie/worktrees';
42
44
 
43
45
  // ============================================================================
44
46
  // Helper Functions
@@ -105,11 +107,19 @@ async function mergeToMain(
105
107
 
106
108
  /**
107
109
  * Remove worktree
108
- * Uses bd worktree when beads registry is enabled
109
- * Falls back to WorktreeManager otherwise
110
+ * Checks .genie/worktrees first, then bd worktree, then WorktreeManager
110
111
  */
111
112
  async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
112
- // Try bd worktree first when beads is enabled
113
+ // First, check .genie/worktrees location (new location)
114
+ const inProjectWorktree = join(repoPath, WORKTREE_DIR_NAME, taskId);
115
+ try {
116
+ await $`git -C ${repoPath} worktree remove ${inProjectWorktree} --force`.quiet();
117
+ return true;
118
+ } catch {
119
+ // Worktree may not exist at this location, continue checking
120
+ }
121
+
122
+ // Try bd worktree when beads is enabled
113
123
  if (useBeads) {
114
124
  try {
115
125
  const removed = await beadsRegistry.removeWorktree(taskId);
@@ -120,7 +130,7 @@ async function removeWorktree(taskId: string, repoPath: string): Promise<boolean
120
130
  }
121
131
  }
122
132
 
123
- // Fallback to WorktreeManager
133
+ // Fallback to WorktreeManager (legacy location)
124
134
  try {
125
135
  const manager = new WorktreeManager({
126
136
  baseDir: WORKTREE_BASE,
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Create command - Simple bd create wrapper
3
+ *
4
+ * Usage:
5
+ * term create "Task title" - Create beads issue
6
+ * term create "Task title" -d "Description" - With description
7
+ * term create "Task title" -p bd-1 - With parent/dependency
8
+ *
9
+ * Options:
10
+ * -d, --description <text> - Issue description
11
+ * -p, --parent <id> - Parent issue ID (creates dependency)
12
+ * --json - Output as JSON
13
+ */
14
+
15
+ import { $ } from 'bun';
16
+
17
+ export interface CreateOptions {
18
+ description?: string;
19
+ parent?: string;
20
+ json?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Run bd command and return result
25
+ */
26
+ async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
27
+ try {
28
+ const result = await $`bd ${args}`.quiet();
29
+ return { stdout: result.stdout.toString().trim(), exitCode: 0 };
30
+ } catch (error: any) {
31
+ return {
32
+ stdout: error.stdout?.toString().trim() || error.message,
33
+ exitCode: error.exitCode || 1
34
+ };
35
+ }
36
+ }
37
+
38
+ export async function createCommand(
39
+ title: string,
40
+ options: CreateOptions = {}
41
+ ): Promise<void> {
42
+ // Build bd create command
43
+ const args = ['create', title];
44
+
45
+ if (options.description) {
46
+ args.push('--description', options.description);
47
+ }
48
+
49
+ // Run bd create
50
+ const { stdout, exitCode } = await runBd(args);
51
+
52
+ if (exitCode !== 0) {
53
+ console.error(`Failed to create issue: ${stdout}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ // Extract issue ID from output
58
+ // bd create typically outputs something like "Created bd-123" or just the ID
59
+ const idMatch = stdout.match(/bd-\d+/);
60
+ const issueId = idMatch ? idMatch[0] : null;
61
+
62
+ if (!issueId) {
63
+ console.log(stdout);
64
+ return;
65
+ }
66
+
67
+ // If parent specified, add blockedBy relationship
68
+ if (options.parent) {
69
+ const { exitCode: updateExit } = await runBd([
70
+ 'update',
71
+ issueId,
72
+ '--blocked-by',
73
+ options.parent,
74
+ ]);
75
+
76
+ if (updateExit !== 0) {
77
+ console.log(`Created ${issueId} (failed to set parent dependency)`);
78
+ }
79
+ }
80
+
81
+ if (options.json) {
82
+ // Fetch full issue details
83
+ const { stdout: showOutput } = await runBd(['show', issueId, '--json']);
84
+ console.log(showOutput);
85
+ } else {
86
+ console.log(`Created: ${issueId} - "${title}"`);
87
+ if (options.parent) {
88
+ console.log(` Blocked by: ${options.parent}`);
89
+ }
90
+ console.log(`\nNext steps:`);
91
+ console.log(` term work ${issueId} - Start working on it`);
92
+ console.log(` term spawn genie:wish -t ${issueId} - Plan with wish skill`);
93
+ console.log(` bd show ${issueId} - View details`);
94
+ }
95
+ }
@@ -9,6 +9,7 @@
9
9
  * --keep-worktree - Don't remove the worktree
10
10
  */
11
11
 
12
+ import { $ } from 'bun';
12
13
  import { confirm } from '@inquirer/prompts';
13
14
  import * as tmux from '../lib/tmux.js';
14
15
  import * as registry from '../lib/worker-registry.js';
@@ -34,6 +35,8 @@ export interface KillOptions {
34
35
  // ============================================================================
35
36
 
36
37
  const WORKTREE_BASE = join(homedir(), '.local', 'share', 'term', 'worktrees');
38
+ // Worktrees are created inside the project at .genie/worktrees/<taskId>
39
+ const WORKTREE_DIR_NAME = '.genie/worktrees';
37
40
 
38
41
  // ============================================================================
39
42
  // Helper Functions
@@ -53,11 +56,19 @@ async function killWorkerPane(paneId: string): Promise<boolean> {
53
56
 
54
57
  /**
55
58
  * Remove worktree
56
- * Uses bd worktree when beads registry is enabled
57
- * Falls back to WorktreeManager otherwise
59
+ * Checks .genie/worktrees first, then bd worktree, then WorktreeManager
58
60
  */
59
61
  async function removeWorktree(taskId: string, repoPath: string): Promise<boolean> {
60
- // Try bd worktree first when beads is enabled
62
+ // First, check .genie/worktrees location (new location)
63
+ const inProjectWorktree = join(repoPath, WORKTREE_DIR_NAME, taskId);
64
+ try {
65
+ await $`git -C ${repoPath} worktree remove ${inProjectWorktree} --force`.quiet();
66
+ return true;
67
+ } catch {
68
+ // Worktree may not exist at this location, continue checking
69
+ }
70
+
71
+ // Try bd worktree when beads is enabled
61
72
  if (useBeads) {
62
73
  try {
63
74
  const removed = await beadsRegistry.removeWorktree(taskId);
@@ -68,7 +79,7 @@ async function removeWorktree(taskId: string, repoPath: string): Promise<boolean
68
79
  }
69
80
  }
70
81
 
71
- // Fallback to WorktreeManager
82
+ // Fallback to WorktreeManager (legacy location)
72
83
  try {
73
84
  const manager = new WorktreeManager({
74
85
  baseDir: WORKTREE_BASE,
@@ -243,8 +243,9 @@ export async function sendMessage(
243
243
  try {
244
244
  const { paneId } = await getSessionPane(sessionName, options.pane);
245
245
 
246
- // Send the message
247
- await tmux.executeCommand(paneId, message, false, false);
246
+ // Send the message cleanly (no TMUX_MCP markers)
247
+ const escapedMessage = message.replace(/'/g, "'\\''");
248
+ await tmux.executeTmux(`send-keys -t '${paneId}' '${escapedMessage}' Enter`);
248
249
 
249
250
  if (options.noWait) {
250
251
  console.log(`✅ Message sent to session "${sessionName}"`);
@@ -1,6 +1,15 @@
1
1
  import * as tmux from '../lib/tmux.js';
2
2
 
3
- export async function sendKeysToSession(sessionName: string, keys: string): Promise<void> {
3
+ export interface SendOptions {
4
+ enter?: boolean;
5
+ pane?: string;
6
+ }
7
+
8
+ export async function sendKeysToSession(
9
+ sessionName: string,
10
+ keys: string,
11
+ options: SendOptions = {}
12
+ ): Promise<void> {
4
13
  try {
5
14
  // Find session
6
15
  const session = await tmux.findSessionByName(sessionName);
@@ -9,24 +18,43 @@ export async function sendKeysToSession(sessionName: string, keys: string): Prom
9
18
  process.exit(1);
10
19
  }
11
20
 
12
- // Get first window and pane
13
- const windows = await tmux.listWindows(session.id);
14
- if (!windows || windows.length === 0) {
15
- console.error(`❌ No windows found in session "${sessionName}"`);
16
- process.exit(1);
17
- }
21
+ let paneId: string;
18
22
 
19
- const panes = await tmux.listPanes(windows[0].id);
20
- if (!panes || panes.length === 0) {
21
- console.error(`❌ No panes found in session "${sessionName}"`);
22
- process.exit(1);
23
+ // Use specified pane or find first pane
24
+ if (options.pane) {
25
+ paneId = options.pane.startsWith('%') ? options.pane : `%${options.pane}`;
26
+ } else {
27
+ // Get first window and pane
28
+ const windows = await tmux.listWindows(session.id);
29
+ if (!windows || windows.length === 0) {
30
+ console.error(`❌ No windows found in session "${sessionName}"`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const panes = await tmux.listPanes(windows[0].id);
35
+ if (!panes || panes.length === 0) {
36
+ console.error(`❌ No panes found in session "${sessionName}"`);
37
+ process.exit(1);
38
+ }
39
+
40
+ paneId = panes[0].id;
23
41
  }
24
42
 
25
- const paneId = panes[0].id;
43
+ // Default: enter is true (append Enter key)
44
+ const withEnter = options.enter !== false;
45
+
46
+ // Escape single quotes for shell
47
+ const escapedKeys = keys.replace(/'/g, "'\\''");
48
+
49
+ if (withEnter) {
50
+ // Send keys with Enter appended
51
+ await tmux.executeTmux(`send-keys -t '${paneId}' '${escapedKeys}' Enter`);
52
+ } else {
53
+ // Send raw keys without Enter
54
+ await tmux.executeTmux(`send-keys -t '${paneId}' '${escapedKeys}'`);
55
+ }
26
56
 
27
- // Send keys without Enter
28
- await tmux.executeCommand(paneId, keys, false, true);
29
- console.log(`✅ Keys sent to session "${sessionName}"`);
57
+ console.log(`✅ Keys sent to session "${sessionName}"${withEnter ? ' (with Enter)' : ''}`);
30
58
  } catch (error: any) {
31
59
  console.error(`❌ Error sending keys: ${error.message}`);
32
60
  process.exit(1);