@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
package/scripts/sync.js
ADDED
|
@@ -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
|
|
253
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
|
|
263
|
-
}
|
|
263
|
+
// Add target pane
|
|
264
|
+
splitCommand += ` -t '${targetPaneId}'`;
|
|
264
265
|
|
|
265
|
-
//
|
|
266
|
-
|
|
266
|
+
// Add -P flag to print new pane info, with format to get pane ID
|
|
267
|
+
splitCommand += ` -P -F '#{pane_id}'`;
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
-
const
|
|
269
|
+
// Execute the split command - returns the new pane ID
|
|
270
|
+
const newPaneId = (await executeTmux(splitCommand)).trim();
|
|
270
271
|
|
|
271
|
-
//
|
|
272
|
-
const
|
|
272
|
+
// Get the window ID for the new pane
|
|
273
|
+
const windowId = await executeTmux(`display-message -p -t '${newPaneId}' '#{window_id}'`);
|
|
273
274
|
|
|
274
|
-
|
|
275
|
-
|
|
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
|
package/src/lib/version.ts
CHANGED
|
@@ -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
|
*/
|