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 @@ -180,13 +180,16 @@ 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 --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 `--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.

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: `--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: `--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.

Only `claude-code` and `codex` are supported today. macOS is the primary target; Linux and Windows are best-effort (please open an issue/PR if your terminal isn't handled). Headless environments — no tmux **and** no usable terminal — error out, since the agent CLIs need an interactive terminal.

Expand Down
6 changes: 6 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ Do NOT manually edit config files. Always use join.sh.
# --ready-timeout N seconds to wait for readiness (default 90; on timeout
# prints status=timeout and exits 3). Codex skips the
# wait (it has no Monitor).
# --prompt <text> hand the new agent an initial task: the boot prompt
# becomes the actas command followed (newline-separated)
# by <text>, so it claims its identity AND starts the task
# in its first turn. The only way to give a one-shot goal
# to a codex peer (no Monitor → a post-spawn send to its
# idle session is never noticed).
~/.agents/skills/agmsg/scripts/spawn.sh <claude-code|codex> <name> [options]

# Tear down a spawned member — the inverse of spawn.
Expand Down
1 change: 1 addition & 0 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Everyday commands (same across agents; shown here as the Claude Code `/agmsg` fo
/agmsg history # replay message history
/agmsg mode <monitor|turn|both|off> # switch delivery mode
/agmsg spawn <type> <name> # launch a new peer agent in its own terminal
/agmsg spawn <type> <name> --prompt "<task>" # ...and start it on <task> in its first turn
```

Alternate installs: `npx agmsg` (or `npm i -g agmsg && agmsg install`), or the Claude Code
Expand Down
25 changes: 25 additions & 0 deletions scripts/spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ set -euo pipefail
#
# Usage:
# spawn.sh <agent-type> <name> [options]
# spawn.sh <agent-type> <name> --prompt "<initial task>" [options]
#
# <agent-type> any registered type whose manifest is spawnable: a `cli=`
# binary (direct-CLI launch) or a `spawn=` node launcher
# <name> actas identity for the spawned agent
#
# Options:
# --prompt <text> an initial task for the spawned agent. When given, the
# boot prompt becomes the actas slash command followed
# (newline-separated) by <text>, so the new agent claims
# its identity AND acts on the task in its first turn —
# handy for a codex peer (no Monitor), where a message
# sent after spawn would never reach the idle session.
# An empty string (`--prompt ""`) means no task.
# --project <path> project to launch in (default: $PWD)
# --team <team> team to join <name> into (default: auto-resolved from
# the project's existing registrations; required when the
Expand Down Expand Up @@ -98,6 +106,9 @@ fi

# --- Parse options ---
PROJECT="$PWD"
PROMPT="" # --prompt: optional initial task appended to the actas prompt
# (empty string = no task, so the `[ -n "$PROMPT" ]` guard
# below leaves the boot prompt unchanged)
TEAM=""
TMUX_TARGET="pane" # pane | window
SPLIT="h" # h | v
Expand All @@ -107,6 +118,11 @@ READY_TIMEOUT=90 # seconds to wait for readiness before giving up

while [ $# -gt 0 ]; do
case "$1" in
# `${2?...}` (not `:?`) errors only when the arg is MISSING; an explicit
# empty string (`--prompt ""`) is allowed through and treated as "no task"
# by the `[ -n "$PROMPT" ]` guard, so a scripted `--prompt "$VAR"` with an
# empty VAR degrades to a plain spawn instead of aborting.
--prompt) PROMPT="${2?--prompt needs a task}"; shift 2 ;;
--project) PROJECT="${2:?--project needs a path}"; shift 2 ;;
--team) TEAM="${2:?--team needs a name}"; shift 2 ;;
--window) TMUX_TARGET="window"; shift ;;
Expand Down Expand Up @@ -254,8 +270,17 @@ AGMSG_RESOLVE_PROJECT=0 "$SCRIPT_DIR/join.sh" "$TEAM" "$NAME" "$AGENT_TYPE" "$PR
# have customized at install time (install.sh --cmd). Derive it from the skill
# dir basename so a custom install (e.g. `/m`) spawns `/m actas <name>` rather
# than a nonexistent `/agmsg actas <name>`.
#
# When --prompt is given, append the task newline-separated so the agent claims
# its identity AND acts on the task in the same first turn. This is the only way
# to hand a one-shot goal to a codex peer, which has no Monitor and so never
# notices a message sent after it goes idle (see docs/codex-monitor-beta.md).
CMD_NAME="$(basename "$SKILL_DIR")"
ACTAS_PROMPT="/${CMD_NAME} actas ${NAME}"
if [ -n "$PROMPT" ]; then
ACTAS_PROMPT="${ACTAS_PROMPT}
${PROMPT}"
fi

