octomux 1.0.34 → 1.1.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 (108) hide show
  1. package/README.md +29 -17
  2. package/bin/octomux.js +20 -0
  3. package/cli/dist/client.js +50 -57
  4. package/cli/dist/commands/close-task.js +1 -1
  5. package/cli/dist/commands/create-task.js +9 -10
  6. package/cli/dist/commands/files.js +67 -0
  7. package/cli/dist/commands/get-task.js +2 -2
  8. package/cli/dist/commands/list-tasks.js +3 -3
  9. package/cli/dist/commands/resume-task.js +3 -3
  10. package/cli/dist/commands/task-move.js +4 -11
  11. package/cli/dist/commands/task-ref-add.js +4 -5
  12. package/cli/dist/commands/task-updates.js +4 -15
  13. package/cli/dist/format.js +11 -0
  14. package/cli/dist/index.js +2 -0
  15. package/dist/assets/{AgentEditor-bmu3IToQ.js → AgentEditor-DO3dt4ri.js} +1 -1
  16. package/dist/assets/{ChatPage-DoNLt9yc.js → ChatPage-Be7NRaz5.js} +2 -2
  17. package/dist/assets/IntegrationsPage-o2qrU4Ly.js +1 -0
  18. package/dist/assets/OrchestratorPage-B6-5Ymz_.js +29 -0
  19. package/dist/assets/ReviewDetailPage-DvnR-g2n.js +1 -0
  20. package/dist/assets/{SetupPage-CJYmbm6K.js → SetupPage-CmzzPZne.js} +1 -1
  21. package/dist/assets/{SkillEditor-BXV-a19B.js → SkillEditor-BYTCGSV_.js} +1 -1
  22. package/dist/assets/{WorkspaceDetailPage-Cns6VZKt.js → WorkspaceDetailPage-ZCgJpIyZ.js} +1 -1
  23. package/dist/assets/{WorkspacesPage-DI_Ilx7o.js → WorkspacesPage-8IwI9uiq.js} +1 -1
  24. package/dist/assets/index-CQh8m5qn.css +1 -0
  25. package/dist/assets/index-D4uuZdW5.js +34 -0
  26. package/dist/assets/{vendor-monaco-CdNyTACs.js → vendor-monaco-BpPnCqoL.js} +1 -1
  27. package/dist/assets/{vendor-react-Dd26lTa7.js → vendor-react-BQxe05fg.js} +1 -1
  28. package/dist/assets/{vendor-router-yJbofmar.js → vendor-router-BxmO8ihp.js} +1 -1
  29. package/dist/assets/{vendor-ui-ChI1tM4e.js → vendor-ui-48-ysyeR.js} +1 -1
  30. package/dist/index.html +6 -6
  31. package/dist-server/agent-session/mcp/submit-result-server.js +8 -0
  32. package/dist-server/agents-45L5TJD3.js +1 -0
  33. package/dist-server/approval-timeout-NYVPHUHG.js +1 -0
  34. package/dist-server/{chunk-BTEWK6EA.js → chunk-3CVFZVVG.js} +108 -18
  35. package/dist-server/chunk-B36SOK6W.js +1 -0
  36. package/dist-server/chunk-CNU4N4AY.js +1 -0
  37. package/dist-server/chunk-COWASN3D.js +1 -0
  38. package/dist-server/chunk-CZT76WRN.js +1 -0
  39. package/dist-server/chunk-FQ324ITT.js +368 -0
  40. package/dist-server/chunk-JACWMK5E.js +4 -0
  41. package/dist-server/chunk-NLRGIOB5.js +2 -0
  42. package/dist-server/chunk-OIRUBP4J.js +1 -0
  43. package/dist-server/chunk-PC2DITVE.js +1 -0
  44. package/dist-server/chunk-R466QRBH.js +2 -0
  45. package/dist-server/chunk-S67AOMGA.js +14 -0
  46. package/dist-server/chunk-SFMUA6HF.js +166 -0
  47. package/dist-server/chunk-TKX326LX.js +13 -0
  48. package/dist-server/chunk-U55T3B3U.js +4 -0
  49. package/dist-server/chunk-UWUVS7F4.js +2 -0
  50. package/dist-server/chunk-WLB7K3JL.js +1 -0
  51. package/dist-server/{chunk-4GEKZIZ6.js → chunk-XQ7EQ3DU.js} +1 -1
  52. package/dist-server/{chunk-OSXDDM27.js → chunk-YZSABAAX.js} +1 -1
  53. package/dist-server/claude-code-M4NFPKSN.js +1 -0
  54. package/dist-server/cli-review.js +94 -0
  55. package/dist-server/harnesses-2PWYFS77.js +1 -0
  56. package/dist-server/hook-settings-KCX3NSNL.js +1 -0
  57. package/dist-server/hooks-install-KBBZ2HEZ.js +1 -0
  58. package/dist-server/index.js +26 -279
  59. package/dist-server/orchestrator/mcp/server.js +2 -0
  60. package/dist-server/prefill-QQYRVBGG.js +1 -0
  61. package/dist-server/preflight-TC52EHUN.js +2 -0
  62. package/dist-server/publish-review-QWKNLWKW.js +5 -0
  63. package/dist-server/registry-Q5TISXTP.js +1 -0
  64. package/dist-server/resolve-env-YZ3RWEBZ.js +1 -0
  65. package/dist-server/settings-YFZ7BOZK.js +1 -0
  66. package/dist-server/setup-status-54WC6S2N.js +1 -0
  67. package/dist-server/startup.js +1 -1
  68. package/dist-server/store-PRUWQM53.js +1 -0
  69. package/dist-server/teams-GIVRUA2J.js +6 -0
  70. package/package.json +28 -6
  71. package/scripts/record-fixtures.ts +57 -0
  72. package/scripts/spike/transcript-tail.ts +241 -0
  73. package/skills/add-agent/SKILL.md +44 -8
  74. package/skills/create-task/SKILL.md +23 -5
  75. package/skills/review-deep/SKILL.md +184 -0
  76. package/skills/review-walkthrough/SKILL.md +107 -0
  77. package/skills/send-agent-message/SKILL.md +56 -16
  78. package/skills/update-task-status/SKILL.md +38 -7
  79. package/dist/assets/IntegrationsPage-D3iTxzhN.js +0 -1
  80. package/dist/assets/ReviewDetailPage-DxCK8Zq4.js +0 -1
  81. package/dist/assets/index-D2mLE9DX.css +0 -1
  82. package/dist/assets/index-DKTtIYvz.js +0 -35
  83. package/dist-server/agents-75BBHCTY.js +0 -1
  84. package/dist-server/chunk-3PYPZRQV.js +0 -1
  85. package/dist-server/chunk-4DGDU2NU.js +0 -49
  86. package/dist-server/chunk-BYZCD2AS.js +0 -16
  87. package/dist-server/chunk-GAXJZSQT.js +0 -2
  88. package/dist-server/chunk-MCI4HOI7.js +0 -2
  89. package/dist-server/chunk-MYIYBV6O.js +0 -16
  90. package/dist-server/chunk-NHDUWYXX.js +0 -2
  91. package/dist-server/chunk-NUVE5API.js +0 -23
  92. package/dist-server/chunk-RFWGJ3HT.js +0 -1
  93. package/dist-server/chunk-YKFMOBXG.js +0 -2
  94. package/dist-server/chunk-ZV3CXK7O.js +0 -1
  95. package/dist-server/claude-code-SUTWME7T.js +0 -1
  96. package/dist-server/harnesses-ACMXKLJO.js +0 -1
  97. package/dist-server/hook-settings-VQOP567H.js +0 -1
  98. package/dist-server/hooks-install-5WIG6NCP.js +0 -1
  99. package/dist-server/prefill-IKIJSGHJ.js +0 -14
  100. package/dist-server/preflight-W4357G4A.js +0 -13
  101. package/dist-server/publish-review-3X766STN.js +0 -7
  102. package/dist-server/registry-QDC56XWP.js +0 -1
  103. package/dist-server/resolve-env-GWFARUZQ.js +0 -1
  104. package/dist-server/settings-VDSTIV6M.js +0 -1
  105. package/dist-server/setup-status-TDHBDU7E.js +0 -1
  106. package/dist-server/store-7ATWFPHP.js +0 -1
  107. package/dist-server/teams-DOJN7H42.js +0 -22
  108. package/skills/review-orchestrator/SKILL.md +0 -205
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **Coding got faster. Managing agents didn't.**
8
8
 
