
Huashu Agent Swarm
- 584 installs
- 1.3k repo stars
- Updated August 2, 2026
- alchaincyf/huashu-skills
huashu-agent-swarm is a Claude Code skill that launches parallel git-coordinated Claude agents via tmux so developers can divide large coding projects without a central master controller.
About
huashu-agent-swarm is a Claude Code skill inspired by Nicholas Carlini's 16-instance C compiler experiment. It runs multiple independent Claude CLI agents in tmux sessions that claim tasks from TASKS.md, write code, and push through git with no master orchestrator. Setup runs bash scripts/setup_project.sh to generate AGENT_PROMPT.md, TASKS.md, and a current_tasks/ claim directory inside an existing git repository. Developers reach for huashu-agent-swarm when a monolithic agent session cannot keep pace with a large refactor, greenfield build, or parallel feature track. Default configuration starts 8 agents, and prerequisites include tmux, the Claude CLI, and a git repo with defined test commands.
- Pure Git-based self-organization with no master agent
- Each agent independently pulls, claims tasks, codes, and pushes in an infinite loop
- Supports 8+ parallel agents using tmux sessions and isolated git worktrees
- Real-time dashboard at localhost:8420 for monitoring progress and sending live instructions
- Full lifecycle scripts: setup, start, status, stop, and automatic branch merging on shutdown
Huashu Agent Swarm by the numbers
- 584 all-time installs (skills.sh)
- Ranked #1,612 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/alchaincyf/huashu-skills --skill huashu-agent-swarmAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 584 |
|---|---|
| repo stars | ★ 1.3k |
| Last updated | August 2, 2026 |
| Repository | alchaincyf/huashu-skills ↗ |
How do you run parallel Claude agents on one git repo?
Orchestrate multiple parallel Claude agents that self-organize via Git to tackle large, complex coding projects without a central master controller.
Who is it for?
Developers tackling large codebases who already use Claude CLI, tmux, and git and want decentralized parallel implementation without manual task handoffs.
Skip if: Developers on small single-file changes, teams without git discipline, or environments where tmux and Claude CLI are unavailable.
When should I use this skill?
User mentions agent swarm, 蜂群模式, multi-agent parallel development, or infinite agent loop for a git-backed project
What you get
AGENT_PROMPT.md, TASKS.md, current_tasks/ claim files, parallel tmux agent sessions, and git commits from coordinated workers
- AGENT_PROMPT.md
- TASKS.md
- parallel agent git commits
By the numbers
- Defaults to 8 parallel Claude agents
- Inspired by Nicholas Carlini's 16-instance autonomous C compiler experiment
- Scaffolds AGENT_PROMPT.md, TASKS.md, and current_tasks/ during setup
Files
Infinite Agent Loop - 无限Agent蜂群模式
受Nicholas Carlini用16个Claude实例自主构建C编译器的启发。
没有master agent,纯git自组织,每个agent独立认领任务、写代码、推送。
触发条件
当用户提到「蜂群模式」「多agent并行」「infinite loop」「agent swarm」「启动蜂群」时使用此技能。
前置要求
- tmux(
brew install tmux) - claude CLI(已安装)
- git 仓库(已有或新建)
使用流程
Step 1: 描述项目
用户告诉我:
- 项目目录路径(必须是git仓库)
- 项目目标和总体描述
- 初始任务列表(或让agent自行拆解)
- agent数量(默认8个)
- 代码规范和测试命令
Step 2: 初始化项目
bash SKILL_DIR/scripts/setup_project.sh <项目目录>这会在项目中创建:
AGENT_PROMPT.md- 从模板生成,需要我根据用户需求定制TASKS.md- 初始任务清单current_tasks/- 任务认领目录agent_logs/- 日志目录
然后我根据 references/agent-prompt-template.md 定制 AGENT_PROMPT.md,填入项目具体信息。
Step 3: 启动蜂群
bash SKILL_DIR/scripts/start_swarm.sh <agent数量> <项目目录>这会: 1. 为每个agent创建 git worktree(共享.git对象库,不浪费磁盘) 2. 创建 tmux session,每个pane一个agent 3. 每个agent进入无限循环:pull → 认领任务 → 执行 → push → 下一个
Step 4: 打开观测台
python3 SKILL_DIR/scripts/dashboard.py <项目目录> 8420浏览器打开 http://localhost:8420,可以:
- 实时查看所有agent状态、git log、任务进度
- 查看每个agent的最新日志
- 输入框直接发送指令给agent(写入HUMAN_INPUT.md)
- 一键停止所有agent
也可以用命令行监控:
# 终端状态
bash SKILL_DIR/scripts/status.sh <项目目录>
# 发送指令
bash SKILL_DIR/scripts/send_input.sh <项目目录> "你的指令"
# 直接进入tmux观察
tmux attach -t swarm-<项目名>Step 5: 停止
bash SKILL_DIR/scripts/stop_swarm.sh <项目目录>自动停止所有agent + 合并分支 + 清理worktrees。
核心机制
Git自组织协调
- 每个agent通过
current_tasks/*.lock文件认领任务 - 通过
TASKS.md了解全局进度 - 通过
git log了解其他agent的工作 - 冲突由agent自行解决
Git Worktree隔离
- 不用多份clone,用
git worktree实现隔离 - 所有worktree共享同一个
.git对象库 - 每个agent在自己的worktree独立工作
无限循环
- 每个agent完成一个session后自动开始下一个
- 通过
git pull获取其他agent的最新成果 - 通过 sleep 间隔避免API限流
关键配置
| 参数 | 默认值 | 说明 |
|---|---|---|
| agent数量 | 8 | 可在启动时指定 |
| sleep间隔 | 5秒 | agent_loop.sh中可调 |
| 模型 | claude-opus-4-6 | agent_loop.sh中可调 |
风险和应对
| 风险 | 应对 |
|---|---|
| API限流 | sleep间隔 + 可调agent数量 |
| 合并冲突 | AGENT_PROMPT指导小粒度commit |
| 死循环无用功 | 日志监控 + 停止条件 |
| 磁盘空间 | stop_swarm.sh自动清理 |
| 成本失控 | 可在AGENT_PROMPT中限制session数 |
---
花叔出品 | AI Native Coder · 独立开发者
公众号「花叔」| 30万+粉丝 | AI工具与效率提升
代表作:小猫补光灯(AppStore付费榜Top1)·《一本书玩转DeepSeek》
项目:{PROJECT_NAME}
你的身份
你是完全自主工作的Agent之一,与其他Agent并行协作。没有人指挥你,你自己决定做什么。
关键认知
- 你运行在
--dangerously-skip-permissions模式,拥有完整的bash、文件读写、git权限 - 绝不请求人类帮助或确认 — 没有人在看你的输出,你必须自己做所有决策
- 如果遇到困难,在TASKS.md中记录问题让其他agent看到,然后换一个任务做
- 如果某个命令失败,分析原因并尝试替代方案,不要停下来等待
- 你可以安装依赖、运行测试、修改配置 — 任何推进项目需要的事
人类指令通道
每次session开始时,检查 HUMAN_INPUT.md 文件:
cat HUMAN_INPUT.md 2>/dev/null如果该文件存在且有内容,优先执行其中的指令,然后清空它:
echo "" > HUMAN_INPUT.md
git add HUMAN_INPUT.md && git commit -m "Agent-{AGENT_ID}: acknowledged human input" && git push origin main你的工作方式:
- 查看任务清单,选择最重要的未完成任务
- 认领任务(创建lock文件),执行,提交成果,释放
- 每个session专注做好一件事
- 做完就commit + push,不积攒大量改动
项目目标
{PROJECT_GOAL}
技术栈
{TECH_STACK}
当前状态
每次session开始时,先了解项目现状:
# 查看最近进展
git log --oneline -20
# 查看任务清单
cat TASKS.md
# 查看其他Agent正在做什么
ls current_tasks/*.lock 2>/dev/null && cat current_tasks/*.lock工作流程
1. 拉取最新
git pull --rebase origin main2. 选择任务
查看 TASKS.md,找到:
- 未完成(
- [ ]标记) - 没有被lock(
current_tasks/中没有对应的.lock文件) - 优先选最重要/阻塞最多的任务
3. 认领任务
# 创建lock文件,内容写你的agent ID
echo "Agent-{AGENT_ID}" > current_tasks/{task_name}.lock
git add current_tasks/{task_name}.lock
git commit -m "Agent-{AGENT_ID}: claim task {task_name}"
git push origin main4. 执行任务
- 写代码、写测试
- 确保代码质量
- 运行测试验证
5. 提交成果
git add -A
git commit -m "Agent-{AGENT_ID}: {简要描述做了什么}"
git push origin main小粒度提交:每完成一个有意义的步骤就提交,不要积攒。
6. 释放任务
rm current_tasks/{task_name}.lock
git add current_tasks/{task_name}.lock
git commit -m "Agent-{AGENT_ID}: complete task {task_name}"
git push origin main7. 更新TASKS.md
- 标记已完成的任务(
- [x]) - 如果发现新任务或子任务,添加到列表
- commit + push
任务选择策略
1. 优先修复失败的测试 2. 优先做阻塞其他任务的工作 3. 避免和其他agent做同一件事(检查lock文件) 4. 如果所有任务都被认领,去找新的改进点(测试覆盖、文档、重构) 5. 如果真的没事做,在TASKS.md中记录你的观察
代码规范
{CODE_STANDARDS}
测试策略
- 每次改动都要跑测试:
{TEST_COMMAND} - 确保你的改动不破坏已有功能
- 新功能必须有对应测试
合并冲突
如果 git pull --rebase 有冲突: 1. 查看冲突文件 2. 理解双方的改动意图 3. 保留功能正确的版本 4. 如果不确定,优先保留其他agent的改动(他们可能有更完整的上下文) 5. 解决后 git add + git rebase --continue
停止条件
如果以下条件全部满足,你可以结束当前session(不用死等):
- TASKS.md中所有任务都标记为
[x] - 没有失败的测试
- 没有
HUMAN_INPUT.md指令
结束时在TASKS.md末尾加一行:<!-- Agent-{AGENT_ID}: all tasks complete at {timestamp} -->
注意事项
- 每次session专注做好一件事,不要贪多
- 做完就commit + push,不要积攒大量改动
- 如果遇到困难,在TASKS.md中记录问题供其他agent参考
- 不要修改 AGENT_PROMPT.md
- 遵循项目已有的代码风格
- 写清楚commit message,其他agent需要通过git log了解你做了什么
- 绝不使用交互式命令(如
git add -i、git rebase -i、nano、vim)— 你没有TTY
Carlini实验经验教训
Nicholas Carlini用16个Claude实例自主构建了一个C编译器。
以下是从他的实验中提炼的关键经验。
核心发现
1. Git是最好的协调机制
不需要复杂的消息队列或中央调度器。Git本身就是:
- 状态同步工具(pull/push)
- 冲突检测器(merge conflict)
- 进度记录器(git log)
- 任务追踪器(通过约定的文件结构)
2. Agent需要清晰的「自我认知」
AGENT_PROMPT.md必须告诉agent:
- 你是谁(多个并行agent之一)
- 你的工作方式(认领-执行-提交循环)
- 如何与其他agent协调(通过git,不是通过对话)
- 什么时候停下来(任务完成/遇到阻塞)
3. 小粒度Commit是关键
Carlini发现,让agent做大量改动再一次性提交会导致:
- 合并冲突概率剧增
- 其他agent无法及时获取进展
- 出错时难以回滚
正确做法:每完成一个有意义的步骤就commit+push。
4. Lock文件机制足够用
不需要数据库或分布式锁服务:
- 创建文件 = 认领任务
- 删除文件 = 释放任务
- Git commit = 广播给所有人
- 偶尔的冲突可以接受(两个agent同时认领同一任务,后push的会失败)
5. Agent会自发分工
给8个agent同样的prompt,它们会:
- 自然地选择不同的任务
- 发现并填补空缺
- 甚至自发地添加新任务到TASKS.md
不需要人工分配,只需要一个好的任务清单。
常见问题和解决方案
问题:Agent陷入循环做无用功
症状:agent反复修改同一个文件,没有实质进展 原因:任务描述不清晰,或agent不知道「完成」的标准 解决:在AGENT_PROMPT中明确定义每个任务的验收标准(通常是测试通过)
问题:合并冲突频繁
症状:多个agent频繁修改同一个文件 原因:任务粒度太粗,或文件职责不清 解决:
- 拆分大文件为小模块
- 在TASKS.md中标注文件所有权
- 鼓励agent先创建新文件,再集成
问题:Agent做了不该做的事
症状:agent修改了配置文件、删除了重要代码 原因:AGENT_PROMPT没有明确禁止 解决:在AGENT_PROMPT中添加「禁止」清单
问题:API限流
症状:agent报错rate limit 原因:太多agent同时请求 解决:
- 增加sleep间隔
- 减少agent数量
- 使用不同的API key(如果有多个)
问题:Worktree磁盘空间
症状:磁盘空间不足 原因:worktree虽然共享.git对象,但工作文件是独立的 解决:stop_swarm.sh会自动清理;大项目减少agent数量
最佳实践
1. 先小规模测试:用2个agent测试,确认流程正常再扩展 2. 任务要具体:「实现用户登录」比「完善认证系统」更好 3. 测试是锚点:有测试的项目,agent更容易判断自己的改动是否正确 4. 日志要看:定期检查agent_logs,发现问题早期干预 5. 主分支要干净:agent在自己的分支工作,合并到main时检查
适用场景
非常适合
- 功能明确、可并行的开发任务
- 有完善测试的项目
- 模块化架构的代码库
不太适合
- 需要大量讨论和决策的架构设计
- 高度耦合的代码修改
- 需要人类审查每一步的敏感操作
#!/bin/bash
# agent_loop.sh - 单个Agent的无限循环逻辑
# 环境变量: AGENT_ID, WORK_DIR, LOG_DIR
# 用法: AGENT_ID=1 WORK_DIR=/path/to/worktree LOG_DIR=/path/to/logs bash agent_loop.sh
set -e
AGENT_ID="${AGENT_ID:-1}"
WORK_DIR="${WORK_DIR:-.}"
LOG_DIR="${LOG_DIR:-./agent_logs}"
SLEEP_INTERVAL="${SLEEP_INTERVAL:-5}"
MODEL="${MODEL:-claude-opus-4-6}"
MAX_SESSIONS="${MAX_SESSIONS:-0}" # 0=无限
mkdir -p "$LOG_DIR"
SESSION_COUNT=0
echo "[Agent-${AGENT_ID}] 启动,工作目录: $WORK_DIR"
echo "[Agent-${AGENT_ID}] 模型: $MODEL, 休息间隔: ${SLEEP_INTERVAL}s"
while true; do
SESSION_COUNT=$((SESSION_COUNT + 1))
# 检查最大session限制
if [ "$MAX_SESSIONS" -gt 0 ] && [ "$SESSION_COUNT" -gt "$MAX_SESSIONS" ]; then
echo "[Agent-${AGENT_ID}] 已达到最大session数 ($MAX_SESSIONS),退出"
break
fi
echo ""
echo "================================================"
echo "[Agent-${AGENT_ID}] Session #${SESSION_COUNT} 开始 - $(date '+%Y-%m-%d %H:%M:%S')"
echo "================================================"
cd "$WORK_DIR"
# 同步最新代码
echo "[Agent-${AGENT_ID}] 同步最新代码..."
git pull --rebase origin main 2>/dev/null || true
COMMIT=$(git rev-parse --short=6 HEAD 2>/dev/null || echo "unknown")
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
LOGFILE="${LOG_DIR}/agent_${AGENT_ID}_session_${SESSION_COUNT}_${TIMESTAMP}.log"
echo "[Agent-${AGENT_ID}] 当前HEAD: $COMMIT"
echo "[Agent-${AGENT_ID}] 日志: $LOGFILE"
# 启动Claude会话
echo "[Agent-${AGENT_ID}] 启动Claude会话..."
claude --dangerously-skip-permissions \
-p "$(cat AGENT_PROMPT.md)
你是 Agent-${AGENT_ID},这是你的第 ${SESSION_COUNT} 个session。
当前时间: $(date '+%Y-%m-%d %H:%M:%S')
当前HEAD: ${COMMIT}
请按照 AGENT_PROMPT.md 的工作流程执行任务。" \
--model "$MODEL" \
2>&1 | tee "$LOGFILE"
EXIT_CODE=${PIPESTATUS[0]}
echo "[Agent-${AGENT_ID}] Claude退出,状态码: $EXIT_CODE"
# 推送成果(如果有新commit)
NEW_COMMIT=$(git rev-parse --short=6 HEAD 2>/dev/null || echo "unknown")
if [ "$NEW_COMMIT" != "$COMMIT" ]; then
echo "[Agent-${AGENT_ID}] 有新提交,推送中..."
git push origin HEAD:main 2>/dev/null || {
echo "[Agent-${AGENT_ID}] 推送失败,尝试rebase后重推..."
git fetch origin main 2>/dev/null && git rebase origin/main 2>/dev/null || true
git push origin HEAD:main 2>/dev/null || echo "[Agent-${AGENT_ID}] 推送仍失败,跳过"
}
else
echo "[Agent-${AGENT_ID}] 无新提交"
fi
# 休息,避免API限流
echo "[Agent-${AGENT_ID}] 休息 ${SLEEP_INTERVAL}s..."
sleep "$SLEEP_INTERVAL"
done
echo "[Agent-${AGENT_ID}] 循环结束"
#!/usr/bin/env python3
"""
蜂群观测台 - Swarm Dashboard
用法: python3 dashboard.py [项目目录] [端口]
示例: python3 dashboard.py /path/to/project 8420
"""
import http.server
import json
import os
import subprocess
import sys
import urllib.parse
from pathlib import Path
PROJECT_DIR = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else Path.cwd()
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8420
PROJECT_NAME = PROJECT_DIR.name
SESSION_NAME = f"swarm-{PROJECT_NAME}"
def run_cmd(cmd, cwd=None):
"""运行shell命令,返回输出"""
try:
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
cwd=cwd or str(PROJECT_DIR), timeout=10
)
return result.stdout.strip()
except Exception as e:
return f"error: {e}"
def get_status():
"""获取蜂群完整状态"""
# tmux状态
tmux_running = run_cmd(f"tmux has-session -t {SESSION_NAME} 2>/dev/null && echo 'yes' || echo 'no'")
pane_count = 0
if tmux_running == "yes":
pane_count = int(run_cmd(f"tmux list-panes -t {SESSION_NAME} 2>/dev/null | wc -l").strip() or "0")
# git log
git_log = run_cmd("git log --oneline --all -30")
# TASKS.md
tasks_file = PROJECT_DIR / "TASKS.md"
tasks_content = tasks_file.read_text() if tasks_file.exists() else ""
# 统计任务
task_total = tasks_content.count("- [")
task_done = tasks_content.count("- [x]")
task_in_progress = tasks_content.count("- [~]")
task_todo = task_total - task_done - task_in_progress
# lock文件
locks = []
lock_dir = PROJECT_DIR / "current_tasks"
if lock_dir.exists():
for f in lock_dir.glob("*.lock"):
locks.append({
"task": f.stem,
"owner": f.read_text().strip()
})
# worktree状态
worktrees = []
for d in sorted(PROJECT_DIR.parent.glob(f"{PROJECT_NAME}-agent-*")):
if d.is_dir():
agent_num = d.name.split("-")[-1]
last_commit = run_cmd("git log --oneline -1", cwd=str(d))
worktrees.append({
"id": agent_num,
"dir": str(d),
"last_commit": last_commit
})
# 最近日志
log_dir = PROJECT_DIR / "agent_logs"
recent_logs = []
if log_dir.exists():
log_files = sorted(log_dir.glob("*.log"), key=lambda f: f.stat().st_mtime, reverse=True)
for lf in log_files[:8]:
try:
lines = lf.read_text().splitlines()
tail = lines[-15:] if len(lines) > 15 else lines
recent_logs.append({
"file": lf.name,
"tail": "\n".join(tail),
"size": lf.stat().st_size,
"mtime": lf.stat().st_mtime
})
except:
pass
# HUMAN_INPUT.md
hi_file = PROJECT_DIR / "HUMAN_INPUT.md"
human_input = hi_file.read_text().strip() if hi_file.exists() else ""
return {
"project": PROJECT_NAME,
"project_dir": str(PROJECT_DIR),
"tmux_running": tmux_running == "yes",
"pane_count": pane_count,
"git_log": git_log,
"tasks_content": tasks_content,
"task_stats": {
"total": task_total,
"done": task_done,
"in_progress": task_in_progress,
"todo": task_todo
},
"locks": locks,
"worktrees": worktrees,
"recent_logs": recent_logs,
"human_input": human_input
}
def send_human_input(message):
"""发送人类指令"""
import datetime
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
hi_file = PROJECT_DIR / "HUMAN_INPUT.md"
hi_file.write_text(f"# 人类指令 - {ts}\n\n{message}\n")
run_cmd("git add HUMAN_INPUT.md")
run_cmd(f'git commit -m "HUMAN: {message[:50]}"')
run_cmd("git push origin main 2>/dev/null || true")
return {"ok": True, "message": message}
DASHBOARD_HTML = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Swarm Dashboard</title>
<style>
:root {
--bg: #1a1a2e;
--surface: #16213e;
--surface2: #1a2744;
--border: #2a3a5c;
--text: #e0e0e0;
--text2: #8892a4;
--accent: #4ecca3;
--accent2: #3ba386;
--warn: #e8a838;
--error: #e84855;
--done: #4ecca3;
--running: #38b6ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* Header */
.header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 18px;
font-weight: 600;
color: var(--accent);
}
.header h1 span { color: var(--text2); font-weight: 400; }
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-badge.running {
background: rgba(78, 204, 163, 0.15);
color: var(--accent);
}
.status-badge.stopped {
background: rgba(232, 72, 85, 0.15);
color: var(--error);
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-dot.running { background: var(--accent); animation: pulse 2s infinite; }
.status-dot.stopped { background: var(--error); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Layout */
.main {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto 1fr;
gap: 16px;
padding: 16px 24px;
max-width: 1600px;
margin: 0 auto;
}
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.card-header {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h2 {
font-size: 13px;
font-weight: 600;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.card-body { padding: 12px 16px; }
/* Stats bar */
.stats-bar {
grid-column: 1 / -1;
display: flex;
gap: 16px;
}
.stat-card {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: var(--accent);
}
.stat-value.warn { color: var(--warn); }
.stat-value.running-color { color: var(--running); }
.stat-label {
font-size: 11px;
color: var(--text2);
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Agent list */
.agent-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
border-bottom: 1px solid rgba(42, 58, 92, 0.5);
font-size: 13px;
}
.agent-item:last-child { border-bottom: none; }
.agent-id {
background: var(--surface2);
color: var(--running);
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
min-width: 28px;
text-align: center;
}
.agent-commit {
color: var(--text2);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Lock list */
.lock-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(42, 58, 92, 0.5);
font-size: 13px;
}
.lock-task {
color: var(--warn);
font-weight: 500;
}
.lock-owner { color: var(--text2); }
/* Tasks */
.tasks-content {
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
/* Git log */
.git-log {
font-size: 12px;
line-height: 1.7;
max-height: 300px;
overflow-y: auto;
color: var(--text2);
}
.git-log .hash {
color: var(--warn);
}
/* Logs */
.logs-area {
grid-column: 1 / -1;
}
.log-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
overflow-x: auto;
}
.log-tab {
padding: 8px 14px;
font-size: 12px;
color: var(--text2);
cursor: pointer;
border-bottom: 2px solid transparent;
white-space: nowrap;
background: none;
border-top: none;
border-left: none;
border-right: none;
font-family: inherit;
}
.log-tab:hover { color: var(--text); }
.log-tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
.log-content {
font-size: 12px;
line-height: 1.6;
padding: 12px 16px;
max-height: 250px;
overflow-y: auto;
white-space: pre-wrap;
color: var(--text2);
background: rgba(0,0,0,0.2);
}
/* Input area */
.input-area {
grid-column: 1 / -1;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
}
.input-area h2 {
font-size: 13px;
font-weight: 600;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.input-row {
display: flex;
gap: 10px;
}
.input-row input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 14px;
color: var(--text);
font-family: inherit;
font-size: 14px;
outline: none;
}
.input-row input:focus {
border-color: var(--accent);
}
.input-row input::placeholder { color: var(--text2); }
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.btn-send {
background: var(--accent);
color: var(--bg);
}
.btn-send:hover { background: var(--accent2); }
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-stop {
background: rgba(232, 72, 85, 0.15);
color: var(--error);
border: 1px solid var(--error);
}
.btn-stop:hover { background: rgba(232, 72, 85, 0.3); }
.pending-input {
margin-top: 8px;
font-size: 12px;
color: var(--warn);
}
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--accent);
color: var(--bg);
padding: 10px 20px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
opacity: 0;
transform: translateY(10px);
transition: all 0.3s;
pointer-events: none;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.empty { color: var(--text2); font-size: 13px; font-style: italic; }
.refresh-info {
font-size: 11px;
color: var(--text2);
}
/* scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
</style>
</head>
<body>
<div class="header">
<h1>SWARM <span id="project-name"></span></h1>
<div style="display:flex;align-items:center;gap:16px">
<span class="refresh-info" id="refresh-info"></span>
<span class="status-badge" id="tmux-badge">
<span class="status-dot" id="tmux-dot"></span>
<span id="tmux-text"></span>
</span>
</div>
</div>
<div class="main">
<!-- Stats -->
<div class="stats-bar">
<div class="stat-card">
<div class="stat-value running-color" id="stat-agents">-</div>
<div class="stat-label">Agents</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-done">-</div>
<div class="stat-label">Done</div>
</div>
<div class="stat-card">
<div class="stat-value warn" id="stat-progress">-</div>
<div class="stat-label">In Progress</div>
</div>
<div class="stat-card">
<div class="stat-value running-color" id="stat-todo">-</div>
<div class="stat-label">Todo</div>
</div>
</div>
<!-- Agents -->
<div class="card">
<div class="card-header"><h2>Agents</h2></div>
<div class="card-body" id="agents-list">
<span class="empty">loading...</span>
</div>
</div>
<!-- Active Tasks (locks) -->
<div class="card">
<div class="card-header"><h2>Active Tasks</h2></div>
<div class="card-body" id="locks-list">
<span class="empty">loading...</span>
</div>
</div>
<!-- TASKS.md -->
<div class="card">
<div class="card-header"><h2>Tasks.md</h2></div>
<div class="card-body">
<div class="tasks-content" id="tasks-content">loading...</div>
</div>
</div>
<!-- Git Log -->
<div class="card">
<div class="card-header"><h2>Git Log</h2></div>
<div class="card-body">
<div class="git-log" id="git-log">loading...</div>
</div>
</div>
<!-- Logs -->
<div class="card logs-area">
<div class="card-header"><h2>Agent Logs</h2></div>
<div class="log-tabs" id="log-tabs"></div>
<div class="log-content" id="log-content">选择一个agent日志查看</div>
</div>
<!-- Human Input -->
<div class="input-area">
<h2>Send Command to Agents</h2>
<div class="input-row">
<input type="text" id="human-input" placeholder="输入指令,agent下一个session会优先执行..."
onkeydown="if(event.key==='Enter')sendInput()">
<button class="btn btn-send" id="send-btn" onclick="sendInput()">发送</button>
<button class="btn btn-stop" onclick="stopSwarm()">Stop All</button>
</div>
<div class="pending-input" id="pending-input"></div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
let currentLogIndex = 0;
let statusData = null;
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
}
function formatGitLog(log) {
if (!log) return '<span class="empty">no commits</span>';
return log.split('\\n').map(line => {
const parts = line.match(/^([a-f0-9]+)\\s(.*)$/);
if (parts) return `<span class="hash">${parts[1]}</span> ${escHtml(parts[2])}`;
return escHtml(line);
}).join('\\n');
}
function escHtml(s) {
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
async function fetchStatus() {
try {
const res = await fetch('/api/status');
statusData = await res.json();
render(statusData);
} catch(e) {
console.error('fetch error:', e);
}
}
function render(d) {
document.getElementById('project-name').textContent = d.project;
document.title = `Swarm - ${d.project}`;
// tmux badge
const badge = document.getElementById('tmux-badge');
const dot = document.getElementById('tmux-dot');
const txt = document.getElementById('tmux-text');
if (d.tmux_running) {
badge.className = 'status-badge running';
dot.className = 'status-dot running';
txt.textContent = `Running (${d.pane_count} panes)`;
} else {
badge.className = 'status-badge stopped';
dot.className = 'status-dot stopped';
txt.textContent = 'Stopped';
}
// refresh info
document.getElementById('refresh-info').textContent =
`Updated ${new Date().toLocaleTimeString()}`;
// stats
document.getElementById('stat-agents').textContent = d.worktrees.length;
document.getElementById('stat-done').textContent = d.task_stats.done;
document.getElementById('stat-progress').textContent = d.task_stats.in_progress;
document.getElementById('stat-todo').textContent = d.task_stats.todo;
// agents
const al = document.getElementById('agents-list');
if (d.worktrees.length === 0) {
al.innerHTML = '<span class="empty">no agents running</span>';
} else {
al.innerHTML = d.worktrees.map(w =>
`<div class="agent-item">
<span class="agent-id">${w.id}</span>
<span class="agent-commit">${escHtml(w.last_commit)}</span>
</div>`
).join('');
}
// locks
const ll = document.getElementById('locks-list');
if (d.locks.length === 0) {
ll.innerHTML = '<span class="empty">no active locks</span>';
} else {
ll.innerHTML = d.locks.map(l =>
`<div class="lock-item">
<span class="lock-task">${escHtml(l.task)}</span>
<span class="lock-owner">${escHtml(l.owner)}</span>
</div>`
).join('');
}
// tasks
document.getElementById('tasks-content').textContent = d.tasks_content || '(empty)';
// git log
document.getElementById('git-log').innerHTML = formatGitLog(d.git_log);
// log tabs
const tabs = document.getElementById('log-tabs');
if (d.recent_logs.length > 0) {
tabs.innerHTML = d.recent_logs.map((l, i) =>
`<button class="log-tab ${i === currentLogIndex ? 'active' : ''}"
onclick="selectLog(${i})">${l.file.replace('agent_','').replace('.log','')}</button>`
).join('');
showLog(d.recent_logs[currentLogIndex]);
} else {
tabs.innerHTML = '';
document.getElementById('log-content').textContent = 'no logs yet';
}
// pending input
const pi = document.getElementById('pending-input');
if (d.human_input) {
pi.textContent = `Pending: ${d.human_input}`;
} else {
pi.textContent = '';
}
}
function selectLog(i) {
currentLogIndex = i;
if (statusData) {
document.querySelectorAll('.log-tab').forEach((t, j) =>
t.classList.toggle('active', j === i));
showLog(statusData.recent_logs[i]);
}
}
function showLog(log) {
const el = document.getElementById('log-content');
el.textContent = log ? log.tail : 'empty';
el.scrollTop = el.scrollHeight;
}
async function sendInput() {
const input = document.getElementById('human-input');
const msg = input.value.trim();
if (!msg) return;
const btn = document.getElementById('send-btn');
btn.disabled = true;
btn.textContent = '...';
try {
const res = await fetch('/api/input', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: msg})
});
const data = await res.json();
if (data.ok) {
showToast('Command sent!');
input.value = '';
fetchStatus();
}
} catch(e) {
showToast('Send failed: ' + e);
} finally {
btn.disabled = false;
btn.textContent = '发送';
}
}
async function stopSwarm() {
if (!confirm('确定要停止所有agent吗?')) return;
try {
const res = await fetch('/api/stop', {method: 'POST'});
const data = await res.json();
showToast(data.message || 'Stopped');
setTimeout(fetchStatus, 1000);
} catch(e) {
showToast('Stop failed: ' + e);
}
}
// Auto refresh
fetchStatus();
setInterval(fetchStatus, 5000);
</script>
</body>
</html>"""
class DashboardHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, format, *args):
pass # 静默日志
def do_GET(self):
if self.path == "/api/status":
self.send_json(get_status())
elif self.path == "/" or self.path == "/index.html":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(DASHBOARD_HTML.encode())
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length).decode() if content_length else "{}"
if self.path == "/api/input":
try:
data = json.loads(body)
result = send_human_input(data.get("message", ""))
self.send_json(result)
except Exception as e:
self.send_json({"ok": False, "error": str(e)})
elif self.path == "/api/stop":
try:
skill_dir = Path(__file__).parent.parent
run_cmd(f"bash {skill_dir}/scripts/stop_swarm.sh {PROJECT_DIR}")
self.send_json({"ok": True, "message": "蜂群已停止"})
except Exception as e:
self.send_json({"ok": False, "error": str(e)})
else:
self.send_response(404)
self.end_headers()
def send_json(self, data):
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
if __name__ == "__main__":
print(f"🐝 蜂群观测台启动")
print(f" 项目: {PROJECT_DIR}")
print(f" 地址: http://localhost:{PORT}")
print(f" 按 Ctrl+C 停止")
print()
server = http.server.HTTPServer(("0.0.0.0", PORT), DashboardHandler)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n观测台已关闭")
server.server_close()
#!/bin/bash
# send_input.sh - 向蜂群发送人类指令
# 用法: bash send_input.sh <项目目录> "你的指令"
# 示例: bash send_input.sh /path/to/project "停止重构,优先修复登录bug"
PROJECT_DIR="${1:-.}"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
MESSAGE="$2"
if [ -z "$MESSAGE" ]; then
echo "用法: bash send_input.sh <项目目录> \"你的指令\""
echo ""
echo "示例:"
echo " bash send_input.sh . \"优先修复登录页面的bug\""
echo " bash send_input.sh . \"不要动 config/ 目录下的文件\""
echo " bash send_input.sh . \"所有agent停止当前工作,先跑一遍完整测试\""
exit 1
fi
# 写入指令
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
cat > "$PROJECT_DIR/HUMAN_INPUT.md" << EOF
# 人类指令 - ${TIMESTAMP}
${MESSAGE}
EOF
# 提交并推送
cd "$PROJECT_DIR"
git add HUMAN_INPUT.md
git commit -m "HUMAN: ${MESSAGE:0:50}" 2>/dev/null
git push origin main 2>/dev/null || true
echo "指令已发送: $MESSAGE"
echo "下一个启动session的agent会优先执行此指令"
#!/bin/bash
# setup_project.sh - 初始化项目结构,为蜂群模式做准备
# 用法: bash setup_project.sh <项目目录>
set -e
PROJECT_DIR="${1:-.}"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
echo "=== 初始化蜂群项目: $PROJECT_DIR ==="
# 检查是否是git仓库
if [ ! -d "$PROJECT_DIR/.git" ]; then
echo "初始化git仓库..."
git -C "$PROJECT_DIR" init
git -C "$PROJECT_DIR" add -A
git -C "$PROJECT_DIR" commit -m "Initial commit" --allow-empty
fi
# 确保有main分支
CURRENT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current)
if [ "$CURRENT_BRANCH" != "main" ]; then
# 检查是否存在main分支
if git -C "$PROJECT_DIR" show-ref --verify --quiet refs/heads/main 2>/dev/null; then
echo "注意: 当前在 $CURRENT_BRANCH 分支,main分支已存在"
else
echo "创建main分支..."
git -C "$PROJECT_DIR" branch -M main
fi
fi
# 创建目录结构
mkdir -p "$PROJECT_DIR/current_tasks"
mkdir -p "$PROJECT_DIR/agent_logs"
# 创建.gitkeep
touch "$PROJECT_DIR/current_tasks/.gitkeep"
# agent_logs加入gitignore
if [ ! -f "$PROJECT_DIR/.gitignore" ]; then
echo "agent_logs/" > "$PROJECT_DIR/.gitignore"
else
if ! grep -q "agent_logs/" "$PROJECT_DIR/.gitignore" 2>/dev/null; then
echo "agent_logs/" >> "$PROJECT_DIR/.gitignore"
fi
fi
# 复制AGENT_PROMPT模板(如果不存在)
if [ ! -f "$PROJECT_DIR/AGENT_PROMPT.md" ]; then
cp "$SKILL_DIR/references/agent-prompt-template.md" "$PROJECT_DIR/AGENT_PROMPT.md"
echo "已创建 AGENT_PROMPT.md(模板),请根据项目需求定制内容"
fi
# 创建HUMAN_INPUT.md(人类指令通道)
if [ ! -f "$PROJECT_DIR/HUMAN_INPUT.md" ]; then
touch "$PROJECT_DIR/HUMAN_INPUT.md"
echo "已创建 HUMAN_INPUT.md(人类指令通道)"
fi
# 创建初始TASKS.md(如果不存在)
if [ ! -f "$PROJECT_DIR/TASKS.md" ]; then
cat > "$PROJECT_DIR/TASKS.md" << 'EOF'
# 任务清单
## 状态说明
- [ ] 待完成
- [x] 已完成
- [~] 进行中(查看 current_tasks/ 目录确认谁在做)
## 任务列表
<!-- Agent会自行维护这个列表 -->
<!-- 在这里添加初始任务 -->
EOF
echo "已创建 TASKS.md,请添加初始任务"
fi
# 提交初始结构
cd "$PROJECT_DIR"
git add -A
git diff --cached --quiet || git commit -m "Setup swarm project structure
- Added current_tasks/ for task claiming
- Added agent_logs/ (gitignored)
- Added AGENT_PROMPT.md template
- Added TASKS.md"
echo ""
echo "=== 初始化完成 ==="
echo ""
echo "下一步:"
echo "1. 编辑 AGENT_PROMPT.md - 填入项目目标、代码规范、测试命令"
echo "2. 编辑 TASKS.md - 添加初始任务"
echo "3. 运行 start_swarm.sh 启动蜂群"
#!/bin/bash
# start_swarm.sh - 启动Agent蜂群(tmux多pane)
# 用法: bash start_swarm.sh [agent数量] [项目目录]
# 示例: bash start_swarm.sh 8 /path/to/project
set -e
NUM_AGENTS="${1:-8}"
PROJECT_DIR="${2:-.}"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SESSION="swarm-${PROJECT_NAME}"
LOG_DIR="${PROJECT_DIR}/agent_logs"
# ===== 前置检查 =====
if ! command -v tmux >/dev/null 2>&1; then
echo "错误: 请先安装tmux"
echo " macOS: brew install tmux"
echo " Ubuntu: sudo apt install tmux"
exit 1
fi
if ! command -v claude >/dev/null 2>&1; then
echo "错误: 请先安装 claude CLI"
exit 1
fi
if [ ! -f "$PROJECT_DIR/AGENT_PROMPT.md" ]; then
echo "错误: 找不到 AGENT_PROMPT.md"
echo "请先运行: bash $SKILL_DIR/scripts/setup_project.sh $PROJECT_DIR"
exit 1
fi
if [ ! -d "$PROJECT_DIR/.git" ]; then
echo "错误: $PROJECT_DIR 不是git仓库"
exit 1
fi
echo "=== 启动蜂群模式 ==="
echo "项目: $PROJECT_DIR"
echo "Agent数量: $NUM_AGENTS"
echo "tmux session: $SESSION"
echo ""
# ===== 创建日志目录 =====
mkdir -p "$LOG_DIR"
# ===== 确保main分支存在且有至少一个commit =====
cd "$PROJECT_DIR"
MAIN_BRANCH=$(git branch --show-current)
# ===== 创建 git worktrees =====
echo "创建 git worktrees..."
for i in $(seq 1 "$NUM_AGENTS"); do
AGENT_DIR="${PROJECT_DIR}-agent-${i}"
BRANCH_NAME="agent-${i}-work"
if [ -d "$AGENT_DIR" ]; then
echo " Agent-${i}: worktree已存在,跳过"
else
# 创建agent专用分支的worktree
git worktree add "$AGENT_DIR" -b "$BRANCH_NAME" "$MAIN_BRANCH" 2>/dev/null || \
git worktree add "$AGENT_DIR" "$BRANCH_NAME" 2>/dev/null || {
echo " Agent-${i}: 创建worktree失败,尝试清理后重试..."
git worktree prune
git branch -D "$BRANCH_NAME" 2>/dev/null || true
git worktree add "$AGENT_DIR" -b "$BRANCH_NAME" "$MAIN_BRANCH"
}
echo " Agent-${i}: worktree 创建完成 → $AGENT_DIR"
fi
# 同步AGENT_PROMPT.md和TASKS.md到worktree
cp "$PROJECT_DIR/AGENT_PROMPT.md" "$AGENT_DIR/AGENT_PROMPT.md"
[ -f "$PROJECT_DIR/TASKS.md" ] && cp "$PROJECT_DIR/TASKS.md" "$AGENT_DIR/TASKS.md"
done
echo ""
# ===== 创建tmux session =====
echo "创建 tmux session..."
tmux kill-session -t "$SESSION" 2>/dev/null || true
# 创建session并启动第一个agent
AGENT_DIR="${PROJECT_DIR}-agent-1"
tmux new-session -d -s "$SESSION" -x 200 -y 50 \
"AGENT_ID=1 WORK_DIR='$AGENT_DIR' LOG_DIR='$LOG_DIR' bash '$SKILL_DIR/scripts/agent_loop.sh'; read"
# 为后续agent创建新pane
for i in $(seq 2 "$NUM_AGENTS"); do
AGENT_DIR="${PROJECT_DIR}-agent-${i}"
tmux split-window -t "$SESSION" \
"AGENT_ID=$i WORK_DIR='$AGENT_DIR' LOG_DIR='$LOG_DIR' bash '$SKILL_DIR/scripts/agent_loop.sh'; read"
# 重新排列布局
tmux select-layout -t "$SESSION" tiled
done
echo ""
echo "=== 蜂群已启动!==="
echo ""
echo "查看agent工作:"
echo " tmux attach -t $SESSION"
echo ""
echo "查看状态:"
echo " bash $SKILL_DIR/scripts/status.sh $PROJECT_DIR"
echo ""
echo "停止所有agent:"
echo " bash $SKILL_DIR/scripts/stop_swarm.sh $PROJECT_DIR"
echo ""
# 自动attach到tmux session
tmux attach -t "$SESSION"
#!/bin/bash
# status.sh - 查看蜂群状态
# 用法: bash status.sh [项目目录]
PROJECT_DIR="${1:-.}"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SESSION="swarm-${PROJECT_NAME}"
echo "=== 蜂群状态: $PROJECT_NAME ==="
echo ""
# ===== tmux状态 =====
echo "--- tmux session ---"
if tmux has-session -t "$SESSION" 2>/dev/null; then
PANE_COUNT=$(tmux list-panes -t "$SESSION" 2>/dev/null | wc -l | tr -d ' ')
echo "运行中: $SESSION ($PANE_COUNT 个agent)"
else
echo "未运行"
fi
echo ""
# ===== worktree状态 =====
echo "--- Worktrees ---"
WORKTREE_COUNT=0
for dir in "${PROJECT_DIR}"-agent-*; do
if [ -d "$dir" ]; then
WORKTREE_COUNT=$((WORKTREE_COUNT + 1))
AGENT_NUM=$(echo "$dir" | grep -o '[0-9]*$')
LAST_COMMIT=$(git -C "$dir" log --oneline -1 2>/dev/null || echo "无提交")
echo " Agent-${AGENT_NUM}: $LAST_COMMIT"
fi
done
[ "$WORKTREE_COUNT" -eq 0 ] && echo " 无(蜂群未启动或已清理)"
echo ""
# ===== 当前任务认领 =====
echo "--- 当前任务认领 ---"
LOCK_COUNT=0
for lock in "$PROJECT_DIR/current_tasks/"*.lock; do
[ -f "$lock" ] || continue
LOCK_COUNT=$((LOCK_COUNT + 1))
TASK_NAME=$(basename "$lock" .lock)
OWNER=$(cat "$lock" 2>/dev/null || echo "unknown")
echo " $TASK_NAME → $OWNER"
done
[ "$LOCK_COUNT" -eq 0 ] && echo " 无(没有进行中的任务)"
echo ""
# ===== 最近提交 =====
echo "--- 最近20条提交 ---"
git -C "$PROJECT_DIR" log --oneline --all -20 2>/dev/null || echo " 无提交记录"
echo ""
# ===== TASKS.md概要 =====
echo "--- 任务清单 (TASKS.md) ---"
if [ -f "$PROJECT_DIR/TASKS.md" ]; then
# 统计任务状态
TOTAL=$(grep -c '^\- \[' "$PROJECT_DIR/TASKS.md" 2>/dev/null || echo "0")
DONE=$(grep -c '^\- \[x\]' "$PROJECT_DIR/TASKS.md" 2>/dev/null || echo "0")
IN_PROGRESS=$(grep -c '^\- \[\~\]' "$PROJECT_DIR/TASKS.md" 2>/dev/null || echo "0")
TODO=$((TOTAL - DONE - IN_PROGRESS))
echo " 总计: $TOTAL | 完成: $DONE | 进行中: $IN_PROGRESS | 待做: $TODO"
echo ""
cat "$PROJECT_DIR/TASKS.md"
else
echo " TASKS.md 不存在"
fi
echo ""
# ===== 日志摘要 =====
echo "--- 最近日志 ---"
if [ -d "$PROJECT_DIR/agent_logs" ]; then
LATEST_LOG=$(ls -t "$PROJECT_DIR/agent_logs/"*.log 2>/dev/null | head -1)
if [ -n "$LATEST_LOG" ]; then
LOG_COUNT=$(ls "$PROJECT_DIR/agent_logs/"*.log 2>/dev/null | wc -l | tr -d ' ')
echo " 共 $LOG_COUNT 个日志文件"
echo " 最新: $(basename "$LATEST_LOG")"
echo " 最后几行:"
tail -5 "$LATEST_LOG" 2>/dev/null | sed 's/^/ /'
else
echo " 暂无日志"
fi
else
echo " 日志目录不存在"
fi
#!/bin/bash
# stop_swarm.sh - 停止所有Agent并清理worktrees
# 用法: bash stop_swarm.sh [项目目录]
PROJECT_DIR="${1:-.}"
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
PROJECT_NAME="$(basename "$PROJECT_DIR")"
SESSION="swarm-${PROJECT_NAME}"
echo "=== 停止蜂群: $SESSION ==="
# ===== 停止tmux session =====
if tmux has-session -t "$SESSION" 2>/dev/null; then
tmux kill-session -t "$SESSION"
echo "tmux session 已停止"
else
echo "tmux session 不存在(可能已停止)"
fi
# ===== 合并agent分支的成果到main =====
echo ""
echo "检查agent分支的未合并提交..."
cd "$PROJECT_DIR"
MAIN_BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
for dir in "${PROJECT_DIR}"-agent-*; do
[ -d "$dir" ] || continue
AGENT_NUM=$(echo "$dir" | grep -o '[0-9]*$')
BRANCH_NAME="agent-${AGENT_NUM}-work"
# 检查分支是否有未合并的提交
AHEAD=$(git log "${MAIN_BRANCH}..${BRANCH_NAME}" --oneline 2>/dev/null | wc -l | tr -d ' ')
if [ "$AHEAD" -gt 0 ]; then
echo " Agent-${AGENT_NUM}: 有 ${AHEAD} 个未合并提交"
echo " 合并 ${BRANCH_NAME} → ${MAIN_BRANCH}..."
git merge "$BRANCH_NAME" --no-edit 2>/dev/null || {
echo " 警告: 合并冲突,请手动解决"
echo " 运行: git merge $BRANCH_NAME"
}
fi
done
# ===== 清理worktrees =====
echo ""
echo "清理 worktrees..."
for dir in "${PROJECT_DIR}"-agent-*; do
if [ -d "$dir" ]; then
AGENT_NUM=$(echo "$dir" | grep -o '[0-9]*$')
git -C "$PROJECT_DIR" worktree remove "$dir" --force 2>/dev/null || {
echo " 警告: 无法清理 $dir,尝试强制删除..."
rm -rf "$dir"
git -C "$PROJECT_DIR" worktree prune
}
echo " Agent-${AGENT_NUM}: worktree 已清理"
# 删除agent分支
BRANCH_NAME="agent-${AGENT_NUM}-work"
git -C "$PROJECT_DIR" branch -D "$BRANCH_NAME" 2>/dev/null || true
fi
done
git -C "$PROJECT_DIR" worktree prune 2>/dev/null
# ===== 清理lock文件 =====
echo ""
echo "清理 lock 文件..."
rm -f "$PROJECT_DIR/current_tasks/"*.lock 2>/dev/null
echo "lock 文件已清理"
echo ""
echo "=== 蜂群已完全停止 ==="
echo ""
echo "查看成果:"
echo " git -C $PROJECT_DIR log --oneline -20"
echo ""
echo "日志在: $PROJECT_DIR/agent_logs/"
Related skills
How it compares
Choose huashu-agent-swarm when you want decentralized git-based parallel agents instead of a single sequential Claude session or a centrally scripted pipeline.
FAQ
What does huashu-agent-swarm require before starting?
huashu-agent-swarm requires tmux (install via brew install tmux), an installed Claude CLI, and a project directory that is already a git repository. The setup script also expects a defined test command and coding standards from the developer.
How many agents does huashu-agent-swarm launch by default?
huashu-agent-swarm defaults to 8 parallel Claude agents, each running in its own tmux session. Developers can adjust agent count when describing the project, and each agent independently claims tasks from TASKS.md and pushes git commits.