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.
- package/README.md +29 -17
- package/bin/octomux.js +20 -0
- package/cli/dist/client.js +50 -57
- package/cli/dist/commands/close-task.js +1 -1
- package/cli/dist/commands/create-task.js +9 -10
- package/cli/dist/commands/files.js +67 -0
- package/cli/dist/commands/get-task.js +2 -2
- package/cli/dist/commands/list-tasks.js +3 -3
- package/cli/dist/commands/resume-task.js +3 -3
- package/cli/dist/commands/task-move.js +4 -11
- package/cli/dist/commands/task-ref-add.js +4 -5
- package/cli/dist/commands/task-updates.js +4 -15
- package/cli/dist/format.js +11 -0
- package/cli/dist/index.js +2 -0
- package/dist/assets/{AgentEditor-bmu3IToQ.js → AgentEditor-DO3dt4ri.js} +1 -1
- package/dist/assets/{ChatPage-DoNLt9yc.js → ChatPage-Be7NRaz5.js} +2 -2
- package/dist/assets/IntegrationsPage-o2qrU4Ly.js +1 -0
- package/dist/assets/OrchestratorPage-B6-5Ymz_.js +29 -0
- package/dist/assets/ReviewDetailPage-DvnR-g2n.js +1 -0
- package/dist/assets/{SetupPage-CJYmbm6K.js → SetupPage-CmzzPZne.js} +1 -1
- package/dist/assets/{SkillEditor-BXV-a19B.js → SkillEditor-BYTCGSV_.js} +1 -1
- package/dist/assets/{WorkspaceDetailPage-Cns6VZKt.js → WorkspaceDetailPage-ZCgJpIyZ.js} +1 -1
- package/dist/assets/{WorkspacesPage-DI_Ilx7o.js → WorkspacesPage-8IwI9uiq.js} +1 -1
- package/dist/assets/index-CQh8m5qn.css +1 -0
- package/dist/assets/index-D4uuZdW5.js +34 -0
- package/dist/assets/{vendor-monaco-CdNyTACs.js → vendor-monaco-BpPnCqoL.js} +1 -1
- package/dist/assets/{vendor-react-Dd26lTa7.js → vendor-react-BQxe05fg.js} +1 -1
- package/dist/assets/{vendor-router-yJbofmar.js → vendor-router-BxmO8ihp.js} +1 -1
- package/dist/assets/{vendor-ui-ChI1tM4e.js → vendor-ui-48-ysyeR.js} +1 -1
- package/dist/index.html +6 -6
- package/dist-server/agent-session/mcp/submit-result-server.js +8 -0
- package/dist-server/agents-45L5TJD3.js +1 -0
- package/dist-server/approval-timeout-NYVPHUHG.js +1 -0
- package/dist-server/{chunk-BTEWK6EA.js → chunk-3CVFZVVG.js} +108 -18
- package/dist-server/chunk-B36SOK6W.js +1 -0
- package/dist-server/chunk-CNU4N4AY.js +1 -0
- package/dist-server/chunk-COWASN3D.js +1 -0
- package/dist-server/chunk-CZT76WRN.js +1 -0
- package/dist-server/chunk-FQ324ITT.js +368 -0
- package/dist-server/chunk-JACWMK5E.js +4 -0
- package/dist-server/chunk-NLRGIOB5.js +2 -0
- package/dist-server/chunk-OIRUBP4J.js +1 -0
- package/dist-server/chunk-PC2DITVE.js +1 -0
- package/dist-server/chunk-R466QRBH.js +2 -0
- package/dist-server/chunk-S67AOMGA.js +14 -0
- package/dist-server/chunk-SFMUA6HF.js +166 -0
- package/dist-server/chunk-TKX326LX.js +13 -0
- package/dist-server/chunk-U55T3B3U.js +4 -0
- package/dist-server/chunk-UWUVS7F4.js +2 -0
- package/dist-server/chunk-WLB7K3JL.js +1 -0
- package/dist-server/{chunk-4GEKZIZ6.js → chunk-XQ7EQ3DU.js} +1 -1
- package/dist-server/{chunk-OSXDDM27.js → chunk-YZSABAAX.js} +1 -1
- package/dist-server/claude-code-M4NFPKSN.js +1 -0
- package/dist-server/cli-review.js +94 -0
- package/dist-server/harnesses-2PWYFS77.js +1 -0
- package/dist-server/hook-settings-KCX3NSNL.js +1 -0
- package/dist-server/hooks-install-KBBZ2HEZ.js +1 -0
- package/dist-server/index.js +26 -279
- package/dist-server/orchestrator/mcp/server.js +2 -0
- package/dist-server/prefill-QQYRVBGG.js +1 -0
- package/dist-server/preflight-TC52EHUN.js +2 -0
- package/dist-server/publish-review-QWKNLWKW.js +5 -0
- package/dist-server/registry-Q5TISXTP.js +1 -0
- package/dist-server/resolve-env-YZ3RWEBZ.js +1 -0
- package/dist-server/settings-YFZ7BOZK.js +1 -0
- package/dist-server/setup-status-54WC6S2N.js +1 -0
- package/dist-server/startup.js +1 -1
- package/dist-server/store-PRUWQM53.js +1 -0
- package/dist-server/teams-GIVRUA2J.js +6 -0
- package/package.json +28 -6
- package/scripts/record-fixtures.ts +57 -0
- package/scripts/spike/transcript-tail.ts +241 -0
- package/skills/add-agent/SKILL.md +44 -8
- package/skills/create-task/SKILL.md +23 -5
- package/skills/review-deep/SKILL.md +184 -0
- package/skills/review-walkthrough/SKILL.md +107 -0
- package/skills/send-agent-message/SKILL.md +56 -16
- package/skills/update-task-status/SKILL.md +38 -7
- package/dist/assets/IntegrationsPage-D3iTxzhN.js +0 -1
- package/dist/assets/ReviewDetailPage-DxCK8Zq4.js +0 -1
- package/dist/assets/index-D2mLE9DX.css +0 -1
- package/dist/assets/index-DKTtIYvz.js +0 -35
- package/dist-server/agents-75BBHCTY.js +0 -1
- package/dist-server/chunk-3PYPZRQV.js +0 -1
- package/dist-server/chunk-4DGDU2NU.js +0 -49
- package/dist-server/chunk-BYZCD2AS.js +0 -16
- package/dist-server/chunk-GAXJZSQT.js +0 -2
- package/dist-server/chunk-MCI4HOI7.js +0 -2
- package/dist-server/chunk-MYIYBV6O.js +0 -16
- package/dist-server/chunk-NHDUWYXX.js +0 -2
- package/dist-server/chunk-NUVE5API.js +0 -23
- package/dist-server/chunk-RFWGJ3HT.js +0 -1
- package/dist-server/chunk-YKFMOBXG.js +0 -2
- package/dist-server/chunk-ZV3CXK7O.js +0 -1
- package/dist-server/claude-code-SUTWME7T.js +0 -1
- package/dist-server/harnesses-ACMXKLJO.js +0 -1
- package/dist-server/hook-settings-VQOP567H.js +0 -1
- package/dist-server/hooks-install-5WIG6NCP.js +0 -1
- package/dist-server/prefill-IKIJSGHJ.js +0 -14
- package/dist-server/preflight-W4357G4A.js +0 -13
- package/dist-server/publish-review-3X766STN.js +0 -7
- package/dist-server/registry-QDC56XWP.js +0 -1
- package/dist-server/resolve-env-GWFARUZQ.js +0 -1
- package/dist-server/settings-VDSTIV6M.js +0 -1
- package/dist-server/setup-status-TDHBDU7E.js +0 -1
- package/dist-server/store-7ATWFPHP.js +0 -1
- package/dist-server/teams-DOJN7H42.js +0 -22
- 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
|
-
- **
|
|
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
|
|
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
|
-
- `
|
|
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
|
+
}
|
package/cli/dist/client.js
CHANGED
|
@@ -1,154 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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(
|
|
18
|
+
return request('/integrations');
|
|
35
19
|
},
|
|
36
20
|
createTask(data) {
|
|
37
|
-
return request(
|
|
21
|
+
return request('/tasks', { method: 'POST', body: JSON.stringify(data) });
|
|
38
22
|
},
|
|
39
23
|
listTasks(params) {
|
|
40
|
-
return request(
|
|
24
|
+
return request(`/tasks${qs({ repo_path: params?.repo_path })}`);
|
|
41
25
|
},
|
|
42
26
|
getTask(id) {
|
|
43
|
-
return request(
|
|
27
|
+
return request(`/tasks/${encodeURIComponent(id)}`);
|
|
44
28
|
},
|
|
45
29
|
updateTask(id, data) {
|
|
46
|
-
return request(
|
|
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(
|
|
36
|
+
return request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
53
37
|
},
|
|
54
38
|
addAgent(taskId, data) {
|
|
55
|
-
return request(
|
|
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(
|
|
45
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}`, { method: 'DELETE' });
|
|
62
46
|
},
|
|
63
47
|
sendMessage(taskId, agentId, message) {
|
|
64
|
-
return request(
|
|
48
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/agents/${encodeURIComponent(agentId)}/message`, { method: 'POST', body: JSON.stringify({ message }) });
|
|
65
49
|
},
|
|
66
50
|
listSkills() {
|
|
67
|
-
return request(
|
|
51
|
+
return request('/skills');
|
|
68
52
|
},
|
|
69
53
|
getSkill(name) {
|
|
70
|
-
return request(
|
|
54
|
+
return request(`/skills/${encodeURIComponent(name)}`);
|
|
71
55
|
},
|
|
72
56
|
createSkill(data) {
|
|
73
|
-
return request(
|
|
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(
|
|
63
|
+
return request(`/skills/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
80
64
|
},
|
|
81
65
|
recentRepos() {
|
|
82
|
-
return request(
|
|
66
|
+
return request('/recent-repos');
|
|
83
67
|
},
|
|
84
68
|
defaultBranch(repoPath) {
|
|
85
|
-
return request(
|
|
69
|
+
return request(`/default-branch${qs({ repo_path: repoPath })}`);
|
|
86
70
|
},
|
|
87
71
|
getRepoConfig(repoPath) {
|
|
88
|
-
return request(
|
|
72
|
+
return request(`/repo-config${qs({ repo_path: repoPath })}`);
|
|
89
73
|
},
|
|
90
74
|
postComment(taskId, data) {
|
|
91
|
-
return request(
|
|
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(
|
|
81
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/comments${qs({ file })}`);
|
|
98
82
|
},
|
|
99
83
|
updateComment(taskId, commentId, data) {
|
|
100
|
-
return request(
|
|
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(
|
|
87
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/comments/${encodeURIComponent(commentId)}`, { method: 'DELETE' });
|
|
104
88
|
},
|
|
105
89
|
moveTask(taskId, data) {
|
|
106
|
-
return request(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
114
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/refs/${encodeURIComponent(integration)}`, { method: 'DELETE' });
|
|
131
115
|
},
|
|
132
116
|
getTaskUpdates(taskId) {
|
|
133
|
-
return request(
|
|
117
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/updates`);
|
|
134
118
|
},
|
|
135
119
|
getTaskRefs(taskId) {
|
|
136
|
-
return request(
|
|
120
|
+
return request(`/tasks/${encodeURIComponent(taskId)}/refs`);
|
|
137
121
|
},
|
|
138
122
|
teamRun(data) {
|
|
139
|
-
return request(
|
|
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(
|
|
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(
|
|
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, {
|
|
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 {
|
|
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 (
|
|
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 (
|
|
23
|
-
throw new Error(`cannot fork from ${forkFromId}: source has no branch (
|
|
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: ${
|
|
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 (!
|
|
73
|
-
cmd.error(`--mode must be one of: ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
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
|
|
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 (${
|
|
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 (!
|
|
11
|
+
if (!WORKFLOW_STATUSES.includes(workflowStatus)) {
|
|
19
12
|
console.error(`Invalid workflow_status: ${workflowStatus}`);
|
|
20
|
-
console.error(`Valid values: ${
|
|
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
|
-
|
|
31
|
+
ref: externalId,
|
|
31
32
|
url: opts.url,
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
});
|
package/cli/dist/format.js
CHANGED
|
@@ -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);
|