9
- A local web app to **dispatch, watch, and review** parallel **Claude Code** and **Cursor** agents from one place. Kanban for fleet status. One inbox for every "allow this tool?" prompt. In-app diff review with **Ship**. No cloud. MIT.
9
+ A local web app to **dispatch, watch, and review** parallel **Claude Code** and **Cursor** agents from one place. Kanban for fleet status. One inbox for every "allow this tool?" prompt. In-app diff review with **Ship**. Run it from npm or the native macOS app. No cloud. MIT.
10
10
 
11
11
  ```bash
12
12
  npm install -g octomux && octomux init && cd your-repo && octomux start
@@ -20,7 +20,7 @@ Open [http://localhost:7777](http://localhost:7777) — describe a task in the c
20
20
 
21
21
  Three phases, one window:
22
22
 
23
- - **01 — Dispatch.** Type a task. Pick Claude Code or Cursor. Hit go. The composer takes plain English, Jira links, or GitHub issue URLs. Drop a second agent on the same branch with one click — or paste a whole list and **bulk-create** a worktree, branch, and agent for each.
23
+ - **01 — Dispatch.** Type a task. Pick Claude Code or Cursor. Hit go. The composer takes plain English, Jira or Linear links, or GitHub issue URLs. Drop a second agent on the same branch with one click — or paste a whole list and **bulk-create** a worktree, branch, and agent for each.
24
24
  - **02 — Watch.** See every agent work, live. Each task streams its own view — files the agent is editing, the diff as it grows, terminal output as it runs. The **Monitor** view tiles every running agent into one grid so you can scan the whole fleet at a glance. When an agent needs permission, the prompt lands in your inbox so you don't have to babysit every pane.
25
25
  - **03 — Review & Ship.** Diff review in the same window. File tree, per-file reviewed state, inline comments. Hit **Ship** and the PR auto-links to the task — closes itself when the PR merges.
26
26
 
@@ -49,7 +49,9 @@ Code never leaves your laptop. No telemetry, no cloud sync. Crash, reboot, close
49
49
  - **Agents that dispatch agents** — `/create-task`, `/list-tasks`, `/send-agent-message` skills work inside any Claude Code window; recursive dispatch from inside an agent.
50
50
  - **Agent teams** — reusable crews defined as code in `<repo>/.octomux/team.yaml`: a lead spawns workers from a roster, each with its own model. Run on demand (`octomux team run`) or on a cron schedule.
51
51
  - **Per-task model** — pin any task or added agent to a specific model (`--model claude-opus-4-8`); mix models across a fleet so the right one lands on each job.
52
- - **Integrations** — Jira wiring plus orchestrator skills for GitHub / auto-review intake.
52
+ - **Completion notifications** — link a worker to an orchestrator with `add-agent --notify-agent`; the lead gets pinged when each worker finishes. Teams wire this up automatically.
53
+ - **Mobile-ready dashboard** — bottom nav, responsive pages, and mobile-friendly terminal scroll buttons; check the fleet and answer prompts from your phone.
54
+ - **Integrations** — Jira and Linear sync (status push + comment-back, composer prefill) plus orchestrator skills for GitHub / auto-review intake.
53
55
  - **CLI ↔ dashboard parity** — `octomux create-task`, `send-message`, `resume-task` — same tasks the UI shows.
54
56
  - **Reboot-proof** — WAL SQLite + preserved worktrees across restarts.
55
57
  - **Local-only** — no telemetry, no cloud sync, no analytics. Your `.env` stays on the host.
@@ -77,7 +79,7 @@ Give an agent the orchestrator skills. It plans the work, breaks down the spec,
77
79
  ## Quick start
78
80
 
79
81
  ```bash
