@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,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
@@ -222,15 +222,25 @@ export async function killPane(paneId: string): Promise<void> {
222
222
  await executeTmux(`kill-pane -t '${paneId}'`);
223
223
  }
224
224
 
225
+ /**
226
+ * Escape a string for safe use in shell single quotes.
227
+ * Replaces ' with '\'' (end quote, escaped quote, start quote).
228
+ */
229
+ function escapeShellPath(path: string): string {
230
+ return path.replace(/'/g, "'\\''");
231
+ }
232
+
225
233
  /**
226
234
  * Split a tmux pane horizontally or vertically
227
235
  */
228
236
  export async function splitPane(
229
237
  targetPaneId: string,
230
238
  direction: 'horizontal' | 'vertical' = 'vertical',
231
- size?: number
239
+ size?: number,
240
+ workingDir?: string
232
241
  ): Promise<TmuxPane | null> {
233
242
  // Build the split-window command
243
+ // Order follows tmux convention: flags, -c, -p, -t, -F
234
244
  let splitCommand = 'split-window';
235
245
 
236
246
  // Add direction flag (-h for horizontal, -v for vertical)
@@ -240,25 +250,34 @@ export async function splitPane(
240
250
  splitCommand += ' -v';
241
251
  }
242
252
 
243
- // Add target pane
244
- splitCommand += ` -t '${targetPaneId}'`;
253
+ // Add working directory if specified (must come before -t)
254
+ if (workingDir) {
255
+ splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
256
+ }
245
257
 
246
258
  // Add size if specified (as percentage)
247
259
  if (size !== undefined && size > 0 && size < 100) {
248
260
  splitCommand += ` -p ${size}`;
249
261
  }
250
262
 
251
- // Execute the split command
252
- await executeTmux(splitCommand);
263
+ // Add target pane
264
+ splitCommand += ` -t '${targetPaneId}'`;
265
+
266
+ // Add -P flag to print new pane info, with format to get pane ID
267
+ splitCommand += ` -P -F '#{pane_id}'`;
253
268
 
254
- // Get the window ID from the target pane to list all panes
255
- 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();
256
271
 
257
- // List all panes in the window to find the newly created one
258
- 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}'`);
259
274
 
260
- // The newest pane is typically the last one in the list
261
- return panes.length > 0 ? panes[panes.length - 1] : null;
275
+ return {
276
+ id: newPaneId,
277
+ windowId: windowId.trim(),
278
+ active: false,
279
+ title: ''
280
+ };
262
281
  }
263
282
 
