refactor(tmux-launch-agent): extract tmux-open helper, add modelflag
This commit is contained in:
@@ -6,52 +6,26 @@ disable-model-invocation: true
|
|||||||
|
|
||||||
## Agent CLI Seed Data
|
## Agent CLI Seed Data
|
||||||
|
|
||||||
The agent config (binary, `args` convention per agent) and field meanings are in [`agents-seed.md`](agents-seed.md). Step 2 reads it to find the calling agent's entry.
|
Agent config (binary, `args`, `modelflag`) is in [`agents-seed.md`](agents-seed.md). Step 2 reads it by `name`.
|
||||||
|
|
||||||
|
## Dispatch
|
||||||
|
|
||||||
## Process
|
1. **Detect agent** — Run `./detect-agent` (sibling). On success, the agent name is known. On failure (exit 1), stop.
|
||||||
|
|
||||||
1. **Detect the calling agent** — Run `./detect-agent` (sibling to this skill). If it exits 1 (agent unknown), report the failure and stop — the agent name is required.
|
2. **Look up config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract `binary`, `args`, and `modelflag`.
|
||||||
|
|
||||||
Completion criterion: The agent name is known and non-empty.
|
3. **Parse flags** — Scan user arguments for flags (before prompt text). For each flag, extract its value and remove both the flag and value from the argument list:
|
||||||
|
- `--name <title>` / `-n <title>` — tmux window title
|
||||||
|
- `-c <path>` — working directory (default: project directory)
|
||||||
|
- `--model <name>` / `-m <name>` — model for child session
|
||||||
|
|
||||||
2. **Look up the agent config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract its `binary` and `args` fields.
|
Remaining text is the prompt. If `--name` is absent, tmux auto-names the window.
|
||||||
|
|
||||||
Completion criterion: The agent's entry is found and its `binary` and `args` are known.
|
4. **Build command** — Assemble the inner command:
|
||||||
|
- Start with `<binary>`.
|
||||||
3. **Parse flags** — Scan the user's arguments for optional flags (which must come before the prompt text):
|
- If model specified: append `<modelflag> <model-name>`.
|
||||||
- `--name <title>` or `-n <title>` — the tmux window title. Extract the title and remove the flag and its value from the arguments list.
|
- If prompt exists and `args` contains `{prompt}`: write to `/tmp/`, substitute path for `{prompt}`.
|
||||||
- `-c <path>` — the working directory for the new window. Extract the path and remove the flag and its value from the arguments list.
|
- If prompt exists and `args` is empty: pipe via `echo`.
|
||||||
|
- If no prompt: launch bare.
|
||||||
The remaining text after stripping both flags is the prompt for the child agent.
|
|
||||||
|
|
||||||
If `--name`/`-n` is absent, tmux auto-names the window.
|
|
||||||
If `-c` is absent, the new window inherits the current pane's working directory.
|
|
||||||
|
|
||||||
Completion criterion: The arguments are split into an optional window name, an optional directory path, and the remaining prompt text.
|
|
||||||
|
|
||||||
4. **Build the inner command** — Combine the agent config with the remaining user-supplied prompt arguments. The `args` template determines how the prompt is delivered:
|
|
||||||
|
|
||||||
- **Prompt-file agents** (`args` contains `{prompt}`) — Write the prompt text to a temporary file under `/tmp/` and substitute the file path for `{prompt}` in the args template. For example, an agent with `args: "@{prompt}"` becomes `<binary> @/tmp/tmux-launch-XXXX.md`.
|
|
||||||
- **Stdin-pipe agents** (`args` is empty) — Pipe the prompt text via `echo` into the binary.
|
|
||||||
- **No prompt** — If the user passed no arguments (after removing flags), launch the binary bare (interactive start) with no prompt file or pipe.
|
|
||||||
|
|
||||||
Completion criterion: The inner command is correctly built per the target agent's `args` convention (prompt-file, stdin-pipe, or bare).
|
|
||||||
|
|
||||||
5. **Wrap with the environment runner** — Detect whether `mise` is available via `command -v mise`:
|
|
||||||
|
|
||||||
- **mise available** — Wrap the inner command as `mise x --allow-env='*' -- <inner-command>`.
|
|
||||||
- **mise absent** — Wrap the inner command as `$SHELL -c '<inner-command>'`.
|
|
||||||
|
|
||||||
Completion criterion: A valid shell command string is ready.
|
|
||||||
|
|
||||||
6. **Fork into a new tmux window** — Build and run:
|
|
||||||
```
|
|
||||||
tmux new-window <name-flag> <dir-flag> "<shell-command>"
|
|
||||||
```
|
|
||||||
- `<shell-command>` — the wrapped command from step 5.
|
|
||||||
- `<name-flag>` — `-n "<title>"` if a window name was parsed in step 3, omitted otherwise.
|
|
||||||
- `<dir-flag>` — `-c <path>` if a directory was parsed in step 3, omitted otherwise.
|
|
||||||
|
|
||||||
Completion criterion: `tmux new-window` exits 0 and a new tmux window appears with the agent CLI session active.
|
|
||||||
|
|
||||||
|
5. **Open window** — Call `./tmux-open <window-name> <start-dir> <inner-command>` where `<inner-command>` is the assembled command from step 4 as separate arguments. On success, a new tmux window appears with the agent session active.
|
||||||
|
|||||||
@@ -7,28 +7,33 @@ agents:
|
|||||||
- name: pi
|
- name: pi
|
||||||
binary: pi
|
binary: pi
|
||||||
args: "@{prompt}"
|
args: "@{prompt}"
|
||||||
|
modelflag: "--model"
|
||||||
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
- name: opencode
|
- name: opencode
|
||||||
binary: opencode
|
binary: opencode
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "-m"
|
||||||
description: "OpenCode agent. Pipes stdin via cat."
|
description: "OpenCode agent. Pipes stdin via cat."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
- name: goose
|
- name: goose
|
||||||
binary: goose
|
binary: goose
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
description: "Goose agent. Accepts prompt file via -i flag."
|
description: "Goose agent. Accepts prompt file via -i flag."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
- name: codex
|
- name: codex
|
||||||
binary: codex
|
binary: codex
|
||||||
args: "@{prompt}"
|
args: "@{prompt}"
|
||||||
|
modelflag: "-m"
|
||||||
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
- name: claude
|
- name: claude
|
||||||
binary: claude
|
binary: claude
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
```
|
```
|
||||||
@@ -36,9 +41,10 @@ agents:
|
|||||||
## Field meanings
|
## Field meanings
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
| ------- | ------------- |
|
||||||
| `name` | Display name used in menus and `--agent` flag |
|
| `name` | Display name used in menus and `--agent` flag |
|
||||||
| `binary` | Command name expected on PATH |
|
| `binary` | Command name expected on PATH |
|
||||||
| `args` | Static arguments appended after the binary. May include `{prompt}` which is substituted at invocation time with the absolute path to the prompt file. Empty string means stdin piping (prompt is piped via `cat`). |
|
| `args` | Static arguments appended after the binary. May include `{prompt}` which is substituted at invocation time with the absolute path to the prompt file. Empty string means stdin piping (prompt is piped via `cat`). |
|
||||||
| `description` | Short human-readable description for the setup menu |
|
| `description` | Short human-readable description for the setup menu |
|
||||||
|
| `modelflag` | CLI flag used to select a model (e.g. `--model`, `-m`). Injected into the inner command when `--model <name>` is passed by the user. |
|
||||||
| `note` | Optional additional context |
|
| `note` | Optional additional context |
|
||||||
|
|||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# tmux-open — open a new tmux window (or session) and run a command
|
||||||
|
# Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
|
||||||
|
|
||||||
|
Open a new tmux window in the current session and run a command.
|
||||||
|
If not inside tmux, create a new session named after the window and attach.
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
window-name Name for the new tmux window (and session, if creating one)
|
||||||
|
start-dir Working directory for the new window
|
||||||
|
cmd [args...] Command to run (variadic)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-k, --keep After the command exits, keep the pane visible (remain-on-exit)
|
||||||
|
so you can inspect output. Default: pane closes when command exits.
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
If `mise` is on PATH, the command runs under `mise x`.
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
- tmux not installed
|
||||||
|
- fewer than 3 positional args
|
||||||
|
- start-dir does not exist
|
||||||
|
- a window with the same name already exists in the current session
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Flag parsing ---
|
||||||
|
keep_shell=0
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
-k|--keep) keep_shell=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Positional arg validation ---
|
||||||
|
if (( $# < 3 )); then
|
||||||
|
echo "error: expected at least 3 positional arguments (window-name, start-dir, cmd)" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
window_name="$1"; shift
|
||||||
|
start_dir="$1"; shift
|
||||||
|
cmd_args=("$@")
|
||||||
|
|
||||||
|
# --- Pre-flight checks ---
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "error: tmux is not installed or not on PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$start_dir" ]]; then
|
||||||
|
echo "error: directory does not exist: $start_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#cmd_args[@]} == 0 )); then
|
||||||
|
echo "error: no command specified" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Duplicate window name check (only relevant inside tmux)
|
||||||
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
|
if tmux list-windows -F '#{window_name}' | grep -Fxq "$window_name"; then
|
||||||
|
echo "error: a window named '$window_name' already exists in the current session" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Detect mise ---
|
||||||
|
has_mise=0
|
||||||
|
if command -v mise >/dev/null 2>&1; then
|
||||||
|
has_mise=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build the command for the new window ---
|
||||||
|
# We need to construct a single shell command string for tmux.
|
||||||
|
# The command runs; pane closes on exit (or stays visible with --keep via remain-on-exit).
|
||||||
|
|
||||||
|
# Build the inner command based on mode and mise availability
|
||||||
|
# For argv mode, quote each argument
|
||||||
|
quoted_args=()
|
||||||
|
for arg in "${cmd_args[@]}"; do
|
||||||
|
quoted_args+=("$(printf '%q' "$arg")")
|
||||||
|
done
|
||||||
|
if (( has_mise )); then
|
||||||
|
inner_cmd="mise x -- ${quoted_args[*]}"
|
||||||
|
else
|
||||||
|
inner_cmd="${quoted_args[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Create window or session ---
|
||||||
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
|
tmux new-window -n "$window_name" -c "$start_dir" "$inner_cmd"
|
||||||
|
if (( keep_shell )); then
|
||||||
|
tmux set-option remain-on-exit on
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tmux new-session -d -n "$window_name" -s "$window_name" -c "$start_dir" "$inner_cmd"
|
||||||
|
if (( keep_shell )); then
|
||||||
|
tmux set-option -t "$window_name" remain-on-exit on
|
||||||
|
fi
|
||||||
|
tmux attach-session -t "$window_name"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user