@brimveyn/aimux 1.19.7 → 1.20.1
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 +6 -0
- package/package.json +3 -2
- package/skills/aimux-orchestrator/SKILL.md +93 -0
- package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
- package/skills/aimux-orchestrator/references/prompts.md +57 -0
- package/skills/aimux-orchestrator/references/review.md +18 -0
- package/src/cli/client/daemon-client.ts +16 -0
- package/src/cli/commands/tab/create.ts +236 -125
- package/src/cli/commands/tab/prompt-io.ts +2 -1
- package/src/cli/commands/worker/await.ts +33 -0
- package/src/cli/commands/worker/doctor.ts +129 -0
- package/src/cli/commands/worker/list.ts +25 -0
- package/src/cli/commands/worker/prompt.ts +49 -0
- package/src/cli/commands/worker/run.ts +97 -0
- package/src/cli/commands/worker/shared.ts +255 -0
- package/src/cli/commands/worker/stop.ts +84 -0
- package/src/cli/commands/worktree/remove.ts +29 -9
- package/src/cli/index.ts +21 -9
- package/src/cli/registry.ts +12 -0
- package/src/daemon/daemon.ts +58 -8
- package/src/daemon/session-registry.ts +5 -0
- package/src/git/worktree.ts +8 -0
- package/src/index.tsx +18 -81
- package/src/ipc/manager-protocol.ts +15 -2
- package/src/ipc/protocol.ts +26 -3
- package/src/state/session-persistence.ts +2 -0
- package/src/state/types.ts +4 -0
- package/src/state/validation.ts +1 -0
- package/src/terminal-manager/manager-client.ts +18 -3
package/README.md
CHANGED
|
@@ -157,6 +157,8 @@ The help modal reflects the resolved keymap, so it includes your overrides.
|
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
159
|
aimux
|
|
160
|
+
aimux worker doctor
|
|
161
|
+
aimux worker run --name investigate --assistant claude "inspect this repository"
|
|
160
162
|
aimux version
|
|
161
163
|
aimux doctor
|
|
162
164
|
aimux update
|
|
@@ -166,6 +168,10 @@ aimux restart-terminal-manager
|
|
|
166
168
|
|
|
167
169
|
See [`docs/reference/cli.md`](docs/reference/cli.md) for behavior details.
|
|
168
170
|
|
|
171
|
+
For agent orchestration, prefer the named `aimux worker` commands. They combine
|
|
172
|
+
isolated worktree creation, prompt dispatch, authoritative turn waiting, fleet
|
|
173
|
+
inspection, and guarded cleanup without shell wrappers or `jq`.
|
|
174
|
+
|
|
169
175
|
## Runtime Model
|
|
170
176
|
|
|
171
177
|
`aimux` is split into:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, Kimi side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"files": [
|
|
36
36
|
"src",
|
|
37
|
+
"skills",
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
"bump:terminal_manager": "bun run scripts/bump-protocol.ts terminal-manager"
|
|
66
67
|
},
|
|
67
68
|
"dependencies": {
|
|
68
|
-
"@brimveyn/aimux-config": "0.8.
|
|
69
|
+
"@brimveyn/aimux-config": "0.8.5",
|
|
69
70
|
"@opentui/core": "^0.1.90",
|
|
70
71
|
"@opentui/react": "^0.1.90",
|
|
71
72
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aimux-orchestrator
|
|
3
|
+
description: Orchestrate implementation plans with parallel Claude, Codex, OpenCode, Grok, or Kimi workers through the aimux worker CLI. Use when an agent must decompose a plan, launch isolated workers, supervise questions and failures, review each worker's diff and checks, integrate accepted work, and clean up safely.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# aimux Orchestrator
|
|
7
|
+
|
|
8
|
+
Own the plan and quality gate. Let aimux own worker transport and lifecycle.
|
|
9
|
+
|
|
10
|
+
## Preflight
|
|
11
|
+
|
|
12
|
+
Run once:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
aimux worker doctor
|
|
16
|
+
aimux worker --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Stop if the doctor reports missing worker capabilities, the wrong workspace, or
|
|
20
|
+
an unavailable assistant. Restart/update aimux as instructed; do not fall back
|
|
21
|
+
to screen scraping or the legacy shell wrappers.
|
|
22
|
+
|
|
23
|
+
## Orchestration loop
|
|
24
|
+
|
|
25
|
+
1. Parse the plan into independently reviewable units and dependencies.
|
|
26
|
+
2. Record units in a ledger. Copy `assets/ledger.template.md` for multi-unit work.
|
|
27
|
+
3. Select only units whose dependencies are accepted.
|
|
28
|
+
4. Launch no more workers than you can review:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
aimux worker run \
|
|
32
|
+
--name feat-auth \
|
|
33
|
+
--assistant claude \
|
|
34
|
+
--prompt-file /tmp/feat-auth.md \
|
|
35
|
+
--detach
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
5. Inspect the fleet with `aimux worker list`.
|
|
39
|
+
6. Await a detached worker with `aimux worker await feat-auth`.
|
|
40
|
+
7. If a worker asks a question, answer only when the plan already determines the
|
|
41
|
+
answer:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
aimux worker prompt feat-auth --prompt-file /tmp/answer.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Escalate design choices, irreversible operations, deployments, pushes,
|
|
48
|
+
migrations, spending, and external communication to the human.
|
|
49
|
+
|
|
50
|
+
8. Review the worktree path returned in the worker JSON. Read the diff and run
|
|
51
|
+
the repository's required tests, typecheck, lint, and build. Worker claims
|
|
52
|
+
are not evidence.
|
|
53
|
+
9. Accept, request a precise correction with `worker prompt`, or escalate.
|
|
54
|
+
10. After integration, close and clean up:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
aimux worker stop feat-auth --cleanup-worktree
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Isolation
|
|
61
|
+
|
|
62
|
+
`worker run` creates a fresh worktree by default. Keep that default when writes
|
|
63
|
+
may overlap or scope is uncertain.
|
|
64
|
+
|
|
65
|
+
- Use `--worktree <id>` only for workers intentionally sharing one review unit.
|
|
66
|
+
- Use `--no-worktree` only for sequential work or provably disjoint writes in
|
|
67
|
+
the active tree.
|
|
68
|
+
- Review co-located workers together because their diff is shared.
|
|
69
|
+
|
|
70
|
+
## Dispatch rules
|
|
71
|
+
|
|
72
|
+
- Give each worker one bounded unit with goal, scope, exclusions, acceptance
|
|
73
|
+
criteria, repository conventions, and required verification.
|
|
74
|
+
- Read `references/prompts.md` when drafting task or correction prompts.
|
|
75
|
+
- Use provider defaults unless the plan or user requires a model. Set
|
|
76
|
+
`--model`/`--effort` only when `worker doctor` says the assistant supports the
|
|
77
|
+
control.
|
|
78
|
+
- Prefer `--prompt-file` for multiline prompts; it avoids shell quoting hazards.
|
|
79
|
+
- Use `--detach` for parallel launch. Without it, `worker run` waits and returns
|
|
80
|
+
a completed/question/timeout/error outcome.
|
|
81
|
+
|
|
82
|
+
## Quality and safety
|
|
83
|
+
|
|
84
|
+
Read `references/review.md` before accepting the first unit.
|
|
85
|
+
|
|
86
|
+
- `completed` means the turn ended, not that the change is correct.
|
|
87
|
+
- Never accept a failing or unrun verification gate.
|
|
88
|
+
- Cap correction attempts at three; then escalate with evidence.
|
|
89
|
+
- Never use `--force` cleanup unless the human authorized discarding dirty work.
|
|
90
|
+
- Keep the ledger truthful after every dispatch, outcome, review, and integration.
|
|
91
|
+
|
|
92
|
+
The CLI is the command reference. Use `aimux worker <verb> --help` instead of
|
|
93
|
+
copying flags or JSON shapes into this skill.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Orchestration ledger
|
|
2
|
+
|
|
3
|
+
Base: `<branch / sha>`
|
|
4
|
+
|
|
5
|
+
| unit | goal | deps | status | worker | worktree / branch | attempts | review evidence |
|
|
6
|
+
| ---- | ---- | ---- | ------- | ------ | ----------------- | -------: | --------------- |
|
|
7
|
+
| 1.1 | | — | pending | | | 0 | |
|
|
8
|
+
|
|
9
|
+
Statuses: `pending`, `dispatched`, `in-review`, `needs-changes`, `accepted`,
|
|
10
|
+
`blocked`, `escalated`.
|
|
11
|
+
|
|
12
|
+
## Decisions
|
|
13
|
+
|
|
14
|
+
- `<date>` — `<decision and reason>`
|
|
15
|
+
|
|
16
|
+
## Human questions
|
|
17
|
+
|
|
18
|
+
- [ ] `<unit>` — `<decision needed, evidence, and options>`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Worker prompts
|
|
2
|
+
|
|
3
|
+
## Task
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# Task: <id> — <title>
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
|
|
10
|
+
<observable outcome>
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
- In: <files or subsystem>
|
|
15
|
+
- Out: <explicit exclusions>
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
1. <behavior>
|
|
20
|
+
2. <edge case>
|
|
21
|
+
|
|
22
|
+
## Acceptance
|
|
23
|
+
|
|
24
|
+
- <machine-checkable criterion>
|
|
25
|
+
|
|
26
|
+
## Verification
|
|
27
|
+
|
|
28
|
+
Run the repository's tests, typecheck, lint, and build commands that apply.
|
|
29
|
+
Report changed files, commands run, results, and remaining risks.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Tell the worker to read repository instructions before editing. Do not ask it to
|
|
33
|
+
merge, push, deploy, or clean up its worktree.
|
|
34
|
+
|
|
35
|
+
## Correction
|
|
36
|
+
|
|
37
|
+
```markdown
|
|
38
|
+
# Correction for <id>
|
|
39
|
+
|
|
40
|
+
The review found:
|
|
41
|
+
|
|
42
|
+
- <specific failing behavior with file/test evidence>
|
|
43
|
+
|
|
44
|
+
Required change:
|
|
45
|
+
|
|
46
|
+
- <bounded correction>
|
|
47
|
+
|
|
48
|
+
Do not broaden scope. Re-run:
|
|
49
|
+
|
|
50
|
+
- <exact verification commands>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Answer
|
|
54
|
+
|
|
55
|
+
Answer the exact question and restate any relevant boundary. If the question
|
|
56
|
+
introduces a product decision or risky action not settled by the plan, escalate
|
|
57
|
+
instead of improvising.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Worker review gate
|
|
2
|
+
|
|
3
|
+
Review inside the returned worktree path.
|
|
4
|
+
|
|
5
|
+
1. Read repository instructions and the dispatched acceptance criteria.
|
|
6
|
+
2. Inspect status and the complete diff against the unit's base.
|
|
7
|
+
3. Confirm every changed hunk belongs to the requested scope.
|
|
8
|
+
4. Check correctness, edge cases, error paths, tests, debug leftovers, secrets,
|
|
9
|
+
stubs, and repository conventions.
|
|
10
|
+
5. Run the required test, typecheck, lint, format-check, and build commands
|
|
11
|
+
yourself.
|
|
12
|
+
6. Record evidence in the ledger.
|
|
13
|
+
|
|
14
|
+
Accept only when the behavior and verification pass. If a check cannot run,
|
|
15
|
+
record why and escalate when it prevents a trustworthy decision.
|
|
16
|
+
|
|
17
|
+
Request changes with concrete evidence: location, observed behavior, expected
|
|
18
|
+
behavior, and the exact gate to rerun. Do not send a vague "fix the tests" prompt.
|
|
@@ -135,10 +135,26 @@ export class DaemonClient {
|
|
|
135
135
|
return this.hello.capabilities
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
getAppVersion(): string | null {
|
|
139
|
+
return this.hello.appVersion ?? null
|
|
140
|
+
}
|
|
141
|
+
|
|
138
142
|
getSelectedVersion(): number {
|
|
139
143
|
return this.hello.selectedVersion
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
getProcessVersion(): string {
|
|
147
|
+
return this.hello.processVersion
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getManagerCapabilities(): readonly string[] {
|
|
151
|
+
return this.hello.managerCapabilities ?? []
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getManagerSelectedVersion(): number | null {
|
|
155
|
+
return this.hello.managerSelectedVersion ?? null
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
private async send(request: ClientRequest): Promise<ServerResponse> {
|
|
143
159
|
return new Promise((resolve, reject) => {
|
|
144
160
|
const timer = setTimeout(() => {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { resolve as resolvePath } from 'node:path'
|
|
2
2
|
|
|
3
|
+
import type { WorktreeRecord } from '../../../state/types'
|
|
4
|
+
import type { CliContext } from '../../context'
|
|
3
5
|
import type { CliCommand } from '../../registry'
|
|
4
6
|
|
|
5
7
|
import { loadConfig } from '../../../config'
|
|
8
|
+
import { removeGitWorktree } from '../../../git/worktree'
|
|
6
9
|
import {
|
|
7
10
|
IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK,
|
|
11
|
+
IPC_CAPABILITY_LIST_TABS,
|
|
8
12
|
IPC_CAPABILITY_THIN_ATTACH,
|
|
13
|
+
IPC_CAPABILITY_WORKER_METADATA,
|
|
9
14
|
} from '../../../ipc/protocol'
|
|
10
15
|
import { createPrefixedId } from '../../../platform/id'
|
|
11
16
|
import {
|
|
@@ -14,13 +19,42 @@ import {
|
|
|
14
19
|
getAllAssistantOptions,
|
|
15
20
|
parseCommand,
|
|
16
21
|
} from '../../../pty/command-registry'
|
|
17
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
22
|
+
import { CliUsageError, SHARED_FLAGS } from '../../flags'
|
|
18
23
|
import { EXIT_OK, writeJson } from '../../output'
|
|
19
24
|
import { createWorkspaceWorktree } from '../worktree/create-core'
|
|
20
25
|
|
|
21
26
|
const FALLBACK_COLS = 200
|
|
22
27
|
const FALLBACK_ROWS = 60
|
|
23
28
|
|
|
29
|
+
export interface CreateCliTabOptions {
|
|
30
|
+
assistantId: string
|
|
31
|
+
base?: string
|
|
32
|
+
branch?: string
|
|
33
|
+
commandOverride?: string
|
|
34
|
+
cwd?: string
|
|
35
|
+
effort?: string
|
|
36
|
+
model?: string
|
|
37
|
+
newWorktree?: boolean | string
|
|
38
|
+
title?: string
|
|
39
|
+
workerName?: string
|
|
40
|
+
worktreeId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateCliTabResult {
|
|
44
|
+
assistant: string
|
|
45
|
+
branch: string | null
|
|
46
|
+
command: string
|
|
47
|
+
cwd: string | null
|
|
48
|
+
effort: string | null
|
|
49
|
+
model: string | null
|
|
50
|
+
name: string | null
|
|
51
|
+
path: string | null
|
|
52
|
+
tabId: string
|
|
53
|
+
title: string
|
|
54
|
+
workerName: string | null
|
|
55
|
+
worktreeId: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
/**
|
|
25
59
|
* Resolve the cwd a spawned tab should run in. Precedence: explicit `--cwd`
|
|
26
60
|
* (resolved to absolute) > the resolved worktree's path > undefined (the
|
|
@@ -50,6 +84,189 @@ export function resolveAssistantCommand(
|
|
|
50
84
|
return commandOverride ?? customCommands[option.id] ?? option.command
|
|
51
85
|
}
|
|
52
86
|
|
|
87
|
+
async function rollbackCreatedWorktree(
|
|
88
|
+
record: WorktreeRecord,
|
|
89
|
+
ctx: CliContext,
|
|
90
|
+
originalError: unknown
|
|
91
|
+
): Promise<never> {
|
|
92
|
+
const daemon = await ctx.getDaemon()
|
|
93
|
+
const workspace = ctx.getWorkspace()
|
|
94
|
+
let rollbackError: unknown
|
|
95
|
+
try {
|
|
96
|
+
await removeGitWorktree({
|
|
97
|
+
force: true,
|
|
98
|
+
repoPath: record.repoRoot,
|
|
99
|
+
targetPath: record.path,
|
|
100
|
+
})
|
|
101
|
+
await daemon.expectOk('removeWorktreeRecord', {
|
|
102
|
+
sessionId: workspace.id,
|
|
103
|
+
worktreeId: record.id,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
rollbackError = error
|
|
107
|
+
}
|
|
108
|
+
const original = originalError instanceof Error ? originalError.message : String(originalError)
|
|
109
|
+
if (rollbackError === undefined) throw originalError
|
|
110
|
+
const rollback =
|
|
111
|
+
rollbackError instanceof Error ? rollbackError.message : JSON.stringify(rollbackError)
|
|
112
|
+
throw new Error(`${original}; rollback failed: ${rollback}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a tab without writing to stdout. Worker commands compose this helper
|
|
117
|
+
* with prompt dispatch/await while `tab create` remains a thin compatibility
|
|
118
|
+
* adapter. All validation and daemon attachment happen before a worktree is
|
|
119
|
+
* created; any later failure rolls the fresh worktree back.
|
|
120
|
+
*/
|
|
121
|
+
export async function createCliTab(
|
|
122
|
+
ctx: CliContext,
|
|
123
|
+
options: CreateCliTabOptions
|
|
124
|
+
): Promise<CreateCliTabResult> {
|
|
125
|
+
const {
|
|
126
|
+
assistantId,
|
|
127
|
+
base,
|
|
128
|
+
branch,
|
|
129
|
+
commandOverride,
|
|
130
|
+
cwd: cwdRaw,
|
|
131
|
+
effort,
|
|
132
|
+
model,
|
|
133
|
+
newWorktree,
|
|
134
|
+
title: requestedTitle,
|
|
135
|
+
workerName,
|
|
136
|
+
worktreeId: requestedWorktreeId,
|
|
137
|
+
} = options
|
|
138
|
+
if (assistantId.length === 0) throw new CliUsageError('--assistant is required')
|
|
139
|
+
if (workerName !== undefined && workerName.trim().length === 0) {
|
|
140
|
+
throw new CliUsageError('--name must be a non-empty string')
|
|
141
|
+
}
|
|
142
|
+
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
143
|
+
throw new CliUsageError(
|
|
144
|
+
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const createFreshWorktree = newWorktree !== undefined && newWorktree !== false
|
|
149
|
+
if (createFreshWorktree && requestedWorktreeId !== undefined) {
|
|
150
|
+
throw new CliUsageError(
|
|
151
|
+
'--new-worktree creates its own; use --worktree <id> to co-locate instead'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
if (createFreshWorktree && cwdRaw !== undefined) {
|
|
155
|
+
throw new CliUsageError('--new-worktree sets the cwd to the new worktree; drop --cwd')
|
|
156
|
+
}
|
|
157
|
+
if (!createFreshWorktree && (base !== undefined || branch !== undefined)) {
|
|
158
|
+
throw new CliUsageError(
|
|
159
|
+
'--base / --branch require --new-worktree (use `worktree create` otherwise)'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Resolve and validate the complete assistant invocation before touching git.
|
|
164
|
+
const { customCommands } = loadConfig()
|
|
165
|
+
const option = getAllAssistantOptions(customCommands).find((entry) => entry.id === assistantId)
|
|
166
|
+
if (!option) {
|
|
167
|
+
const known = getAllAssistantOptions(customCommands)
|
|
168
|
+
.map((entry) => entry.id)
|
|
169
|
+
.join(', ')
|
|
170
|
+
throw new CliUsageError(`unknown assistant: ${assistantId} (known: ${known})`)
|
|
171
|
+
}
|
|
172
|
+
const command = resolveAssistantCommand(commandOverride, customCommands, option)
|
|
173
|
+
const { args: baseArgs, executable } = parseCommand(command)
|
|
174
|
+
const args = [...baseArgs, ...buildAssistantModelArgs(option, { effort, model })]
|
|
175
|
+
const title = requestedTitle ?? option.label
|
|
176
|
+
const tabId = createPrefixedId('tab')
|
|
177
|
+
|
|
178
|
+
const workspace = ctx.getWorkspace()
|
|
179
|
+
const daemon = await ctx.getDaemon()
|
|
180
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (workerName !== undefined && !daemon.hasCapability(IPC_CAPABILITY_WORKER_METADATA)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'daemon predates workerMetadata capability — restart aimux to use worker commands'
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
if (workerName !== undefined) {
|
|
191
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
192
|
+
throw new Error('daemon cannot validate worker-name uniqueness — restart aimux')
|
|
193
|
+
}
|
|
194
|
+
const existing = await daemon.listTabs(workspace.id)
|
|
195
|
+
if (existing.tabs.some((tab) => tab.workerName === workerName)) {
|
|
196
|
+
throw new Error(`worker name already exists in workspace "${workspace.name}": ${workerName}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Attach before creating a worktree so stale daemon/session failures have no
|
|
201
|
+
// git-side effect.
|
|
202
|
+
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
203
|
+
|
|
204
|
+
let worktreeId = requestedWorktreeId ?? workspace.activeWorktreeId
|
|
205
|
+
let worktreeRecord =
|
|
206
|
+
worktreeId !== undefined
|
|
207
|
+
? workspace.worktrees?.find((entry) => entry.id === worktreeId)
|
|
208
|
+
: undefined
|
|
209
|
+
if (requestedWorktreeId !== undefined && worktreeRecord === undefined) {
|
|
210
|
+
const ids = workspace.worktrees?.map((entry) => entry.id).join(', ') ?? '(none)'
|
|
211
|
+
throw new Error(`unknown worktree id: ${requestedWorktreeId} (known: ${ids})`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let createdWorktree: WorktreeRecord | undefined
|
|
215
|
+
if (createFreshWorktree) {
|
|
216
|
+
const worktreeName =
|
|
217
|
+
typeof newWorktree === 'string' && newWorktree !== ''
|
|
218
|
+
? newWorktree
|
|
219
|
+
: `${assistantId}-${tabId.slice(-6)}`
|
|
220
|
+
createdWorktree = await createWorkspaceWorktree({
|
|
221
|
+
base: base ?? 'HEAD',
|
|
222
|
+
branch: branch ?? `aimux/${worktreeName}`,
|
|
223
|
+
daemon,
|
|
224
|
+
name: worktreeName,
|
|
225
|
+
workspace,
|
|
226
|
+
})
|
|
227
|
+
worktreeId = createdWorktree.id
|
|
228
|
+
worktreeRecord = createdWorktree
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const cwd = resolveTabCwd(cwdRaw, worktreeRecord)
|
|
232
|
+
const useFallback = daemon.hasCapability(IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK)
|
|
233
|
+
try {
|
|
234
|
+
await daemon.expectOk('createTab', {
|
|
235
|
+
args,
|
|
236
|
+
assistant: assistantId,
|
|
237
|
+
autoRenameCandidate: requestedTitle === undefined,
|
|
238
|
+
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
239
|
+
command: executable,
|
|
240
|
+
cwd,
|
|
241
|
+
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
242
|
+
tabId,
|
|
243
|
+
title,
|
|
244
|
+
workerName,
|
|
245
|
+
worktreeId,
|
|
246
|
+
})
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (createdWorktree !== undefined) {
|
|
249
|
+
return rollbackCreatedWorktree(createdWorktree, ctx, error)
|
|
250
|
+
}
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
assistant: assistantId,
|
|
256
|
+
branch: worktreeRecord?.branch ?? null,
|
|
257
|
+
command: [executable, ...args].join(' '),
|
|
258
|
+
cwd: cwd ?? null,
|
|
259
|
+
effort: effort ?? null,
|
|
260
|
+
model: model ?? null,
|
|
261
|
+
name: worktreeRecord?.name ?? null,
|
|
262
|
+
path: worktreeRecord?.path ?? null,
|
|
263
|
+
tabId,
|
|
264
|
+
title,
|
|
265
|
+
workerName: workerName ?? null,
|
|
266
|
+
worktreeId: worktreeId ?? null,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
53
270
|
export const tabCreate: CliCommand = {
|
|
54
271
|
args: [],
|
|
55
272
|
flags: [
|
|
@@ -97,143 +314,37 @@ export const tabCreate: CliCommand = {
|
|
|
97
314
|
run: async (ctx) => {
|
|
98
315
|
const assistantId = ctx.args.flags.assistant
|
|
99
316
|
if (typeof assistantId !== 'string' || assistantId.length === 0) {
|
|
100
|
-
throw new
|
|
101
|
-
}
|
|
102
|
-
// Load the workspace's persisted customCommands so CLI-spawned tabs honor
|
|
103
|
-
// the same assistant commands the UI uses (e.g. skip-permissions flags), and
|
|
104
|
-
// so purely-custom assistant ids resolve. loadConfig degrades to {} on a
|
|
105
|
-
// missing/invalid config — no new failure mode.
|
|
106
|
-
const { customCommands } = loadConfig()
|
|
107
|
-
const options = getAllAssistantOptions(customCommands)
|
|
108
|
-
const option = options.find((o) => o.id === assistantId)
|
|
109
|
-
if (!option) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`unknown assistant: ${assistantId} (known: ${options.map((o) => o.id).join(', ')})`
|
|
112
|
-
)
|
|
317
|
+
throw new CliUsageError('--assistant is required')
|
|
113
318
|
}
|
|
114
319
|
const commandOverride =
|
|
115
320
|
typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : undefined
|
|
116
321
|
const model = typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined
|
|
117
322
|
const effort = typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined
|
|
118
323
|
|
|
119
|
-
|
|
120
|
-
// `--effort` (which only make sense as additions to the assistant default)
|
|
121
|
-
// would be ambiguous alongside it — reject rather than silently drop them.
|
|
122
|
-
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const command = resolveAssistantCommand(commandOverride, customCommands, option)
|
|
129
|
-
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : option.label
|
|
324
|
+
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : undefined
|
|
130
325
|
const cwdRaw = typeof ctx.args.flags.cwd === 'string' ? ctx.args.flags.cwd : undefined
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const newWorktree = newWorktreeFlag !== undefined
|
|
326
|
+
const newWorktreeRaw = ctx.args.flags['new-worktree']
|
|
327
|
+
const newWorktreeFlag =
|
|
328
|
+
typeof newWorktreeRaw === 'string' || typeof newWorktreeRaw === 'boolean'
|
|
329
|
+
? newWorktreeRaw
|
|
330
|
+
: undefined
|
|
137
331
|
const worktreeFlag =
|
|
138
332
|
typeof ctx.args.flags.worktree === 'string' ? ctx.args.flags.worktree : undefined
|
|
139
333
|
const baseFlag = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : undefined
|
|
140
334
|
const branchFlag = typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : undefined
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const workspace = ctx.getWorkspace()
|
|
152
|
-
const daemon = await ctx.getDaemon()
|
|
153
|
-
|
|
154
|
-
if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Resolve the worktree the tab belongs to + its record (for the cwd default
|
|
161
|
-
// and the output). Three modes: create a fresh one, use an explicit id, or
|
|
162
|
-
// fall back to the workspace's active worktree.
|
|
163
|
-
let worktreeId: string | undefined
|
|
164
|
-
let worktreeRecord: { branch?: string; name?: string; path: string } | undefined
|
|
165
|
-
if (newWorktree) {
|
|
166
|
-
const worktreeName =
|
|
167
|
-
typeof newWorktreeFlag === 'string' && newWorktreeFlag !== ''
|
|
168
|
-
? newWorktreeFlag
|
|
169
|
-
: `${assistantId}-${tabId.slice(-6)}`
|
|
170
|
-
const record = await createWorkspaceWorktree({
|
|
171
|
-
base: baseFlag ?? 'HEAD',
|
|
172
|
-
branch: branchFlag ?? `aimux/${worktreeName}`,
|
|
173
|
-
daemon,
|
|
174
|
-
name: worktreeName,
|
|
175
|
-
workspace,
|
|
176
|
-
})
|
|
177
|
-
worktreeId = record.id
|
|
178
|
-
worktreeRecord = record
|
|
179
|
-
} else {
|
|
180
|
-
worktreeId = worktreeFlag ?? workspace.activeWorktreeId
|
|
181
|
-
if (worktreeFlag !== undefined) {
|
|
182
|
-
const known = workspace.worktrees?.some((w) => w.id === worktreeFlag) ?? false
|
|
183
|
-
if (!known) {
|
|
184
|
-
const ids = workspace.worktrees?.map((w) => w.id).join(', ') ?? '(none)'
|
|
185
|
-
throw new Error(`unknown worktree id: ${worktreeFlag} (known: ${ids})`)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
worktreeRecord =
|
|
189
|
-
worktreeId !== undefined ? workspace.worktrees?.find((w) => w.id === worktreeId) : undefined
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Default the PTY cwd to the resolved worktree's path so a worker spawned
|
|
193
|
-
// into a worktree actually runs inside it. An explicit --cwd always wins.
|
|
194
|
-
const cwd = resolveTabCwd(cwdRaw, worktreeRecord)
|
|
195
|
-
|
|
196
|
-
// Thin-attach so we don't clobber the UI's dimensions on the same session.
|
|
197
|
-
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
198
|
-
|
|
199
|
-
const { args: baseArgs, executable } = parseCommand(command)
|
|
200
|
-
// Append the model/effort flags to the assistant default. Throws if the
|
|
201
|
-
// assistant has no control for a requested dimension.
|
|
202
|
-
const modelArgs = buildAssistantModelArgs(option, { effort, model })
|
|
203
|
-
const args = [...baseArgs, ...modelArgs]
|
|
204
|
-
|
|
205
|
-
// cols/rows = 0 means "fall back to the session's last attached size" on
|
|
206
|
-
// v11 daemons. Without that capability we have nothing reasonable to put
|
|
207
|
-
// here (the CLI has no terminal of its own), so use a roomy fallback —
|
|
208
|
-
// PTYs are reflowable, so 200×60 won't break anything that adapts.
|
|
209
|
-
const useFallback = daemon.hasCapability(IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK)
|
|
210
|
-
await daemon.expectOk('createTab', {
|
|
211
|
-
args,
|
|
212
|
-
assistant: assistantId,
|
|
213
|
-
autoRenameCandidate: ctx.args.flags.title === undefined,
|
|
214
|
-
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
215
|
-
command: executable,
|
|
216
|
-
cwd,
|
|
217
|
-
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
218
|
-
tabId,
|
|
219
|
-
title,
|
|
220
|
-
worktreeId,
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
const resolvedCommand = [executable, ...args].join(' ')
|
|
224
|
-
writeJson({
|
|
225
|
-
assistant: assistantId,
|
|
226
|
-
branch: worktreeRecord?.branch ?? null,
|
|
227
|
-
command: resolvedCommand,
|
|
228
|
-
cwd: cwd ?? null,
|
|
229
|
-
effort: effort ?? null,
|
|
230
|
-
model: model ?? null,
|
|
231
|
-
name: worktreeRecord?.name ?? null,
|
|
232
|
-
path: worktreeRecord?.path ?? null,
|
|
233
|
-
tabId,
|
|
335
|
+
const result = await createCliTab(ctx, {
|
|
336
|
+
assistantId,
|
|
337
|
+
base: baseFlag,
|
|
338
|
+
branch: branchFlag,
|
|
339
|
+
commandOverride,
|
|
340
|
+
cwd: cwdRaw,
|
|
341
|
+
effort,
|
|
342
|
+
model,
|
|
343
|
+
newWorktree: newWorktreeFlag,
|
|
234
344
|
title,
|
|
235
|
-
worktreeId:
|
|
345
|
+
worktreeId: worktreeFlag,
|
|
236
346
|
})
|
|
347
|
+
writeJson(result)
|
|
237
348
|
return EXIT_OK
|
|
238
349
|
},
|
|
239
350
|
summary: 'Create a new tab in the active workspace',
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { DaemonClient } from '../../client/daemon-client'
|
|
7
7
|
|
|
8
8
|
import { bracketedPaste, notationToBytes } from '../../chord'
|
|
9
|
+
import { CliUsageError } from '../../flags'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Gap between the bracketed-paste write and the trailing carriage return when
|
|
@@ -33,7 +34,7 @@ export async function resolvePromptText(
|
|
|
33
34
|
(present) => present
|
|
34
35
|
).length
|
|
35
36
|
if (sources !== 1) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new CliUsageError(
|
|
37
38
|
'provide exactly one prompt source: --prompt-file <f>, --stdin, or a [text] positional'
|
|
38
39
|
)
|
|
39
40
|
}
|