264
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.1833';
2
+ export const VERSION = '0.260203.0043';
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
  */
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Worker Service for automagik-genie
4
+ *
5
+ * Background HTTP service for workflow state management.
6
+ * Port: 48888 (avoids collision with claude-mem's 37777)
7
+ */
8
+
9
+ import { createServer, IncomingMessage, ServerResponse } from 'http';
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { homedir } from 'os';
13
+ import { spawn, execSync } from 'child_process';
14
+
15
+ const PORT = 48888;
16
+ const GENIE_DIR = join(homedir(), '.genie');
17
+ const PID_FILE = join(GENIE_DIR, 'worker.pid');
18
+ const STATE_FILE = join(GENIE_DIR, 'workflow-state.json');
19
+
20
+ // Ensure .genie directory exists
21
+ if (!existsSync(GENIE_DIR)) {
22
+ mkdirSync(GENIE_DIR, { recursive: true });
23
+ }
24
+
25
+ interface WorkflowState {
26
+ activeWish?: string;
27
+ activeForge?: {
28
+ wishSlug: string;
29
+ currentTask?: string;
30
+ completedTasks: string[];
31
+ failedTasks: string[];
32
+ };
33
+ lastUpdate: string;
34
+ }
35
+
36
+ function loadState(): WorkflowState {
37
+ try {
38
+ if (existsSync(STATE_FILE)) {
39
+ return JSON.parse(readFileSync(STATE_FILE, 'utf-8'));
40
+ }
41
+ } catch {
42
+ // Ignore parse errors
43
+ }
44
+ return { lastUpdate: new Date().toISOString() };
45
+ }
46
+
47
+ function saveState(state: WorkflowState): void {
48
+ state.lastUpdate = new Date().toISOString();
49
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
50
+ }
51
+
52
+ function json(res: ServerResponse, data: unknown, status = 200): void {
53
+ res.writeHead(status, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify(data));
55
+ }
56
+
57
+ function parseBody(req: IncomingMessage): Promise<unknown> {
58
+ return new Promise((resolve, reject) => {
59
+ let body = '';
60
+ req.on('data', chunk => body += chunk);
61
+ req.on('end', () => {
62
+ try {
63
+ resolve(body ? JSON.parse(body) : {});
64
+ } catch {
65
+ reject(new Error('Invalid JSON'));
66
+ }
67
+ });
68
+ });
69
+ }
70
+
71
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
72
+ const url = new URL(req.url || '/', `http://localhost:${PORT}`);
73
+ const path = url.pathname;
74
+ const method = req.method || 'GET';
75
+
76
+ // CORS headers
77
+ res.setHeader('Access-Control-Allow-Origin', '*');
78
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
79
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
80
+
81
+ if (method === 'OPTIONS') {
82
+ res.writeHead(204);
83
+ res.end();
84
+ return;
85
+ }
86
+
87
+ // Health check
88
+ if (path === '/health' || path === '/') {
89
+ json(res, {
90
+ status: 'ok',
91
+ service: 'automagik-genie',
92
+ version: process.env.GENIE_VERSION || 'dev',
93
+ port: PORT,
94
+ uptime: process.uptime()
95
+ });
96
+ return;
97
+ }
98
+
99
+ // Workflow status
100
+ if (path === '/api/workflow/status' && method === 'GET') {
101
+ const state = loadState();
102
+ json(res, state);
103
+ return;
104
+ }
105
+
106
+ // Update workflow state
107
+ if (path === '/api/workflow/update' && method === 'POST') {
108
+ try {
109
+ const body = await parseBody(req) as Partial<WorkflowState>;
110
+ const state = loadState();
111
+ Object.assign(state, body);
112
+ saveState(state);
113
+ json(res, { success: true, state });
114
+ } catch (error) {
115
+ json(res, { error: 'Invalid request body' }, 400);
116
+ }
117
+ return;
118
+ }
119
+
120
+ // Start wish tracking
121
+ if (path === '/api/workflow/wish/start' && method === 'POST') {
122
+ try {
123
+ const body = await parseBody(req) as { slug: string };
124
+ const state = loadState();
125
+ state.activeWish = body.slug;
126
+ saveState(state);
127
+ json(res, { success: true, wish: body.slug });
128
+ } catch {
129
+ json(res, { error: 'Invalid request' }, 400);
130
+ }
131
+ return;
132
+ }
133
+
134
+ // Start forge session
135
+ if (path === '/api/workflow/forge/start' && method === 'POST') {
136
+ try {
137
+ const body = await parseBody(req) as { wishSlug: string };
138
+ const state = loadState();
139
+ state.activeForge = {
140
+ wishSlug: body.wishSlug,
141
+ completedTasks: [],
142
+ failedTasks: []
143
+ };
144
+ saveState(state);
145
+ json(res, { success: true, forge: state.activeForge });
146
+ } catch {
147
+ json(res, { error: 'Invalid request' }, 400);
148
+ }
149
+ return;
150
+ }
151
+
152
+ // Update forge task status
153
+ if (path === '/api/workflow/forge/task' && method === 'POST') {
154
+ try {
155
+ const body = await parseBody(req) as { task: string; status: 'started' | 'completed' | 'failed' };
156
+ const state = loadState();
157
+ if (!state.activeForge) {
158
+ json(res, { error: 'No active forge session' }, 400);
159
+ return;
160
+ }
161
+ if (body.status === 'started') {
162
+ state.activeForge.currentTask = body.task;
163
+ } else if (body.status === 'completed') {
164
+ state.activeForge.completedTasks.push(body.task);
165
+ state.activeForge.currentTask = undefined;
166
+ } else if (body.status === 'failed') {
167
+ state.activeForge.failedTasks.push(body.task);
168
+ state.activeForge.currentTask = undefined;
169
+ }
170
+ saveState(state);
171
+ json(res, { success: true, forge: state.activeForge });
172
+ } catch {
173
+ json(res, { error: 'Invalid request' }, 400);
174
+ }
175
+ return;
176
+ }
177
+
178
+ // Context hook - inject active workflow into Claude session
179
+ if (path === '/api/hook/context' && method === 'GET') {
180
+ const state = loadState();
181
+ let context = '';
182
+
183
+ if (state.activeWish) {
184
+ context += `Active Wish: ${state.activeWish}\n`;
185
+ }
186
+ if (state.activeForge) {
187
+ context += `Active Forge: ${state.activeForge.wishSlug}\n`;
188
+ if (state.activeForge.currentTask) {
189
+ context += ` Current Task: ${state.activeForge.currentTask}\n`;
190
+ }
191
+ context += ` Completed: ${state.activeForge.completedTasks.length} tasks\n`;
192
+ if (state.activeForge.failedTasks.length > 0) {
193
+ context += ` Failed: ${state.activeForge.failedTasks.length} tasks\n`;
194
+ }
195
+ }
196
+
197
+ if (context) {
198
+ json(res, { context });
199
+ } else {
200
+ json(res, { context: null });
201
+ }
202
+ return;
203
+ }
204
+
205
+ // Admin restart endpoint
206
+ if (path === '/api/admin/restart' && method === 'POST') {
207
+ json(res, { success: true, message: 'Worker restarting...' });
208
+ // Schedule restart after response is sent
209
+ setTimeout(() => {
210
+ process.exit(0);
211
+ }, 100);
212
+ return;
213
+ }
214
+
215
+ // 404 for unknown routes
216
+ json(res, { error: 'Not found', path }, 404);
217
+ }
218
+
219
+ // Check if another instance is running
220
+ function isAlreadyRunning(): boolean {
221
+ try {
222
+ if (existsSync(PID_FILE)) {
223
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
224
+ // Check if process exists
225
+ process.kill(pid, 0);
226
+ return true;
227
+ }
228
+ } catch {
229
+ // Process doesn't exist or PID file invalid
230
+ }
231
+ return false;
232
+ }
233
+
234
+ // Write PID file
235
+ function writePidFile(): void {
236
+ writeFileSync(PID_FILE, String(process.pid));
237
+ }
238
+
239
+ // CLI commands
240
+ const command = process.argv[2];
241
+
242
+ if (command === 'start') {
243
+ if (isAlreadyRunning()) {
244
+ console.log('Worker already running');
245
+ process.exit(0);
246
+ }
247
+
248
+ // Start as daemon (background process)
249
+ if (process.argv[3] !== '--foreground') {
250
+ const child = spawn(process.argv[0], [process.argv[1], 'start', '--foreground'], {
251
+ detached: true,
252
+ stdio: 'ignore'
253
+ });
254
+ child.unref();
255
+ console.log(`Worker started (PID: ${child.pid})`);
256
+ process.exit(0);
257
+ }
258
+
259
+ // Running in foreground
260
+ const server = createServer((req, res) => {
261
+ handleRequest(req, res).catch(err => {
262
+ console.error('Request error:', err);
263
+ json(res, { error: 'Internal server error' }, 500);
264
+ });
265
+ });
266
+
267
+ server.listen(PORT, '127.0.0.1', () => {
268
+ writePidFile();
269
+ console.log(`automagik-genie worker listening on http://127.0.0.1:${PORT}`);
270
+ });
271
+
272
+ // Graceful shutdown
273
+ process.on('SIGTERM', () => {
274
+ server.close();
275
+ process.exit(0);
276
+ });
277
+ process.on('SIGINT', () => {
278
+ server.close();
279
+ process.exit(0);
280
+ });
281
+
282
+ } else if (command === 'stop') {
283
+ try {
284
+ if (existsSync(PID_FILE)) {
285
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
286
+ process.kill(pid, 'SIGTERM');
287
+ console.log('Worker stopped');
288
+ } else {
289
+ console.log('Worker not running');
290
+ }
291
+ } catch {
292
+ console.log('Worker not running');
293
+ }
294
+ process.exit(0);
295
+
296
+ } else if (command === 'status') {
297
+ if (isAlreadyRunning()) {
298
+ const pid = readFileSync(PID_FILE, 'utf-8').trim();
299
+ console.log(`Worker running (PID: ${pid})`);
300
+ // Try to get health status
301
+ try {
302
+ execSync(`curl -s http://127.0.0.1:${PORT}/health`, { encoding: 'utf-8' });
303
+ console.log('Health: OK');
304
+ } catch {
305
+ console.log('Health: Unable to connect');
306
+ }
307
+ } else {
308
+ console.log('Worker not running');
309
+ }
310
+ process.exit(0);
311
+
312
+ } else if (command === 'hook') {
313
+ // Hook subcommand for lifecycle hooks
314
+ const hookType = process.argv[3];
315
+
316
+ if (hookType === 'context') {
317
+ // Inject workflow context - called by SessionStart hook
318
+ try {
319
+ const response = execSync(`curl -s http://127.0.0.1:${PORT}/api/hook/context`, { encoding: 'utf-8' });
320
+ const data = JSON.parse(response);
321
+ if (data.context) {
322
+ console.log(`\n<genie-workflow>\n${data.context}</genie-workflow>\n`);
323
+ }
324
+ } catch {
325
+ // Worker not running, no context to inject
326
+ }
327
+ }
328
+ process.exit(0);
329
+
330
+ } else {
331
+ console.log(`
332
+ automagik-genie worker service
333
+
334
+ Usage:
335
+ worker-service start Start the worker (daemonized)
336
+ worker-service stop Stop the worker
337
+ worker-service status Check worker status
338
+ worker-service hook <type> Run hook command
339
+
340
+ Endpoints:
341
+ GET /health Health check
342
+ GET /api/workflow/status Get workflow state
343
+ POST /api/workflow/update Update workflow state
344
+ POST /api/workflow/wish/start Start tracking a wish
345
+ POST /api/workflow/forge/start Start forge session
346
+ POST /api/workflow/forge/task Update forge task status
347
+ GET /api/hook/context Get context for injection
348
+ POST /api/admin/restart Restart worker
349
+ `);
350
+ process.exit(0);
351
+ }