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.json 和 runs/sessions.db 算出:
这一跑的形状¶
| 项 | 值 |
|---|---|
| 任务 | 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.md16:14、artifacts/01–05(15:40–16:12)、notes/决策-项目形态与验收路径.md15: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_mtimespec 和 O_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 -c 是 0。 这个精确说法是合规审计逼出来的 —— 协调者原本想笼统写"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 / 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,少一条就偏。