@brimveyn/aimux 1.19.6 → 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.
Files changed (42) hide show
  1. package/README.md +6 -0
  2. package/package.json +3 -2
  3. package/skills/aimux-orchestrator/SKILL.md +93 -0
  4. package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
  5. package/skills/aimux-orchestrator/references/prompts.md +57 -0
  6. package/skills/aimux-orchestrator/references/review.md +18 -0
  7. package/src/app-runtime/backend-runtime-events.ts +16 -0
  8. package/src/app-runtime/side-effects.ts +15 -2
  9. package/src/auto-rename/coordinator.ts +97 -0
  10. package/src/auto-rename/prompt-capture.ts +211 -0
  11. package/src/auto-rename/title-runner.ts +96 -0
  12. package/src/cli/client/daemon-client.ts +16 -0
  13. package/src/cli/commands/tab/create.ts +236 -124
  14. package/src/cli/commands/tab/prompt-io.ts +2 -1
  15. package/src/cli/commands/worker/await.ts +33 -0
  16. package/src/cli/commands/worker/doctor.ts +129 -0
  17. package/src/cli/commands/worker/list.ts +25 -0
  18. package/src/cli/commands/worker/prompt.ts +49 -0
  19. package/src/cli/commands/worker/run.ts +97 -0
  20. package/src/cli/commands/worker/shared.ts +255 -0
  21. package/src/cli/commands/worker/stop.ts +84 -0
  22. package/src/cli/commands/worktree/remove.ts +29 -9
  23. package/src/cli/index.ts +21 -9
  24. package/src/cli/registry.ts +12 -0
  25. package/src/daemon/daemon.ts +173 -12
  26. package/src/daemon/session-manager.ts +8 -0
  27. package/src/daemon/session-registry.ts +22 -1
  28. package/src/git/worktree.ts +8 -0
  29. package/src/index.tsx +21 -82
  30. package/src/input/modes/types.ts +1 -0
  31. package/src/ipc/manager-protocol.ts +52 -2
  32. package/src/ipc/protocol.ts +74 -3
  33. package/src/session-backend/bootstrap.ts +3 -1
  34. package/src/session-backend/local-session-backend.ts +63 -2
  35. package/src/session-backend/remote-session-backend.ts +17 -0
  36. package/src/session-backend/types.ts +7 -0
  37. package/src/state/reducers/tab-state.ts +14 -1
  38. package/src/state/session-persistence.ts +4 -0
  39. package/src/state/types.ts +18 -1
  40. package/src/state/validation.ts +5 -1
  41. package/src/terminal-manager/manager-client.ts +33 -1
  42. package/src/terminal-manager/terminal-manager.ts +9 -0
