26 Commits
Author SHA1 Message Date
wuguanganandClaude Opus 5 b3c40abf5a chore: 版本号 2.6.0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:47:30 +08:00
wuguanganandClaude Opus 5 e3b9153b66 feat: 拦住 AI 把密钥搬出去;README 补 AI 时代的隐私那一节
## 密钥泄漏闸

把活交给 agent 之后,泄密的形态变了。以前是人手滑,现在是**每一步都很合理的
一串操作**:git add . 顺手把 .env 提了、把真实 token 内联进配置文件当"示例"、
为了确认配置 cat 一下密钥(值就进了 transcript,0644、永久、每轮重发给 API)。

**没有一条能靠「这条命令危不危险」判断出来。** 它们都不危险。唯一可靠的信号是
这次调用的内容里有没有出现真实的密钥值。

所以加了一道闸,默认开:参数里出现库里的真实值 → 拒;git add/commit 会把
.env 一类文件或含真实值的文件带进仓库 → 拒。拒绝理由里写明正确做法
(用 $KEY、先加 .gitignore),否则模型只会换个写法再试一次。

**它跑在 hubctl 本地,不依赖 app 在不在跑。** 仓库其它拦截都是「app 没开就放行」
(一个监控工具不该让你的命令跑不起来),这一条反过来 —— 密钥漏出去不可逆。

几条守住的边界,判宽了比判窄了危险:值短于 8 字符不参与匹配(abc/true/test
会命中一切);.example/.template/.sample 结尾放行(模板文件本来就该提交,
误拦一次用户就把整个功能关掉了);只认 EnvRenderer 写出来的格式,不做通用
dotenv 解析;不是 git add/commit 就一个子进程都不起。

拒了之后事件照发给 app(不等应答),进审批日志 —— 一道看不见的闸,
用户第一次被拦时只会觉得 Claude 抽风。开关在密钥页底部,
落成 ~/.vibe-foreman/guard.json,且**只在关掉时才写**:
默认开,「文件不存在 = 开着」,这样没打开过密钥页的人也受保护。

明确写了它挡不住什么:python -c 读、软链、base64 转一道、库外手写的值。
它是一道减少手滑的闸,不是沙箱。

## 截图

主窗口现在也能自动截了:HUB_WINDOW_SECTION 钉住打开哪一页,
配 DemoFixtures.seed 造的假密钥和假账号(刻意编得一眼假 —— 一个像真的
token 混进去,读者会去试)。

两个坑:find-window 按面积取第一个会截到岛(它比主窗口先出现一瞬),
改成按宽度 ≥900 筛;.accessory 的进程从后台 shell 起来抢不到焦点,
而非活跃窗口里的系统控件会被去饱和 —— 截出来开关全是灰的,
正好把要展示的东西说反了,所以截图模式下改用 .regular。

顺带:进密钥页自动选中第一组。有组却什么都不选,右边一大片空白,
那是"还没建组"的样子而不是"有四组等你挑"的样子。

646 个测试全过(原 625)。真 hubctl 二进制端到端验过四种情况:
写入真值被拒、约定的 source 用法放行、git add . 带 .env 被拒、普通命令放行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 10:46:46 +08:00
wuguanganandClaude Opus 5 11786954cf docs: 功能长图 + 全部截图用演示数据重截
## 长图

docs/poster.png:六段式,一张图讲完这个工具是什么。
生成脚本 Resources/make-poster.swift,改文案或调顺序改那个数组即可。

修了个坑:NSImage.size 返回的是**点**不是像素,这批截图是 2x 抓的,
1880px 的图它报 940,于是大图被当小图处理,第一版只占了半幅。

## 截图

scripts/shoot-screenshots.sh:用 HUB_DEMO=1 + HUB_ISLAND_STATE 把形态钉死,
自动截 + 自动裁掉透明留白,五种形态全自动。

之前那批是改名前截的,岛头还写着旧名。重截之后顺带修好三件事:
- 演示工作区路径 claude-hub-demo → vibe-foreman-demo
- 新图带上了「在问你」横幅和状态徽章,比旧图更能说明问题
- 删掉 04-main-window.png:验收页本来就带主窗口侧边栏,一张顶两张,
  而且那张的标题栏是旧名

两个配套小工具:
- scripts/find-window.swift  拿窗口 id 给 screencapture -l 用。
  CGWindowList 报的是显示名不是可执行名,写死 ClaudeHub 一个都找不到。
- scripts/crop-alpha.swift   裁到不透明内容的外接矩形。岛画在 640×720 的
  透明容器窗口里,不裁的话拼进长图就是一大块空白。

截图脚本踩了 NOTES.md 里记过的那个 bash 坑:变量后面紧跟中文标点
(`$state)`)会被算进变量名,set -u 下直接报未定义。加花括号。

窗口探测改成轮询而不是固定 sleep —— 实测 4 秒对 hover 够、对 expanded 不够。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 00:04:25 +08:00
wuguanganandClaude Opus 5 bae4f9af86 feat: 零配置装 hook;改名 Vibe Foreman Free
## 零配置

装法从三步变两步:拖进应用程序 → 打开。中间那步「双击安装 hook.command」
没有任何用户能做的决定,漏点它的后果却是**静默的** —— app 开着、
界面正常、一条事件都收不到,看起来像"装了但没用"。

HookInstaller 在启动时自己装。四条边界,都有测试钉着:

- 只碰自己的条目:只删命令里含 `hubctl hook` / `hub-hook-` 的(我们的旧版),
  用户手写的 hook 和 permissions / env 等其它键一律原样保留
- 幂等:内容没变一个字节都不写。`.sortedKeys` 是这条的前提 ——
  不排序时 JSON 键序跟着哈希走,"没变就不写"的判断会随机失效
- 坏 JSON 不覆盖:用户可能只是手改时漏了个逗号,覆盖等于删了他的配置
- 失败不阻断启动:hook 没装好的 app 仍能显示会话,起不来的 app 什么都不是

