aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-13 16:40:07 -0500
committerCraig Jennings <c@cjennings.net>2026-07-13 16:40:07 -0500
commit04c3b29a968d62eac1a4123cf03606fc667c2d14 (patch)
treedf10eb05ce73b74184bef5d8f64bb144a2a5d6c3
parent6cd3aa32b9f3b033c3890f08ab430e757696e5e3 (diff)
downloadrulesets-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-xclaude-templates/bin/ai98
-rw-r--r--scripts/tests/ai-launcher-runtime.bats58
-rw-r--r--todo.org6
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"* ]]
+}
diff --git a/todo.org b/todo.org
index 02215c9..d22eaea 100644
--- a/todo.org
+++ b/todo.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.