@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,134 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Sync script for automagik-genie
4
+ *
5
+ * Deploys the built plugin to the install target:
6
+ * ~/.claude/plugins/automagik-genie/
7
+ *
8
+ * Uses pure Node.js - no rsync dependency.
9
+ * Also triggers worker restart after sync.
10
+ */
11
+
12
+ import { execSync } from 'child_process';
13
+ import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, copyFileSync, rmSync } from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+ import http from 'http';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const rootDir = path.join(__dirname, '..');
21
+ const pluginDir = path.join(rootDir, 'plugin');
22
+ const INSTALLED_PATH = path.join(os.homedir(), '.claude', 'plugins', 'automagik-genie');
23
+ const WORKER_PORT = 48888;
24
+
25
+ /**
26
+ * Recursively copy directory contents
27
+ */
28
+ function copyDir(src, dest) {
29
+ mkdirSync(dest, { recursive: true });
30
+
31
+ const entries = readdirSync(src, { withFileTypes: true });
32
+
33
+ for (const entry of entries) {
34
+ const srcPath = path.join(src, entry.name);
35
+ const destPath = path.join(dest, entry.name);
36
+
37
+ if (entry.isDirectory()) {
38
+ copyDir(srcPath, destPath);
39
+ } else {
40
+ copyFileSync(srcPath, destPath);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Clean directory but preserve .git if it exists
47
+ */
48
+ function cleanDir(dir) {
49
+ if (!existsSync(dir)) return;
50
+
51
+ const entries = readdirSync(dir, { withFileTypes: true });
52
+
53
+ for (const entry of entries) {
54
+ // Preserve .git directory for git-based updates
55
+ if (entry.name === '.git') continue;
56
+
57
+ const fullPath = path.join(dir, entry.name);
58
+ rmSync(fullPath, { recursive: true, force: true });
59
+ }
60
+ }
61
+
62
+ function getPluginVersion() {
63
+ try {
64
+ const pluginJsonPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
65
+ const pluginJson = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
66
+ return pluginJson.version;
67
+ } catch (error) {
68
+ console.error('Failed to read plugin version:', error.message);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function triggerWorkerRestart() {
74
+ return new Promise((resolve) => {
75
+ console.log('\nTriggering worker restart...');
76
+ const req = http.request({
77
+ hostname: '127.0.0.1',
78
+ port: WORKER_PORT,
79
+ path: '/api/admin/restart',
80
+ method: 'POST',
81
+ timeout: 2000
82
+ }, (res) => {
83
+ if (res.statusCode === 200) {
84
+ console.log('Worker restart triggered');
85
+ } else {
86
+ console.log(`Worker restart returned status ${res.statusCode}`);
87
+ }
88
+ resolve();
89
+ });
90
+ req.on('error', () => {
91
+ console.log('Worker not running, will start on next hook');
92
+ resolve();
93
+ });
94
+ req.on('timeout', () => {
95
+ req.destroy();
96
+ console.log('Worker restart timed out');
97
+ resolve();
98
+ });
99
+ req.end();
100
+ });
101
+ }
102
+
103
+ async function main() {
104
+ const version = getPluginVersion();
105
+ console.log(`Syncing automagik-genie ${version || 'unknown'} to ${INSTALLED_PATH}...`);
106
+
107
+ try {
108
+ // Ensure target directory exists
109
+ mkdirSync(INSTALLED_PATH, { recursive: true });
110
+
111
+ // Clean existing files (preserving .git)
112
+ console.log('Cleaning target directory...');
113
+ cleanDir(INSTALLED_PATH);
114
+
115
+ // Copy plugin files
116
+ console.log('Copying plugin files...');
117
+ copyDir(pluginDir, INSTALLED_PATH);
118
+
119
+ // Run bun install in target
120
+ console.log('\nRunning bun install in target...');
121
+ execSync('bun install', { cwd: INSTALLED_PATH, stdio: 'inherit' });
122
+
123
+ console.log('\nSync complete!');
124
+
125
+ // Trigger worker restart
126
+ await triggerWorkerRestart();
127
+
128
+ } catch (error) {
129
+ console.error('Sync failed:', error.message);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ main();
@@ -85,6 +85,7 @@ export async function createAgent(
85
85
  repoPath: string;
86
86
  taskId: string;
87
87
  taskTitle?: string;
88
+ claudeSessionId?: string;
88
89
  }
89
90
  ): Promise<string> {
90
91
  // Create agent bead with metadata
@@ -115,6 +116,7 @@ export async function createAgent(
115
116
  repoPath: metadata.repoPath,
116
117
  taskId: metadata.taskId,
117
118
  taskTitle: metadata.taskTitle,
119
+ claudeSessionId: metadata.claudeSessionId,
118
120
  startedAt: new Date().toISOString(),
119
121
  });
120
122
 
@@ -135,6 +137,7 @@ export async function ensureAgent(
135
137
  repoPath: string;
136
138
  taskId: string;
137
139
  taskTitle?: string;
140
+ claudeSessionId?: string;
138
141
  }
139
142
  ): Promise<string> {
140
143
  // Check if agent already exists
@@ -302,6 +305,7 @@ interface AgentMetadata {
302
305
  startedAt: string;
303
306
  wishSlug?: string;
304
307
  groupNumber?: number;
308
+ claudeSessionId?: string;
305
309
  }
306
310
 
307
311
  /**
@@ -321,6 +325,7 @@ function agentToWorker(agent: AgentBead, metadata: AgentMetadata): Worker {
321
325
  state: mapFromBeadsState(agent.state || 'idle'),
322
326
  lastStateChange: new Date().toISOString(), // Would need to track this in beads
323
327
  repoPath: metadata.repoPath,
328
+ claudeSessionId: metadata.claudeSessionId,
324
329
  };
325
330
  }
326
331
 
@@ -391,6 +396,14 @@ export async function hasWorkerForTask(taskId: string): Promise<boolean> {
391
396
  return worker !== null;
392
397
  }
393
398
 
399
+ /**
400
+ * Find worker by Claude session ID
401
+ */
402
+ export async function findBySessionId(sessionId: string): Promise<Worker | null> {
403
+ const workers = await listWorkers();
404
+ return workers.find(w => w.claudeSessionId === sessionId) || null;
405
+ }
406
+
394
407
  // ============================================================================
395
408
  // Daemon Management
396
409
  // ============================================================================
@@ -524,6 +537,7 @@ export async function register(worker: Worker): Promise<void> {
524
537
  repoPath: worker.repoPath,
525
538
  taskId: worker.taskId,
526
539
  taskTitle: worker.taskTitle,
540
+ claudeSessionId: worker.claudeSessionId,
527
541
  });
528
542
 
529
543
  await setAgentState(worker.id, worker.state);
@@ -544,3 +558,38 @@ export async function updateState(workerId: string, state: WorkerState): Promise
544
558
  await setAgentState(workerId, state);
545
559
  await heartbeat(workerId);
546
560
  }
561
+
562
+ /**
563
+ * Update Claude session ID for a worker
564
+ */
565
+ export async function updateSessionId(workerId: string, sessionId: string): Promise<void> {
566
+ const agent = await findAgentByWorkerId(workerId);
567
+ if (!agent) {
568
+ throw new Error(`Agent not found for worker ${workerId}`);
569
+ }
570
+
571
+ // Get current metadata
572
+ const { stdout, exitCode } = await runBd(['show', agent.id, '--json']);
573
+ if (exitCode !== 0 || !stdout) {
574
+ throw new Error(`Failed to get agent metadata for ${workerId}`);
575
+ }
576
+
577
+ const fullAgent = parseJson<AgentBead & { metadata?: AgentMetadata }>(stdout);
578
+ if (!fullAgent?.metadata) {
579
+ throw new Error(`Agent ${workerId} has no metadata`);
580
+ }
581
+
582
+ // Update metadata with new session ID
583
+ const updatedMetadata = { ...fullAgent.metadata, claudeSessionId: sessionId };
584
+ const metadataJson = JSON.stringify(updatedMetadata);
585
+
586
+ const { exitCode: updateExitCode, stderr } = await runBd([
587
+ 'update',
588
+ agent.id,
589
+ `--metadata=${metadataJson}`,
590
+ ]);
591
+
592
+ if (updateExitCode !== 0) {
593
+ throw new Error(`Failed to update session ID: ${stderr}`);
594
+ }
595
+ }
@@ -96,7 +96,9 @@ export class EventMonitor extends EventEmitter {
96
96
  await this.poll();
97
97
 
98
98
  // Start polling
99
+ // Use unref() so the timer doesn't prevent process exit
99
100
  this.pollTimer = setInterval(() => this.poll(), this.options.pollIntervalMs);
101
+ this.pollTimer.unref();
100
102
 
101
103
  this.emit('started', { sessionName: this.sessionName, paneId: this.paneId });
102
104
  }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Skill Loader - Find and load Claude skills
3
+ *
4
+ * Skills are stored in:
5
+ * 1. .claude/skills/<skill-name>/SKILL.md (project local)
6
+ * 2. ~/.claude/skills/<skill-name>/SKILL.md (user global)
7
+ *
8
+ * Skill names are simple (wish, forge, review) and map to directories:
9
+ * - Direct match: wish -> wish/
10
+ * - Prefixed: wish -> genie-wish/
11
+ */
12
+
13
+ import { access, readFile } from 'fs/promises';
14
+ import { join } from 'path';
15
+ import { homedir } from 'os';
16
+
17
+ export interface SkillInfo {
18
+ name: string;
19
+ path: string;
20
+ skillFile: string;
21
+ description?: string;
22
+ }
23
+
24
+ /**
25
+ * Get possible directory names for a skill
26
+ * e.g., "wish" -> ["wish", "genie-wish"]
27
+ */
28
+ function skillNameToDirs(skillName: string): string[] {
29
+ return [skillName, `genie-${skillName}`];
30
+ }
31
+
32
+ /**
33
+ * Check if a path exists
34
+ */
35
+ async function pathExists(path: string): Promise<boolean> {
36
+ try {
37
+ await access(path);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Find a skill by name
46
+ *
47
+ * Search order:
48
+ * 1. .claude/skills/<skill-dir>/SKILL.md (project local)
49
+ * 2. ~/.claude/skills/<skill-dir>/SKILL.md (user global)
50
+ *
51
+ * For each location, tries both exact name and genie-prefixed name.
52
+ *
53
+ * @param skillName - Skill name (e.g., "wish", "forge", "review")
54
+ * @param projectRoot - Project root directory (defaults to cwd)
55
+ * @returns SkillInfo if found, null otherwise
56
+ */
57
+ export async function findSkill(
58
+ skillName: string,
59
+ projectRoot?: string
60
+ ): Promise<SkillInfo | null> {
61
+ const dirNames = skillNameToDirs(skillName);
62
+ const cwd = projectRoot || process.cwd();
63
+
64
+ // Search locations in order of precedence
65
+ const searchLocations = [
66
+ join(cwd, '.claude', 'skills'),
67
+ join(homedir(), '.claude', 'skills'),
68
+ ];
69
+
70
+ for (const location of searchLocations) {
71
+ for (const dirName of dirNames) {
72
+ const skillPath = join(location, dirName);
73
+ const skillFile = join(skillPath, 'SKILL.md');
74
+
75
+ if (await pathExists(skillFile)) {
76
+ // Parse description from frontmatter if available
77
+ let description: string | undefined;
78
+ try {
79
+ const content = await readFile(skillFile, 'utf-8');
80
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
81
+ if (frontmatterMatch) {
82
+ const descMatch = frontmatterMatch[1].match(/description:\s*["']?([^"'\n]+)["']?/);
83
+ if (descMatch) {
84
+ description = descMatch[1];
85
+ }
86
+ }
87
+ } catch {
88
+ // Ignore parse errors
89
+ }
90
+
91
+ return {
92
+ name: skillName,
93
+ path: skillPath,
94
+ skillFile,
95
+ description,
96
+ };
97
+ }
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Validate that a skill exists and has required files
106
+ *
107
+ * @param skillPath - Path to skill directory
108
+ * @returns true if valid, false otherwise
109
+ */
110
+ export async function validateSkill(skillPath: string): Promise<boolean> {
111
+ const skillFile = join(skillPath, 'SKILL.md');
112
+ return pathExists(skillFile);
113
+ }
114
+
115
+ /**
116
+ * Read skill content from SKILL.md
117
+ *
118
+ * @param skillFile - Path to SKILL.md
119
+ * @returns Skill content as string
120
+ */
121
+ export async function readSkillContent(skillFile: string): Promise<string> {
122
+ return readFile(skillFile, 'utf-8');
123
+ }
124
+
125
+ /**
126
+ * Build a prompt that loads a skill
127
+ *
128
+ * Reads the skill file and includes its content directly in the prompt.
129
+ *
130
+ * @param skill - SkillInfo from findSkill
131
+ * @param additionalPrompt - Optional additional context/instructions
132
+ * @returns Combined prompt string
133
+ */
134
+ export async function buildSkillPrompt(
135
+ skill: SkillInfo,
136
+ additionalPrompt?: string
137
+ ): Promise<string> {
138
+ // Read the skill content
139
+ const skillContent = await readFile(skill.skillFile, 'utf-8');
140
+
141
+ const parts = [
142
+ `You are running skill: ${skill.name}`,
143
+ '',
144
+ '## Skill Instructions',
145
+ '',
146
+ skillContent,
147
+ ];
148
+
149
+ if (additionalPrompt) {
150
+ parts.push('', '---', '', '## Additional Context', '', additionalPrompt);
151
+ }
152
+
153
+ return parts.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Parse skill name from SKILL.md frontmatter
158
+ * Falls back to directory name conversion if no frontmatter name
159
+ */
160
+ async function parseSkillName(skillFile: string, dirName: string): Promise<string> {
161
+ try {
162
+ const content = await readFile(skillFile, 'utf-8');
163
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
164
+ if (frontmatterMatch) {
165
+ const nameMatch = frontmatterMatch[1].match(/name:\s*["']?([^"'\n]+)["']?/);
166
+ if (nameMatch) {
167
+ return nameMatch[1].trim();
168
+ }
169
+ }
170
+ } catch {
171
+ // Ignore errors
172
+ }
173
+ // Fallback: convert first hyphen to colon (genie-wish -> genie:wish)
174
+ // but leave subsequent hyphens as-is for multi-word names
175
+ return dirName.replace(/-/, ':');
176
+ }
177
+
178
+ /**
179
+ * List all available skills
180
+ *
181
+ * @param projectRoot - Project root directory (defaults to cwd)
182
+ * @returns Array of skill names found
183
+ */
184
+ export async function listSkills(projectRoot?: string): Promise<string[]> {
185
+ const { readdir } = await import('fs/promises');
186
+ const cwd = projectRoot || process.cwd();
187
+ const skills: string[] = [];
188
+
189
+ const searchPaths = [
190
+ join(cwd, '.claude', 'skills'),
191
+ join(homedir(), '.claude', 'skills'),
192
+ ];
193
+
194
+ for (const searchPath of searchPaths) {
195
+ try {
196
+ const entries = await readdir(searchPath, { withFileTypes: true });
197
+ for (const entry of entries) {
198
+ if (entry.isDirectory()) {
199
+ const skillFile = join(searchPath, entry.name, 'SKILL.md');
200
+ if (await pathExists(skillFile)) {
201
+ // Get skill name from frontmatter or convert directory name
202
+ const skillName = await parseSkillName(skillFile, entry.name);
203
+ if (!skills.includes(skillName)) {
204
+ skills.push(skillName);
205
+ }
206
+ }
207
+ }
208
+ }
209
+ } catch {
210
+ // Directory doesn't exist, skip
211
+ }
212
+ }
213
+
214
+ return skills.sort();
215
+ }
package/src/lib/tmux.ts CHANGED
@@ -240,6 +240,7 @@ export async function splitPane(
240
240
  workingDir?: string
241
241
  ): Promise<TmuxPane | null> {
242
242
  // Build the split-window command
243
+ // Order follows tmux convention: flags, -c, -p, -t, -F
243
244
  let splitCommand = 'split-window';
244
245
 
245
246
  // Add direction flag (-h for horizontal, -v for vertical)
@@ -249,30 +250,34 @@ export async function splitPane(
249
250
  splitCommand += ' -v';
250
251
  }
251
252
 
252
- // Add target pane
253
- splitCommand += ` -t '${targetPaneId}'`;
253
+ // Add working directory if specified (must come before -t)
254
+ if (workingDir) {
255
+ splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
256
+ }
254
257
 
255
258
  // Add size if specified (as percentage)
256
259
  if (size !== undefined && size > 0 && size < 100) {
257
260
  splitCommand += ` -p ${size}`;
258
261
  }
259
262
 
260
- // Add working directory if specified
261
- if (workingDir) {
262
- splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
263
- }
263
+ // Add target pane
264
+ splitCommand += ` -t '${targetPaneId}'`;
264
265
 
265
- // Execute the split command
266
- await executeTmux(splitCommand);
266
+ // Add -P flag to print new pane info, with format to get pane ID
267
+ splitCommand += ` -P -F '#{pane_id}'`;
267
268
 
268
- // Get the window ID from the target pane to list all panes
269
- const windowInfo = await executeTmux(`display-message -p -t '${targetPaneId}' '#{window_id}'`);
269
+ // Execute the split command - returns the new pane ID
270
+ const newPaneId = (await executeTmux(splitCommand)).trim();
270
271
 
271
- // List all panes in the window to find the newly created one
272
- const panes = await listPanes(windowInfo);
272
+ // Get the window ID for the new pane
273
+ const windowId = await executeTmux(`display-message -p -t '${newPaneId}' '#{window_id}'`);
273
274
 
274
- // The newest pane is typically the last one in the list
275
- return panes.length > 0 ? panes[panes.length - 1] : null;
275
+ return {
276
+ id: newPaneId,
277
+ windowId: windowId.trim(),
278
+ active: false,
279
+ title: ''
280
+ };
276
281
  }
277
282
 
278
283
  // Map to track ongoing command executions
@@ -1,5 +1,5 @@
1
1
  // Runtime version (baked in at build time)
2
- export const VERSION = '0.260202.1901';
2
+ export const VERSION = '0.260203.0135';
3
3
 
4
4
  // Generate version string from current datetime
5
5
  // Format: 0.YYMMDD.HHMM (e.g., 0.260201.1430 = Feb 1, 2026 at 14:30)
@@ -47,6 +47,8 @@ export interface Worker {
47
47
  lastStateChange: string;
48
48
  /** Repository path where worker operates */
49
49
  repoPath: string;
50
+ /** Claude session ID for resume capability */
51
+ claudeSessionId?: string;
50
52
  }
51
53
 
52
54
  export interface WorkerRegistry {
@@ -176,6 +178,14 @@ export async function findByWish(wishSlug: string): Promise<Worker[]> {
176
178
  return workers.filter(w => w.wishSlug === wishSlug);
177
179
  }
178
180
 
181
+ /**
182
+ * Find worker by Claude session ID
183
+ */
184
+ export async function findBySessionId(sessionId: string): Promise<Worker | null> {
185
+ const workers = await list();
186
+ return workers.find(w => w.claudeSessionId === sessionId) || null;
187
+ }
188
+
179
189
  /**
180
190
  * Check if a worker exists for a given task
181
191
  */