hubctl 每次比内容再决定要不要拷 —— 升级 app 后 hook 还指着旧二进制的话,
协议一变整条链路静默失效。设置页显示状态 + 一键修复。

dmg 里那个 command 文件改名成「手工修复 hook(通常用不到)」,留作兜底。

实测:本机 settings.json 装前装后逐键对比,顶层键无增无减、
permissions 等非 hooks 值全部原样、9 类 hook 各 1 条无重复;
第二次启动 sha256 未变。

## 改名

显示名 Claude Hub → Vibe Foreman Free。

**bundle id、签名证书名、可执行文件名 ClaudeHub 全部不动** ——
换任何一个都会让已授予的 TCC 权限(控制 iTerm 的 Automation、通知)失效,
而 `pgrep -x ClaudeHub` 那条判活约束也依赖可执行名(见 NOTES.md)。
安装时清掉旧名字那份,避免 /Applications 里同时躺着两个共用 bundle id 的 app。

530 个测试全过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 22:27:28 +08:00
wuguanganandClaude Opus 5 c6dccca3d4 chore: 2.4.0 —— 补齐截图,署名 Artux AI
- LICENSE 版权人:Artux AI
- 补上分发仓库独有的 4 张截图(nudge / answer / stall-detail / ticker),
  逐张确认过是 DemoFixtures 合成数据
- 版本 2.3.0 → 2.4.0:上游 v2.3.0 已发布过,今天这批改动(iTerm 检测、
  会话回收与接回、右键菜单、失效项目清理)不能再挂同一个号

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:48:17 +08:00
wuguanganandClaude Opus 5 4c16d7826e docs: 追问清单放仓库外
清单会写满「这个项目该做什么、卡在哪、下一步是什么」——那是项目内情,
不是工具的一部分。放进仓库就等于跟着工具一起发出去了,
这次开源审计里最严重的一条就是这么来的。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:33:24 +08:00
wuguanganandClaude Opus 5 b435a05efa fix: 盯梢误判「在干活」,还会拼到用户没发完的话后面
两个都是实机撞出来的,任何一个都会让盯梢失效或者帮倒忙。

## shell 状态不等于在干活

状态文件里的 shell 表示"挂着一个后台 shell",和 Claude 自己在不在思考无关。
实测它空在提示符上等输入时,状态照样是 shell —— 而脚本把 shell 当成在干活,
于是它停了 20 分钟一次都没催到。用户先发现的,不是脚本。

改成只有 busy 算在干活,另外看屏幕上有没有转圈计时
("✻ Boondoggling… (17s · ↓ 201 tokens)")作为必要补充。
这里用屏幕是有理由的:状态文件在这一点上信息不足,两个信号合起来才准。

## 用户正在打字时不能发

send-keys 是往当前输入位置敲字。用户在输入框里打了一半时,追问会直接拼到
他那句后面 —— 实测发生过,把用户的话和追问接成了一句。轻则语义变了,
重则改变他本来要说的意思。发之前先看输入框有没有草稿,有就跳过这轮。

## 追问清单重排

前四条全部改成关于「落地」。用户实测下来最大的问题不是它偷懒,是**它在写报告**:
训练跑了、报告漂亮,但产出没进代码、没进发布包,下一次发布就冲没了。
先问落到哪些文件、发布后还在不在、有没有落到 agents/ 下、
是在训练还是在写关于训练的报告;再问深度。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 02:41:44 +08:00
wuguanganandClaude Opus 5 501334cb65 fix: 老实回答「没做」的要点会被永远重复问;盯梢升级成追问
## 诚实反而被惩罚

答 done:false 的项保持 .open,而 .open 正是注入的筛选条件 ——
于是每一轮问的都是同一批 6 条,后面的要点一次都轮不到。
实测在我自己身上撞到:连着两轮被拦,问的是一模一样的 6 条。

加 lastAskedAt / askCount,按「最久没问过」轮换。问过 3 次还说没做的
标成「疑似误拆」并停止再问 —— 多半不是它偷懒,是这条拆偏了。
真实例子:用户说「加个人工确认吧 不然都清理不掉了」(意思是要一个手动
确认的入口),被拆成了「清理操作前增加确认步骤」和「取消确认时中止清理」
两条根本不存在的需求。

「疑似误拆」必须在界面上标出来:它已经被排除出注入了,不标的话就是
安静地消失,用户永远不知道有一条要点再也不会被问。

## 盯梢:从「说继续」升级成「追问」

「继续」只能防它停,防不了它做浅。它完全可以每次都继续、每次都做表面功夫,
最后给一份看起来很完整的报告。

scripts/keep-going.sh 改成读追问清单轮着问;
scripts/probes/agentx-training.txt 是 agentx 训练的 11 条:
训练包逐条对照、AI 浏览器实际验证、定时任务、handoff 覆盖、
测了几轮调了什么参数、卡点落点、有没有落成 skills、有没有新增子员工、
上下文被压缩后有没有遗漏、拿给真实用户最可能在哪翻车。

413 个测试。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 02:36:31 +08:00
wuguanganandClaude Opus 5 fe3febbfd2 feat: Stop 拦截注入 —— 收工时拿清单逼它逐条报证据
Stop 从「非阻塞、输出被忽略」改成阻塞式,有待验收项时输出
{"decision":"block","reason":<清单>} 让 Claude 不停下来,先逐条核对。
下一轮的 lastAssistantMessage 里的自查 JSON 被收回来,把 open 推到 claimed。

Stop 的超时阶梯和审批那条**完全分开**,方向也相反:

  3s HookCoordinator / 5s hubctl / 10s settings.json,超时一律 = 不拦。

审批超时 = 拒绝(安全刹车);这里超时 = 放行。写反的话 Hub 一出问题
所有会话就再也收不了工,比"少提醒一次验收"严重得多。实测 hook 本身
6-11ms,Hub 在不在都一样。