80
- brew install tmux git
82
+ brew install git
81
83
  npm install -g @anthropic-ai/claude-code # and/or Cursor CLI
82
84
  npm install -g octomux
83
85
  octomux init
@@ -85,11 +87,20 @@ cd your-project
85
87
  octomux start
86
88
  ```
87
89
 
90
+ `tmux` ships bundled — octomux installs a static `tmux` for your platform (macOS and Linux,
91
+ arm64/x64) and uses it automatically, so there's no separate `brew install tmux` step.
92
+
88
93
  ```bash
89
94
  octomux create-task -t "Add OAuth login" -r .
90
95
  octomux create-task -t "Spike with Cursor" -r . --harness cursor
91
96
  ```
92
97
 
98
+ **Prefer a desktop app?** macOS users can download the `.dmg` from
99
+ [GitHub Releases](https://github.com/ShreyPaharia/octomux/releases) instead of the npm CLI.
100
+ It bundles its own `tmux` and runs against an isolated data directory (its own DB, tmux
101
+ socket, and logs), so it never collides with a CLI install on the same machine. The build is
102
+ ad-hoc signed (not notarized) — if macOS warns on first launch, right-click the app → **Open**.
103
+
93
104
  Step-by-step setup, Jira, and orchestrator skills: [ONBOARDING.md](./ONBOARDING.md)
94
105
 
95
106
  ## How it works
@@ -110,17 +121,17 @@ DISPATCH → BRANCH → CODE → INBOX → REVIEW → MERGE
110
121
 
111
122
  ## CLI
112
123
 
113
- | Command | Description |
114
- | ---------------------------------------- | -------------------------------------------------------- |
115
- | `octomux start` | Dashboard at `:7777` |
116
- | `octomux init` | Defaults wizard (Jira, base branch, harness prefs) |
117
- | `octomux create-task` | New task (`--harness cursor` optional) |
118
- | `octomux list-tasks` / `get-task` | Inspect tasks |
119
- | `octomux close-task` / `delete-task` | Stop or fully remove |
120
- | `octomux resume-task` | Resume a closed task |
121
- | `octomux add-agent` | Another agent window (`--skeleton`, `--model` optional) |
122
- | `octomux send-message` | Message a running agent — course-correct without restart |
123
- | `octomux team run` / `schedule` / `list` | Run or schedule an agent crew from `.octomux/team.yaml` |
124
+ | Command | Description |
125
+ | ---------------------------------------- | ------------------------------------------------------------------------- |
126
+ | `octomux start` | Dashboard at `:7777` |
127
+ | `octomux init` | Defaults wizard (Jira, base branch, harness prefs) |
128
+ | `octomux create-task` | New task (`--harness cursor` optional) |
129
+ | `octomux list-tasks` / `get-task` | Inspect tasks |
130
+ | `octomux close-task` / `delete-task` | Stop or fully remove |
131
+ | `octomux resume-task` | Resume a closed task |
132
+ | `octomux add-agent` | Another agent window (`--skeleton`, `--model`, `--notify-agent` optional) |
133
+ | `octomux send-message` | Message a running agent — course-correct without restart |
134
+ | `octomux team run` / `schedule` / `list` | Run or schedule an agent crew from `.octomux/team.yaml` |
124
135
 
125
136
  ## Architecture
126
137
 
@@ -156,11 +167,12 @@ flowchart LR
156
167
  ## Requirements
157
168
 
158
169
  - macOS (ARM64 or x64), Node.js 20+ (24 LTS recommended)
159
- - `tmux`, `git`
170
+ - `git` (`tmux` ships bundled — installed automatically, no manual step)
160
171
  - At least one harness: **Claude Code** (`claude`) and/or **Cursor CLI** (`cursor-agent`)
161
172
  - Recommended: `lazygit`, `neovim`
173
+ - macOS desktop app (`.dmg`) available on [GitHub Releases](https://github.com/ShreyPaharia/octomux/releases) as an alternative to the npm CLI
162
174
 
163
- > First run flags only the deps you're actually missing — install what the setup banner asks for and you're good. Jira and other integrations are configured later from the in-app **Integrations** page.
175
+ > First run flags only the deps you're actually missing — install what the setup banner asks for and you're good. Jira, Linear, and other integrations are configured later from the in-app **Integrations** page.
164
176
 
165
177
  ## Configuration
166
178
 
package/bin/octomux.js CHANGED
@@ -69,6 +69,7 @@ async function runStart(startArgs) {
69
69
 
70
70
  // Install bundled skills and configs
71
71
  installSkills();
72
+ installWorkflows();
72
73
  installLazygitConfig();
73
74
 
74
75
  // Ensure cli/ dependencies are installed
@@ -268,3 +269,22 @@ function installSkills() {
268
269
  console.log('Installed octomux skills for Claude Code');
269
270
  }
270
271
  }
272
+
273
+ function installWorkflows() {
274
+ const src = path.join(__dirname, '..', 'workflows');
275
+ const target = path.join(os.homedir(), '.claude', 'workflows');
276
+ if (!existsSync(src)) return;
277
+
278
+ mkdirSync(target, { recursive: true });
279
+ let installed = false;
280
+ for (const file of readdirSync(src)) {
281
+ if (!file.endsWith('.js')) continue;
282
+ // Overwrite so workflow updates actually ship (unlike skills, which only
283
+ // install when missing). The review-deep skill invokes these by scriptPath.
284
+ cpSync(path.join(src, file), path.join(target, file));
285
+ installed = true;
286
+ }
287
+ if (installed) {
288
+ console.log('Installed octomux review workflows for Claude Code');
289
+ }
290
+ }
@@ -1,154 +1,147 @@
1
- function qs(params) {
2
- const entries = Object.entries(params).filter(([, v]) => v !== undefined);
3
- if (entries.length === 0)
4
- return '';
5
- return '?' + entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
6
- }
7
- async function request(baseUrl, path, options) {
8
- let res;
9
- try {
10
- res = await fetch(`${baseUrl}${path}`, {
11
- headers: { 'Content-Type': 'application/json' },
12
- ...options,
13
- });
14
- }
15
- catch (err) {
16
- if (err.cause &&
17
- err.cause.code === 'ECONNREFUSED') {
18
- throw new Error(`Cannot connect to octomux server at ${baseUrl.replace('/api', '')}\nStart it with: octomux start`);
19
- }
20
- throw err;
21
- }
22
- if (!res.ok) {
23
- const body = await res.json().catch(() => ({ error: res.statusText }));
24
- throw new Error(body.error || res.statusText);
1
+ import { createRequestCore, qs } from '@octomux/api-client';
2
+ function cliFetchError(err, { baseUrl }) {
3
+ if (err.cause &&
4
+ err.cause.code === 'ECONNREFUSED') {
5
+ throw new Error(`Cannot connect to octomux server at ${baseUrl.replace('/api', '')}\nStart it with: octomux start`);
25
6
  }
26
- if (res.status === 204)
27
- return undefined;
28
- return res.json();
7
+ throw err;
29
8
  }
30
9
  export function createClient(serverUrl) {
31
10
  const baseUrl = serverUrl.replace(/\/$/, '') + '/api';
11
+ const { request } = createRequestCore({
12
+ baseUrl,
13
+ alwaysJsonContentType: true,
14
+ onFetchError: cliFetchError,
15
+ });
32
16
  return {
33
17
  listIntegrations() {
34
- return request(baseUrl, '/integrations');
18
+ return request('/integrations');
35
19
  },
36
20
  createTask(data) {
37
- return request(baseUrl, '/tasks', { method: 'POST', body: JSON.stringify(data) });
21
+ return request('/tasks', { method: 'POST', body: JSON.stringify(data) });
38
22
  },
39
23
  listTasks(params) {
40
- return request(baseUrl, `/tasks${qs({ repo_path: params?.repo_path })}`);
24
+ return request(`/tasks${qs({ repo_path: params?.repo_path })}`);
41
25
  },
42
26
  getTask(id) {
43
- return request(baseUrl, `/tasks/${encodeURIComponent(id)}`);
27
+ return request(`/tasks/${encodeURIComponent(id)}`);
44
28
  },
45
29
  updateTask(id, data) {
46
- return request(baseUrl, `/tasks/${encodeURIComponent(id)}`, {
30
+ return request(`/tasks/${encodeURIComponent(id)}`, {
47
31
  method: 'PATCH',
48
32
  body: JSON.stringify(data),
49
33
  });
50
34
  },
51
35
  deleteTask(id) {
52
- return request(baseUrl, `/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' });
36
+ return request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' });
53
37
  },
