From 0daa11851d97a7ec6488de00321e9bec3b92170b Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Sun, 19 Jul 2026 09:52:18 -0400 Subject: [PATCH] refactor(tmux-launch-agent): extract tmux-open helper, add modelflag --- common/misc/tmux-launch-agent/SKILL.md | 58 +++------- common/misc/tmux-launch-agent/agents-seed.md | 8 +- common/misc/tmux-launch-agent/tmux-open | 115 +++++++++++++++++++ 3 files changed, 138 insertions(+), 43 deletions(-) create mode 100755 common/misc/tmux-launch-agent/tmux-open diff --git a/common/misc/tmux-launch-agent/SKILL.md b/common/misc/tmux-launch-agent/SKILL.md index 124bf38..db1b51c 100644 --- a/common/misc/tmux-launch-agent/SKILL.md +++ b/common/misc/tmux-launch-agent/SKILL.md @@ -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 ` / `-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. diff --git a/common/misc/tmux-launch-agent/agents-seed.md b/common/misc/tmux-launch-agent/agents-seed.md index e9f10bc..41a41b6 100644 --- a/common/misc/tmux-launch-agent/agents-seed.md +++ b/common/misc/tmux-launch-agent/agents-seed.md @@ -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 | diff --git a/common/misc/tmux-launch-agent/tmux-open b/common/misc/tmux-launch-agent/tmux-open new file mode 100755 index 0000000..6b50930 --- /dev/null +++ b/common/misc/tmux-launch-agent/tmux-open @@ -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