@@ -0,0 +1,96 @@
1
+ import { buildHeadlessInvocation, type HeadlessInvocation } from '../auto-commit/headless-commands'
2
+
3
+ export type TitleSpawnFn = (
4
+ invocation: HeadlessInvocation,
5
+ signal: AbortSignal
6
+ ) => Promise<{ stdout: string; exitCode: number } | null>
7
+
8
+ export function buildTitlePrompt(firstPrompt: string): string {
9
+ return [
10
+ 'Create a concise tab title for the user request below.',
11
+ 'Return only the title: 2 to 6 words, at most 48 characters, in the same language as the request.',
12
+ 'Do not use quotes, a label, markdown, or ending punctuation.',
13
+ '',
14
+ firstPrompt.slice(0, 8_000),
15
+ ].join('\n')
16
+ }
17
+
18
+ export function sanitizeGeneratedTitle(raw: string): string | null {
19
+ const first = raw
20
+ .split(/\r?\n/u)
21
+ .map((line) => line.trim())
22
+ .find(Boolean)
23
+ if (first == null || first === '') return null
24
+
25
+ const unlabelled = first.replace(/^TITLE\s*:\s*/iu, '').replaceAll(/^["'“”‘’]+|["'“”‘’]+$/gu, '')
26
+ const clean = unlabelled
27
+ .replaceAll(/\s+/gu, ' ')
28
+ .replace(/[.!?,;:…]+$/u, '')
29
+ .trim()
30
+ const words = clean.split(' ').filter(Boolean)
31
+ const usesUnspacedScript = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(clean)
32
+ if (words.length < 2 && !usesUnspacedScript) return null
33
+
34
+ let title = words.slice(0, 6).join(' ')
35
+ if (title.length > 48) {
36
+ title = title
37
+ .slice(0, 48)
38
+ .replace(/\s+\S*$/u, '')
39
+ .trim()
40
+ }
41
+ return title === '' || (title.split(' ').filter(Boolean).length < 2 && !usesUnspacedScript)
42
+ ? null
43
+ : title
44
+ }
45
+
46
+ export async function generateTabTitle(options: {
47
+ provider: string
48
+ model?: string
49
+ firstPrompt: string
50
+ timeoutMs: number
51
+ signal: AbortSignal
52
+ spawn?: TitleSpawnFn
53
+ }): Promise<string | null> {
54
+ const invocation = buildHeadlessInvocation(
55
+ options.provider,
56
+ buildTitlePrompt(options.firstPrompt),
57
+ options.model
58
+ )
59
+ if (!invocation) return null
60
+
61
+ const signal = AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs)])
62
+ try {
63
+ const result = await (options.spawn ?? defaultSpawn)(invocation, signal)
64
+ if (!result || result.exitCode !== 0 || signal.aborted) return null
65
+ return sanitizeGeneratedTitle(result.stdout)
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ async function defaultSpawn(
72
+ invocation: HeadlessInvocation,
73
+ signal: AbortSignal
74
+ ): Promise<{ stdout: string; exitCode: number } | null> {
75
+ try {
76
+ const proc = Bun.spawn([invocation.executable, ...invocation.args], {
77
+ stderr: 'ignore',
78
+ stdin: 'ignore',
79
+ stdout: 'pipe',
80
+ })
81
+ const abort = () => {
82
+ try {
83
+ proc.kill()
84
+ } catch {
85
+ // Best effort: the process may already have exited.
86
+ }
87
+ }
88
+ signal.addEventListener('abort', abort, { once: true })
89
+ const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
90
+ signal.removeEventListener('abort', abort)
91
+ if (signal.aborted) return null
92
+ return { exitCode: exitCode ?? 1, stdout }
93
+ } catch {
94
+ return null
95
+ }
96
+ }
@@ -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,142 +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
- cols: useFallback ? 0 : FALLBACK_COLS,
214
- command: executable,
215
- cwd,
216
- rows: useFallback ? 0 : FALLBACK_ROWS,
217
- tabId,
218
- title,
219
- worktreeId,
220
- })
221
-
222
- const resolvedCommand = [executable, ...args].join(' ')
223
- writeJson({
224
- assistant: assistantId,
225
- branch: worktreeRecord?.branch ?? null,
226
- command: resolvedCommand,
227
- cwd: cwd ?? null,
228
- effort: effort ?? null,
229
- model: model ?? null,
230
- name: worktreeRecord?.name ?? null,
231
- path: worktreeRecord?.path ?? null,
232
- 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,
233
344
  title,
234
- worktreeId: worktreeId ?? null,
345
+ worktreeId: worktreeFlag,
235
346
  })
347
+ writeJson(result)
236
348
  return EXIT_OK
237
349
  },
238
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
  }
@@ -0,0 +1,33 @@
1
+ import type { CliCommand } from '../../registry'
2
+
3
+ import { SHARED_FLAGS } from '../../flags'
4
+ import { writeJson } from '../../output'
5
+ import { DEFAULT_TIMEOUT_MS } from '../tab/await-turn'
6
+ import {
7
+ awaitExistingWorker,
8
+ resolveWorkerTab,
9
+ workerEnvelope,
10
+ workerOutcomeExitCode,
11
+ workerView,
12
+ } from './shared'
13
+
14
+ export const workerAwait: CliCommand = {
15
+ args: [{ name: 'worker', required: true }],
16
+ flags: [
17
+ ...SHARED_FLAGS,
18
+ { description: 'overall turn cap in milliseconds', kind: 'number', name: 'timeout' },
19
+ ],
20
+ group: 'worker',
21
+ run: async (ctx) => {
22
+ const tab = await resolveWorkerTab(ctx, ctx.args.positionals[0] ?? '')
23
+ const outcome = await awaitExistingWorker(
24
+ ctx,
25
+ tab.id,
26
+ typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS
27
+ )
28
+ writeJson(workerEnvelope(workerView(ctx, tab), outcome))
29
+ return workerOutcomeExitCode(outcome)
30
+ },
31
+ summary: "Await an existing worker's in-flight turn",
32
+ verb: 'await',
33
+ }