diff --git a/README.md b/README.md index b47ec75..24a5557 100644 --- a/README.md +++ b/README.md @@ -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 ` pre-joins ``, then launches the target CLI with the actas slash command (`/ actas `, 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 ` 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 ` 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 ` (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: `--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. 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. diff --git a/SKILL.md b/SKILL.md index 1137b2c..2f76c3c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 hand the new agent an initial task: the boot prompt +# becomes the actas command followed (newline-separated) +# by , 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 [options] # Tear down a spawned member — the inverse of spawn. diff --git a/llms.txt b/llms.txt index 02be16d..9bd1796 100644 --- a/llms.txt +++ b/llms.txt @@ -25,6 +25,7 @@ Everyday commands (same across agents; shown here as the Claude Code `/agmsg` fo /agmsg history # replay message history /agmsg mode # switch delivery mode /agmsg spawn # launch a new peer agent in its own terminal +/agmsg spawn --prompt "" # ...and start it on in its first turn ``` Alternate installs: `npx agmsg` (or `npm i -g agmsg && agmsg install`), or the Claude Code diff --git a/scripts/spawn.sh b/scripts/spawn.sh index bfe2950..023c692 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -14,12 +14,20 @@ set -euo pipefail # # Usage: # spawn.sh [options] +# spawn.sh --prompt "" [options] # # any registered type whose manifest is spawnable: a `cli=` # binary (direct-CLI launch) or a `spawn=` node launcher # actas identity for the spawned agent # # Options: +# --prompt an initial task for the spawned agent. When given, the +# boot prompt becomes the actas slash command followed +# (newline-separated) by , 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 project to launch in (default: $PWD) # --team team to join into (default: auto-resolved from # the project's existing registrations; required when the @@ -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 @@ -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 ;; @@ -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 ` rather # than a nonexistent `/agmsg actas `. +# +# 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 diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index 4de694f..c6d54ec 100644 --- a/tests/test_spawn.bats +++ b/tests/test_spawn.bats @@ -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 @@ -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"* ]] +}