diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-13 16:40:07 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-13 16:40:07 -0500 |
| commit | 04c3b29a968d62eac1a4123cf03606fc667c2d14 (patch) | |
| tree | df10eb05ce73b74184bef5d8f64bb144a2a5d6c3 | |
| parent | 6cd3aa32b9f3b033c3890f08ab430e757696e5e3 (diff) | |
| download | rulesets-04c3b29a968d62eac1a4123cf03606fc667c2d14.tar.gz rulesets-04c3b29a968d62eac1a4123cf03606fc667c2d14.zip | |
feat(launcher): add ai --runtime for choosing the agent CLI
The ai launcher can now start codex sessions: --runtime claude|codex (or AI_RUNTIME), a runtime-to-CLI map with local reserved behind a clear error until the local-model evaluation picks a CLI, and a runtime-aware dependency check. A new --print-launch mode prints the pane launch command without touching tmux or fzf, and is the seam the six new bats tests drive. Both current CLIs take the opening line as a positional prompt, so only the command name varies.
| -rwxr-xr-x | claude-templates/bin/ai | 98 | ||||
| -rw-r--r-- | scripts/tests/ai-launcher-runtime.bats | 58 | ||||
| -rw-r--r-- | todo.org | 6 |
3 files changed, 146 insertions, 16 deletions
diff --git a/claude-templates/bin/ai b/claude-templates/bin/ai index 994dc1f..c09faab 100755 --- a/claude-templates/bin/ai +++ b/claude-templates/bin/ai @@ -1,5 +1,5 @@ #!/bin/bash -# ai — Claude Code session launcher (unified aix + hey) +# ai — agent session launcher (unified aix + hey) # # Usage: # ai Select one or more projects via fzf and open each in @@ -11,6 +11,10 @@ # in the 'ai' session (new window or switch to existing). # Use '.' for current directory. Git prep per dir. # +# ai --runtime <rt> Launch with a different agent CLI: claude (default) or +# codex. 'local' is reserved for a local-LLM CLI, pending +# the model evaluation. Also settable via AI_RUNTIME. +# # ai --attach Attach to the existing 'ai' session without changes. # # ai -h | --help Show this help. @@ -25,7 +29,25 @@ # would kill the script. SESSION="ai" -CLAUDE_CMD="claude" +RUNTIME="${AI_RUNTIME:-claude}" + +# Map the runtime name to the agent CLI a pane launches. Both current CLIs +# take the opening instructions as a positional prompt, so only the command +# name varies; a runtime whose CLI needs different plumbing gets its own case. +resolve_agent_cmd() { + case "$RUNTIME" in + claude) AGENT_CMD="claude" ;; + codex) AGENT_CMD="codex" ;; + local) + echo "ai: the 'local' runtime is reserved but not wired yet (pending the local-model evaluation) — valid runtimes: claude, codex" >&2 + exit 2 + ;; + *) + echo "ai: unknown runtime '$RUNTIME' — valid runtimes: claude, codex" >&2 + exit 2 + ;; + esac +} # Run in the pane's shell just before Claude launches. `stty susp undef` clears # the tty's SIGTSTP (C-z) character for this pane only, so an accidental C-z is @@ -50,12 +72,14 @@ usage() { exit 0 } -for cmd in fzf tmux claude; do - if ! command -v "$cmd" &>/dev/null; then - echo "ai: $cmd is not installed" >&2 - exit 1 - fi -done +check_deps() { + for cmd in fzf tmux "$AGENT_CMD"; do + if ! command -v "$cmd" &>/dev/null; then + echo "ai: $cmd is not installed" >&2 + exit 1 + fi + done +} # ---------- shared helpers ---------- @@ -73,7 +97,7 @@ create_window() { wid=$(tmux new-window -a -t "$SESSION:{end}" -n "$name" -c "$dir" -P -F '#{window_id}') sleep 0.1 instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter echo "$wid" } @@ -319,7 +343,7 @@ single_mode() { local instructions wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter fi sort_windows @@ -379,7 +403,7 @@ multi_mode() { local instructions first_wid=$(tmux new-session -d -s "$SESSION" -n "$name" -c "$dir" -P -F '#{window_id}') instructions=$(build_instructions "$name") - tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$CLAUDE_CMD \"$instructions\"" Enter + tmux send-keys -t "$first_wid" "${LAUNCH_PREFIX}$AGENT_CMD \"$instructions\"" Enter for entry in "${selected[@]:1}"; do dir="${entry/#\~/$HOME}" name="$(basename "$dir")" @@ -403,12 +427,58 @@ multi_mode() { attach_session } +# Print the launch command a real run would send to the pane, then exit. +# Exists for the launcher's bats tests: exercises runtime resolution and the +# opening line with no tmux or fzf involved. +print_launch_mode() { + local arg="$1" dir name + dir="$(cd "$arg" 2>/dev/null && pwd)" || { echo "ai: cannot access '$arg'" >&2; exit 1; } + if [ ! -f "$dir/.ai/protocols.org" ]; then + echo "ai: $dir has no .ai/protocols.org — not an agent-template project" >&2 + exit 1 + fi + name="$(basename "$dir")" + printf '%s "%s"\n' "$AGENT_CMD" "$(build_instructions "$name")" + exit 0 +} + # ---------- dispatch ---------- +print_launch="" +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + ;; + --runtime) + [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex" >&2; exit 2; } + RUNTIME="$2" + shift 2 + ;; + --runtime=*) + RUNTIME="${1#--runtime=}" + shift + ;; + --print-launch) + print_launch=1 + shift + ;; + *) + break + ;; + esac +done + +resolve_agent_cmd + +if [ -n "$print_launch" ]; then + [ $# -eq 0 ] && { echo "ai: --print-launch needs a project directory" >&2; exit 2; } + print_launch_mode "$1" +fi + +check_deps + case "${1:-}" in - -h|--help) - usage - ;; --attach) attach_mode ;; diff --git a/scripts/tests/ai-launcher-runtime.bats b/scripts/tests/ai-launcher-runtime.bats new file mode 100644 index 0000000..4ec24bf --- /dev/null +++ b/scripts/tests/ai-launcher-runtime.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats +# The ai launcher's --runtime flag: pick the agent CLI (claude, codex) that +# a project window launches. Part of the generic-agent-runtime arc — the +# tmux-side counterpart of .emacs.d's ai-term multi-LLM handoff. The +# --print-launch mode exists for exactly these tests: it prints the launch +# command a real run would send to the pane, without touching tmux or fzf. + +setup() { + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" + AI="$REPO_ROOT/claude-templates/bin/ai" + PROJ="$(mktemp -d)" + mkdir -p "$PROJ/.ai" + touch "$PROJ/.ai/protocols.org" +} + +teardown() { + rm -rf "$PROJ" +} + +@test "default runtime launches claude" { + run bash "$AI" --print-launch "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == claude\ * ]] + [[ "$output" == *"protocols.org"* ]] +} + +@test "--runtime codex launches codex with the same opening line" { + run bash "$AI" --runtime codex --print-launch "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == codex\ * ]] + [[ "$output" == *"protocols.org"* ]] + [[ "$output" == *"$(basename "$PROJ")"* ]] +} + +@test "AI_RUNTIME env selects the runtime without the flag" { + AI_RUNTIME=codex run bash "$AI" --print-launch "$PROJ" + [ "$status" -eq 0 ] + [[ "$output" == codex\ * ]] +} + +@test "--runtime local is reserved and says why it is not wired" { + run bash "$AI" --runtime local --print-launch "$PROJ" + [ "$status" -eq 2 ] + [[ "$output" == *"not wired"* ]] +} + +@test "an unknown runtime errors and names the valid ones" { + run bash "$AI" --runtime frobnitz --print-launch "$PROJ" + [ "$status" -eq 2 ] + [[ "$output" == *"claude"* ]] + [[ "$output" == *"codex"* ]] +} + +@test "--print-launch refuses a non-project directory" { + run bash "$AI" --print-launch "$BATS_TEST_TMPDIR" + [ "$status" -eq 1 ] + [[ "$output" == *"protocols.org"* ]] +} @@ -272,8 +272,8 @@ Analysis in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]] *** 2026-07-13 Mon @ 13:34:17 -0500 Hook parity inventoried — only two hooks carry real porting work Full mapping in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]]. AskUserQuestion deny is moot off Claude (no popup tool to deny); PostToolUse validators survive via the bundles' git pre-commit hooks; clear-resume folds into the session-plumbing child; session-title is cosmetic. Real gaps: PreCompact priority-save (prose downgrade) and Stop wrap-teardown (Codex notify / manual elsewhere) — decisions in the VERIFY below. -*** TODO Launcher runtime flag -=ai --runtime <claude|codex|local>=. The =AI_AGENT_ID= shape already carries a runtime segment (=host.project.runtime.epoch=). Keep the change small and contained per the 2026-06-24 helper-task caveat — the helper's =ai --helper= touches the same launcher. +*** 2026-07-13 Mon @ 16:39:41 -0500 Launcher runtime flag built — ai --runtime claude|codex +Shipped TDD (new =scripts/tests/ai-launcher-runtime.bats=, 6 tests red→green): =--runtime= flag + =AI_RUNTIME= env on =bin/ai=, runtime→CLI mapping (claude default, codex 1:1 — both take the opening line as a positional prompt), runtime-aware dependency check, =local= reserved with a clear not-wired-yet error (pending the model-floor evaluation), and a =--print-launch= mode as the test seam (prints the pane launch command, no tmux/fzf). Kept small per the 2026-06-24 helper-task caveat — dispatch pre-parse only, no launcher restructure. The Emacs-side equivalent (ai-term multi-LLM) remains .emacs.d's June handoff. Live smoke: correct commands for both runtimes against real projects. *** TODO Session plumbing per runtime =/flush auto= self-inject types Claude-specific keystrokes (=/clear= + resume line); clear/resume mechanics differ per harness. Also confirm =session-context-path= and the suspend/startup anchor cycle behave identically when a non-Claude agent drives them. @@ -290,6 +290,8 @@ Recorded in [[file:docs/design/2026-07-13-runtime-portability-inventories.org][t *** TODO Local model floor evaluation The workflows assume long-context instruction-following (startup's multi-file read; the commits.md publish chain). Establish the minimum viable local tier (likely strong-70B+/MoE, 100k+ context), and what compensations a weaker model needs: shortened protocols, more checklist gates, more hook-level enforcement. Feeds the spec's "default local runtime/server" and "first supported local editing CLI" blocker decisions. +Environment inventory done 2026-07-13 (KB node "Local LLM inference inventory — daily drivers"): ratio is the inference box — Strix Halo iGPU + 125 GiB unified RAM, ollama 0.17.7 on demand (no systemd unit), llama3.1:8b + llama3.3 70B already in the 45 GB store. velox is out of scope (Iris Xe, 60 GiB, no ollama). So the floor evaluation can run today on ratio against the on-disk 70B: drive a scripted startup + a small publish-flow transcript through it and grade instruction-following. Remaining work is that evaluation, not procurement. + ** TODO [#C] Docs-lifecycle convention — manual validation :test: The human-eyes half of the docs-lifecycle acceptance surface. The convention shipped IMPLEMENTED 2026-07-04 (spec [[id:80b0787b-4a60-4c82-8a16-b383d3e3c8f2][docs-lifecycle]]); these checks confirm the human-facing behavior. A failed check promotes to a bug. |
