跳转至

HT002 · 装到 macOS 上跑起来

2026-09-07,任务是一句话:cppide clone 下来, 在这台 macOS 上装好、跑起来。 结果是 $38.24 / 4 步 / 约 1 小时, 程序确实在 macOS 上跑起来了(PID 96040,arm64 Mach-O), 而判定结论是无法达成,浮上来问人,人选择接受。

这是第一次带目标看守的运行 —— 从 HT001 教训里改出来的东西,当天全部第一次上真实 API。 它救下了一个 HT001 栽过的坑,同时造出了一个方向完全相反的新失败: git clone && make && ./cppide 做成了一小时。 这一页最有价值的部分是后者。

原始记录在 human-test/HT002/, 账目由 runs/manifest.jsonruns/sessions.db 算出:

python tools/analyze_run.py human-test/HT002/runs

这一跑的形状

任务 clone cppide,在这台 macOS 上装好、跑起来,人能实际打开界面用
花费 $38.24 —— 4 个步骤,明细见下
时长 约 1 小时(合计 0.97h)
轮数 70(四步 num_turns 之和)
主线程上下文 36.8K → 80.2K,在步骤边界断崖回落,不是单调增长
分工 6 个 subagent;协调者 7 次工具调用 vs subagent 212 次
缓存 命中 90.8%
工作台 5 份产出、4 份笔记、11 份落盘件、0 个可复用脚本
结果 跑起来了(PID 96040,arm64 Mach-O);判定无法达成,人选择接受
版本 第一次带目标看守的运行

每一步的花费、轮数和时长

步骤 花费 轮数(num_turns) 时长
确认需求 $0.5306 9 0.10h
设定目标 $0.4117 12 0.05h
干活 $35.8950 12 0.73h
干活·判定#1 $1.4037 37 0.09h
合计 $38.2409 70 0.97h

时长那一列是各步骤四舍五入之后相加。按原始记录复算,manifest.json 里四个 duration_s 精确合计是 3471.53 秒 = 0.9643h;本页沿用 0.97h,两者差 0.006h。

钱几乎全在"干活"那一步 —— $35.8950 占了 93.9%。 下面第一节解释这笔钱花在哪,以及为什么它本不该这么花。


一、头条:它把 git clone && make && ./cppide 变成了一小时

这是这次最重要的发现,比下面所有"机制跑通了"都重要。

设定目标那一步,判定者给自己写了 15 条判定清单 (原文在 .flower/notes/ 下的 目标.md)。 按性质分:

类别 条数 例子
真正验证"东西能用" 约 5 构建 exit 0;进程存活 ≥30 秒;重启命令在新 shell 里可重跑
验证"过程守规矩" 6 ~/.zshrc / .zprofile / .bash_profile 的 mtime 都早于本次会话;没跑过 brew install / npm -g / sudo;.flower/ 的 mtime 没变、runs/ 没被人为增删
原理上验不了 4 截图并看图;devtools console;人工点击;本人确认

只有三分之一在验"这东西到底能不能用"。 有六条在验它自己有没有守规矩 —— 其中一条查的是 .flower/ 目录的 mtime,那是框架自己的目录

那条查 .flower/ 的清单,自己把自己判失败了

这不是理论问题。判定那一轮,判定者在"未通过"里如实写了:

.flower/ 下文件 mtime 均早于会话开始 —— 字面失败:INDEX.md 16:14、 artifacts/01–05(15:40–16:12)、notes/决策-项目形态与验收路径.md 15:43 均在会话内写入。

原因是:框架硬性要求把长产出写进 .flower/artifacts/、把决策写进 .flower/notes/ 判定清单里那一条要求这个目录别动。两条规则直接对撞,而且是判定清单自己制造的 —— 执行者一份长产出都没写,反而更合规。

它没有蒙混过去:干活那一步在最终报告里主动列了这处冲突, 决策记录里也写了处理方式 —— 已有文件一个不动 (需求.md目标.md问答记录.md 三份 mtime 与 MD5 前后一致),只新增。 判定者复核后同意"这条要防的篡改没有发生",但仍按规则把它列进未通过,交给人定。

根因一:边界被当成了判定项

前置确认阶段问出的边界("只能在项目目录内装""业务代码别动") 本来约束的是怎么干活,结果被写成了验证产出的检查项。 于是就有了查 .zshrc mtime、查 .flower/ mtime 这种条目。

边界 ≠ 判定项。边界约束过程,判定项验证结果。 混掉之后,每加一条边界就等于多一条判定, 而边界恰恰是在确认阶段被鼓励写满的 —— CLARIFIER_RULES 明写着 "明确不做什么。这一段会管住后面每一个干活的人"。

