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.
Files changed (65) hide show
  1. package/Makefile +8 -2
  2. package/README.md +57 -36
  3. package/VERSION +1 -1
  4. package/bin/board +112 -34
  5. package/bin/cnb +152 -65
  6. package/bin/dispatcher +25 -11
  7. package/bin/doctor +3 -5
  8. package/bin/init +13 -47
  9. package/bin/notify +224 -0
  10. package/bin/registry +8 -23
  11. package/bin/swarm +41 -860
  12. package/bin/sync-version +131 -0
  13. package/lib/board_admin.py +19 -9
  14. package/lib/board_bbs.py +23 -8
  15. package/lib/board_bug.py +2 -1
  16. package/lib/board_db.py +31 -141
  17. package/lib/board_lock.py +5 -1
  18. package/lib/board_mailbox.py +18 -8
  19. package/lib/board_maintenance.py +26 -27
  20. package/lib/board_msg.py +76 -39
  21. package/lib/board_pending.py +233 -0
  22. package/lib/board_pulse.py +14 -0
  23. package/lib/board_task.py +41 -32
  24. package/lib/board_tui.py +120 -0
  25. package/lib/board_view.py +70 -50
  26. package/lib/board_vote.py +9 -3
  27. package/lib/build_lock.py +7 -7
  28. package/lib/common.py +45 -3
  29. package/lib/concerns/__init__.py +7 -11
  30. package/lib/concerns/{coral_manager.py → coral.py} +54 -4
  31. package/lib/concerns/digest_scheduler.py +109 -0
  32. package/lib/concerns/file_watcher.py +73 -68
  33. package/lib/concerns/health.py +136 -0
  34. package/lib/concerns/helpers.py +1 -5
  35. package/lib/concerns/idle.py +130 -0
  36. package/lib/concerns/notification_push.py +171 -0
  37. package/lib/concerns/notifications.py +145 -0
  38. package/lib/concerns/nudge_coordinator.py +148 -0
  39. package/lib/digest.py +62 -0
  40. package/lib/health.py +2 -2
  41. package/lib/inject.py +2 -2
  42. package/lib/migrate.py +1 -0
  43. package/lib/monitor.py +9 -22
  44. package/lib/notification_config.py +101 -0
  45. package/lib/swarm.py +464 -0
  46. package/lib/swarm_backend.py +300 -0
  47. package/lib/theme_profiles.py +89 -0
  48. package/migrations/004_heartbeat.sql +1 -0
  49. package/migrations/005_notification_log.sql +12 -0
  50. package/migrations/006_pending_actions.sql +15 -0
  51. package/package.json +4 -3
  52. package/pyproject.toml +3 -2
  53. package/registry/README.md +9 -0
  54. package/registry/pubkeys.json +2 -1
  55. package/schema.sql +29 -1
  56. package/lib/concerns/bug_sla_checker.py +0 -32
  57. package/lib/concerns/coral_poker.py +0 -57
  58. package/lib/concerns/health_checker.py +0 -72
  59. package/lib/concerns/idle_detector.py +0 -56
  60. package/lib/concerns/idle_killer.py +0 -41
  61. package/lib/concerns/idle_nudger.py +0 -38
  62. package/lib/concerns/inbox_nudger.py +0 -34
  63. package/lib/concerns/resource_monitor.py +0 -47
  64. package/lib/concerns/session_keepalive.py +0 -23
  65. 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) exec "$CLAUDES_HOME/bin/board" overview ;;
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 pokemon 宝可梦主题\n"
30
- printf " cnb 5 pokemon 5位同学 + 宝可梦主题\n\n"
31
- printf " 主题: ai animal food lang music myth pokemon space\n\n"
32
- printf " cnb status 团队面板\n"
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
- # ---- Start session: parse [number] [theme] in any order ----
40
- _NUM=2
41
- _THEME_IDX=6 # default: AI 大佬
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 pokemon:7 space:3"
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
- if [[ "$arg" =~ ^[0-9]+$ ]]; then
47
- _NUM="$arg"
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
- printf "${Y}未知参数: ${arg}${N}\n" >&2
54
- echo "用法: cnb [数量] [主题]" >&2
55
- echo "主题: ai animal food lang music myth pokemon space" >&2
56
- exit 1
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
- fi
59
- done
60
-
61
- # ---- Theme data ----
62
- _THEMES=(
63
- "panda otter fox raccoon bunny koala sloth penguin hamster hedgehog duck seal capybara alpaca corgi shiba husky kitten ferret quokka"
64
- "python ruby rust go swift kotlin scala perl haskell erlang elixir clojure julia ocaml zig lua dart cobol lisp fortran"
65
- "mochi waffle taco ramen sushi bagel pretzel dumpling cookie brownie churro crepe noodle tofu falafel burrito pancake donut nacho boba"
66
- "nebula pulsar quasar comet aurora meteor nova orbit cosmos galaxy venus mars saturn pluto titan europa io ceres vega sirius"
67
- "dragon phoenix griffin hydra kraken sphinx unicorn pixie goblin imp djinn yeti nymph chimera basilisk manticore cerberus minotaur golem banshee"
68
- "jazz blues funk reggae disco punk grunge techno dubstep waltz tango salsa bossa swing opera gospel motown bebop ska hiphop"
69
- "altman dario ilya lecun karpathy hassabis sutskever hinton bengio fei-fei ng zuck bezos nadella pichai musk huang lisa-su amodei jack-clark"
70
- "pikachu charmander snorlax eevee jigglypuff mewtwo gengar squirtle bulbasaur vulpix psyduck togepi mudkip lucario gardevoir rayquaza mimikyu ditto zorua umbreon"
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/cs-*' .claude/.gitignore 2>/dev/null || echo 'commands/cs-*' >> .claude/.gitignore 2>/dev/null || true
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/cs-watch.md" <<EOF
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/cs-overview.md" <<EOF
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/cs-progress.md" <<EOF
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/cs-history.md" <<EOF
188
+ cat > "$CMD_DIR/cnb-history.md" <<EOF
108
189
  运行 \`${BOARD} --as ${ME} log 50\`,把完整的消息历史展示给我。
109
190
  EOF
110
- cat > "$CMD_DIR/cs-update.md" <<EOF
111
- 运行 \`pip install --upgrade cnb\`,更新到最新版本。把结果告诉我,如果更新成功提醒用户重启 cnb 以生效。
191
+ cat > "$CMD_DIR/cnb-update.md" <<EOF
192
+ 运行 \`pip install --upgrade claude-nb\`,更新到最新版本。把结果告诉我,如果更新成功提醒用户重启 cnb 以生效。
112
193
  EOF
113
- cat > "$CMD_DIR/cs-help.md" <<EOF
114
- 列出所有 /cs-* 命令:
115
- - /cs-overview — 团队总览:谁在干什么、谁卡了
116
- - /cs-watch <名字> — 看某个同学在干什么
117
- - /cs-progress — 最近进展汇总
118
- - /cs-history — 查看完整消息历史
119
- - /cs-update — 更新 cnb 到最新版
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
- - 定期 inbox 看同学的回复,汇总进展给用户。
147
- - 用户可以用 /cs-help 查看所有协作命令。
148
- - 同学之间可以互相 send 消息协作,不用什么都通过你。"
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
- IdleNudger — nudge idle sessions to continue working
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
- IdleNudger,
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
- inbox = InboxNudger(cfg)
108
+ nudge = NudgeCoordinator(cfg, idle)
96
109
  throttle = AdaptiveThrottle()
97
- file_watcher = FileWatcher(cfg, inbox)
110
+ file_watcher = FileWatcher(cfg, nudge)
98
111
 
99
- # Order matters: idle must tick before idle_killer / idle_nudger
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
- inbox,
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
- if not tmux_ok("has-session", "-t", cfg.coral_sess):
135
- log("Coral session gone. Shutting down.")
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
- sys.exit(1)
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
- sys.exit(1)
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
- f'if [ -n "$CLAUDE_SESSION_NAME" ]; then '
121
- f"unread=$({board} --as $CLAUDE_SESSION_NAME inbox 2>/dev/null "
122
- f"| grep -oE '[0-9]+ 条未读' | head -1); "
123
- f'[ -n "$unread" ] && echo "[inbox] $unread" || true; fi'
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", "") and "inbox" 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\n## @inbox\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', '2')")
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