@brimveyn/aimux 1.20.1 → 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.
- package/README.md +9 -1
- package/package.json +1 -1
- package/skills/aimux-orchestrator/SKILL.md +39 -5
- package/skills/aimux-orchestrator/references/prompts.md +11 -0
- package/src/cli/client/workspace-resolver.ts +61 -8
- package/src/cli/commands/tab/await.ts +1 -1
- package/src/cli/commands/tab/close.ts +1 -1
- package/src/cli/commands/tab/create.ts +27 -3
- package/src/cli/commands/tab/focus.ts +1 -1
- package/src/cli/commands/tab/prompt-io.ts +6 -1
- package/src/cli/commands/tab/run.ts +10 -2
- package/src/cli/commands/tab/send.ts +12 -7
- package/src/cli/commands/tab/snapshot.ts +2 -1
- package/src/cli/commands/tab/tail.ts +1 -1
- package/src/cli/commands/tab/wait.ts +2 -1
- package/src/cli/commands/worker/await.ts +5 -4
- package/src/cli/commands/worker/doctor.ts +25 -1
- package/src/cli/commands/worker/list.ts +50 -8
- package/src/cli/commands/worker/prompt.ts +31 -6
- package/src/cli/commands/worker/run.ts +56 -10
- package/src/cli/commands/worker/shared.ts +312 -52
- package/src/cli/commands/worker/stop.ts +39 -10
- package/src/cli/commands/worker/submit.ts +40 -0
- package/src/cli/commands/workspace/close.ts +1 -1
- package/src/cli/commands/workspace/create.ts +2 -1
- package/src/cli/commands/workspace/switch.ts +1 -1
- package/src/cli/commands/worktree/create-core.ts +27 -7
- package/src/cli/commands/worktree/create.ts +18 -3
- package/src/cli/commands/worktree/remove.ts +3 -1
- package/src/cli/completion/entry.ts +181 -0
- package/src/cli/completion/install.ts +222 -0
- package/src/cli/completion/plan.ts +216 -0
- package/src/cli/completion/scripts.ts +147 -0
- package/src/cli/completion/sources.ts +74 -0
- package/src/cli/context.ts +15 -0
- package/src/cli/flags.ts +45 -2
- package/src/cli/index.ts +20 -10
- package/src/cli/output.ts +3 -0
- package/src/cli/registry.ts +2 -0
- package/src/doctor.ts +4 -0
- package/src/git/worktree.ts +15 -1
- package/src/index.tsx +35 -11
- package/src/platform/worktree-paths.ts +21 -1
package/README.md
CHANGED
|
@@ -36,6 +36,12 @@ bun install -g @brimveyn/aimux
|
|
|
36
36
|
|
|
37
37
|
Requires [Bun](https://bun.sh).
|
|
38
38
|
|
|
39
|
+
Shell completion (bash, zsh, fish) installs itself the first time you launch
|
|
40
|
+
the TUI — one file in your shell's completions directory, no dotfile edits.
|
|
41
|
+
Run `aimux doctor` to see where it landed, or
|
|
42
|
+
`aimux completion install --shell zsh` to place it yourself. Opt out with
|
|
43
|
+
`AIMUX_NO_COMPLETION_INSTALL=1`. See [docs/reference/cli.md](docs/reference/cli.md#aimux-completion).
|
|
44
|
+
|
|
39
45
|
## Quick Start
|
|
40
46
|
|
|
41
47
|
Create the default profile and install the typed config package into it:
|
|
@@ -170,7 +176,9 @@ See [`docs/reference/cli.md`](docs/reference/cli.md) for behavior details.
|
|
|
170
176
|
|
|
171
177
|
For agent orchestration, prefer the named `aimux worker` commands. They combine
|
|
172
178
|
isolated worktree creation, prompt dispatch, authoritative turn waiting, fleet
|
|
173
|
-
inspection, and guarded cleanup without shell wrappers or `jq`.
|
|
179
|
+
inspection, and guarded cleanup without shell wrappers or `jq`. Pin the target
|
|
180
|
+
with `--workspace` (or `AIMUX_WORKSPACE`) for anything long-running: the default
|
|
181
|
+
follows whichever workspace the UI opened last.
|
|
174
182
|
|
|
175
183
|
## Runtime Model
|
|
176
184
|
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ Own the plan and quality gate. Let aimux own worker transport and lifecycle.
|
|
|
12
12
|
Run once:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
aimux worker doctor
|
|
15
|
+
aimux worker doctor --workspace <target>
|
|
16
16
|
aimux worker --help
|
|
17
17
|
```
|
|
18
18
|
|
|
@@ -20,6 +20,14 @@ Stop if the doctor reports missing worker capabilities, the wrong workspace, or
|
|
|
20
20
|
an unavailable assistant. Restart/update aimux as instructed; do not fall back
|
|
21
21
|
to screen scraping or the legacy shell wrappers.
|
|
22
22
|
|
|
23
|
+
**Pin the workspace and verify the repo before dispatching anything.** Without
|
|
24
|
+
`--workspace` (or `AIMUX_WORKSPACE`) aimux targets whichever workspace the UI
|
|
25
|
+
opened last, and that can change under you mid-run — including to a different
|
|
26
|
+
project. Check `checks.workspace.repoRoot` in the doctor output against the
|
|
27
|
+
repository you mean, then pass `--workspace <id|name>` on **every** call (all
|
|
28
|
+
examples below do). Every worker response echoes `workspace.repoRoot`; read it on
|
|
29
|
+
the first dispatch instead of assuming.
|
|
30
|
+
|
|
23
31
|
## Orchestration loop
|
|
24
32
|
|
|
25
33
|
1. Parse the plan into independently reviewable units and dependencies.
|
|
@@ -29,19 +37,20 @@ to screen scraping or the legacy shell wrappers.
|
|
|
29
37
|
|
|
30
38
|
```bash
|
|
31
39
|
aimux worker run \
|
|
40
|
+
--workspace <target> \
|
|
32
41
|
--name feat-auth \
|
|
33
42
|
--assistant claude \
|
|
34
43
|
--prompt-file /tmp/feat-auth.md \
|
|
35
44
|
--detach
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
5. Inspect the fleet with `aimux worker list
|
|
39
|
-
6. Await a detached worker with `aimux worker await feat-auth
|
|
47
|
+
5. Inspect the fleet with `aimux worker list --workspace <target>`.
|
|
48
|
+
6. Await a detached worker with `aimux worker await feat-auth --workspace <target>`.
|
|
40
49
|
7. If a worker asks a question, answer only when the plan already determines the
|
|
41
50
|
answer:
|
|
42
51
|
|
|
43
52
|
```bash
|
|
44
|
-
aimux worker prompt feat-auth --prompt-file /tmp/answer.md
|
|
53
|
+
aimux worker prompt feat-auth --workspace <target> --replace --prompt-file /tmp/answer.md
|
|
45
54
|
```
|
|
46
55
|
|
|
47
56
|
Escalate design choices, irreversible operations, deployments, pushes,
|
|
@@ -54,9 +63,32 @@ migrations, spending, and external communication to the human.
|
|
|
54
63
|
10. After integration, close and clean up:
|
|
55
64
|
|
|
56
65
|
```bash
|
|
57
|
-
aimux worker stop feat-auth --cleanup-worktree
|
|
66
|
+
aimux worker stop feat-auth --workspace <target> --cleanup-worktree
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
## Dispatch failure modes
|
|
70
|
+
|
|
71
|
+
Read these before the first dispatch; each one otherwise reads as a lost fleet.
|
|
72
|
+
|
|
73
|
+
- **`status: "pending-submit"` (exit 11)** — the prompt is sitting unsubmitted in
|
|
74
|
+
the worker's composer. The worker is alive and healthy. Recover with
|
|
75
|
+
`aimux worker submit <name> --workspace <target>`; never re-dispatch, which
|
|
76
|
+
would double the fleet onto the same branches. Widen the confirmation window
|
|
77
|
+
with `--uptake-timeout <ms>` for slow-booting assistants.
|
|
78
|
+
- **An empty `worker list`** — read the `workspace` field in the response. An
|
|
79
|
+
empty fleet in the wrong workspace is not a dead fleet. Add
|
|
80
|
+
`--all-workspaces` to answer "are they really gone?" in one call;
|
|
81
|
+
`git worktree list` in the target repo is the on-disk cross-check.
|
|
82
|
+
- **Composer contamination** — a human typing in a worker's tab leaves text that
|
|
83
|
+
`worker prompt` would append to, merging both into one incoherent instruction.
|
|
84
|
+
Use `--replace` (clears with `<C-u>`) for every correction, and
|
|
85
|
+
`aimux tab snapshot <tabId>` when a worker's behaviour doesn't match the
|
|
86
|
+
prompt you believe you sent.
|
|
87
|
+
- **Fresh worktrees are not provisioned** — a new worktree has no installed
|
|
88
|
+
dependencies and no generated files. Every dispatch prompt must tell the worker
|
|
89
|
+
to bootstrap before running any gate (see `references/prompts.md`), or it will
|
|
90
|
+
report environment failures as its own.
|
|
91
|
+
|
|
60
92
|
## Isolation
|
|
61
93
|
|
|
62
94
|
`worker run` creates a fresh worktree by default. Keep that default when writes
|
|
@@ -78,6 +110,8 @@ may overlap or scope is uncertain.
|
|
|
78
110
|
- Prefer `--prompt-file` for multiline prompts; it avoids shell quoting hazards.
|
|
79
111
|
- Use `--detach` for parallel launch. Without it, `worker run` waits and returns
|
|
80
112
|
a completed/question/timeout/error outcome.
|
|
113
|
+
- Pass `--workspace` on every call, and confirm the `repoRoot` the first response
|
|
114
|
+
reports is the repository you intend to change.
|
|
81
115
|
|
|
82
116
|
## Quality and safety
|
|
83
117
|
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
- <machine-checkable criterion>
|
|
25
25
|
|
|
26
|
+
## Environment
|
|
27
|
+
|
|
28
|
+
This is a fresh worktree: it has no installed dependencies and no generated
|
|
29
|
+
files. Run <the repo's install + codegen commands> before any verification gate,
|
|
30
|
+
and report a bootstrap failure as a bootstrap failure — never as a failing gate.
|
|
31
|
+
|
|
26
32
|
## Verification
|
|
27
33
|
|
|
28
34
|
Run the repository's tests, typecheck, lint, and build commands that apply.
|
|
@@ -32,6 +38,11 @@ Report changed files, commands run, results, and remaining risks.
|
|
|
32
38
|
Tell the worker to read repository instructions before editing. Do not ask it to
|
|
33
39
|
merge, push, deploy, or clean up its worktree.
|
|
34
40
|
|
|
41
|
+
Keep the **Environment** block. A fresh `git worktree` shares history, not build
|
|
42
|
+
state: no `node_modules`, no generated clients or route files. Without it workers
|
|
43
|
+
run the gates in a half-provisioned tree, fail for environment reasons, and report
|
|
44
|
+
those failures as if they were their own.
|
|
45
|
+
|
|
35
46
|
## Correction
|
|
36
47
|
|
|
37
48
|
```markdown
|
|
@@ -1,17 +1,55 @@
|
|
|
1
|
-
import type { SessionRecord } from '../../state/types'
|
|
1
|
+
import type { SessionRecord, WorktreeRecord } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { findMostRecentSession, loadSessionCatalog } from '../../state/session-catalog'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
6
|
+
* Where a command's target workspace came from. An orchestrator needs this to
|
|
7
|
+
* tell "I asked for pragma-once" from "aimux guessed, and the UI had moved on":
|
|
8
|
+
* `active` is the only origin that can silently follow the UI to another repo.
|
|
9
|
+
*/
|
|
10
|
+
export type WorkspaceOrigin = 'flag' | 'env' | 'active'
|
|
11
|
+
|
|
12
|
+
export interface ResolvedWorkspace {
|
|
13
|
+
origin: WorkspaceOrigin
|
|
14
|
+
record: SessionRecord
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Env pin for headless orchestrators — `--workspace` still wins over it. */
|
|
18
|
+
export const WORKSPACE_ENV_VAR = 'AIMUX_WORKSPACE'
|
|
19
|
+
|
|
20
|
+
/** The workspace's primary (root) worktree, i.e. the repository it is about. */
|
|
21
|
+
export function findPrimaryWorktree(session: SessionRecord): WorktreeRecord | undefined {
|
|
22
|
+
return session.worktrees?.find((worktree) => worktree.source === 'primary')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The repo every fresh worktree for this workspace is cut from. Surfaced in
|
|
27
|
+
* every worker envelope so an agent can see *which project* it just acted on
|
|
28
|
+
* instead of inferring it from a worktree path hash.
|
|
29
|
+
*/
|
|
30
|
+
export function workspaceRepoRoot(session: SessionRecord): string | null {
|
|
31
|
+
return findPrimaryWorktree(session)?.repoRoot ?? session.projectPath ?? null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Stable workspace identity block embedded in command output. */
|
|
35
|
+
export function workspaceIdentity(session: SessionRecord): {
|
|
36
|
+
id: string
|
|
37
|
+
name: string
|
|
38
|
+
repoRoot: string | null
|
|
39
|
+
} {
|
|
40
|
+
return { id: session.id, name: session.name, repoRoot: workspaceRepoRoot(session) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `--workspace W` to a session record from the catalog, reporting where
|
|
45
|
+
* the choice came from. Precedence: the explicit flag, then `AIMUX_WORKSPACE`,
|
|
46
|
+
* then the most recently opened session. Throws when the catalog is empty (no
|
|
47
|
+
* session has ever been created) or when the explicit name/id doesn't match.
|
|
10
48
|
*
|
|
11
49
|
* Matching: exact id wins; otherwise exact name (case-sensitive); otherwise
|
|
12
50
|
* unique case-insensitive name match.
|
|
13
51
|
*/
|
|
14
|
-
export function
|
|
52
|
+
export function resolveWorkspaceWithOrigin(name: string | undefined): ResolvedWorkspace {
|
|
15
53
|
const sessions = loadSessionCatalog()
|
|
16
54
|
if (sessions.length === 0) {
|
|
17
55
|
throw new Error(
|
|
@@ -19,14 +57,29 @@ export function resolveWorkspace(name: string | undefined): SessionRecord {
|
|
|
19
57
|
)
|
|
20
58
|
}
|
|
21
59
|
|
|
22
|
-
|
|
60
|
+
const flag = name !== undefined && name !== '' ? name : undefined
|
|
61
|
+
const env = process.env[WORKSPACE_ENV_VAR]
|
|
62
|
+
const fromEnv = env != null && env !== '' ? env : undefined
|
|
63
|
+
const selector = flag ?? fromEnv
|
|
64
|
+
if (selector === undefined) {
|
|
23
65
|
const active = findMostRecentSession(sessions)
|
|
24
66
|
if (!active) {
|
|
25
67
|
throw new Error('no active workspace and the catalog is empty')
|
|
26
68
|
}
|
|
27
|
-
return active
|
|
69
|
+
return { origin: 'active', record: active }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
origin: flag !== undefined ? 'flag' : 'env',
|
|
74
|
+
record: matchWorkspace(sessions, selector),
|
|
28
75
|
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveWorkspace(name: string | undefined): SessionRecord {
|
|
79
|
+
return resolveWorkspaceWithOrigin(name).record
|
|
80
|
+
}
|
|
29
81
|
|
|
82
|
+
function matchWorkspace(sessions: SessionRecord[], name: string): SessionRecord {
|
|
30
83
|
const byId = sessions.find((session) => session.id === name)
|
|
31
84
|
if (byId) return byId
|
|
32
85
|
|
|
@@ -27,7 +27,7 @@ import { awaitTurn, DEFAULT_TIMEOUT_MS, type TurnOutcome, turnOutcomeExitCode }
|
|
|
27
27
|
const QUESTION_TAIL_LINES = 25
|
|
28
28
|
|
|
29
29
|
export const tabAwait: CliCommand = {
|
|
30
|
-
args: [{ name: 'tabId', required: true }],
|
|
30
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
31
31
|
flags: [
|
|
32
32
|
...SHARED_FLAGS,
|
|
33
33
|
{
|
|
@@ -5,7 +5,7 @@ import { SHARED_FLAGS } from '../../flags'
|
|
|
5
5
|
import { EXIT_OK, writeJson } from '../../output'
|
|
6
6
|
|
|
7
7
|
export const tabClose: CliCommand = {
|
|
8
|
-
args: [{ name: 'tabId', required: true }],
|
|
8
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
9
9
|
flags: SHARED_FLAGS,
|
|
10
10
|
group: 'tab',
|
|
11
11
|
run: async (ctx) => {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
IPC_CAPABILITY_WORKER_METADATA,
|
|
14
14
|
} from '../../../ipc/protocol'
|
|
15
15
|
import { createPrefixedId } from '../../../platform/id'
|
|
16
|
+
import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
|
|
16
17
|
import {
|
|
17
18
|
type AssistantOption,
|
|
18
19
|
buildAssistantModelArgs,
|
|
@@ -98,6 +99,7 @@ async function rollbackCreatedWorktree(
|
|
|
98
99
|
repoPath: record.repoRoot,
|
|
99
100
|
targetPath: record.path,
|
|
100
101
|
})
|
|
102
|
+
await pruneEmptyWorktreeParent(record.path)
|
|
101
103
|
await daemon.expectOk('removeWorktreeRecord', {
|
|
102
104
|
sessionId: workspace.id,
|
|
103
105
|
worktreeId: record.id,
|
|
@@ -272,39 +274,61 @@ export const tabCreate: CliCommand = {
|
|
|
272
274
|
flags: [
|
|
273
275
|
...SHARED_FLAGS,
|
|
274
276
|
{
|
|
277
|
+
complete: { kind: 'dynamic', source: 'assistant' },
|
|
275
278
|
description: 'assistant id (claude, codex, opencode, grok, kimi, terminal, ...)',
|
|
276
279
|
kind: 'string',
|
|
277
280
|
name: 'assistant',
|
|
278
281
|
},
|
|
279
|
-
{ description: 'tab title (defaults to assistant label)', kind: 'string', name: 'title' },
|
|
280
|
-
{ description: 'cwd for the spawned PTY', kind: 'string', name: 'cwd' },
|
|
281
282
|
{
|
|
283
|
+
complete: { kind: 'none' },
|
|
284
|
+
description: 'tab title (defaults to assistant label)',
|
|
285
|
+
kind: 'string',
|
|
286
|
+
name: 'title',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
complete: { kind: 'file' },
|
|
290
|
+
description: 'cwd for the spawned PTY',
|
|
291
|
+
kind: 'string',
|
|
292
|
+
name: 'cwd',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
complete: { kind: 'none' },
|
|
282
296
|
description: 'explicit command (overrides the assistant default)',
|
|
283
297
|
kind: 'string',
|
|
284
298
|
name: 'command',
|
|
285
299
|
},
|
|
286
300
|
{
|
|
301
|
+
complete: { kind: 'none' },
|
|
287
302
|
description: 'model for the worker (maps to the assistant’s model flag)',
|
|
288
303
|
kind: 'string',
|
|
289
304
|
name: 'model',
|
|
290
305
|
},
|
|
291
306
|
{
|
|
307
|
+
complete: { kind: 'none' },
|
|
292
308
|
description: 'reasoning-effort level (maps to the assistant’s effort flag)',
|
|
293
309
|
kind: 'string',
|
|
294
310
|
name: 'effort',
|
|
295
311
|
},
|
|
296
312
|
{
|
|
313
|
+
complete: { kind: 'dynamic', source: 'worktree' },
|
|
297
314
|
description: 'worktree id the tab belongs to (defaults to the workspace’s active worktree)',
|
|
298
315
|
kind: 'string',
|
|
299
316
|
name: 'worktree',
|
|
300
317
|
},
|
|
301
318
|
{
|
|
319
|
+
complete: { kind: 'none' },
|
|
302
320
|
description: 'create a fresh worktree for this tab (optionally named: --new-worktree=<name>)',
|
|
303
321
|
kind: 'optional-string',
|
|
304
322
|
name: 'new-worktree',
|
|
305
323
|
},
|
|
306
|
-
{ description: 'base ref for --new-worktree (default HEAD)', kind: 'string', name: 'base' },
|
|
307
324
|
{
|
|
325
|
+
complete: { kind: 'dynamic', source: 'git-ref' },
|
|
326
|
+
description: 'base ref for --new-worktree (default HEAD)',
|
|
327
|
+
kind: 'string',
|
|
328
|
+
name: 'base',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
complete: { kind: 'none' },
|
|
308
332
|
description: 'branch for --new-worktree (default aimux/<name>)',
|
|
309
333
|
kind: 'string',
|
|
310
334
|
name: 'branch',
|
|
@@ -5,7 +5,7 @@ import { SHARED_FLAGS } from '../../flags'
|
|
|
5
5
|
import { EXIT_OK, writeJson } from '../../output'
|
|
6
6
|
|
|
7
7
|
export const tabFocus: CliCommand = {
|
|
8
|
-
args: [{ name: 'tabId', required: true }],
|
|
8
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
9
9
|
flags: SHARED_FLAGS,
|
|
10
10
|
group: 'tab',
|
|
11
11
|
run: async (ctx) => {
|
|
@@ -92,7 +92,12 @@ export async function writePromptPayload(
|
|
|
92
92
|
payload: PromptPayload,
|
|
93
93
|
appendEnter: boolean
|
|
94
94
|
): Promise<number> {
|
|
95
|
-
|
|
95
|
+
// Skip a zero-length write: "submit only" (empty payload + --enter) is a
|
|
96
|
+
// legitimate operation, but a 0-byte write reaches the pty as an empty
|
|
97
|
+
// ArrayBufferView and surfaces a raw Bun ERR_INVALID_ARG_TYPE to the user.
|
|
98
|
+
if (payload.data !== '') {
|
|
99
|
+
await daemon.expectOk('write', { data: payload.data, tabId })
|
|
100
|
+
}
|
|
96
101
|
let bytesWritten = Buffer.byteLength(payload.data, 'utf8')
|
|
97
102
|
if (appendEnter) {
|
|
98
103
|
// A bracketed paste swallows a same-burst `\r`, so settle first, then
|
|
@@ -20,10 +20,18 @@ import { awaitTurn, DEFAULT_TIMEOUT_MS, turnOutcomeExitCode } from './await-turn
|
|
|
20
20
|
import { buildPromptPayload, resolvePromptText, writePromptPayload } from './prompt-io'
|
|
21
21
|
|
|
22
22
|
export const tabRun: CliCommand = {
|
|
23
|
-
args: [
|
|
23
|
+
args: [
|
|
24
|
+
{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true },
|
|
25
|
+
{ complete: { kind: 'none' }, name: 'text' },
|
|
26
|
+
],
|
|
24
27
|
flags: [
|
|
25
28
|
...SHARED_FLAGS,
|
|
26
|
-
{
|
|
29
|
+
{
|
|
30
|
+
complete: { kind: 'file' },
|
|
31
|
+
description: 'read the prompt from this file',
|
|
32
|
+
kind: 'string',
|
|
33
|
+
name: 'prompt-file',
|
|
34
|
+
},
|
|
27
35
|
{ description: 'read the prompt from stdin', kind: 'boolean', name: 'stdin' },
|
|
28
36
|
{
|
|
29
37
|
description: 'overall turn cap in milliseconds (default 900000 = 15 min)',
|
|
@@ -9,7 +9,10 @@ import { buildPromptPayload, writePromptPayload } from './prompt-io'
|
|
|
9
9
|
const DEFAULT_AWAIT_TIMEOUT_MS = 15_000
|
|
10
10
|
|
|
11
11
|
export const tabSend: CliCommand = {
|
|
12
|
-
args: [
|
|
12
|
+
args: [
|
|
13
|
+
{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true },
|
|
14
|
+
{ complete: { kind: 'none' }, name: 'text' },
|
|
15
|
+
],
|
|
13
16
|
flags: [
|
|
14
17
|
...SHARED_FLAGS,
|
|
15
18
|
{ description: 'append \\r so the receiving CLI submits', kind: 'boolean', name: 'enter' },
|
|
@@ -24,6 +27,7 @@ export const tabSend: CliCommand = {
|
|
|
24
27
|
name: 'stdin',
|
|
25
28
|
},
|
|
26
29
|
{
|
|
30
|
+
complete: { kind: 'file' },
|
|
27
31
|
description: 'read the payload from this file instead of <text>',
|
|
28
32
|
kind: 'string',
|
|
29
33
|
name: 'prompt-file',
|
|
@@ -59,12 +63,13 @@ export const tabSend: CliCommand = {
|
|
|
59
63
|
? ctx.args.flags['await-timeout']
|
|
60
64
|
: DEFAULT_AWAIT_TIMEOUT_MS
|
|
61
65
|
|
|
62
|
-
// Uptake only means something once
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Uptake only means something once something submits: either the appended
|
|
67
|
+
// `\r` (--enter) or a chord that carries its own submit (--keys "<CR>",
|
|
68
|
+
// which is the recovery path for a prompt already sitting in a composer).
|
|
69
|
+
// Without one of those there is nothing to confirm, so fail loudly rather
|
|
70
|
+
// than block forever on a transition that can't come.
|
|
71
|
+
if (awaitSubmit && !appendEnter && !asKeys) {
|
|
72
|
+
throw new Error('--await-submit requires --enter or --keys (the chord carries the submit)')
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
// At most one payload source. Unlike `tab run`, zero sources is valid here
|
|
@@ -9,11 +9,12 @@ import { snapshotTailLines, snapshotToLines } from '../../snapshot-render'
|
|
|
9
9
|
const RENDER_WAIT_MS = 500
|
|
10
10
|
|
|
11
11
|
export const tabSnapshot: CliCommand = {
|
|
12
|
-
args: [{ name: 'tabId', required: true }],
|
|
12
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
13
13
|
flags: [
|
|
14
14
|
...SHARED_FLAGS,
|
|
15
15
|
{ description: 'return only the last N non-blank lines', kind: 'number', name: 'tail' },
|
|
16
16
|
{
|
|
17
|
+
complete: { kind: 'values', values: ['json', 'text'] },
|
|
17
18
|
description: 'output format: json (default) or text (raw screen dump)',
|
|
18
19
|
kind: 'string',
|
|
19
20
|
name: 'format',
|
|
@@ -21,7 +21,7 @@ function toCursor(snapshot: TerminalSnapshot): Cursor {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export const tabTail: CliCommand = {
|
|
24
|
-
args: [{ name: 'tabId', required: true }],
|
|
24
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
25
25
|
flags: [
|
|
26
26
|
...SHARED_FLAGS,
|
|
27
27
|
{
|
|
@@ -12,10 +12,11 @@ function isTabActivity(value: string): value is TabActivity {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const tabWait: CliCommand = {
|
|
15
|
-
args: [{ name: 'tabId', required: true }],
|
|
15
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
16
16
|
flags: [
|
|
17
17
|
...SHARED_FLAGS,
|
|
18
18
|
{
|
|
19
|
+
complete: { kind: 'values', values: ['idle', 'waiting-input', 'working'] },
|
|
19
20
|
description: 'target activity (idle | working | waiting-input)',
|
|
20
21
|
kind: 'string',
|
|
21
22
|
name: 'status',
|
|
@@ -5,27 +5,28 @@ import { writeJson } from '../../output'
|
|
|
5
5
|
import { DEFAULT_TIMEOUT_MS } from '../tab/await-turn'
|
|
6
6
|
import {
|
|
7
7
|
awaitExistingWorker,
|
|
8
|
-
|
|
8
|
+
resolveWorkerTarget,
|
|
9
9
|
workerEnvelope,
|
|
10
10
|
workerOutcomeExitCode,
|
|
11
11
|
workerView,
|
|
12
12
|
} from './shared'
|
|
13
13
|
|
|
14
14
|
export const workerAwait: CliCommand = {
|
|
15
|
-
args: [{ name: 'worker', required: true }],
|
|
15
|
+
args: [{ complete: { kind: 'dynamic', source: 'worker' }, name: 'worker', required: true }],
|
|
16
16
|
flags: [
|
|
17
17
|
...SHARED_FLAGS,
|
|
18
18
|
{ description: 'overall turn cap in milliseconds', kind: 'number', name: 'timeout' },
|
|
19
19
|
],
|
|
20
20
|
group: 'worker',
|
|
21
21
|
run: async (ctx) => {
|
|
22
|
-
const tab = await
|
|
22
|
+
const { tab, workspace } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
|
|
23
23
|
const outcome = await awaitExistingWorker(
|
|
24
24
|
ctx,
|
|
25
|
+
workspace,
|
|
25
26
|
tab.id,
|
|
26
27
|
typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS
|
|
27
28
|
)
|
|
28
|
-
writeJson(workerEnvelope(workerView(
|
|
29
|
+
writeJson(workerEnvelope(workspace, workerView(workspace, tab), outcome))
|
|
29
30
|
return workerOutcomeExitCode(outcome)
|
|
30
31
|
},
|
|
31
32
|
summary: "Await an existing worker's in-flight turn",
|
|
@@ -18,6 +18,11 @@ import {
|
|
|
18
18
|
isCommandAvailable,
|
|
19
19
|
parseCommand,
|
|
20
20
|
} from '../../../pty/command-registry'
|
|
21
|
+
import {
|
|
22
|
+
findPrimaryWorktree,
|
|
23
|
+
WORKSPACE_ENV_VAR,
|
|
24
|
+
workspaceRepoRoot,
|
|
25
|
+
} from '../../client/workspace-resolver'
|
|
21
26
|
import { SHARED_FLAGS } from '../../flags'
|
|
22
27
|
import { EXIT_OK, EXIT_RUNTIME, writeJson } from '../../output'
|
|
23
28
|
import { WORKER_SCHEMA_VERSION } from './shared'
|
|
@@ -56,7 +61,8 @@ export const workerDoctor: CliCommand = {
|
|
|
56
61
|
const missingManagerCapabilities = [MANAGER_CAPABILITY_WORKER_METADATA].filter(
|
|
57
62
|
(capability) => !managerCapabilities.includes(capability)
|
|
58
63
|
)
|
|
59
|
-
const primaryWorktree = workspace
|
|
64
|
+
const primaryWorktree = findPrimaryWorktree(workspace)
|
|
65
|
+
const workspaceOrigin = ctx.getWorkspaceOrigin?.() ?? 'active'
|
|
60
66
|
const availableAssistants = assistants.filter((assistant) => assistant.available)
|
|
61
67
|
const skillPath = fileURLToPath(
|
|
62
68
|
new URL('../../../../skills/aimux-orchestrator/', import.meta.url)
|
|
@@ -81,6 +87,16 @@ export const workerDoctor: CliCommand = {
|
|
|
81
87
|
if (!existsSync(skillPath)) {
|
|
82
88
|
issues.push(`packaged orchestrator skill is missing: ${skillPath}`)
|
|
83
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
|
+
}
|
|
84
100
|
const ready = issues.length === 0
|
|
85
101
|
writeJson({
|
|
86
102
|
assistants,
|
|
@@ -100,7 +116,12 @@ export const workerDoctor: CliCommand = {
|
|
|
100
116
|
skill: { ok: existsSync(skillPath), path: skillPath },
|
|
101
117
|
workspace: {
|
|
102
118
|
hasPrimaryWorktree: primaryWorktree !== undefined,
|
|
119
|
+
name: workspace.name,
|
|
103
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,
|
|
104
125
|
},
|
|
105
126
|
},
|
|
106
127
|
cliVersion: version,
|
|
@@ -116,10 +137,13 @@ export const workerDoctor: CliCommand = {
|
|
|
116
137
|
ready,
|
|
117
138
|
schemaVersion: WORKER_SCHEMA_VERSION,
|
|
118
139
|
skillPath,
|
|
140
|
+
warnings,
|
|
119
141
|
workspace: {
|
|
120
142
|
id: workspace.id,
|
|
121
143
|
name: workspace.name,
|
|
122
144
|
projectPath: workspace.projectPath ?? null,
|
|
145
|
+
repoRoot: workspaceRepoRoot(workspace),
|
|
146
|
+
source: workspaceOrigin,
|
|
123
147
|
},
|
|
124
148
|
})
|
|
125
149
|
return ready ? EXIT_OK : EXIT_RUNTIME
|
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
import type { CliCommand } from '../../registry'
|
|
2
2
|
|
|
3
|
+
import { workspaceIdentity } from '../../client/workspace-resolver'
|
|
3
4
|
import { SHARED_FLAGS } from '../../flags'
|
|
4
5
|
import { EXIT_OK, writeJson } from '../../output'
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
listNamedWorkerTabs,
|
|
8
|
+
listWorkerTargets,
|
|
9
|
+
resolveWorkerTarget,
|
|
10
|
+
WORKER_SCHEMA_VERSION,
|
|
11
|
+
workerView,
|
|
12
|
+
} from './shared'
|
|
6
13
|
|
|
7
14
|
export const workerList: CliCommand = {
|
|
8
|
-
args: [{ name: 'worker' }],
|
|
9
|
-
flags:
|
|
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
|
+
],
|
|
10
24
|
group: 'worker',
|
|
11
25
|
run: async (ctx) => {
|
|
12
26
|
const selector = ctx.args.positionals[0]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
17
58
|
writeJson({
|
|
18
59
|
schemaVersion: WORKER_SCHEMA_VERSION,
|
|
19
|
-
workers: tabs.map((tab) => workerView(
|
|
60
|
+
workers: tabs.map((tab) => workerView(workspace, tab)),
|
|
61
|
+
workspace: workspaceIdentity(workspace),
|
|
20
62
|
})
|
|
21
63
|
return EXIT_OK
|
|
22
64
|
},
|