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.