wuguangan and Claude 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
wuguangan
a0a4d44f8f
merge: 共用密钥中心 + 项目账号密码库 + 密钥泄漏闸
...
三件事一起进来,它们回答的是同一个问题:AI 替你干活的时候,你的密钥在哪儿。
- 共用密钥:一处配好勾给项目,物化成 0600 的 .env,给 AI 的是路径不是值,
而且是「source 它,别 Read 它」的用法契约
- 账号密码:本地/测试/线上分开记,AES-GCM 加密,看和复制过 Touch ID
- 密钥泄漏闸:AI 要把真实值写进文件、内联进命令,或者 git add/commit
会把 .env 带进仓库时直接拒绝。跑在 hubctl 本地,不依赖 app 在不在跑
646 个测试全过(原 530)。
2026-08-18 10:46:58 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 5
7b5dbbce95
feat: 给 AI 的那段话摊在界面上,不再藏在复制按钮背后
...
原来只有一个「复制给 AI 的话」按钮 —— 复制之前你根本不知道它会往剪贴板里
塞什么。一个内容不可见的复制按钮,用户要么不敢点,要么点了粘出来才发现
不是想要的。而这段话恰恰是要替他去管住 AI 的,他更有理由先看一眼。
绑定的项目行上多一个「看话术」,展开就是原文,等宽字体、可选中 ——
整段复制走按钮,只想拿其中一句就直接划走。下面再补一句说明为什么
第二句是重点(source 只把值放进那条命令的环境,Read 会把值留在对话记录里)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-17 13:41:33 +08:00
wuguangan and Claude Opus 5
8510e1de28
feat: 读密钥文件的 Read/Grep/Glob 拦一下
...
`RiskClassifier` 里 readOnlyTools 是无条件早退的(注释写明理由:Read 一个讲
`rm -rf` 的 README 不该触发审批)。但「读密钥文件」恰恰是要拦的那件事——
共用密钥交给 AI 的契约是 `set -a; source`,值只进子进程环境;改用 Read
之后文件内容就进了 ~/.claude/projects/*.jsonl(0644、永久、每轮重发给 API)。
判断**只放在 readOnlyTools 那个分支里,不放在 classify 开头**。放开头的话,
约定用法里那条 source 命令每次都会弹审批,正常流程当场堵死。Bash 走下面的
模式匹配,`\.env\b` 已经把它定成 dangerous(记录不拦截),不动。
写清楚它挡不住什么:python -c 读、软链、base64 转一道都绕得过,
而且 hubctl 超时是 fail-open——app 没开着的时候这道闸是关的。
它是一次提醒,不是安全边界,README 和注释都按这个写。
路径常量抄在 HubIPC 里(那个模块刻意不依赖 HubProjects,hubctl 要小而快)。
抄就会漂,漂了的症状是静默的——改个文件名,闸就再也命不中而界面毫无变化。
所以加了 SensitivePathsAgreementTests,在同时看得见两个模块的测试目录里
把两边的真实路径对一遍。
625 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-17 12:48:30 +08:00
wuguangan and Claude Opus 5
a9363e43c9
feat: 共用密钥中心 + 项目账号密码库
...
两个功能来自同一个痛点,但威胁模型正好相反,这是整个设计的支点。
**共用密钥**:平台密钥(飞书、领星、OpenAI)在十几个项目里要各配一遍,
而最省事的做法——直接粘进聊天框告诉 AI——是最糟的那个。现在按平台建组、
勾给项目,物化成 ~/.vibe-foreman/env/by-project/<项目>-<哈希>.env
(目录 0700、文件 0600),复制路径给 AI。这份数据**故意是明文、故意给 AI 用**。
**账号密码**:本地/测试/线上分开记,只给人看。AES-GCM 加密,
查看和复制过 Touch ID。这份数据**绝不能被任何自动化读到**。
两者不共用存储、不共用目录、不共用界面,并用一条哨兵测试钉死
「密码的值不会出现在给 AI 的那个目录里」。
三个决定了做法的发现:
1. 「让 AI Read 那个文件」会把密钥写进两个明文文件——
~/.claude/projects/*.jsonl(0644、永久、每轮重发给 API),
以及 Hub 自己的验收清单(Evidence.ran 会存命令和输出尾巴,同样不过期)。
所以「复制给 AI」给的不是裸路径,是带 `set -a; source` 用法和
「不要用 Read/cat/grep」禁令的三句话。换成 source 之后,
transcript 里只剩路径和变量名,值一次都不出现。
2. 按项目分文件**是分装不是隔离**——同 uid 下 cat ../别的.env 照样拿得到。
README 和代码注释都按分装写,不吹成边界。
3. Keychain 的授权弹窗**就是那道防线**。SecItemAdd 不传 kSecAttrAccess 时,
默认 ACL 只允许创建它的 app 无提示读取,而在「攻击者 = 同 uid 的自动化」
这个模型下,能区分「Hub 在读」和「security 命令在读」的只有它——
文件权限做不到(同 uid),Touch ID 也做不到(LAContext 不参与取密钥)。
为了消弹窗放宽 ACL,等于一条 Bash 就能取走主密钥。
几个不写下来一定会再踩的坑(已进 NOTES.md):
- .atomic 写换 inode,chmod 必须每次都做,只在创建时设一次从第二次写起就是 0644
- GCM 的 AAD 必须用文件里存下来的那段字节,重新序列化会让历史文件永久打不开
- 「钥匙不对」和「文件坏了」处置完全相反,靠 header 里的 keyID 分辨;
EncryptedFile.save() 的 guard state.canWrite 是这套区分唯一的落地点
- LAContext 复用才有 reuse duration,每次新建等于每条密码按一次指纹
- 剪贴板定时清空必须比对 changeCount,否则用户复制的别的东西会被莫名清掉
- .env 的引号:shell / node dotenv / python-dotenv 三家的交集只有单引号;
多行值用字面换行不能用 \n;含单引号的值没有共同解,改写成 _B64 显式降级
617 个测试全过(原 530)。真 bash `set -a; source` 验过含空格、$、反引号、
#、多行 PEM 和含单引号的值。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-17 12:45:20 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 5
f1d218c56c
docs: 加贡献指南,README 补「参与共创」
...
「欢迎共创」得有东西接得住,否则是句空话。所以写的是具体缺口
(终端适配、别的 CLI、公证、4K 核对)而不是泛泛的欢迎语 ——
每一条都是真实的、现在就存在的缺口。
CONTRIBUTING 里重点写了这个仓库的两条不成文规矩:
注释写"为什么"不写"是什么",测试钉的是"写错了不会报错只会静默失灵"
的那些判断。这两条不说清楚,外来 PR 只能靠猜。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 23:29:45 +08:00
wuguangan and Claude Opus 5
3e23d1efba
docs: 加上应用图标和社交预览图
...
- docs/icon.png 满幅方图,去掉 icns 里四周 100px 的透明留白 ——
GitHub 会自己加圆角,留白会让图标缩成小小一颗
- docs/social-preview.png 1280×640。仓库被分享到 Twitter / Slack / 飞书时
唯一会被看到的东西;没有它链接展开是一片灰底加一行小字
- README 头部放上图标
社交预览图由 Resources/make-social.swift 生成,图标直接读 AppIcon.icns
而不是重画一遍 —— 两处各画一遍必然漂移,"两个地方的图标不一样"
是最廉价也最刺眼的破绽。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 23:24:16 +08:00
wuguangan and Claude Opus 5
3af92f27cf
chore: 仓库改名 vibe-foreman,代码认新旧两个目录名
...
仓库 gulugulu-cn/claude-hub → gulugulu-cn/vibe-foreman。
GitHub 自动重定向旧地址,6 个 Release 全部保留。
代码里按 ~/Documents/code/claude-hub 找脚本和 projects.yaml 的地方,
改成新旧两个目录名都试 —— 老用户的工作区还叫旧名,新 clone 的是新名,
少认一个的表现是"从界面开项目时项目配置没被补齐",而那是静默的。
**Application Support 下的 claude-hub 目录不改名。** 那是用户数据落盘的
地方(验收清单、盯梢配置、已结束会话名册、置顶、审批日志),
改了等于把现有配置全孤立掉。
530 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 23:05:03 +08:00
wuguangan and Claude Opus 5
172ae46515
chore: 界面和提示语里的产品名也改过来
...
上一版只改了 app 显示名,Swift 里的字面量还写着旧名 —— 而那批是
**用户直接看得到的**:验收守望的提示头、审批被拦时的理由、
岛上作答回给 Claude 的那句、验收页的空状态。
正文用短名 Vibe Foreman,app 显示名保持 Vibe Foreman Free:
全名带 Free 夹在句子中间很别扭,而且以后真出付费版时正文不用再改一遍。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 22:51:42 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 5
985d77eca6
docs: 恢复图文版落地页,安装与实现细节挪进 docs
...
强推源码时我把公开仓库的落地页换成了纯文字的安装手册 ——
那份图文介绍(8 张排过版的截图 + 说明文案)是特意设计的,被我弄丢了。
这是回退。
现在的分工:
- README.md 产品介绍。8 张图 + "它能做什么",给第一次看到的人。
- docs/安装与实现.md 装机、开发、故障排查、目录结构、各模块实现细节。
顺带把三个新能力按同样的语气写进落地页(验收复核、盯梢追问、
关窗即结束),而不是只堆在技术文档里 —— 它们是这个工具最不一样的地方。
旧落地页的「许可」段写的是"本仓库不含源代码,所以没套源码许可证",
源码进来之后这句不成立,换成 MIT。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 22:08:46 +08:00
wuguangan and Claude Opus 5
a8707ba6be
docs: README 补上验收清单、盯梢、关窗即结束
...
这三块是这个工具最不一样的地方,而 README 的功能列表里一条都没有 ——
一个开源项目最该讲清楚的恰恰是"它和别的有什么不同"。
- 验收清单:要点来源分级、Stop 拦截逼报证据、拿真实 diff 旁路复核、
为什么只有两档没有"部分完成"、以及"它压根不提的事没人查"那条独立路径。
自跑验收命令的三道闸(白名单/拒元字符/默认关+逐条授权)也写明。
- 会话观察者:讲清楚它治的是"做浅"不是"停了",三种追问怎么交替,
以及三条实机撞出来的硬约束(只有 busy 算干活、打字时不发、画面在变不打扰)。
- 关窗即结束:四条边界都写上,尤其"只记接得回来的"和"绑不到 pane 的不碰"。
- 项目管理:补上右键菜单、「添加目录」和扫描的分工、失效项目为什么静默有害。
所有数字和行为都对着代码核过(白名单取自 AcceptanceVerifier.whitelist,
节奏取自 SessionWatchdog 的 needStreak/cooldown/interval)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 17:55:22 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 5
4c16d7826e
docs: 追问清单放仓库外
...
清单会写满「这个项目该做什么、卡在哪、下一步是什么」——那是项目内情,
不是工具的一部分。放进仓库就等于跟着工具一起发出去了,
这次开源审计里最严重的一条就是这么来的。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 17:33:24 +08:00
wuguangan and Claude Opus 5
0077991e73
chore: 开源前脱敏 —— 移除内部训练清单,项目名换成中性名
...
配合 filter-repo 重写历史(51 个 commit 全过一遍):
- 删除 scripts/probes/(agentx 训练追问清单)。那两个文件描述了一个
未发布商业产品的架构、训练策略、内部通道拆解和内部素材路径 ——
不是密钥泄露,是路线图泄露。已从全部历史移除。
- 真实项目名换成中性名:内容和 commit message 一起替换,
实战细节全部保留(NOTES.md 里每条坑的现场是这个仓库最有价值的部分)。
- 蓝湖 skill 的真实项目 UUID 换成 YOUR_* 占位符,并说明去哪儿取。
- /Users/dev → /Users/dev。
- 补 LICENSE(MIT,README 早就写了 MIT 但没有文件)。
- README 的 clone 地址指向新的公开仓库。
ProjectSearchTests 的 fixture 跟着改了:`demo-ios` 有 n 和两个 u,
`demo-ios` 没有,原来的断言字符在新名字上不成立。断言的性质没变
(子序列必须按顺序、字符不能复用),只换了在新 fixture 上等价的字符。
保留 bundle id dev.hengjun.claude-hub —— 改它会让已授予的 TCC 权限
(控制 iTerm 的 Automation、通知)全部失效,而 app 标识本来就是公开信息。
518 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 17:31:13 +08:00
wuguangan and Claude Opus 5
bfc4ebaecb
fix: 失效项目静默开到家目录;补上删除和"添加任意目录"
...
## 根因
tmux 对不存在的 `-c` 目录**返回 0 并悄悄用家目录**,`open /不存在` 则
静默什么都不做。实测:
tmux new-session -d -s t -c /不存在 ; echo $? → 0
pane_current_path → /Users/dev
projects.yaml 里 40 条有 6 条指向已删除的目录(agentx-agent 那一批),
于是表现成「Finder / VS Code 全没反应」+「Claude 开到 /Users/dev」——
看起来像刚加的右键菜单坏了,实际是数据坏了。
两个静默回落把数据问题伪装成了功能问题。我连猜三轮代码都没猜中,
最后靠"派发参数落盘 + tmux 实测"定位。
## 改动
- TerminalDispatch.open 派发前查 fileExists(isDirectory:),不通过就拒绝
- 派发日志写 launch.log(log show 对这个 app 是瞎的,ad-hoc 签名)
- hubprobe launch <项目名> [mode]:走完全相同那条路,把"界面传了哪个项目"
和"派发本身对不对"分开,这次就是没有它才只能猜
- ProjectStore.missingPaths:失效项目在两个界面上都标出来(岛上尤其必要,
那里根本不显示路径),菜单只留「从列表移除」——目录没了时每一个"打开"
类选项都是静默失败,给假的可点项等于把刚修好的问题再造一遍
- ProjectStore.remove / 工具栏「清理失效 N」:只动清单,不碰磁盘
- ProjectStore.addExisting + 「添加目录」:扫描只认 .git,monorepo 子目录和
没 git init 的目录永远进不来(实机 7 条),这是缺的那个入口
删除是文本手术,不是解析完重写 —— 保住手写的注释/别名/描述。
边界是删到下一个同级列表项或顶层键;只删 name 那行会让剩下的字段被挂到
前一条项目上,删一条弄坏两条且完全静默。11 个测试钉着。
518 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 15:58:14 +08:00
wuguangan and Claude Opus 5
ed287a992e
feat: 关掉终端窗口就结束会话,并且接得回来
...
用户选了「关窗即结束,一律 kill」,紧接着补了一句「那我也打不开了呀」。
两件事一起做才成立 —— 只会杀不会还的回收器是把一个显示错误的会话
换成一个找不回来的会话。
## SessionReaper
检测到 tmux 会话没客户端连着就结束它。四条边界:
- 杀之前先把 sessionId 记进 ClosedSessionStore(落盘)
- 只记接得回来的:claude --resume 认的是磁盘上那份 transcript,不是 id。
一次都没说过话的会话没有 transcript —— 实机验证过,一个只停在提示符
上的会话 ~/.claude/projects 里一个字节都没有。不查就记会长出一个
点下去报错的按钮。判据用 TranscriptReader.locate。
- 绑不到 pane 的一个都不碰:不在 tmux 里的会话判断不了"有没有终端连着",
杀它们等于拿"不知道"当处决理由
- 新起的会话 90 秒宽限 + 连续两轮确认(30 秒):createSession 是先建
detached 再让终端 attach 的,iTerm 冷启动那几秒客户端数就是 0,
没宽限期会把用户刚点开的项目当场杀掉
设置里能关(默认开)。这是全案第二个会主动杀进程的组件。
## 接回来
项目行和岛的启动菜单长出「接着上次(Nm 前结束)」,走 claude --resume <id>。
main.swift 的 launch() 在 mode == .resumeSession 时自己去名册取 id ——
不取的话会退化成 claude --resume 的交互列表,而那个列表里全是一模一样的
项目名,用户得人肉认自己刚关掉的是哪一个。
实测:pid 6450(acme-admin,detach 2h)被正确结束,tmux server 随之退出,
名册记下了 sessionId/cwd/name。18 个新测试,500 个全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 14:04:51 +08:00
wuguangan and Claude Opus 5
5ce8c9893e
fix: 重启后开项目弹系统终端;终端关了会话还显示在线
...
两个都是「看起来对、其实一直错」的判断。
## 1. iTerm 检测
iTerm2 的包叫 iTerm.app,只有可执行文件叫 iTerm2。旧的 isInstalled 拿
AppleScript 名拼 /Applications/iTerm2.app,永远不存在,detectTerminal()
一路降级到 macOS 终端。
平时看不出来:hub session 通常已存在且有客户端连着,走 addWindow 根本
不碰终端检测。只有开机后第一次建 session 才暴露 —— 所以现象是"重启后",
其实和重启无关。
改成 bundle id 走 LaunchServices,路径只做兜底且两个候选包名都试。
顺带修 pgrep:macOS 的 pgrep 默认排除自己的祖先进程(要 -a),所以
从 iTerm 里跑的 hubctl/hubprobe 问"iTerm 在跑吗"恒为否。改用
NSRunningApplication(也不会像 AppleScript 那样触发冷启动)。
## 2. detached 会话
关掉终端不结束会话 —— claude 的父进程是 tmux server,关窗口只是 detach。
kill(pid, 0) 依然为真,项目行一直亮着蓝点写"1 个会话"。
不是把它报成已退出(那会掩盖一个还在烧 token 的进程),而是把两半都说:
还在跑,但终端已经关了。空心点 + "1 个会话 · 终端已关",
会话详情卡的位置标注也加上"(终端已关,仍在后台跑)"。
边界:按 tmux 会话名判而不是"有没有任何客户端";绑不到 pane 的不算
detached —— 那是根本不在 tmux 里,判断不了的事不能当结论下。
14 个新测试,482 个全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-14 13:47:06 +08:00
wuguangan and Claude Opus 5
d55941561f
fix: 验收把训练卡死了 —— 加冷却、降条数、追问收窄
...
实测:agentx 清单涨到 175 条,它每次收工被拦下答 6 条,答完又被拦。
22:02–22:08 六分钟全在答验收,一点活没干。
**验收本来是为了让工作更实,不是替代工作。**
三处一起收:
- 拦截加 15 分钟冷却(卸膛仍在最前面,防死循环结构不变)
- 注入上限 6 → 3
- 追问清单 77 条 → 5 条,全部以「继续」开头,只保证不停 + 目标通道打通
(完整版备份在 scripts/probes/agentx-training-full.txt.bak)
用户原话:「只要保证最后的目标通道打通,让他围绕训练就好,
不要加另外的要求。」
468 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-09 03:39:28 +08:00
wuguangan and Claude Opus 5
a6186e9505
feat: 主动去仓库核对,不等它自报
...
补的是最大的缺口:旁路复核只在它自报做完时才跑,**它压根不提的事情
没有任何东西会去查**。三份训练素材发过去两小时一个都没接,
系统里没有任何机制发现,是用户先问的。
「它停了就催」防偷懒;「它说做完了就核」防虚报;
**都防不住「它假装这事不存在」** —— 这一层补这个。
HubProbe/RepoTrace.swift:从要点文本挑关键词(引号里的名字、
路径、带连字符的标识符),去 commit 信息 / 改动文件名 / 现存文件名
三处找痕迹。一个都找不到的排到追问最前面,问法是
「我去仓库里搜过了三处都找不到任何痕迹,这条是漏了还是判断不该做还是卡住了」。
两条刻意的取舍:
- 挑不出关键词就返回空、跳过这条。拿「训练」「验证」这种词去搜什么都能命中,
会把「有痕迹」这个信号稀释成噪音 —— 比不查更糟,因为让人以为查过了。
- 多个关键词只要一个有痕迹就不报。宁可漏报不可误告:误告一次用户就不信了。
判据只回答「有没有痕迹」,不判「做得对不对」(那是 auditor 的事)。
零痕迹是极强的信号 —— 干过的事不可能一点印子都不留。
468 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-09 02:26:07 +08:00
wuguangan and Claude Opus 5
048cec7953
docs: 观察者交接(上下文压缩前)
...
这套系统存在的理由就是「压缩会让人忘事」,所以它自己的状态必须落盘。
记了六件事:系统现状(含自己查状态的命令)、还没做的 8 项(按用户关注度排)、
agentx 今天的真实结论(32 commit / 6 版本落地了,但全天 feishu 样本 0 条、
方向存疑)、已写进 trainer 规范的两节、我自己这两天犯的 7 个错、
下次接手先做什么。
那 7 个错是同一个形状:验证的是「我做了这个动作」,不是「这个动作产生了效果」。
和 agentx 那边「改了代码但没打包发布」一模一样。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 22:14:18 +08:00
wuguangan and Claude Opus 5
6b73747ea5
feat: 盯梢改成事件驱动;追问计数按会话不按项目
...
用户指出的两个问题。
## 反应力:那边停了这边没马上跟上
Hub 在 Stop hook 里第一时间就知道会话收工了,而盯梢却在 45 秒一轮地
轮询、还要连续两次确认 —— 最少 90 秒才会追问。架构选错了。
HookCoordinator 加 onSessionStopped,收工那一刻直接叫醒盯梢;
事件驱动这条路不需要反复确认(Stop hook 已经是确凿证据),
只等 12 秒让用户有插话的余地。轮询保留作兜底 —— Stop hook 没装、
或者会话被别的方式打断时还得靠它。
## 一个项目会开很多会话
probeIndex 原来按项目计数,同项目的多个会话共用一个游标 ——
每个会话只拿到清单的一个片段,跳着问的半截,没有一个能被完整问一遍。
改成按 sessionId。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 15:58:39 +08:00
wuguangan and Claude Opus 5
1c4cf0ff5e
fix: 追问显示成别的项目的;手改清单被 app 盖掉
...
用户从界面上看出来的两个问题,都是真的。
## 上次追问显示的是别的项目
取的是 history.first(全局最近一条),于是 agentx 的页面上显示着发给
demo-android 会话的追问。NudgeRecord 加 projectPath,按项目过滤。
## 手改的清单被盖回去
我往 watchdog.json 写了 47 条,app 用内存里的 25 条盖了回去。
**这个 bug 我今天早上在 AcceptanceStore 上修过一次**(写盘前先重读),
当时没想到 SessionWatchdog 有一模一样的一份。
merge 只捡清单不捡 watching:watching 是界面开关,内存里的才是用户最新
意图,合并的话「关掉盯梢」会被盘上的旧值又打开 —— 开关静默失灵。
这一条是 testCanStopWatching 抓到的,不是我想到的。
455 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 15:55:47 +08:00
wuguangan and Claude Opus 5
9ba277ccaf
fix: 修回自己改掉的 pid 和进度计数
...
两处都是我改别的东西时顺手删的 —— 正是刚提醒 agentx 那边
「别为了改一处顺手把好东西改坏」的同一个错,当场自己犯了一遍。
- 找不到 pane 时把 pid 带回来:没有它就没法人工核查是哪个进程够不着。
我为了区分「故障」和「正常情况」改措辞时删掉了。
- 追问带回「第 N/M 条」:看不出问到第几条、总共几条,就没法判断
它是不是在原地打转。改成自动生成追问时换格式删掉了。
455 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 15:51:19 +08:00
wuguangan and Claude Opus 5
9cd6f7d331
fix: 分不清「停了」和「命令跑得慢」;它提问时不只是踢回球
...
## 画面在变就别打扰
原来只看转圈符号。但慢命令跑起来时状态可能是 shell、转圈行可能被滚屏顶掉,
而输出一直在往外吐 —— 会被误判成停了,然后在它跑到一半时插一句话。
改成对比上一轮的画面:不一样就说明有东西在动,不管状态字段说什么。
这比"有没有转圈符号"可靠得多,因为它不依赖任何特定的显示形态。
## 它提问时顺带派活
原来只回一句"自己判断着办" —— 那是把球踢回去,它下一轮很可能又来问一次。
现在从验收清单里挑一条还没做的一起带上,让它有明确的下一步。
455 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 15:45:42 +08:00
wuguangan and Claude Opus 5
929f1ab06e
fix: 盯梢照稿子念,不看对面说了什么
...
它问「要我从第 1 步开始吗?」时,盯梢照常甩出轮到的那条追问,
问题被直接埋掉 —— 它在等的决定没人回,用户看到一句驴唇不对马嘴的话。
现在发之前先看最后一句是不是在提问。是的话不发清单里的问题,
改成回应它并把决定权交回去(原问题原样带上,用户能在历史里看到它问过什么)。
判据刻意保守:只认最后一个非空正文行以问号结尾,中间的反问句不算 ——
误判会让它正常干活时也被插一句废话。同 StallDetector 的窄启发式,
不另发明一套。
455 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 15:38:49 +08:00
wuguangan and Claude Opus 5
5b4293b54c
feat: 追问自动从验收清单生成,不再只问通用问题
...
手写的通用清单问几轮之后就被摸清套路了,答案越来越像模板
("落到哪些文件了" → 每次都能编出一份路径)。而清单里明明躺着
18 条待验收、7 条存疑 —— **那些才是该问的具体问题**,
而且每一条都问不出模板答案。
generatedProbes 当场从 ledger 生成:
存疑:「你说做完了『X』,但真实 diff 里找不到对应改动(复核说:...)。
具体是哪个文件的哪几行实现的?找不到就直说没做。」
待验收:「『X』这条做了吗?验收条件是:Y。做了给文件路径和行号。」
存疑的排最前 —— 那一档是「自报做完但 diff 里找不到」,
整个清单里最该当面对质的一类,排后面会被 12 条的上限挤掉。
通用和清单生成**交替**:只问清单会让它一直在对答案,
只问通用又摸不到具体的漏项。
界面上两个数字分开显示("通用 8 条 + 清单 25 条 · 交替问")——
用户要能看出这是两回事,否则改了文本框却发现问出来的对不上,会以为坏了。
已定论的、已标「疑似误拆」的都不再问。超长要点截断:
注入那层有单行和长度限制,整段贴进去会被拒发。
450 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 12:19:50 +08:00
wuguangan and Claude Opus 5
a66f8a6ca9
fix: 盯梢记着"追问了",终端里一个字都没有
...
同一个 shell 误解的第二份副本,在另一个文件里。
TerminalReply.send 里有 guard !session.status.isWorking,而 isWorking
包含 shell。那个判断对它本来的用途(答终端弹出的权限对话框)是对的 ——
会话在忙时往输入框敲字会串进别的地方。
但对盯梢来说 **shell 恰恰是最该催的状态**(挂着后台 shell、人却停在提示符
上)。于是每次追问都被静默挡掉返回 .sessionBecameBusy,而我把返回值扔了
(`_ = await reply.send(...)`),历史里记着"已追问第 1/25 条",
终端 300 行历史里那句话出现 0 次。
我在 PaneActivity 里修过这个误解,却没想到 TerminalReply 有自己的一份。
改法:盯梢走自己的注入路径(它本来就已经定位到 pane 了),
并且**把结果记进心跳** —— 原来扔掉返回值才是根因,
不知道成功失败的操作等于没做。
心跳现在长这样:
追问 xxx 第 1/25 条 —— 已送达
追问 xxx 第 1/25 条 —— 敲字失败:can't find pane
独立验证过两步注入在真实 pane 上有效(先 send-keys -l 文字、
停 0.5 秒、再 Enter;合在一起时终端偶尔把回车吃进上一段输入)。
441 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 11:36:23 +08:00
wuguangan and Claude Opus 5
f37e39ba69
fix: 盯梢三处静默失效 —— 定位、落盘、可观测性
...
实机上盯梢整整 83 分钟一次都没催,用户先发现的,不是程序。三个 bug
互相掩盖,而且我用其中一个坏掉的东西去诊断另一个,得出了错误结论。
## 1. locate() 返回的是标签,不是 pane 目标
TerminalReply.locate 给的是 "tmux · hub:agentx-dev-kit"(给人看的),
我拿它去 capture-pane -t 必然失败 —— 而失败是静默的:抓到空字符串,
代码接着往下判断。改成从 pid 往上找真正的 pane(%15),
并且**抓不到画面时明确跳过**,不再拿抓取失败当结论。
## 2. history 从来没落盘
我读文件看到"追问 0 次"就下了结论 —— 但那个数字本来就永远是 0。
用一个坏掉的仪表去判断另一个坏掉的东西。追问后补 persist()。
## 3. 没有心跳,"死了"和"活着但还没到时候"长得一模一样
都是没有任何输出。昨晚一个死掉的 bash 盯梢挂了快 4 小时没被发现,
就是因为分不清这两种。
心跳写文件不写日志:本机实测 log show 完全看不到这个 app 的输出
(连启动那条都没有)。**一个看不见的仪表比没有仪表更糟**——
我拿它做过诊断。
心跳内容是"这轮为什么没催",可能值全部拆开:找不到 pane / 抓不到画面 /
不打扰(原因) / 第 N/2 次确认 / 冷却期内 / 刚追问了第 N/M 条。
下次失灵时是哪一环坏的会直接写在文件里。
顺带:心跳原来只记最后一个会话的结果,一个残留的死会话会把真正在跑的
那个整个盖掉 —— 第一条心跳就撞上了。改成每个会话各记一句。
441 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 11:14:21 +08:00
wuguangan and Claude Opus 5
415f2e7caf
feat: 会话观察者做进 Hub —— 停了就按清单追问
...
原来是个飘在后台的 bash 脚本:关机就没了,也没人知道它在不在跑。
现在是 app 的一部分,配置落盘、开关在界面上、每次追问都留痕。
## 它解决的不是「停了」,是「做浅了」
只会说「继续」的盯梢防不了它每次都继续、每次都做表面功夫,最后交一份
看起来很完整的报告。所以追问是一份轮换的清单:落到哪些文件了、
发布后还在不在、有哪些卡点没解决、勾了框但没动代码的直接说 ——
每次问一条,问完从头再来。默认清单有一条测试专门守着"不能只有继续"。
## 三条硬约束,都是实机撞出来的
1. 只有 busy 算在干活。shell 表示"挂着后台 shell",和 Claude 在不在思考
无关 —— bash 版把它当成在干活,会话停了 20 分钟一次没催到,
是用户先发现的,不是程序。
2. 用户在输入框里打字时绝不发。注入是往当前光标位置敲字,会拼到他没发完的
那句后面 —— 真发生过,把用户的话和追问接成了一句。
3. 发送分两步(先文字、停一下、再回车)。合在一起时终端偶尔会把回车吃进
上一段输入,消息卡在输入框发不出去,看起来催过了其实一个字没送。
前两条的判断抽成了 PaneActivity 的纯函数,16 条测试。这是整套里唯一
容易写错、而且写错了不报错只静默失灵的部分 —— 转圈检测刻意只匹配
"(数字s · "这个计时格式,不匹配 Wandering/Boondoggling 那些动词:
那是一份永远补不完的名单,漏一个就是一次误催。
排队提示("Press up to edit queued messages")不算草稿 ——
当成草稿的话一旦有排队盯梢就再也不敢发言,而排队恰恰说明它正忙,
忙完就会读,这时候补一条完全没问题。
截图模式下不启动:那会往演示会话里敲字。
441 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-08 02:50:15 +08:00
wuguangan and Claude 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
wuguangan and Claude 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
wuguangan and Claude Opus 5
b8520c3b14
fix: Claude 勾掉的框不算证据;一条 todo 里的多件事要拆开标注
...
两条都来自用户的观察,两条我都犯了自己一直在防的错误。
## 勾掉 ≠ 做完
先前把 Claude 勾了 completed 的 todo 直接记成 .confirmed(已验收),
理由是"勾状态是它边干边写进文件的,比嘴上说可信"。这个理由站不住 ——
它勾自己的框和它嘴上说"做完了"是同一性质的东西:都是声明,都没有外部证据。
拿它自己勾的框当证据,正是这整个功能存在的理由所要防的事。
改成落到 .claimed(待复核),证据记成 .claimed(...)(isProof = false),
由旁路复核拿真实 git diff 判 confirmed / disputed。
用户的原话:「他勾掉的到底有没有做」—— 只有 diff 能回答。
顺带补上 baselineCommit:缺了它复核只能拿未提交的改动去比,结论全是噪音。
## 一条 todo 里常常不止一件事
「批次2:设备自动连接(Android 地基修复→状态机→UI→iOS)」是四件。
整条勾掉时里面做了三件还是四件,从条目本身看不出来 ——
而漏掉的往往正是最后那件。
拆分是机械的(按 →、;+/ 等明确分隔符),不调模型:这一步要对每条 todo
都跑,而它要解决的问题很窄。切不出来的("深色模式双端"其实是 iOS+Android)
就不切 —— 切错比不切更糟,用户会对着一堆碎片然后不再相信这个标注。
界面上:行内直接显示「2/4 项」,展开逐项勾。藏在展开区里等于没标,
用户不会为了看有没有分项去逐条展开。
全部分项勾完**不自动**把整条标成已验收 —— 那等于绕过"拿 diff 核对"这一步。
修 bug:标题前缀门槛原来是 12 字,把「把整个权限体系重构一遍:改A、改B」
的前半句当标题剥掉了。中文里 11 个字是实打实的内容,压到 6 字。
411 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 22:51:03 +08:00
wuguangan and Claude Opus 5
920ad7505f
feat: 追踪 AI 自己答应的事,并能推回对应 session
...
用户一眼看出的缺口:清单里全是他说过的话,**AI 自己列的 todo 一条都没有**。
我先前的判断是"Claude 的 todo 不能当基线"——这没错,翻译那一步就已经丢
东西了。但我顺手把它整个排除了,而「它自己列了 6 项做完 3 项就说完事了」
恰恰是遗漏最直接的证据,且这类承诺不在用户原话里,别处根本抓不到。
现在收,但单独标成「AI 计划」,Origin.isFromUser 把"用户的权威基线"和
"待核对的 AI 自述"在类型层面分开 —— 混成一档就等于把自述当成了基线。
- TaskStateReader.readTasks 逐条读 ~/.claude/tasks/(零成本,不调模型)。
原来只给计数,而验收要的是"没做的那几项分别是什么"。
- Claude 自己勾掉的跟着同步成已确认(否则永远挂在未验收,用户手动清不动,
清不动的清单最后没人看)。只动 .assistantTask 那一档:它勾掉自己的 todo
不代表用户提的需求做完了,同名时尤其危险("发布新版本")。
- 按**会话**统计而不是按项目:用户问的是"这一轮它答应的做全了没"。
- 「告知这个会话」走 TerminalReply.send 把未做项注入回那个终端 ——
不是发通知让用户自己切过去,是直接把话说到那个 Claude 面前。
实测本机某会话读出 6 项完成 + 1 项 pending「真机验证」——
正是典型的遗漏形态:活干完了,验证挂着,而它已经说"完成了"。
401 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 22:31:44 +08:00
wuguangan and Claude Opus 5
f1cdcd2547
feat: 验收清单行内直接确认/划掉
...
原来只有展开单条才有确认按钮。一份清单几十条,逐条展开再确认根本清不动 ——
清不掉的清单会一直堆着,堆到没人看,那时这个功能就等于不存在了。
按钮放在可点击区域**外面**:套在同一个 onTapGesture 里的话,
点「确认」会顺手把行也展开。
展开区里原来的接受/划掉去掉了,只留删除 —— 同一个动作在两处出现,
用户会以为它们不是一回事。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 21:03:01 +08:00
wuguangan and Claude Opus 5
78d24c934f
fix: 加字段会把用户已有的验收清单整个抹掉
...
给 AcceptanceLedger 加了非可选的 authorizedCommands 之后,所有已存在的
清单文件当场解码失败 —— Swift 合成的 Codable 要求非可选字段必须存在。
而读取是 try? 静默吞错误的,于是下一次写入从空清单开始,把内容整个覆盖掉,
全程无声无息。
真丢了数据:本机 9 个项目里,claude-hub 的 13 条和 storefront-a 的 1 条没了,
另有 demo-release 的 7 条正等着被同样地清空(赶在它之前修好了,
实测那 7 条经历一次真实写入后完整存活)。
我在 HookEvent 那边特意把新字段都设成可选并写了兼容性测试,却没对持久化的
ledger 做同样的事。而 ledger 的后果严重得多:hook 事件丢一条无所谓,
用户攒几周的清单没了是真损失。
三层修复:
1. 手写 init(from:),每个字段 decodeIfPresent —— 加字段不再让老数据全废。
2. 逐条解码要点,坏的跳过。原来一条坏的会让整个数组抛错,整份清单报废。
同 TaskStateReader 的处理:边跑边写的数据撞上半条是正常情况。
3. **解不出来的文件先改名保住,绝不静默覆盖。** 丢数据和读不出来是两个
量级的事故 —— 后者还能捞回来。
391 个测试,三条都有对应的回归测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 20:33:55 +08:00
wuguangan and Claude Opus 5
a5ba3f7573
feat: Hub 自己跑验收命令 —— 实际功能证明
...
阶段 4 的教训直接催生了这一步:diff 复核把 `case ran(command:exitCode:)`
这个**枚举声明**当成了功能已实现,而没有任何代码产生它。读代码判断不了
"功能是否真的能用",只有跑一遍能。
执行者必须是 Hub 而不是 Claude —— 让它跑完再转述,就又回到了「说说而已」,
转述那一步正是失真发生的地方。产出 Evidence.ran(isProof = true)。
三道闸,全部有测试,元字符那道做过变异验证:
1. 白名单:只放行构建/测试类。刻意排除 npx / pnpm dlx(会下载执行任意包,
放行它等于白名单形同虚设)、npm install(跑 postinstall 等于执行任意
代码)、git 的写操作。bash 限定在项目的 scripts/ 下。
2. 元字符:16 个字符逐个测。**这道检查必须在白名单之前** ——
变异验证时把它挪到后面,`swift test | tee /tmp/x` 立刻因为前缀匹配上
`swift test` 而被放行。配合 Shell.run 走 execve 数组参数(不是 sh -c)。
3. 总开关默认 off + 逐条授权。授权按命令原文精确匹配,改一个字要重新授权;
而且手工往 authorized 里塞危险命令也绕不过前两道(单独一条测试守)。
顺带修了注入正文太长:真实项目上拦了 12 条带完整验收条件,糊了满屏。
要点一多就没人看,反而比不拦更容易被忽略。上限 6 条,存疑的排前面,
省略了多少明确说出来。
Shell.run 加 currentDirectory —— 验收命令必须在项目目录里跑,
否则 swift test 找不到 Package.swift。
388 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 19:37:45 +08:00
wuguangan and Claude Opus 5
d93a624b4e
feat: 旁路 diff 复核 —— 拿真实改动核对它的自报
...
自查治「忘」,但自查仍然是它在说自己,治不了「骗」,也治不了
「它真心以为自己做了」。这一层读 git diff 里到底有没有对应改动。
结论只有 confirmed / disputed 两档:中间档会变成模型拿不准时的垃圾桶,
而用户看到"部分完成"也不知道该做什么。
新增 GitDiff(numstat/patch/balancedPatch)和 AcceptanceAuditor。
UI 上点 .diff 证据能看那个文件从基线到现在的真实 diff。
三次真机实验(一条真做了的 + 一条我故意谎报的),前两次都判错,
两个 bug 都只有在真机上才暴露得出来:
1. 整份 diff 截断 → git diff 按字母序输出,前几个文件吃光 24KB 预算,
MainWindow.swift(真改了 79 行)一行都没进模型眼里,于是判"存疑,
实际 diff 未展示"。任何真实规模的改动都会这样,不是调大上限能解决的。
改成按文件均分预算(balancedPatch)。
2. 均分之后每文件只有 1200 字符,只露出第一个 hunk,对应那条要点的改动
在后面 —— 同一类问题降了一层。提到 120k 总预算 / 6k 每文件。
省这点 token 换来的是不可信的结论,而不可信的结论比没有结论更糟:
用户会照着它去改本来没问题的东西。
3. 谎报的那条被判成 confirmed,理由是"Evidence.ran 真实执行命令"——
模型把**类型声明**当成了功能存在。而 .ran 只是个 enum case,
没有任何代码产生它。声明读起来非常像功能已经实现,这是 diff 复核
最容易翻的一类车。提示词里单独加了一条。
第三次两条都判对,理由精确到代码位置("SessionsPane 会话行新增
Button(\"清单\") 调 onOpenLedger" / "仅见 Evidence.ran 声明,
未见实际执行验收命令并填充 exitCode/tail 的调用点")。
**但这仍然是一个弱信号,不该被当成结论。** 第 3 个 bug 恰好证明了
读 diff 判断不了"功能是否真的能用"——这正是阶段 5(Hub 自己跑验收命令)
存在的理由。
369 个测试。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 19:10:05 +08:00
wuguangan and Claude 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
wuguangan and Claude 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 and Claude Opus 5
61aaea6062
fix: 关掉「跟随光标」后灵动岛仍然跟着光标跑
...
加这个设置时只改了三处调用 activeScreen() 的地方,漏了控制器里的
followCursorAcrossScreens() —— 它直接调 screenUnderCursor(),名字对不上
所以 grep 没找到。用户在界面上关掉跟随,岛照样跑到外接屏压住浏览器标签栏。
除了补 guard,把 NotchGeometry.screenUnderCursor() 收成 private:
绕过设置去取屏现在在编译期就不成立,而不是靠下一个人记得加判断。
新增的那条测试特意标注了它的局限——检出能力取决于跑测试时光标在哪块屏,
单屏机器上永远绿。真正的护栏是 private 那条,别把它当保证。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com >
2026-08-07 17:49:57 +08:00
wuguangan
aa9330afed
chore: 版本号 2.3.0
2026-08-06 17:20:16 +08:00
wuguangan
0b4cec785b
fix: 灵动岛默认固定在刘海屏,跟随外接屏改成可选
...
岛以前写死跟着光标跑。动机没错(用户在外接屏工作时看不见岛),但代价没算清:
外接屏没有刘海,岛走「悬浮胶囊」形态,正好压在浏览器标签栏那一带 —— 命中测试
虽然是按形状精确做的,但那块形状底下本来就有东西,于是外接屏上点标签页点不动。
这不是 hit-test 的 bug,是岛出现在了一个它不该占位的地方。所以默认改回固定在
刘海屏,想跟随的人去设置里开,并把代价写在开关下面。
- 新增 IslandPlacementStore,落 island-placement.json,URL 可注入(测试不碰真实设置)
- NotchGeometry.activeScreen() 改成显式的 screen(followsCursor:),不再藏全局行为
- 设置面板加「灵动岛位置」卡片
- relocate() 不复用 realign():后者有 geo != geometry 的去重 guard,两块同型号
外接屏时会让手动切设置看起来没生效
7 条新测试,其中默认值那条做过变异验证(改成 true 立刻红)。全量 264 绿。
2026-08-06 16:34:38 +08:00
wuguangan and Claude 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
wuguangan and Claude 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
wuguangan and Claude Opus 4.6
a3f707d629
docs: 重写 README 为开箱即用级别完整文档
...
- iTerm2 配置:精确到选项名 + 解释为什么需要每一项
- macOS 权限:说明 osascript 首次运行需授权 + 补救步骤
- 通知机制:完整链路图(Claude → hook → 通知/语音/面板/信号)
- 故障排查:通知不工作、标签页异常、权限报错、编译失败、浏览器连接
- 补充:cc.sh 路径限制、Chrome >= 144 版本要求、磁盘空间提示
- 补充:macOS 内置工具说明、sounds 目录、Rust 源码目录结构
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-31 09:53:50 +08:00
wuguangan and Claude Opus 4.6
669c58f0e6
docs: 完善新电脑安装文档,补充 iTerm2 配置和依赖说明
...
- 新增 7 步完整安装流程(从 Homebrew 到启动)
- 补充 iTerm2 tmux -CC 模式关键配置说明
- 补充 terminal-notifier、Node.js 等依赖
- 新增前置条件汇总表
- 功能列表补充用量统计
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-31 09:45:24 +08:00
wuguangan and Claude Opus 4.6
b008172ace
docs: 更新 README,补充新电脑安装说明和用量统计功能
...
- 前置条件:说明 Rust 自动安装、Apple Silicon/Intel 均支持
- 通知面板:补充用量统计、编译机制、cc 别名说明
- 目录结构:补充 build-app.sh 和 cc.sh
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-26 14:19:23 +08:00
wuguangan and Claude 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
wuguangan and Claude 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
wuguangan and Claude Opus 4.6
0bca945994
docs: 补充通知面板和浏览器自动化文档到 README
...
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-18 18:51:13 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 4.6
fe98293154
fix: browser-auto skill 自动配置 .mcp.json
...
AI 首次使用浏览器时,直接检查并写入 chrome-devtools 配置,不问用户
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-17 14:39:28 +08:00
wuguangan and Claude Opus 4.6
92650e8843
fix: browser-auto skill 强制连接用户日常 Chrome
...
核心问题:chrome-devtools-mcp 默认启动独立 Chrome,和 Claude in Chrome 不在同一个浏览器
- 顶部加铁律:永远不开新浏览器
- 说明 --connect-url=http://localhost:9222 的正确配置方式
- 新项目配置检查清单
- Chrome 调试端口检测作为启动流程第一步
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-17 13:40:37 +08:00
wuguangan and Claude 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
wuguangan and Claude Opus 4.6
5db1bc1822
feat: Dashboard 项目列表 + 交互
...
- 启动时从 projects.yaml 加载所有项目
- 运行中的项目(有 tmux 窗口)绿色高亮,点击跳转 iTerm2
- 未运行项目灰色,点击弹出菜单(Claude Code / 终端 / Finder / VS Code)
- 每 5 秒自动刷新项目状态
- 新增 projects.rs:读取 yaml + 检查 tmux + 打开项目
- 添加 Tauri capabilities 配置
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com >
2026-03-17 12:26:01 +08:00
wuguangan and Claude 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
wuguangan and Claude 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
wuguangan and Claude 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
wuguangan and Claude 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