对照原始记录可以看到这条放大链的每一环: 需求确认书的边界写的是"不碰当前目录下已有的 .flower/runs/", 到了判定清单就升级成".flower/ 下文件的 mtime 均早于本次会话开始" —— 从"别动已有的"变成了"整个目录不许有新文件"。

根因二:JUDGE_RULES 只有一个方向的压力 —— 而那是当天加的

当天早些时候,从 HT001 的教训里往 JUDGE_RULES 写了这些:

  • "每条都必须能当场验证""缺了的、含糊的,你补齐"
  • "判的是产出物,不是源码"(带 macOS 那个反面例子)
  • "在当前环境里验不了的条目,当场标出来"
  • "先看清楚你在哪(可以 uname -a)"

每一条都推向更多检查、更严格。整份规则里没有任何一句说"要和任务的规模相称"。

也就是说:照着 HT001 那个"过度宣称通过"的失败去优化,造出了完全相反的失败 —— 过度检查。 这一条是自己造的,不是模型的问题。

根因三:和 issue #3 叠加了

真正卡住的是 macOS 上两处可移植性缺陷:

  • src/proc.cpp::sigemptyset,而 Apple SDK 在函数原型之后又把它定义成函数式宏 (只被 #ifndef _ANSI_SOURCE 守卫),于是 ::sigemptyset(&s) 展开成 ::(*(&s)=0,0),语法错误
  • src/ui.cpp::_exit 却没 include <unistd.h>

三行就能改完。 但用户在确认阶段答过"业务代码别动",agent 严格执行, 于是绕道去试编译 flag、派 subagent、写决策记录。 如果他当时能插一句"那两行改了没关系",十分钟就结束了。 边界写成禁令会被最大化严格解释,而中途没有任何通道能松绑。

原始记录里能看到这条绕路有多长:总共四个阻断点, 其中只有 ::_exit 那个能被纯编译 flag 解决;::sigemptyset 那个试了 6 组 -D/-U 组合全部失败 (_POSIX_C_SOURCE 系列还会连带弄坏 st_mtimespecO_CLOEXEC)。 这条死路本身产出了一份 13.6K 的可行性报告。

但要公道:边界逼出了一个更好的解法

最后的方案不是改源码,是在 Makefile 里生成一个只做 #undef 的垫片头-include 强制前置:

$(MACSHIM):
    @printf '#include <signal.h>\n#undef sigemptyset\n#undef sigfillset\n...' > $@
CPPFLAGS += -include $(MACSHIM) -include unistd.h

这比改源码更好 —— 上游仓库一行不用动,任何人 clone 下来都能构建。垫片生成到 build/、不入库, clean 里加 rm -rf build 负责回收,make -n UNAME_S=Linux 实测展开后没有 -include,Linux 路径未被污染。 最终 git diff 只有 Makefile 一个文件,+14 / −2

所以边界产生的是更高成本的更优解。问题不在于它做错了, 在于这个取舍没有让人来做 —— 而做不了的原因正是 issue #3。

为什么 git status 有 64 条却说源码零改动

cppide 仓库把 30 个 .o、30 个 .d 和 2 个二进制提交进了版本库,而且是 Linux ELF aarch64、 mtime 还晚于 .cpp —— 不先 make clean,make 会跳过编译直接拿 ELF 去链接,必挂。 所以 git status --porcelain 的 64 条里,63 条是被 git 跟踪的编译产物加一个 ?? build/。 按扩展名过滤后 git diff -- 'src/*.cpp' 'src/*.h' README.md config.sample.json | wc -c0。 这个精确说法是合规审计逼出来的 —— 协调者原本想笼统写"src/ 零改动",审计指出那句话不成立。


二、目标看守做对了什么

机制侧全部跑通,而且当天那两处改动在产出里留下了直接指纹

它写的判定清单第一条:

uname -a 显示 Darwin 25.2.0(macOS)……下面所有"跑起来/打开"的条目必须是在这台机器上 实跑得到的结果,不接受"源码里有 macOS 分支所以应该能跑"这类推断

那正是当天早上从 HT001 教训写进 JUDGE_RULES 的话。

它三次用了当天加的 [此环境无法验证:…] 标记,各自给了理由 —— devtools console(没有可驱动的浏览器,而为此装无头浏览器越过了边界)、 人工点击(需要人在屏幕前)、本人确认(只能由他给出)。

判定者的第一句话是"我不看那段回话下结论。去现场。" 然后:

file cppide        → Mach-O 64-bit executable arm64
lsof -p 96040      → txt 指向 /Users/hechenyu/explore/test-ide/cppide/cppide
                     起于 16:10,16:15 仍活着

它自己写道:"meta 教训里那个'交付 Linux ELF'的坑,这次没踩。 一个 Linux ELF 不可能在 Darwin 上跑成 PID。这不是从 Makefile 里推断出来的。"

它还核了源码确实没动:src/ 下所有 .cpp / .h 的 mtime 全是 15:32(clone 时刻), 改的只有 Makefile(16:06),.o / .d(16:07)是被跟踪的编译产物重建。

它也如实交代了自己够不着的地方。 判定者的 Bash 被限制在工作目录内, git ls-remote 的比对和 ~/.zshrc 的 mtime 它跑不了,只能采信独立审计的输出 —— 这两条它单列出来说明"我没能亲手复跑"。

三、判定"无法达成",然后问人

三态判定加人工介入,整条链路第一次真实跑通。

判定结论是无法达成,卡在截图那一条:screencapture -x-l <window-id> 都返回 could not create image from display —— 屏幕录制的 TCC 权限没授予, 而授权需要人去系统设置里点,那本身又是被边界禁止的系统改动。

"再派一轮 subagent 也变不出这张图。按规则这不许判通过,也不该判'未达成', 它就是无法达成,该停下来交给人。"

于是清单里"原理上验不了"的条目从 3 条变成 4 条 —— 15 条里有 4 条在这个环境根本验不了, 而这件事是在花掉 $35.8950 的干活和 $1.4037 的判定之后才发现的。 设定目标那一步只花了 $0.4117,当时就该发现。

框架把这个结论问给人(记在问答记录 q7),三个选项,人选了"接受这个结果",gate 放行,流程完成。

它还拒绝了一个替代品。 执行者用 AppleScript 读回了终端窗口文本, 是真实渲染的 TUI —— 行号栏、src/main.cpp 正文、─[编译]─[运行]─[AI*]─[输入]─ 面板栏、 练习模式 │ main.cpp │ 1:1 │ C++ 状态栏,不是白屏也不是报错屏。判定者说:

"干活的人没有拿它冒充'看图',这个自我克制是对的。 但它替代不了清单要求的那条,我也不替他算过。"

"披露了风险"不等于通过

HT001 栽的就是这个:它把"macOS 一次都没跑过"写进了交付说明,然后仍然判验收 1 通过。 HT002 里判定者拿到了一个更好的替代证据(真实 TUI 文本),仍然拒绝拿它顶替清单条目。 这个区别是判定三态设计的全部意义 —— UNREACHABLE 不是 NOT_YET 的委婉说法,它是"该停下来问人"。

四、allowed_tools 不是硬白名单

分析这次运行时发现一个和主张不符的事实。按会话拆开看工具调用:

会话 实际用了 白名单里有什么
确认需求 WebFetch、Glob、ask×6 clarify() 当时只给 ask + Read/Glob/Grep
设定目标 Bash ×11 judge() 默认 can_run=False,没有 Bash
干活·判定 Bash ×31、WebFetch 同上
干活(协调者) Bash ×1、Agent ×6 正确

goal_step 没有任何传 can_run 的代码路径,所以"设定目标"那一行不可能是配置问题。

$0.1 的探针验证:给一个 allowed_tools=["Read"] 的 agent 让它写文件 ——

Write → 权限层:"requested permissions to write ... but you haven't granted it yet"
Bash  → 路径安全:"Output redirection was blocked. For security, Claude Code may
        only write to files in the allowed working directories"

模型能调用不在白名单里的工具,只是被 permission 层和路径安全挡下了。所以:

  • allowed_tools免审批清单,不是排他白名单
  • 当时真正保护 clarify() / judge() 的是继承来的 permission_mode="default"
  • 而它俩 delegate_only=False没有 delegate_guard 那道 hook。 协调者有 hook 护着,这两个角色当时没有任何 flower 自己的机制

clarify() 的文档字符串写着"不给写工具 —— 它没法开工", 那个 $0.8908 的反面实测整个是为这句话做的。这句话的机制依据是错的。

这一节的两个事实后来都变了

whitelist_guard 已经无条件接上了:delegate_only=False 的角色现在由 Runtime 自动装这道 hook, clarify() / judge() 不再"没有任何机制"。另外 WebFetch 现在就在 clarify() 的白名单里, 所以表格第一行那个例子已经作废 —— 结论仍然成立,但它现在只靠 $0.1 探针那条证据。 详见本页最后一节。

五、上下文曲线:步骤边界会重置

第 1轮   36.8K
第25轮   80.2K   ← 峰值
第26轮   32.9K   ← 断崖:新步骤 = 新会话(resume_from=None)
第121轮  64.8K

四个采样点里有三个(第 1 / 25 / 121 轮,36.8K / 80.2K / 64.8K)与原始记录逐一吻合; 第 26 轮那个 32.9K 复算下来对不上:按 store_key 排序重放主线程 assistant 消息, 第 26 条(新会话的第一条)的 input + cache_read + cache_creation 是 31,972 token = 32.0K, 而 32.9K 是同一会话的第二个取值(31,970 + 948)。本页保留 32.9K —— 差的是 0.9K, "断崖回落"这个结论两个值都成立。

HT001 是单调涨到 185.9K —— 一个步骤跑了 10 小时。 HT002 因为分了四步,每步开新会话,上下文被结构性地重置。 这是"需求确认书和目标是冻结件、下游只拿文书不拿对话"那条设计的直接效果 —— 曲线上看得见。

缓存命中 90.8%(HT001 是 96.1%)。命中率随会话长度上升,所以短会话反而单位更贵一点 —— 这和 HT001 第三节的结论是同一枚硬币的两面。

六、和 HT001 对照

HT001 HT002
任务 从零写一个终端 IDE 把它装到 macOS 上跑起来
花费 / 时长 $171.62 / 10.44h $38.24 / 0.97h
步骤 2(无目标看守) 4(含设定目标 + 判定)
subagent 23 个 6 个
协调者工具调用 32 7(其中 6 次 Agent)
subagent 工具调用 1,893 212
主线程上下文 28.7K → 185.9K 单调涨 36.8K → 80.2K,步骤边界重置
缓存命中 96.1% 90.8%
工作台脚本 58 个,执行 331 次,92% 被复用 0 个
判定 无(协调者自发内审) 三态判定,判"无法达成",问人

最后一行值得注意。 HT002 一个可复用脚本都没写 —— 任务只有一小时,没有什么值得沉淀。 这说明工作台的价值随任务长度增长,短任务里它是纯开销。

原始记录长什么样

.flower/INDEX.md 是这次运行结束时的工作台快照,可以直接对着看:

目录 内容
scripts/ 0 个
artifacts/ 5 份 —— 01-recon.md(32.6K)、02-build-run.md(19.4K)、03-flag-only-feasibility.md(13.6K)、04-compliance-audit.md(16.4K)、05-final-build-run.md(19.8K)
notes/ 4 份 —— 需求.md目标.md问答记录.md 三份是框架写的冻结件,agent 自己写的决策记录只有 1 份
spill/ 11 份落盘

runs/ 下是 manifest.json(4 条步骤记录)和 sessions.db。 数据库里有 10 条会话:4 条主线程会话加 6 条 subagent 会话 —— 和"6 个 subagent"对得上。

问答记录里一共 7 问:q1–q6 是前置确认阶段问的 (和确认需求那一步的 6 次 ask 调用对得上),q7 是判定"无法达成"之后的那道人工闸门。

这一跑改了框架什么

发现 落地的改动
15 条判定清单只有约 5 条在验能不能用(第一节) JUDGE_RULES 加了反向压力:清单长度由失败方式的数量决定,不是由严谨程度决定,并把这次当反面例子写进规则原文("一次'装起来跑'的任务被写成 15 条,只有 5 条在验能不能用" —— 规则里取整写作 5,本页第一节的分类口径是约 5)
边界被写成了判定项 JUDGE_RULES 直接写死一句 "边界不是判定项"
4/15 条在这个环境验不了,花掉 $35.8950 + $1.4037 之后才发现 设定目标那一步要求当场给验不了的条目加 [此环境无法验证:原因] 后缀 —— 这次它用对了 3 条,漏了截图那条
clarify() / judge() 没有 hook 保护(第四节) whitelist_guard 已无条件接上:delegate_only=False 的角色由 Runtime 自动装,不再依赖工作台是否开启
副作用:设定目标的 judge 现在拿不到 Bash 有了 whitelist_guard 之后,不显式传 can_run=True 的话,JUDGE_RULES 里"先 uname -a 看清楚你在哪"那条执行不了 —— 这是修完之后新出现的取舍
clarify() 文档字符串的机制依据是错的 已改。WebFetch 也进了 clarify() 的白名单,所以那个例子作废,结论改挂在 $0.1 探针上
中途没有通道插一句话(第一节根因三) 记为 issue #3,未闭环 —— 这次它和过度复杂化直接叠加,一句"那两行改了没关系"能省下一小时

一句话总结这一跑:照着上一次的失败去优化,很容易造出方向相反的新失败。 HT001 教会框架"别轻易判通过",HT002 立刻演示了这条规则没有配上"别过度检查"时会发生什么。 两条都得写进 JUDGE_RULES,少一条就偏。