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
|
||||
|
||||
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."
|
||||
```
|
||||
@@ -36,9 +41,10 @@ agents:
|
||||
## Field meanings
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| ------- | ------------- |
|
||||
| `name` | Display name used in menus and `--agent` flag |
|
||||
| `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 |
|
||||
|
||||
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