@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
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|