查出来三个真问题,都不是靠读代码发现的:

1. 服务端从不给 stop 回写应答。我改了 hubctl 让它等,却没动
   HubSocketServer 里写死的 guard kind == .preToolUse —— 拦截一次都没生效
   过,而且不报错、不超时、10ms 就返回,表现得和"清单里没有待办"
   一模一样。端到端测了三轮才找到。修法不是补一个 if,是加
   Kind.expectsDecision 让两边共用同一个判断。

   这和上次「grep activeScreen() 漏了一个调用点」是同一类错误:
   改了协议的一端,假设另一端会跟上。

2. SIGPIPE 会杀掉 Hub。服务端开始给 stop 回写之后,遇到不等应答就关连接
   的客户端(旧版 hubctl、被 Ctrl-C 的 hook),那次 write 直接终止进程 ——
   不是报错,是进程没了。测试崩在 signal 13 上才暴露。
   先加了 per-socket 的 SO_NOSIGPIPE(看起来更讲究),结果没生效,
   照样崩;改成进程级 signal(SIGPIPE, SIG_IGN) 才好。
   更讲究的写法如果不生效,就只是更讲究的 bug。

3. stop 事件不能去重。缓存决策只有在决策是事件的纯函数时才成立,
   而 stop 的结论取决于会变的上膛状态:同样内容的两条 stop,
   一条在用户说话前、一条在之后,结论截然相反。实测被这个坑到过。

4. {"done":1} 被判成做完了。JSONSerialization 把数字 1 桥接成 NSNumber,
   as? Bool 对它成立 —— 模型只要把 done 写成数字就能绕过"含糊按没做算"。
   改用 CFBooleanGetTypeID 严格判定。

端到端实测(真实 hubctl 二进制):未上膛不拦、上膛后拦下 13 条要点、
连着两次收工都放行、Hub 存活、自查回答(带散文和围栏)正确回收成
claimed 且 hasProof 仍为 false。

346 个测试。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 18:10:35 +08:00
wuguanganandClaude Opus 5 f0f6cd7454 feat: 验收守望 —— 独立于 Claude 自述的需求清单
Claude 会把用户需求翻译成自己的 TodoWrite,翻译那一步就已经丢东西了:
「修复 apple-header sticky」是施工步骤,不是原始需求里的功能要点。
再拿它自己的 todo 去验收,等于让它自己判自己的卷子。

清单挂在项目上跨会话累积,基线取自用户原话和用户批准过的计划。

几个刻意的设计:

- Evidence 在类型层面就分开 claimed / diff / ran。全塞进 [String] 等于在
  数据结构上承认「它说改了」和「它真改了」是一回事,而这正是要解决的问题。
  汇报条的「已证」只数 isProof 的,想把自述渲染成已验证得先改类型。

- 拦截的「上膛」只有 UserPromptSubmit 能做,Stop 拦过立即卸膛。Claude
  没有任何路径能给自己上膛,所以「拦一次 → 它回一轮 → 又拦」这个死循环
  是构造不出来的,不是被 if 挡住的。

- applyAudit 跳过用户终裁过的项。勾了「接受」一转身被机器打回 disputed,
  那个勾就白点了。

- extract 区分「失败」(nil) 和「没有新要点」([])。混在一起的话模型抽风
  一次就会清掉原话缓冲,而那是这个功能唯一的基线来源。

hook 从 5 类补到 9 类。PostToolUse 是最大的收获:Write/Edit 之后 Hub
第一手知道哪个文件被改了,不用回头问 Claude——问这个问题本身就给了
转述失真的机会。

实测确认(都不是推断):
- hubctl hook userprompt 的 stdout 是 0 字节。这条必须验,因为
  UserPromptSubmit 的 stdout 会被当成附加 context 注入进对话。
- 项目级和全局 hook:命令字符串相同时 Claude Code 自己去重,不同才叠加。
  第一次实验用了两条不同命令,据此普遍化成「一定会重复」是错的。
  HookDedup 保留为深度防御(路径不一致时才真用得上),不是必需品。
- 真实拆解链路 20 秒出结果,正确忽略了缓冲里的闲聊、正确去重了已有条目。

ensure-project-config.sh 接了三个调用点。其中 TerminalDispatch.open() 是
关键:app 从岛/主窗口启动项目完全不经过 project-menu.sh,漏了那里等于
最常用的入口一个都没检查。

326 个测试。上膛、自述不算证明、外部编辑不被覆盖三条做过变异验证。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:50:25 +08:00
wuguangan aa9330afed chore: 版本号 2.3.0 2026-08-06 17:20:16 +08:00
wuguanganandClaude Fable 5 288e4844f4 fix: 防污染模板去掉无效的 Write deny 规则
Edit(path) 已经覆盖所有文件编辑工具(含 Write),而单独的 Write(path)
规则不被权限检查识别 —— 加了不起作用,还会在每次调用 claude 时刷警告:
「Write(...) is not matched by file permission checks — only Edit(path) rules are」

防护本身一直是生效的(靠 Edit 那组),这次只是去掉噪音并在注释里写明原因,
防止以后有人看着「只禁了 Edit 没禁 Write」又给加回去。

