From 5b164778fd04a3cd9313b9db2748781f4b0ed7a5 Mon Sep 17 00:00:00 2001 From: Eotel <49769982+Eotel@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:36:30 +0900 Subject: [PATCH 1/3] feat(spawn): allow passing an initial prompt to the spawned agent Add an optional --prompt to spawn.sh. When provided, the boot prompt becomes the existing actas slash command followed (newline- separated) by , so the spawned 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: codex has no Monitor (docs/codex-monitor-beta.md), so a message sent after spawn is never noticed by the now-idle session. Carrying the task in the boot prompt sidesteps that with no monitor/bridge needed. With no --prompt, behavior is byte-for-byte unchanged. Claude-Session: https://claude.ai/code/session_01JD5uF5Z9Y1cydABGDFwCbA --- README.md | 5 ++++- scripts/spawn.sh | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) 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/scripts/spawn.sh b/scripts/spawn.sh index bfe2950..12b5c37 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -20,6 +20,12 @@ set -euo pipefail # 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. # --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 +104,7 @@ fi # --- Parse options --- PROJECT="$PWD" +PROMPT="" # --prompt: optional initial task appended to the actas prompt TEAM="" TMUX_TARGET="pane" # pane | window SPLIT="h" # h | v @@ -107,6 +114,7 @@ READY_TIMEOUT=90 # seconds to wait for readiness before giving up while [ $# -gt 0 ]; do case "$1" in + --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 +262,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 From 21d6a2fec3a736ae9aa65b6f006ce8f24a6080f1 Mon Sep 17 00:00:00 2001 From: Eotel <49769982+Eotel@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:04:14 +0900 Subject: [PATCH 2/3] test(spawn): cover --prompt initial-prompt behavior Add two bats tests for the new --prompt flag: - with --prompt, the generated boot script contains the actas command AND the task text; - without --prompt, the boot script carries no extra task text (guards the byte-identical claim). Also surface --prompt in the top-of-file usage synopsis, matching how the options block already documents it. Claude-Session: https://claude.ai/code/session_01JD5uF5Z9Y1cydABGDFwCbA --- scripts/spawn.sh | 1 + tests/test_spawn.bats | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/scripts/spawn.sh b/scripts/spawn.sh index 12b5c37..85f0acf 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -14,6 +14,7 @@ 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 diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index 4de694f..67ae910 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 From 03f4403167def9b2347b3cebfdf64ed3f7bcac26 Mon Sep 17 00:00:00 2001 From: Eotel <49769982+Eotel@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:29:45 +0900 Subject: [PATCH 3/3] test(spawn): add codex + empty-string coverage for --prompt; docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complements 21d6a2f (claude-code folding + backward-compat tests) on this branch. Adds the coverage it didn't: - codex folding: the task lands in a spawned codex's first prompt too — the no-Monitor case --prompt exists for; - the missing-arg guard; - `--prompt ""` as a no-op. Also fix `--prompt ""`: the parser used ${2:?...}, which rejects an explicit empty string and contradicts the existing `[ -n "$PROMPT" ]` no-op guard. Switch to ${2?...} so a MISSING arg still errors but an empty string degrades to a plain spawn (e.g. a scripted `--prompt "$VAR"` with an empty VAR). Document --prompt in SKILL.md (the agent-facing command surface) and llms.txt; the prior commits only updated README + the spawn.sh synopsis/options. --- SKILL.md | 6 ++++++ llms.txt | 1 + scripts/spawn.sh | 9 ++++++++- tests/test_spawn.bats | 41 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 1 deletion(-) 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 85f0acf..023c692 100755 --- a/scripts/spawn.sh +++ b/scripts/spawn.sh @@ -27,6 +27,7 @@ set -euo pipefail # 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 @@ -106,6 +107,8 @@ 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 @@ -115,7 +118,11 @@ READY_TIMEOUT=90 # seconds to wait for readiness before giving up while [ $# -gt 0 ]; do case "$1" in - --prompt) PROMPT="${2:?--prompt needs a task}"; shift 2 ;; + # `${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 ;; diff --git a/tests/test_spawn.bats b/tests/test_spawn.bats index 67ae910..c6d54ec 100644 --- a/tests/test_spawn.bats +++ b/tests/test_spawn.bats @@ -283,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"* ]] +}