@brimveyn/aimux 1.19.7 → 1.20.2

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 (54) hide show
  1. package/README.md +14 -0
  2. package/package.json +3 -2
  3. package/skills/aimux-orchestrator/SKILL.md +127 -0
  4. package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
  5. package/skills/aimux-orchestrator/references/prompts.md +68 -0
  6. package/skills/aimux-orchestrator/references/review.md +18 -0
  7. package/src/cli/client/daemon-client.ts +16 -0
  8. package/src/cli/client/workspace-resolver.ts +61 -8
  9. package/src/cli/commands/tab/await.ts +1 -1
  10. package/src/cli/commands/tab/close.ts +1 -1
  11. package/src/cli/commands/tab/create.ts +263 -128
  12. package/src/cli/commands/tab/focus.ts +1 -1
  13. package/src/cli/commands/tab/prompt-io.ts +8 -2
  14. package/src/cli/commands/tab/run.ts +10 -2
  15. package/src/cli/commands/tab/send.ts +12 -7
  16. package/src/cli/commands/tab/snapshot.ts +2 -1
  17. package/src/cli/commands/tab/tail.ts +1 -1
  18. package/src/cli/commands/tab/wait.ts +2 -1
  19. package/src/cli/commands/worker/await.ts +34 -0
  20. package/src/cli/commands/worker/doctor.ts +153 -0
  21. package/src/cli/commands/worker/list.ts +67 -0
  22. package/src/cli/commands/worker/prompt.ts +74 -0
  23. package/src/cli/commands/worker/run.ts +143 -0
  24. package/src/cli/commands/worker/shared.ts +515 -0
  25. package/src/cli/commands/worker/stop.ts +113 -0
  26. package/src/cli/commands/worker/submit.ts +40 -0
  27. package/src/cli/commands/workspace/close.ts +1 -1
  28. package/src/cli/commands/workspace/create.ts +2 -1
  29. package/src/cli/commands/workspace/switch.ts +1 -1
  30. package/src/cli/commands/worktree/create-core.ts +27 -7
  31. package/src/cli/commands/worktree/create.ts +18 -3
  32. package/src/cli/commands/worktree/remove.ts +32 -10
  33. package/src/cli/completion/entry.ts +181 -0
  34. package/src/cli/completion/install.ts +222 -0
  35. package/src/cli/completion/plan.ts +216 -0
  36. package/src/cli/completion/scripts.ts +147 -0
  37. package/src/cli/completion/sources.ts +74 -0
  38. package/src/cli/context.ts +15 -0
  39. package/src/cli/flags.ts +45 -2
  40. package/src/cli/index.ts +40 -18
  41. package/src/cli/output.ts +3 -0
  42. package/src/cli/registry.ts +14 -0
  43. package/src/daemon/daemon.ts +58 -8
  44. package/src/daemon/session-registry.ts +5 -0
  45. package/src/doctor.ts +4 -0
  46. package/src/git/worktree.ts +23 -1
  47. package/src/index.tsx +53 -92
  48. package/src/ipc/manager-protocol.ts +15 -2
  49. package/src/ipc/protocol.ts +26 -3
  50. package/src/platform/worktree-paths.ts +21 -1
  51. package/src/state/session-persistence.ts +2 -0
  52. package/src/state/types.ts +4 -0
  53. package/src/state/validation.ts +1 -0
  54. package/src/terminal-manager/manager-client.ts +18 -3
