Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/drivers/types/claude-code/type.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/drivers/types/codex/type.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/drivers/types/grok-build/type.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion scripts/spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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: `<cli> [<model_arg> <model_id>] "/<cmd> actas <name>"`.
# 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'
Expand Down
47 changes: 46 additions & 1 deletion tests/test_spawn.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading