| name | claw-dispatch |
| description | Use when dispatching work through the Hermes gateway with /claw, especially when the task may resolve slash commands or hand off into AO worker orchestration. |
Claw Dispatch
Default behavior — AO workers first
/claw defaults to spawning AO workers directly. Hermes gateway is the fallback, not the default.
| Input | Default action |
|---|
PR number (#633, PR 633, 633) | ao spawn --project <detected> --claim-pr N |
| General task description | ao spawn "<task>" |
--bidi prefix | Hermes interactive session (streaming) |
--hermes prefix | Force through Hermes gateway |
Slash command resolution (e.g. /green) | Resolve skill then AO spawn with it |
Project auto-detection for PR tasks: resolve from current git remote (git remote get-url origin).
When to use Hermes instead of AO: only when the user explicitly says --hermes, uses --bidi for interactive output, or the task is explicitly a Hermes-native operation (routing config, gateway status, etc.).
Overview
/claw routes work to AO workers by default. Use:
~/.claude/commands/ao.md
~/.claude/skills/ao-operator-discipline/SKILL.md
User-specified AO parameters remain mandatory.
Requirements
- For AO tasks:
ao session ls must not show ≥20 active sessions (spawn cap)
- For Hermes fallback: gateway must be healthy via
hermes gateway status
- Slash command resolution must search
.claude/commands first, then .claude/skills
Execution
When invoked with a task description:
TASK_DESCRIPTION="$ARGUMENTS"
set -euo pipefail
LOGDIR="/tmp/hermes"
mkdir -p "$LOGDIR"
chmod 700 "$LOGDIR" 2>/dev/null || true
STATUS_LOG="$(mktemp "$LOGDIR/.claw-status-XXXXXXXX")"
LAUNCHD_PLIST="$HOME/Library/LaunchAgents/ai.smartclaw.prod.plist"
if [ -f "$LAUNCHD_PLIST" ]; then
while IFS='=' read -r key value; do
[ -n "$key" ] || continue
export "$key=$value"
done < <(
python3 - "$LAUNCHD_PLIST" <<'PY'
import plistlib
import sys
path = sys.argv[1]
try:
with open(path, "rb") as fh:
data = plistlib.load(fh)
except FileNotFoundError:
raise SystemExit(0)
env = data.get("EnvironmentVariables") or {}
for key in ("HERMES_HOME", "HERMES_LOG_LEVEL"):
value = env.get(key)
if value:
print(f"{key}={value}")
PY
)
fi
export HERMES_HOME="${HERMES_HOME:-$HOME/.smartclaw_prod}"
HERMES_CFG="$HERMES_HOME/config.yaml"
if [ ! -f "$HERMES_CFG" ]; then
echo "Hermes config not found: $HERMES_CFG"
exit 1
fi
if ! python3 -c "import yaml; yaml.safe_load(open('$HERMES_CFG'))" 2>/dev/null; then
echo "Hermes config is not valid YAML: $HERMES_CFG"
exit 1
fi
HEALTH_LOG="$(mktemp "$LOGDIR/.claw-health-XXXXXXXX")"
if ! curl -sS -m 3 http://127.0.0.1:8642/health >"$HEALTH_LOG" 2>&1; then
echo "Hermes gateway is not reachable on :8642."
sed -n '1,20p' "$HEALTH_LOG"
exit 1
fi
if ! grep -q '"status": "ok"' "$HEALTH_LOG"; then
echo "Hermes gateway health check did not return ok."
sed -n '1,20p' "$HEALTH_LOG"
exit 1
fi
TASK_WITH_RESOLVED="$TASK_DESCRIPTION"
PR_NUMBER=$(printf '%s' "$TASK_DESCRIPTION" | python3 -c "
import sys, re
text = sys.stdin.read().strip()
m = re.match(r'^(?:PR\s*#?|#)\s*(\d+)$', text)
if m:
print(m.group(1))
" 2>/dev/null)
if [ -n "$PR_NUMBER" ]; then
REPO_SLUG=$(git remote get-url origin 2>/dev/null | python3 -c "
import sys, re
url = sys.stdin.read().strip()
m = re.match(r'(?:https?://github\.com/|git@github\.com:)([\w-]+/[\w-]+?)(?:\.git)?$', url)
if m:
print(m.group(1))
" 2>/dev/null || echo "")
if [ -n "$REPO_SLUG" ]; then
PR_URL="https://github.com/${REPO_SLUG}/pull/${PR_NUMBER}"
else
PR_URL="PR #${PR_NUMBER}"
fi
TASK_WITH_RESOLVED="Bring ${PR_URL} to 7-green. Fix CI failures, resolve review comments, address CodeRabbit issues, resolve merge conflicts, and ensure all checks pass. Use /green ${PR_NUMBER} to verify. Act autonomously — do not ask for permission to fix things."
TASK_DESCRIPTION="$TASK_WITH_RESOLVED"
fi
PR_URL=$(printf '%s' "$TASK_DESCRIPTION" | python3 -c "
import sys, re
m = re.search(r'https://github\.com/[\w-]+/[\w-]+/pull/\d+', sys.stdin.read())
if m:
print(m.group(0))
" 2>/dev/null)
if [ -n "$PR_URL" ]; then
TASK_WITH_RESOLVED="${TASK_DESCRIPTION}
IMPORTANT: You are acting as an autonomous agent. If you find that CI checks are failing, review threads are unresolved, or the PR needs remediation, automatically route to AO for remediation (ao spawn --claim-pr N). Do NOT ask 'Would you like me to route to AO?' — just do it. The user invoked /claw to get autonomous action, not a status report."
fi
SLASH_CMD=$(printf '%s' "$TASK_DESCRIPTION" | python3 -c "
import sys, re
text = sys.stdin.read().strip()
clean = re.sub(r'https?://\\S+', '', text)
m = re.search(r'(?:^|\\s)/([\\w-]+)', clean)
if m:
print(m.group(1))
" 2>/dev/null)
if [ -n "$SLASH_CMD" ]; then
RESOLVED_CONTENT=""
RESOLVED_SOURCE=""
for search_dir in ".claude/commands" "$HOME/.claude/commands"; do
if [ -f "$search_dir/$SLASH_CMD.md" ]; then
RESOLVED_CONTENT=$(cat "$search_dir/$SLASH_CMD.md" 2>/dev/null)
RESOLVED_SOURCE="$search_dir/$SLASH_CMD.md"
break
fi
done
if [ -z "$RESOLVED_CONTENT" ]; then
for search_dir in ".claude/skills" "$HOME/.claude/skills"; do
if [ -f "$search_dir/$SLASH_CMD/SKILL.md" ]; then
RESOLVED_CONTENT=$(cat "$search_dir/$SLASH_CMD/SKILL.md" 2>/dev/null)
RESOLVED_SOURCE="$search_dir/$SLASH_CMD/SKILL.md"
break
elif [ -f "$search_dir/$SLASH_CMD.md" ]; then
RESOLVED_CONTENT=$(cat "$search_dir/$SLASH_CMD.md" 2>/dev/null)
RESOLVED_SOURCE="$search_dir/$SLASH_CMD.md"
break
fi
done
fi
if [ -n "$RESOLVED_CONTENT" ]; then
echo "Resolved /$SLASH_CMD from $RESOLVED_SOURCE"
TASK_WITH_RESOLVED="The user asked: $TASK_DESCRIPTION
Below is the full definition of /$SLASH_CMD (resolved from $RESOLVED_SOURCE). Execute it as instructed:
---
$RESOLVED_CONTENT
---"
fi
fi
BIDI_MODE=false
CONTINUE_SESSION=""
if printf '%s' "$TASK_WITH_RESOLVED" | grep -q '^--bidi'; then
BIDI_MODE=true
TASK_WITH_RESOLVED=$(printf '%s' "$TASK_WITH_RESOLVED" | sed 's/^--bidi[[:space:]]*//')
fi
if printf '%s' "$TASK_WITH_RESOLVED" | grep -q '^--continue'; then
CONTINUE_SESSION=$(printf '%s' "$TASK_WITH_RESOLVED" | sed 's/^--continue[[:space:]]*//' | awk '{print $1}')
fi
if [ "$BIDI_MODE" = true ]; then
echo "Bidi mode: streaming Hermes response (synchronous)"
hermes chat \
-q "$TASK_WITH_RESOLVED" \
--yolo \
--max-turns 30 \
--source tool \
--pass-session-id
echo ""
echo "Session ended. To continue: /claw --continue <session_name>"
exit 0
fi
if [ -n "$CONTINUE_SESSION" ]; then
echo "Continuing Hermes session: $CONTINUE_SESSION"
hermes chat \
--continue "$CONTINUE_SESSION" \
--yolo \
--source tool
exit 0
fi
if printf '%s' "$TASK_WITH_RESOLVED" | grep -q '^--status'; then
STATUS_LOG=$(printf '%s' "$TASK_WITH_RESOLVED" | sed 's/^--status[[:space:]]*//' | awk '{print $1}')
if [ ! -f "$STATUS_LOG" ]; then
echo "❌ Log not found: $STATUS_LOG"
exit 1
fi
LOG_LINES=$(wc -l <"$STATUS_LOG")
LAST_LINE=$(tail -1 "$STATUS_LOG" 2>/dev/null)
PID_RUNNING=$(pgrep -f "hermes chat" | head -1)
echo "=== Claw Session Status ==="
echo "Log: $STATUS_LOG ($LOG_LINES lines)"
echo "Process: ${PID_RUNNING:-(completed)}"
echo "Last output: $LAST_LINE"
echo "--- Tail ---"
tail -20 "$STATUS_LOG"
exit 0
fi
CLAW_CHANNEL="${CLAW_CHANNEL:-C0B9W8D609M}"
CLAW_BOT_ID="${CLAW_BOT_ID:-U0AEZC7RX1Q}"
CLAW_TOKEN="${SLACK_MCP_XOXP_TOKEN:-${HERMES_SLACK_USER_TOKEN:-}}"
if [ -z "$CLAW_TOKEN" ]; then
echo "❌ No Slack user token (SLACK_MCP_XOXP_TOKEN) available — cannot dispatch via Slack."
echo " Set SLACK_MCP_XOXP_TOKEN (xoxp user token for jleechan) in env or launchd plist."
exit 1
fi
TASK_TEXT=$(printf '%s' "$TASK_WITH_RESOLVED" | sed -E 's|<@[UW][A-Z0-9]+>||g; s/^[[:space:]]+//; s|[[:space:]]+$||')
MESSAGE="[via /claw] <@${CLAW_BOT_ID}> ${TASK_TEXT}"
MESSAGE="${MESSAGE:0:39000}"
POST_LOG="$LOGDIR/.claw-post-$(date +%s).json"
curl -sS -m 15 -X POST \
-H "Authorization: Bearer $CLAW_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
--data-binary "$(python3 -c "import json,sys; print(json.dumps({'channel': '$CLAW_CHANNEL', 'text': sys.argv[1]}))" "$MESSAGE")" \
"https://slack.com/api/chat.postMessage" >"$POST_LOG" 2>&1
OK=$(python3 -c "import json; print(json.load(open('$POST_LOG')).get('ok', False))" 2>/dev/null)
if [ "$OK" != "True" ]; then
ERR=$(python3 -c "import json; print(json.load(open('$POST_LOG')).get('error', 'unknown'))" 2>/dev/null)
echo "❌ Slack post failed: ${ERR}"
echo " response: $POST_LOG"
exit 1
fi
THREAD_URL=$(python3 -c "
import json
d = json.load(open('$POST_LOG'))
ts = d.get('ts', '').replace('.', '')
ch = d.get('channel', '')
print(f'https://jleechanai.slack.com/archives/{ch}/p{ts}')
")
MSG_TS=$(python3 -c "import json; print(json.load(open('$POST_LOG')).get('ts', ''))")
echo "✅ Dispatched to Hermes via Slack"
echo " channel: #claw-dispatch"
echo " thread: $THREAD_URL"
echo " message_ts: $MSG_TS"
echo "⏳ Waiting for Hermes ack (up to 30s)..."
ACKED=0
for i in $(seq 1 30); do
sleep 1
REACT=$(curl -sS -m 3 -H "Authorization: Bearer $CLAW_TOKEN" \
"https://slack.com/api/reactions.get?channel=${CLAW_CHANNEL}×tamp=${MSG_TS}" 2>/dev/null)
REACT_HIT=$(printf '%s' "$REACT" | python3 -c "import json,sys
try:
d=json.load(sys.stdin)
print(d.get('ok', False) and bool(d.get('message', {}).get('reactions')))
except: print(False)" 2>/dev/null)
if [ "$REACT_HIT" = "True" ]; then
echo "✅ Hermes acked (reaction at ${i}s)"
ACKED=1
break
fi
REPLIES=$(curl -sS -m 3 -H "Authorization: Bearer $CLAW_TOKEN" \
"https://slack.com/api/conversations.replies?channel=${CLAW_CHANNEL}&ts=${MSG_TS}&limit=5" 2>/dev/null)
REPLY_HIT=$(printf '%s' "$REPLIES" | python3 -c "import json,sys
try:
d=json.load(sys.stdin)
print(d.get('ok', False) and len(d.get('messages', [])) > 1)
except: print(False)" 2>/dev/null)
if [ "$REPLY_HIT" = "True" ]; then
echo "✅ Hermes acked (thread reply at ${i}s)"
ACKED=1
break
fi
done
if [ "$ACKED" != "1" ]; then
echo "⚠️ No ack within 30s — Hermes may be rate-limited, stuck, or the channel is wrong."
echo " Check: $THREAD_URL"
exit 0
fi
echo ""
echo "✅ Dispatched via Slack #claw-dispatch"
echo " Thread: $THREAD_URL"
echo " Monitor: watch for Hermes replies in the thread above"
Post-dispatch output (MANDATORY)
After every /claw dispatch — AO or Hermes — always print these lines in your reply:
AO spawn path
✅ AO worker spawned: <session-name>
Attach: ao attach <session-name>
Dashboard: ao session ls --project <project>
Status: ao status <session-name>
Get the session name from ao session ls --project <project> immediately after spawn.
Hermes chat path (fallback)
✅ Hermes worker dispatched (PID: <pid>)
Log: <logfile>
Monitor: tail -f <logfile>
Kill: kill <pid>
Always emit the exact logfile path so the user can monitor or kill.
Slack path
✅ Dispatched via Slack #claw-dispatch
Thread: <thread_url>
Never omit these lines. The user must always know how to attach, monitor, or kill the worker without asking.
Notes
- Hermes replaced OpenClaw as the live gateway agent
- slash commands are resolved before dispatch
/claw sessions are hidden from normal Hermes session lists via --source tool
--yolo bypasses tool approvals for autonomous runs
- when
/claw leads to AO dispatch, spawned AO sessions must still be verified post-spawn
Modes
| Prefix | Behavior |
|---|
| (none) | Async fire-and-forget with ack detection (✅ ACK / ❌ RATE_LIMITED / ❌ FAILED) |
--bidi | Synchronous streaming — blocks until Hermes finishes, shows session ID for follow-up |
--continue <name> | Resume a previous --bidi session interactively |
--status <logfile> | Check tail + completion status of any async claw session |