@automagik/genie 0.260202.1833 → 0.260203.43

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 (108) hide show
  1. package/.beads/README.md +81 -0
  2. package/.beads/config.yaml +67 -0
  3. package/.beads/interactions.jsonl +0 -0
  4. package/.beads/issues.jsonl +9 -0
  5. package/.beads/metadata.json +4 -0
  6. package/.claude/skills/brainstorm/SKILL.md +53 -0
  7. package/.claude/skills/genie-base/SKILL.md +66 -0
  8. package/.claude/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  9. package/.claude/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  10. package/.claude/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  11. package/.claude/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  12. package/.claude/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  13. package/.claude/skills/genie-base/assets/workspace/ROLE.md +14 -0
  14. package/.claude/skills/genie-base/assets/workspace/SOUL.md +36 -0
  15. package/.claude/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  16. package/.claude/skills/genie-base/assets/workspace/USER.md +13 -0
  17. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-30.md +6 -0
  18. package/.claude/skills/genie-base/assets/workspace/memory/2026-01-31.md +16 -0
  19. package/.claude/skills/genie-base/assets/workspace/memory/882c22be-9710-41c1-91f8-ed82947ef6ce.txt +1 -0
  20. package/.claude/skills/genie-base/scripts/install-workspace.sh +107 -0
  21. package/.claude/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  22. package/.claude/skills/genie-blank-init/SKILL.md +37 -0
  23. package/.claude/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  24. package/.claude/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  25. package/.claude/skills/genie-blank-init/assets/SOUL.md +10 -0
  26. package/.claude/skills/genie-blank-init/assets/USER.md +9 -0
  27. package/.claude/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  28. package/.claude/skills/genie-forge/SKILL.md +171 -0
  29. package/.claude/skills/genie-plan-review/CLAUDE.md +11 -0
  30. package/.claude/skills/genie-plan-review/SKILL.md +53 -0
  31. package/.claude/skills/genie-review/SKILL.md +171 -0
  32. package/.claude/skills/genie-wish/SKILL.md +141 -0
  33. package/.claude-plugin/marketplace.json +18 -0
  34. package/.genie/.gitkeep +3 -0
  35. package/.genie/backlog/hooks-v2.md +82 -0
  36. package/.genie/wishes/upgrade-brainstorm-handoff/wish.md +124 -0
  37. package/.gitattributes +3 -0
  38. package/AGENTS.md +75 -0
  39. package/bun.lock +55 -0
  40. package/dist/claudio.js +1 -1
  41. package/dist/genie.js +1 -1
  42. package/dist/term.js +123 -99
  43. package/docs/CO-ORCHESTRATION-GUIDE.md +368 -0
  44. package/package.json +5 -1
  45. package/plugin/.claude-plugin/plugin.json +18 -0
  46. package/plugin/README.md +120 -0
  47. package/plugin/agents/implementor.md +92 -0
  48. package/plugin/agents/quality-reviewer.md +113 -0
  49. package/plugin/agents/spec-reviewer.md +90 -0
  50. package/plugin/hooks/hooks.json +3 -0
  51. package/plugin/references/review-criteria.md +72 -0
  52. package/plugin/references/wish-template.md +92 -0
  53. package/plugin/scripts/genie.cjs +141 -0
  54. package/plugin/scripts/smart-install.js +308 -0
  55. package/plugin/scripts/src/install-genie-cli.sh +120 -0
  56. package/plugin/scripts/src/validate-completion.ts +142 -0
  57. package/plugin/scripts/src/validate-wish.ts +137 -0
  58. package/plugin/scripts/term.cjs +229 -0
  59. package/plugin/scripts/validate-completion.cjs +16 -0
  60. package/plugin/scripts/validate-wish.cjs +17 -0
  61. package/plugin/scripts/worker-service.cjs +28 -0
  62. package/plugin/skills/brainstorm/SKILL.md +106 -0
  63. package/plugin/skills/forge/SKILL.md +171 -0
  64. package/plugin/skills/genie-base/SKILL.md +99 -0
  65. package/plugin/skills/genie-base/assets/workspace/AGENTS.md +191 -0
  66. package/plugin/skills/genie-base/assets/workspace/ENVIRONMENT.md +18 -0
  67. package/plugin/skills/genie-base/assets/workspace/HEARTBEAT.md +4 -0
  68. package/plugin/skills/genie-base/assets/workspace/IDENTITY.md +17 -0
  69. package/plugin/skills/genie-base/assets/workspace/MEMORY.md +16 -0
  70. package/plugin/skills/genie-base/assets/workspace/ROLE.md +14 -0
  71. package/plugin/skills/genie-base/assets/workspace/SOUL.md +36 -0
  72. package/plugin/skills/genie-base/assets/workspace/TOOLS.md +25 -0
  73. package/plugin/skills/genie-base/assets/workspace/USER.md +13 -0
  74. package/plugin/skills/genie-base/scripts/install-workspace.sh +107 -0
  75. package/plugin/skills/genie-base/scripts/sanity-sweep.sh +60 -0
  76. package/plugin/skills/genie-blank-init/SKILL.md +73 -0
  77. package/plugin/skills/genie-blank-init/assets/BOOTSTRAP.md +44 -0
  78. package/plugin/skills/genie-blank-init/assets/IDENTITY.md +9 -0
  79. package/plugin/skills/genie-blank-init/assets/SOUL.md +10 -0
  80. package/plugin/skills/genie-blank-init/assets/USER.md +9 -0
  81. package/plugin/skills/genie-blank-init/scripts/apply-blank-init.sh +117 -0
  82. package/plugin/skills/genie-cli-dev/CLAUDE.md +19 -0
  83. package/plugin/skills/genie-cli-dev/SKILL.md +292 -0
  84. package/plugin/skills/plan-review/SKILL.md +101 -0
  85. package/plugin/skills/review/SKILL.md +221 -0
  86. package/plugin/skills/wish/SKILL.md +110 -0
  87. package/plugin/skills/work-orchestration/SKILL.md +110 -0
  88. package/scripts/build.js +132 -0
  89. package/scripts/smart-install.js +308 -0
  90. package/scripts/sync.js +134 -0
  91. package/src/lib/beads-registry.ts +595 -0
  92. package/src/lib/orchestrator/event-monitor.ts +2 -0
  93. package/src/lib/skill-loader.ts +215 -0
  94. package/src/lib/tmux.ts +30 -11
  95. package/src/lib/version.ts +1 -1
  96. package/src/lib/worker-registry.ts +10 -0
  97. package/src/services/worker-service.ts +351 -0
  98. package/src/term-commands/close.ts +48 -3
  99. package/src/term-commands/create.ts +95 -0
  100. package/src/term-commands/daemon.ts +176 -0
  101. package/src/term-commands/kill.ts +56 -2
  102. package/src/term-commands/orchestrate.ts +3 -2
  103. package/src/term-commands/send.ts +43 -15
  104. package/src/term-commands/spawn.ts +446 -0
  105. package/src/term-commands/split.ts +20 -8
  106. package/src/term-commands/work.ts +279 -37
  107. package/src/term-commands/workers.ts +36 -2
  108. package/src/term.ts +120 -7
