Skip to content
Open
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,19 @@ 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
```

`spawn <type> <name>` pre-joins `<name>`, then launches the target CLI with the actas slash command (`/<your-command> actas <name>`, matching your install command name) as its initial prompt. If the current session is inside **tmux**, it opens in a new pane (or `--window` for a new window, `--split h|v` for the direction); otherwise it opens a new **OS terminal** window.

Pass `--boot-prompt <text>` 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 <level>` 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 <secs>` 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 <text>` (initial task; see above), `--project <path>` (default: current project), `--team <team>` (auto-resolved when the project has a single team), and `--terminal <tmpl>` / `$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 <text>` (initial task; see above), `--project <path>` (default: current project), `--team <team>` (auto-resolved when the project has a single team), `--effort <level>`, and `--terminal <tmpl>` / `$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.

Expand Down
2 changes: 2 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Do NOT manually edit config files. Always use join.sh.
# errors out.
# --project <path> project to launch in (default: $PWD)
# --team <team> team to join into (default: auto-resolved from project)
# --effort <level> 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 <tmpl> terminal command template ({cmd} = path to the boot
Expand Down
9 changes: 9 additions & 0 deletions docs/agent-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>` passes through to the CLI; a type with no `model_arg` refuses `--model` |
| `effort_arg` | — | for direct-CLI types, the flag `spawn --effort <level>` 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` |
Expand Down Expand Up @@ -83,6 +85,10 @@ as an external plugin (under `<install_dir>/plugins/types/<name>/` or a dir on
with `agmsg plugin trust types/<name>` — 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/<name>/type.conf` with at least `name`,
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions scripts/drivers/types/claude-code/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>` from this project."

If argument starts with "spawn" (e.g. "spawn codex reviewer", "spawn claude-code alice --window"):
1. Parse `<type>` (must be `claude-code` or `codex`), `<name>`, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
If argument starts with "spawn" (e.g. "spawn codex reviewer --effort xhigh", "spawn claude-code alice --window"):
1. Parse `<type>` (must be `claude-code` or `codex`), `<name>`, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh <type> <name> --project "$(pwd)" [options]`
- spawn.sh pre-joins `<name>`, 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 <name>` 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 `<name>` 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 `<name>` 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.
Expand Down
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 @@ -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 <team>-<agent>`
# 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
Expand Down
5 changes: 3 additions & 2 deletions scripts/drivers/types/codex/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>`, clear that state.
4. Tell the user: "Dropped role `<name>` from this project."

If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --window"):
1. Parse `<type>` (must be `claude-code` or `codex`), `<name>`, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --effort xhigh"):
1. Parse `<type>` (must be `claude-code` or `codex`), `<name>`, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh <type> <name> --project "$(pwd)" [options]`
- spawn.sh pre-joins `<name>`, 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 <name>` 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 `<name>` 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.
Expand Down
4 changes: 4 additions & 0 deletions scripts/drivers/types/codex/type.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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=<level>`.
effort_arg=-c
effort_value_prefix=model_reasoning_effort=
detect=CODEX_SANDBOX CODEX_THREAD_ID
detect_proc=codex codex-*
hooks_file=.codex/hooks.json
Expand Down
4 changes: 2 additions & 2 deletions scripts/drivers/types/hermes/template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>`, clear that state.
4. Tell the user: "Dropped role `<name>` from this project."

If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --window"):
1. Parse `<type>` (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), `<name>`, and any options (`--project`, `--team`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
If argument starts with "spawn" (e.g. "spawn claude-code alice", "spawn codex reviewer --effort xhigh"):
1. Parse `<type>` (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), `<name>`, and any options (`--project`, `--team`, `--effort`, `--window`, `--split h|v`, `--terminal`, `--no-wait`, `--ready-timeout <secs>`).
2. Run: `~/.agents/skills/__SKILL_NAME__/scripts/spawn.sh <type> <name> --project "$(pwd)" [options]`
3. Show the script's output.

Expand Down
29 changes: 26 additions & 3 deletions scripts/spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <level> 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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -451,21 +473,22 @@ esac
printf ' --initial-input %q\n' "$ACTAS_PROMPT"
else
# Direct-CLI launch:
# `<cli> [<resume_arg> <uuid>] [<model_arg> <model_id>] [spawn-options...] [<name_arg> <name>] [<prompt_arg>] "/<cmd> actas <name>"`.
# `<cli> [<resume_arg> <uuid>] [<model_arg> <model_id>] [<effort_arg> <effort_value>] [spawn-options...] [<name_arg> <name>] [<prompt_arg>] "/<cmd> actas <name>"`.
# 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 <id>`),
# harmless for a flag-shaped one (claude `--resume <id>`) -- 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;
# emitted with %s (not %q) so it stays an assignment, not a single token.
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
Expand Down
46 changes: 46 additions & 0 deletions tests/test_spawn.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
Loading