BOOT_DIR="${TMPDIR:-/tmp}/agmsg-spawn"
mkdir -p "$BOOT_DIR" 2>/dev/null || true
Expand Down
73 changes: 73 additions & 0 deletions tests/test_spawn.bats
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,38 @@ teardown() {
rm -rf "$custom"
}

@test "spawn: --prompt appends an initial task to the actas prompt" {
bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ"
run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --no-wait \
--prompt "review the diff"
[ "$status" -eq 0 ]

# The boot script still carries the actas slash command, and now ALSO the
# task text, so the spawned agent claims its identity AND acts on the task in
# its first turn. (printf %q escapes spaces, so assert on tokens.)
boot="$(cat "$CAPTURE")"
[ -f "$boot" ]
run cat "$boot"
[[ "$output" == *"actas"* ]]
[[ "$output" == *"alice"* ]]
[[ "$output" == *"review"* ]]
[[ "$output" == *"diff"* ]]
}

@test "spawn: without --prompt the boot script carries no extra task text" {
bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ"
run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --no-wait
[ "$status" -eq 0 ]

# Guards the byte-identical claim: with no --prompt, only the actas command
# is passed — no task text leaks into the boot script.
boot="$(cat "$CAPTURE")"
[ -f "$boot" ]
run cat "$boot"
[[ "$output" == *"actas"* ]]
[[ "$output" != *"review the diff"* ]]
}

@test "spawn: errors when \$TMUX is set but tmux is not on PATH" {
bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ"
# $TMUX set (we look like we're inside tmux) but a PATH that lacks the tmux
Expand Down Expand Up @@ -251,3 +283,44 @@ teardown() {
[ "$status" -eq 0 ]
[[ "$output" == *"skipping readiness wait"* ]]
}

# --- initial prompt (--prompt) ---
# spawn folds an optional initial task into the agent's first prompt: the boot
# prompt becomes the actas slash command followed (newline-separated) by the
# task, so the new agent claims its identity AND starts the task in one turn —
# the only way to hand a one-shot goal to a no-Monitor peer (codex). These tests
# assert on the generated boot script the terminal template is handed (captured
# via record.sh), the same way the actas-prompt tests above do.

@test "spawn: --prompt requires a task (missing arg errors)" {
run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --prompt
[ "$status" -ne 0 ]
[[ "$output" == *"--prompt needs a task"* ]]
}

@test "spawn: --prompt \"\" is treated as no task (no-op, not an error)" {
bash "$SCRIPTS/join.sh" myteam existing claude-code "$PROJ"
# An explicit empty string must NOT abort the spawn — it degrades to a plain
# spawn (so a scripted `--prompt "$VAR"` with an empty VAR still works).
run bash "$SCRIPTS/spawn.sh" claude-code alice --project "$PROJ" --no-wait --prompt ""
[ "$status" -eq 0 ]
boot="$(cat "$CAPTURE")"
run cat "$boot"
[[ "$output" == *"actas"* ]]
[[ "$output" == *"alice"* ]]
# No task appended → no newline-join → boot prompt unchanged.
[[ "$output" != *'\n'* ]]
}

@test "spawn: --prompt folds the initial task into the boot prompt (codex)" {
bash "$SCRIPTS/join.sh" myteam existing codex "$PROJ"
run bash "$SCRIPTS/spawn.sh" codex reviewer --project "$PROJ" \
--prompt "REVIEW_THE_DIFF"
[ "$status" -eq 0 ]
boot="$(cat "$CAPTURE")"
[ -f "$boot" ]
run cat "$boot"
[[ "$output" == *"actas"* ]]
[[ "$output" == *"reviewer"* ]]
[[ "$output" == *"REVIEW_THE_DIFF"* ]]
}