main
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |