@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,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn command - Spawn Claude with a skill loaded
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* term spawn - Interactive skill picker
|
|
6
|
+
* term spawn <skill> - Spawn Claude with skill loaded
|
|
7
|
+
* term spawn wish - Start wish brainstorming
|
|
8
|
+
* term spawn forge - Execute approved wish
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* -s, --session <name> - Target tmux session
|
|
12
|
+
* --no-worktree - Skip worktree creation (when taskId provided)
|
|
13
|
+
* --focus - Focus the new pane (default: true)
|
|
14
|
+
* -p, --prompt <message> - Additional context for the skill
|
|
15
|
+
* -t, --task-id <id> - Bind to beads issue
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { $ } from 'bun';
|
|
19
|
+
import { randomUUID } from 'crypto';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { search } from '@inquirer/prompts';
|
|
22
|
+
import * as tmux from '../lib/tmux.js';
|
|
23
|
+
import * as registry from '../lib/worker-registry.js';
|
|
24
|
+
import * as beadsRegistry from '../lib/beads-registry.js';
|
|
25
|
+
import * as skillLoader from '../lib/skill-loader.js';
|
|
26
|
+
|
|
27
|
+
// Use beads registry when enabled
|
|
28
|
+
const useBeads = beadsRegistry.isBeadsRegistryEnabled();
|
|
29
|
+
|
|
30
|
+
// Worktrees are created inside the project at .genie/worktrees/<taskId>
|
|
31
|
+
const WORKTREE_DIR_NAME = '.genie/worktrees';
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Types
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
export interface SpawnOptions {
|
|
38
|
+
session?: string;
|
|
39
|
+
noWorktree?: boolean;
|
|
40
|
+
focus?: boolean;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
taskId?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Skill Picker
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Interactive skill picker using fuzzy search
|
|
51
|
+
*/
|
|
52
|
+
async function pickSkill(projectRoot?: string): Promise<skillLoader.SkillInfo | null> {
|
|
53
|
+
const skills = await skillLoader.listSkills(projectRoot);
|
|
54
|
+
|
|
55
|
+
if (skills.length === 0) {
|
|
56
|
+
console.log('No skills found in .claude/skills/ or ~/.claude/skills/');
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build skill options with descriptions
|
|
61
|
+
const skillInfos: Array<{ name: string; info: skillLoader.SkillInfo }> = [];
|
|
62
|
+
for (const name of skills) {
|
|
63
|
+
const info = await skillLoader.findSkill(name, projectRoot);
|
|
64
|
+
if (info) {
|
|
65
|
+
skillInfos.push({ name, info });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const selected = await search({
|
|
70
|
+
message: 'Select skill:',
|
|
71
|
+
source: async (term) => {
|
|
72
|
+
const searchTerm = (term || '').toLowerCase();
|
|
73
|
+
return skillInfos
|
|
74
|
+
.filter(s =>
|
|
75
|
+
s.name.toLowerCase().includes(searchTerm) ||
|
|
76
|
+
(s.info.description?.toLowerCase().includes(searchTerm) ?? false)
|
|
77
|
+
)
|
|
78
|
+
.map(s => ({
|
|
79
|
+
name: s.info.description
|
|
80
|
+
? `${s.name} - ${s.info.description.substring(0, 60)}${s.info.description.length > 60 ? '...' : ''}`
|
|
81
|
+
: s.name,
|
|
82
|
+
value: s.info,
|
|
83
|
+
}));
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return selected;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Helper Functions
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run bd command and parse output
|
|
96
|
+
*/
|
|
97
|
+
async function runBd(args: string[]): Promise<{ stdout: string; exitCode: number }> {
|
|
98
|
+
try {
|
|
99
|
+
const result = await $`bd ${args}`.quiet();
|
|
100
|
+
return { stdout: result.stdout.toString().trim(), exitCode: 0 };
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
return { stdout: error.stdout?.toString().trim() || '', exitCode: error.exitCode || 1 };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get a beads issue by ID
|
|
108
|
+
*/
|
|
109
|
+
async function getBeadsIssue(id: string): Promise<{ id: string; title: string; description?: string } | null> {
|
|
110
|
+
const { stdout, exitCode } = await runBd(['show', id, '--json']);
|
|
111
|
+
if (exitCode !== 0 || !stdout) return null;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(stdout);
|
|
115
|
+
const issue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
116
|
+
if (!issue) return null;
|
|
117
|
+
return {
|
|
118
|
+
id: issue.id,
|
|
119
|
+
title: issue.title || issue.description?.substring(0, 50) || 'Untitled',
|
|
120
|
+
description: issue.description,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get current tmux session name
|
|
129
|
+
*/
|
|
130
|
+
async function getCurrentSession(): Promise<string | null> {
|
|
131
|
+
try {
|
|
132
|
+
const result = await tmux.executeTmux(`display-message -p '#{session_name}'`);
|
|
133
|
+
return result.trim() || null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create worktree for task
|
|
141
|
+
*/
|
|
142
|
+
async function createWorktree(taskId: string, repoPath: string): Promise<string | null> {
|
|
143
|
+
const fs = await import('fs/promises');
|
|
144
|
+
const worktreeDir = join(repoPath, WORKTREE_DIR_NAME);
|
|
145
|
+
const worktreePath = join(worktreeDir, taskId);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await fs.mkdir(worktreeDir, { recursive: true });
|
|
149
|
+
} catch {
|
|
150
|
+
// Directory may already exist
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const stat = await fs.stat(worktreePath);
|
|
155
|
+
if (stat.isDirectory()) {
|
|
156
|
+
console.log(` Worktree for ${taskId} already exists`);
|
|
157
|
+
return worktreePath;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Doesn't exist, will create
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const branchName = `work/${taskId}`;
|
|
164
|
+
try {
|
|
165
|
+
try {
|
|
166
|
+
await $`git -C ${repoPath} branch ${branchName}`.quiet();
|
|
167
|
+
} catch {
|
|
168
|
+
// Branch may already exist
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await $`git -C ${repoPath} worktree add ${worktreePath} ${branchName}`.quiet();
|
|
172
|
+
|
|
173
|
+
// Set up .genie redirect so bd commands work in the worktree
|
|
174
|
+
const genieRedirect = join(worktreePath, '.genie');
|
|
175
|
+
await fs.mkdir(genieRedirect, { recursive: true });
|
|
176
|
+
await fs.writeFile(join(genieRedirect, 'redirect'), join(repoPath, '.genie'));
|
|
177
|
+
|
|
178
|
+
return worktreePath;
|
|
179
|
+
} catch (error: any) {
|
|
180
|
+
console.error(` Failed to create worktree: ${error.message}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Spawn Claude worker in new pane (splits the CURRENT active pane)
|
|
187
|
+
*/
|
|
188
|
+
async function spawnWorkerPane(
|
|
189
|
+
session: string,
|
|
190
|
+
workingDir: string
|
|
191
|
+
): Promise<{ paneId: string } | null> {
|
|
192
|
+
try {
|
|
193
|
+
const sessionObj = await tmux.findSessionByName(session);
|
|
194
|
+
if (!sessionObj) {
|
|
195
|
+
console.error(`Session "${session}" not found`);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const windows = await tmux.listWindows(sessionObj.id);
|
|
200
|
+
if (!windows || windows.length === 0) {
|
|
201
|
+
console.error(`No windows in session "${session}"`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const activeWindow = windows.find(w => w.active) || windows[0];
|
|
206
|
+
const panes = await tmux.listPanes(activeWindow.id);
|
|
207
|
+
if (!panes || panes.length === 0) {
|
|
208
|
+
console.error(`No panes in window "${activeWindow.name}"`);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const activePane = panes.find(p => p.active) || panes[0];
|
|
213
|
+
|
|
214
|
+
const newPane = await tmux.splitPane(
|
|
215
|
+
activePane.id,
|
|
216
|
+
'horizontal',
|
|
217
|
+
50,
|
|
218
|
+
workingDir
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (!newPane) {
|
|
222
|
+
console.error(`Failed to create new pane`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { paneId: newPane.id };
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
console.error(`Error spawning worker pane: ${error.message}`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Main Command
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
export async function spawnCommand(
|
|
238
|
+
skillName: string | undefined,
|
|
239
|
+
options: SpawnOptions = {}
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const repoPath = process.cwd();
|
|
242
|
+
|
|
243
|
+
// 1. Get skill (via picker or direct lookup)
|
|
244
|
+
let skill: skillLoader.SkillInfo | null;
|
|
245
|
+
|
|
246
|
+
if (!skillName) {
|
|
247
|
+
// Interactive picker
|
|
248
|
+
skill = await pickSkill(repoPath);
|
|
249
|
+
if (!skill) {
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
// Direct lookup
|
|
254
|
+
skill = await skillLoader.findSkill(skillName, repoPath);
|
|
255
|
+
|
|
256
|
+
if (!skill) {
|
|
257
|
+
// List available skills as help
|
|
258
|
+
const available = await skillLoader.listSkills(repoPath);
|
|
259
|
+
console.error(`Skill "${skillName}" not found.`);
|
|
260
|
+
if (available.length > 0) {
|
|
261
|
+
console.log('\nAvailable skills:');
|
|
262
|
+
for (const s of available) {
|
|
263
|
+
console.log(` - ${s}`);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
console.log('\nNo skills found in .claude/skills/ or ~/.claude/skills/');
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`Skill: ${skill.name}`);
|
|
273
|
+
if (skill.description) {
|
|
274
|
+
console.log(` ${skill.description}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 2. Get session
|
|
278
|
+
const session = options.session || await getCurrentSession();
|
|
279
|
+
if (!session) {
|
|
280
|
+
console.error('Not in a tmux session. Attach to a session first or use --session.');
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 3. Handle taskId binding (optional)
|
|
285
|
+
let workingDir = repoPath;
|
|
286
|
+
let worktreePath: string | null = null;
|
|
287
|
+
let issue: { id: string; title: string; description?: string } | null = null;
|
|
288
|
+
|
|
289
|
+
if (options.taskId) {
|
|
290
|
+
// Verify issue exists
|
|
291
|
+
issue = await getBeadsIssue(options.taskId);
|
|
292
|
+
if (!issue) {
|
|
293
|
+
console.error(`Issue "${options.taskId}" not found. Run \`bd list\` to see issues.`);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check not already assigned
|
|
298
|
+
const existingWorker = useBeads
|
|
299
|
+
? await beadsRegistry.findByTask(options.taskId)
|
|
300
|
+
: await registry.findByTask(options.taskId);
|
|
301
|
+
|
|
302
|
+
if (existingWorker) {
|
|
303
|
+
console.error(`${options.taskId} already has a worker (pane ${existingWorker.paneId})`);
|
|
304
|
+
console.log(` Run \`term kill ${existingWorker.id}\` first.`);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Create worktree if not disabled
|
|
309
|
+
if (!options.noWorktree) {
|
|
310
|
+
console.log(`Creating worktree for ${options.taskId}...`);
|
|
311
|
+
worktreePath = await createWorktree(options.taskId, repoPath);
|
|
312
|
+
if (worktreePath) {
|
|
313
|
+
workingDir = worktreePath;
|
|
314
|
+
console.log(` Created: ${worktreePath}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 4. Spawn Claude pane
|
|
320
|
+
console.log(`Spawning Claude pane...`);
|
|
321
|
+
const paneResult = await spawnWorkerPane(session, workingDir);
|
|
322
|
+
if (!paneResult) {
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { paneId } = paneResult;
|
|
327
|
+
|
|
328
|
+
// 5. Build prompt with skill
|
|
329
|
+
let prompt = await skillLoader.buildSkillPrompt(skill, options.prompt);
|
|
330
|
+
|
|
331
|
+
// Add issue context if bound to a task
|
|
332
|
+
if (issue) {
|
|
333
|
+
prompt += `\n\n---\n\nBound to beads issue ${issue.id}: "${issue.title}"`;
|
|
334
|
+
if (issue.description) {
|
|
335
|
+
prompt += `\n\n${issue.description}`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 6. Generate Claude session ID for resume capability (only if task-bound)
|
|
340
|
+
const claudeSessionId = options.taskId ? randomUUID() : undefined;
|
|
341
|
+
|
|
342
|
+
// 7. Register worker (if taskId provided)
|
|
343
|
+
if (options.taskId && issue) {
|
|
344
|
+
const worker: registry.Worker = {
|
|
345
|
+
id: options.taskId,
|
|
346
|
+
paneId,
|
|
347
|
+
session,
|
|
348
|
+
worktree: worktreePath,
|
|
349
|
+
taskId: options.taskId,
|
|
350
|
+
taskTitle: issue.title,
|
|
351
|
+
startedAt: new Date().toISOString(),
|
|
352
|
+
state: 'spawning',
|
|
353
|
+
lastStateChange: new Date().toISOString(),
|
|
354
|
+
repoPath,
|
|
355
|
+
claudeSessionId,
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (useBeads) {
|
|
359
|
+
try {
|
|
360
|
+
await beadsRegistry.ensureAgent(options.taskId, {
|
|
361
|
+
paneId,
|
|
362
|
+
session,
|
|
363
|
+
worktree: worktreePath,
|
|
364
|
+
repoPath,
|
|
365
|
+
taskId: options.taskId,
|
|
366
|
+
taskTitle: issue.title,
|
|
367
|
+
claudeSessionId,
|
|
368
|
+
});
|
|
369
|
+
await beadsRegistry.bindWork(options.taskId, options.taskId);
|
|
370
|
+
await beadsRegistry.setAgentState(options.taskId, 'spawning');
|
|
371
|
+
} catch (error: any) {
|
|
372
|
+
console.log(` Beads registration failed: ${error.message} (non-fatal)`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await registry.register(worker);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 8. Escape prompt for shell
|
|
380
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
381
|
+
|
|
382
|
+
// Set BEADS_DIR so bd commands work in worktrees
|
|
383
|
+
const beadsDir = join(repoPath, '.genie');
|
|
384
|
+
|
|
385
|
+
// Escape workingDir for shell
|
|
386
|
+
const escapedWorkingDir = workingDir.replace(/'/g, "'\\''");
|
|
387
|
+
|
|
388
|
+
// 9. Start Claude with prompt (include session ID if task-bound)
|
|
389
|
+
// First cd to correct directory (shell rc files may have overridden tmux -c)
|
|
390
|
+
const sessionIdArg = claudeSessionId ? `--session-id '${claudeSessionId}' ` : '';
|
|
391
|
+
await tmux.executeCommand(paneId, `cd '${escapedWorkingDir}' && BEADS_DIR='${beadsDir}' claude ${sessionIdArg}'${escapedPrompt}'`, true, false);
|
|
392
|
+
|
|
393
|
+
// 10. Update state if task-bound
|
|
394
|
+
if (options.taskId) {
|
|
395
|
+
if (useBeads) {
|
|
396
|
+
await beadsRegistry.setAgentState(options.taskId, 'working').catch(() => {});
|
|
397
|
+
}
|
|
398
|
+
await registry.updateState(options.taskId, 'working');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 11. Focus pane (unless disabled)
|
|
402
|
+
if (options.focus !== false) {
|
|
403
|
+
await tmux.executeTmux(`select-pane -t '${paneId}'`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 12. Output summary
|
|
407
|
+
console.log(`\nSpawned Claude with skill: ${skillName}`);
|
|
408
|
+
console.log(` Pane: ${paneId}`);
|
|
409
|
+
console.log(` Session: ${session}`);
|
|
410
|
+
if (worktreePath) {
|
|
411
|
+
console.log(` Worktree: ${worktreePath}`);
|
|
412
|
+
}
|
|
413
|
+
if (options.taskId) {
|
|
414
|
+
console.log(` Task: ${options.taskId}`);
|
|
415
|
+
if (claudeSessionId) {
|
|
416
|
+
console.log(` Claude Session: ${claudeSessionId}`);
|
|
417
|
+
}
|
|
418
|
+
console.log(`\nCommands:`);
|
|
419
|
+
console.log(` term workers - Check worker status`);
|
|
420
|
+
console.log(` term approve ${options.taskId} - Approve permissions`);
|
|
421
|
+
console.log(` term close ${options.taskId} - Close issue when done`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* List available skills
|
|
427
|
+
*/
|
|
428
|
+
export async function listSkillsCommand(): Promise<void> {
|
|
429
|
+
const skills = await skillLoader.listSkills();
|
|
430
|
+
|
|
431
|
+
if (skills.length === 0) {
|
|
432
|
+
console.log('No skills found in .claude/skills/ or ~/.claude/skills/');
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log('Available skills:\n');
|
|
437
|
+
for (const skillName of skills) {
|
|
438
|
+
const skill = await skillLoader.findSkill(skillName);
|
|
439
|
+
if (skill?.description) {
|
|
440
|
+
console.log(` ${skillName}`);
|
|
441
|
+
console.log(` ${skill.description}\n`);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(` ${skillName}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -20,20 +20,25 @@ export async function splitSessionPane(
|
|
|
20
20
|
process.exit(1);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Get
|
|
23
|
+
// Get windows and find active one
|
|
24
24
|
const windows = await tmux.listWindows(session.id);
|
|
25
25
|
if (!windows || windows.length === 0) {
|
|
26
26
|
console.error(`❌ No windows found in session "${sessionName}"`);
|
|
27
27
|
process.exit(1);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Find active window (default to first if none marked active)
|
|
31
|
+
const activeWindow = windows.find(w => w.active) || windows[0];
|
|
32
|
+
|
|
33
|
+
const panes = await tmux.listPanes(activeWindow.id);
|
|
31
34
|
if (!panes || panes.length === 0) {
|
|
32
35
|
console.error(`❌ No panes found in session "${sessionName}"`);
|
|
33
36
|
process.exit(1);
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
// Find active pane (default to first if none marked active)
|
|
40
|
+
const activePane = panes.find(p => p.active) || panes[0];
|
|
41
|
+
const paneId = activePane.id;
|
|
37
42
|
|
|
38
43
|
// Determine direction
|
|
39
44
|
const splitDirection = direction === 'h' ? 'horizontal' : 'vertical';
|
|
@@ -77,6 +82,12 @@ export async function splitSessionPane(
|
|
|
77
82
|
process.exit(1);
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
// Send cd command to new pane to ensure correct working directory
|
|
86
|
+
// (works around shell rc files that override the starting directory)
|
|
87
|
+
if (workingDir) {
|
|
88
|
+
await tmux.executeTmux(`send-keys -t '${newPane.id}' 'cd ${workingDir.replace(/'/g, "'\\''")} && clear' Enter`);
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
console.log(`✅ Pane split ${splitDirection}ly in session "${sessionName}"`);
|
|
81
92
|
if (workingDir) {
|
|
82
93
|
console.log(` Working directory: ${workingDir}`);
|