
Claude Code Dispatch
- 10 installs
- 44 repo stars
- Updated March 3, 2026
- win4r/claude-code-dispatch
Helps with ai & agent building tasks.
About
claude-code-dispatch is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- claude-code-dispatch
- AI & Agent Building
- AI-coding skill
Claude Code Dispatch by the numbers
- 10 all-time installs (skills.sh)
- Ranked #11,937 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/win4r/claude-code-dispatch --skill claude-code-dispatchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 10 |
|---|---|
| repo stars | ★ 44 |
| Last updated | March 3, 2026 |
| Repository | win4r/claude-code-dispatch ↗ |
What it does
Helps with ai & agent building tasks.
Files
Claude Code Dispatch Skill
Dispatch development tasks to Claude Code with automatic notification on completion. Zero polling, zero token waste.
Architecture
dispatch.sh
→ write task-meta.json
→ launch Claude Code via claude_code_run.py (PTY wrapper)
→ [Agent Teams: --agents JSON defines Testing Agent + custom subagents]
→ Claude Code finishes → Stop/TaskCompleted hook fires automatically
→ notify-agi.sh reads meta + output
→ writes latest.json
→ sends Telegram notification (group + callback)
→ writes pending-wake.json (heartbeat fallback)Quick Reference
Basic dispatch
⚠️ Always use `nohup` + background (`&`) — dispatch runs until done.
nohup bash scripts/dispatch.sh \
-p "Build a Python REST API with FastAPI" \
-n "my-api" \
-g "-5006066016" \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-api \
> /tmp/dispatch-my-api.log 2>&1 &With Agent Teams
nohup bash scripts/dispatch.sh \
-p "Build a full-stack app with React + Express" \
-n "fullstack-app" \
--agent-teams \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/fullstack-app \
> /tmp/dispatch-fullstack.log 2>&1 &When --agent-teams is passed without --agents-json, a default Testing Agent is auto-defined via the --agents CLI flag (structured JSON, not prompt injection).
With cost controls
nohup bash scripts/dispatch.sh \
-p "Refactor the database layer" \
-n "db-refactor" \
--max-budget-usd 5.00 \
--max-turns 50 \
--fallback-model sonnet \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-refactor.log 2>&1 &With custom subagents
nohup bash scripts/dispatch.sh \
-p "Build CLI tool" \
-n "cli-tool" \
--agent-teams \
--agents-json '{"security-reviewer":{"description":"Security expert","prompt":"Review for vulnerabilities","tools":["Read","Grep","Glob"],"model":"opus"}}' \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/cli-tool \
> /tmp/dispatch-cli.log 2>&1 &With git worktree isolation
nohup bash scripts/dispatch.sh \
-p "Implement feature X" \
-n "feature-x" \
--worktree feature-x \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-feature.log 2>&1 &All Parameters
| Param | Short | Description |
|---|---|---|
--prompt | -p | Task description (required*) |
--prompt-file | Read prompt from file (required*) | |
--name | -n | Task name for tracking |
--group | -g | Telegram group ID for notifications |
--workdir | -w | Working directory |
--agent-teams | Enable Agent Teams mode | |
--agents-json | Custom subagent definitions (JSON) | |
--teammate-mode | Display: auto / in-process / tmux | |
--permission-mode | bypassPermissions / plan / acceptEdits / default | |
--allowed-tools | Tool allowlist | |
--disallowed-tools | Tool denylist | |
--model | Model override (sonnet/opus/haiku/full name) | |
--fallback-model | Auto-fallback when primary is overloaded | |
--max-budget-usd | Maximum dollar spend before stopping | |
--max-turns | Maximum agentic turns | |
--worktree | Git worktree name for isolation | |
--no-session-persistence | Don't save session to disk | |
--append-system-prompt | Append text to system prompt | |
--append-system-prompt-file | Append system prompt from file | |
--mcp-config | Load MCP servers from JSON file | |
--verbose | Enable verbose logging | |
--callback-group | Callback to dispatching agent's group | |
--callback-dm | DM callback user ID | |
--callback-account | DM callback bot account | |
--session | -s | Callback session key |
\* One of --prompt or --prompt-file is required.
Hook Setup
See references/hook-setup.md for full hook configuration. The skill uses Stop, TaskCompleted, and SessionEnd hooks with the notify-agi.sh script. HTTP hooks are also supported as an alternative.
Prompt Tips
See references/prompt-guide.md for examples and best practices, including cost control, Agent Teams, worktree isolation, custom subagents, and MCP integration.
Debugging
# Watch hook log
tail -f data/claude-code-results/hook.log
# Check latest result
cat data/claude-code-results/latest.json | jq .
# Check task metadata
cat data/claude-code-results/task-meta.json | jq .
# Test Telegram delivery
openclaw message send --channel telegram --target "-5006066016" --message "test"
# Check dispatch log
tail -f /tmp/dispatch-*.logGotchas
1. Must use PTY wrapper — Direct claude -p can hang in exec environments 2. Hook fires twice — Stop + SessionEnd both trigger; .hook-lock deduplicates (30s window) 3. Hook stdin is empty in PTY — Output read from task-output.txt, not stdin 4. tee pipe race — Hook sleeps 1s for pipe flush before reading output 5. Meta freshness — Hook validates meta age (<2h) and session ID 6. Agent Teams cost — Use --max-budget-usd to cap spend on multi-agent tasks 7. Rate limits — Claude Code has daily rate limits resetting at 11:00 UTC; check hook.log for "limit" messages
Claude Code Dispatch
一键分发开发任务到 Claude Code,任务完成后自动通过 Telegram 通知。零轮询,零 token 浪费。
这是一个 OpenClaw 技能,将 Claude Code CLI 封装成"发射后不管"的工作流:派发任务,去忙别的,完成后自动收到通知。
特性
- 发射即忘 —
nohup后台派发,通过 Stop Hook 自动回调 - Agent Teams — 通过结构化
--agentsJSON 定义多智能体并行开发,配备专职测试 Agent - 成本控制 —
--max-budget-usd花费上限 +--max-turns轮次限制 +--fallback-model过载自动降级 - Git Worktree 隔离 —
--worktree实现并行任务在独立分支中工作 - 自定义 Subagent — 通过
--agents-json定义专用 Agent(安全审计、测试、性能分析等) - 自动回调 — 支持群组通知、DM 回调、webhook 唤醒事件
- 富通知 — 任务状态、耗时、测试结果、文件树,一条 Telegram 消息全搞定
- PTY 包装器 — 在非 TTY 环境(CI、exec、cron)中也能可靠运行
- MCP 集成 — 通过
--mcp-config加载 MCP 服务器 - System Prompt 定制 —
--append-system-prompt/--append-system-prompt-file
架构
dispatch.sh
→ 写入 task-meta.json
→ 通过 claude_code_run.py (PTY) 启动 Claude Code
→ [Agent Teams: --agents JSON 定义 Testing Agent + 自定义子 Agent]
→ Claude Code 完成 → Stop/TaskCompleted Hook 自动触发
→ notify-agi.sh 读取 meta + 输出
→ 写入 latest.json
→ 发送 Telegram 通知
→ 写入 pending-wake.json(心跳兜底)快速开始
前置条件
- 已安装并配置 OpenClaw
- 已安装 Claude Code CLI(
claude命令) - 已在 OpenClaw 中配置 Telegram bot(用于通知)
安装
将技能复制到 OpenClaw 技能目录:
cp -r claude-code-dispatch ~/.openclaw/skills/
# 或创建软链接
ln -s /path/to/claude-code-dispatch ~/.openclaw/skills/claude-code-dispatch设置 Hook(详见 Hook 配置指南):
mkdir -p ~/.claude/hooks
cp scripts/notify-agi.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/notify-agi.sh在 ~/.claude/settings.json 中配置 hooks:
{
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }],
"TaskCompleted": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }]
}
}使用方法
⚠️ 必须使用 `nohup` + 后台运行(`&`) — dispatch 会持续运行直到 Claude Code 完成。
# 简单任务
nohup bash scripts/dispatch.sh \
-p "用 FastAPI 构建一个 Python REST API" \
-n "my-api" \
-g "-5006066016" \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-api \
> /tmp/dispatch-my-api.log 2>&1 &
# 使用 Agent Teams(并行开发 + 测试)
nohup bash scripts/dispatch.sh \
-p "用 FastAPI 构建一个 Python REST API" \
-n "my-api" \
--agent-teams \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-api \
> /tmp/dispatch-my-api.log 2>&1 &
# 带成本控制 + 模型降级
nohup bash scripts/dispatch.sh \
-p "重构认证模块" \
-n "auth-refactor" \
--max-budget-usd 5.00 \
--max-turns 50 \
--fallback-model sonnet \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-auth.log 2>&1 &
# 使用 git worktree 隔离
nohup bash scripts/dispatch.sh \
-p "实现功能 X" \
-n "feature-x" \
--worktree feature-x \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-feature.log 2>&1 &参数说明
| 参数 | 缩写 | 必填 | 说明 |
|---|---|---|---|
--prompt | -p | ✅* | 任务描述 |
--prompt-file | ✅* | 从文件读取 prompt | |
--name | -n | 任务名称(用于追踪) | |
--group | -g | Telegram 群组 ID(用于通知) | |
--workdir | -w | 工作目录(默认:当前目录) | |
--agent-teams | 启用 Agent Teams 模式 | ||
--agents-json | 自定义 subagent 定义(JSON 字符串) | ||
--teammate-mode | 显示模式:auto / in-process / tmux | ||
--permission-mode | bypassPermissions / plan / acceptEdits / default | ||
--allowed-tools | 工具白名单(如 "Read,Bash") | ||
--disallowed-tools | 工具黑名单 | ||
--model | 模型覆盖(sonnet/opus/haiku/完整名称) | ||
--fallback-model | 主模型过载时的降级模型 | ||
--max-budget-usd | 最大花费上限(美元),超出自动停止 | ||
--max-turns | 最大 Agent 轮次,超出自动停止 | ||
--worktree | Git worktree 名称,用于隔离 | ||
--no-session-persistence | 不保存 session 到磁盘 | ||
--append-system-prompt | 追加文本到系统提示词 | ||
--append-system-prompt-file | 从文件追加系统提示词 | ||
--mcp-config | MCP 服务器 JSON 配置文件路径 | ||
--verbose | 启用详细日志 | ||
--callback-group | 派发 agent 的回调群组 ID | ||
--callback-dm | DM 回调的 Telegram 用户 ID | ||
--callback-account | DM 回调的 Telegram bot 账号 |
\* --prompt 或 --prompt-file 二选一,必填。
Agent Teams
默认模式
启用 --agent-teams 但不传 --agents-json 时,dispatch 脚本会通过 Claude Code 的 --agents CLI 参数自动定义一个结构化的 Testing Agent:
{
"testing-agent": {
"description": "专职测试 Agent,负责全面的测试覆盖",
"prompt": "为所有代码变更编写并运行测试...",
"tools": ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
"model": "sonnet"
}
}这取代了旧版的 prompt 注入方式,使用 Claude Code 原生的 --agents 参数,让 Testing Agent 拥有独立的上下文窗口、工具限制和模型选择。
自定义 Subagent
通过 --agents-json 定义你自己的团队:
--agents-json '{
"security-reviewer": {
"description": "代码安全审查专家",
"prompt": "关注 OWASP Top 10 安全问题...",
"tools": ["Read", "Grep", "Glob"],
"model": "opus"
},
"perf-analyst": {
"description": "性能分析和优化专家",
"prompt": "对代码进行性能分析并提出优化建议...",
"tools": ["Read", "Bash", "Grep"],
"model": "sonnet"
}
}'每个子 Agent 是独立的 Claude Code 进程,拥有独立上下文窗口,共享同一文件系统。
成本控制
通过以下参数控制花费:
| 参数 | 说明 |
|---|---|
--max-budget-usd 5.00 | 硬性花费上限(美元) |
--max-turns 50 | 最大 Agent 轮次 |
--fallback-model sonnet | 主模型过载时自动切换 |
这对 Agent Teams 尤为重要,多 Agent 任务的 token 消耗显著增加。
Git Worktree 隔离
使用 --worktree <名称> 在隔离的 git worktree 中运行任务:
--worktree feature-auth
# Claude Code 在 <repo>/.claude/worktrees/feature-auth 中运行这允许多个并行 dispatch 任务在同一仓库上工作而不冲突。
自动回调检测
如果没有传 --callback-group 或 --callback-dm,脚本会在工作目录中查找 dispatch-callback.json:
// 群组回调
{ "type": "group", "group": "-5189558203" }
// DM 回调
{ "type": "dm", "dm": "8009709280", "account": "coding-bot" }
// Wake 钩子(主 agent 用)
{ "type": "wake" }Hook 事件
通知 hook(notify-agi.sh)处理多个 Claude Code 生命周期事件:
| 事件 | 触发时机 | 用途 |
|---|---|---|
Stop | Claude 完成响应时 | 主要的完成信号 |
TaskCompleted | 任务被明确标记为完成 | 精确完成检测(Agent Teams) |
SessionEnd | 会话终止时 | 兜底信号 |
内置去重机制(.hook-lock,30 秒窗口)防止重复通知。
也支持 HTTP hooks 作为替代方案 — 详见 Hook 配置指南。
结果文件
所有结果写入 data/claude-code-results/:
| 文件 | 内容 |
|---|---|
latest.json | 完整结果(输出、任务名、群组、时间戳) |
task-meta.json | 任务元数据(prompt、工作目录、状态、成本参数) |
task-output.txt | Claude Code 原始输出 |
pending-wake.json | 心跳兜底通知 |
hook.log | Hook 执行日志 |
调试
# 查看 hook 日志
tail -f data/claude-code-results/hook.log
# 检查最新结果
cat data/claude-code-results/latest.json | jq .
# 检查任务元数据
cat data/claude-code-results/task-meta.json | jq .
# 测试 Telegram 发送
openclaw message send --channel telegram --target "-5006066016" --message "test"注意事项
1. 必须使用 PTY 包装器 — 直接 claude -p 在 exec 环境中会挂起 2. Hook 会触发两次 — Stop + SessionEnd 都会触发;.hook-lock 做了 30 秒去重 3. PTY 模式下 Hook 的 stdin 为空 — 输出从 task-output.txt 读取,而非 stdin 4. tee 管道竞态 — Hook 等待 1 秒让 pipe flush 完成后再读取输出 5. Meta 新鲜度检查 — Hook 验证 meta 文件时间(<2h)和 session ID,避免误发旧任务通知 6. Agent Teams 成本 — 多 Agent 任务 token 消耗大幅增加;务必使用 --max-budget-usd 7. 速率限制 — Claude Code 有每日速率限制(UTC 11:00 重置);Stop hook 仍会以 status=done 触发,看似成功
Prompt 技巧
详见 Prompt 指南,包含成本控制、Agent Teams、worktree 隔离、自定义子 Agent 和 MCP 集成的示例和最佳实践。
许可证
MIT
Claude Code Dispatch
One-command dispatch of development tasks to Claude Code with automatic Telegram notification on completion. Zero polling, zero token waste.
An OpenClaw skill that wraps Claude Code CLI into a fire-and-forget workflow: dispatch a task, walk away, get notified when it's done.
Features
- Fire & Forget —
nohupdispatch, automatic callback via Stop Hook - Agent Teams — Multi-agent parallel development with dedicated Testing Agent via structured
--agentsJSON - Cost Controls —
--max-budget-usdspend cap +--max-turnslimit +--fallback-modelfor overload resilience - Git Worktree Isolation —
--worktreefor parallel tasks in isolated branches - Custom Subagents — Define specialized agents (security reviewer, testing agent, etc.) via
--agents-json - Auto-Callback — Group notifications, DM callbacks, webhook wake events
- Rich Notifications — Task status, duration, test results, file tree — all in one Telegram message
- PTY Wrapper — Reliable execution even in non-TTY environments (CI, exec, cron)
- MCP Integration — Load MCP servers for tasks via
--mcp-config - System Prompt Customization —
--append-system-prompt/--append-system-prompt-file
Architecture
dispatch.sh
→ write task-meta.json
→ launch Claude Code via claude_code_run.py (PTY)
→ [Agent Teams: --agents JSON defines Testing Agent + custom subagents]
→ Claude Code finishes → Stop/TaskCompleted hook fires automatically
→ notify-agi.sh reads meta + output
→ writes latest.json
→ sends Telegram notification
→ writes pending-wake.json (heartbeat fallback)Quick Start
Prerequisites
- OpenClaw installed and configured
- Claude Code CLI (
claude) installed - A Telegram bot configured in OpenClaw (for notifications)
Installation
Copy the skill into your OpenClaw skills directory:
cp -r claude-code-dispatch ~/.openclaw/skills/
# Or symlink
ln -s /path/to/claude-code-dispatch ~/.openclaw/skills/claude-code-dispatchSet up the Stop Hook (see Hook Setup):
mkdir -p ~/.claude/hooks
cp scripts/notify-agi.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/notify-agi.shConfigure hooks in ~/.claude/settings.json:
{
"hooks": {
"Stop": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }],
"TaskCompleted": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }],
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-agi.sh" }] }]
}
}Usage
⚠️ Always use `nohup` + background (`&`) — dispatch runs until Claude Code finishes (minutes to hours).
# Simple task
nohup bash scripts/dispatch.sh \
-p "Build a Python REST API with FastAPI" \
-n "my-api" \
-g "-5006066016" \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-api \
> /tmp/dispatch-my-api.log 2>&1 &
# With Agent Teams (parallel dev + testing)
nohup bash scripts/dispatch.sh \
-p "Build a Python REST API with FastAPI" \
-n "my-api" \
--agent-teams \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-api \
> /tmp/dispatch-my-api.log 2>&1 &
# With cost controls + fallback
nohup bash scripts/dispatch.sh \
-p "Refactor the auth module" \
-n "auth-refactor" \
--max-budget-usd 5.00 \
--max-turns 50 \
--fallback-model sonnet \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-auth.log 2>&1 &
# With git worktree isolation
nohup bash scripts/dispatch.sh \
-p "Implement feature X" \
-n "feature-x" \
--worktree feature-x \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-feature.log 2>&1 &Parameters
| Param | Short | Required | Description |
|---|---|---|---|
--prompt | -p | ✅* | Task description |
--prompt-file | ✅* | Read prompt from file | |
--name | -n | Task name for tracking | |
--group | -g | Telegram group ID for notifications | |
--workdir | -w | Working directory (default: cwd) | |
--agent-teams | Enable Agent Teams mode | ||
--agents-json | Custom subagent definitions (JSON string) | ||
--teammate-mode | Display: auto / in-process / tmux | ||
--permission-mode | bypassPermissions / plan / acceptEdits / default | ||
--allowed-tools | Tool allowlist (e.g. "Read,Bash") | ||
--disallowed-tools | Tool denylist | ||
--model | Model override (sonnet/opus/haiku/full name) | ||
--fallback-model | Fallback model when primary is overloaded | ||
--max-budget-usd | Maximum dollar spend before auto-stopping | ||
--max-turns | Maximum agentic turns before stopping | ||
--worktree | Git worktree name for isolation | ||
--no-session-persistence | Don't save session to disk | ||
--append-system-prompt | Append to default system prompt | ||
--append-system-prompt-file | Append system prompt from file | ||
--mcp-config | MCP servers JSON file path | ||
--verbose | Enable verbose logging | ||
--callback-group | Telegram group for dispatching agent callback | ||
--callback-dm | Telegram user ID for DM callback | ||
--callback-account | Telegram bot account for DM callback |
\* One of --prompt or --prompt-file is required.
Agent Teams
When --agent-teams is enabled without --agents-json, the dispatch script automatically defines a structured Testing Agent via the --agents CLI flag:
{
"testing-agent": {
"description": "Dedicated testing agent for comprehensive test coverage",
"prompt": "Write and run tests for all code changes...",
"tools": ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
"model": "sonnet"
}
}This replaces the older prompt-injection approach with Claude Code's native --agents flag, giving the Testing Agent its own context window, tool restrictions, and model selection.
Custom Subagents
Pass --agents-json to define your own team:
--agents-json '{
"security-reviewer": {
"description": "Reviews code for security vulnerabilities",
"prompt": "You are a security expert. Focus on OWASP top 10...",
"tools": ["Read", "Grep", "Glob"],
"model": "opus"
},
"perf-analyst": {
"description": "Analyzes and optimizes performance",
"prompt": "Profile code and suggest optimizations...",
"tools": ["Read", "Bash", "Grep"],
"model": "sonnet"
}
}'Each subagent is an independent Claude Code process with its own context window, sharing the same filesystem.
Cost Controls
Control spending with:
| Flag | Description |
|---|---|
--max-budget-usd 5.00 | Hard spend cap in dollars |
--max-turns 50 | Maximum agentic turns |
--fallback-model sonnet | Auto-switch when primary model is overloaded |
These are especially important for Agent Teams, which consume significantly more tokens.
Git Worktree Isolation
Use --worktree <name> to run the task in an isolated git worktree:
--worktree feature-auth
# Claude Code runs at <repo>/.claude/worktrees/feature-authThis allows parallel dispatch tasks to work on the same repo without conflicts.
Auto-Callback Detection
If no --callback-group or --callback-dm is passed, the script looks for dispatch-callback.json in the working directory:
// Group callback
{ "type": "group", "group": "-5189558203" }
// DM callback
{ "type": "dm", "dm": "8009709280", "account": "coding-bot" }
// Wake hook (for main agent)
{ "type": "wake" }Hook Events
The notification hook (notify-agi.sh) handles multiple Claude Code lifecycle events:
| Event | When | Purpose |
|---|---|---|
Stop | Claude finishes responding | Primary completion signal |
TaskCompleted | Task explicitly marked done | Precise completion (Agent Teams) |
SessionEnd | Session terminates | Fallback signal |
Built-in deduplication (.hook-lock, 30s window) prevents double notifications.
HTTP hooks are also supported as an alternative — see Hook Setup.
Result Files
All results are written to data/claude-code-results/:
| File | Content |
|---|---|
latest.json | Full result (output, task name, group, timestamp) |
task-meta.json | Task metadata (prompt, workdir, status, cost params) |
task-output.txt | Raw Claude Code stdout |
pending-wake.json | Heartbeat fallback notification |
hook.log | Hook execution log |
Debugging
# Watch hook log
tail -f data/claude-code-results/hook.log
# Check latest result
cat data/claude-code-results/latest.json | jq .
# Check task metadata
cat data/claude-code-results/task-meta.json | jq .
# Test Telegram delivery
openclaw message send --channel telegram --target "-5006066016" --message "test"Gotchas
1. Must use PTY wrapper — Direct claude -p hangs in exec environments 2. Hook fires twice — Stop + SessionEnd both trigger; .hook-lock deduplicates (30s window) 3. Hook stdin is empty in PTY — Output is read from task-output.txt, not stdin 4. tee pipe race — Hook sleeps 1s to wait for pipe flush before reading output 5. Meta freshness — Hook validates meta age (<2h) and session ID to avoid stale notifications 6. Agent Teams cost — Multi-agent tasks use significantly more tokens; always use --max-budget-usd 7. Rate limits — Claude Code has daily rate limits (reset at 11:00 UTC); the stop hook still fires with status=done, making it look like success
Prompt Tips
See Prompt Guide for examples and best practices.
License
MIT
Hook Setup Guide
The Claude Code Dispatch skill relies on Claude Code's hook system to automatically notify you when tasks complete.
Hook Events Used
| Event | Purpose |
|---|---|
Stop | Primary: fires when Claude finishes responding |
TaskCompleted | Enhanced: fires when a task is explicitly marked complete (Agent Teams) |
SessionEnd | Fallback: fires when session terminates |
The notify-agi.sh script handles all three events with built-in deduplication (.hook-lock, 30s window).
Setup
1. Copy the hook script
mkdir -p ~/.claude/hooks
cp scripts/notify-agi.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/notify-agi.sh2. Configure hooks in settings.json
Edit ~/.claude/settings.json and add the hooks configuration:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/home/ubuntu/.claude/hooks/notify-agi.sh"
}
]
}
],
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "/home/ubuntu/.claude/hooks/notify-agi.sh"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "/home/ubuntu/.claude/hooks/notify-agi.sh"
}
]
}
]
}
}3. Verify
# Check settings
cat ~/.claude/settings.json | jq '.hooks'
# Test a dispatch
nohup bash scripts/dispatch.sh \
-p "echo hello world" \
-n "test-hook" \
--permission-mode bypassPermissions \
> /tmp/test-dispatch.log 2>&1 &
# Watch the hook log
tail -f data/claude-code-results/hook.logHook Input (JSON via stdin)
All events receive JSON with these common fields:
{
"session_id": "abc123",
"transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
"cwd": "/home/user/my-project",
"permission_mode": "bypassPermissions",
"hook_event_name": "Stop"
}Hook Event Details
Stop
- Fires when Claude finishes a response turn
- Most common trigger for task completion
- No matcher support (fires on every stop)
TaskCompleted
- Fires when a task is explicitly marked as completed
- More precise than Stop for Agent Teams workflows
- Can be blocked (exit code 2) to prevent premature completion
- No matcher support
SessionEnd
- Fires when session terminates
- Matchers:
clear,logout,prompt_input_exit,bypass_permissions_disabled,other - Used as fallback; deduplication prevents double notifications
Alternative: HTTP Hooks
Instead of a shell script, you can send task completion events to an HTTP endpoint:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/task-complete",
"timeout": 30,
"headers": {
"Authorization": "Bearer $WEBHOOK_TOKEN"
},
"allowedEnvVars": ["WEBHOOK_TOKEN"]
}
]
}
]
}
}HTTP hooks receive the same JSON as the POST body. Non-2xx responses are non-blocking errors.
Alternative: Hooks in Skill Frontmatter
Hooks can also be defined in the SKILL.md frontmatter (scoped to skill lifetime):
---
name: my-dispatch-task
hooks:
Stop:
- hooks:
- type: command
command: "/path/to/notify-agi.sh"
TaskCompleted:
- hooks:
- type: command
command: "/path/to/notify-agi.sh"
---Deduplication
The hook script uses a .hook-lock file to prevent double notifications:
- Stop and SessionEnd both fire on normal completion
- Only the first event within 30s is processed
- The lock file is in
data/claude-code-results/.hook-lock
Troubleshooting
Hook not firing
1. Check ~/.claude/settings.json has valid JSON: jq . ~/.claude/settings.json 2. Hooks are snapshot at session start — restart Claude Code after config changes 3. Check data/claude-code-results/hook.log for errors
Output is empty
1. PTY mode: hook reads from task-output.txt, not stdin 2. Hook sleeps 1s to wait for tee pipe flush 3. Check data/claude-code-results/task-output.txt exists and has content
Telegram notification not sent
1. Check openclaw binary is accessible: which openclaw 2. Verify group ID: openclaw message send --channel telegram --target "<group_id>" --message "test" 3. Check task-meta.json has valid telegram_group
Stale notifications
1. Meta file age check: >2h old meta is ignored 2. Session ID mismatch: meta session_id must match current session 3. Clear stale meta: rm data/claude-code-results/task-meta.json
Prompt Guide for Claude Code Dispatch
Best practices and examples for writing effective dispatch prompts.
Principles
1. Be specific — Describe the exact outcome, not just the general idea 2. Include acceptance criteria — What does "done" look like? 3. Mention test requirements — Claude Code works best when told to verify its work 4. Reference existing code — Point to files/patterns to follow 5. Set boundaries — What should NOT be changed
Basic Examples
Build a feature
nohup bash scripts/dispatch.sh \
-p "Build a REST API for user management with FastAPI:
- CRUD endpoints: POST/GET/PUT/DELETE /api/users
- SQLite database with SQLAlchemy
- Pydantic models for request/response validation
- Write tests with pytest, run them, fix any failures
- Add a README with setup instructions" \
-n "user-api" \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/user-api \
> /tmp/dispatch-user-api.log 2>&1 &Fix a bug
nohup bash scripts/dispatch.sh \
-p "Fix the authentication timeout bug:
- Error: 'Token expired' after 5 minutes even with remember-me checked
- Look at src/auth/token.ts and src/middleware/auth.ts
- The refresh token logic seems to not extend the session
- Write a regression test before fixing
- Run the full test suite after" \
-n "fix-auth-timeout" \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-fix.log 2>&1 &Code review
nohup bash scripts/dispatch.sh \
-p "Review the codebase for security issues:
- Focus on: input validation, SQL injection, XSS, auth bypass
- Check all API endpoints in src/routes/
- Report findings as a markdown file at SECURITY_REVIEW.md
- Include severity (critical/high/medium/low) and fix suggestions" \
-n "security-review" \
--permission-mode plan \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-review.log 2>&1 &Advanced Examples
With cost control
nohup bash scripts/dispatch.sh \
-p "Refactor the database layer to use connection pooling" \
-n "db-refactor" \
--max-budget-usd 5.00 \
--max-turns 50 \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-refactor.log 2>&1 &With fallback model
nohup bash scripts/dispatch.sh \
-p "Add comprehensive error handling to all API endpoints" \
-n "error-handling" \
--model opus \
--fallback-model sonnet \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-errors.log 2>&1 &With Agent Teams (structured subagents)
nohup bash scripts/dispatch.sh \
-p "Build a full-stack todo app with React frontend and Express backend" \
-n "todo-app" \
--agent-teams \
--teammate-mode in-process \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/todo-app \
> /tmp/dispatch-todo.log 2>&1 &With custom subagents
nohup bash scripts/dispatch.sh \
-p "Build a CLI tool for file encryption" \
-n "encrypt-cli" \
--agents-json '{
"security-reviewer": {
"description": "Reviews code for cryptographic correctness and security best practices",
"prompt": "You are a security expert. Review all crypto implementations for correctness, timing attacks, key management issues, and OWASP compliance.",
"tools": ["Read", "Grep", "Glob", "Bash"],
"model": "opus"
},
"testing-agent": {
"description": "Writes and runs comprehensive tests",
"prompt": "You are a testing specialist. Write unit tests, integration tests, and edge case tests. Always run tests after writing them.",
"tools": ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
"model": "sonnet"
}
}' \
--agent-teams \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/encrypt-cli \
> /tmp/dispatch-encrypt.log 2>&1 &With git worktree isolation
nohup bash scripts/dispatch.sh \
-p "Implement the new dashboard feature from the spec in docs/dashboard-spec.md" \
-n "dashboard-feature" \
--worktree dashboard-feature \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-dashboard.log 2>&1 &With MCP servers
nohup bash scripts/dispatch.sh \
-p "Read the Jira tickets tagged 'sprint-42' and implement the highest priority one" \
-n "jira-sprint42" \
--mcp-config ./mcp-servers.json \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-jira.log 2>&1 &With system prompt customization
nohup bash scripts/dispatch.sh \
-p "Refactor the codebase to follow our style guide" \
-n "style-refactor" \
--append-system-prompt "Always use TypeScript strict mode. Prefer functional patterns. Use Bun instead of npm." \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-style.log 2>&1 &With system prompt from file
# Create a reusable prompt file
cat > /tmp/team-conventions.txt << 'EOF'
Follow these team conventions:
- Use Bun, not npm
- All functions must have JSDoc comments
- Use zod for runtime validation
- Error messages must be user-friendly
- Run bun test before marking as done
EOF
nohup bash scripts/dispatch.sh \
-p "Add input validation to all API routes" \
-n "validation" \
--append-system-prompt-file /tmp/team-conventions.txt \
--permission-mode bypassPermissions \
--workdir /home/ubuntu/projects/my-app \
> /tmp/dispatch-validation.log 2>&1 &Tips
Prompt length
- Short prompts (<1500 chars): passed as CLI args
- Long prompts: automatically piped via stdin
- Very complex prompts: use
--prompt-filefor reliability
Agent Teams vs single agent
- Single agent: Simple tasks, bug fixes, code review
- Agent Teams: Multi-module features, full-stack work, tasks needing parallel exploration
- Agent Teams use significantly more tokens — use
--max-budget-usdto cap costs
Permission modes
| Mode | When to use |
|---|---|
bypassPermissions | Trusted tasks, background dispatch (most common for dispatch) |
plan | Read-only analysis, code review, security audit |
acceptEdits | Allow file edits but prompt for shell commands |
default | Interactive use (not recommended for dispatch) |
Model selection
- Default (inherit): Uses whatever Claude Code is configured with
- `--model opus`: Complex architecture, multi-file refactors
- `--model sonnet`: Standard tasks, good balance of speed/quality
- `--model haiku`: Simple fixes, formatting, quick lookups
- `--fallback-model sonnet`: Auto-fallback when primary model is overloaded
#!/usr/bin/env python3
"""Run Claude Code (claude CLI) reliably.
Default mode is *auto*:
- If the prompt looks like it uses interactive slash commands (e.g. /speckit.*)
we start an interactive Claude Code session in tmux (PTY).
- Otherwise we run headless (-p) through `script(1)` to force a pseudo-terminal.
Why this wrapper exists:
- Claude Code can hang when run without a TTY.
- CI / exec environments are often non-interactive.
Docs:
- Headless (Agent SDK): https://code.claude.com/docs/en/headless
- Agent Teams: https://code.claude.com/docs/en/agent-teams
- Subagents: https://code.claude.com/docs/en/sub-agents
- Hooks: https://code.claude.com/docs/en/hooks
- CLI Reference: https://code.claude.com/docs/en/cli-reference
"""
from __future__ import annotations
import argparse
import json
import os
import shlex
import subprocess
import sys
import time
from pathlib import Path
DEFAULT_CLAUDE = os.environ.get("CLAUDE_CODE_BIN", "/home/ubuntu/.local/bin/claude")
def which(name: str) -> str | None:
paths = os.environ.get("PATH", "").split(":")
for p in paths:
cand = Path(p) / name
try:
if cand.is_file() and os.access(cand, os.X_OK):
return str(cand)
except OSError:
pass
return None
def looks_like_slash_commands(prompt: str | None) -> bool:
if not prompt:
return False
for line in prompt.splitlines():
if line.strip().startswith("/"):
return True
return False
def build_headless_cmd(args: argparse.Namespace) -> list[str]:
cmd: list[str] = [args.claude_bin]
if args.permission_mode:
cmd += ["--permission-mode", args.permission_mode]
# For short prompts, pass inline. For long ones, caller will pipe via stdin.
if args.prompt is not None and len(args.prompt) <= 1500:
cmd += ["-p", args.prompt]
elif args.prompt is not None:
# Long prompt — use stdin pipe mode: claude -p - (reads from stdin)
cmd += ["-p", "-"]
if args.allowedTools:
cmd += ["--allowedTools", args.allowedTools]
if args.disallowedTools:
cmd += ["--disallowedTools", args.disallowedTools]
if args.tools:
cmd += ["--tools", args.tools]
if args.output_format:
cmd += ["--output-format", args.output_format]
if args.json_schema:
cmd += ["--json-schema", args.json_schema]
if args.append_system_prompt:
cmd += ["--append-system-prompt", args.append_system_prompt]
if args.append_system_prompt_file:
cmd += ["--append-system-prompt-file", args.append_system_prompt_file]
if args.system_prompt:
cmd += ["--system-prompt", args.system_prompt]
if args.system_prompt_file:
cmd += ["--system-prompt-file", args.system_prompt_file]
if args.continue_latest:
cmd.append("--continue")
if args.resume:
cmd += ["--resume", args.resume]
# Agent Teams support
if args.teammate_mode:
cmd += ["--teammate-mode", args.teammate_mode]
# Dynamic subagent definitions via JSON
if args.agents_json:
cmd += ["--agents", args.agents_json]
# Cost & turn controls
if args.max_budget_usd is not None:
cmd += ["--max-budget-usd", str(args.max_budget_usd)]
if args.max_turns is not None:
cmd += ["--max-turns", str(args.max_turns)]
if args.fallback_model:
cmd += ["--fallback-model", args.fallback_model]
# Git worktree isolation
if args.worktree:
cmd += ["--worktree", args.worktree]
# Session persistence
if args.no_session_persistence:
cmd.append("--no-session-persistence")
# MCP config
if args.mcp_config:
cmd += ["--mcp-config", args.mcp_config]
# Verbose / debug
if args.verbose:
cmd.append("--verbose")
if args.debug:
cmd += ["--debug", args.debug] if args.debug != "all" else ["--debug"]
# Model override
if args.model:
cmd += ["--model", args.model]
if args.extra:
cmd += args.extra
return cmd
def build_agent_teams_env(args: argparse.Namespace) -> dict[str, str]:
"""Build environment dict with Agent Teams support."""
env = os.environ.copy()
if args.agent_teams:
env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
return env
def run_with_pty(cmd: list[str], cwd: str | None, env: dict[str, str] | None = None, stdin_text: str | None = None) -> int:
cmd_str = " ".join(shlex.quote(c) for c in cmd)
script_bin = which("script")
if stdin_text:
# For long prompts: pipe via stdin instead of CLI args.
# Write prompt to a temp file, then use shell redirection with script(1).
import tempfile
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, prefix="claude-prompt-") as f:
f.write(stdin_text)
prompt_path = f.name
try:
shell_cmd = f"cat {shlex.quote(prompt_path)} | {cmd_str}"
if script_bin:
proc = subprocess.run([script_bin, "-q", "-c", shell_cmd, "/dev/null"], cwd=cwd, text=True, env=env)
else:
proc = subprocess.run(["bash", "-c", shell_cmd], cwd=cwd, text=True, env=env)
return proc.returncode
finally:
try:
os.unlink(prompt_path)
except OSError:
pass
else:
if not script_bin:
proc = subprocess.run(cmd, cwd=cwd, text=True, env=env)
return proc.returncode
proc = subprocess.run([script_bin, "-q", "-c", cmd_str, "/dev/null"], cwd=cwd, text=True, env=env)
return proc.returncode
def tmux_cmd(socket_path: str, *args: str) -> list[str]:
return ["tmux", "-S", socket_path, *args]
def tmux_capture(socket_path: str, target: str, lines: int = 200) -> str:
out = subprocess.check_output(
tmux_cmd(socket_path, "capture-pane", "-p", "-J", "-t", target, "-S", f"-{lines}"),
text=True,
)
return out
def tmux_wait_for_text(socket_path: str, target: str, pattern: str, timeout_s: int = 30, poll_s: float = 0.5) -> bool:
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
buf = tmux_capture(socket_path, target, lines=200)
if pattern in buf:
return True
except subprocess.CalledProcessError:
pass
time.sleep(poll_s)
return False
def run_interactive_tmux(args: argparse.Namespace) -> int:
if not which("tmux"):
print("tmux not found in PATH; cannot run interactive mode.", file=sys.stderr)
return 2
socket_dir = args.tmux_socket_dir or os.environ.get("CLAWDBOT_TMUX_SOCKET_DIR") or f"{os.environ.get('TMPDIR', '/tmp')}/clawdbot-tmux-sockets"
Path(socket_dir).mkdir(parents=True, exist_ok=True)
socket_path = str(Path(socket_dir) / args.tmux_socket_name)
session = args.tmux_session
target = f"{session}:0.0"
subprocess.run(tmux_cmd(socket_path, "kill-session", "-t", session), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.check_call(tmux_cmd(socket_path, "new", "-d", "-s", session, "-n", "shell"))
cwd = args.cwd or os.getcwd()
# Set Agent Teams env var inside tmux session if enabled
if args.agent_teams:
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "-l", "--", "export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1"))
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "Enter"))
time.sleep(0.3)
claude_parts = [args.claude_bin]
if args.permission_mode:
claude_parts += ["--permission-mode", args.permission_mode]
if args.allowedTools:
claude_parts += ["--allowedTools", args.allowedTools]
if args.disallowedTools:
claude_parts += ["--disallowedTools", args.disallowedTools]
if args.tools:
claude_parts += ["--tools", args.tools]
if args.append_system_prompt:
claude_parts += ["--append-system-prompt", args.append_system_prompt]
if args.append_system_prompt_file:
claude_parts += ["--append-system-prompt-file", args.append_system_prompt_file]
if args.system_prompt:
claude_parts += ["--system-prompt", args.system_prompt]
if args.system_prompt_file:
claude_parts += ["--system-prompt-file", args.system_prompt_file]
if args.continue_latest:
claude_parts.append("--continue")
if args.resume:
claude_parts += ["--resume", args.resume]
# Agent Teams teammate mode
if args.teammate_mode:
claude_parts += ["--teammate-mode", args.teammate_mode]
# Dynamic subagents
if args.agents_json:
claude_parts += ["--agents", args.agents_json]
# Cost & turn controls
if args.max_budget_usd is not None:
claude_parts += ["--max-budget-usd", str(args.max_budget_usd)]
if args.max_turns is not None:
claude_parts += ["--max-turns", str(args.max_turns)]
if args.fallback_model:
claude_parts += ["--fallback-model", args.fallback_model]
# Git worktree
if args.worktree:
claude_parts += ["--worktree", args.worktree]
# Verbose
if args.verbose:
claude_parts.append("--verbose")
# Model
if args.model:
claude_parts += ["--model", args.model]
# MCP config
if args.mcp_config:
claude_parts += ["--mcp-config", args.mcp_config]
if args.extra:
claude_parts += args.extra
launch = f"cd {shlex.quote(cwd)} && " + " ".join(shlex.quote(p) for p in claude_parts)
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "-l", "--", launch))
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "Enter"))
# Workspace trust prompt (first run in a new folder).
if tmux_wait_for_text(socket_path, target, "Yes, I trust this folder", timeout_s=20):
subprocess.run(tmux_cmd(socket_path, "send-keys", "-t", target, "Enter"), check=False)
time.sleep(0.8)
if tmux_wait_for_text(socket_path, target, "Yes, I trust this folder", timeout_s=2):
subprocess.run(tmux_cmd(socket_path, "send-keys", "-t", target, "1"), check=False)
subprocess.run(tmux_cmd(socket_path, "send-keys", "-t", target, "Enter"), check=False)
if args.prompt:
for line in [ln for ln in args.prompt.splitlines() if ln.strip()]:
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "-l", "--", line))
subprocess.check_call(tmux_cmd(socket_path, "send-keys", "-t", target, "Enter"))
time.sleep(args.interactive_send_delay_ms / 1000.0)
print("Started interactive Claude Code in tmux.")
print("To monitor:")
print(f" tmux -S {shlex.quote(socket_path)} attach -t {shlex.quote(session)}")
print("To snapshot output:")
print(f" tmux -S {shlex.quote(socket_path)} capture-pane -p -J -t {shlex.quote(target)} -S -200")
if args.interactive_wait_s > 0:
time.sleep(args.interactive_wait_s)
try:
snap = tmux_capture(socket_path, target, lines=200)
print("\n--- tmux snapshot (last 200 lines) ---\n")
print(snap)
except subprocess.CalledProcessError:
pass
return 0
def main() -> int:
ap = argparse.ArgumentParser(description="Run Claude Code reliably (headless or interactive via tmux)")
ap.add_argument("-p", "--prompt", help="Prompt text. In headless mode this is passed via -p. In interactive mode it is sent as keystrokes.")
ap.add_argument("--prompt-file", dest="prompt_file", help="Read prompt from file (avoids shell escaping issues with long/complex prompts).")
ap.add_argument(
"--mode",
choices=["auto", "headless", "interactive"],
default="auto",
help="Execution mode. auto switches to interactive when prompt contains slash commands (lines starting with '/').",
)
# Permission & tool control
ap.add_argument(
"--permission-mode",
default=None,
help=(
"Claude Code permission mode (passed through to `claude --permission-mode`). "
"Common values: plan, acceptEdits, dontAsk, bypassPermissions, default."
),
)
ap.add_argument("--allowedTools", dest="allowedTools", help="Tools that execute without prompting for permission")
ap.add_argument("--disallowedTools", dest="disallowedTools", help="Tools removed from model context (cannot be used)")
ap.add_argument("--tools", dest="tools", help="Restrict which built-in tools Claude can use")
# Output format
ap.add_argument("--output-format", dest="output_format", choices=["text", "json", "stream-json"], help="Output format (headless)")
ap.add_argument("--json-schema", dest="json_schema", help="JSON schema (string) when using --output-format json")
# System prompt
ap.add_argument("--append-system-prompt", dest="append_system_prompt", help="Append to Claude Code default system prompt")
ap.add_argument("--append-system-prompt-file", dest="append_system_prompt_file", help="Append system prompt from file")
ap.add_argument("--system-prompt", dest="system_prompt", help="Replace system prompt entirely")
ap.add_argument("--system-prompt-file", dest="system_prompt_file", help="Replace system prompt from file")
# Session management
ap.add_argument("--continue", dest="continue_latest", action="store_true", help="Continue the most recent session")
ap.add_argument("--resume", help="Resume a specific session ID")
ap.add_argument("--no-session-persistence", dest="no_session_persistence", action="store_true",
help="Don't save session to disk (one-off tasks, print mode only)")
# Agent Teams options
ap.add_argument(
"--agent-teams",
action="store_true",
help="Enable Agent Teams (sets CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1).",
)
ap.add_argument(
"--teammate-mode",
choices=["auto", "in-process", "tmux"],
default=None,
help="Agent Teams display mode. auto (default) uses in-process; tmux creates split panes.",
)
# Dynamic subagent definitions (new)
ap.add_argument(
"--agents-json",
dest="agents_json",
default=None,
help='Define custom subagents via JSON string, e.g. \'{"reviewer":{"description":"...","prompt":"..."}}\'',
)
# Cost & turn controls (new)
ap.add_argument(
"--max-budget-usd",
dest="max_budget_usd",
type=float,
default=None,
help="Maximum dollar amount to spend on API calls before stopping (print mode only).",
)
ap.add_argument(
"--max-turns",
dest="max_turns",
type=int,
default=None,
help="Limit the number of agentic turns (print mode only).",
)
ap.add_argument(
"--fallback-model",
dest="fallback_model",
default=None,
help="Automatic fallback model when default is overloaded (print mode only).",
)
# Git worktree isolation (new)
ap.add_argument(
"--worktree", "-w",
dest="worktree",
default=None,
help="Run in an isolated git worktree at <repo>/.claude/worktrees/<name>.",
)
# MCP config (new)
ap.add_argument(
"--mcp-config",
dest="mcp_config",
default=None,
help="Load MCP servers from JSON file or string.",
)
# Model override (new)
ap.add_argument(
"--model",
default=None,
help="Model override for this session (e.g. sonnet, opus, haiku, or full model name).",
)
# Verbose / debug (new)
ap.add_argument("--verbose", action="store_true", help="Enable verbose logging")
ap.add_argument("--debug", nargs="?", const="all", default=None, help="Enable debug mode with optional category filter")
# Claude binary path
ap.add_argument(
"--claude-bin",
default=DEFAULT_CLAUDE,
help=f"Path to claude binary (default: {DEFAULT_CLAUDE}). You can also set CLAUDE_CODE_BIN.",
)
ap.add_argument("--cwd", help="Working directory to run claude in (defaults to current directory)")
# tmux options (interactive mode)
ap.add_argument("--tmux-session", default="cc", help="tmux session name (interactive mode)")
ap.add_argument("--tmux-socket-dir", default=None, help="tmux socket dir")
ap.add_argument("--tmux-socket-name", default="claude-code.sock", help="tmux socket file name")
ap.add_argument("--interactive-wait-s", type=int, default=0, help="Wait N seconds then print a tmux output snapshot")
ap.add_argument("--interactive-send-delay-ms", type=int, default=800, help="Delay between sending lines in interactive mode")
ap.add_argument("extra", nargs=argparse.REMAINDER, help="Extra args after --")
args = ap.parse_args()
# --prompt-file takes precedence over -p
if args.prompt_file:
pf = Path(args.prompt_file)
if not pf.exists():
print(f"Prompt file not found: {args.prompt_file}", file=sys.stderr)
return 2
args.prompt = pf.read_text(encoding="utf-8").strip()
extra = args.extra
if extra and extra[0] == "--":
extra = extra[1:]
args.extra = extra
if not Path(args.claude_bin).exists():
print(f"claude binary not found: {args.claude_bin}", file=sys.stderr)
print("Tip: set CLAUDE_CODE_BIN=/path/to/claude", file=sys.stderr)
return 2
mode = args.mode
if mode == "auto" and looks_like_slash_commands(args.prompt):
mode = "interactive"
if mode == "interactive":
return run_interactive_tmux(args)
cmd = build_headless_cmd(args)
env = build_agent_teams_env(args)
# For long prompts (>1500 chars), pipe via stdin instead of CLI args
stdin_text = args.prompt if (args.prompt and len(args.prompt) > 1500) else None
return run_with_pty(cmd, cwd=args.cwd, env=env, stdin_text=stdin_text)
if __name__ == "__main__":
raise SystemExit(main())
#!/bin/bash
# dispatch-claude-code.sh — Dispatch a task to Claude Code with auto-callback
#
# Usage:
# dispatch-claude-code.sh [OPTIONS] -p "your prompt here"
#
# Options:
# -p, --prompt TEXT Task prompt (required, or use --prompt-file)
# --prompt-file FILE Read prompt from file
# -n, --name NAME Task name (for tracking)
# -g, --group ID Telegram group ID for result delivery
# -s, --session KEY Callback session key
# -w, --workdir DIR Working directory for Claude Code
# --agent-teams Enable Agent Teams (lead + teammates)
# --agents-json JSON Define custom subagents via JSON (--agents flag)
# --teammate-mode MODE Agent Teams display mode (auto/in-process/tmux)
# --permission-mode MODE Claude Code permission mode
# --allowed-tools TOOLS Allowed tools string
# --disallowed-tools TOOLS Disallowed tools string
# --model MODEL Model override
# --fallback-model MODEL Fallback model when primary is overloaded
# --max-budget-usd AMOUNT Maximum dollar spend before stopping
# --max-turns N Maximum agentic turns
# --worktree NAME Run in isolated git worktree
# --no-session-persistence Don't save session to disk
# --append-system-prompt TEXT Append to system prompt
# --append-system-prompt-file Append system prompt from file
# --mcp-config FILE Load MCP servers from JSON file
# --verbose Enable verbose logging
#
# The script:
# 1. Writes task metadata to task-meta.json (hook reads this)
# 2. Runs Claude Code via claude_code_run.py
# 3. When Claude Code finishes, Stop/TaskCompleted hook fires automatically
# 4. Hook reads meta, writes results, wakes AGI
# 5. AGI reads results and relays to Telegram group
set -euo pipefail
RESULT_DIR="/home/ubuntu/clawd/data/claude-code-results"
META_FILE="${RESULT_DIR}/task-meta.json"
OUTPUT_FILE="/tmp/claude-code-output.txt"
TASK_OUTPUT="${RESULT_DIR}/task-output.txt"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RUNNER="${SCRIPT_DIR}/claude_code_run.py"
# Defaults
PROMPT=""
PROMPT_FILE=""
TASK_NAME="adhoc-$(date +%s)"
TELEGRAM_GROUP="-5006066016" # Default: Claude Code Tasks group
CALLBACK_GROUP="" # Agent's own group for callback
CALLBACK_DM="" # Telegram user ID for DM callback
CALLBACK_ACCOUNT="" # Telegram bot account for DM callback
CALLBACK_SESSION="${OPENCLAW_SESSION_KEY:-}"
WORKDIR="/home/ubuntu/clawd"
AGENT_TEAMS=""
AGENT_ID=""
AGENTS_JSON=""
TEAMMATE_MODE=""
PERMISSION_MODE=""
ALLOWED_TOOLS=""
DISALLOWED_TOOLS=""
MODEL=""
FALLBACK_MODEL=""
MAX_BUDGET_USD=""
MAX_TURNS=""
WORKTREE=""
NO_SESSION_PERSISTENCE=""
APPEND_SYSTEM_PROMPT=""
APPEND_SYSTEM_PROMPT_FILE=""
MCP_CONFIG=""
VERBOSE=""
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--prompt) PROMPT="$2"; shift 2;;
--prompt-file) PROMPT_FILE="$2"; shift 2;;
-n|--name) TASK_NAME="$2"; shift 2;;
-g|--group) TELEGRAM_GROUP="$2"; shift 2;;
-s|--session) CALLBACK_SESSION="$2"; shift 2;;
--callback-group) CALLBACK_GROUP="$2"; shift 2;;
--callback-dm) CALLBACK_DM="$2"; shift 2;;
--callback-account) CALLBACK_ACCOUNT="$2"; shift 2;;
-w|--workdir) WORKDIR="$2"; shift 2;;
--agent-teams) AGENT_TEAMS="1"; shift;;
--agent-id) AGENT_ID="$2"; shift 2;;
--agents-json) AGENTS_JSON="$2"; shift 2;;
--teammate-mode) TEAMMATE_MODE="$2"; shift 2;;
--permission-mode) PERMISSION_MODE="$2"; shift 2;;
--allowed-tools) ALLOWED_TOOLS="$2"; shift 2;;
--disallowed-tools) DISALLOWED_TOOLS="$2"; shift 2;;
--model) MODEL="$2"; shift 2;;
--fallback-model) FALLBACK_MODEL="$2"; shift 2;;
--max-budget-usd) MAX_BUDGET_USD="$2"; shift 2;;
--max-turns) MAX_TURNS="$2"; shift 2;;
--worktree) WORKTREE="$2"; shift 2;;
--no-session-persistence) NO_SESSION_PERSISTENCE="1"; shift;;
--append-system-prompt) APPEND_SYSTEM_PROMPT="$2"; shift 2;;
--append-system-prompt-file) APPEND_SYSTEM_PROMPT_FILE="$2"; shift 2;;
--mcp-config) MCP_CONFIG="$2"; shift 2;;
--verbose) VERBOSE="1"; shift;;
*) echo "Unknown option: $1" >&2; exit 1;;
esac
done
# ---- Resolve prompt (--prompt-file takes precedence if both given) ----
if [ -n "$PROMPT_FILE" ]; then
if [ ! -f "$PROMPT_FILE" ]; then
echo "Error: prompt file not found: $PROMPT_FILE" >&2
exit 1
fi
PROMPT="$(cat "$PROMPT_FILE")"
fi
if [ -z "$PROMPT" ]; then
echo "Error: --prompt or --prompt-file is required" >&2
exit 1
fi
# ---- Auto-detect callback from workspace config ----
if [ -z "$CALLBACK_GROUP" ] && [ -z "$CALLBACK_DM" ]; then
for SEARCH_DIR in "$(pwd)" "$WORKDIR" "${OPENCLAW_AGENT_DIR:-}"; do
CALLBACK_CONFIG="${SEARCH_DIR}/dispatch-callback.json"
if [ -f "$CALLBACK_CONFIG" ] 2>/dev/null; then
CB_TYPE=$(jq -r '.type // ""' "$CALLBACK_CONFIG" 2>/dev/null || echo "")
case "$CB_TYPE" in
group)
CALLBACK_GROUP=$(jq -r '.group // ""' "$CALLBACK_CONFIG" 2>/dev/null || echo "")
[ -n "$CALLBACK_GROUP" ] && echo "📡 Auto-detected callback: group $CALLBACK_GROUP (from $CALLBACK_CONFIG)"
;;
dm)
CALLBACK_DM=$(jq -r '.dm // ""' "$CALLBACK_CONFIG" 2>/dev/null || echo "")
CALLBACK_ACCOUNT=$(jq -r '.account // ""' "$CALLBACK_CONFIG" 2>/dev/null || echo "")
[ -n "$CALLBACK_DM" ] && echo "📡 Auto-detected callback: DM $CALLBACK_DM via ${CALLBACK_ACCOUNT:-default} (from $CALLBACK_CONFIG)"
;;
esac
break
fi
done
fi
# ---- Agent Teams: build structured --agents JSON if no custom agents-json given ----
if [ -n "$AGENT_TEAMS" ] && [ -z "$AGENTS_JSON" ]; then
# Default Agent Teams: define a structured Testing Agent via --agents JSON
# This replaces the old approach of injecting instructions into the prompt
AGENTS_JSON='{
"testing-agent": {
"description": "Dedicated testing agent. Use proactively to write and run tests for all code changes.",
"prompt": "You are a Testing Agent. Your responsibilities:\n1. Write comprehensive unit tests for every module\n2. Run all tests and ensure they pass\n3. Check edge cases and error handling\n4. Report test results clearly\n5. If tests fail, communicate failures to the lead for fixes.\n\nAlways run tests after writing them. Never mark work as done until all tests pass.",
"tools": ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
"model": "sonnet"
}
}'
# Still add a lighter prompt hint for the lead (no longer the full injection)
PROMPT="${PROMPT}
Note: A dedicated Testing Agent is available via --agents. Delegate test writing and execution to it. All tests must pass before the task is complete."
fi
# ---- 1. Write task metadata ----
mkdir -p "$RESULT_DIR"
jq -n \
--arg name "$TASK_NAME" \
--arg group "$TELEGRAM_GROUP" \
--arg callback_group "$CALLBACK_GROUP" \
--arg callback_dm "$CALLBACK_DM" \
--arg callback_account "$CALLBACK_ACCOUNT" \
--arg session "$CALLBACK_SESSION" \
--arg prompt "$PROMPT" \
--arg workdir "$WORKDIR" \
--arg ts "$(date -Iseconds)" \
--arg agent_teams "${AGENT_TEAMS:-0}" \
--arg agent_id "$AGENT_ID" \
--arg model "${MODEL:-}" \
--arg fallback_model "${FALLBACK_MODEL:-}" \
--arg max_budget "${MAX_BUDGET_USD:-}" \
--arg max_turns "${MAX_TURNS:-}" \
--arg worktree "${WORKTREE:-}" \
'{task_name: $name, telegram_group: $group, callback_group: $callback_group, callback_dm: $callback_dm, callback_account: $callback_account, callback_session: $session, prompt: $prompt, workdir: $workdir, started_at: $ts, agent_teams: ($agent_teams == "1"), agent_id: $agent_id, model: $model, fallback_model: $fallback_model, max_budget_usd: $max_budget, max_turns: $max_turns, worktree: $worktree, status: "running"}' \
> "$META_FILE"
echo "📋 Task metadata written: $META_FILE"
echo " Task: $TASK_NAME"
echo " Group: ${TELEGRAM_GROUP:-none}"
echo " Agent Teams: ${AGENT_TEAMS:-no}"
[ -n "$MAX_BUDGET_USD" ] && echo " Budget: \$${MAX_BUDGET_USD}"
[ -n "$MAX_TURNS" ] && echo " Max Turns: ${MAX_TURNS}"
[ -n "$FALLBACK_MODEL" ] && echo " Fallback Model: ${FALLBACK_MODEL}"
[ -n "$WORKTREE" ] && echo " Worktree: ${WORKTREE}"
[ -n "$MODEL" ] && echo " Model: ${MODEL}"
# ---- 2. Clear previous output ----
> "$OUTPUT_FILE"
> "$TASK_OUTPUT"
# ---- 3. Build runner command ----
# Write prompt to a temp file to avoid shell escaping issues with complex prompts
PROMPT_TMPFILE="$(mktemp /tmp/dispatch-prompt-XXXXXX.txt)"
printf '%s' "$PROMPT" > "$PROMPT_TMPFILE"
trap 'rm -f "$PROMPT_TMPFILE"' EXIT
CMD=(python3 "$RUNNER" --prompt-file "$PROMPT_TMPFILE" --cwd "$WORKDIR")
if [ -n "$AGENT_TEAMS" ]; then
CMD+=(--agent-teams)
fi
if [ -n "$AGENTS_JSON" ]; then
CMD+=(--agents-json "$AGENTS_JSON")
fi
if [ -n "$TEAMMATE_MODE" ]; then
CMD+=(--teammate-mode "$TEAMMATE_MODE")
fi
if [ -n "$PERMISSION_MODE" ]; then
CMD+=(--permission-mode "$PERMISSION_MODE")
fi
if [ -n "$ALLOWED_TOOLS" ]; then
CMD+=(--allowedTools "$ALLOWED_TOOLS")
fi
if [ -n "$DISALLOWED_TOOLS" ]; then
CMD+=(--disallowedTools "$DISALLOWED_TOOLS")
fi
if [ -n "$MODEL" ]; then
CMD+=(--model "$MODEL")
fi
if [ -n "$FALLBACK_MODEL" ]; then
CMD+=(--fallback-model "$FALLBACK_MODEL")
fi
if [ -n "$MAX_BUDGET_USD" ]; then
CMD+=(--max-budget-usd "$MAX_BUDGET_USD")
fi
if [ -n "$MAX_TURNS" ]; then
CMD+=(--max-turns "$MAX_TURNS")
fi
if [ -n "$WORKTREE" ]; then
CMD+=(--worktree "$WORKTREE")
fi
if [ -n "$NO_SESSION_PERSISTENCE" ]; then
CMD+=(--no-session-persistence)
fi
if [ -n "$APPEND_SYSTEM_PROMPT" ]; then
CMD+=(--append-system-prompt "$APPEND_SYSTEM_PROMPT")
fi
if [ -n "$APPEND_SYSTEM_PROMPT_FILE" ]; then
CMD+=(--append-system-prompt-file "$APPEND_SYSTEM_PROMPT_FILE")
fi
if [ -n "$MCP_CONFIG" ]; then
CMD+=(--mcp-config "$MCP_CONFIG")
fi
if [ -n "$VERBOSE" ]; then
CMD+=(--verbose)
fi
# ---- 4. Set environment ----
export OPENCLAW_GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-477d47934e5f6b02bfb823ba681bb743eae55479b7d260e8}"
export OPENCLAW_GATEWAY="${OPENCLAW_GATEWAY:-http://127.0.0.1:18789}"
# ---- 5. Run Claude Code (output tee'd for hook) ----
echo "🚀 Launching Claude Code..."
echo " Command: ${CMD[*]}"
echo ""
# Use tee to capture output while also displaying it
"${CMD[@]}" 2>&1 | tee "$TASK_OUTPUT"
EXIT_CODE=${PIPESTATUS[0]}
echo ""
echo "✅ Claude Code exited with code: $EXIT_CODE"
echo " Hook should have fired automatically."
echo " Results: ${RESULT_DIR}/latest.json"
# Update meta with completion
if [ -f "$META_FILE" ]; then
jq --arg code "$EXIT_CODE" --arg ts "$(date -Iseconds)" \
'. + {exit_code: ($code | tonumber), completed_at: $ts, status: "done"}' \
"$META_FILE" > "${META_FILE}.tmp" && mv "${META_FILE}.tmp" "$META_FILE"
fi
exit $EXIT_CODE
#!/bin/bash
# Claude Code Stop Hook: 任务完成后通知 AGI
# 触发时机: Stop (生成停止) + SessionEnd (会话结束)
# 支持 Agent Teams: lead 完成后自动触发
set -uo pipefail
LOG="/home/ubuntu/clawd/data/claude-code-results/hook.log"
RESULT_DIR="/home/ubuntu/clawd/data/claude-code-results"
META_FILE="${RESULT_DIR}/task-meta.json"
OPENCLAW_BIN="/home/ubuntu/.npm-global/bin/openclaw"
mkdir -p "$RESULT_DIR"
log() { echo "[$(date -Iseconds)] $*" >> "$LOG"; }
log "=== Hook fired ==="
# ---- 读 stdin ----
INPUT=""
if [ -t 0 ]; then
log "stdin is tty, skip"
elif [ -e /dev/stdin ]; then
INPUT=$(timeout 2 cat /dev/stdin 2>/dev/null || true)
fi
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"' 2>/dev/null || echo "unknown")
CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null || echo "")
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"' 2>/dev/null || echo "unknown")
log "session=$SESSION_ID cwd=$CWD event=$EVENT"
# ---- 防重复:只处理第一个事件(Stop),跳过后续的 SessionEnd ----
LOCK_FILE="${RESULT_DIR}/.hook-lock"
LOCK_AGE_LIMIT=30 # 30秒内重复触发视为同一任务
if [ -f "$LOCK_FILE" ]; then
LOCK_TIME=$(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0)
NOW=$(date +%s)
AGE=$(( NOW - LOCK_TIME ))
if [ "$AGE" -lt "$LOCK_AGE_LIMIT" ]; then
log "Duplicate hook within ${AGE}s, skipping"
exit 0
fi
fi
touch "$LOCK_FILE"
# ---- 读取 Claude Code 输出 ----
OUTPUT=""
# 等待 tee 管道 flush(hook 可能在 pipe 写完前触发)
sleep 1
# 来源1: task-output.txt (dispatch 脚本 tee 写入)
TASK_OUTPUT="${RESULT_DIR}/task-output.txt"
if [ -f "$TASK_OUTPUT" ] && [ -s "$TASK_OUTPUT" ]; then
OUTPUT=$(tail -c 4000 "$TASK_OUTPUT")
log "Output from task-output.txt (${#OUTPUT} chars)"
fi
# 来源2: /tmp/claude-code-output.txt
if [ -z "$OUTPUT" ] && [ -f "/tmp/claude-code-output.txt" ] && [ -s "/tmp/claude-code-output.txt" ]; then
OUTPUT=$(tail -c 4000 /tmp/claude-code-output.txt)
log "Output from /tmp fallback (${#OUTPUT} chars)"
fi
# 来源3: 工作目录
if [ -z "$OUTPUT" ] && [ -n "$CWD" ] && [ -d "$CWD" ]; then
FILES=$(ls -1t "$CWD" 2>/dev/null | head -20 | tr '\n' ', ')
OUTPUT="Working dir: ${CWD}\nFiles: ${FILES}"
log "Output from dir listing"
fi
# ---- 读取任务元数据(仅当 meta 文件足够新时才信任)----
TASK_NAME="unknown"
TELEGRAM_GROUP=""
if [ -f "$META_FILE" ]; then
# 检查 meta 文件是否在最近 2 小时内写入(防止复用旧任务的 meta)
META_AGE=$(( $(date +%s) - $(stat -c %Y "$META_FILE" 2>/dev/null || echo 0) ))
if [ "$META_AGE" -gt 7200 ]; then
log "Meta file is ${META_AGE}s old (>2h), ignoring stale meta"
else
# 检查 meta 中的 session_id 是否匹配当前 session(如果有的话)
META_SESSION=$(jq -r '.session_id // ""' "$META_FILE" 2>/dev/null || echo "")
if [ -n "$META_SESSION" ] && [ "$META_SESSION" != "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ]; then
log "Meta session=$META_SESSION != current=$SESSION_ID, ignoring"
else
TASK_NAME=$(jq -r '.task_name // "unknown"' "$META_FILE" 2>/dev/null || echo "unknown")
TELEGRAM_GROUP=$(jq -r '.telegram_group // ""' "$META_FILE" 2>/dev/null || echo "")
CALLBACK_GROUP=$(jq -r '.callback_group // ""' "$META_FILE" 2>/dev/null || echo "")
CALLBACK_DM=$(jq -r '.callback_dm // ""' "$META_FILE" 2>/dev/null || echo "")
CALLBACK_ACCOUNT=$(jq -r '.callback_account // ""' "$META_FILE" 2>/dev/null || echo "")
log "Meta: task=$TASK_NAME group=$TELEGRAM_GROUP callback_group=$CALLBACK_GROUP callback_dm=$CALLBACK_DM callback_account=$CALLBACK_ACCOUNT age=${META_AGE}s"
fi
fi
fi
# ---- 如果没有有效的 telegram 目标,跳过通知 ----
if [ -z "$TELEGRAM_GROUP" ]; then
log "No valid telegram_group, skipping notification (non-dispatch run)"
fi
# ---- 写入结果 JSON ----
jq -n \
--arg sid "$SESSION_ID" \
--arg ts "$(date -Iseconds)" \
--arg cwd "$CWD" \
--arg event "$EVENT" \
--arg output "$OUTPUT" \
--arg task "$TASK_NAME" \
--arg group "$TELEGRAM_GROUP" \
'{session_id: $sid, timestamp: $ts, cwd: $cwd, event: $event, output: $output, task_name: $task, telegram_group: $group, status: "done"}' \
> "${RESULT_DIR}/latest.json" 2>/dev/null
log "Wrote latest.json"
# ---- 方式1: 直接发 Telegram 消息(如果有目标群组)----
if [ -n "$TELEGRAM_GROUP" ] && [ -x "$OPENCLAW_BIN" ]; then
# ---- 提取丰富信息 ----
PROJECT_DIR=""
DURATION=""
AGENT_TEAMS_ENABLED="false"
AGENTS_INFO=""
TEST_SUMMARY=""
FEATURES_DONE=""
EXIT_CODE_VAL="0"
# 从 task-meta.json 提取
if [ -f "$META_FILE" ]; then
PROJECT_DIR=$(jq -r '.workdir // ""' "$META_FILE" 2>/dev/null || echo "")
AGENT_TEAMS_ENABLED=$(jq -r '.agent_teams // false' "$META_FILE" 2>/dev/null || echo "false")
EXIT_CODE_VAL=$(jq -r '.exit_code // 0' "$META_FILE" 2>/dev/null || echo "0")
# 计算耗时
STARTED=$(jq -r '.started_at // ""' "$META_FILE" 2>/dev/null || echo "")
COMPLETED=$(jq -r '.completed_at // ""' "$META_FILE" 2>/dev/null || echo "")
if [ -n "$STARTED" ] && [ -n "$COMPLETED" ]; then
START_TS=$(date -d "$STARTED" +%s 2>/dev/null || echo 0)
END_TS=$(date -d "$COMPLETED" +%s 2>/dev/null || echo 0)
if [ "$START_TS" -gt 0 ] && [ "$END_TS" -gt 0 ]; then
ELAPSED=$(( END_TS - START_TS ))
MINS=$(( ELAPSED / 60 ))
SECS=$(( ELAPSED % 60 ))
DURATION="${MINS}m${SECS}s"
fi
fi
fi
# 从 task-output.txt 提取结构化信息
if [ -f "$TASK_OUTPUT" ] && [ -s "$TASK_OUTPUT" ]; then
# 提取 Agent 信息(查找包含 agent 的表格行或列表)
AGENTS_INFO=$(grep -iE '(agent|developer|testing).*\|.*✅' "$TASK_OUTPUT" 2>/dev/null | head -6 || true)
# 提取测试结果
TEST_SUMMARY=$(grep -iE '(tests? (passed|failed)|test_|pytest|✅.*test|tests passing)' "$TASK_OUTPUT" 2>/dev/null | tail -5 || true)
# 提取 Feature/功能状态
FEATURES_DONE=$(grep -E '✅' "$TASK_OUTPUT" 2>/dev/null | grep -ivE 'agent|developer' | head -10 || true)
fi
# ---- 构建丰富的消息 ----
STATUS_EMOJI="✅"
[ "$EXIT_CODE_VAL" != "0" ] && STATUS_EMOJI="❌"
MSG="${STATUS_EMOJI} *Claude Code 任务完成*
📋 *任务:* \`${TASK_NAME}\`"
# 项目路径
[ -n "$PROJECT_DIR" ] && MSG="${MSG}
📂 *路径:* \`${PROJECT_DIR}\`"
# 耗时
[ -n "$DURATION" ] && MSG="${MSG}
⏱ *耗时:* ${DURATION}"
# Exit code (只在失败时显示)
[ "$EXIT_CODE_VAL" != "0" ] && MSG="${MSG}
⚠️ *Exit Code:* ${EXIT_CODE_VAL}"
# Agent Teams 信息
if [ "$AGENT_TEAMS_ENABLED" = "true" ]; then
MSG="${MSG}
👥 *Agent Teams:* 已启用"
if [ -n "$AGENTS_INFO" ]; then
# 清理表格格式,转为列表
AGENTS_LIST=$(echo "$AGENTS_INFO" | sed 's/|//g; s/ */ /g; s/^ //; s/ $//' | while IFS= read -r line; do echo " • $line"; done)
MSG="${MSG}
${AGENTS_LIST}"
fi
fi
# 测试结果
if [ -n "$TEST_SUMMARY" ]; then
# 提取关键测试行
TESTS_CLEAN=$(echo "$TEST_SUMMARY" | head -4 | sed 's/^[[:space:]]*//' | tr '\n' '; ' | sed 's/; $//')
MSG="${MSG}
🧪 *测试:* ${TESTS_CLEAN}"
fi
# 功能列表
if [ -n "$FEATURES_DONE" ]; then
FEAT_COUNT=$(echo "$FEATURES_DONE" | wc -l)
MSG="${MSG}
📦 *完成功能:* ${FEAT_COUNT} 项"
FEAT_LIST=$(echo "$FEATURES_DONE" | head -8 | sed 's/|//g; s/ */ /g; s/^ //; s/ $//' | while IFS= read -r line; do echo " $line"; done)
MSG="${MSG}
${FEAT_LIST}"
fi
# 生成的文件列表
if [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ]; then
FILE_TREE=$(find "$PROJECT_DIR" -maxdepth 3 -type f \
! -path '*/venv/*' ! -path '*/__pycache__/*' ! -path '*/.git/*' ! -path '*.pyc' \
2>/dev/null | sort | sed "s|${PROJECT_DIR}/||" | head -20 | while IFS= read -r f; do echo " 📄 $f"; done)
if [ -n "$FILE_TREE" ]; then
MSG="${MSG}
📁 *项目文件:*
${FILE_TREE}"
fi
fi
"$OPENCLAW_BIN" message send \
--channel telegram \
--target "$TELEGRAM_GROUP" \
--message "$MSG" 2>/dev/null && log "Sent rich Telegram message to $TELEGRAM_GROUP" || log "Telegram send failed"
# ---- 回调通知: 发到调用者 agent 的群(如果不同于通知群)----
if [ -n "$CALLBACK_GROUP" ] && [ "$CALLBACK_GROUP" != "$TELEGRAM_GROUP" ]; then
CALLBACK_MSG="🔔 *Claude Code 任务完成回调*
📋 *任务:* \`${TASK_NAME}\`
📊 *状态:* ${STATUS_EMOJI} 完成"
[ -n "$DURATION" ] && CALLBACK_MSG="${CALLBACK_MSG}
⏱ *耗时:* ${DURATION}"
# 摘要 output(限500字符)
SUMMARY=$(echo "$OUTPUT" | head -c 500 | tr '\n' ' ')
[ -n "$SUMMARY" ] && CALLBACK_MSG="${CALLBACK_MSG}
📝 *摘要:* ${SUMMARY}"
"$OPENCLAW_BIN" message send \
--channel telegram \
--target "$CALLBACK_GROUP" \
--message "$CALLBACK_MSG" 2>/dev/null && log "Sent callback to agent group $CALLBACK_GROUP" || log "Callback to $CALLBACK_GROUP failed"
fi
# ---- DM 回调: 通过指定 bot account 发 DM 给调用者 ----
if [ -n "$CALLBACK_DM" ]; then
CALLBACK_MSG="🔔 *Claude Code 任务完成*
📋 *任务:* \`${TASK_NAME}\`
📊 *状态:* ${STATUS_EMOJI} 完成"
[ -n "$DURATION" ] && CALLBACK_MSG="${CALLBACK_MSG}
⏱ *耗时:* ${DURATION}"
SUMMARY=$(echo "$OUTPUT" | head -c 500 | tr '\n' ' ')
[ -n "$SUMMARY" ] && CALLBACK_MSG="${CALLBACK_MSG}
📝 *摘要:* ${SUMMARY}"
DM_CMD=("$OPENCLAW_BIN" message send --channel telegram --target "$CALLBACK_DM" --message "$CALLBACK_MSG")
[ -n "$CALLBACK_ACCOUNT" ] && DM_CMD+=(--account "$CALLBACK_ACCOUNT")
"${DM_CMD[@]}" 2>/dev/null && log "Sent DM callback to $CALLBACK_DM (account=${CALLBACK_ACCOUNT:-default})" || log "DM callback to $CALLBACK_DM failed"
fi
fi
# ---- 方式2: 唤醒 AGI 主会话 ----
# 写入 wake 标记文件,AGI 在下次 heartbeat 时读取
WAKE_FILE="${RESULT_DIR}/pending-wake.json"
jq -n \
--arg task "$TASK_NAME" \
--arg group "$TELEGRAM_GROUP" \
--arg ts "$(date -Iseconds)" \
--arg summary "$(echo "$OUTPUT" | head -c 500 | tr '\n' ' ')" \
'{task_name: $task, telegram_group: $group, timestamp: $ts, summary: $summary, processed: false}' \
> "$WAKE_FILE" 2>/dev/null
log "Wrote pending-wake.json"
# ---- 方式3: 唤醒 AGI 主会话(通过 /hooks/wake REST API)----
# 旧方案 `openclaw agent --session-id` 有两个 bug:
# 1) session UUID 在 /new 或 /reset 后会变,解析不可靠
# 2) openclaw agent 命令本身会挂起/超时
# 新方案: POST /hooks/wake — 注入系统事件到主会话,可靠且无阻塞
GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
HOOK_TOKEN=""
# 从 config 文件读取 webhook token
OPENCLAW_CONFIG="/home/ubuntu/.openclaw/openclaw.json"
if [ -f "$OPENCLAW_CONFIG" ]; then
HOOK_TOKEN=$(jq -r '.hooks.token // ""' "$OPENCLAW_CONFIG" 2>/dev/null || echo "")
fi
WAKE_TEXT="[CLAUDE_CODE_DONE] task=${TASK_NAME} status=done group=${TELEGRAM_GROUP:-none} ts=$(date -Iseconds)"
if [ -n "$HOOK_TOKEN" ]; then
(
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
"http://localhost:${GATEWAY_PORT}/hooks/wake" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${HOOK_TOKEN}" \
-d "{\"text\":\"${WAKE_TEXT}\",\"mode\":\"now\"}" 2>/dev/null)
if [ "$HTTP_CODE" = "200" ]; then
log "Wake event sent via /hooks/wake (HTTP $HTTP_CODE)"
else
log "Wake failed (HTTP $HTTP_CODE), trying DM fallback"
# Fallback: 直接发 Telegram DM 给 Master
CALLBACK_DM=""
if [ -f "$META_FILE" ]; then
CALLBACK_DM=$(jq -r '.callback_dm // ""' "$META_FILE" 2>/dev/null || echo "")
fi
DM_TARGET="${CALLBACK_DM:-8009709280}"
timeout 10 "$OPENCLAW_BIN" message send \
--channel telegram \
--target "$DM_TARGET" \
--message "🔔 $WAKE_TEXT" </dev/null >>"$LOG" 2>&1 && \
log "Sent DM fallback to $DM_TARGET" || \
log "DM fallback also failed"
fi
) &
log "Dispatching async wake notification via /hooks/wake"
else
log "No hook token found, skipping wake notification"
fi
log "=== Hook completed ==="
exit 0