octomux 1.0.0

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 (56) hide show
  1. package/README.md +60 -0
  2. package/bin/octomux.js +139 -0
  3. package/cli/dist/client.js +56 -0
  4. package/cli/dist/commands/add-agent.js +20 -0
  5. package/cli/dist/commands/cancel-task.js +10 -0
  6. package/cli/dist/commands/close-task.js +16 -0
  7. package/cli/dist/commands/create-task.js +35 -0
  8. package/cli/dist/commands/delete-task.js +16 -0
  9. package/cli/dist/commands/get-task.js +37 -0
  10. package/cli/dist/commands/list-tasks.js +31 -0
  11. package/cli/dist/commands/resume-task.js +18 -0
  12. package/cli/dist/commands/send-message.js +18 -0
  13. package/cli/dist/format.js +40 -0
  14. package/cli/dist/index.js +36 -0
  15. package/cli/package.json +9 -0
  16. package/dist/assets/TaskDetail-GGGQ2C1J.js +1 -0
  17. package/dist/assets/TerminalView-CguHyqU9.js +3 -0
  18. package/dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
  19. package/dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
  20. package/dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
  21. package/dist/assets/index-Br9dLOzs.css +1 -0
  22. package/dist/assets/index-Bsj_BLLM.js +2 -0
  23. package/dist/assets/vendor-react-BZ8ItZjw.js +49 -0
  24. package/dist/assets/vendor-router-DRLGqALp.js +12 -0
  25. package/dist/assets/vendor-ui-CWZtXYLx.js +31 -0
  26. package/dist/assets/vendor-xterm-DYP7pi_n.css +32 -0
  27. package/dist/assets/vendor-xterm-DvXGiZvM.js +9 -0
  28. package/dist/index.html +17 -0
  29. package/dist/logo.png +0 -0
  30. package/dist-server/api.d.ts +2 -0
  31. package/dist-server/api.js +447 -0
  32. package/dist-server/app.d.ts +2 -0
  33. package/dist-server/app.js +13 -0
  34. package/dist-server/db.d.ts +7 -0
  35. package/dist-server/db.js +107 -0
  36. package/dist-server/events.d.ts +13 -0
  37. package/dist-server/events.js +35 -0
  38. package/dist-server/hook-settings.d.ts +5 -0
  39. package/dist-server/hook-settings.js +195 -0
  40. package/dist-server/hooks.d.ts +2 -0
  41. package/dist-server/hooks.js +118 -0
  42. package/dist-server/index.d.ts +5 -0
  43. package/dist-server/index.js +81 -0
  44. package/dist-server/orchestrator.d.ts +4 -0
  45. package/dist-server/orchestrator.js +37 -0
  46. package/dist-server/poller.d.ts +17 -0
  47. package/dist-server/poller.js +170 -0
  48. package/dist-server/pr-template.d.ts +7 -0
  49. package/dist-server/pr-template.js +29 -0
  50. package/dist-server/task-runner.d.ts +28 -0
  51. package/dist-server/task-runner.js +359 -0
  52. package/dist-server/terminal.d.ts +13 -0
  53. package/dist-server/terminal.js +173 -0
  54. package/dist-server/types.d.ts +73 -0
  55. package/dist-server/types.js +1 -0
  56. package/package.json +113 -0
