@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
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { CliCommand } from '../../registry'
|
|
2
2
|
|
|
3
3
|
import { removeGitWorktree } from '../../../git/worktree'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
IPC_CAPABILITY_LIST_TABS,
|
|
6
|
+
IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS,
|
|
7
|
+
} from '../../../ipc/protocol'
|
|
5
8
|
import { SHARED_FLAGS } from '../../flags'
|
|
6
9
|
import { EXIT_OK, writeJson } from '../../output'
|
|
7
10
|
|
|
@@ -36,20 +39,37 @@ export const worktreeRemove: CliCommand = {
|
|
|
36
39
|
throw new Error('workspace has no primary worktree — cannot resolve repoRoot for git remove')
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
// Git side first — matches the UI's discipline in side-effects.ts. If
|
|
40
|
-
// git refuses (dirty, uncommitted changes) the catalog stays intact.
|
|
41
|
-
await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: worktree.path })
|
|
42
|
-
|
|
43
42
|
const daemon = await ctx.getDaemon()
|
|
44
43
|
if (!daemon.hasCapability(IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS)) {
|
|
45
44
|
throw new Error(
|
|
46
45
|
'daemon predates worktreeLifecycleEvents capability — restart aimux to pick up the new daemon'
|
|
47
46
|
)
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
49
|
+
const live = (await daemon.listTabs(workspace.id)).tabs.filter(
|
|
50
|
+
(tab) => tab.worktreeId === worktree.id
|
|
51
|
+
)
|
|
52
|
+
if (live.length > 0) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`refusing to remove worktree with live tabs: ${live.map((tab) => tab.id).join(', ')}`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// All capability and liveness checks happen before touching git. If git
|
|
60
|
+
// refuses a dirty worktree the catalog remains unchanged.
|
|
61
|
+
await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: worktree.path })
|
|
62
|
+
try {
|
|
63
|
+
await daemon.expectOk('removeWorktreeRecord', {
|
|
64
|
+
sessionId: workspace.id,
|
|
65
|
+
worktreeId: worktree.id,
|
|
66
|
+
})
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
69
|
+
throw new Error(
|
|
70
|
+
`worktree removed from git but catalog reconciliation failed for ${worktree.id}: ${message}`
|
|
71
|
+
)
|
|
72
|
+
}
|
|
53
73
|
|
|
54
74
|
writeJson({ id: worktree.id, name: worktree.name, path: worktree.path })
|
|
55
75
|
return EXIT_OK
|
package/src/cli/index.ts
CHANGED
|
@@ -64,9 +64,10 @@ function formatFlagLine(flag: FlagSpec): string {
|
|
|
64
64
|
function printHelp(): void {
|
|
65
65
|
process.stdout.write(
|
|
66
66
|
[
|
|
67
|
-
'aimux
|
|
67
|
+
'aimux — terminal multiplexer and agent-friendly control plane.',
|
|
68
68
|
'',
|
|
69
69
|
'Usage:',
|
|
70
|
+
' aimux Start the interactive TUI',
|
|
70
71
|
' aimux <group> <verb> [flags] [args]',
|
|
71
72
|
' aimux <group> --help List verbs in a group',
|
|
72
73
|
' aimux <group> <verb> --help Show flags/args for a verb',
|
|
@@ -99,15 +100,12 @@ function printHelp(): void {
|
|
|
99
100
|
'Env:',
|
|
100
101
|
' AIMUX_PROFILE Runtime profile (state dir, socket paths); --profile overrides.',
|
|
101
102
|
'',
|
|
102
|
-
'Agent
|
|
103
|
-
' #
|
|
104
|
-
'
|
|
105
|
-
' aimux tab send "$TAB" "explain this repo" --enter',
|
|
106
|
-
' aimux tab wait "$TAB" --status idle --timeout 60000',
|
|
107
|
-
' aimux tab snapshot "$TAB" --tail 40 --format text',
|
|
103
|
+
'Agent recipe:',
|
|
104
|
+
' # create an isolated named worker, dispatch, and await one structured outcome',
|
|
105
|
+
' aimux worker run --name fixup --assistant claude "explain this repo"',
|
|
108
106
|
'',
|
|
109
|
-
'
|
|
110
|
-
' aimux
|
|
107
|
+
'Maintenance:',
|
|
108
|
+
' aimux doctor | update | restart-daemon | restart-terminal-manager | version',
|
|
111
109
|
'',
|
|
112
110
|
].join('\n')
|
|
113
111
|
)
|
|
@@ -201,6 +199,11 @@ export async function runCli(argv: readonly string[]): Promise<number> {
|
|
|
201
199
|
if (error instanceof CliUsageError) {
|
|
202
200
|
writeError(error.message)
|
|
203
201
|
writeError(`usage: aimux ${command.group} ${command.verb}`)
|
|
202
|
+
writeJson({
|
|
203
|
+
command: `${command.group} ${command.verb}`,
|
|
204
|
+
error: error.message,
|
|
205
|
+
kind: 'usage-error',
|
|
206
|
+
})
|
|
204
207
|
return EXIT_USAGE
|
|
205
208
|
}
|
|
206
209
|
throw error
|
|
@@ -241,6 +244,15 @@ export async function runCli(argv: readonly string[]): Promise<number> {
|
|
|
241
244
|
return code
|
|
242
245
|
} catch (error) {
|
|
243
246
|
const message = error instanceof Error ? error.message : String(error)
|
|
247
|
+
if (error instanceof CliUsageError) {
|
|
248
|
+
writeError(message)
|
|
249
|
+
writeJson({
|
|
250
|
+
command: `${command.group} ${command.verb}`,
|
|
251
|
+
error: message,
|
|
252
|
+
kind: 'usage-error',
|
|
253
|
+
})
|
|
254
|
+
return EXIT_USAGE
|
|
255
|
+
}
|
|
244
256
|
// Classify by error type, not by string-sniffing the message: a runtime
|
|
245
257
|
// error whose message happens to include "socket" (e.g. daemon reply
|
|
246
258
|
// "socket write failed for tab X") must not masquerade as
|
package/src/cli/registry.ts
CHANGED
|
@@ -11,6 +11,12 @@ import { tabSend } from './commands/tab/send'
|
|
|
11
11
|
import { tabSnapshot } from './commands/tab/snapshot'
|
|
12
12
|
import { tabTail } from './commands/tab/tail'
|
|
13
13
|
import { tabWait } from './commands/tab/wait'
|
|
14
|
+
import { workerAwait } from './commands/worker/await'
|
|
15
|
+
import { workerDoctor } from './commands/worker/doctor'
|
|
16
|
+
import { workerList } from './commands/worker/list'
|
|
17
|
+
import { workerPrompt } from './commands/worker/prompt'
|
|
18
|
+
import { workerRun } from './commands/worker/run'
|
|
19
|
+
import { workerStop } from './commands/worker/stop'
|
|
14
20
|
import { workspaceClose } from './commands/workspace/close'
|
|
15
21
|
import { workspaceCreate } from './commands/workspace/create'
|
|
16
22
|
import { workspaceList } from './commands/workspace/list'
|
|
@@ -48,6 +54,12 @@ export const COMMANDS: readonly CliCommand[] = [
|
|
|
48
54
|
worktreeList,
|
|
49
55
|
worktreeCreate,
|
|
50
56
|
worktreeRemove,
|
|
57
|
+
workerRun,
|
|
58
|
+
workerPrompt,
|
|
59
|
+
workerAwait,
|
|
60
|
+
workerList,
|
|
61
|
+
workerStop,
|
|
62
|
+
workerDoctor,
|
|
51
63
|
]
|
|
52
64
|
|
|
53
65
|
export function resolveCommand(group: string, verb: string): CliCommand | null {
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { connect, createServer, type Socket } from 'node:net'
|
|
|
3
3
|
|
|
4
4
|
import type { AssistantId, TabSession, TabStatus, TerminalSnapshot } from '../state/types'
|
|
5
5
|
|
|
6
|
+
import { version as APP_VERSION } from '../../package.json'
|
|
6
7
|
import { AutoRenameCoordinator, initialAutoRenameStatus } from '../auto-rename/coordinator'
|
|
7
8
|
import { loadUserConfig } from '../config/loader'
|
|
8
9
|
import { logDebug } from '../debug/input-log'
|
|
9
10
|
import { type ClaudeHookServer, startClaudeHookServer } from '../integrations/claude-hook-server'
|
|
11
|
+
import { MANAGER_CAPABILITY_WORKER_METADATA } from '../ipc/manager-protocol'
|
|
10
12
|
import {
|
|
11
13
|
type ClientRequest,
|
|
12
14
|
encodeMessage,
|
|
@@ -72,6 +74,7 @@ export interface DaemonTabEntry {
|
|
|
72
74
|
title?: string
|
|
73
75
|
status?: TabStatus
|
|
74
76
|
worktreeId?: string
|
|
77
|
+
workerName?: string
|
|
75
78
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -98,6 +101,7 @@ export function mergeTabRegistryEntry(
|
|
|
98
101
|
title?: string
|
|
99
102
|
status?: TabStatus
|
|
100
103
|
worktreeId?: string
|
|
104
|
+
workerName?: string
|
|
101
105
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
102
106
|
}
|
|
103
107
|
): DaemonTabEntry {
|
|
@@ -115,12 +119,24 @@ export function mergeTabRegistryEntry(
|
|
|
115
119
|
title: preserveAttemptedMetadata ? existing.title : (metadata?.title ?? existing?.title),
|
|
116
120
|
viewport,
|
|
117
121
|
viewportSeq: existing?.viewportSeq ?? (viewport ? allocateSeq() : 0),
|
|
122
|
+
workerName: metadata?.workerName ?? existing?.workerName,
|
|
118
123
|
worktreeId: metadata?.worktreeId ?? existing?.worktreeId,
|
|
119
124
|
}
|
|
120
125
|
registry.set(tabId, entry)
|
|
121
126
|
return entry
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
export function findWorkerNameConflict(
|
|
130
|
+
registry: ReadonlyMap<string, DaemonTabEntry>,
|
|
131
|
+
sessionId: string,
|
|
132
|
+
workerName: string
|
|
133
|
+
): string | undefined {
|
|
134
|
+
for (const [tabId, entry] of registry) {
|
|
135
|
+
if (entry.sessionId === sessionId && entry.workerName === workerName) return tabId
|
|
136
|
+
}
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
/**
|
|
125
141
|
* Turn-complete settle window for the status loop, overridable via
|
|
126
142
|
* `AIMUX_TURN_SETTLE_MS` for slow/loaded machines. Falls back to the loop's
|
|
@@ -264,6 +280,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
264
280
|
title?: string
|
|
265
281
|
status?: TabStatus
|
|
266
282
|
worktreeId?: string
|
|
283
|
+
workerName?: string
|
|
267
284
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
268
285
|
}
|
|
269
286
|
): DaemonTabEntry => {
|
|
@@ -589,7 +606,9 @@ export async function runDaemon(): Promise<void> {
|
|
|
589
606
|
send(socket, {
|
|
590
607
|
id: message.id,
|
|
591
608
|
payload: {
|
|
609
|
+
appVersion: APP_VERSION,
|
|
592
610
|
capabilities: [...IPC_PROTOCOL_CAPABILITIES],
|
|
611
|
+
managerCapabilities: [...manager.getCapabilities()],
|
|
593
612
|
...(managerSelectedVersion !== null && { managerSelectedVersion }),
|
|
594
613
|
maxVersion: IPC_PROTOCOL_VERSION,
|
|
595
614
|
minVersion: IPC_PROTOCOL_MIN_VERSION,
|
|
@@ -680,6 +699,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
680
699
|
autoRenameStatus: tab.autoRenameStatus,
|
|
681
700
|
status: tab.status,
|
|
682
701
|
title: tab.title,
|
|
702
|
+
workerName: tab.workerName,
|
|
683
703
|
worktreeId: tab.worktreeId,
|
|
684
704
|
}
|
|
685
705
|
)
|
|
@@ -714,6 +734,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
714
734
|
activity: statusLoop.getTabStatus(tab.id) ?? tab.activity,
|
|
715
735
|
autoRenameStatus: metadata?.autoRenameStatus ?? tab.autoRenameStatus,
|
|
716
736
|
title: metadata?.title ?? tab.title,
|
|
737
|
+
workerName: metadata?.workerName ?? tab.workerName,
|
|
717
738
|
}
|
|
718
739
|
})
|
|
719
740
|
const initialSessionStatuses = statusLoop.snapshotSessions()
|
|
@@ -742,6 +763,23 @@ export async function runDaemon(): Promise<void> {
|
|
|
742
763
|
case 'createTab': {
|
|
743
764
|
const sessionId = requireSession(socket, attachedSessions)
|
|
744
765
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
766
|
+
if (message.payload.workerName !== undefined) {
|
|
767
|
+
if (!manager.hasCapability(MANAGER_CAPABILITY_WORKER_METADATA)) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
'the running terminal manager does not support worker metadata; restart aimux before creating named workers'
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
const conflictTabId = findWorkerNameConflict(
|
|
773
|
+
tabRegistry,
|
|
774
|
+
sessionId,
|
|
775
|
+
message.payload.workerName
|
|
776
|
+
)
|
|
777
|
+
if (conflictTabId !== undefined) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
`worker name already exists in this workspace: ${message.payload.workerName} (${conflictTabId})`
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
745
783
|
// Capability `createTabSizeFallback`: cols/rows = 0 means
|
|
746
784
|
// "use the session's last attached dimensions". Headless
|
|
747
785
|
// CLIs don't have a viewport of their own, so this lets
|
|
@@ -789,6 +827,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
789
827
|
autoRenameStatus,
|
|
790
828
|
status: 'starting',
|
|
791
829
|
title: message.payload.title,
|
|
830
|
+
workerName: message.payload.workerName,
|
|
792
831
|
worktreeId: message.payload.worktreeId,
|
|
793
832
|
}
|
|
794
833
|
)
|
|
@@ -802,14 +841,23 @@ export async function runDaemon(): Promise<void> {
|
|
|
802
841
|
if (hookServer) env.AIMUX_HOOK_URL_FILE = hookUrlFilePath
|
|
803
842
|
const { autoRenameCandidate: _autoRenameCandidate, ...managerTabPayload } =
|
|
804
843
|
message.payload
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
844
|
+
try {
|
|
845
|
+
await manager.createTab({
|
|
846
|
+
...managerTabPayload,
|
|
847
|
+
autoRenameStatus,
|
|
848
|
+
cols,
|
|
849
|
+
env,
|
|
850
|
+
rows,
|
|
851
|
+
sessionId,
|
|
852
|
+
})
|
|
853
|
+
} catch (error) {
|
|
854
|
+
// `rememberTab` must happen before the manager call so
|
|
855
|
+
// early render events have metadata to merge into. Undo
|
|
856
|
+
// that optimistic entry when creation fails, otherwise a
|
|
857
|
+
// ghost worker name would block a clean retry.
|
|
858
|
+
tabRegistry.delete(message.payload.tabId)
|
|
859
|
+
throw error
|
|
860
|
+
}
|
|
813
861
|
sendOk(socket, message.id)
|
|
814
862
|
// Fan a `tabAdded` event only to peers that negotiated at
|
|
815
863
|
// least v11 — older parsers throw on unknown message types
|
|
@@ -825,6 +873,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
825
873
|
status: 'starting',
|
|
826
874
|
terminalModes: createDefaultTerminalModes(),
|
|
827
875
|
title: message.payload.title,
|
|
876
|
+
workerName: message.payload.workerName,
|
|
828
877
|
worktreeId: message.payload.worktreeId,
|
|
829
878
|
}
|
|
830
879
|
broadcastForSessionVersioned(sessionId, 11, {
|
|
@@ -940,6 +989,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
940
989
|
lastLine: lastNonBlankLine(entry.viewport),
|
|
941
990
|
status: entry.status ?? 'running',
|
|
942
991
|
title: entry.title ?? '',
|
|
992
|
+
workerName: entry.workerName,
|
|
943
993
|
worktreeId: entry.worktreeId,
|
|
944
994
|
})
|
|
945
995
|
}
|
|
@@ -94,6 +94,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
94
94
|
existing.autoRenameStatus = persisted.autoRenameStatus
|
|
95
95
|
}
|
|
96
96
|
existing.worktreeId = persisted.worktreeId
|
|
97
|
+
existing.workerName = persisted.workerName
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
if (
|
|
@@ -155,6 +156,8 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
155
156
|
* worktree column. Optional — tabs not bound to a worktree are valid.
|
|
156
157
|
*/
|
|
157
158
|
worktreeId?: string
|
|
159
|
+
/** Workspace-scoped orchestration handle; does not affect PTY behavior. */
|
|
160
|
+
workerName?: string
|
|
158
161
|
}): void {
|
|
159
162
|
logDebug('daemon.registry.createSession', {
|
|
160
163
|
args: options.args ?? [],
|
|
@@ -175,6 +178,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
175
178
|
status: 'starting',
|
|
176
179
|
terminalModes: createDefaultTerminalModes(),
|
|
177
180
|
title: options.title,
|
|
181
|
+
workerName: options.workerName,
|
|
178
182
|
worktreeId: options.worktreeId,
|
|
179
183
|
})
|
|
180
184
|
} else {
|
|
@@ -189,6 +193,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
189
193
|
existing.command = [options.command, ...(options.args ?? [])].join(' ')
|
|
190
194
|
existing.autoRenameStatus = options.autoRenameStatus
|
|
191
195
|
if (options.worktreeId !== undefined) existing.worktreeId = options.worktreeId
|
|
196
|
+
if (options.workerName !== undefined) existing.workerName = options.workerName
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
this.activeTabId = options.tabId
|
package/src/git/worktree.ts
CHANGED
|
@@ -33,6 +33,14 @@ export async function getHeadSha(cwd: string): Promise<string | undefined> {
|
|
|
33
33
|
return result.text().trim() || undefined
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export async function isGitWorktreeDirty(cwd: string): Promise<boolean> {
|
|
37
|
+
const result = await $`git -C ${cwd} status --porcelain`.quiet().nothrow()
|
|
38
|
+
if (result.exitCode !== 0) {
|
|
39
|
+
throw new Error(result.stderr.toString().trim() || `failed to inspect worktree: ${cwd}`)
|
|
40
|
+
}
|
|
41
|
+
return result.text().trim().length > 0
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// Local branch names, ordered most-recently-committed first so the likely base
|
|
37
45
|
// surfaces near the top of the picker.
|
|
38
46
|
export async function listLocalBranches(cwd: string): Promise<string[]> {
|
package/src/index.tsx
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { createCliRenderer } from '@opentui/core'
|
|
3
|
-
import { createRoot } from '@opentui/react'
|
|
4
|
-
|
|
5
|
-
import { App } from './app'
|
|
6
|
-
import { loadUserConfig } from './config/loader'
|
|
7
2
|
import { runDaemon } from './daemon/daemon'
|
|
8
3
|
import { getRuntimeProfile } from './daemon/runtime-paths'
|
|
9
4
|
import { logDebug } from './debug/input-log'
|
|
@@ -12,8 +7,6 @@ import { runRestartDaemon } from './restart-daemon'
|
|
|
12
7
|
import { runRestartTerminalManager } from './restart-terminal-manager'
|
|
13
8
|
import { createSessionBackend } from './session-backend/bootstrap'
|
|
14
9
|
import { runTerminalManager } from './terminal-manager/terminal-manager'
|
|
15
|
-
import { BreakingUpdateScreen } from './ui/breaking-update-screen'
|
|
16
|
-
import { setHostPalette } from './ui/host-palette'
|
|
17
10
|
import { runUpdate } from './update'
|
|
18
11
|
|
|
19
12
|
const command = process.argv[2]
|
|
@@ -22,7 +15,7 @@ const runtimeProfile = getRuntimeProfile()
|
|
|
22
15
|
// CLI control plane (docs/reference/cli.md). Branch BEFORE the UI bootstrap so
|
|
23
16
|
// `aimux tab list` from a non-TTY shell never spins up the React renderer.
|
|
24
17
|
// Dynamic import keeps the CLI code out of the UI's cold-start cost.
|
|
25
|
-
const CLI_GROUPS = new Set(['tab', 'workspace', 'worktree'])
|
|
18
|
+
const CLI_GROUPS = new Set(['tab', 'workspace', 'worktree', 'worker'])
|
|
26
19
|
if (typeof command === 'string' && CLI_GROUPS.has(command)) {
|
|
27
20
|
const { runCli } = await import('./cli')
|
|
28
21
|
process.exit(await runCli(process.argv.slice(2)))
|
|
@@ -61,81 +54,25 @@ if (command === 'terminal-manager') {
|
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'aimux — terminal multiplexer for AI CLIs',
|
|
67
|
-
'',
|
|
68
|
-
'Two surfaces:',
|
|
69
|
-
' • Interactive TUI (no args): drive assistants side-by-side in one window.',
|
|
70
|
-
' • CLI control plane (`aimux <group> <verb>`): script-friendly, JSON output,',
|
|
71
|
-
' designed to be driven by another agent or a shell pipeline.',
|
|
72
|
-
'',
|
|
73
|
-
'Interactive',
|
|
74
|
-
' aimux Start the TUI in the current profile',
|
|
75
|
-
'',
|
|
76
|
-
'CLI control plane (JSON on stdout, human-readable errors on stderr)',
|
|
77
|
-
' aimux tab list | create | send | focus | close | snapshot | tail | wait',
|
|
78
|
-
' aimux workspace list | show | create | switch | close',
|
|
79
|
-
' aimux worktree list | create | remove',
|
|
80
|
-
'',
|
|
81
|
-
' aimux <group> --help List verbs in that group',
|
|
82
|
-
' aimux <group> <verb> --help Show flags, args, and exit codes',
|
|
83
|
-
'',
|
|
84
|
-
'Common CLI verbs at a glance',
|
|
85
|
-
' aimux tab list Enumerate tabs (+ activeTabId)',
|
|
86
|
-
' aimux tab create --assistant <id> [--title …] Spawn claude / codex / opencode / grok / kimi / terminal / …',
|
|
87
|
-
' aimux tab send <tabId> [text] [--enter|--keys|--stdin] Type, chord, or paste into a tab',
|
|
88
|
-
' aimux tab focus <tabId> Bring a tab to the foreground',
|
|
89
|
-
' aimux tab close <tabId> Terminate a tab',
|
|
90
|
-
' aimux tab snapshot <tabId> [--tail N] [--format …] Capture the screen (json | text)',
|
|
91
|
-
' aimux tab tail <tabId> [--follow-status] […] NDJSON stream of renders / status',
|
|
92
|
-
' aimux tab wait <tabId> --status <idle|working|waiting-input> Block on an activity state',
|
|
93
|
-
' aimux workspace show Dump the active workspace + worktrees',
|
|
94
|
-
' aimux workspace create <name> [--project P] [--switch [--wait]]',
|
|
95
|
-
' aimux workspace switch <ws> [--wait --timeout N] Move the running UI to another workspace',
|
|
96
|
-
' aimux worktree create --name <n> [--branch B --base R]',
|
|
97
|
-
' aimux worktree remove <id|path> [--force]',
|
|
98
|
-
'',
|
|
99
|
-
'Shared flags (accepted by every CLI verb)',
|
|
100
|
-
' --workspace <id|name> Target a specific workspace (default: active)',
|
|
101
|
-
' --profile <name> Runtime profile (also settable via AIMUX_PROFILE)',
|
|
102
|
-
' --json Reserved; JSON is always on',
|
|
103
|
-
'',
|
|
104
|
-
'Maintenance',
|
|
105
|
-
' aimux update Update aimux to the latest published version',
|
|
106
|
-
' aimux doctor Diagnose setup (paths, sockets, PTY, integrations)',
|
|
107
|
-
' aimux restart-daemon Restart the IPC daemon (keeps live workspaces)',
|
|
108
|
-
' aimux restart-terminal-manager Restart terminal-manager (kills live workspaces)',
|
|
109
|
-
' aimux --version Print the installed version',
|
|
110
|
-
'',
|
|
111
|
-
'Exit codes',
|
|
112
|
-
' 0 success',
|
|
113
|
-
' 2 usage error (bad flags, unknown command, missing argument)',
|
|
114
|
-
' 3 runtime error (server replied with error, command failed)',
|
|
115
|
-
' 4 daemon unreachable (socket missing and autostart failed)',
|
|
116
|
-
' 124 timeout (tab wait, tab tail --timeout, workspace switch --wait)',
|
|
117
|
-
'',
|
|
118
|
-
'Env',
|
|
119
|
-
' AIMUX_PROFILE Runtime profile (state dir, socket paths)',
|
|
120
|
-
'',
|
|
121
|
-
'Recipes for agents',
|
|
122
|
-
' # spawn Claude, wait for idle, dump the last 40 non-blank lines',
|
|
123
|
-
' TAB=$(aimux tab create --assistant claude --title fixup | jq -r .tabId)',
|
|
124
|
-
' aimux tab send "$TAB" "explain this repo" --enter',
|
|
125
|
-
' aimux tab wait "$TAB" --status idle --timeout 60000',
|
|
126
|
-
' aimux tab snapshot "$TAB" --tail 40 --format text',
|
|
127
|
-
'',
|
|
128
|
-
' # send a control chord (Ctrl-C then Esc) using vim-style notation',
|
|
129
|
-
' aimux tab send "$TAB" "<C-c><Esc>" --keys',
|
|
130
|
-
'',
|
|
131
|
-
' # follow a tab as NDJSON, one render (or tabStatus) per line',
|
|
132
|
-
' aimux tab tail "$TAB" --rate-limit-ms 100 --follow-status',
|
|
133
|
-
'',
|
|
134
|
-
].join('\n')
|
|
135
|
-
)
|
|
136
|
-
process.exit(0)
|
|
57
|
+
const { runCli } = await import('./cli')
|
|
58
|
+
process.exit(await runCli([]))
|
|
137
59
|
}
|
|
138
60
|
|
|
61
|
+
const [
|
|
62
|
+
{ createCliRenderer },
|
|
63
|
+
{ createRoot },
|
|
64
|
+
{ App },
|
|
65
|
+
{ loadUserConfig },
|
|
66
|
+
{ BreakingUpdateScreen },
|
|
67
|
+
{ setHostPalette },
|
|
68
|
+
] = await Promise.all([
|
|
69
|
+
import('@opentui/core'),
|
|
70
|
+
import('@opentui/react'),
|
|
71
|
+
import('./app'),
|
|
72
|
+
import('./config/loader'),
|
|
73
|
+
import('./ui/breaking-update-screen'),
|
|
74
|
+
import('./ui/host-palette'),
|
|
75
|
+
])
|
|
139
76
|
const resolvedConfig = await loadUserConfig()
|
|
140
77
|
|
|
141
78
|
const renderer = await createCliRenderer({
|
|
@@ -35,8 +35,14 @@ import {
|
|
|
35
35
|
//
|
|
36
36
|
// v9: additive — `tabMetadata` updates titles and auto-rename state without
|
|
37
37
|
// restarting PTYs. Capability-gated; MIN remains at 8.
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
//
|
|
39
|
+
// v10: additive — `workerName` persists a workspace-scoped orchestration
|
|
40
|
+
// handle on tabs created through the headless worker facade.
|
|
41
|
+
//
|
|
42
|
+
// v11: breaking release boundary — force a fresh terminal-manager so named
|
|
43
|
+
// worker metadata is guaranteed to survive daemon reattach and process swaps.
|
|
44
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 11
|
|
45
|
+
export const MANAGER_PROTOCOL_VERSION = 11
|
|
40
46
|
|
|
41
47
|
/**
|
|
42
48
|
* Capability strings advertised by *this* process in its `helloResult`. New
|
|
@@ -48,9 +54,11 @@ export const MANAGER_PROTOCOL_CAPABILITIES: readonly string[] = [
|
|
|
48
54
|
'setBroadcastEnabled',
|
|
49
55
|
'createTabWorktreeId',
|
|
50
56
|
'tabMetadata',
|
|
57
|
+
'workerMetadata',
|
|
51
58
|
]
|
|
52
59
|
|
|
53
60
|
export const MANAGER_CAPABILITY_TAB_METADATA = 'tabMetadata'
|
|
61
|
+
export const MANAGER_CAPABILITY_WORKER_METADATA = 'workerMetadata'
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
64
|
* Capability name a daemon must observe on the TM's helloResult before
|
|
@@ -125,6 +133,7 @@ export type ManagerRequest =
|
|
|
125
133
|
* `worktreeId = undefined`.
|
|
126
134
|
*/
|
|
127
135
|
worktreeId?: string
|
|
136
|
+
workerName?: string
|
|
128
137
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -381,6 +390,10 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
381
390
|
value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
|
|
382
391
|
'createTab.worktreeId must be a string when present'
|
|
383
392
|
)
|
|
393
|
+
assert(
|
|
394
|
+
value.payload.workerName === undefined || isString(value.payload.workerName),
|
|
395
|
+
'createTab.workerName must be a string when present'
|
|
396
|
+
)
|
|
384
397
|
assert(
|
|
385
398
|
value.payload.autoRenameStatus === undefined ||
|
|
386
399
|
value.payload.autoRenameStatus === 'eligible' ||
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -40,8 +40,14 @@ import { isWorkspaceSnapshotV1, isWorktreeRecord } from '../state/validation'
|
|
|
40
40
|
//
|
|
41
41
|
// v14: additive — tab metadata synchronization for manual and automatic
|
|
42
42
|
// renames. The new request/event are capability-gated; MIN stays at 10.
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
//
|
|
44
|
+
// v15: additive — workspace-scoped `workerName` metadata on create, attach,
|
|
45
|
+
// and list results. This powers stable name-or-id orchestration selectors.
|
|
46
|
+
//
|
|
47
|
+
// v16: breaking release boundary — the agent-first worker control plane and
|
|
48
|
+
// its metadata guarantees are now required. Old app/daemon pairs must not mix.
|
|
49
|
+
export const IPC_PROTOCOL_MIN_VERSION = 16
|
|
50
|
+
export const IPC_PROTOCOL_VERSION = 16
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Capability advertised by a daemon that knows how to drain + handoff its
|
|
@@ -106,6 +112,7 @@ export const IPC_CAPABILITY_WORKSPACE_LIFECYCLE = 'workspaceLifecycle'
|
|
|
106
112
|
* fanout as workspaceLifecycle.
|
|
107
113
|
*/
|
|
108
114
|
export const IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS = 'worktreeLifecycleEvents'
|
|
115
|
+
export const IPC_CAPABILITY_WORKER_METADATA = 'workerMetadata'
|
|
109
116
|
|
|
110
117
|
/**
|
|
111
118
|
* v12 — capability marker for `aimux tab tail`. Functionally it's just a
|
|
@@ -167,6 +174,7 @@ export const IPC_PROTOCOL_CAPABILITIES: readonly string[] = [
|
|
|
167
174
|
IPC_CAPABILITY_QUESTION_EVENTS,
|
|
168
175
|
IPC_CAPABILITY_LIST_TABS_LAST_LINE,
|
|
169
176
|
IPC_CAPABILITY_TAB_METADATA,
|
|
177
|
+
IPC_CAPABILITY_WORKER_METADATA,
|
|
170
178
|
]
|
|
171
179
|
|
|
172
180
|
export interface ProtocolHelloRequest {
|
|
@@ -175,6 +183,8 @@ export interface ProtocolHelloRequest {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
export interface ProtocolHelloResult {
|
|
186
|
+
/** aimux package version; absent on older daemons. */
|
|
187
|
+
appVersion?: string
|
|
178
188
|
minVersion: number
|
|
179
189
|
maxVersion: number
|
|
180
190
|
processVersion: string
|
|
@@ -184,6 +194,8 @@ export interface ProtocolHelloResult {
|
|
|
184
194
|
* not yet advertise capabilities are normalised to `[]` at parse time.
|
|
185
195
|
*/
|
|
186
196
|
capabilities: string[]
|
|
197
|
+
/** Capabilities negotiated with the terminal-manager; absent on older daemons. */
|
|
198
|
+
managerCapabilities?: string[]
|
|
187
199
|
/**
|
|
188
200
|
* Manager-protocol version currently negotiated between the daemon and
|
|
189
201
|
* its terminal-manager. `undefined` when the daemon has not yet connected
|
|
@@ -227,6 +239,7 @@ export interface TabSessionSummary {
|
|
|
227
239
|
activity?: TabActivity
|
|
228
240
|
command: string
|
|
229
241
|
worktreeId?: string
|
|
242
|
+
workerName?: string
|
|
230
243
|
/**
|
|
231
244
|
* v13 / capability `listTabsLastLine`. The tab's last non-blank rendered
|
|
232
245
|
* line, trimmed. Present only when the daemon advertises the capability;
|
|
@@ -276,6 +289,8 @@ export type ClientRequest =
|
|
|
276
289
|
* only forwards it to the TM when its own capability is in play.
|
|
277
290
|
*/
|
|
278
291
|
worktreeId?: string
|
|
292
|
+
/** v15 / capability `workerMetadata`: stable workspace-scoped handle. */
|
|
293
|
+
workerName?: string
|
|
279
294
|
/** True only when the creator did not provide an explicit title. */
|
|
280
295
|
autoRenameCandidate?: boolean
|
|
281
296
|
}
|
|
@@ -566,12 +581,14 @@ function isProtocolHelloResult(value: unknown): value is ProtocolHelloResult {
|
|
|
566
581
|
isFiniteNumber(value.maxVersion) &&
|
|
567
582
|
isFiniteNumber(value.selectedVersion) &&
|
|
568
583
|
isString(value.processVersion) &&
|
|
584
|
+
(value.appVersion === undefined || isString(value.appVersion)) &&
|
|
569
585
|
// Wire-back-compat: peers that predate the capabilities field omit it
|
|
570
586
|
// entirely. parseServerMessage normalises that to `[]` before the cast
|
|
571
587
|
// so the typed shape stays non-optional.
|
|
572
588
|
(value.capabilities === undefined || isStringArray(value.capabilities)) &&
|
|
573
589
|
// Additive: peers that predate managerSelectedVersion omit it.
|
|
574
|
-
(value.managerSelectedVersion === undefined || isFiniteNumber(value.managerSelectedVersion))
|
|
590
|
+
(value.managerSelectedVersion === undefined || isFiniteNumber(value.managerSelectedVersion)) &&
|
|
591
|
+
(value.managerCapabilities === undefined || isStringArray(value.managerCapabilities))
|
|
575
592
|
)
|
|
576
593
|
}
|
|
577
594
|
|
|
@@ -592,6 +609,7 @@ function isTabSessionSummary(value: unknown): value is TabSessionSummary {
|
|
|
592
609
|
value.activity === 'idle') &&
|
|
593
610
|
isString(value.command) &&
|
|
594
611
|
(value.worktreeId === undefined || isString(value.worktreeId)) &&
|
|
612
|
+
(value.workerName === undefined || isString(value.workerName)) &&
|
|
595
613
|
(value.lastLine === undefined || isString(value.lastLine))
|
|
596
614
|
)
|
|
597
615
|
}
|
|
@@ -627,6 +645,7 @@ function isTabSession(value: unknown): value is TabSession {
|
|
|
627
645
|
isString(value.buffer) &&
|
|
628
646
|
isTerminalModeState(value.terminalModes) &&
|
|
629
647
|
isString(value.command) &&
|
|
648
|
+
(value.workerName === undefined || isString(value.workerName)) &&
|
|
630
649
|
(value.autoRenameStatus === undefined ||
|
|
631
650
|
value.autoRenameStatus === 'eligible' ||
|
|
632
651
|
value.autoRenameStatus === 'attempted') &&
|
|
@@ -736,6 +755,10 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
736
755
|
value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
|
|
737
756
|
'createTab.worktreeId must be a string when present'
|
|
738
757
|
)
|
|
758
|
+
assert(
|
|
759
|
+
value.payload.workerName === undefined || isString(value.payload.workerName),
|
|
760
|
+
'createTab.workerName must be a string when present'
|
|
761
|
+
)
|
|
739
762
|
assert(
|
|
740
763
|
value.payload.autoRenameCandidate === undefined ||
|
|
741
764
|
typeof value.payload.autoRenameCandidate === 'boolean',
|
|
@@ -59,6 +59,7 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
|
|
|
59
59
|
terminalModes: tab.terminalModes,
|
|
60
60
|
title: tab.title,
|
|
61
61
|
viewport: tab.viewport,
|
|
62
|
+
workerName: tab.workerName,
|
|
62
63
|
worktreeId: tab.worktreeId,
|
|
63
64
|
})),
|
|
64
65
|
version: 1,
|
|
@@ -124,6 +125,7 @@ export function restoreTabsFromWorkspace(
|
|
|
124
125
|
terminalModes: tab.terminalModes,
|
|
125
126
|
title: tab.title,
|
|
126
127
|
viewport: tab.viewport,
|
|
128
|
+
workerName: tab.workerName,
|
|
127
129
|
worktreeId: tab.worktreeId,
|
|
128
130
|
}))
|
|
129
131
|
return pruneOrphanedTabs(restored, options.validWorktreeIds)
|