@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 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.19.7",
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.4",
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 Error('--assistant is required')
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
- // A full `--command` override owns the whole invocation, so `--model` /
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 tabId = createPrefixedId('tab')
132
-
133
- // `--new-worktree[=<name>]`: create a fresh worktree and run the tab in it.
134
- // A bare flag parses as `true` (name derived below); the `=` form names it.
135
- const newWorktreeFlag = ctx.args.flags['new-worktree']
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
- if (newWorktree && worktreeFlag !== undefined) {
142
- throw new Error('--new-worktree creates its own; use --worktree <id> to co-locate instead')
143
- }
144
- if (newWorktree && cwdRaw !== undefined) {
145
- throw new Error('--new-worktree sets the cwd to the new worktree; drop --cwd')
146
- }
147
- if (!newWorktree && (baseFlag !== undefined || branchFlag !== undefined)) {
148
- throw new Error('--base / --branch require --new-worktree (use `worktree create` otherwise)')
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: worktreeId ?? null,
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 Error(
37
+ throw new CliUsageError(
37
38
  'provide exactly one prompt source: --prompt-file <f>, --stdin, or a [text] positional'
38
39
  )
39
40
  }