@@ -0,0 +1,195 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ /**
4
+ * Tools that agents are allowed to use without permission prompts.
5
+ * Categorized as read-only or safe-write operations.
6
+ *
7
+ * NEVER auto-allow: git push --force, git reset --hard, rm -rf, deploy commands
8
+ */
9
+ // Tools that are neither allowed nor denied will prompt for permission each time.
10
+ // git commit and gh pr create are intentionally omitted from both lists so agents
11
+ // must get explicit approval before committing or creating PRs.
12
+ const DENIED_TOOLS = [
13
+ // Destructive operations — always blocked
14
+ 'Bash(git push --force:*)',
15
+ 'Bash(git reset --hard:*)',
16
+ 'Bash(rm -rf:*)',
17
+ ];
18
+ const ALLOWED_TOOLS = [
19
+ // --- Read-only shell commands ---
20
+ 'Bash(cat:*)',
21
+ 'Bash(cd:*)',
22
+ 'Bash(echo:*)',
23
+ 'Bash(env:*)',
24
+ 'Bash(find:*)',
25
+ 'Bash(grep:*)',
26
+ 'Bash(head:*)',
27
+ 'Bash(jq:*)',
28
+ 'Bash(ls:*)',
29
+ 'Bash(lsof:*)',
30
+ 'Bash(pwd:*)',
31
+ 'Bash(rg:*)',
32
+ 'Bash(sed:*)',
33
+ 'Bash(sort:*)',
34
+ 'Bash(sqlite3:*)',
35
+ 'Bash(stat:*)',
36
+ 'Bash(tail:*)',
37
+ 'Bash(tree:*)',
38
+ 'Bash(wc:*)',
39
+ 'Bash(which:*)',
40
+ 'Bash(xargs:*)',
41
+ // --- Safe write: build tools & package managers ---
42
+ 'Bash(bun:*)',
43
+ 'Bash(bunx:*)',
44
+ 'Bash(go:*)',
45
+ 'Bash(node:*)',
46
+ 'Bash(npx:*)',
47
+ 'Bash(npm:*)',
48
+ 'Bash(python3:*)',
49
+ 'Bash(tsc:*)',
50
+ 'Bash(vitest:*)',
51
+ 'Bash(playwright:*)',
52
+ // --- Safe: git (read + stage + push, commit denied — use /octomux-create-commit) ---
53
+ 'Bash(git add:*)',
54
+ 'Bash(git branch:*)',
55
+ 'Bash(git checkout:*)',
56
+ 'Bash(git diff:*)',
57
+ 'Bash(git log:*)',
58
+ 'Bash(git pull:*)',
59
+ 'Bash(git push:*)',
60
+ 'Bash(git stash:*)',
61
+ 'Bash(git status:*)',
62
+ 'Bash(git show:*)',
63
+ 'Bash(git rev-parse:*)',
64
+ 'Bash(git worktree:*)',
65
+ 'Bash(git ls-remote:*)',
66
+ 'Bash(git remote:*)',
67
+ 'Bash(git fetch:*)',
68
+ 'Bash(git merge:*)',
69
+ 'Bash(git rebase:*)',
70
+ 'Bash(git cherry-pick:*)',
71
+ 'Bash(git tag:*)',
72
+ // --- Safe: GitHub CLI (read only, PR creation denied — use /octomux-create-pr) ---
73
+ 'Bash(gh issue:*)',
74
+ 'Bash(gh api:*)',
75
+ 'Bash(gh repo view:*)',
76
+ 'Bash(gh pr view:*)',
77
+ 'Bash(gh pr list:*)',
78
+ // --- Safe write: file operations ---
79
+ 'Bash(cp:*)',
80
+ 'Bash(mkdir:*)',
81
+ 'Bash(chmod:*)',
82
+ 'Bash(touch:*)',
83
+ 'Bash(mv:*)',
84
+ // --- Safe write: network & misc ---
85
+ 'Bash(curl:*)',
86
+ 'Bash(for:*)',
87
+ // --- Claude Code internal tools ---
88
+ 'ExitPlanMode',
89
+ 'AskUserQuestion',
90
+ // --- Atlassian MCP tools (read-only) ---
91
+ 'mcp__plugin_atlassian_atlassian__getJiraIssue',
92
+ 'mcp__plugin_atlassian_atlassian__searchJiraIssuesUsingJql',
93
+ 'mcp__plugin_atlassian_atlassian__getVisibleJiraProjects',
94
+ 'mcp__plugin_atlassian_atlassian__getConfluencePage',
95
+ 'mcp__plugin_atlassian_atlassian__searchConfluenceUsingCql',
96
+ 'mcp__plugin_atlassian_atlassian__searchAtlassian',
97
+ // --- Playwright MCP tools ---
98
+ 'mcp__plugin_playwright_playwright__browser_click',
99
+ 'mcp__plugin_playwright_playwright__browser_close',
100
+ 'mcp__plugin_playwright_playwright__browser_console_messages',
101
+ 'mcp__plugin_playwright_playwright__browser_evaluate',
102
+ 'mcp__plugin_playwright_playwright__browser_fill_form',
103
+ 'mcp__plugin_playwright_playwright__browser_navigate',
104
+ 'mcp__plugin_playwright_playwright__browser_run_code',
105
+ 'mcp__plugin_playwright_playwright__browser_snapshot',
106
+ 'mcp__plugin_playwright_playwright__browser_take_screenshot',
107
+ 'mcp__plugin_playwright_playwright__browser_type',
108
+ ];
109
+ const HOOK_EVENTS = {
110
+ UserPromptSubmit: [
111
+ {
112
+ hooks: [
113
+ {
114
+ type: 'http',
115
+ url: 'http://localhost:7777/api/hooks/user-prompt-submit',
116
+ timeout: 5,
117
+ },
118
+ ],
119
+ },
120
+ ],
121
+ PermissionRequest: [
122
+ {
123
+ hooks: [
124
+ {
125
+ type: 'http',
126
+ url: 'http://localhost:7777/api/hooks/permission-request',
127
+ timeout: 5,
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ PostToolUse: [
133
+ {
134
+ hooks: [
135
+ {
136
+ type: 'http',
137
+ url: 'http://localhost:7777/api/hooks/post-tool-use',
138
+ timeout: 5,
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ Stop: [
144
+ {
145
+ hooks: [
146
+ {
147
+ type: 'http',
148
+ url: 'http://localhost:7777/api/hooks/stop',
149
+ timeout: 5,
150
+ },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ /**
156
+ * Install Claude Code hook settings into a worktree's `.claude/settings.local.json`.
157
+ * Merges with any existing settings, preserving non-hook keys and non-overlapping hook events.
158
+ */
159
+ export function installHookSettings(worktreePath) {
160
+ const claudeDir = path.join(worktreePath, '.claude');
161
+ const settingsPath = path.join(claudeDir, 'settings.local.json');
162
+ // Ensure .claude/ directory exists
163
+ fs.mkdirSync(claudeDir, { recursive: true });
164
+ // Read existing settings (if any)
165
+ let existing = {};
166
+ try {
167
+ const raw = fs.readFileSync(settingsPath, 'utf-8');
168
+ existing = JSON.parse(raw);
169
+ if (typeof existing !== 'object' || existing === null || Array.isArray(existing)) {
170
+ existing = {};
171
+ }
172
+ }
173
+ catch {
174
+ // File doesn't exist or is corrupted — start fresh
175
+ existing = {};
176
+ }
177
+ // Merge hooks: our events override, but preserve other hook events
178
+ const existingHooks = typeof existing.hooks === 'object' && existing.hooks !== null && !Array.isArray(existing.hooks)
179
+ ? existing.hooks
180
+ : {};
181
+ const mergedHooks = { ...existingHooks, ...HOOK_EVENTS };
182
+ // Merge permissions: combine our allowed tools with any existing ones (deduplicated)
183
+ const existingPerms = typeof existing.permissions === 'object' &&
184
+ existing.permissions !== null &&
185
+ !Array.isArray(existing.permissions)
186
+ ? existing.permissions
187
+ : {};
188
+ const existingAllow = Array.isArray(existingPerms.allow) ? existingPerms.allow : [];
189
+ const mergedAllow = [...new Set([...ALLOWED_TOOLS, ...existingAllow])];
190
+ const existingDeny = Array.isArray(existingPerms.deny) ? existingPerms.deny : [];
191
+ const mergedDeny = [...new Set([...DENIED_TOOLS, ...existingDeny])];
192
+ const mergedPermissions = { ...existingPerms, allow: mergedAllow, deny: mergedDeny };
193
+ const merged = { ...existing, permissions: mergedPermissions, hooks: mergedHooks };
194
+ fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n');
195
+ }
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export { router as hookRoutes };
@@ -0,0 +1,118 @@
1
+ import { Router } from 'express';
2
+ import { nanoid } from 'nanoid';
3
+ import { getDb } from './db.js';
4
+ import { broadcast } from './events.js';
5
+ const router = Router();
6
+ function findAgentBySessionId(sessionId) {
7
+ return getDb()
8
+ .prepare(`SELECT a.id, a.task_id FROM agents a
9
+ WHERE a.claude_session_id = ? AND a.status != 'stopped'
10
+ LIMIT 1`)
11
+ .get(sessionId);
12
+ }
13
+ // POST /api/hooks/user-prompt-submit
14
+ // Fires when the user submits a prompt — agent resumes working
15
+ router.post('/user-prompt-submit', (req, res) => {
16
+ const { session_id } = req.body;
17
+ if (!session_id) {
18
+ res.status(200).send();
19
+ return;
20
+ }
21
+ const agent = findAgentBySessionId(session_id);
22
+ if (!agent) {
23
+ res.status(200).send();
24
+ return;
25
+ }
26
+ getDb()
27
+ .prepare(`UPDATE agents SET hook_activity = 'active', hook_activity_updated_at = datetime('now')
28
+ WHERE id = ?`)
29
+ .run(agent.id);
30
+ broadcast({ type: 'task:updated', payload: { taskId: agent.task_id } });
31
+ res.status(200).send();
32
+ });
33
+ // POST /api/hooks/permission-request
34
+ router.post('/permission-request', (req, res) => {
35
+ const { session_id, tool_name, tool_input } = req.body;
36
+ if (!session_id || !tool_name) {
37
+ res.status(200).send();
38
+ return;
39
+ }
40
+ const agent = findAgentBySessionId(session_id);
41
+ if (!agent) {
42
+ res.status(200).send();
43
+ return;
44
+ }
45
+ const txn = getDb().transaction(() => {
46
+ getDb()
47
+ .prepare(`INSERT INTO permission_prompts (id, task_id, agent_id, session_id, tool_name, tool_input, status, created_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', datetime('now'))`)
49
+ .run(nanoid(12), agent.task_id, agent.id, session_id, tool_name, JSON.stringify(tool_input || {}));
50
+ getDb()
51
+ .prepare(`UPDATE agents SET hook_activity = 'waiting', hook_activity_updated_at = datetime('now')
52
+ WHERE id = ?`)
53
+ .run(agent.id);
54
+ });
55
+ txn();
56
+ broadcast({ type: 'task:updated', payload: { taskId: agent.task_id } });
57
+ res.status(200).send();
58
+ });
59
+ // POST /api/hooks/post-tool-use
60
+ router.post('/post-tool-use', (req, res) => {
61
+ const { session_id } = req.body;
62
+ if (!session_id) {
63
+ res.status(200).send();
64
+ return;
65
+ }
66
+ const agent = findAgentBySessionId(session_id);
67
+ if (!agent) {
68
+ res.status(200).send();
69
+ return;
70
+ }
71
+ const txn = getDb().transaction(() => {
72
+ // Resolve oldest pending prompt (FIFO)
73
+ getDb()
74
+ .prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
75
+ WHERE id = (
76
+ SELECT id FROM permission_prompts
77
+ WHERE agent_id = ? AND status = 'pending'
78
+ ORDER BY created_at ASC LIMIT 1
79
+ )`)
80
+ .run(agent.id);
81
+ // Only set active if not already idle (Stop hook may have fired first)
82
+ getDb()
83
+ .prepare(`UPDATE agents SET hook_activity = 'active', hook_activity_updated_at = datetime('now')
84
+ WHERE id = ? AND hook_activity != 'idle'`)
85
+ .run(agent.id);
86
+ });
87
+ txn();
88
+ broadcast({ type: 'task:updated', payload: { taskId: agent.task_id } });
89
+ res.status(200).send();
90
+ });
91
+ // POST /api/hooks/stop
92
+ router.post('/stop', (req, res) => {
93
+ const { session_id } = req.body;
94
+ if (!session_id) {
95
+ res.status(200).send();
96
+ return;
97
+ }
98
+ const agent = findAgentBySessionId(session_id);
99
+ if (!agent) {
100
+ res.status(200).send();
101
+ return;
102
+ }
103
+ const txn = getDb().transaction(() => {
104
+ // Resolve ALL pending prompts for this agent
105
+ getDb()
106
+ .prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
107
+ WHERE agent_id = ? AND status = 'pending'`)
108
+ .run(agent.id);
109
+ getDb()
110
+ .prepare(`UPDATE agents SET hook_activity = 'idle', hook_activity_updated_at = datetime('now')
111
+ WHERE id = ?`)
112
+ .run(agent.id);
113
+ });
114
+ txn();
115
+ broadcast({ type: 'task:updated', payload: { taskId: agent.task_id } });
116
+ res.status(200).send();
117
+ });
118
+ export { router as hookRoutes };
@@ -0,0 +1,5 @@
1
+ import { type IncomingMessage } from 'http';
2
+ import express from 'express';
3
+ declare const app: express.Express;
4
+ declare const server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
5
+ export { server, app };
@@ -0,0 +1,81 @@
1
+ import{createServer as De}from"http";import Ut from"path";import ve from"fs";import Ne from"express";import{fileURLToPath as Oe}from"url";import St from"express";import{nanoid as pe}from"nanoid";import w from"fs";import Y from"path";import ue from"os";import Ft from"better-sqlite3";import k from"path";import et from"fs";import Wt from"os";import{fileURLToPath as Ht}from"url";var st=k.dirname(Ht(import.meta.url)),nt=process.env.NODE_ENV==="production",jt=k.join(Wt.homedir(),".octomux","data"),Mt=k.join(st,"..","data"),rt=nt?jt:Mt,B=k.join(rt,"tasks.db"),F=k.join(st,"..","data","tasks.db"),$t=`
2
+ CREATE TABLE IF NOT EXISTS tasks (
3
+ id TEXT PRIMARY KEY,
4
+ title TEXT NOT NULL,
5
+ description TEXT NOT NULL,
6
+ repo_path TEXT NOT NULL,
7
+ status TEXT NOT NULL DEFAULT 'draft',
8
+ branch TEXT,
9
+ base_branch TEXT,
10
+ worktree TEXT,
11
+ tmux_session TEXT,
12
+ pr_url TEXT,
13
+ pr_number INTEGER,
14
+ initial_prompt TEXT,
15
+ error TEXT,
16
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
17
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS agents (
21
+ id TEXT PRIMARY KEY,
22
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
23
+ window_index INTEGER NOT NULL,
24
+ label TEXT NOT NULL,
25
+ status TEXT NOT NULL DEFAULT 'running',
26
+ claude_session_id TEXT,
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS permission_prompts (
31
+ id TEXT PRIMARY KEY,
32
+ task_id TEXT NOT NULL,
33
+ agent_id TEXT,
34
+ session_id TEXT NOT NULL,
35
+ tool_name TEXT NOT NULL,
36
+ tool_input TEXT NOT NULL DEFAULT '{}',
37
+ status TEXT NOT NULL DEFAULT 'pending',
38
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
39
+ resolved_at TEXT,
40
+ FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
41
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
42
+ );
43
+
44
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
45
+ CREATE INDEX IF NOT EXISTS idx_agents_task ON agents(task_id);
46
+ CREATE INDEX IF NOT EXISTS idx_permission_prompts_task_id ON permission_prompts(task_id);
47
+ CREATE INDEX IF NOT EXISTS idx_permission_prompts_status ON permission_prompts(status);
48
+ CREATE INDEX IF NOT EXISTS idx_permission_prompts_agent_status ON permission_prompts(agent_id, status);
49
+ `,A;function d(){return A||(et.mkdirSync(rt,{recursive:!0}),nt&&F!==B&&et.existsSync(F)&&console.log(`Found database at old location: ${F}
50
+ Copy to ${B} to migrate your data.`),A=new Ft(B),Xt(A)),A}function Xt(t){t.pragma("journal_mode = WAL"),t.pragma("foreign_keys = ON"),t.exec($t);let e=t.pragma("table_info(tasks)").map(i=>i.name);e.includes("initial_prompt")||t.exec("ALTER TABLE tasks ADD COLUMN initial_prompt TEXT"),t.pragma("table_info(agents)").map(i=>i.name).includes("claude_session_id")||t.exec("ALTER TABLE agents ADD COLUMN claude_session_id TEXT"),e.includes("base_branch")||t.exec("ALTER TABLE tasks ADD COLUMN base_branch TEXT"),e.includes("user_window_index")||t.exec("ALTER TABLE tasks ADD COLUMN user_window_index INTEGER"),t.pragma("table_info(agents)").map(i=>i.name).includes("hook_activity")||(t.exec("ALTER TABLE agents ADD COLUMN hook_activity TEXT NOT NULL DEFAULT 'active'"),t.exec("ALTER TABLE agents ADD COLUMN hook_activity_updated_at TEXT")),t.exec("UPDATE tasks SET status = 'draft' WHERE status = 'created'"),t.exec("UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now') WHERE status = 'pending'"),t.exec(`UPDATE agents SET hook_activity = 'active', hook_activity_updated_at = datetime('now')
51
+ WHERE hook_activity = 'waiting' AND status = 'running'`)}import{execFile as ce}from"child_process";import{promisify as le}from"util";import{execFile as Jt,spawn as Gt}from"child_process";import{promisify as zt}from"util";import H from"crypto";import R from"path";import x from"fs";import{nanoid as j}from"nanoid";import at from"path";import W from"fs";var qt=["Bash(git push --force:*)","Bash(git reset --hard:*)","Bash(rm -rf:*)"],Yt=["Bash(cat:*)","Bash(cd:*)","Bash(echo:*)","Bash(env:*)","Bash(find:*)","Bash(grep:*)","Bash(head:*)","Bash(jq:*)","Bash(ls:*)","Bash(lsof:*)","Bash(pwd:*)","Bash(rg:*)","Bash(sed:*)","Bash(sort:*)","Bash(sqlite3:*)","Bash(stat:*)","Bash(tail:*)","Bash(tree:*)","Bash(wc:*)","Bash(which:*)","Bash(xargs:*)","Bash(bun:*)","Bash(bunx:*)","Bash(go:*)","Bash(node:*)","Bash(npx:*)","Bash(npm:*)","Bash(python3:*)","Bash(tsc:*)","Bash(vitest:*)","Bash(playwright:*)","Bash(git add:*)","Bash(git branch:*)","Bash(git checkout:*)","Bash(git diff:*)","Bash(git log:*)","Bash(git pull:*)","Bash(git push:*)","Bash(git stash:*)","Bash(git status:*)","Bash(git show:*)","Bash(git rev-parse:*)","Bash(git worktree:*)","Bash(git ls-remote:*)","Bash(git remote:*)","Bash(git fetch:*)","Bash(git merge:*)","Bash(git rebase:*)","Bash(git cherry-pick:*)","Bash(git tag:*)","Bash(gh issue:*)","Bash(gh api:*)","Bash(gh repo view:*)","Bash(gh pr view:*)","Bash(gh pr list:*)","Bash(cp:*)","Bash(mkdir:*)","Bash(chmod:*)","Bash(touch:*)","Bash(mv:*)","Bash(curl:*)","Bash(for:*)","ExitPlanMode","AskUserQuestion","mcp__plugin_atlassian_atlassian__getJiraIssue","mcp__plugin_atlassian_atlassian__searchJiraIssuesUsingJql","mcp__plugin_atlassian_atlassian__getVisibleJiraProjects","mcp__plugin_atlassian_atlassian__getConfluencePage","mcp__plugin_atlassian_atlassian__searchConfluenceUsingCql","mcp__plugin_atlassian_atlassian__searchAtlassian","mcp__plugin_playwright_playwright__browser_click","mcp__plugin_playwright_playwright__browser_close","mcp__plugin_playwright_playwright__browser_console_messages","mcp__plugin_playwright_playwright__browser_evaluate","mcp__plugin_playwright_playwright__browser_fill_form","mcp__plugin_playwright_playwright__browser_navigate","mcp__plugin_playwright_playwright__browser_run_code","mcp__plugin_playwright_playwright__browser_snapshot","mcp__plugin_playwright_playwright__browser_take_screenshot","mcp__plugin_playwright_playwright__browser_type"],Vt={UserPromptSubmit:[{hooks:[{type:"http",url:"http://localhost:7777/api/hooks/user-prompt-submit",timeout:5}]}],PermissionRequest:[{hooks:[{type:"http",url:"http://localhost:7777/api/hooks/permission-request",timeout:5}]}],PostToolUse:[{hooks:[{type:"http",url:"http://localhost:7777/api/hooks/post-tool-use",timeout:5}]}],Stop:[{hooks:[{type:"http",url:"http://localhost:7777/api/hooks/stop",timeout:5}]}]};function y(t){let n=at.join(t,".claude"),e=at.join(n,"settings.local.json");W.mkdirSync(n,{recursive:!0});let s={};try{let f=W.readFileSync(e,"utf-8");s=JSON.parse(f),(typeof s!="object"||s===null||Array.isArray(s))&&(s={})}catch{s={}}let a={...typeof s.hooks=="object"&&s.hooks!==null&&!Array.isArray(s.hooks)?s.hooks:{},...Vt},o=typeof s.permissions=="object"&&s.permissions!==null&&!Array.isArray(s.permissions)?s.permissions:{},i=Array.isArray(o.allow)?o.allow:[],p=[...new Set([...Yt,...i])],l=Array.isArray(o.deny)?o.deny:[],c=[...new Set([...qt,...l])],_={...o,allow:p,deny:c},E={...s,permissions:_,hooks:a};W.writeFileSync(e,JSON.stringify(E,null,2)+`
52
+ `)}var u=zt(Jt),Kt=process.env.NODE_ENV==="test"?0:3e4,Qt=500;async function ot(t){let{stdout:n}=await u("tmux",["display-message","-t",t,"-p","#{window_index}"]);return parseInt(n.trim(),10)}async function M(t){let{stdout:n}=await u("tmux",["list-windows","-t",t,"-F","#{window_index}"]),e=n.trim().split(`
53
+ `).map(Number);return Math.max(...e)}function Zt(t){return new Promise(n=>setTimeout(n,t))}async function it(t,n,e=Kt){if(e===0)return;let s=`${t}:${n}`,r=Date.now();for(;Date.now()-r<e;){try{let{stdout:a}=await u("tmux",["capture-pane","-t",s,"-p"]);if(a.split(`
54
+ `).some(i=>/^\s*[>❯]/.test(i)))return}catch{}await Zt(Qt)}}async function $(t){let n;try{({stdout:n}=await u("tmux",["list-sessions","-F","#{session_name}"]))}catch{return}let e=`${t}-v-`,s=n.trim().split(`
55
+ `).filter(r=>r.startsWith(e));for(let r of s)await u("tmux",["kill-session","-t",r]).catch(()=>{})}async function dt(){let t;try{({stdout:t}=await u("tmux",["list-sessions","-F","#{session_name}"]))}catch{return}let n=new Set(t.trim().split(`
56
+ `).filter(Boolean)),e=/^(octomux-agent-.+)-v-/;for(let s of n){let r=s.match(e);if(r){let a=r[1];n.has(a)||await u("tmux",["kill-session","-t",s]).catch(()=>{})}}}function te(t,n){let e=t.toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/-{2,}/g,"-").replace(/^-|-$/g,"").slice(0,50),s=n.slice(0,6);return`${e}-${s}`}async function X(t){let n=d(),e=t.id,s=`octomux-agent-${e}`,r=te(t.title,e),a=t.branch||`agents/${r}`,o=t.branch||r,i=R.join(t.repo_path,".worktrees",o);try{if(n.prepare("UPDATE tasks SET status = ?, tmux_session = ?, branch = ?, worktree = ?, updated_at = datetime('now') WHERE id = ?").run("setting_up",s,a,i,e),!x.existsSync(t.repo_path))throw new Error(`Repository path does not exist: ${t.repo_path}`);await u("git",["-C",t.repo_path,"rev-parse","--is-inside-work-tree"]);let p=R.join(t.repo_path,".worktrees");x.mkdirSync(p,{recursive:!0});let l=["-C",t.repo_path,"worktree","add",i,"-b",a];t.base_branch&&l.push(t.base_branch),await u("git",l);let c=R.join(t.repo_path,".claude","settings.local.json"),_=R.join(i,".claude","settings.local.json");x.existsSync(c)&&(x.mkdirSync(R.dirname(_),{recursive:!0}),x.copyFileSync(c,_)),y(i),await u("tmux",["new-session","-d","-s",s,"-c",i]),await u("tmux",["set-option","-t",s,"aggressive-resize","on"]);let E=await ot(s),f=j(12),tt=H.randomUUID();n.prepare("INSERT INTO agents (id, task_id, window_index, label, claude_session_id) VALUES (?, ?, ?, ?, ?)").run(f,e,E,"Agent 1",tt),await u("tmux",["send-keys","-t",`${s}:${E}`,`claude --session-id ${tt}`,"Enter"]),t.initial_prompt&&(await it(s,E),await mt(s,E,t.initial_prompt)),n.prepare("UPDATE tasks SET status = ?, updated_at = datetime('now') WHERE id = ?").run("running",e)}catch(p){n.prepare("UPDATE tasks SET status = ?, error = ?, updated_at = datetime('now') WHERE id = ?").run("error",p.message,e)}}async function pt(t,n){let e=d(),r=`Agent ${e.prepare("SELECT * FROM agents WHERE task_id = ? AND status != 'stopped' ORDER BY window_index").all(t.id).length+1}`;await u("tmux",["new-window","-t",t.tmux_session,"-c",t.worktree]);let a=await M(t.tmux_session),o=j(12),i=H.randomUUID();return e.prepare("INSERT INTO agents (id, task_id, window_index, label, claude_session_id) VALUES (?, ?, ?, ?, ?)").run(o,t.id,a,r,i),await u("tmux",["send-keys","-t",`${t.tmux_session}:${a}`,`claude --session-id ${i}`,"Enter"]),n&&(await it(t.tmux_session,a),await mt(t.tmux_session,a,n)),{id:o,task_id:t.id,window_index:a,label:r,status:"running",claude_session_id:i,hook_activity:"active",hook_activity_updated_at:null,created_at:new Date().toISOString()}}async function D(t){let n=d();n.prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
57
+ WHERE task_id = ? AND status = 'pending'`).run(t.id),n.prepare("UPDATE tasks SET status = 'closed', updated_at = datetime('now') WHERE id = ?").run(t.id),n.prepare("UPDATE agents SET status = 'stopped', hook_activity = 'idle', hook_activity_updated_at = datetime('now') WHERE task_id = ?").run(t.id),t.tmux_session&&(await $(t.tmux_session),await u("tmux",["kill-session","-t",t.tmux_session]).catch(()=>{}))}async function ut(t){t.tmux_session&&(await $(t.tmux_session),await u("tmux",["kill-session","-t",t.tmux_session]).catch(()=>{})),t.worktree&&await u("git",["-C",t.repo_path,"worktree","remove",t.worktree,"--force"]).catch(()=>{}),t.branch&&await u("git",["-C",t.repo_path,"branch","-D",t.branch]).catch(()=>{})}async function ct(t,n){let e=d();e.prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
58
+ WHERE agent_id = ? AND status = 'pending'`).run(n.id),await u("tmux",["kill-window","-t",`${t.tmux_session}:${n.window_index}`]).catch(()=>{}),e.prepare("UPDATE agents SET status = 'stopped', hook_activity = 'idle', hook_activity_updated_at = datetime('now') WHERE id = ?").run(n.id)}async function lt(t){if(t.user_window_index!==null&&t.user_window_index!==void 0)return t.user_window_index;let n=d();await u("tmux",["new-window","-t",t.tmux_session,"-c",t.worktree]);let e=await M(t.tmux_session);return await u("tmux",["send-keys","-t",`${t.tmux_session}:${e}`,"nvim .","Enter"]),n.prepare("UPDATE tasks SET user_window_index = ?, updated_at = datetime('now') WHERE id = ?").run(e,t.id),e}async function v(t){let n=d(),e=t.tmux_session;try{n.prepare("UPDATE tasks SET status = 'setting_up', error = NULL, user_window_index = NULL, updated_at = datetime('now') WHERE id = ?").run(t.id),await $(e),await u("tmux",["kill-session","-t",e]).catch(()=>{}),await u("tmux",["new-session","-d","-s",e,"-c",t.worktree]),await u("tmux",["set-option","-t",e,"aggressive-resize","on"]),y(t.worktree);let s=n.prepare("SELECT * FROM agents WHERE task_id = ? AND status = 'stopped' ORDER BY window_index").all(t.id);for(let r=0;r<s.length;r++){let a=s[r],o;r===0?o=await ot(e):(await u("tmux",["new-window","-t",e,"-c",t.worktree]),o=await M(e));let i;if(a.claude_session_id)i=`claude --resume ${a.claude_session_id}`;else{let p=H.randomUUID();i=`claude --continue --session-id ${p}`,n.prepare("UPDATE agents SET claude_session_id = ? WHERE id = ?").run(p,a.id)}await u("tmux",["send-keys","-t",`${e}:${o}`,i,"Enter"]),n.prepare("UPDATE agents SET window_index = ?, status = 'running' WHERE id = ?").run(o,a.id)}n.prepare("UPDATE tasks SET status = 'running', updated_at = datetime('now') WHERE id = ?").run(t.id)}catch(s){n.prepare("UPDATE tasks SET status = 'error', error = ?, updated_at = datetime('now') WHERE id = ?").run(s.message,t.id)}}async function mt(t,n,e){let s=`${t}:${n}`,r=`octomux-${j(8)}`;await new Promise((a,o)=>{let i=Gt("tmux",["load-buffer","-b",r,"-"]);i.stdin.write(e),i.stdin.end(),i.on("close",p=>{p===0?a():o(new Error(`tmux load-buffer exited with code ${p}`))})}),await u("tmux",["paste-buffer","-b",r,"-d","-t",s]),await u("tmux",["send-keys","-t",s,"Enter"])}import{execFile as ee}from"child_process";import{promisify as se}from"util";import Et from"path";import{fileURLToPath as ne}from"url";var N=se(ee),S="octomux-orchestrator",re=Et.dirname(ne(import.meta.url)),ae=Et.resolve(re,"orchestrator-prompt.md");async function q(){try{return await N("tmux",["has-session","-t",S]),!0}catch{return!1}}async function _t(t){if(await q())return;await N("tmux",["new-session","-d","-s",S,"-c",t||process.cwd()]);let n=`claude --system-prompt "$(cat ${ae})"`;await N("tmux",["send-keys","-t",S,n,"Enter"])}async function gt(){await N("tmux",["kill-session","-t",S]).catch(()=>{})}function b(){return S}import{Router as ie}from"express";import{nanoid as de}from"nanoid";import{WebSocketServer as oe,WebSocket as ft}from"ws";var h=new Set,ht;function Tt(){ht=new oe({noServer:!0})}function wt(t,n,e){return t.url!=="/ws/events"?!1:(ht.handleUpgrade(t,n,e,s=>{h.add(s),s.on("close",()=>h.delete(s)),s.on("error",()=>h.delete(s))}),!0)}function m(t){let n=JSON.stringify(t);for(let e of h)e.readyState===ft.OPEN&&e.send(n)}function kt(){for(let t of h)t.readyState===ft.OPEN&&t.close(1001,"Server shutting down");h.clear()}var T=ie();function O(t){return d().prepare(`SELECT a.id, a.task_id FROM agents a
59
+ WHERE a.claude_session_id = ? AND a.status != 'stopped'
60
+ LIMIT 1`).get(t)}T.post("/user-prompt-submit",(t,n)=>{let{session_id:e}=t.body;if(!e){n.status(200).send();return}let s=O(e);if(!s){n.status(200).send();return}d().prepare(`UPDATE agents SET hook_activity = 'active', hook_activity_updated_at = datetime('now')
61
+ WHERE id = ?`).run(s.id),m({type:"task:updated",payload:{taskId:s.task_id}}),n.status(200).send()});T.post("/permission-request",(t,n)=>{let{session_id:e,tool_name:s,tool_input:r}=t.body;if(!e||!s){n.status(200).send();return}let a=O(e);if(!a){n.status(200).send();return}d().transaction(()=>{d().prepare(`INSERT INTO permission_prompts (id, task_id, agent_id, session_id, tool_name, tool_input, status, created_at)
62
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', datetime('now'))`).run(de(12),a.task_id,a.id,e,s,JSON.stringify(r||{})),d().prepare(`UPDATE agents SET hook_activity = 'waiting', hook_activity_updated_at = datetime('now')
63
+ WHERE id = ?`).run(a.id)})(),m({type:"task:updated",payload:{taskId:a.task_id}}),n.status(200).send()});T.post("/post-tool-use",(t,n)=>{let{session_id:e}=t.body;if(!e){n.status(200).send();return}let s=O(e);if(!s){n.status(200).send();return}d().transaction(()=>{d().prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
64
+ WHERE id = (
65
+ SELECT id FROM permission_prompts
66
+ WHERE agent_id = ? AND status = 'pending'
67
+ ORDER BY created_at ASC LIMIT 1
68
+ )`).run(s.id),d().prepare(`UPDATE agents SET hook_activity = 'active', hook_activity_updated_at = datetime('now')
69
+ WHERE id = ? AND hook_activity != 'idle'`).run(s.id)})(),m({type:"task:updated",payload:{taskId:s.task_id}}),n.status(200).send()});T.post("/stop",(t,n)=>{let{session_id:e}=t.body;if(!e){n.status(200).send();return}let s=O(e);if(!s){n.status(200).send();return}d().transaction(()=>{d().prepare(`UPDATE permission_prompts SET status = 'resolved', resolved_at = datetime('now')
70
+ WHERE agent_id = ? AND status = 'pending'`).run(s.id),d().prepare(`UPDATE agents SET hook_activity = 'idle', hook_activity_updated_at = datetime('now')
71
+ WHERE id = ?`).run(s.id)})(),m({type:"task:updated",payload:{taskId:s.task_id}}),n.status(200).send()});var V=le(ce);function yt(t){try{return JSON.parse(t||"{}")}catch{return{}}}function Rt(t){if(t.status!=="running")return null;let n=t.agents.filter(e=>e.status!=="stopped").map(e=>e.hook_activity);return n.length===0?"done":n.includes("active")?"working":n.includes("waiting")?"needs_attention":"done"}function xt(t){t.use("/api/hooks",T),t.get("/api/browse",(n,e)=>{let s=n.query.path||ue.homedir();try{if(!w.statSync(s).isDirectory()){e.status(400).json({error:"Path is not a directory"});return}}catch{e.status(400).json({error:"Path does not exist"});return}let r=[];for(let o of w.readdirSync(s)){let i=Y.join(s,o);try{if(!w.statSync(i).isDirectory())continue;let l=w.existsSync(Y.join(i,".git"));r.push({name:o,path:i,isGit:l})}catch{}}r.sort((o,i)=>{if(o.isGit!==i.isGit)return o.isGit?-1:1;let p=o.name.startsWith("."),l=i.name.startsWith(".");return p!==l?p?1:-1:o.name.localeCompare(i.name)});let a=Y.dirname(s);e.json({current:s,parent:a!==s?a:null,entries:r})}),t.get("/api/branches",async(n,e)=>{let s=n.query.repo_path;if(!s){e.status(400).json({error:"repo_path is required"});return}try{let{stdout:r}=await V("git",["-C",s,"branch","-a","--format=%(refname:short)"]),a=r.trim().split(`
72
+ `).filter(Boolean).map(i=>i.replace(/^origin\//,"")),o=[...new Set(a)].filter(i=>i!=="HEAD");e.json(o)}catch{e.status(400).json({error:"Failed to list branches"})}}),t.get("/api/default-branch",async(n,e)=>{let s=n.query.repo_path;if(!s){e.status(400).json({error:"repo_path is required"});return}try{let{stdout:r}=await V("git",["-C",s,"symbolic-ref","refs/remotes/origin/HEAD"]),a=r.trim().replace("refs/remotes/origin/","");e.json({branch:a})}catch{e.json({branch:"main"})}}),t.get("/api/recent-repos",(n,e)=>{let r=d().prepare("SELECT repo_path, MAX(created_at) as last_used FROM tasks GROUP BY repo_path ORDER BY last_used DESC LIMIT 10").all();e.json(r)}),t.get("/api/tasks",(n,e)=>{let s=d(),r=n.query.repo_path,a;r?a=s.prepare("SELECT * FROM tasks WHERE repo_path = ? ORDER BY created_at DESC").all(r):a=s.prepare("SELECT * FROM tasks ORDER BY created_at DESC").all();let o=s.prepare("SELECT * FROM agents WHERE task_id = ? ORDER BY window_index"),i=s.prepare(`SELECT pp.*, a.label as agent_label
73
+ FROM permission_prompts pp
74
+ LEFT JOIN agents a ON pp.agent_id = a.id
75
+ WHERE pp.task_id = ? AND pp.status = 'pending'
76
+ ORDER BY pp.created_at ASC`),p=a.map(l=>{let c=o.all(l.id),E=i.all(l.id).map(f=>({...f,tool_input:yt(f.tool_input)}));return{...l,agents:c,pending_prompts:E,derived_status:Rt({status:l.status,agents:c})}});e.json(p)}),t.get("/api/tasks/:id",(n,e)=>{let s=d(),r=s.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}let a=s.prepare("SELECT * FROM agents WHERE task_id = ? ORDER BY window_index").all(r.id),i=s.prepare(`SELECT pp.*, a.label as agent_label
77
+ FROM permission_prompts pp
78
+ LEFT JOIN agents a ON pp.agent_id = a.id
79
+ WHERE pp.task_id = ? AND pp.status = 'pending'
80
+ ORDER BY pp.created_at ASC`).all(r.id).map(p=>({...p,tool_input:yt(p.tool_input)}));e.json({...r,agents:a,pending_prompts:i,derived_status:Rt({status:r.status,agents:a})})}),t.post("/api/tasks",async(n,e)=>{let s=n.body;if(!s.title||!s.description||!s.repo_path){e.status(400).json({error:"title, description, and repo_path are required"});return}let r=d(),a=pe(12);if(r.prepare("INSERT INTO tasks (id, title, description, repo_path, status, branch, base_branch, initial_prompt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(a,s.title,s.description,s.repo_path,"draft",s.branch??null,s.base_branch??null,s.initial_prompt??null),!s.draft){let i=r.prepare("SELECT * FROM tasks WHERE id = ?").get(a);await X(i)}let o=r.prepare("SELECT * FROM tasks WHERE id = ?").get(a);o.agents=r.prepare("SELECT * FROM agents WHERE task_id = ? ORDER BY window_index").all(a),m({type:"task:created",payload:{taskId:a}}),e.status(201).json(o)}),t.patch("/api/tasks/:id",async(n,e)=>{let s=n.body,r=d(),a=r.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!a){e.status(404).json({error:"Task not found"});return}if(["title","description","repo_path","branch","base_branch","initial_prompt"].some(p=>s[p]!==void 0)){if(a.status!=="draft"){e.status(400).json({error:"Can only edit fields on draft tasks"});return}if(s.title!==void 0&&!s.title.trim()){e.status(400).json({error:"title is required"});return}if(s.description!==void 0&&!s.description.trim()){e.status(400).json({error:"description is required"});return}if(s.repo_path!==void 0){if(!s.repo_path.trim()){e.status(400).json({error:"repo_path is required"});return}if(!w.existsSync(s.repo_path)){e.status(400).json({error:"repo_path does not exist"});return}}let p=[],l=[];for(let c of["title","description","repo_path","branch","base_branch","initial_prompt"])s[c]!==void 0&&(p.push(`${c} = ?`),l.push(s[c]??null));p.length>0&&(p.push("updated_at = datetime('now')"),l.push(a.id),r.prepare(`UPDATE tasks SET ${p.join(", ")} WHERE id = ?`).run(...l))}else if(s.status==="running"){if(a.status!=="closed"&&a.status!=="error"){e.status(400).json({error:"Can only resume tasks in closed or error state"});return}if(!a.worktree||!w.existsSync(a.worktree)){e.status(400).json({error:"Worktree no longer exists on disk"});return}await v(a)}else s.status==="closed"&&await D(a);let i=r.prepare("SELECT * FROM tasks WHERE id = ?").get(a.id);i.agents=r.prepare("SELECT * FROM agents WHERE task_id = ? ORDER BY window_index").all(a.id),m({type:"task:updated",payload:{taskId:a.id}}),e.json(i)}),t.post("/api/tasks/:id/start",async(n,e)=>{let s=d(),r=s.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}if(r.status!=="draft"){e.status(400).json({error:"Only draft tasks can be started"});return}await X(r);let a=s.prepare("SELECT * FROM tasks WHERE id = ?").get(r.id);a.agents=s.prepare("SELECT * FROM agents WHERE task_id = ? ORDER BY window_index").all(r.id),m({type:"task:updated",payload:{taskId:r.id}}),e.json(a)}),t.delete("/api/tasks/:id",async(n,e)=>{let s=d(),r=s.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}let a=r.id;await ut(r),s.prepare("DELETE FROM agents WHERE task_id = ?").run(a),s.prepare("DELETE FROM tasks WHERE id = ?").run(a),m({type:"task:deleted",payload:{taskId:a}}),e.status(204).send()}),t.post("/api/tasks/:id/agents",async(n,e)=>{let r=d().prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}if(r.status!=="running"){e.status(400).json({error:"Can only add agents to running tasks"});return}let a=n.body,o=await pt(r,a.prompt);m({type:"task:updated",payload:{taskId:r.id}}),e.status(201).json(o)}),t.delete("/api/tasks/:id/agents/:agentId",async(n,e)=>{let s=d(),r=s.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id),a=s.prepare("SELECT * FROM agents WHERE id = ? AND task_id = ?").get(n.params.agentId,n.params.id);if(!r||!a){e.status(404).json({error:"Task or agent not found"});return}await ct(r,a),m({type:"task:updated",payload:{taskId:r.id}}),e.json({success:!0})}),t.post("/api/tasks/:id/user-terminal",async(n,e)=>{let r=d().prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}if(r.status!=="running"){e.status(400).json({error:"Can only create user terminal for running tasks"});return}if(!r.tmux_session){e.status(400).json({error:"Task has no tmux session"});return}try{let a=await lt(r);m({type:"task:updated",payload:{taskId:r.id}}),e.json({user_window_index:a})}catch(a){e.status(500).json({error:a.message})}}),t.post("/api/tasks/:id/agents/:agentId/message",async(n,e)=>{let s=d(),r=s.prepare("SELECT * FROM tasks WHERE id = ?").get(n.params.id);if(!r){e.status(404).json({error:"Task not found"});return}if(r.status!=="running"){e.status(400).json({error:"Task is not running"});return}let a=s.prepare("SELECT * FROM agents WHERE id = ? AND task_id = ?").get(n.params.agentId,n.params.id);if(!a){e.status(404).json({error:"Agent not found"});return}let{message:o}=n.body;if(!o){e.status(400).json({error:"message is required"});return}await V("tmux",["send-keys","-t",`${r.tmux_session}:${a.window_index}`,o,"Enter"]),e.json({success:!0})}),t.get("/api/orchestrator/status",async(n,e)=>{let s=await q();e.json({running:s,session:b()})}),t.post("/api/orchestrator/start",async(n,e)=>{let s=n.body?.cwd;await _t(s),e.json({running:!0,session:b()})}),t.post("/api/orchestrator/stop",async(n,e)=>{await gt(),e.json({running:!1})})}function bt(){let t=St();return t.use(St.json({limit:"1mb"})),xt(t),t.use((n,e,s,r)=>{console.error("Unhandled error:",n.message,n.stack),s.status(500).json({error:n.message})}),t}import{execFile as me}from"child_process";import{promisify as Ee}from"util";import{WebSocketServer as _e,WebSocket as J}from"ws";import{spawn as ge}from"node-pty";import{nanoid as fe}from"nanoid";var L=Ee(me),g=new Map,G;function At(){G=new _e({noServer:!0})}function Dt(t,n,e){if(t.url?.match(/^\/ws\/terminal\/orchestrator$/))return G.handleUpgrade(t,n,e,a=>{Te(a)}),!0;let r=t.url?.match(/^\/ws\/terminal\/([^/]+)\/(\d+)$/);return r?(G.handleUpgrade(t,n,e,a=>{let o=r[1],i=parseInt(r[2],10);he(a,o,i)}),!0):!1}function vt(t,n,e,s,r,a){let o;try{o=ge("tmux",["attach-session","-t",n],{name:"xterm-256color",cols:120,rows:30,env:process.env})}catch{t.close(4005,s);return}let i=!1;g.has(e)||g.set(e,[]),g.get(e).push({ws:t,pty:o}),o.onData(c=>{t.readyState===J.OPEN&&t.send(c)});let p=c=>{if(i)return;let _=typeof c=="string"?c:c.toString();try{let E=JSON.parse(_);if(E.type==="resize"&&E.cols&&E.rows){o.resize(E.cols,E.rows);return}}catch{}try{o.write(_)}catch{}};if(t.removeAllListeners("message"),t.on("message",p),a)for(let c of a)p(c);let l=()=>{r&&L("tmux",["kill-session","-t",r]).catch(()=>{})};t.on("close",()=>{i||o.kill(),l();let c=g.get(e);if(c){let _=c.findIndex(E=>E.ws===t);_>=0&&c.splice(_,1),c.length===0&&g.delete(e)}}),o.onExit(()=>{i=!0,l(),t.readyState===J.OPEN&&t.close(4006,"Terminal process exited")})}async function he(t,n,e){let s=[];t.on("message",p=>{s.push(p)});let a=d().prepare("SELECT * FROM tasks WHERE id = ?").get(n);if(!a||!a.tmux_session){t.close(4004,"Task not found or no tmux session");return}let o=`${a.tmux_session}-v-${fe(6)}`;try{await L("tmux",["new-session","-d","-t",a.tmux_session,"-s",o]),await L("tmux",["set-option","-t",o,"aggressive-resize","on"]).catch(()=>{}),await L("tmux",["select-window","-t",`${o}:${e}`])}catch{t.close(4005,"Failed to create terminal view session");return}let i=`${n}:${e}`;vt(t,o,i,"Failed to attach to tmux session",o,s)}function Te(t){let n=b();vt(t,n,"orchestrator","Failed to attach to orchestrator session")}function Nt(){for(let[,t]of g)for(let{ws:n,pty:e}of t){try{e.kill()}catch{}n.readyState===J.OPEN&&n.close(1001,"Server shutting down")}g.clear()}import{execFile as we}from"child_process";import{promisify as ke}from"util";var z=ke(we),Ot=process.env.NODE_ENV==="test"?0:5e3,Lt=process.env.NODE_ENV==="test"?0:3e4,It=process.env.NODE_ENV==="test"?0:3e4,I=null,P=null,C=null;async function K(t){if(!t.tmux_session)return"dead";try{return await z("tmux",["has-session","-t",t.tmux_session]),"alive"}catch{return"dead"}}async function ye(){let t=d(),n=t.prepare("SELECT * FROM tasks WHERE status IN ('running', 'setting_up')").all(),e=await Promise.allSettled(n.map(async s=>{let r=await K(s);return{task:s,status:r}}));for(let s of e){if(s.status!=="fulfilled")continue;let{task:r,status:a}=s.value;a==="dead"&&r.status==="running"?(t.prepare("UPDATE tasks SET status = 'closed', updated_at = datetime('now') WHERE id = ?").run(r.id),t.prepare("UPDATE agents SET status = 'stopped', hook_activity = 'idle', hook_activity_updated_at = datetime('now') WHERE task_id = ? AND status = 'running'").run(r.id),m({type:"task:updated",payload:{taskId:r.id}})):a==="dead"&&r.status==="setting_up"&&(t.prepare("UPDATE tasks SET status = 'error', error = 'Setup interrupted', updated_at = datetime('now') WHERE id = ?").run(r.id),t.prepare("UPDATE agents SET status = 'stopped', hook_activity = 'idle', hook_activity_updated_at = datetime('now') WHERE task_id = ? AND status = 'running'").run(r.id),m({type:"task:updated",payload:{taskId:r.id}}))}}function Re(){let n=d().prepare("SELECT * FROM tasks WHERE status IN ('running', 'setting_up') AND worktree IS NOT NULL").all();for(let e of n)try{y(e.worktree)}catch{}}async function xe(t){if(!t.branch||!t.repo_path)return null;try{let{stdout:n}=await z("gh",["pr","list","--head",t.branch,"--json","url,number","--limit","1"],{cwd:t.repo_path}),e=JSON.parse(n.trim()||"[]");return e.length>0?{url:e[0].url,number:e[0].number}:null}catch{return null}}async function Se(){let t=d(),n=t.prepare("SELECT * FROM tasks WHERE status IN ('running', 'closed') AND pr_url IS NULL AND branch IS NOT NULL").all(),e=await Promise.allSettled(n.map(async s=>{let r=await xe(s);return{task:s,pr:r}}));for(let s of e){if(s.status!=="fulfilled")continue;let{task:r,pr:a}=s.value;a&&(t.prepare("UPDATE tasks SET pr_url = ?, pr_number = ?, updated_at = datetime('now') WHERE id = ?").run(a.url,a.number,r.id),m({type:"task:updated",payload:{taskId:r.id}}))}}async function be(){let n=d().prepare("SELECT * FROM tasks WHERE status = 'running' AND pr_number IS NOT NULL").all(),e=await Promise.allSettled(n.map(async s=>{let{stdout:r}=await z("gh",["pr","view",String(s.pr_number),"--json","state"],{cwd:s.repo_path}),{state:a}=JSON.parse(r.trim());return{task:s,state:a}}));for(let s of e){if(s.status!=="fulfilled")continue;let{task:r,state:a}=s.value;if(a==="MERGED")try{await D(r),m({type:"task:updated",payload:{taskId:r.id}})}catch{}}}async function Ae(){try{await be()}catch(t){console.error("pollMergedPRs error:",t)}}function Pt(){Re(),Ot>0&&(I=setInterval(ye,Ot)),Lt>0&&(P=setInterval(Se,Lt)),It>0&&(C=setInterval(Ae,It))}function Ct(){I&&(clearInterval(I),I=null),P&&(clearInterval(P),P=null),C&&(clearInterval(C),C=null)}var Le=Ut.dirname(Oe(import.meta.url)),Q=bt(),U=De(Q),Z=process.env.PORT||7777;At();Tt();U.on("upgrade",(t,n,e)=>{Dt(t,n,e)||wt(t,n,e)||n.destroy()});async function Ie(){let t=d(),n=t.prepare("SELECT * FROM tasks WHERE status IN ('running', 'setting_up')").all();for(let e of n)await K(e)!=="alive"&&(e.worktree&&ve.existsSync(e.worktree)?(console.warn(`[recovery] Resuming task ${e.id}: ${e.title}`),v(e)):e.status==="setting_up"?(console.warn(`[recovery] Setup interrupted for task ${e.id}: ${e.title}`),t.prepare("UPDATE tasks SET status = 'error', error = 'Setup interrupted', updated_at = datetime('now') WHERE id = ?").run(e.id)):(console.warn(`[recovery] Worktree missing for task ${e.id}: ${e.title}`),t.prepare("UPDATE tasks SET status = 'error', error = 'Worktree missing after restart', updated_at = datetime('now') WHERE id = ?").run(e.id)))}await Ie();await dt();Pt();if(process.env.NODE_ENV==="production"){let t=Ut.join(Le,"..","dist");Q.use(Ne.static(t)),Q.get("/{*path}",(n,e)=>{e.sendFile("index.html",{root:t})})}U.on("error",t=>{throw t.code==="EADDRINUSE"&&(console.error(`
81
+ Port ${Z} is in use. Try: octomux start --port 8080`),process.exit(1)),t});U.listen(Z,()=>{console.warn(`octomux running at http://localhost:${Z}`)});function Bt(){console.warn("[shutdown] Stopping pollers..."),Ct(),console.warn("[shutdown] Cleaning up connections..."),Nt(),kt(),console.warn("[shutdown] Closing HTTP server..."),U.close(()=>{console.warn("[shutdown] Done."),process.exit(0)}),setTimeout(()=>process.exit(1),5e3).unref()}process.on("SIGTERM",Bt);process.on("SIGINT",Bt);export{Q as app,U as server};
@@ -0,0 +1,4 @@
1
+ export declare function isOrchestratorRunning(): Promise<boolean>;
2
+ export declare function startOrchestrator(cwd?: string): Promise<void>;
3
+ export declare function stopOrchestrator(): Promise<void>;
4
+ export declare function getOrchestratorSession(): string;
@@ -0,0 +1,37 @@
1
+ import { execFile as execFileCb } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const execFile = promisify(execFileCb);
6
+ const ORCHESTRATOR_SESSION = 'octomux-orchestrator';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const PROMPT_FILE = path.resolve(__dirname, 'orchestrator-prompt.md');
9
+ export async function isOrchestratorRunning() {
10
+ try {
11
+ await execFile('tmux', ['has-session', '-t', ORCHESTRATOR_SESSION]);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export async function startOrchestrator(cwd) {
19
+ if (await isOrchestratorRunning())
20
+ return;
21
+ await execFile('tmux', [
22
+ 'new-session',
23
+ '-d',
24
+ '-s',
25
+ ORCHESTRATOR_SESSION,
26
+ '-c',
27
+ cwd || process.cwd(),
28
+ ]);
29
+ const claudeCmd = `claude --system-prompt "$(cat ${PROMPT_FILE})"`;
30
+ await execFile('tmux', ['send-keys', '-t', ORCHESTRATOR_SESSION, claudeCmd, 'Enter']);
31
+ }
32
+ export async function stopOrchestrator() {
33
+ await execFile('tmux', ['kill-session', '-t', ORCHESTRATOR_SESSION]).catch(() => { });
34
+ }
35
+ export function getOrchestratorSession() {
36
+ return ORCHESTRATOR_SESSION;
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { Task } from './types.js';
2
+ export declare function checkTaskStatus(task: Task): Promise<'alive' | 'dead'>;
3
+ export declare function pollStatuses(): Promise<void>;
4
+ /**
5
+ * Ensure hooks are installed in all running task worktrees.
6
+ * Handles tasks created before the hook feature existed.
7
+ */
8
+ export declare function ensureHooksInstalled(): void;
9
+ export declare function detectPR(task: Task): Promise<{
10
+ url: string;
11
+ number: number;
12
+ } | null>;
13
+ export declare function pollPRs(): Promise<void>;
14
+ export declare function checkMergedPRs(): Promise<void>;
15
+ export declare function pollMergedPRs(): Promise<void>;
16
+ export declare function startPolling(): void;
17
+ export declare function stopPolling(): void;