@@ -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,24 +20,32 @@ export async function splitSessionPane(
20
20
  process.exit(1);
21
21
  }
22
22
 
23
- // Get first window and pane
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
- const panes = await tmux.listPanes(windows[0].id);
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
- const paneId = panes[0].id;
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';
40
45
 
46
+ // Get source pane's current working directory
47
+ const sourcePath = await tmux.executeTmux(`display-message -p -t '${paneId}' '#{pane_current_path}'`);
48
+
41
49
  // Handle workspace and worktree options
42
50
  let workingDir: string | undefined;
43
51
 
@@ -62,18 +70,22 @@ export async function splitSessionPane(
62
70
  workingDir = manager.getWorktreePath(options.worktree);
63
71
  } else if (options.workspace) {
64
72
  workingDir = options.workspace;
73
+ } else {
74
+ // Default to source pane's current directory
75
+ workingDir = sourcePath.trim() || undefined;
65
76
  }
66
77
 
67
- // Split pane
68
- const newPane = await tmux.splitPane(paneId, splitDirection);
78
+ // Split pane with working directory (tmux -c flag handles this natively)
79
+ const newPane = await tmux.splitPane(paneId, splitDirection, undefined, workingDir);
69
80
  if (!newPane) {
70
81
  console.error('❌ Failed to split pane');
71
82
  process.exit(1);
72
83
  }
73
84
 
74
- // Change to working directory if specified
75
- if (workingDir && newPane) {
76
- await tmux.executeTmux(`send-keys -t '${newPane.id}' 'cd ${workingDir}' Enter`);
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`);
77
89
  }
78
90
 
79
91
  console.log(`✅ Pane split ${splitDirection}ly in session "${sessionName}"`);