@@ -0,0 +1,153 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ import type { CliCommand } from '../../registry'
5
+
6
+ import { loadConfig } from '../../../config'
7
+ import { MANAGER_CAPABILITY_WORKER_METADATA } from '../../../ipc/manager-protocol'
8
+ import {
9
+ IPC_CAPABILITY_LIST_TABS,
10
+ IPC_CAPABILITY_QUESTION_EVENTS,
11
+ IPC_CAPABILITY_THIN_ATTACH,
12
+ IPC_CAPABILITY_TURN_LIFECYCLE,
13
+ IPC_CAPABILITY_WORKER_METADATA,
14
+ IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS,
15
+ } from '../../../ipc/protocol'
16
+ import {
17
+ getAllAssistantOptions,
18
+ isCommandAvailable,
19
+ parseCommand,
20
+ } from '../../../pty/command-registry'
21
+ import {
22
+ findPrimaryWorktree,
23
+ WORKSPACE_ENV_VAR,
24
+ workspaceRepoRoot,
25
+ } from '../../client/workspace-resolver'
26
+ import { SHARED_FLAGS } from '../../flags'
27
+ import { EXIT_OK, EXIT_RUNTIME, writeJson } from '../../output'
28
+ import { WORKER_SCHEMA_VERSION } from './shared'
29
+
30
+ const REQUIRED_DAEMON_CAPABILITIES = [
31
+ IPC_CAPABILITY_LIST_TABS,
32
+ IPC_CAPABILITY_QUESTION_EVENTS,
33
+ IPC_CAPABILITY_THIN_ATTACH,
34
+ IPC_CAPABILITY_TURN_LIFECYCLE,
35
+ IPC_CAPABILITY_WORKER_METADATA,
36
+ IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS,
37
+ ] as const
38
+
39
+ export const workerDoctor: CliCommand = {
40
+ args: [],
41
+ flags: SHARED_FLAGS,
42
+ group: 'worker',
43
+ run: async (ctx) => {
44
+ const daemon = await ctx.getDaemon()
45
+ const workspace = ctx.getWorkspace()
46
+ const { version } = await import('../../../../package.json')
47
+ const { customCommands } = loadConfig()
48
+ const assistants = getAllAssistantOptions(customCommands).map((assistant) => ({
49
+ available: isCommandAvailable(
50
+ parseCommand(customCommands[assistant.id] ?? assistant.command).executable
51
+ ),
52
+ id: assistant.id,
53
+ supportsEffort: assistant.model?.buildEffortArgs !== undefined,
54
+ supportsModel: assistant.model?.buildModelArgs !== undefined,
55
+ }))
56
+ const daemonCapabilities = daemon.getCapabilities()
57
+ const managerCapabilities = daemon.getManagerCapabilities()
58
+ const missingDaemonCapabilities = REQUIRED_DAEMON_CAPABILITIES.filter(
59
+ (capability) => !daemonCapabilities.includes(capability)
60
+ )
61
+ const missingManagerCapabilities = [MANAGER_CAPABILITY_WORKER_METADATA].filter(
62
+ (capability) => !managerCapabilities.includes(capability)
63
+ )
64
+ const primaryWorktree = findPrimaryWorktree(workspace)
65
+ const workspaceOrigin = ctx.getWorkspaceOrigin?.() ?? 'active'
66
+ const availableAssistants = assistants.filter((assistant) => assistant.available)
67
+ const skillPath = fileURLToPath(
68
+ new URL('../../../../skills/aimux-orchestrator/', import.meta.url)
69
+ )
70
+ const issues: string[] = []
71
+ if (missingDaemonCapabilities.length > 0) {
72
+ issues.push(
73
+ `restart or update aimux; daemon is missing: ${missingDaemonCapabilities.join(', ')}`
74
+ )
75
+ }
76
+ if (missingManagerCapabilities.length > 0) {
77
+ issues.push(
78
+ `restart the terminal manager; it is missing: ${missingManagerCapabilities.join(', ')}`
79
+ )
80
+ }
81
+ if (primaryWorktree === undefined) {
82
+ issues.push('workspace has no primary worktree; default isolated worker runs are unavailable')
83
+ }
84
+ if (availableAssistants.length === 0) {
85
+ issues.push('no configured assistant executable is available on PATH')
86
+ }
87
+ if (!existsSync(skillPath)) {
88
+ issues.push(`packaged orchestrator skill is missing: ${skillPath}`)
89
+ }
90
+ // Not an issue — an inferred workspace is the normal interactive case — but
91
+ // it IS the one resolution mode that can follow the UI to another project
92
+ // between two calls. An orchestrator dispatching a multi-hour fleet wants to
93
+ // see this before it starts, not after it reviews diffs from the wrong repo.
94
+ const warnings: string[] = []
95
+ if (workspaceOrigin === 'active') {
96
+ warnings.push(
97
+ `workspace "${workspace.name}" was inferred from the most recently opened session and follows the UI; pin it with --workspace or ${WORKSPACE_ENV_VAR}`
98
+ )
99
+ }
100
+ const ready = issues.length === 0
101
+ writeJson({
102
+ assistants,
103
+ checks: {
104
+ assistants: {
105
+ available: availableAssistants.map((assistant) => assistant.id),
106
+ ok: availableAssistants.length > 0,
107
+ },
108
+ daemonCapabilities: {
109
+ missing: missingDaemonCapabilities,
110
+ ok: missingDaemonCapabilities.length === 0,
111
+ },
112
+ managerCapabilities: {
113
+ missing: missingManagerCapabilities,
114
+ ok: missingManagerCapabilities.length === 0,
115
+ },
116
+ skill: { ok: existsSync(skillPath), path: skillPath },
117
+ workspace: {
118
+ hasPrimaryWorktree: primaryWorktree !== undefined,
119
+ name: workspace.name,
120
+ ok: primaryWorktree !== undefined,
121
+ /** Repo every fresh worker worktree is cut from — confirm before dispatching. */
122
+ repoRoot: workspaceRepoRoot(workspace),
123
+ /** 'flag' | 'env' | 'active'; only 'active' can follow the UI. */
124
+ source: workspaceOrigin,
125
+ },
126
+ },
127
+ cliVersion: version,
128
+ daemon: {
129
+ appVersion: daemon.getAppVersion(),
130
+ capabilities: daemonCapabilities,
131
+ managerCapabilities,
132
+ managerProtocolVersion: daemon.getManagerSelectedVersion(),
133
+ processVersion: daemon.getProcessVersion(),
134
+ protocolVersion: daemon.getSelectedVersion(),
135
+ },
136
+ issues,
137
+ ready,
138
+ schemaVersion: WORKER_SCHEMA_VERSION,
139
+ skillPath,
140
+ warnings,
141
+ workspace: {
142
+ id: workspace.id,
143
+ name: workspace.name,
144
+ projectPath: workspace.projectPath ?? null,
145
+ repoRoot: workspaceRepoRoot(workspace),
146
+ source: workspaceOrigin,
147
+ },
148
+ })
149
+ return ready ? EXIT_OK : EXIT_RUNTIME
150
+ },
151
+ summary: 'Check worker prerequisites, versions, assistants, and workspace',
152
+ verb: 'doctor',
153
+ }
@@ -0,0 +1,67 @@
1
+ import type { CliCommand } from '../../registry'
2
+
3
+ import { workspaceIdentity } from '../../client/workspace-resolver'
4
+ import { SHARED_FLAGS } from '../../flags'
5
+ import { EXIT_OK, writeJson } from '../../output'
6
+ import {
7
+ listNamedWorkerTabs,
8
+ listWorkerTargets,
9
+ resolveWorkerTarget,
10
+ WORKER_SCHEMA_VERSION,
11
+ workerView,
12
+ } from './shared'
13
+
14
+ export const workerList: CliCommand = {
15
+ args: [{ complete: { kind: 'dynamic', source: 'worker' }, name: 'worker' }],
16
+ flags: [
17
+ ...SHARED_FLAGS,
18
+ {
19
+ description: 'list workers in every catalogued workspace, not just the target one',
20
+ kind: 'boolean',
21
+ name: 'all-workspaces',
22
+ },
23
+ ],
24
+ group: 'worker',
25
+ run: async (ctx) => {
26
+ const selector = ctx.args.positionals[0]
27
+
28
+ // `{"workers":[]}` alone is indistinguishable from "every worker died", and
29
+ // the natural recovery from that reading is a destructive re-dispatch. Name
30
+ // the workspace that was queried so an empty fleet is legible as "not here"
31
+ // rather than "gone", and offer one call that answers "are they really gone?"
32
+ if (ctx.args.flags['all-workspaces'] === true) {
33
+ const targets = await listWorkerTargets(ctx)
34
+ writeJson({
35
+ schemaVersion: WORKER_SCHEMA_VERSION,
36
+ workers: targets
37
+ .filter((target) => selector === undefined || target.tab.workerName === selector)
38
+ .map((target) => ({
39
+ ...workerView(target.workspace, target.tab),
40
+ workspace: workspaceIdentity(target.workspace),
41
+ })),
42
+ })
43
+ return EXIT_OK
44
+ }
45
+
46
+ if (selector !== undefined) {
47
+ const { tab, workspace } = await resolveWorkerTarget(ctx, selector)
48
+ writeJson({
49
+ schemaVersion: WORKER_SCHEMA_VERSION,
50
+ workers: [workerView(workspace, tab)],
51
+ workspace: workspaceIdentity(workspace),
52
+ })
53
+ return EXIT_OK
54
+ }
55
+
56
+ const workspace = ctx.getWorkspace()
57
+ const tabs = await listNamedWorkerTabs(ctx, workspace)
58
+ writeJson({
59
+ schemaVersion: WORKER_SCHEMA_VERSION,
60
+ workers: tabs.map((tab) => workerView(workspace, tab)),
61
+ workspace: workspaceIdentity(workspace),
62
+ })
63
+ return EXIT_OK
64
+ },
65
+ summary: 'List named workers with liveness and worktree context',
66
+ verb: 'list',
67
+ }
@@ -0,0 +1,74 @@
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 { resolvePromptText } from '../tab/prompt-io'
7
+ import {
8
+ dispatchWorkerPrompt,
9
+ resolveWorkerTarget,
10
+ workerEnvelope,
11
+ workerOutcomeExitCode,
12
+ workerView,
13
+ } from './shared'
14
+
15
+ export const workerPrompt: CliCommand = {
16
+ args: [
17
+ { complete: { kind: 'dynamic', source: 'worker' }, name: 'worker', required: true },
18
+ { complete: { kind: 'none' }, name: 'text' },
19
+ ],
20
+ flags: [
21
+ ...SHARED_FLAGS,
22
+ {
23
+ complete: { kind: 'file' },
24
+ description: 'read the prompt from this file',
25
+ kind: 'string',
26
+ name: 'prompt-file',
27
+ },
28
+ { description: 'read the prompt from stdin', kind: 'boolean', name: 'stdin' },
29
+ {
30
+ description: 'return after prompt uptake instead of turn completion',
31
+ kind: 'boolean',
32
+ name: 'detach',
33
+ },
34
+ { description: 'overall turn cap in milliseconds', kind: 'number', name: 'timeout' },
35
+ {
36
+ description:
37
+ 'milliseconds to wait for the detached submit→working confirmation (default 15000)',
38
+ kind: 'number',
39
+ name: 'uptake-timeout',
40
+ },
41
+ {
42
+ description:
43
+ 'clear the composer (<C-u>) before writing, so text typed by a human in the UI cannot be concatenated onto this prompt',
44
+ kind: 'boolean',
45
+ name: 'replace',
46
+ },
47
+ ],
48
+ group: 'worker',
49
+ run: async (ctx) => {
50
+ const selector = ctx.args.positionals[0] ?? ''
51
+ const { tab, workspace } = await resolveWorkerTarget(ctx, selector)
52
+ const promptFile =
53
+ typeof ctx.args.flags['prompt-file'] === 'string' ? ctx.args.flags['prompt-file'] : undefined
54
+ const text = await resolvePromptText(
55
+ promptFile,
56
+ ctx.args.flags.stdin === true,
57
+ ctx.args.positionals[1]
58
+ )
59
+ const outcome = await dispatchWorkerPrompt(ctx, workspace, tab.id, text, {
60
+ detach: ctx.args.flags.detach === true,
61
+ replace: ctx.args.flags.replace === true,
62
+ timeoutMs:
63
+ typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS,
64
+ uptakeTimeoutMs:
65
+ typeof ctx.args.flags['uptake-timeout'] === 'number'
66
+ ? ctx.args.flags['uptake-timeout']
67
+ : undefined,
68
+ })
69
+ writeJson(workerEnvelope(workspace, workerView(workspace, tab), outcome))
70
+ return workerOutcomeExitCode(outcome)
71
+ },
72
+ summary: 'Prompt an existing named worker and await its outcome',
73
+ verb: 'prompt',
74
+ }
@@ -0,0 +1,143 @@
1
+ import type { CliCommand } from '../../registry'
2
+
3
+ import { CliUsageError, SHARED_FLAGS } from '../../flags'
4
+ import { writeJson } from '../../output'
5
+ import { DEFAULT_TIMEOUT_MS } from '../tab/await-turn'
6
+ import { createCliTab } from '../tab/create'
7
+ import { resolvePromptText } from '../tab/prompt-io'
8
+ import {
9
+ dispatchWorkerPrompt,
10
+ validateWorkerName,
11
+ workerEnvelope,
12
+ workerOutcomeExitCode,
13
+ workerView,
14
+ } from './shared'
15
+
16
+ export const workerRun: CliCommand = {
17
+ args: [{ complete: { kind: 'none' }, name: 'text' }],
18
+ flags: [
19
+ ...SHARED_FLAGS,
20
+ {
21
+ complete: { kind: 'none' },
22
+ description: 'unique workspace-scoped worker name',
23
+ kind: 'string',
24
+ name: 'name',
25
+ },
26
+ {
27
+ complete: { kind: 'dynamic', source: 'assistant' },
28
+ description: 'assistant id (claude, codex, opencode, ...)',
29
+ kind: 'string',
30
+ name: 'assistant',
31
+ },
32
+ {
33
+ complete: { kind: 'none' },
34
+ description: 'model passed through to the assistant',
35
+ kind: 'string',
36
+ name: 'model',
37
+ },
38
+ {
39
+ complete: { kind: 'none' },
40
+ description: 'reasoning effort passed through to the assistant',
41
+ kind: 'string',
42
+ name: 'effort',
43
+ },
44
+ {
45
+ complete: { kind: 'file' },
46
+ description: 'read the prompt from this file',
47
+ kind: 'string',
48
+ name: 'prompt-file',
49
+ },
50
+ { description: 'read the prompt from stdin', kind: 'boolean', name: 'stdin' },
51
+ {
52
+ description: 'return after prompt uptake instead of turn completion',
53
+ kind: 'boolean',
54
+ name: 'detach',
55
+ },
56
+ { description: 'overall turn cap in milliseconds', kind: 'number', name: 'timeout' },
57
+ {
58
+ description:
59
+ 'milliseconds to wait for the detached submit→working confirmation (default 15000)',
60
+ kind: 'number',
61
+ name: 'uptake-timeout',
62
+ },
63
+ {
64
+ complete: { kind: 'dynamic', source: 'worktree' },
65
+ description: 'co-locate in an existing worktree id',
66
+ kind: 'string',
67
+ name: 'worktree',
68
+ },
69
+ {
70
+ description: 'run in the workspace active worktree instead of creating one',
71
+ kind: 'boolean',
72
+ name: 'no-worktree',
73
+ },
74
+ {
75
+ complete: { kind: 'dynamic', source: 'git-ref' },
76
+ description: 'base ref for the fresh worktree (default HEAD)',
77
+ kind: 'string',
78
+ name: 'base',
79
+ },
80
+ {
81
+ complete: { kind: 'none' },
82
+ description: 'branch for the fresh worktree (default aimux/<name>)',
83
+ kind: 'string',
84
+ name: 'branch',
85
+ },
86
+ ],
87
+ group: 'worker',
88
+ run: async (ctx) => {
89
+ const name = typeof ctx.args.flags.name === 'string' ? ctx.args.flags.name : ''
90
+ const assistant = typeof ctx.args.flags.assistant === 'string' ? ctx.args.flags.assistant : ''
91
+ validateWorkerName(name)
92
+ if (assistant === '') throw new CliUsageError('--assistant is required')
93
+ const worktree =
94
+ typeof ctx.args.flags.worktree === 'string' ? ctx.args.flags.worktree : undefined
95
+ const noWorktree = ctx.args.flags['no-worktree'] === true
96
+ if (worktree !== undefined && noWorktree) {
97
+ throw new CliUsageError('--worktree and --no-worktree are mutually exclusive')
98
+ }
99
+ const promptFile =
100
+ typeof ctx.args.flags['prompt-file'] === 'string' ? ctx.args.flags['prompt-file'] : undefined
101
+ const text = await resolvePromptText(
102
+ promptFile,
103
+ ctx.args.flags.stdin === true,
104
+ ctx.args.positionals[0]
105
+ )
106
+ // Resolve the target workspace ONCE, up front, and use that record for
107
+ // every later step. The repo a worktree is cut from comes from this record,
108
+ // so an orchestrator that omits --workspace at least gets the resolved
109
+ // identity echoed back in the response instead of having to infer it.
110
+ const workspace = ctx.getWorkspace()
111
+ const result = await createCliTab(ctx, {
112
+ assistantId: assistant,
113
+ base: typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : undefined,
114
+ branch: typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : undefined,
115
+ effort: typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined,
116
+ model: typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined,
117
+ newWorktree: noWorktree || worktree !== undefined ? undefined : name,
118
+ title: name,
119
+ workerName: name,
120
+ worktreeId: worktree,
121
+ })
122
+ const tabs = await (await ctx.getDaemon()).listTabs(workspace.id)
123
+ const tab = tabs.tabs.find((entry) => entry.id === result.tabId)
124
+ if (!tab) throw new Error(`created worker disappeared: ${result.tabId}`)
125
+ const worker = workerView(workspace, tab)
126
+ const outcome = await dispatchWorkerPrompt(ctx, workspace, result.tabId, text, {
127
+ // The tab was spawned microseconds ago: its assistant is still booting, so
128
+ // the prompt must not be written until the TUI is reading keystrokes.
129
+ awaitFirstPaint: true,
130
+ detach: ctx.args.flags.detach === true,
131
+ timeoutMs:
132
+ typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS,
133
+ uptakeTimeoutMs:
134
+ typeof ctx.args.flags['uptake-timeout'] === 'number'
135
+ ? ctx.args.flags['uptake-timeout']
136
+ : undefined,
137
+ })
138
+ writeJson(workerEnvelope(workspace, worker, outcome))
139
+ return workerOutcomeExitCode(outcome)
140
+ },
141
+ summary: 'Create a named worker, dispatch a prompt, and await its outcome',
142
+ verb: 'run',
143
+ }