refactor(tmux-launch-agent): extract tmux-open helper, add modelflag

This commit is contained in:
2026-07-19 09:52:18 -04:00
parent 22c3b3e45f
commit 0daa11851d
3 changed files with 138 additions and 43 deletions
+16 -42
View File
@@ -6,52 +6,26 @@ disable-model-invocation: true
## 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.
3. **Parse flags** — Scan the user's arguments for optional flags (which must come before the prompt text):
- `--name <title>` or `-n <title>` — the tmux window title. Extract the title and remove the flag and its value from the arguments list.
- `-c <path>` — the working directory for the new window. Extract the path and remove the flag and its value from the arguments list.
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.
4. **Build command** — Assemble the inner command:
- Start with `<binary>`.
- If model specified: append `<modelflag> <model-name>`.
- If prompt exists and `args` contains `{prompt}`: write to `/tmp/`, substitute path for `{prompt}`.
- If prompt exists and `args` is empty: pipe via `echo`.
- If no prompt: launch bare.
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
binary: pi
args: "@{prompt}"
modelflag: "--model"
description: "My primary agent harness. Accepts prompt file via {prompt}."
- name: opencode
binary: opencode
args: ""
modelflag: "-m"
description: "OpenCode agent. Pipes stdin via cat."
note: "Uses stdin piping: args must be empty, prompt via pipe."
- name: goose
binary: goose
args: ""
modelflag: "--model"
description: "Goose agent. Accepts prompt file via -i flag."
note: "Uses stdin piping: args must be empty, prompt via pipe."
- name: codex
binary: codex
args: "@{prompt}"
modelflag: "-m"
description: "OpenAI Codex. Accepts prompt file via {prompt}."
- name: claude
binary: claude
args: ""
modelflag: "--model"
description: "Anthropic Claude CLI. Pipes stdin via cat."
note: "Uses stdin piping: args must be empty, prompt via pipe."
```
@@ -41,4 +46,5 @@ agents:
| `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`). |
| `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 |
+115
View File
@@ -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