claude-nb 0.3.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Makefile +8 -2
- package/README.md +57 -36
- package/VERSION +1 -1
- package/bin/board +112 -34
- package/bin/cnb +152 -65
- package/bin/dispatcher +25 -11
- package/bin/doctor +3 -5
- package/bin/init +13 -47
- package/bin/notify +224 -0
- package/bin/registry +8 -23
- package/bin/swarm +41 -860
- package/bin/sync-version +131 -0
- package/lib/board_admin.py +19 -9
- package/lib/board_bbs.py +23 -8
- package/lib/board_bug.py +2 -1
- package/lib/board_db.py +31 -141
- package/lib/board_lock.py +5 -1
- package/lib/board_mailbox.py +18 -8
- package/lib/board_maintenance.py +26 -27
- package/lib/board_msg.py +76 -39
- package/lib/board_pending.py +233 -0
- package/lib/board_pulse.py +14 -0
- package/lib/board_task.py +41 -32
- package/lib/board_tui.py +120 -0
- package/lib/board_view.py +70 -50
- package/lib/board_vote.py +9 -3
- package/lib/build_lock.py +7 -7
- package/lib/common.py +45 -3
- package/lib/concerns/__init__.py +7 -11
- package/lib/concerns/{coral_manager.py → coral.py} +54 -4
- package/lib/concerns/digest_scheduler.py +109 -0
- package/lib/concerns/file_watcher.py +73 -68
- package/lib/concerns/health.py +136 -0
- package/lib/concerns/helpers.py +1 -5
- package/lib/concerns/idle.py +130 -0
- package/lib/concerns/notification_push.py +171 -0
- package/lib/concerns/notifications.py +145 -0
- package/lib/concerns/nudge_coordinator.py +148 -0
- package/lib/digest.py +62 -0
- package/lib/health.py +2 -2
- package/lib/inject.py +2 -2
- package/lib/migrate.py +1 -0
- package/lib/monitor.py +9 -22
- package/lib/notification_config.py +101 -0
- package/lib/swarm.py +464 -0
- package/lib/swarm_backend.py +300 -0
- package/lib/theme_profiles.py +89 -0
- package/migrations/004_heartbeat.sql +1 -0
- package/migrations/005_notification_log.sql +12 -0
- package/migrations/006_pending_actions.sql +15 -0
- package/package.json +4 -3
- package/pyproject.toml +3 -2
- package/registry/README.md +9 -0
- package/registry/pubkeys.json +2 -1
- package/schema.sql +29 -1
- package/lib/concerns/bug_sla_checker.py +0 -32
- package/lib/concerns/coral_poker.py +0 -57
- package/lib/concerns/health_checker.py +0 -72
- package/lib/concerns/idle_detector.py +0 -56
- package/lib/concerns/idle_killer.py +0 -41
- package/lib/concerns/idle_nudger.py +0 -38
- package/lib/concerns/inbox_nudger.py +0 -34
- package/lib/concerns/resource_monitor.py +0 -47
- package/lib/concerns/session_keepalive.py +0 -23
- package/lib/concerns/time_announcer.py +0 -34
package/bin/cnb
CHANGED
|
@@ -10,113 +10,194 @@ else
|
|
|
10
10
|
B='' D='' G='' C='' Y='' N=''
|
|
11
11
|
fi
|
|
12
12
|
|
|
13
|
+
# ---- Export project root so all subprocesses can find .claudes/ ----
|
|
14
|
+
export CNB_PROJECT="${CNB_PROJECT:-$(pwd)}"
|
|
15
|
+
|
|
13
16
|
# ---- Subcommands (exact match, always first) ----
|
|
14
17
|
if [ $# -gt 0 ]; then
|
|
15
18
|
case "$1" in
|
|
16
19
|
init) shift; exec "$CLAUDES_HOME/bin/init" "$@" ;;
|
|
17
|
-
status)
|
|
20
|
+
status|ps) exec "$CLAUDES_HOME/bin/board" overview ;;
|
|
18
21
|
board) shift; exec "$CLAUDES_HOME/bin/board" "$@" ;;
|
|
19
22
|
swarm) shift; exec "$CLAUDES_HOME/bin/swarm" "$@" ;;
|
|
20
23
|
dispatcher) shift; exec "$CLAUDES_HOME/bin/dispatcher" "$@" ;;
|
|
21
24
|
watchdog) shift; exec "$CLAUDES_HOME/bin/dispatcher-watchdog" "$@" ;;
|
|
22
25
|
doctor) shift; exec "$CLAUDES_HOME/bin/doctor" "$@" ;;
|
|
26
|
+
logs)
|
|
27
|
+
shift
|
|
28
|
+
if [ $# -eq 0 ]; then
|
|
29
|
+
echo "用法: cnb logs <name>" >&2; exit 1
|
|
30
|
+
fi
|
|
31
|
+
exec "$CLAUDES_HOME/bin/board" --as "$1" log 50 ;;
|
|
32
|
+
stop)
|
|
33
|
+
shift
|
|
34
|
+
if [ $# -eq 0 ]; then
|
|
35
|
+
echo "用法: cnb stop <name>" >&2; exit 1
|
|
36
|
+
fi
|
|
37
|
+
exec "$CLAUDES_HOME/bin/swarm" stop "$@" ;;
|
|
38
|
+
exec)
|
|
39
|
+
shift
|
|
40
|
+
if [ $# -lt 2 ]; then
|
|
41
|
+
echo "用法: cnb exec <name> \"message\"" >&2; exit 1
|
|
42
|
+
fi
|
|
43
|
+
_target="$1"; shift
|
|
44
|
+
exec "$CLAUDES_HOME/bin/board" send "$_target" "$*" ;;
|
|
45
|
+
compose)
|
|
46
|
+
shift
|
|
47
|
+
_TEAM_FILE="${1:-cnb.toml}"
|
|
48
|
+
if [ ! -f "$_TEAM_FILE" ]; then
|
|
49
|
+
echo "错误: 找不到团队配置文件 '$_TEAM_FILE'" >&2
|
|
50
|
+
echo "用法: cnb compose [team-file.toml]" >&2
|
|
51
|
+
echo "" >&2
|
|
52
|
+
echo "示例 cnb.toml:" >&2
|
|
53
|
+
echo ' [session.alice]' >&2
|
|
54
|
+
echo ' persona = "Backend engineer"' >&2
|
|
55
|
+
echo ' [session.bob]' >&2
|
|
56
|
+
echo ' persona = "Frontend specialist"' >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
"$CLAUDES_HOME/bin/init" --team "$_TEAM_FILE"
|
|
60
|
+
_NAMES=$(python3 -c "
|
|
61
|
+
import tomllib, sys
|
|
62
|
+
data = tomllib.loads(open(sys.argv[1]).read())
|
|
63
|
+
names = list(data.get('session', {}).keys())
|
|
64
|
+
print(' '.join(names))
|
|
65
|
+
" "$_TEAM_FILE")
|
|
66
|
+
if [ -n "$_NAMES" ]; then
|
|
67
|
+
"$CLAUDES_HOME/bin/swarm" start $_NAMES >/dev/null 2>&1 || true
|
|
68
|
+
echo "OK 团队已启动: $_NAMES"
|
|
69
|
+
fi
|
|
70
|
+
exit 0 ;;
|
|
71
|
+
ui) shift; exec "$CLAUDES_HOME/bin/board" tui ;;
|
|
23
72
|
version|--version|-v) echo "cnb v${VERSION}"; exit 0 ;;
|
|
24
73
|
help|--help|-h)
|
|
25
74
|
printf "${B}${G}◆ cnb${N} ${D}v${VERSION}${N}\n"
|
|
26
75
|
printf " 多个 Claude Code 实例协作的团队开发工具\n\n"
|
|
27
76
|
printf " cnb 开始(默认2位同学,AI大佬主题)\n"
|
|
28
77
|
printf " cnb 5 5位同学\n"
|
|
29
|
-
printf " cnb
|
|
30
|
-
printf " cnb 5
|
|
31
|
-
printf "
|
|
32
|
-
printf "
|
|
78
|
+
printf " cnb threebody 三体主题\n"
|
|
79
|
+
printf " cnb 5 titan 5位同学 + 科技先锋主题\n"
|
|
80
|
+
printf " cnb compose [file] 从配置文件启动团队\n\n"
|
|
81
|
+
printf " 主题: ai animal food lang music myth space threebody titan\n\n"
|
|
82
|
+
printf " cnb ui 交互式团队面板\n"
|
|
83
|
+
printf " cnb ps 列出同学状态\n"
|
|
84
|
+
printf " cnb logs <name> 查看某个同学的消息历史\n"
|
|
85
|
+
printf " cnb exec <name> \"msg\" 给某个同学发指令\n"
|
|
86
|
+
printf " cnb stop <name> 停止某个同学\n"
|
|
87
|
+
printf " cnb doctor 健康检查\n\n"
|
|
88
|
+
printf " cnb board [...] 底层消息/任务命令\n"
|
|
33
89
|
printf " cnb swarm [...] 管理后台同学\n"
|
|
34
|
-
printf " cnb board [...] 消息/任务\n"
|
|
35
90
|
exit 0 ;;
|
|
36
91
|
esac
|
|
37
92
|
fi
|
|
38
93
|
|
|
39
|
-
# ----
|
|
40
|
-
|
|
41
|
-
|
|
94
|
+
# ---- Helper: start dispatcher if not already running ----
|
|
95
|
+
_start_dispatcher() {
|
|
96
|
+
local pidfile="$CNB_PROJECT/.claudes/dispatcher.pid"
|
|
97
|
+
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
|
|
98
|
+
return
|
|
99
|
+
fi
|
|
100
|
+
mkdir -p "$CNB_PROJECT/.claudes/logs"
|
|
101
|
+
"$CLAUDES_HOME/bin/dispatcher" >> "$CNB_PROJECT/.claudes/logs/dispatcher.log" 2>&1 &
|
|
102
|
+
echo $! > "$pidfile"
|
|
103
|
+
disown
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ---- Compose mode: reuse existing team from config.toml ----
|
|
107
|
+
if [ -f .claudes/config.toml ] && [ -f .claudes/board.db ]; then
|
|
108
|
+
_EXISTING=$(grep '^sessions' .claudes/config.toml | sed 's/sessions = \[//;s/\]//;s/"//g;s/,/ /g;s/ */ /g' | xargs)
|
|
109
|
+
if [ -n "$_EXISTING" ]; then
|
|
110
|
+
ME=$(echo "$_EXISTING" | cut -d' ' -f1)
|
|
111
|
+
WORKERS=$(echo "$_EXISTING" | cut -d' ' -f2-)
|
|
112
|
+
LABEL=$(grep '^prefix' .claudes/config.toml | cut -d'"' -f2)
|
|
113
|
+
_NUM=$(echo "$WORKERS" | wc -w | tr -d ' ')
|
|
114
|
+
"$CLAUDES_HOME/bin/swarm" start $WORKERS >/dev/null 2>&1 &
|
|
115
|
+
disown
|
|
116
|
+
_start_dispatcher
|
|
117
|
+
fi
|
|
118
|
+
else
|
|
119
|
+
# ---- Fresh start: parse [number] [theme] in any order ----
|
|
120
|
+
_NUM=2
|
|
121
|
+
_THEME_IDX=6 # default: AI 大佬
|
|
42
122
|
|
|
43
|
-
_THEME_MAP="ai:6 animal:0 food:2 lang:1 music:5 myth:4
|
|
123
|
+
_THEME_MAP="ai:6 animal:0 food:2 lang:1 music:5 myth:4 space:3 threebody:7 titan:8"
|
|
44
124
|
|
|
45
|
-
for arg in "$@"; do
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
else
|
|
49
|
-
_idx=$(echo "$_THEME_MAP" | tr ' ' '\n' | grep "^${arg}:" | cut -d: -f2)
|
|
50
|
-
if [ -n "$_idx" ]; then
|
|
51
|
-
_THEME_IDX="$_idx"
|
|
125
|
+
for arg in "$@"; do
|
|
126
|
+
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
|
127
|
+
_NUM="$arg"
|
|
52
128
|
else
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
129
|
+
_idx=$(echo "$_THEME_MAP" | tr ' ' '\n' | grep "^${arg}:" | cut -d: -f2 || true)
|
|
130
|
+
if [ -n "$_idx" ]; then
|
|
131
|
+
_THEME_IDX="$_idx"
|
|
132
|
+
else
|
|
133
|
+
printf "${Y}未知参数: ${arg}${N}\n" >&2
|
|
134
|
+
echo "用法: cnb [数量] [主题]" >&2
|
|
135
|
+
echo "主题: ai animal food lang music myth space threebody titan" >&2
|
|
136
|
+
exit 1
|
|
137
|
+
fi
|
|
57
138
|
fi
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
)
|
|
72
|
-
_LABELS=("小动物" "编程语言" "美食" "太空" "神话生物" "音乐风格" "AI 大佬" "
|
|
73
|
-
|
|
74
|
-
[ "$_NUM" -lt 1 ] && _NUM=1
|
|
75
|
-
[ "$_NUM" -gt 20 ] && _NUM=20
|
|
76
|
-
|
|
77
|
-
LABEL="${_LABELS[$_THEME_IDX]}"
|
|
78
|
-
ALL_NAMES=$(echo "${_THEMES[$_THEME_IDX]}" | tr ' ' '\n' | sort -R | head -n $((_NUM + 1)) | tr '\n' ' ' | sed 's/ $//')
|
|
79
|
-
ME=$(echo "$ALL_NAMES" | cut -d' ' -f1)
|
|
80
|
-
WORKERS=$(echo "$ALL_NAMES" | cut -d' ' -f2-)
|
|
81
|
-
|
|
82
|
-
# ---- Init + start ----
|
|
83
|
-
if [ ! -f .claudes/board.db ]; then
|
|
139
|
+
done
|
|
140
|
+
|
|
141
|
+
# ---- Theme data ----
|
|
142
|
+
_THEMES=(
|
|
143
|
+
"panda otter fox raccoon bunny koala sloth penguin hamster hedgehog duck seal capybara alpaca corgi shiba husky kitten ferret quokka"
|
|
144
|
+
"python ruby rust go swift kotlin scala perl haskell erlang elixir clojure julia ocaml zig lua dart cobol lisp fortran"
|
|
145
|
+
"mochi waffle taco ramen sushi bagel pretzel dumpling cookie brownie churro crepe noodle tofu falafel burrito pancake donut nacho boba"
|
|
146
|
+
"nebula pulsar quasar comet aurora meteor nova orbit cosmos galaxy venus mars saturn pluto titan europa io ceres vega sirius"
|
|
147
|
+
"dragon phoenix griffin hydra kraken sphinx unicorn pixie goblin imp djinn yeti nymph chimera basilisk manticore cerberus minotaur golem banshee"
|
|
148
|
+
"jazz blues funk reggae disco punk grunge techno dubstep waltz tango salsa bossa swing opera gospel motown bebop ska hiphop"
|
|
149
|
+
"altman dario ilya lecun karpathy hassabis vaswani hinton bengio fei-fei ng zuck bezos nadella pichai musk huang lisa-su radford jack-clark"
|
|
150
|
+
"luo-ji shi-qiang ye-wenjie cheng-xin zhang-beihai yun-tianming wang-miao ding-yi yang-dong zhuang-yan guan-yifan shen-yufei wei-cheng hines tyler wade evans rey-diaz tomoko singer"
|
|
151
|
+
"wang-xingxing ren-zhengfei zhang-yiming lei-jun li-yanhong li-kaifu liang-wenfeng yang-zhilin kaiming-he ma-huateng wang-jian lu-qi goodfellow schmidhuber dean chollet silver carmack torvalds wolfram"
|
|
152
|
+
)
|
|
153
|
+
_LABELS=("小动物" "编程语言" "美食" "太空" "神话生物" "音乐风格" "AI 大佬" "三体" "科技先锋")
|
|
154
|
+
|
|
155
|
+
[ "$_NUM" -lt 1 ] && _NUM=1
|
|
156
|
+
[ "$_NUM" -gt 20 ] && _NUM=20
|
|
157
|
+
|
|
158
|
+
LABEL="${_LABELS[$_THEME_IDX]}"
|
|
159
|
+
ALL_NAMES=$(echo "${_THEMES[$_THEME_IDX]}" | tr ' ' '\n' | sort -R | head -n $((_NUM + 1)) | tr '\n' ' ' | sed 's/ $//')
|
|
160
|
+
ME=$(echo "$ALL_NAMES" | cut -d' ' -f1)
|
|
161
|
+
WORKERS=$(echo "$ALL_NAMES" | cut -d' ' -f2-)
|
|
162
|
+
|
|
84
163
|
printf "${D}初始化 .claudes/ ...${N}\n"
|
|
85
164
|
"$CLAUDES_HOME/bin/init" $ALL_NAMES >/dev/null 2>&1
|
|
165
|
+
"$CLAUDES_HOME/bin/swarm" start $WORKERS >/dev/null 2>&1 &
|
|
166
|
+
disown
|
|
167
|
+
_start_dispatcher
|
|
86
168
|
fi
|
|
87
|
-
"$CLAUDES_HOME/bin/swarm" start $WORKERS >/dev/null 2>&1 || true
|
|
88
169
|
|
|
89
170
|
# ---- Slash commands (runtime, gitignored) ----
|
|
90
171
|
CMD_DIR=".claude/commands"
|
|
91
172
|
mkdir -p "$CMD_DIR"
|
|
92
|
-
grep -qx 'commands/
|
|
173
|
+
grep -qx 'commands/cnb-*' .claude/.gitignore 2>/dev/null || echo 'commands/cnb-*' >> .claude/.gitignore 2>/dev/null || true
|
|
93
174
|
BOARD="$CLAUDES_HOME/bin/board"
|
|
94
175
|
SWARM="$CLAUDES_HOME/bin/swarm"
|
|
95
176
|
_PREFIX=$(grep '^prefix' .claudes/config.toml 2>/dev/null | cut -d'"' -f2)
|
|
96
|
-
cat > "$CMD_DIR/
|
|
177
|
+
cat > "$CMD_DIR/cnb-watch.md" <<EOF
|
|
97
178
|
看某个同学在干什么。解析 \$ARGUMENTS 拿到名字。
|
|
98
179
|
运行 \`tmux capture-pane -t ${_PREFIX}-<名字> -p -S -30 2>/dev/null | tail -20\`,用简洁的话告诉用户这个同学在做什么、进展到哪了。
|
|
99
180
|
EOF
|
|
100
|
-
cat > "$CMD_DIR/
|
|
181
|
+
cat > "$CMD_DIR/cnb-overview.md" <<EOF
|
|
101
182
|
团队总览。对每个同学运行 \`tmux capture-pane -t ${_PREFIX}-<名字> -p -S -10 2>/dev/null | tail -5\`,汇总成一个简洁的表格告诉用户:谁在干什么、谁卡了、谁闲着。
|
|
102
183
|
同学列表:${WORKERS}
|
|
103
184
|
EOF
|
|
104
|
-
cat > "$CMD_DIR/
|
|
185
|
+
cat > "$CMD_DIR/cnb-progress.md" <<EOF
|
|
105
186
|
运行 \`${BOARD} --as ${ME} inbox\` 和 \`${BOARD} --as ${ME} view\`,汇总最近的进展:谁完成了什么、有什么新消息、当前整体进度如何。用简洁的话告诉用户。
|
|
106
187
|
EOF
|
|
107
|
-
cat > "$CMD_DIR/
|
|
188
|
+
cat > "$CMD_DIR/cnb-history.md" <<EOF
|
|
108
189
|
运行 \`${BOARD} --as ${ME} log 50\`,把完整的消息历史展示给我。
|
|
109
190
|
EOF
|
|
110
|
-
cat > "$CMD_DIR/
|
|
111
|
-
运行 \`pip install --upgrade
|
|
191
|
+
cat > "$CMD_DIR/cnb-update.md" <<EOF
|
|
192
|
+
运行 \`pip install --upgrade claude-nb\`,更新到最新版本。把结果告诉我,如果更新成功提醒用户重启 cnb 以生效。
|
|
112
193
|
EOF
|
|
113
|
-
cat > "$CMD_DIR/
|
|
114
|
-
列出所有 /
|
|
115
|
-
- /
|
|
116
|
-
- /
|
|
117
|
-
- /
|
|
118
|
-
- /
|
|
119
|
-
- /
|
|
194
|
+
cat > "$CMD_DIR/cnb-help.md" <<EOF
|
|
195
|
+
列出所有 /cnb-* 命令:
|
|
196
|
+
- /cnb-overview — 团队总览:谁在干什么、谁卡了
|
|
197
|
+
- /cnb-watch <名字> — 看某个同学在干什么
|
|
198
|
+
- /cnb-progress — 最近进展汇总
|
|
199
|
+
- /cnb-history — 查看完整消息历史
|
|
200
|
+
- /cnb-update — 更新 cnb 到最新版
|
|
120
201
|
EOF
|
|
121
202
|
|
|
122
203
|
# ---- Require interactive terminal ----
|
|
@@ -143,8 +224,14 @@ SYSPROMPT="你是 ${ME}。你的终端直接面对用户。
|
|
|
143
224
|
工作方式:
|
|
144
225
|
- 用户说想做什么,你拆任务分给同学,自己也干活。
|
|
145
226
|
- 不要让用户跑命令。你直接用上面的命令操作。
|
|
146
|
-
-
|
|
147
|
-
- 用户可以用 /
|
|
148
|
-
|
|
227
|
+
- 同学之间可以互相 send 消息协作,不用什么都通过你。
|
|
228
|
+
- 用户可以用 /cnb-help 查看所有协作命令。
|
|
229
|
+
|
|
230
|
+
重要 — 监控团队进展:
|
|
231
|
+
派任务给同学后,你必须主动跟进,不要派完就忘。使用 Monitor 工具挂一个后台轮询:
|
|
232
|
+
Monitor 命令示例(每 30 秒检查一次 inbox,有新消息就通知你):
|
|
233
|
+
last_count=0; while true; do out=\$(${BOARD} --as ${ME} inbox 2>&1); count=\$(echo \"\$out\" | grep -c '<message'); if [ \"\$count\" -gt \"\$last_count\" ]; then echo \"\$out\"; last_count=\$count; fi; sleep 30; done
|
|
234
|
+
设置 persistent: true,这样整个会话期间都会监控。
|
|
235
|
+
收到通知后汇总进展给用户。派完任务后应立刻启动监控。"
|
|
149
236
|
|
|
150
237
|
exec claude --name "${ME}" --append-system-prompt "$SYSPROMPT"
|
package/bin/dispatcher
CHANGED
|
@@ -6,8 +6,7 @@ Concerns (each with independent check intervals):
|
|
|
6
6
|
SessionKeepAlive — detect dead dev sessions
|
|
7
7
|
IdleDetector — batch screen snapshot comparison
|
|
8
8
|
IdleKiller — kill sessions idle >30min
|
|
9
|
-
|
|
10
|
-
InboxNudger — detect unread inboxes, nudge sessions
|
|
9
|
+
NudgeCoordinator — unified nudge orchestrator (inbox/queued/idle)
|
|
11
10
|
CoralPoker — periodic heartbeat to dispatcher session
|
|
12
11
|
HealthChecker — periodic full status report + team idle detection
|
|
13
12
|
BugSLAChecker — check overdue bugs
|
|
@@ -39,8 +38,7 @@ from lib.concerns import (
|
|
|
39
38
|
HealthChecker,
|
|
40
39
|
IdleDetector,
|
|
41
40
|
IdleKiller,
|
|
42
|
-
|
|
43
|
-
InboxNudger,
|
|
41
|
+
NudgeCoordinator,
|
|
44
42
|
ResourceMonitor,
|
|
45
43
|
SessionKeepAlive,
|
|
46
44
|
TimeAnnouncer,
|
|
@@ -86,25 +84,39 @@ if not Path(cfg.board_sh).exists():
|
|
|
86
84
|
# ---------------------------------------------------------------------------
|
|
87
85
|
|
|
88
86
|
|
|
87
|
+
def _acquire_pidlock() -> Path:
|
|
88
|
+
pidfile = cfg.claudes_dir / "dispatcher.pid"
|
|
89
|
+
if pidfile.exists():
|
|
90
|
+
try:
|
|
91
|
+
old_pid = int(pidfile.read_text().strip())
|
|
92
|
+
os.kill(old_pid, 0)
|
|
93
|
+
print(f"FATAL: dispatcher already running (pid {old_pid})", file=sys.stderr)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
96
|
+
pass
|
|
97
|
+
pidfile.write_text(str(os.getpid()))
|
|
98
|
+
return pidfile
|
|
99
|
+
|
|
100
|
+
|
|
89
101
|
def main() -> None:
|
|
102
|
+
pidfile = _acquire_pidlock()
|
|
90
103
|
base_interval = 2
|
|
91
104
|
|
|
92
105
|
coral = CoralManager(cfg)
|
|
93
106
|
idle = IdleDetector(cfg)
|
|
94
107
|
poker = CoralPoker(cfg)
|
|
95
|
-
|
|
108
|
+
nudge = NudgeCoordinator(cfg, idle)
|
|
96
109
|
throttle = AdaptiveThrottle()
|
|
97
|
-
file_watcher = FileWatcher(cfg,
|
|
110
|
+
file_watcher = FileWatcher(cfg, nudge)
|
|
98
111
|
|
|
99
|
-
# Order matters: idle must tick before idle_killer /
|
|
112
|
+
# Order matters: idle must tick before idle_killer / nudge_coordinator
|
|
100
113
|
concerns: list[Concern] = [
|
|
101
114
|
coral,
|
|
102
115
|
TimeAnnouncer(cfg),
|
|
103
116
|
idle,
|
|
104
117
|
SessionKeepAlive(cfg),
|
|
105
118
|
IdleKiller(cfg, idle, coral),
|
|
106
|
-
|
|
107
|
-
IdleNudger(cfg, idle),
|
|
119
|
+
nudge,
|
|
108
120
|
poker,
|
|
109
121
|
BugSLAChecker(cfg, poker),
|
|
110
122
|
HealthChecker(cfg, poker, coral),
|
|
@@ -131,8 +143,9 @@ def main() -> None:
|
|
|
131
143
|
while running:
|
|
132
144
|
now = int(time.time())
|
|
133
145
|
|
|
134
|
-
|
|
135
|
-
|
|
146
|
+
any_alive = any(tmux_ok("has-session", "-t", f"{cfg.prefix}-{s}") for s in cfg.dev_sessions)
|
|
147
|
+
if not any_alive:
|
|
148
|
+
log("No dev sessions alive. Shutting down.")
|
|
136
149
|
break
|
|
137
150
|
|
|
138
151
|
for c in concerns:
|
|
@@ -144,6 +157,7 @@ def main() -> None:
|
|
|
144
157
|
finally:
|
|
145
158
|
log("Shutting down...")
|
|
146
159
|
file_watcher.stop()
|
|
160
|
+
pidfile.unlink(missing_ok=True)
|
|
147
161
|
log("Stopped.")
|
|
148
162
|
|
|
149
163
|
|
package/bin/doctor
CHANGED
|
@@ -103,9 +103,7 @@ def check_foreign_keys(path: Path) -> bool:
|
|
|
103
103
|
try:
|
|
104
104
|
conn = sqlite3.connect(str(path))
|
|
105
105
|
# PRAGMA foreign_keys is connection-level; check table definitions instead
|
|
106
|
-
rows = conn.execute(
|
|
107
|
-
"SELECT sql FROM sqlite_master WHERE type='table' AND sql LIKE '%REFERENCES%'"
|
|
108
|
-
).fetchall()
|
|
106
|
+
rows = conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND sql LIKE '%REFERENCES%'").fetchall()
|
|
109
107
|
conn.close()
|
|
110
108
|
if rows:
|
|
111
109
|
_ok(f"Foreign keys defined ({len(rows)} tables)")
|
|
@@ -288,7 +286,7 @@ def main() -> None:
|
|
|
288
286
|
check_git()
|
|
289
287
|
check_claude_cli()
|
|
290
288
|
print("\nResult: project not initialized — run 'cnb init' first")
|
|
291
|
-
|
|
289
|
+
raise SystemExit(1)
|
|
292
290
|
|
|
293
291
|
print("=== cnb Doctor ===\n")
|
|
294
292
|
|
|
@@ -321,7 +319,7 @@ def main() -> None:
|
|
|
321
319
|
print("✓ All checks passed.")
|
|
322
320
|
else:
|
|
323
321
|
print("⚠ Some checks failed — review warnings above.")
|
|
324
|
-
|
|
322
|
+
raise SystemExit(1)
|
|
325
323
|
|
|
326
324
|
|
|
327
325
|
if __name__ == "__main__":
|
package/bin/init
CHANGED
|
@@ -115,12 +115,11 @@ def _update_claude_md(project_dir: Path, snippet: str) -> None:
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
def _hook_command(claudes_home: Path) -> str:
|
|
118
|
-
board = f"{claudes_home}/bin/board"
|
|
119
118
|
return (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
'if [ -n "$CLAUDE_SESSION_NAME" ] && [ -n "$CNB_PROJECT" ]; then '
|
|
120
|
+
"BOARD=$(grep '^claudes_home' \"$CNB_PROJECT/.claudes/config.toml\" 2>/dev/null "
|
|
121
|
+
"| cut -d'\"' -f2)/bin/board; "
|
|
122
|
+
'[ -x "$BOARD" ] && $BOARD --as $CLAUDE_SESSION_NAME pulse 2>/dev/null; fi'
|
|
124
123
|
)
|
|
125
124
|
|
|
126
125
|
|
|
@@ -148,7 +147,8 @@ def _update_settings(project_dir: Path, claudes_home: Path) -> None:
|
|
|
148
147
|
already = any(
|
|
149
148
|
isinstance(h, dict)
|
|
150
149
|
and any(
|
|
151
|
-
"board" in sub.get("command", "")
|
|
150
|
+
"board" in sub.get("command", "")
|
|
151
|
+
and ("pulse" in sub.get("command", "") or "inbox" in sub.get("command", ""))
|
|
152
152
|
for sub in h.get("hooks", [])
|
|
153
153
|
if isinstance(sub, dict)
|
|
154
154
|
)
|
|
@@ -160,44 +160,6 @@ def _update_settings(project_dir: Path, claudes_home: Path) -> None:
|
|
|
160
160
|
settings_path.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n")
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
def _create_slash_commands(project_dir: Path, claudes_home: Path) -> None:
|
|
164
|
-
cmd_dir = project_dir / ".claude" / "commands"
|
|
165
|
-
cmd_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
-
|
|
167
|
-
board = f"{claudes_home}/bin/board"
|
|
168
|
-
swarm = f"{claudes_home}/bin/swarm"
|
|
169
|
-
|
|
170
|
-
commands = {
|
|
171
|
-
"cs-team": f"运行 `{swarm} status` 和 `{board} --as lead view`,用简洁的表格告诉我每个同学的状态。",
|
|
172
|
-
"cs-inbox": f"运行 `{board} --as lead inbox`,把收到的消息汇总给我。",
|
|
173
|
-
"cs-broadcast": f'把以下消息广播给所有同学:\n`{board} --as lead send all "$ARGUMENTS"`',
|
|
174
|
-
"cs-assign": f'把任务分配给指定同学。参数格式:<名字> <任务描述>\n解析 $ARGUMENTS,运行 `{board} --as lead send <名字> "<任务描述>"`',
|
|
175
|
-
"cs-kick": f"让指定同学下线。解析 $ARGUMENTS 拿到名字,运行 `{swarm} stop $ARGUMENTS`",
|
|
176
|
-
"cs-add": f"拉新同学上线。解析 $ARGUMENTS 拿到名字,运行 `{swarm} start $ARGUMENTS`",
|
|
177
|
-
"cs-log": f"查看指定同学最近的工作日志。解析 $ARGUMENTS 拿到名字,运行 `{board} --as lead view` 并重点关注该同学的状态和最近消息。",
|
|
178
|
-
"cs-stop": f"停掉所有后台同学。运行 `{swarm} stop`,告诉用户已全部下线。",
|
|
179
|
-
"cs-bugs": f"运行 `{board} --as lead bug list`,汇总当前所有 bug。",
|
|
180
|
-
"cs-history": f"运行 `{board} --as lead log 50`,把完整的消息历史展示给我。",
|
|
181
|
-
"cs-update": "运行 `pip install --upgrade cnb`,更新到最新版本。把结果告诉我,如果更新成功提醒用户重启 cnb 以生效。",
|
|
182
|
-
"cs-help": "列出所有 /cs-* 命令及用途:\n"
|
|
183
|
-
"- /cs-team — 看同学状态\n"
|
|
184
|
-
"- /cs-inbox — 查收消息\n"
|
|
185
|
-
"- /cs-broadcast <消息> — 广播\n"
|
|
186
|
-
"- /cs-assign <名字> <任务> — 派任务\n"
|
|
187
|
-
"- /cs-add <名字> — 拉同学上线\n"
|
|
188
|
-
"- /cs-kick <名字> — 让同学下线\n"
|
|
189
|
-
"- /cs-log <名字> — 看同学工作日志\n"
|
|
190
|
-
"- /cs-stop — 全部下线\n"
|
|
191
|
-
"- /cs-bugs — 看 bug 列表\n"
|
|
192
|
-
"- /cs-history — 查看完整消息历史\n"
|
|
193
|
-
"- /cs-update — 更新到最新版\n"
|
|
194
|
-
"- /cs-help — 本帮助",
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
for name, content in commands.items():
|
|
198
|
-
(cmd_dir / f"{name}.md").write_text(content + "\n")
|
|
199
|
-
|
|
200
|
-
|
|
201
163
|
DEFAULT_SESSIONS = ["s1", "s2", "s3"]
|
|
202
164
|
|
|
203
165
|
|
|
@@ -286,11 +248,11 @@ def main() -> None:
|
|
|
286
248
|
persona = personas.get(n, "")
|
|
287
249
|
conn.execute("INSERT OR IGNORE INTO sessions(name, persona) VALUES (?, ?)", (n, persona))
|
|
288
250
|
persona_section = f"\n## Persona\n{persona}\n" if persona else ""
|
|
289
|
-
md_content = f"# {n}\n{persona_section}\n## Current task\n(none)\n
|
|
251
|
+
md_content = f"# {n}\n{persona_section}\n## Current task\n(none)\n"
|
|
290
252
|
(claudes_dir / "sessions" / f"{n}.md").write_text(md_content)
|
|
291
253
|
conn.execute("INSERT OR IGNORE INTO meta(key, value) VALUES ('dispatcher_session', 'dispatcher')")
|
|
292
254
|
# Record schema version so migration runner knows where we are
|
|
293
|
-
conn.execute("INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '
|
|
255
|
+
conn.execute("INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', '4')")
|
|
294
256
|
conn.commit()
|
|
295
257
|
conn.close()
|
|
296
258
|
|
|
@@ -302,12 +264,16 @@ def main() -> None:
|
|
|
302
264
|
print(f"Applied {applied} schema migration(s).")
|
|
303
265
|
|
|
304
266
|
# .gitignore for generated stuff
|
|
305
|
-
gitignore_content = "board.db\nboard.db-shm\nboard.db-wal\nlogs/\n"
|
|
267
|
+
gitignore_content = "board.db\nboard.db-shm\nboard.db-wal\nlogs/\ndispatcher.pid\n"
|
|
306
268
|
(claudes_dir / ".gitignore").write_text(gitignore_content)
|
|
307
269
|
|
|
308
270
|
# Update .claude/settings.json (merge hooks if exists, create if not)
|
|
309
271
|
_update_settings(project_dir, claudes_home)
|
|
310
272
|
|
|
273
|
+
# Write multi-agent coordination section into CLAUDE.md
|
|
274
|
+
snippet = _claude_md_snippet(sessions, claudes_home, personas)
|
|
275
|
+
_update_claude_md(project_dir, snippet)
|
|
276
|
+
|
|
311
277
|
sessions_str = " ".join(sessions)
|
|
312
278
|
print(f"Initialized cnb (sessions: {sessions_str})")
|
|
313
279
|
|