From 12a82cc6737580a82a757ac5344bcfe1685a065b Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 22 Jul 2026 08:54:50 -0400 Subject: [PATCH 1/3] docs(orchestrating-agent-relay): document plain-shell orchestration Co-Authored-By: Claude Fable 5 --- skills/orchestrating-agent-relay/SKILL.md | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/skills/orchestrating-agent-relay/SKILL.md b/skills/orchestrating-agent-relay/SKILL.md index 6eeb350..40764f3 100644 --- a/skills/orchestrating-agent-relay/SKILL.md +++ b/skills/orchestrating-agent-relay/SKILL.md @@ -269,6 +269,29 @@ the `message` group: `agent-relay message inbox check`, `agent-relay message post `, `agent-relay message reply `. +### Plain-Shell Orchestration (no relay MCP configured) + +An orchestrating session without the relay MCP still has full read access: +`agent-relay message list --limit 50` works with no credentials and +returns a JSON array (**newest first**; `[]` when the channel is empty — each +item carries `text`, `from.name`, `createdAt`, `kind`). Every **send**, however, +fails with `requires agentToken or agentClient` until the shell holds an agent +token. Mint one once per project and keep it inside the gitignored broker state +dir: + +```bash +TOKEN=$(agent-relay agent register orchestrator --type system | grep -oE 'at_live_[A-Za-z0-9_-]+' | head -1) +(umask 077 && printf '%s' "$TOKEN" > .agentworkforce/relay/orchestrator.token) + +TOKEN=$(cat .agentworkforce/relay/orchestrator.token) # later shells re-read it +RELAY_AGENT_TOKEN="$TOKEN" agent-relay message post general "checkpoint: reviews start after both DONEs" +RELAY_AGENT_TOKEN="$TOKEN" agent-relay message dm send Worker1 "NO-GO findings: …" +RELAY_AGENT_TOKEN="$TOKEN" agent-relay message inbox check +``` + +Never echo, commit, or log the token. Registering also makes `orchestrator` a +DM-able recipient for workers — the same identity the relay MCP auto-registers. + ### Monitoring Workers (Essential) Spawn/send/release commands are in the Quick Reference and Bootstrap Step 3 — @@ -287,6 +310,23 @@ real-time output when debugging. > `agent-relay node agent list` from inside the backgrounded loop rather than > blocking the foreground on `sleep`. +The push-style alternative to polling is a backgrounded `node tail` pipeline +used purely as a **wake-up trigger**: + +```bash +# Backgrounded; exits the moment a relay_inbound event carrying the marker arrives +agent-relay node tail | grep -m1 '"body":"DONE' +``` + +`relay_inbound` events carry each message's `body`, `from`, and `target`, and +the stream replays a bounded window before following live — a historical match +fires instantly, which is correct for first-occurrence waits. This wakes you +the second the event happens instead of on the next poll tick; it is still not +the durable log, so read the actual messages with `check_inbox` / +`list_messages` after waking. (Stock macOS has no `timeout(1)` — bound a +one-off listen with `python3` `subprocess.run(..., timeout=N)` or a +backgrounded kill.) + ### Troubleshooting ```bash @@ -463,6 +503,8 @@ named `target_node`). | Not monitoring workers | Attach with `agent-relay node agent attach --mode view` frequently to track progress | | Workers seem stuck | Inspect with `agent-relay node agent attach --mode view` for errors | | Messages not delivered | Check channel history with `list_messages(channel: "general")`; for new DMs use `check_inbox`, for already-read DM history use `list_dms` + `agent-relay message dm list ` | +| CLI send fails with `requires agentToken or agentClient` | Plain-shell sends need an agent token — register an `orchestrator` identity and pass it via `RELAY_AGENT_TOKEN` (see Plain-Shell Orchestration). Channel reads need no token | +| Monitor loop never matches the DONE post | `agent-relay message list ` returns **newest first**; in `node tail`, message text is in `relay_inbound` events' `body` field | | Tried to read replies with `node tail` | `node tail` streams broker events; `node tail --agent ` streams the worker's raw output — neither is durable messages. Read replies with `check_inbox` / `list_messages` / `get_message_thread` | | Worker DM to `broker` fails with `Agent "broker" not found` | Expected — `broker` is the broker's internal routing self-name, not a DM-able agent. Workers must ACK/DONE to `orchestrator` or `general`. Fix the worker task prompt; never instruct "DM the broker" | | `node status` says running but `node agent list`/MCP calls return empty or `Failed to query broker session` | The CLI is dialing a **stale/wrong broker** — leftover `.agentworkforce/relay/connection.json` from a prior run on an old port, or a second broker process. `ps aux \| grep -c '[a]gent-relay-broker'` (>1 ⇒ kill extras), compare `.agentworkforce/relay/connection.json` to the actual listening port, then `agent-relay node down --force`, delete `.agentworkforce/relay/`, `agent-relay node up` clean | From 509ed6648a929840527f0a277fb88d33f7f9f1b4 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 22 Jul 2026 09:03:35 -0400 Subject: [PATCH 2/3] docs(orchestrating-agent-relay): registration is explicit; harden token capture Co-Authored-By: Claude Fable 5 --- skills/orchestrating-agent-relay/SKILL.md | 50 ++++++++++++++--------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/skills/orchestrating-agent-relay/SKILL.md b/skills/orchestrating-agent-relay/SKILL.md index 40764f3..c02f322 100644 --- a/skills/orchestrating-agent-relay/SKILL.md +++ b/skills/orchestrating-agent-relay/SKILL.md @@ -17,8 +17,10 @@ A headless orchestrator is an agent that: 4. Coordinates work without human intervention The orchestrator drives the team and reads/sends/lists through the **Agent -Relay MCP server** (`agent-relay mcp`), which auto-registers the orchestrating -session as the `orchestrator` agent when a workspace key is present. Lifecycle +Relay MCP server** (`agent-relay mcp`). The session registers itself once with +the `register_agent` tool (name it `orchestrator`) — every messaging tool +errors with `Not registered. Call the "register_agent" tool first.` until it +does. Lifecycle control — starting the broker, spawning/releasing local agents, streaming broker debug events — goes through the `agent-relay node` command group. The workers it spawns are registered participants too; their peer-messaging @@ -158,8 +160,8 @@ runs in `interactive` spawn mode; pass `--exit-after-task` for a one-shot worker ### Step 3: Monitor and Coordinate -The orchestrator reads and sends through the relay MCP (it is auto-registered as -`orchestrator`): +The orchestrator reads and sends through the relay MCP (after its one-time +`register_agent` call): ```text # Read messages directed to you — DM replies, mentions, reactions @@ -280,8 +282,11 @@ token. Mint one once per project and keep it inside the gitignored broker state dir: ```bash +grep -qxF '.agentworkforce/' .gitignore 2>/dev/null || echo '.agentworkforce/' >> .gitignore TOKEN=$(agent-relay agent register orchestrator --type system | grep -oE 'at_live_[A-Za-z0-9_-]+' | head -1) +[ -n "$TOKEN" ] || { echo "registration failed — no token captured" >&2; exit 1; } (umask 077 && printf '%s' "$TOKEN" > .agentworkforce/relay/orchestrator.token) +chmod 600 .agentworkforce/relay/orchestrator.token # tighten a pre-existing file too TOKEN=$(cat .agentworkforce/relay/orchestrator.token) # later shells re-read it RELAY_AGENT_TOKEN="$TOKEN" agent-relay message post general "checkpoint: reviews start after both DONEs" @@ -290,7 +295,8 @@ RELAY_AGENT_TOKEN="$TOKEN" agent-relay message inbox check ``` Never echo, commit, or log the token. Registering also makes `orchestrator` a -DM-able recipient for workers — the same identity the relay MCP auto-registers. +DM-able recipient for workers — the same identity an MCP-based session creates +with the `register_agent` tool. ### Monitoring Workers (Essential) @@ -310,22 +316,25 @@ real-time output when debugging. > `agent-relay node agent list` from inside the backgrounded loop rather than > blocking the foreground on `sleep`. -The push-style alternative to polling is a backgrounded `node tail` pipeline -used purely as a **wake-up trigger**: +The push-style alternative to polling is a `node tail` pipeline used purely as +a **wake-up trigger**, run through the harness's background-task facility (or +with `&` in a plain shell — in the foreground it blocks until a match): ```bash -# Backgrounded; exits the moment a relay_inbound event carrying the marker arrives -agent-relay node tail | grep -m1 '"body":"DONE' +# Exits the moment a matching relay_inbound event arrives +agent-relay node tail | grep -m1 '"from":"Implementer".*"body":"DONE Implementer r2' ``` `relay_inbound` events carry each message's `body`, `from`, and `target`, and -the stream replays a bounded window before following live — a historical match -fires instantly, which is correct for first-occurrence waits. This wakes you -the second the event happens instead of on the next poll tick; it is still not -the durable log, so read the actual messages with `check_inbox` / -`list_messages` after waking. (Stock macOS has no `timeout(1)` — bound a -one-off listen with `python3` `subprocess.run(..., timeout=N)` or a -backgrounded kill.) +the stream replays a bounded window before following live. Scope the pattern to +the sender plus a run-specific marker — have workers post +`DONE ` — so a replayed event from an earlier +round can't fire it; a bare `"body":"DONE` match is only safe for a first, +one-shot wait. This wakes you the second the event happens instead of on the +next poll tick; it is still not the durable log, so read the actual messages +with `check_inbox` / `list_messages` after waking. (Stock macOS has no +`timeout(1)` — bound a one-off listen with `python3` +`subprocess.run(..., timeout=N)` or a backgrounded kill.) ### Troubleshooting @@ -366,7 +375,7 @@ Quick Reference. Then enforce this protocol: - In a harnessed environment, never wait with a bare foreground `sleep` (it is blocked) — run ACK/DONE poll loops with run_in_background or a Monitor/until-loop, polling `check_inbox` and `node agent list` from inside it -- **ACK/DONE target: `orchestrator` (the auto-registered spawning identity) or +- **ACK/DONE target: `orchestrator` (the registered spawning identity) or the `general` channel — NEVER `broker`.** `broker` is the broker's internal routing self-name, not a spawnable/DM-able agent: a worker DM to `broker` fails with `Agent "broker" not found`. Write the worker task prompt to DM @@ -527,6 +536,7 @@ named `target_node`). - Set `ANTHROPIC_API_KEY` or authenticate via `claude auth login` 3. **For MCP-based coordination**: run `agent-relay mcp` as the relay MCP stdio - server in your client's MCP settings. With a workspace key present it - auto-registers the session as `orchestrator`; messaging tools (`send_dm`, - `post_message`, `check_inbox`, …) then work from the orchestrating session. + server in your client's MCP settings, then register the session once with + the `register_agent` tool (as `orchestrator`). Messaging tools (`send_dm`, + `post_message`, `check_inbox`, …) work only after that call — before it they + fail with `Not registered. Call the "register_agent" tool first.` From f015c4886959a9c3f9931499acb506ca0aa142eb Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Wed, 22 Jul 2026 09:21:44 -0400 Subject: [PATCH 3/3] docs(orchestrating-agent-relay): anchor tail wake-up patterns on body start Co-Authored-By: Claude Fable 5 --- skills/orchestrating-agent-relay/SKILL.md | 35 ++++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/skills/orchestrating-agent-relay/SKILL.md b/skills/orchestrating-agent-relay/SKILL.md index c02f322..e6373a9 100644 --- a/skills/orchestrating-agent-relay/SKILL.md +++ b/skills/orchestrating-agent-relay/SKILL.md @@ -321,20 +321,33 @@ a **wake-up trigger**, run through the harness's background-task facility (or with `&` in a plain shell — in the foreground it blocks until a match): ```bash -# Exits the moment a matching relay_inbound event arrives -agent-relay node tail | grep -m1 '"from":"Implementer".*"body":"DONE Implementer r2' +# Exits the moment a matching relay_inbound event arrives. +# Anchor on the body VALUE starting with the marker — not a loose substring. +agent-relay node tail | grep -m1 '"body":"DONE Implementer r2' ``` `relay_inbound` events carry each message's `body`, `from`, and `target`, and -the stream replays a bounded window before following live. Scope the pattern to -the sender plus a run-specific marker — have workers post -`DONE ` — so a replayed event from an earlier -round can't fire it; a bare `"body":"DONE` match is only safe for a first, -one-shot wait. This wakes you the second the event happens instead of on the -next poll tick; it is still not the durable log, so read the actual messages -with `check_inbox` / `list_messages` after waking. (Stock macOS has no -`timeout(1)` — bound a one-off listen with `python3` -`subprocess.run(..., timeout=N)` or a backgrounded kill.) +the stream replays a bounded window before following live. Two independent traps +make a loose `grep` fire on the wrong event, and you need to defend against both: + +- **Replay + quoting.** A substring like `REVIEW VERDICT: GO` matches not only the + real verdict but every earlier message that *quoted* the marker — an ACK saying + "I'll post `REVIEW VERDICT: GO` when done" is in the replay window and fires + first. Anchoring the pattern on `"body":"` (the body value **beginning** + with the marker) rejects quotes, since a quoted marker sits mid-sentence after + some other opening text. +- **Your own outbound.** If you DM a worker the instructions containing the marker + string, that DM is a `relay_inbound` event too (with `"from":"orchestrator"`). + Add `"from":""` to the pattern, or anchor on the body as above, so your + own messages can't trip it. + +Give workers a run-specific marker (`DONE `) so a +replayed event from an earlier round can't match either. A bare `"body":"DONE` +substring is only safe for a first, one-shot wait in a fresh workspace. This +wakes you the second the event happens instead of on the next poll tick; it is +still not the durable log, so read the actual messages with `check_inbox` / +`list_messages` after waking. (Stock macOS has no `timeout(1)` — bound a one-off +listen with `python3` `subprocess.run(..., timeout=N)` or a backgrounded kill.) ### Troubleshooting