Install
$ agentstack add skill-zrozoom-agent-skills-core-dispatch-watcher-claude ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Dispatch Watcher Skill — Claude Code Adapter
This is the Claude Code-specific adapter for the shared agent-presence protocol. All shared logic (pre-flight, claim, lease, roster, takeover, presence event format) lives in .agent/skills/agent-presence/SKILL.md and scripts/agent-presence-helpers.sh — this file only contains Claude-specific glue.
When this Claude Code session starts (or when explicitly triggered by @claude # in ``), follow the workflow below.
Contract
- Input: environment variables
AGENT_ID(must start withclaude-),
GH_TOKEN_CLAUDE_BOT, SLACK_BOT_TOKEN, ROSTER_MESSAGE_TS, ROSTER_CHANNEL_ID, REPO. Slack MCP (mcp__claude_ai_Slack__*) available.
- Output: if a
@claudedispatch is pending, claim and execute it;
otherwise return idle. start is emitted only AFTER pre-flight passes (NOT on session boot — pre-flight uses tool calls itself, so emitting start first would create a circular requirement). After the shared helpers are sourced, end is emitted on session close via trap, with reason set in the EXIT_REASON shell variable (e.g., declined, idle, error, task-done).
- Side effects: posts comments on GitHub issues, opens branches/PRs,
posts presence events in ``.
- Scope: handle
@claude/@claude #for issue
implementation only. Ad-hoc @claude messages and @claude review # (review dispatch) are explicitly OUT of scope for this adapter — the implementation workflow below assumes a GitHub issue, not a PR.
Preconditions
AGENT_IDis set AND starts withclaude-:
```bash if [ -z "${AGENTID:-}" ]; then echo "Set AGENTID, e.g. claude-${HOSTNAME}-wsl-1" >&2 exit 1 fi
case "$AGENTID" in claude-) ;; ) echo "AGENTID '$AGENTID' is not a claude-* identity; this adapter is Claude-only" >&2 echo "Use the matching adapter for your model or fix AGENTID." >&2 exit 1 ;; esac ```
AGENT_ID is otherwise treated as an opaque registry key beyond the model prefix.
GH_TOKEN_CLAUDE_BOTis set and works:
```bash if [ -z "${GHTOKENCLAUDEBOT:-}" ]; then echo "Set GHTOKENCLAUDEBOT before running the Claude adapter." >&2 exit 1 fi
GHTOKEN="$GHTOKENCLAUDEBOT" gh auth status ```
- Slack MCP is reachable. The required channel is ``
(channel ID in ROSTER_CHANNEL_ID env). The pinned roster message TS is in ROSTER_MESSAGE_TS.
- The shared protocol skill and helpers script are present and sourceable.
Helpers expect GH_TOKEN_BOT (model-agnostic name) and REPO; the Claude adapter resolves GH_TOKEN_BOT from GH_TOKEN_CLAUDE_BOT and exports both before sourcing (a one-shot prefix assignment like GH_TOKEN_BOT=... source ... would only set the var for the source builtin itself, not for later function calls):
```bash if [ ! -f .agent/skills/agent-presence/SKILL.md ] || [ ! -f scripts/agent-presence-helpers.sh ]; then echo "Missing shared agent-presence dependencies (.agent/skills/agent-presence/SKILL.md or scripts/agent-presence-helpers.sh)" >&2 exit 1 fi
export GHTOKENBOT="$GHTOKENCLAUDE_BOT" export REPO="/" source scripts/agent-presence-helpers.sh ```
Workflow
0. Wire shutdown wrapper
After preconditions pass and scripts/agent-presence-helpers.sh is sourced, wire the trap-based shutdown wrapper before pre-flight or dispatch work so an end event is emitted on every clean runtime exit path. Use an EXIT_REASON shell variable the workflow updates throughout, so the trap message is meaningful:
EXIT_REASON="boot-incomplete" # default — overwritten as workflow progresses
emit_presence_end_once() {
local reason="${EXIT_REASON}"
trap - EXIT INT TERM HUP
emit_presence end "${reason}" || true
}
trap 'emit_presence_end_once' EXIT
trap 'EXIT_REASON="interrupted (${EXIT_REASON})"; emit_presence_end_once; exit 130' INT
trap 'EXIT_REASON="terminated (${EXIT_REASON})"; emit_presence_end_once; exit 143' TERM
trap 'EXIT_REASON="hangup (${EXIT_REASON})"; emit_presence_end_once; exit 129' HUP
(MUST NOT use exec for any subsequent command — that would replace the shell and prevent the trap from firing.)
1. Pre-flight check (mandatory)
if ! pre_flight_check; then
EXIT_REASON="declined (pre-flight conflict)"
echo "Pre-flight detected conflicts; aborting before claiming." >&2
# Pre-flight already wrote details to stderr. Operator decides next move.
# Note: NO `start` event was emitted — pre-flight uses tool calls itself,
# so emitting `start` first would create a circular requirement. The trap
# below emits `end (declined ...)` so the operator still sees a lifecycle
# event.
exit 0 # NOT 1 — this is intentional refusal, not an error
fi
Pre-flight covers:
- Local git state check (no uncommitted changes from peer agents)
- Read pinned roster (cache layer)
- Cross-ref GitHub SSOT (in-flight issues, open PRs, claim comments with
lease_until > now) - Reconcile + flag conflicts (legacy Phase 1 markers also detected via synthetic lease = created_at + 4h)
- Worktree ownership advisory (if uncommitted changes exist that look like another agent's work, warn — operator should
git worktree addfor parallel work)
2. Emit start presence event
start is emitted AFTER pre-flight passes (not on session boot). Top-level message in dispatch channel:
emit_presence start
EXIT_REASON="post-start" # workflow proceeded past start
3. Read dispatch channel for @claude mentions
Use Claude's Slack MCP (NOT the shell helpers' read functions) so the session has direct access to message content:
Tool: mcp__claude_ai_Slack__slack_read_channel
channel_id: ${ROSTER_CHANNEL_ID}
limit: 50
Filter messages matching the POSIX ERE pattern (^|[[:space:]])@claude[[:space:],:]+#?([0-9]+)([[:space:]]|$) (the leading (^|[[:space:]]) anchor stops substring matches like some-agent@claude5; the separator class catches @claude:, @claude,, plus whitespace; the trailing boundary stops the digit run from absorbing unrelated trailing text). The issue number is capture group 2. Skip messages older than 1h to avoid replaying stale dispatches. Scope is implementation only — @claude review # patterns are NOT actionable in this adapter and should be skipped silently.
4. For each actionable dispatch
Call the shared claim algorithm via helpers:
ISSUE_NUM=
# Default task type is 'quick' (30 min lease). For known long-running
# patterns (merge supervisor, content batch), declare 'standard' or 'long'
# upfront based on the issue body's "Acceptance Criteria" or "Estimate"
# section.
TASK_TYPE=quick # or standard|long based on issue inspection
if claim_issue "$ISSUE_NUM" "$TASK_TYPE"; then
# Won the race — proceed to execute
# claim_issue already emitted `busy` and updated roster
:
else
# Lost the race or eventual-consistency failed — claim_issue handled cleanup
continue # try next dispatch in the loop
fi
5. Execute the task
Use the standard Claude Code task workflow per CLAUDE.md:
- Read the issue body via
gh issue view - Branch:
git checkout -b fix/issue--orfeature/issue-- - Implement the change using Claude's Edit/Write tools
- Verify locally per the project's
Verification After Changeschecklist
(typecheck, lint, build, tests as applicable)
- Commit on the feature branch with a descriptive message
- Open PR with body
Closes #${ISSUE_NUM}and capture the number:
``bash PR_URL=$(gh pr create --fill --body "Closes #${ISSUE_NUM}") PR_NUM=$(gh pr view "$PR_URL" --json number --jq '.number') ``
6. Renew lease during long execution
If the task is taking longer than TTL/2 (15 min for quick, 1 h for standard, 2 h for long), renew the lease BEFORE expiration:
renew_lease "$ISSUE_NUM" # uses original TTL by default
Skip renewal once the PR is opened — the PR's commit activity itself proves liveness, and the takeover gate's "no GH activity" condition will fail safely for any peer agent.
7. Report completion
After the PR is opened (or task is blocked), update EXIT_REASON so the trap-fired end event later carries useful context:
# Success path
emit_presence ready "PR #${PR_NUM} done"
update_roster_chat_update --event ready --clear-task
EXIT_REASON="task-done (PR #${PR_NUM})"
# Block path
emit_presence ready "blocked #${ISSUE_NUM}: "
update_roster_chat_update --event ready --stale-reason "blocked #${ISSUE_NUM}"
# Also leave a :warning: thread reply on the original dispatch message
# (use mcp__claude_ai_Slack__slack_send_message with thread_ts)
emit_presence only posts the Slack presence message — it does not touch the pinned roster. Call update_roster_chat_update explicitly (as above) so the roster entry reflects the new state and the operator sees the agent is back to idle.
8. Idle exit
When the dispatch loop has no more actionable items, set the exit reason and exit cleanly. The trap fires the end event with the right context:
EXIT_REASON="idle (no actionable dispatches)"
exit 0
Slack MCP wiring (Claude-specific)
For CHANNEL READ + THREAD POST use Claude's Slack MCP tools directly:
| Action | MCP tool | |---|---| | Read 50 dispatch messages | mcp__claude_ai_Slack__slack_read_channel | | Read a thread for context | mcp__claude_ai_Slack__slack_read_thread | | Post a thread reply (:eyes:, :warning:) | mcp__claude_ai_Slack__slack_send_message with thread_ts | | Post a top-level message | helpers' emit_presence (uses curl + xoxb token) | | Update pinned roster | helpers' update_roster_chat_update (uses curl + chat.update) |
Why split between MCP and curl? The MCP tools work great for content reads/replies but are not available in the helpers shell library. The helpers use raw Slack Web API via curl for atomic operations (emit_presence, chat.update) so any CLI can call them. Claude can use either path for top-level posts; preferring helpers keeps the codebase DRY.
Required env summary
# Identity (per-machine)
export AGENT_ID="claude-${HOSTNAME}-wsl-1" # or your environment slug
# Bot tokens (per-machine .envrc / .env.local — never committed)
export GH_TOKEN_CLAUDE_BOT="ghp_..."
export SLACK_BOT_TOKEN="xoxb-..."
# Repository (matches .agent/context/project-ids.md)
export REPO="/"
# Roster pinned message (set up once per project; see project-ids.md)
export ROSTER_MESSAGE_TS=""
export ROSTER_CHANNEL_ID=""
# Helpers expect a model-agnostic name
export GH_TOKEN_BOT="$GH_TOKEN_CLAUDE_BOT"
# Source helpers (REPO must already be exported)
if [ ! -f .agent/skills/agent-presence/SKILL.md ] || [ ! -f scripts/agent-presence-helpers.sh ]; then
echo "Missing shared agent-presence dependencies (.agent/skills/agent-presence/SKILL.md or scripts/agent-presence-helpers.sh)" >&2
exit 1
fi
source scripts/agent-presence-helpers.sh
Out of scope
- Auto-dispatch by PM — separate spec
- Polling loop after first task done — Claude session exits after one
dispatch cycle for now; the operator restarts when needed
- Cost optimization (idle exit at 30 min, polling backoff)
- Cross-project dispatch (
@claude other-repo#123) - Slack DM dispatch — channel only
Shared protocol references
.agent/skills/agent-presence/SKILL.md— shared protocol contractscripts/agent-presence-helpers.sh— shared shell library.agent/skills/dispatch-watcher/SKILL.md— Codex adapter (parallel to
this one, same shape)
.agent/skills/dispatch-watcher-gemini/SKILL.md— Gemini adapter
Stop conditions (per shared agent-presence skill)
Stop and ask the human or PM agent when:
- Two active agents are editing the same non-generated file set
- A lease is expired but there is fresh PR/branch/commit activity
- The task requires overwriting uncommitted changes you did not make
- The roster and GitHub disagree in a way that changes ownership
- Secrets, tokens, or private Slack/GitHub identities would need to be exposed
If pre-flight detects any of these, exit cleanly (trap emits end) and let the operator decide.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ZroZoom
- Source: ZroZoom/agent-skills-core
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.