diff --git a/README.md b/README.md index 5c86155a..a90518ab 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,7 @@ Where `actas` switches *this* session to a different role, `spawn` brings up a * ``` /agmsg spawn codex reviewer # new codex agent, joins and becomes "reviewer" /agmsg spawn claude-code alice --window # new claude-code agent in a fresh tmux window +/agmsg spawn codex reviewer --effort xhigh # exact effort for this worker /agmsg spawn codex reviewer --boot-prompt "review the diff on this branch" # joins AND starts the task ``` @@ -193,9 +194,11 @@ Where `actas` switches *this* session to a different role, `spawn` brings up a * Pass `--boot-prompt ` to hand the new agent an initial task: the boot prompt becomes the actas slash command followed (newline-separated) by your text, so the agent claims its identity **and** acts on the task in the same first turn. This is the only way to give a one-shot goal to a **codex** peer, which has no Monitor and so never notices a message you `send` after it goes idle. +Pass `--effort ` to select the reasoning effort for this spawn. The value is passed through unchecked, while each agent type's manifest supplies its CLI-specific argument shape. For example, Claude receives `--effort high`, while Codex receives `-c model_reasoning_effort=xhigh`. A type that does not declare the effort fields rejects the option instead of silently ignoring it. + By default `spawn` **blocks until the new agent is actually listening** — its watcher attaches and touches a readiness sentinel — then prints `status=ready`, so you can send work the moment `spawn` returns without losing it to the agent's cold start. Use `--no-wait` for fire-and-forget, or `--ready-timeout ` to bound the wait (default 90; on timeout it prints `status=timeout` and exits 3 so a caller can re-spawn). Codex skips the wait (it has no Monitor). -Options: `--boot-prompt ` (initial task; see above), `--project ` (default: current project), `--team ` (auto-resolved when the project has a single team), and `--terminal ` / `$AGMSG_TERMINAL` / config `spawn.terminal` to override the terminal command on the non-tmux path (a `{cmd}` placeholder is replaced with the path to the generated boot script). On macOS the default opens whichever terminal you're currently in (iTerm or Terminal, via `$TERM_PROGRAM`) using `open -a` — a plain app launch, so it does **not** trigger the Automation/AppleScript permission prompts that scripting the terminal directly would. +Options: `--boot-prompt ` (initial task; see above), `--project ` (default: current project), `--team ` (auto-resolved when the project has a single team), `--effort `, and `--terminal ` / `$AGMSG_TERMINAL` / config `spawn.terminal` to override the terminal command on the non-tmux path (a `{cmd}` placeholder is replaced with the path to the generated boot script). On macOS the default opens whichever terminal you're currently in (iTerm or Terminal, via `$TERM_PROGRAM`) using `open -a` — a plain app launch, so it does **not** trigger the Automation/AppleScript permission prompts that scripting the terminal directly would. To always pass a given agent type extra CLI flags on spawn (e.g. a default permission mode or sandbox policy), set them in a YAML **spawn options** file — one section per type, a flat `--flag: value` map underneath. Path: `$AGMSG_SPAWN_OPTIONS_FILE`, else `~/.agmsg/config/spawn_options.yaml`; a missing file or section is a no-op. diff --git a/SKILL.md b/SKILL.md index f6d2c257..1cda9d7a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -116,6 +116,8 @@ Do NOT manually edit config files. Always use join.sh. # errors out. # --project project to launch in (default: $PWD) # --team team to join into (default: auto-resolved from project) +# --effort exact reasoning effort (passed through using the type +# manifest; Claude --effort, Codex model_reasoning_effort) # --window new tmux window instead of splitting the current one # --split h|v tmux split direction (default h) # --terminal terminal command template ({cmd} = path to the boot diff --git a/docs/agent-types.md b/docs/agent-types.md index a14e57b9..a57c4208 100644 --- a/docs/agent-types.md +++ b/docs/agent-types.md @@ -25,6 +25,8 @@ a manifest cannot execute code. Multi-value keys are whitespace-separated. | `spawnable` | — | `yes` if `spawn.sh` can launch this type | | `spawn` | — | a `.mjs` node-launcher (beside the manifest) `spawn.sh` runs via Node; also marks the type spawnable | | `model_arg` | — | the `--model`/`-m`-style flag `spawn --model ` passes through to the CLI; a type with no `model_arg` refuses `--model` | +| `effort_arg` | — | for direct-CLI types, the flag `spawn --effort ` uses (e.g. Claude's `--effort`, Codex's `-c`); a type with no `effort_arg` refuses `--effort` | +| `effort_value_prefix` | — | optional text prepended to the effort level before it is passed as the `effort_arg` value (e.g. Codex's `model_reasoning_effort=`); empty for a plain value such as Claude's `high` | | `prompt_arg` | — | for a CLI that does not accept the actas prompt as a bare positional argument, the named flag whose value IS the prompt (e.g. antigravity's `--prompt-interactive`, copilot's `--interactive`) — `spawn.sh` inserts it immediately before the (already-quoted) prompt. Unset = bare positional, the default | | `hooks_file` | yes | project-relative delivery hooks file (e.g. `.codex/hooks.json`) | | `monitor` | — | `yes` if the type exposes a native Monitor tool; `spawn` skips the readiness wait when `no` | @@ -83,6 +85,10 @@ as an external plugin (under `/plugins/types//` or a dir on with `agmsg plugin trust types/` — see [ADR 0002](adr/0002-driver-discovery-and-plugin-opt-in.md). +The core `--effort` mapping is for direct-CLI types. A node launcher owns its +effort configuration, so `spawn.sh` refuses core `--effort` for it even if its +manifest declares an `effort_arg`. + ## Adding a type 1. Create `scripts/drivers/types//type.conf` with at least `name`, @@ -109,6 +115,9 @@ name=codex template=template.md cli=codex spawnable=yes +model_arg=-m +effort_arg=-c +effort_value_prefix=model_reasoning_effort= detect=CODEX_SANDBOX CODEX_THREAD_ID detect_proc=codex codex-* hooks_file=.codex/hooks.json diff --git a/scripts/drivers/types/claude-code/template.md b/scripts/drivers/types/claude-code/template.md index e0c41547..14146cb9 100644 --- a/scripts/drivers/types/claude-code/template.md +++ b/scripts/drivers/types/claude-code/template.md @@ -164,10 +164,11 @@ If argument starts with "drop" followed by an agent name (e.g. "drop alice"): Otherwise (mode `turn` or `off`), leave it stopped. 4. Tell the user: "Dropped role `` from this project." -If argument starts with "spawn" (e.g. "spawn codex reviewer", "spawn claude-code alice --window"): -1. Parse `` (must be `claude-code` or `codex`), ``, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). +If argument starts with "spawn" (e.g. "spawn codex reviewer --effort xhigh", "spawn claude-code alice --window"): +1. Parse `` (must be `claude-code` or `codex`), ``, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). 2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh --project "$(pwd)" [options]` - spawn.sh pre-joins ``, then opens a tmux pane/window (when this session is inside tmux) or a new OS terminal, and launches the target CLI with `/__SKILL_NAME__ actas ` as its initial prompt. + - `--effort` selects the reasoning effort through the target type's manifest. Unsupported types reject it instead of ignoring it. - By default it BLOCKS until the new agent's watcher attaches and prints `status=ready` — so you can message `` right away. It prints `status=timeout` and exits 3 if not ready within `--ready-timeout` (default 90s); pass `--no-wait` for fire-and-forget. Codex skips the wait (no Monitor). - It refuses early if `` is already held by another live session, if the target CLI is not installed, or if there is no tmux and no usable terminal (headless). 3. Show the script's output. Do NOT TaskStop or relaunch this session's own Monitor — spawn affects a separate, newly launched agent, not this session's subscription. diff --git a/scripts/drivers/types/claude-code/type.conf b/scripts/drivers/types/claude-code/type.conf index 52aff2c5..5879c51d 100644 --- a/scripts/drivers/types/claude-code/type.conf +++ b/scripts/drivers/types/claude-code/type.conf @@ -4,6 +4,7 @@ template=template.md cli=claude spawnable=yes model_arg=--model +effort_arg=--effort # Flag that sets a session's display name (#339): spawn passes `-n -` # so the session is born named after its role -- meaningful in the prompt box and # the /resume picker, and the marker the resurrect hook keys on. Types without diff --git a/scripts/drivers/types/codex/template.md b/scripts/drivers/types/codex/template.md index 8dcf1195..6260fd85 100644 --- a/scripts/drivers/types/codex/template.md +++ b/scripts/drivers/types/codex/template.md @@ -121,10 +121,11 @@ If argument starts with "drop" followed by an agent name (e.g. "drop alice"): 3. If the session's active FROM was ``, clear that state. 4. Tell the user: "Dropped role `` from this project." -If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --window"): -1. Parse `` (must be `claude-code` or `codex`), ``, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). +If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --effort xhigh"): +1. Parse `` (must be `claude-code` or `codex`), ``, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). 2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh --project "$(pwd)" [options]` - spawn.sh pre-joins ``, then opens a tmux pane/window (when this session is inside tmux) or a new OS terminal, and launches the target CLI with `/__SKILL_NAME__ actas ` as its initial prompt. + - `--effort` selects the reasoning effort through the target type's manifest. Unsupported types reject it instead of ignoring it. - By default it BLOCKS until a spawned claude-code agent's watcher attaches (`status=ready`); `status=timeout` + exit 3 if not ready within `--ready-timeout` (default 90s). `--no-wait` for fire-and-forget. Spawning a codex agent skips the wait (codex has no Monitor). - It refuses early if `` is already held by another live session, if the target CLI is not installed, or if there is no tmux and no usable terminal (headless). 3. Show the script's output. diff --git a/scripts/drivers/types/codex/type.conf b/scripts/drivers/types/codex/type.conf index ce35a1ea..1b934cd5 100644 --- a/scripts/drivers/types/codex/type.conf +++ b/scripts/drivers/types/codex/type.conf @@ -13,6 +13,10 @@ resume_arg=resume # codex invokes a skill with "$", not Claude Code's "/" (see spawn.sh, #283). cmd_prefix=$ model_arg=-m +# Codex exposes reasoning effort through its generic config override: +# `codex -c model_reasoning_effort=`. +effort_arg=-c +effort_value_prefix=model_reasoning_effort= detect=CODEX_SANDBOX CODEX_THREAD_ID detect_proc=codex codex-* hooks_file=.codex/hooks.json diff --git a/scripts/drivers/types/hermes/template.md b/scripts/drivers/types/hermes/template.md index 19a5b9c1..751be46c 100644 --- a/scripts/drivers/types/hermes/template.md +++ b/scripts/drivers/types/hermes/template.md @@ -98,8 +98,8 @@ If argument starts with "drop" followed by an agent name (e.g. "drop alice"): 3. If the session's active FROM was ``, clear that state. 4. Tell the user: "Dropped role `` from this project." -If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --window"): -1. Parse `` (must be `claude-code` or `codex` — `hermes` itself is not spawnable via this command; its CLI has no mode that starts an interactive session pre-seeded with an initial prompt, #279), ``, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). +If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --effort xhigh"): +1. Parse `` (must be `claude-code` or `codex` — `hermes` itself is not spawnable via this command; its CLI has no mode that starts an interactive session pre-seeded with an initial prompt, #279), ``, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout `). 2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh --project "$(pwd)" [options]` 3. Show the script's output. diff --git a/scripts/spawn.sh b/scripts/spawn.sh index ebf26278..5f1aebdd 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -49,6 +49,10 @@ set -euo pipefail # 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. +# --effort launch the agent with a specific reasoning effort. The +# value is passed through unchecked; argument spelling and +# any value prefix come from manifest `effort_arg=` and +# `effort_value_prefix=`. Refused when effort_arg is absent. # --fresh force a brand-new session even when the role has a # resumable prior session. Without it, a type that supports # resume (manifest `resume_arg=`) is brought back into its @@ -140,6 +144,7 @@ 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 +EFFORT="" # --effort: pass-through effort level for the launched CLI FRESH=0 # --fresh: force a fresh session even if the role is resumable while [ $# -gt 0 ]; do @@ -157,6 +162,7 @@ while [ $# -gt 0 ]; do --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 ;; + --effort) EFFORT="${2:?--effort needs a level}"; shift 2 ;; --fresh) FRESH=1; shift ;; *) die "unknown option: $1" ;; esac @@ -222,6 +228,22 @@ if [ -n "$MODEL_ID" ] && [ -z "$MODEL_ARG" ]; then die "agent type '$AGENT_TYPE' does not support --model (no model_arg in its manifest)" fi +# --effort follows the same pass-through contract as --model, but some CLIs +# encode the value. Claude uses `--effort high`; Codex uses +# `-c model_reasoning_effort=high`. Both shapes stay manifest data: +# `effort_arg=` supplies the flag and optional `effort_value_prefix=` supplies +# the value prefix. Types without effort_arg refuse the option rather than +# guessing or silently ignoring it. +EFFORT_ARG="$(agmsg_type_get "$AGENT_TYPE" effort_arg)" +EFFORT_VALUE_PREFIX="$(agmsg_type_get "$AGENT_TYPE" effort_value_prefix)" +if [ -n "$EFFORT" ] && [ -n "$SPAWN_LAUNCHER" ]; then + die "agent type '$AGENT_TYPE' is a node-launcher type; its launcher owns effort configuration and core --effort is not supported" +fi +if [ -n "$EFFORT" ] && [ -z "$EFFORT_ARG" ]; then + die "agent type '$AGENT_TYPE' does not support --effort (no effort_arg in its manifest)" +fi +EFFORT_VALUE="${EFFORT_VALUE_PREFIX}${EFFORT}" + # Note: prompt_arg= (some CLIs require the actas prompt as a named flag's value # rather than a bare positional, e.g. antigravity's --prompt-interactive) is # resolved inside agmsg_role_cli_args (lib/boot-command.sh) now, so it stays in @@ -451,14 +473,14 @@ esac printf ' --initial-input %q\n' "$ACTAS_PROMPT" else # Direct-CLI launch: - # ` [ ] [ ] [spawn-options...] [ ] [] "/ actas "`. + # ` [ ] [ ] [ ] [spawn-options...] [ ] [] "/ actas "`. # cli is emitted unquoted — it is trusted fixed-prefix manifest data (see # above) that may itself be several tokens (e.g. `opencode run --interactive`). # The resume head (#339) is emitted RIGHT AFTER the cli, before all other # args: mandatory for a subcommand-shaped resume (codex `resume `), # harmless for a flag-shaped one (claude `--resume `) -- see - # agmsg_role_resume_head. model_arg is the manifest flag spelling (bare, not - # %q-quoted); the model id and every spawn-options token are quoted. The + # agmsg_role_resume_head. model_arg/effort_arg are manifest flag spellings + # (bare, not %q-quoted); their values and every spawn-options token are quoted. The # role-identity tail (name/prompt_arg + the actas prompt) is emitted by # agmsg_role_cli_args so its flag order matches resurrect-panes.sh. # MSYS_GUARD (#336) prefixes the CLI line as a command-local env assignment; @@ -466,6 +488,7 @@ esac printf '%s%s' "$MSYS_GUARD" "$CLI_BIN" agmsg_role_resume_head "$AGENT_TYPE" "$RESUME_UUID" [ -n "$MODEL_ID" ] && printf ' %s %q' "$MODEL_ARG" "$MODEL_ID" + [ -n "$EFFORT" ] && printf ' %s %q' "$EFFORT_ARG" "$EFFORT_VALUE" for _tok in ${SPAWN_OPT_TOKENS[@]+"${SPAWN_OPT_TOKENS[@]}"}; do printf ' %q' "$_tok" done diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index cecc90e8..09b45981 100644 --- a/tests/test_spawn.bats +++ b/tests/test_spawn.bats @@ -408,6 +408,52 @@ seed_resumable() { [[ "$output" != *"--model"* ]] } +# --- --effort: per-type manifest mapping, pass-through level --- + +@test "spawn --effort: claude-code launch includes --effort + level" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --effort high --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"claude --effort high"* ]] + [[ "$output" == *"actas"* ]] +} + +@test "spawn --effort: codex launch maps the level to model_reasoning_effort" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" codex alice --project "$PROJ" --effort max --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"codex -c model_reasoning_effort=max"* ]] +} + +@test "spawn --effort: model then effort preserve manifest-defined CLI order" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" codex alice --project "$PROJ" \ + --model gpt-test --effort xhigh --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" == *"codex -m gpt-test -c model_reasoning_effort=xhigh"* ]] +} + +@test "spawn --effort: refused for a type with no effort_arg in its manifest" { + run bash "$SCRIPTS/spawn.sh" grok-build alice --project "$PROJ" --effort high --no-wait + [ "$status" -ne 0 ] + [[ "$output" =~ "does not support --effort" ]] +} + +@test "spawn: no --effort leaves effort flags absent" { + bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ" + run bash "$SCRIPTS/spawn.sh" codex alice --project "$PROJ" --no-wait + [ "$status" -eq 0 ] + boot="$(cat "$CAPTURE")" + run cat "$boot" + [[ "$output" != *"model_reasoning_effort="* ]] +} + # --- newly spawnable types (#277): cursor, gemini, antigravity, copilot, opencode --- @test "spawn: cursor launches cursor-agent with a bare positional prompt" { diff --git a/tests/test_type_registry.bats b/tests/test_type_registry.bats index 8c8e38f6..9cf4891f 100644 --- a/tests/test_type_registry.bats +++ b/tests/test_type_registry.bats @@ -88,6 +88,15 @@ write_node_launcher_fixtures() { [ "$(g opencode detect_proc)" = "opencode opencode-*" ] } +@test "type-registry: effort mapping is manifest data" { + g() { env -i PATH="$PATH" bash -c "source '$SCRIPTS/lib/type-registry.sh'; agmsg_type_get $1 $2"; } + [ "$(g claude-code effort_arg)" = "--effort" ] + [ "$(g claude-code effort_value_prefix)" = "" ] + [ "$(g codex effort_arg)" = "-c" ] + [ "$(g codex effort_value_prefix)" = "model_reasoning_effort=" ] + [ "$(g grok-build effort_arg)" = "" ] +} + @test "type-registry: whoami detects codex end-to-end from CODEX_THREAD_ID" { # Join a codex agent so whoami has a registration to report, then call it with # no explicit type: detection must pick codex from the manifest's detect= key. @@ -210,6 +219,17 @@ write_node_launcher_fixtures() { ! echo "$output" | grep -q "unknown agent type" } +@test "spawn --effort: node-launcher types keep effort configuration launcher-owned" { + write_node_launcher_fixtures + printf 'effort_arg=--effort\n' >> \ + "$TEST_SKILL_DIR/scripts/drivers/types/nodetype/type.conf" + run "$SCRIPTS/spawn.sh" nodetype someagent --project "$BATS_TEST_TMPDIR" \ + --effort high + [ "$status" -ne 0 ] + [[ "$output" == *"node-launcher type"* ]] + [[ "$output" == *"core --effort is not supported"* ]] +} + @test "spawn: the node-launcher path also splices spawn-options tokens (before --initial-input) (#273)" { write_node_launcher_fixtures local proj="$BATS_TEST_TMPDIR/nodeproj"