54
38
  addAgent(taskId, data) {
55
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/agents`, {
39
+ return request(`/tasks/${encodeURIComponent(taskId)}/agents`, {
56
40
  method: 'POST',
57
41
  body: JSON.stringify(data || {}),
58
42
  });
59
43
  },
60
44
  stopAgent(taskId, agentId) {
61
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
45
+ return request(`/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
62
46
  },
63
47
  sendMessage(taskId, agentId, message) {
64
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}/message`, { method: 'POST', body: JSON.stringify({ message }) });
48
+ return request(`/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}/message`, { method: 'POST', body: JSON.stringify({ message }) });
65
49
  },
66
50
  listSkills() {
67
- return request(baseUrl, '/skills');
51
+ return request('/skills');
68
52
  },
69
53
  getSkill(name) {
70
- return request(baseUrl, `/skills/${encodeURIComponent(name)}`);
54
+ return request(`/skills/${encodeURIComponent(name)}`);
71
55
  },
72
56
  createSkill(data) {
73
- return request(baseUrl, '/skills', {
57
+ return request('/skills', {
74
58
  method: 'POST',
75
59
  body: JSON.stringify(data),
76
60
  });
77
61
  },
78
62
  deleteSkill(name) {
79
- return request(baseUrl, `/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
63
+ return request(`/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
80
64
  },
81
65
  recentRepos() {
82
- return request(baseUrl, '/recent-repos');
66
+ return request('/recent-repos');
83
67
  },
84
68
  defaultBranch(repoPath) {
85
- return request(baseUrl, `/default-branch${qs({ repo_path: repoPath })}`);
69
+ return request(`/default-branch${qs({ repo_path: repoPath })}`);
86
70
  },
87
71
  getRepoConfig(repoPath) {
88
- return request(baseUrl, `/repo-config${qs({ repo_path: repoPath })}`);
72
+ return request(`/repo-config${qs({ repo_path: repoPath })}`);
89
73
  },
90
74
  postComment(taskId, data) {
91
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/comments`, {
75
+ return request(`/tasks/${encodeURIComponent(taskId)}/comments`, {
92
76
  method: 'POST',
93
77
  body: JSON.stringify(data),
94
78
  });
95
79
  },
96
80
  listComments(taskId, file) {
97
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/comments${qs({ file })}`);
81
+ return request(`/tasks/${encodeURIComponent(taskId)}/comments${qs({ file })}`);
98
82
  },
99
83
  updateComment(taskId, commentId, data) {
100
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/comments/${encodeURIComponent(commentId)}`, { method: 'PATCH', body: JSON.stringify(data) });
84
+ return request(`/tasks/${encodeURIComponent(taskId)}/comments/${encodeURIComponent(commentId)}`, { method: 'PATCH', body: JSON.stringify(data) });
101
85
  },
102
86
  deleteComment(taskId, commentId) {
103
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/comments/${encodeURIComponent(commentId)}`, { method: 'DELETE' });
87
+ return request(`/tasks/${encodeURIComponent(taskId)}/comments/${encodeURIComponent(commentId)}`, { method: 'DELETE' });
104
88
  },
105
89
  moveTask(taskId, data) {
106
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/move`, {
90
+ return request(`/tasks/${encodeURIComponent(taskId)}/move`, {
107
91
  method: 'POST',
108
92
  body: JSON.stringify(data),
109
93
  });
110
94
  },
111
95
  postTaskSummary(taskId, data) {
112
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/summary`, {
96
+ return request(`/tasks/${encodeURIComponent(taskId)}/summary`, {
113
97
  method: 'POST',
114
98
  body: JSON.stringify(data),
115
99
  });
116
100
  },
117
101
  postTaskNote(taskId, data) {
118
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/note`, {
102
+ return request(`/tasks/${encodeURIComponent(taskId)}/note`, {
119
103
  method: 'POST',
120
104
  body: JSON.stringify(data),
121
105
  });
122
106
  },
123
107
  addTaskRef(taskId, data) {
124
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/refs`, {
108
+ return request(`/tasks/${encodeURIComponent(taskId)}/refs`, {
125
109
  method: 'POST',
126
110
  body: JSON.stringify(data),
127
111
  });
128
112
  },
129
113
  deleteTaskRef(taskId, integration) {
130
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/refs/${encodeURIComponent(integration)}`, { method: 'DELETE' });
114
+ return request(`/tasks/${encodeURIComponent(taskId)}/refs/${encodeURIComponent(integration)}`, { method: 'DELETE' });
131
115
  },
132
116
  getTaskUpdates(taskId) {
133
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/updates`);
117
+ return request(`/tasks/${encodeURIComponent(taskId)}/updates`);
134
118
  },
135
119
  getTaskRefs(taskId) {
136
- return request(baseUrl, `/tasks/${encodeURIComponent(taskId)}/refs`);
120
+ return request(`/tasks/${encodeURIComponent(taskId)}/refs`);
137
121
  },
138
122
  teamRun(data) {
139
- return request(baseUrl, '/teams/run', {
123
+ return request('/teams/run', {
140
124
  method: 'POST',
141
125
  body: JSON.stringify(data),
142
126
  });
143
127
  },
144
128
  teamSchedule(data) {
145
- return request(baseUrl, '/teams/schedule', {
129
+ return request('/teams/schedule', {
146
130
  method: 'POST',
147
131
  body: JSON.stringify(data),
148
132
  });
149
133
  },
150
134
  listTeams() {
151
- return request(baseUrl, '/teams');
135
+ return request('/teams');
136
+ },
137
+ listSavedFiles(repoPath) {
138
+ return request(`/repos/${encodeURIComponent(repoPath)}/files`);
139
+ },
140
+ getSavedFile(repoPath, relPath) {
141
+ return request(`/repos/${encodeURIComponent(repoPath)}/files/content${qs({ path: relPath })}`);
142
+ },
143
+ putSavedFile(repoPath, relPath, content) {
144
+ return request(`/repos/${encodeURIComponent(repoPath)}/files/content${qs({ path: relPath })}`, { method: 'PUT', body: JSON.stringify({ content }) });
152
145
  },
153
146
  };
154
147
  }
@@ -6,7 +6,7 @@ export function registerCloseTask(program) {
6
6
  .description('Close a running task')
7
7
  .action(async (id, _opts, cmd) => {
8
8
  const { client, json } = getContext(cmd);
9
- const task = await client.updateTask(id, { status: 'closed' });
9
+ const task = await client.updateTask(id, { runtime_state: 'idle' });
10
10
  if (json) {
11
11
  outputJson(task);
12
12
  return;
@@ -2,9 +2,9 @@ import { execFile } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import chalk from 'chalk';
4
4
  import { getContext } from '../action.js';
5
- import { outputJson, label, success, colorStatus } from '../format.js';
5
+ import { RUN_MODES } from '@octomux/types';
6
+ import { outputJson, label, success, colorStatus, taskDisplayStatus } from '../format.js';
6
7
  const execFileAsync = promisify(execFile);
7
- const VALID_MODES = ['new', 'existing', 'none', 'scratch'];
8
8
  export async function resolveForkFrom(client, forkFromId, explicitRepoPath, git = defaultGit) {
9
9
  let source;
10
10
  try {
@@ -14,13 +14,12 @@ export async function resolveForkFrom(client, forkFromId, explicitRepoPath, git
14
14
  const msg = err instanceof Error ? err.message : String(err);
15
15
  throw new Error(`cannot fork from ${forkFromId}: source not found (${msg})`);
16
16
  }
17
- const status = source.status;
18
17
  const runMode = source.run_mode ?? 'new';
19
18
  if (runMode === 'scratch' || runMode === 'none' || runMode === 'existing') {
20
- throw new Error(`cannot fork from ${forkFromId}: source has no managed branch (status=${status}, run_mode=${runMode})`);
19
+ throw new Error(`cannot fork from ${forkFromId}: source has no managed branch (runtime_state=${source.runtime_state}, run_mode=${runMode})`);
21
20
  }
22
- if (status === 'draft') {
23
- throw new Error(`cannot fork from ${forkFromId}: source has no branch (status=${status}, run_mode=${runMode})`);
21
+ if (source.runtime_state === 'idle') {
22
+ throw new Error(`cannot fork from ${forkFromId}: source has no branch (runtime_state=${source.runtime_state}, run_mode=${runMode})`);
24
23
  }
25
24
  const baseBranch = `agents/${forkFromId}`;
26
25
  const repoPath = explicitRepoPath ?? source.repo_path;
@@ -59,7 +58,7 @@ export function registerCreateTask(program) {
59
58
  .option('-p, --initial-prompt <prompt>', 'initial prompt for the agent')
60
59
  .option('-b, --branch <name>', 'branch name (new mode only)')
61
60
  .option('--base-branch <name>', 'base branch name (new mode only)')
62
- .option('--mode <mode>', `run mode: ${VALID_MODES.join(' | ')} (default: new)`, 'new')
61
+ .option('--mode <mode>', `run mode: ${RUN_MODES.join(' | ')} (default: new)`, 'new')
63
62
  .option('--worktree-path <path>', 'existing worktree path (required for existing mode)')
64
63
  .option('--fork-from <task-id>', 'fork from an existing new-mode task (sets base_branch to agents/<id>)')
65
64
  .option('--draft', 'create as draft without starting')
@@ -69,8 +68,8 @@ export function registerCreateTask(program) {
69
68
  .action(async (opts, cmd) => {
70
69
  const { client, json } = getContext(cmd);
71
70
  const mode = opts.mode;
72
- if (!VALID_MODES.includes(mode)) {
73
- cmd.error(`--mode must be one of: ${VALID_MODES.join(', ')}`);
71
+ if (!RUN_MODES.includes(mode)) {
72
+ cmd.error(`--mode must be one of: ${RUN_MODES.join(', ')}`);
74
73
  }
75
74
  // --fork-from is only meaningful for new mode — it derives base_branch.
76
75
  if (opts.forkFrom) {
@@ -148,7 +147,7 @@ export function registerCreateTask(program) {
148
147
  success(`Created task ${task.id}`);
149
148
  console.log(label('Title', task.title));
150
149
  console.log(label('Mode', mode));
151
- console.log(label('Status', colorStatus(task.status)));
150
+ console.log(label('Status', colorStatus(taskDisplayStatus(task))));
152
151
  console.log(label('Branch', task.branch));
153
152
  console.log(label('Repo', task.repo_path));
154
153
  });
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import { getContext } from '../action.js';
4
+ import { outputJson, printTable, errorMessage } from '../format.js';
5
+ function repoPath(opts) {
6
+ return opts.repo ?? process.cwd();
7
+ }
8
+ export function registerFiles(program) {
9
+ const files = program
10
+ .command('files')
11
+ .description('List, read, and write repo-portable saved files under .octomux/files/');
12
+ files
13
+ .command('list')
14
+ .description('List saved files in <repo>/.octomux/files/')
15
+ .option('-r, --repo <path>', 'repository path (default: cwd)')
16
+ .action(async (opts, cmd) => {
17
+ const { client, json } = getContext(cmd);
18
+ const entries = await client.listSavedFiles(repoPath(opts));
19
+ if (json) {
20
+ outputJson(entries);
21
+ return;
22
+ }
23
+ if (entries.length === 0) {
24
+ console.log('No saved files.');
25
+ return;
26
+ }
27
+ printTable([
28
+ { header: 'PATH', width: 40, get: (e) => e.path },
29
+ { header: 'SIZE', width: 10, get: (e) => String(e.size) },
30
+ ], entries);
31
+ });
32
+ files
33
+ .command('get <path>')
34
+ .description('Read a saved file')
35
+ .option('-r, --repo <path>', 'repository path (default: cwd)')
36
+ .action(async (filePath, opts, cmd) => {
37
+ const { client, json } = getContext(cmd);
38
+ const file = await client.getSavedFile(repoPath(opts), filePath);
39
+ if (json) {
40
+ outputJson(file);
41
+ return;
42
+ }
43
+ console.log(file.content);
44
+ });
45
+ files
46
+ .command('put <path>')
47
+ .description('Write a saved file (content from --content or stdin)')
48
+ .option('-r, --repo <path>', 'repository path (default: cwd)')
49
+ .option('-c, --content <text>', 'file content (default: read stdin)')
50
+ .action(async (filePath, opts, cmd) => {
51
+ const { client, json } = getContext(cmd);
52
+ let content = opts.content;
53
+ if (content === undefined) {
54
+ if (process.stdin.isTTY) {
55
+ errorMessage('Provide --content or pipe content on stdin');
56
+ process.exit(1);
57
+ }
58
+ content = fs.readFileSync(0, 'utf-8');
59
+ }
60
+ const file = await client.putSavedFile(repoPath(opts), filePath, content);
61
+ if (json) {
62
+ outputJson(file);
63
+ return;
64
+ }
65
+ console.log(chalk.green(`Wrote ${file.path}`));
66
+ });
67
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { getContext } from '../action.js';
3
- import { outputJson, label, heading, colorStatus, colorAgentStatus } from '../format.js';
3
+ import { outputJson, label, heading, colorStatus, colorAgentStatus, taskDisplayStatus, } from '../format.js';
4
4
  export function registerGetTask(program) {
5
5
  program
6
6
  .command('get-task <id>')
@@ -15,7 +15,7 @@ export function registerGetTask(program) {
15
15
  }
16
16
  heading(`Task ${task.id}`);
17
17
  console.log(label('Title', task.title));
18
- console.log(label('Status', colorStatus(task.status)));
18
+ console.log(label('Status', colorStatus(taskDisplayStatus(task))));
19
19
  console.log(label('Repo', task.repo_path));
20
20
  console.log(label('Branch', task.branch));
21
21
  if (task.base_branch)
@@ -1,5 +1,5 @@
1
1
  import { getContext } from '../action.js';
2
- import { outputJson, colorStatus, printTable } from '../format.js';
2
+ import { outputJson, colorStatus, printTable, taskDisplayStatus, taskMatchesStatusFilter, } from '../format.js';
3
3
  export function registerListTasks(program) {
4
4
  program
5
5
  .command('list-tasks')
@@ -11,7 +11,7 @@ export function registerListTasks(program) {
11
11
  const { client, json } = getContext(cmd);
12
12
  let tasks = await client.listTasks(opts.repoPath ? { repo_path: opts.repoPath } : undefined);
13
13
  if (opts.status) {
14
- tasks = tasks.filter((t) => t.status === opts.status);
14
+ tasks = tasks.filter((t) => taskMatchesStatusFilter(t, opts.status));
15
15
  }
16
16
  if (json) {
17
17
  outputJson(tasks);
@@ -23,7 +23,7 @@ export function registerListTasks(program) {
23
23
  }
24
24
  printTable([
25
25
  { header: 'ID', width: 14, get: (t) => t.id },
26
- { header: 'STATUS', width: 14, get: (t) => colorStatus(t.status) },
26
+ { header: 'STATUS', width: 14, get: (t) => colorStatus(taskDisplayStatus(t)) },
27
27
  { header: 'TITLE', get: (t) => t.title },
28
28
  ], tasks);
29
29
  });
@@ -1,18 +1,18 @@
1
1
  import { getContext } from '../action.js';
2
- import { outputJson, success, colorStatus, label } from '../format.js';
2
+ import { outputJson, success, colorStatus, label, taskDisplayStatus } from '../format.js';
3
3
  export function registerResumeTask(program) {
4
4
  program
5
5
  .command('resume-task <id>')
6
6
  .description('Resume a closed or errored task')
7
7
  .action(async (id, _opts, cmd) => {
8
8
  const { client, json } = getContext(cmd);
9
- const task = await client.updateTask(id, { status: 'running' });
9
+ const task = await client.updateTask(id, { runtime_state: 'running' });
10
10
  if (json) {
11
11
  outputJson(task);
12
12
  return;
13
13
  }
14
14
  success(`Resumed task ${task.id}`);
15
15
  console.log(label('Title', task.title));
16
- console.log(label('Status', colorStatus(task.status)));
16
+ console.log(label('Status', colorStatus(taskDisplayStatus(task))));
17
17
  });
18
18
  }
@@ -1,23 +1,16 @@
1
1
  import { getContext } from '../action.js';
2
+ import { WORKFLOW_STATUSES } from '@octomux/types';
2
3
  import { outputJson, success, label } from '../format.js';
3
- const VALID_STATUSES = [
4
- 'backlog',
5
- 'planned',
6
- 'in_progress',
7
- 'human_review',
8
- 'pr',
9
- 'done',
10
- ];
11
4
  export function registerTaskMove(program) {
12
5
  program
13
6
  .command('task-move <id> <workflow_status>')
14
- .description(`Move a task to a workflow column (${VALID_STATUSES.join(' | ')})`)
7
+ .description(`Move a task to a workflow column (${WORKFLOW_STATUSES.join(' | ')})`)
15
8
  .option('-n, --note <note>', 'optional note to attach to the move')
16
9
  .action(async (id, workflowStatus, opts, cmd) => {
17
10
  const { client, json } = getContext(cmd);
18
- if (!VALID_STATUSES.includes(workflowStatus)) {
11
+ if (!WORKFLOW_STATUSES.includes(workflowStatus)) {
19
12
  console.error(`Invalid workflow_status: ${workflowStatus}`);
20
- console.error(`Valid values: ${VALID_STATUSES.join(', ')}`);
13
+ console.error(`Valid values: ${WORKFLOW_STATUSES.join(', ')}`);
21
14
  process.exit(1);
22
15
  }
23
16
  const task = await client.moveTask(id, {
@@ -25,12 +25,12 @@ export function registerTaskRefAdd(program) {
25
25
  .action(async (id, integration, externalId, opts, cmd) => {
26
26
  const { client, json } = getContext(cmd);
27
27
  const metadata = parseMetadata(opts.metadata);
28
+ const mergedMetadata = opts.title !== undefined ? { ...metadata, title: opts.title } : metadata;
28
29
  const ref = await client.addTaskRef(id, {
29
30
  integration,
30
- external_id: externalId,
31
+ ref: externalId,
31
32
  url: opts.url,
32
- title: opts.title,
33
- ...(metadata !== undefined ? { metadata } : {}),
33
+ ...(mergedMetadata !== undefined ? { metadata: mergedMetadata } : {}),
34
34
  });
35
35
  if (json) {
36
36
  outputJson(ref);
@@ -39,7 +39,6 @@ export function registerTaskRefAdd(program) {
39
39
  success(`Linked ${integration}:${externalId} to task ${id}`);
40
40
  if (ref.url)
41
41
  console.log(label('URL', ref.url));
42
- if (ref.title)
43
- console.log(label('Title', ref.title));
42
+ console.log(label('Ref', ref.ref));
44
43
  });
45
44
  }
@@ -17,22 +17,11 @@ export function registerTaskUpdates(program) {
17
17
  }
18
18
  heading(`Updates for task ${id}`);
19
19
  for (const u of updates) {
20
- const who = u.author ? ` [${u.author}]` : '';
21
20
  const ts = new Date(u.created_at).toLocaleString();
22
- console.log(` ${ts}${who} ${u.kind}`);
23
- if (u.payload) {
24
- try {
25
- const parsed = JSON.parse(u.payload);
26
- if (parsed.note)
27
- console.log(` ${parsed.note}`);
28
- if (parsed.summary)
29
- console.log(` ${parsed.summary}`);
30
- if (parsed.to)
31
- console.log(` → ${parsed.to}`);
32
- }
33
- catch {
34
- console.log(` ${u.payload}`);
35
- }
21
+ const transition = u.from_status && u.to_status ? `${u.from_status}${u.to_status}` : null;
22
+ console.log(` ${ts} ${u.kind}${transition ? ` ${transition}` : ''}`);
23
+ if (u.body) {
24
+ console.log(` ${u.body}`);
36
25
  }
37
26
  }
38
27
  });
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  const STATUS_COLORS = {
3
3
  draft: chalk.cyan,
4
+ idle: chalk.cyan,
4
5
  setting_up: chalk.yellow,
5
6
  running: chalk.green,
6
7
  closed: chalk.dim,
@@ -16,6 +17,16 @@ export function colorStatus(status) {
16
17
  const colorFn = STATUS_COLORS[status] || chalk.white;
17
18
  return colorFn(status);
18
19
  }
20
+ /** Legacy CLI status label derived from runtime_state + workflow_status. */
21
+ export function taskDisplayStatus(task) {
22
+ if (task.runtime_state === 'idle') {
23
+ return task.workflow_status === 'backlog' ? 'draft' : 'closed';
24
+ }
25
+ return task.runtime_state;
26
+ }
27
+ export function taskMatchesStatusFilter(task, status) {
28
+ return taskDisplayStatus(task) === status;
29
+ }
19
30
  export function colorAgentStatus(status) {
20
31
  const colorFn = AGENT_STATUS_COLORS[status] || chalk.white;
21
32
  return colorFn(status);