From c1cad2ccfe60edf4ab4dc95f2838a9f2311fee0a Mon Sep 17 00:00:00 2001 From: fujibee Date: Wed, 24 Jun 2026 16:38:03 -0700 Subject: [PATCH] feat(spawn): add --model to launch a spawned agent on a chosen model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `spawn.sh --model ` launches the agent on a specific model. The id is pass-through — handed to the CLI unchecked (the CLI rejects an unknown id), so agmsg never has to track each vendor's model list, which would go stale. The model FLAG spelling differs per CLI, so it comes from the manifest `model_arg=`: claude-code uses `--model`, codex uses `-m`. A type with no model_arg has no known flag, so `--model` is refused with a clear error rather than guessed. The flag is inserted into the direct-CLI launch between the binary and the actas prompt. Tests: per-type flag spelling lands in the boot script (claude `--model`, codex `-m`); a type with no model_arg is refused; no --model leaves the launch flag-free. --- scripts/drivers/types/claude-code/type.conf | 1 + scripts/drivers/types/codex/type.conf | 1 + scripts/drivers/types/grok-build/type.conf | 1 + scripts/spawn.sh | 23 +++++++++- tests/test_spawn.bats | 47 ++++++++++++++++++++- 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/scripts/drivers/types/claude-code/type.conf b/scripts/drivers/types/claude-code/type.conf index 616a992..610c645 100644 --- a/scripts/drivers/types/claude-code/type.conf +++ b/scripts/drivers/types/claude-code/type.conf @@ -3,6 +3,7 @@ name=claude-code template=template.md cli=claude spawnable=yes +model_arg=--model detect=CLAUDE_CODE_SESSION_ID detect_proc=claude claude-code claude-* hooks_file=.claude/settings.local.json diff --git a/scripts/drivers/types/codex/type.conf b/scripts/drivers/types/codex/type.conf index 00225fd..33a0494 100644 --- a/scripts/drivers/types/codex/type.conf +++ b/scripts/drivers/types/codex/type.conf @@ -3,6 +3,7 @@ name=codex template=template.md cli=codex spawnable=yes +model_arg=-m detect=CODEX_SANDBOX CODEX_THREAD_ID detect_proc=codex codex-* hooks_file=.codex/hooks.json diff --git a/scripts/drivers/types/grok-build/type.conf b/scripts/drivers/types/grok-build/type.conf index 5565932..e0f60c2 100644 --- a/scripts/drivers/types/grok-build/type.conf +++ b/scripts/drivers/types/grok-build/type.conf @@ -3,6 +3,7 @@ name=grok-build template=template.md cli=grok spawnable=yes +model_arg=--model detect=GROK_SESSION_ID detect_proc=grok grok-* hooks_file=.grok/rules/agmsg.md diff --git a/scripts/spawn.sh b/scripts/spawn.sh index bfe2950..92534c5 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -37,6 +37,10 @@ set -euo pipefail # as the agent is launched (fire-and-forget) # --ready-timeout N seconds to wait for readiness before giving up # (default 90; on timeout, prints status=timeout, exit 3) +# --model launch the agent on a specific model. The id is passed +# through to the CLI unchecked (the CLI rejects unknown +# ids); the flag spelling comes from the type's manifest +# `model_arg=`. Refused for a type with no model_arg. # # Readiness: by default spawn blocks until the new agent's watcher attaches and # is receiving (it prints `status=ready ...`), so a leader can safely send work @@ -104,6 +108,7 @@ SPLIT="h" # h | v TERMINAL_TMPL="" # --terminal override (resolved below if empty) WAIT_READY=1 # block until the spawned agent's watcher attaches READY_TIMEOUT=90 # seconds to wait for readiness before giving up +MODEL_ID="" # --model: pass-through model id for the launched CLI while [ $# -gt 0 ]; do case "$1" in @@ -114,6 +119,7 @@ while [ $# -gt 0 ]; do --terminal) TERMINAL_TMPL="${2:?--terminal needs a template}"; shift 2 ;; --no-wait) WAIT_READY=0; shift ;; --ready-timeout) READY_TIMEOUT="${2:?--ready-timeout needs seconds}"; shift 2 ;; + --model) MODEL_ID="${2:?--model needs a model id}"; shift 2 ;; *) die "unknown option: $1" ;; esac done @@ -158,6 +164,16 @@ if [ -n "$CLI_BIN" ]; then elif [ -z "$SPAWN_LAUNCHER" ]; then die "agent type '$AGENT_TYPE' manifest declares neither a 'cli' binary nor a 'spawn' launcher" fi + +# --model is pass-through: the model id is handed to the CLI unchecked (the CLI +# rejects an unknown id), so agmsg never has to track each vendor's model list. +# The flag SPELLING differs per CLI, so it comes from the manifest `model_arg=` +# (e.g. claude-code/grok-build use --model, codex uses -m). A type with no +# model_arg has no known flag, so --model is refused rather than guessed. +MODEL_ARG="$(agmsg_type_get "$AGENT_TYPE" model_arg)" +if [ -n "$MODEL_ID" ] && [ -z "$MODEL_ARG" ]; then + die "agent type '$AGENT_TYPE' does not support --model (no model_arg in its manifest)" +fi # Resolve the node launcher path from the manifest (not hardcoded), if any. SPAWN_AGENT="" if [ -n "$SPAWN_LAUNCHER" ]; then @@ -278,7 +294,12 @@ BOOT="$BOOT.command" printf ' --project %q \\\n' "$PROJECT" printf ' --initial-input %q\n' "$ACTAS_PROMPT" else - printf '%q %q\n' "$CLI_BIN" "$ACTAS_PROMPT" + # Direct-CLI launch: ` [ ] "/ actas "`. + # model_arg is the manifest flag spelling (not %q-quoted — a bare flag like + # --model or -m); the model id is quoted. + printf '%q' "$CLI_BIN" + [ -n "$MODEL_ID" ] && printf ' %s %q' "$MODEL_ARG" "$MODEL_ID" + printf ' %q\n' "$ACTAS_PROMPT" fi echo 'rm -f "$0" 2>/dev/null' # self-clean once the agent exits echo 'exec "${SHELL:-/bin/bash}" -i' diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index beca2f0..ed3f76d 100644 --- a/tests/test_spawn.bats +++ b/tests/test_spawn.bats @@ -10,7 +10,7 @@ setup() { # a terminal. PATH is prepended so the stubs win. export STUB_BIN="$TEST_SKILL_DIR/stub-bin" mkdir -p "$STUB_BIN" - for bin in claude codex grok; do + for bin in claude codex grok hermes; do printf '#!/usr/bin/env bash\nexit 0\n' > "$STUB_BIN/$bin" chmod +x "$STUB_BIN/$bin" done @@ -167,6 +167,51 @@ teardown() { [[ "$output" != *"--trust"* ]] } +# --- --model (#135): per-type model flag, pass-through id --- + +@test "spawn --model: claude-code launch includes its --model flag + id" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --model claude-opus-4-8 --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"claude --model claude-opus-4-8"* ]] + [[ "$output" == *"actas"* ]] +} + +@test "spawn --model: codex launch uses its -m model flag" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" codex alice --project "$PROJ" --model gpt-5 --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"codex -m gpt-5"* ]] +} + +@test "spawn --model: grok-build launch uses its --model flag" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" grok-build alice --project "$PROJ" --model grok-build --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"grok --model grok-build"* ]] +} + +@test "spawn --model: refused for a type with no model_arg in its manifest" { + run bash "$SCRIPTS/spawn.sh" hermes foo --project "$PROJ" --model whatever --no-wait + [ "$status" -ne 0 ] + [[ "$output" =~ "does not support --model" ]] +} + +@test "spawn: no --model leaves the launch flag-free" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" != *"--model"* ]] +} + @test "spawn: actas prompt uses the install command name (not hardcoded agmsg)" { # Rename the skill dir to a custom command name and re-point SCRIPTS so the # script resolves SKILL_DIR basename = the custom name.