已同步修正三个已生成项目的 settings.json。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 22:31:26 +08:00
078f6f5e46 feat: 岛上作答交互卡 + 项目置顶/排序 + 建仓与克隆 (#6)
## 岛上能直接作答 Claude 的交互请求

AskUserQuestion / ExitPlanMode 的 tool_input 里没有 command/path,
RiskClassifier 提不出 summary,风险判定为 normal 直接放行 ——
岛上从来不弹任何东西。这是这次要修的核心 bug。

现在按 toolName 在风险判定**之前**分流,岛上弹可作答的卡:

- 选择题:单题单选点选即答;多题表单逐题作答,多选勾完提交,
  最后合成一条回传。答案借 deny 的 reason 送回模型(PreToolUse
  没有「替用户作答」的通道,这是唯一的口子)。
- 计划审批:显式 allow 压不掉 plan 状态机自己的确认框(实测
  2.1.220),所以「批准」= 放行后替用户在终端框里按 1/2,带重试
  等对话框渲染出来;驳回走 hook deny 有效。
- 进入计划模式(EnterPlanMode)同样接管。
- 终端权限框("Do you want to...")走 Notification hook 弹卡,
  允许=注入 1、拒绝=Esc,复用 TerminalReply 的全套护栏。

超时方向和审批卡**相反**:审批超时必须 deny(唯一的安全刹车),
交互卡超时 allow(不输出决策 = 交还终端原生对话框,用户零损失)。
这条镜像关系有专门的测试钉住。

## 项目列表

- 置顶:岛和主窗口菜单里都能置顶,落盘 pinned.json。
- 排序统一成 置顶 > 正在开发 > 最近提交 > 名字。GitInfo 加
  lastCommitAt(38 个仓库合计约 300ms,后台队列)—— 三十多个项目
  按 yaml 顺序排,找一个要翻半屏。
- 岛上无会话的行点击弹启动菜单,不再直接起 claude(误触成本太高)。
  用原生 NSMenu:SwiftUI Menu 的两种整行方案都翻过车(label 被
  压扁 / 透明盖层收不到点击)。

## 建仓与克隆

- scripts/new-project.sh:目录 + git + GitHub 远程 + 防污染
  settings.json + 注册 projects.yaml,一条命令。app、/new-project
  skill、命令行三个入口共用同一份逻辑。
- scripts/clone-project.sh + 克隆表单:列出账号可见的全部仓库
  (含组织仓,之前 gh repo list 只列个人仓),按 owner 分组筛选,
  多账号可切换(gh 只用活跃账号的凭据,不切就拉不了另一个账号的
  私有仓)。列表按账号分开缓存 + 后台静默刷新。
- 防污染 deny 刻意**不禁** ~/.claude/plans/:plan mode 的计划文件
  写在那里,禁了所有 plan 会话都会报 Error writing file(真踩过)。

## 顺带修掉

add-project.sh 最后一行是 `[ ${#ALIASES[@]} -gt 0 ] && echo ...`,
没有别名时测试为假,整个脚本退出码变成 1 —— 明明写进去了,调用方
却报「注册 projects.yaml 失败」。改成显式 exit 0。

Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 17:19:39 +08:00
ef76328ec8 滞留守望:从「事件通知」升级成「状态守望」 (#5)
* feat: 演示模式(合成会话 + 真 git 工作区)用于脱敏截图

真实数据里全是内部项目名、分支名和 commit 标题,不能对外;而且要等真实
会话凑出"1 个在等你 + 2 个在忙 + 1 个跑命令 + 几个闲着"这种展示效果好的
组合纯靠运气。合成数据同时解决不泄露和每次截图完全一致两件事。

实现上往临时目录写 sessions/<PID>.json 再让 reader 指过去,而不是在 store
里塞假数据 —— 这样走的是和生产完全一样的解析路径,截出来的图能反映真实
渲染结果。git 工作区也建真仓库(真分支、真提交、真未提交改动),理由相同。

ClaudeSessionReader 因此需要 requiresLiveProcess 开关:合成会话的进程当然
不存在,kill(pid,0) 会把它们全过滤掉。生产必须保持 true。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(hubctl): 加 HUB_JUDGE 守卫,封死 AI 判定的 hook 回环

Hub 接下来要自己起 claude -p 给滞留的会话生成摘要。那个子进程会照常
触发 Stop/SessionEnd hook,于是 hook → Hub → claude -p → hook 形成
无限回环,每判定一次多生一个会话。

守卫见到 HUB_JUDGE=1 就在发 socket 之前 exit(0)。这道防线不依赖 Claude
CLI 的任何行为开关,是唯一保证有效的。

顺带加 HUB_TRACE:指向文件路径时每次执行追加一行。这是本机唯一能把
"Hub 起的那个 claude"和"用户另外八个会话"区分开的探针 —— 环境变量会被
子进程继承,别人的会话没有它。之前试过用 hubctl 的 atime 做间接探测,
全是其他会话的噪音,毫无结论。

实测(trace 文件为证):
  claude -p --setting-sources ''         → 0 行,hook 根本没被调起
  claude -p(无开关)+ HUB_JUDGE=1        → 2 行 stop/sessionend,均 judge=true
两道防线各自独立有效,实现时两条都上。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(probe): 滞留检测的确定性探针与五类判定

岛现在只在事件发生那一瞬间提醒(闯入 2 秒然后回落成一个静止徽章),
之后再也不喊。用户给的五张实拍截图都是"那一瞬没看到就永远错过"。

这一层负责回答"哪个会话卡住了、为什么",纯逻辑、不碰 UI。

TranscriptTail —— 只读文件尾部 64 KB。这些 jsonl 单个几十 MB、全目录实测
945 MB,UsageStats 早期整个读进内存导致用量页永远转不出来的教训不能重犯。
窗口切在行中间时残缺的首行必须丢掉,否则会解析出半截内容。
定位不猜编码规则:主路径按"非字母数字换 -"拼(30 个真实样本全中),
拼不中就按 sessionId 全局 glob 兜底 —— UUID 唯一,兜底是精确的。

TaskStateReader —— 读 ~/.claude/tasks/。对过账:gateway-config-management
读出 {completed:2, in_progress:1, pending:1},与 Claude 自己界面上的
"4 tasks (2 done, 1 in progress, 1 open)" 完全一致。

StallDetector —— 五类原因按优先级判定:
  断线 > 在提问 > 等授权 > 有后续 > 待验收
断线是异常,立即报不等阈值。提问的启发式只看**最后一个非空行**是否以问号
结尾,不扫全文 —— 正文中间的反问句非常常见,扫全文必然误判。
拿真实数据验证:storefront-a-47 被正确判为"❓ 在问你 · 需要我帮你把这两个提交
精准推送到 live 吗?",正是用户截图里那一条,且全程没用 AI。

分级:只有"干了没多久就安静收工"降级成不主动闯入。干活时长必须自己观测
(Claude 显示的 "Crunched for 29m 54s" 不落盘),观测时排除 waiting ——
否则等了两小时授权的会话会被记成"干了两小时"。

hubprobe stall 子命令:滞留提醒是时间驱动的,靠 UI 验证意味着干等几分钟。
这个子命令把每个会话的判定和判据一次打出来,理由同已有的 usage 子命令。

164 个测试通过(原 131)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(probe): StallJudge —— 用订阅额度给滞留会话生成一句人话

职责刻意收窄:**不判断要不要提醒**,只把事情说清楚(摘要、是不是在提问、
回答选项、建议动作)。让模型决定要不要打扰用户是高风险的,误报一次用户
就会把整个功能关掉。收窄之后它失败的代价很低 —— 解析不出来就退回
"完成了一轮,去看看",提醒照发,只是文案糙一点。

走 claude 二进制而不是直接调 API,用的是用户已登录的订阅额度,
不需要另配 ANTHROPIC_API_KEY。不能用 --bare:它跳过 hooks 的同时也跳过
OAuth(帮助原文写明 keychain is never read),等于用不上订阅额度。

踩到的三个坑,都写进注释了:

1. **--disallowedTools 是变长参数,会吞掉后面的位置参数。**
   提示词放在它后面时被当成又一个工具名吃掉,模型压根收不到指令。
   表现极具迷惑性:三次调用全部"成功"(有返回、扣了钱),但模型只收到
   正文,于是直接去回答正文里的问题,返回散文而不是 JSON。
   提示词必须紧跟 -p,排在所有变长选项之前。

2. **正文不能走 stdin。** 待分析的文本经常是直接对着读者提问的
   ("需要我帮你推送到 live 吗?"),和指令混在一条消息里时模型分不清
   边界,会把它当成对自己说的话去执行。改成全塞进提示词、用 <待分析>
   标签把数据围死。顺带 stdin 接 /dev/null,省掉 claude -p 干等 3 秒。

3. **字数限制不能指望模型守约。** 实测它给 5 个选项(要求最多 4 个)、
   14 字的 nextAction(要求 ≤10 字)。岛上位置是死的,解析时必须再截一次。

围栏也必须剥:即使明说"只输出 JSON 对象",它照样包 ```json。

实测(hubprobe stall --ai):2 次调用 0 失败,$0.024,约 9 秒/次。
storefront-a-47 拿到 4 个可直接做成岛上按钮的选项。
HUB_TRACE 回环探针全程 0 行 —— 守卫有效。

175 个测试通过(原 164)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(probe): NudgeCoordinator —— 会闭嘴的升级阶梯

0 / 3 / 8 / 20 分钟四档递增,之后每 20 分钟。递增而不是固定间隔:
固定间隔在用户"知道但暂时不想处理"时会变成必须关掉的骚扰。
第一轮静默(刚停下来就叫太急躁,而且这时用户多半就在机器前面),
第二轮起出声,第三轮起换 identifier 重发通知 —— 同 id 的通知在通知中心
里是静默替换,不换 id 用户根本看不到"又催了一次"。

**加了计划里没有的两条封顶**,是拿真实数据逼出来的:
本机有个会话已经 idle 46 小时,按"每 20 分钟一轮、不再加码"会主动弹
138 次。那不是提醒,那是必须被关掉的东西。

  maxActiveRounds = 5     主动闯入最多五次
  giveUpAfter = 2 小时     之后只留跑马灯和下唇染色

放弃"闯入"不等于放弃"告知"—— passive 列表照常带着它,信息一直在岛上。
giveUpAfter 顺手解决了另一个场景:app 重启时会一次性看到几个隔夜会话,
它们的 since 早就超过阈值,所以启动瞬间不会被一串闯入淹没。

多个会话合并成一次提醒,按原因优先级排序,不排队弹 N 次。
用户跳转或应答只压制一轮 —— 跳过去看一眼不等于处理完了。真正的处理完是
会话重新开工,那时它就从 findings 里消失,状态自然清掉,下次再停是新的一件事。

纯状态机,now 显式传入,10 个时序测试跑完整条阶梯不用真等 20 分钟。
185 个测试通过(原 175)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(island): 滞留提醒的岛上表现 + 应答 + 跨屏跟随

## 两个新形态

.nudge —— 卡住的会话的像素小人从刘海下沿弹出排开,逐个延迟 60ms
(一起弹是一整块在动,错开才读得出"一个一个冒出来")。每个小人上方一个
原因图标、下方截断的会话名,整格可点。刻意不复用 .intrusion:后者是
"某件事刚发生",单会话两秒划过;前者是"你有 N 件事卡着",聚合、停 5 秒、可交互。

.answer —— 显示 AI 的问题和它给的快捷回答。**点了按钮不直接发**,
先出"将发送到 iTerm · xxx:是"让用户确认一次。多这一步是因为刘海很小、
用户可能在剔背景的情况下随手点一下,而那一下可能是往生产推代码。
两态取同一个高度:不同的话点按钮时岛会跳,而那一跳恰好发生在用户
正要去点"确认发送"的时候,按钮会在指针底下移位。

折叠态:有东西卡着时点阵换成跑马灯(点阵回答的是"跑着几个",
卡住时要知道的是"哪个、为什么",这时点阵是纯噪音),整条下唇按最高优先级
原因染色。只在有 stall 时才滚,平时回到静态点阵,不引入常驻动画耗电。

## 回写终端(唯一不可逆的动作)

TerminalReply 的五条护栏:发送前重读会话状态(变 busy 说明用户已经自己
在那边动手了,取消并如实说,绝不硬发);只发一行纯文本 + 回车(换行会被
当成多敲一次回车,控制字符能触发终端快捷键);绑定只走 PID 祖先链,
绝不用窗口名匹配;定位不到就退化成跳转;每次发送留痕。

## 跨屏跟随

岛以前锁死在内建刘海屏,用户看着外接屏时提醒在物理上不在他视野里 ——
这条不解决,跑马灯和主动闯入全是白做。screenUnderCursor() 其实一开始
就写好了,只是全项目零调用。只在折叠态搬家:展开/审批/应答态搬家会把
用户正在点的东西从手底下挪走,尤其应答态按钮一移位,本来要点"取消"的
那一下可能落到"确认发送"上。

## 实机验证发现并修掉

点快捷回答 → 出预览而不是直接发(护栏生效);确认发送 → 目标会话不存在时
如实报错。但失败提示在点"取消"回到选项后仍挂着,读起来像这一次也失败了 ——
加 onClearFeedback,任何新动作先抹掉上一次的回显。

199 个测试通过(原 185),含 nudge/answer 的尺寸回归 ——
SwiftUI 的布局裁切是完全静默的失败,只能靠算术断言挡住。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: 小人多长了一只右手;滞留信息补进展开态;钉住不再卡死界面

## 挥手的小人有两只右手(用户在 48pt 详情卡上看出来的)

waiting 用挥手姿势,bodyRows 在肩膀那行画了举起来的右手,而 laptopRows
仍然把两只手都画在笔记本两侧 —— 同一个人两只右手。而 waiting 恰恰是
最需要被看清的状态,偏偏是它画错。

    9   ##ssssssss##f     ← 举起来挥的
   13   #LLLggggLLL#f     ← 同一只手又画了一遍

新增 SpriteAnatomyTests 断言每侧最多一只手。像素画的错误在 32pt 下肉眼
几乎发现不了,但在字符网格上可以精确断言。**回插 bug 验证过测试会红**
(24 条断言失败),不然写了等于没写。

## 滞留信息只活在 nudge 和跑马灯里

用户一展开列表 —— 也就是他真正要动手的地方 —— 滞留信息就消失了,
反而在最该看到的位置看不到。现在:

- 详情卡加滞留横幅(原因 + 具体内容),卡住时**顶掉**提交记录:
  两者都要位置,而"这件事在等你"永远比"三天前提交了什么"更急;
  是提问的话直接给「回答」按钮进应答态。
- 列表行加原因徽章,优先于笼统的 waitingFor("在问你"比 "input needed" 具体)。
- 折叠态点跑马灯**直达那件事**,不再跳到通用列表让用户自己再找一遍。
- 非提问的滞留点开是展开+选中,不直接把人甩到终端 ——
  用户点这一下是想先看清楚是什么事。

## HUB_ISLAND_STATE 会把界面卡死

实机暴露:岛钉在展开态、又跟着光标跑到了外接屏,用户点收起没反应,
只能去杀进程。钉住的本意是挡掉自动迁移(悬停会把它收回去),
不该把用户主动的操作也冻掉。现在 setState 分 userInitiated,
收起和展开一律放行。

顺带:收起时强制重新对齐到光标所在屏 —— 岛跟随只在折叠态生效,
而刚收起正是最该对齐的时刻(用户很可能已经换屏了)。

203 个测试通过(原 199)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: 换掉占位图标,并入 agentx 系列视觉

原来那个三机器人是占位图,和岛里的像素小人、刘海形态完全是两套语言。

沿用 agentx 系列的记号(青→蓝→紫→品红→橙的斜向渐变 + 四角星芒 + 外辉光,
对着 AgentX 和浏览器扩展的图标取的),但主体换成**灵动岛的胶囊形** ——
那是这个 app 唯一的门面,也是它在无刘海屏上的真实形态。

极简的判据是"16pt 下还认得出":一个胶囊 + 一颗星,没有任何细节要分辨。
实测 16pt 下胶囊清楚,但星芒只剩两三个像素、糊成一个污点反而像脏了,
所以 <32pt 不画星芒 —— 光学尺寸的常规做法,小尺寸减细节。

图标用代码生成(Resources/make-icon.swift)而不是塞一个二进制资产:
可以 diff、可以 code review、改一个参数不用重新导出,
和这个项目里像素小人 art 写在代码里是同一个理由。

    swift Resources/make-icon.swift AppIcon.iconset
    iconutil -c icns AppIcon.iconset -o Resources/AppIcon.icns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(nudge): 升级阶梯按第一次提醒计时,不是按会话卡住的时刻

实机验证抓出来的。修复前的真实时间线:

  17:38:10  第 0 轮
  17:38:26  第 1 轮 有声    ← 隔 16 秒,阶梯说该隔 3 分钟

延迟原来从 since(会话变 idle 的时刻)算。product-library-fixes 被发现时
已经 idle 4 分钟(240 秒),于是一次就同时满足第 0 轮(≥0 秒)和
第 1 轮(≥180 秒),下一次扫描(30 秒后)立刻又喊。

关键在于这不是边角情况:**"被发现时已经卡了一会儿"才是常态** ——
app 刚启动会看到一批、刚满阈值才被判出来的也都是。等于阶梯对绝大多数
会话都是失效的,会连着把前几轮一次性喊完。

改成记 firstNudgedAt,阶梯从第一次喊开始计时。giveUpAfter 仍然按 since
算 —— 它要挡的正是"这事早就在那儿了,你显然不打算现在处理"。

原来那条阶梯测试让 since 和 now 从同一时刻起步,正好绕开了这个场景,
所以一直是绿的。新测试直接构造"发现时已卡 4 分钟",并**回插 bug 验证过
它会红**(2 条断言失败)。

顺带给 present() 加了提醒日志。滞留提醒是时间驱动的,没有日志根本没法
验证"到底喊没喊、第几轮、为什么"—— 这次的 bug 就是靠它才看出来的。

实机同时确认:多会话合并成一次弹出;idle 了 446 分钟的会话一次都没喊
(闭嘴上限有效);折叠态跑马灯在真实数据上正常滚动。

204 个测试通过(原 203)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(nudge): 封住"反复变原因"绕过两道闭嘴上限的洞

"原因变了就重新计时"本身是对的("断线了"和"待验收"是两件事,
不该沿用旧轮次),但它同时开了两个洞:

1. 每翻一次从第 0 轮重来,于是既不升级、也永远撞不到 maxActiveRounds;
2. since 跟着被推到现在,giveUpAfter 那条"卡了两小时"永远够不着。

结果是一个在 idle / waiting 之间来回翻的会话可以无限期地每隔几分钟
弹一次同一个项目 —— 而这正是用户最容易直接关掉整个功能的那种行为。

两处修正:

  maxLadderRestarts = 2   重置本身封顶,超过之后原因照常更新但轮次不再
                          归零,阶梯继续往上走直到闭嘴
  firstSeenAt             记"连续卡着从何时开始",原因变了也不重置;
                          giveUpAfter 改按它算

会话真的恢复(从 findings 里消失)时整条记录被丢弃,重置额度也跟着清零 ——
否则一个用了很久的会话会因为历史上的几次翻转而再也不能重新计时。

测试断言的是**最终会安静**这个性质,不是某个具体次数:90 分钟里每 5 分钟
翻一次原因,断言最后半小时完全无声、且进入只做被动提示的状态。
回插两个洞验证过会红 —— 无防护时 18 次翻转全部触发,且换原因能绕过 giveUpAfter。

208 个测试通过(原 204)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: 版本升到 2.1.0

滞留守望、岛上应答、跨屏跟随、新图标 —— 都是新能力,走 minor。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: 截图模式不启动守望;钉住日志打实际形态

守望 30 秒后会用自己对演示会话的判定覆盖预置的那组,而演示会话没有真实
transcript 和任务清单,判出来清一色待验收—— 五类原因的区分恰恰是要展示的。

钉住日志原来只打环境变量的值,钉住失效时照样显示已钉住,把排查带偏过一次。
改成同时打实际形态和滞留条数。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:43:01 +08:00
b6d90cac2b feat: 重写为原生 Swift 灵动岛,修掉通知跳转与主窗口数据链路 (#4)
删掉 Tauri 版(app/),全部重写为 SwiftUI + SwiftPM(HubKit/,8.2k 行源码 +
2k 行测试,131 个测试)。核心动机是旧版"点通知跳不到对应终端"——
根因是身份链路全程用会漂移的字符串:项目名 → tmux 窗口名 → pane_title →
AppleScript 子串匹配。实测 9 个 tmux 窗口只有 2 个能和 projects.yaml 对上,
其中一个窗口名是 `2.1.173`(tmux automatic-rename 把它改成了 claude 的版本号)。

主要变化
- 会话状态直接读 ~/.claude/sessions/<PID>.json(Claude Code 自己维护的权威源),
  不再用 spinner 字符猜 busy;死进程的残留 json 用 kill(pid,0) 过滤
- 跳转改用 iTerm jobPid + 进程祖先链求交,全程只有整数 PID 和 UUID;
  bg 会话不在任何终端里,单独用稳定 job 名兜底。实测 9/9 绑定成功
- hook 从 bash 换成 Swift 的 hubctl,走 Unix socket;Hook 没运行时立即放行
- 高风险操作在岛上审批,六重防误点;拒绝是超时和异常的默认值
- 主窗口:会话 / 项目 / 用量 / 审批日志 / 设置

修掉的真实缺陷(详见 HubKit/NOTES.md)
- 超时阶梯是反的:ApprovalCoordinator 和 hubctl 读超时都是 60s,同时到期时
  hubctl 按 fail-open 放行了一条已被服务端判定拒绝的 git push --force origin main。
  改成 60/65/75/90 严格递增,并区分「连不上」(放行)与「连上但没应答」(拒绝)
- 点击全部被吞:app 是 .accessory + panel 是 .nonactivatingPanel,
  NSHostingView.acceptsFirstMouse 默认 false,每次点击都算 first mouse 被丢弃
- hitTest 假设 NSHostingView 恒为 flipped,而 isFlipped 是 final 的可读写属性
- 形态切换靠替换 rootView,导致动画不可能发生且 @State 每次归零
- 全局 leftMouseDown 监听会收到点在自己身上的点击,展开态一点就收起
- hook 进程死后审批卡不消失,且会挡住后面的请求(按 clientPid 扫描清理)
- app 读的 projects.yaml 不是脚本维护的那份,且那份被写坏成
  `projects: []` + 117 条内容,解析器静默返回 0 —— 三个页面因此全空
- 用量对 945MB/341 个 jsonl 整份读进内存,跑不完;改流式 + 增量缓存后
  7 天首扫 3.6s、命中缓存 0.22s、全量 4s
- 辅助二进制放 Contents/MacOS/ 且共用 bundle id 会被 exec 瞬间 SIGKILL
- 同一 bundle id 对应多个 .app 让存活判断和 TCC 授权互相打架

刻意的取舍
- 放弃 Liquid Glass 做岛的基底:它会跟着背景亮度双向补偿,而刘海恒为纯黑,
  两者物理上互斥。改用固定 86% 暗色基底 + 背景模糊 + 边缘高光
- 用 SwiftPM + 打包脚本而不是 .xcodeproj:手写 xcodeproj 的 XML 无法 review,
  且 SwiftPM 保住了 swift test

Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 19:20:12 +08:00
2a12a1611c feat: .app 真·开箱即用 + 终端派发修复 Keychain 登录态 (#3)
让 .app 不再依赖 hub 项目仓库存在,在新电脑上装上就能用。

## 自包含改造
- scripts/sounds 嵌入 .app Resources,启动自动同步到
  ~/Library/Application Support/claude-hub/(覆盖式)
- projects.yaml 迁到 ~/Library/Application Support/claude-hub/,
  首次启动自建空模板
- scripts 改自定位($(dirname $0) 推断 HUB_DIR),
  不再硬编码 ~/Documents/code/claude-hub
- 启动时 ensure_path() 跑 zsh -l -i 继承 user shell PATH,
  解决 launchd 启动只有 /usr/bin:/bin 找不到 brew/bun 的问题

## 依赖检查
- 新增 deps.rs 启动时 which tmux/iTerm/claude/git/terminal-notifier
- UI 红卡片显示缺失依赖 + 一键复制 brew install 命令

## 终端派发(修复 Claude 非登录状态)
- 新增 term.rs:检测 iTerm > Warp > WezTerm > Terminal.app
  优先级,第一版只支持 iTerm + Terminal.app
- 第一次 tmux new-session 改由终端跑,server 父进程是终端,
  claude 继承终端 Keychain ACL 解决登录态读不到的问题
- UI deps panel 显示当前终端 + 检测到旧版 server 时提示
  tmux kill-server

## UI 增强
- 空项目时显示扫描引导卡片
- 顶部加「⟳ 扫描」按钮,Rust 实现 git 仓库自动发现
- 缺失依赖卡片支持点击复制安装命令

Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:52:41 +08:00
0dd5edfcae feat: hub 显示 git 状态 + 右键菜单修复 (#1)
* feat: hub 项目列表显示 git 分支与提交状态

托盘 app 项目卡片和 welcome.sh 终端列表都新增 git 状态徽章:
当前分支、是否干净(✓/●N)、未推送 ↑n、落后远端 ↓n、无 upstream。
后端用 git status --porcelain=v2 --branch 单次取齐所有信息。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: 项目运行中时右键也能弹出操作菜单

之前左键点 running 项目只会切到 tmux 窗口,无法访问"打开 Finder / VS Code"等操作。
现在右键任何项目都弹菜单,左键行为保持不变。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:19:45 +08:00
wuguanganandClaude Opus 4.6 e029bbc1ce feat: setup.sh 自动配置 iTerm2 tmux 集成,无需手动 GUI 操作
- 通过 defaults write 直接修改 iTerm2 plist 配置
- OpenTmuxWindowsIn=2(标签页模式)+ AutoHideTmuxClientSession=true
- README 更新:说明 iTerm2 配置已自动化

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:10:38 +08:00
wuguanganandClaude Opus 4.6 1649ed9070 fix: 完善新电脑初始化流程,build-app 自动检查环境
- build-app.sh: 自动检查/安装 Rust、检查 Xcode CLT、显示架构信息
- setup.sh: 首次运行自动触发编译托盘 app

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 14:18:07 +08:00
wuguanganandClaude Opus 4.6 ffa18e2575 feat: 添加用量统计面板 + 优化托盘 app 启动方式
- 新增用量统计 tab,按项目展示 token 消耗、会话数、模型分布
- 新增 usage.rs 模块解析 Claude 日志计算用量
- 托盘 app 改用 release 编译产物直接启动,移除 claude-hub-bin
- 项目窗口管理:修复新建 session 时 _init 窗口清理逻辑
- 新增 build-app.sh 构建脚本

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 21:06:21 +08:00
wuguanganandClaude Opus 4.6 af1f9307e1 feat: 修复托盘图标交互 + 项目搜索 + 浏览器配置优化
- 修复托盘图标无响应:显式设置 icon(include_bytes)、移除 config 重复 trayIcon
- 托盘交互:左键点击显示窗口、右键退出菜单、关闭窗口隐藏到托盘
- 新增项目搜索框:支持按名称/描述/路径模糊匹配
- 启动序列自动拉起通知面板
- browser-auto skill 改用 --autoConnect 连接用户日常 Chrome
- 新增 setup-browser.sh 一键配置浏览器环境
- CLAUDE.md 补充 skill 管理和浏览器配置文档

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:47:51 +08:00
wuguanganandClaude Opus 4.6 ab268b3825 feat: cc 一键启动命令
- scripts/cc.sh: 启动托盘 app + claude 调度中心
- alias cc 加入 ~/.zshrc,任何终端输入 cc 即可启动
- release 二进制编译完成

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:29:10 +08:00
wuguanganandClaude Opus 4.6 2344ecf4cf feat: terminal-notifier 替代 osascript,点击通知跳转 iTerm2
- hook 脚本优先使用 terminal-notifier(点击跳转 iTerm2)
- osascript 作为 fallback
- 修复 Tauri app 编译错误(去掉 dirs_next,修复 notification 配置)
- 窗口默认可见
- setup.sh 增加 terminal-notifier 检查

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 11:39:21 +08:00
wuguanganandClaude Opus 4.6 5403bdad1f feat: Tauri 桌面通知 app + 状态面板
- app/ 目录:Tauri v2 项目(Rust + HTML/JS)
- 托盘图标 + 点击显示状态面板
- 监听 /tmp/hub-signals/events/ 的 JSON 事件
- 系统原生通知,点击跳转到对应 tmux 窗口
- hooks 改为写 JSON 事件文件,Tauri 没运行时 fallback 到 osascript

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:59:04 +08:00
wuguanganandClaude Opus 4.6 f92763f8fb feat: scan_dirs 配置 + 智能扫描
- projects.yaml 支持 scan_dirs 字段,配置一次后续扫描无需传参
- scan-projects.sh 自动跳过已存在的项目
- 支持多目录扫描

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:41:23 +08:00
wuguanganandClaude Opus 4.6 5519188cd5 initial commit: claude-hub dispatch center
tmux 多项目窗口管理 + 全局通知 hooks(Stop/Notification)
- scripts/setup.sh 一键初始化
- scan-projects.sh 自动发现项目
- hub-hook-stop.sh / hub-hook-permission.sh 全局通知
- skills: tmux-hub, browser-auto, lanhu-design-viewer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:38:42 +08:00