belay 是做什么的?
你通过许多小步骤来达成一个目标。一个检查在没有人工干预的情况下对每一步进行评分。scripts/belay.py 维护分数:你编辑代码,它运行检查,如果得分提高则提交,否则撤销,并写入日志。你从不自己写下分数,也从不决定某一步“基本”通过了。
每个目标都变成同一件事:一个输出数字的命令。更快是时间或计数。审计是失败的测试数,每个发现对应一个。移植是匹配的输出数。UI 是评判者的分数。
如何工作:
- 用户启动一次攀登然后离开。 不要停下来更新状态。他们阅读三件事:卡片、只有他们能回答的问题以及报告。如果有的话,使用工具的问题工具来提问。
- 状态存在于磁盘上,而不是聊天中。 卡片、日志、你的笔记以及上次检查的输出都在
.belay/<name>/下。在压缩或崩溃后,调用一次status即可恢复所有信息。 - 永远不要使用 sleep 循环等待。 在后台运行长命令,并在它们运行时计划下一步。
- 下面的
BELAY是这个文件旁边scripts/belay.py的绝对路径。 将完整路径写入每个命令。在目标仓库内部运行它。你传递的路径和命令是相对于仓库根目录的。 - 攀登有自己的分支
climb/<name>。 一个撤销的步骤会重置工作树并删除 git 没有忽略的未跟踪文件。如果还有其他人在此检出中工作,请在一个单独的工作树中进行攀登。当你已经在为此工作创建的分支上时,传递--here。
1. 开始前先恢复
运行 python3 "$BELAY" list。如果存在针对此目标的攀登,切换到它的分支,运行 status <name>,然后从步骤 4 继续。在崩溃或新会话后“继续”就是指这个。不要重新开始。
2. 构建检查
攀登的好坏取决于检查。一个衡量了错误东西的检查会耗费整个运行时间让错误的东西变得更好。先让一个粗糙的检查运行起来,稍后通过 relock 来改进它。
问问自己:为了看到目标是否达成,我会运行什么?让那个命令输出一个数字。使用最适合的形式:精确匹配或测试计数,然后是工作量计数(查询、往返次数、字节),接着是时钟,最后是模型评判者,仅当没有其他方法能区分好坏时使用。
| 目标 | 分数 |
|---|---|
| Bug 或特性 | 根据规范(而非你的修复)编写的失败测试 |
| 审计、稳定化、“让 X 工作” | 失败测试,每个发现对应一个 |
| 速度或大小 | 工作量(时间背后)的计数,或时间本身 |
| 移植或克隆 | 与真实工具匹配的输出 |
| 同时达成多个目标 | 向最差指标的进展 |
| 外观、感受、写作 | 带有评分量表和好例子的 judge.py |
每种情况的配方都在 references/checks.md 中。在编写检查之前,阅读与你的目标匹配的部分。
使用仓库已有的东西。--score '<regex>' 从任何测试运行器的输出中读取分数,例如 --score '(\d+) failed'。它会取最后匹配的行并累加捕获的数字。一次通过但无匹配的运行得 0 分。没有 --score 时,检查标准输出的最后一行必须是裸数字。重复 --check 以同时运行多个命令;它们的分数相加。为攀登编辑的每个其他检出添加 --repo <path>。然后一步在所有检出中同时保留或撤销。
在相信检查之前先验证它:
- 它能够失败。 故意破坏代码并观察分数变差。使用
git checkout -- <file>恢复。 - 它能看到成功。 一个小的真实改进必须能改变分数。
- 它是稳定的。 运行它 3 次。如果分数在没有代码变更的情况下漂移,用计数替换时钟,或者使用
--runs 3、一个高于漂移的--min-gain(当分数会大幅下降时使用百分比,如3%)以及--paired。 - 它不能被欺骗。 每次运行改变输入并与参考值比较。手动尝试一次作弊,比如硬编码答案,并确认检查或守护能抓住它。
- 它很快。 每一步都要付出检查的时间。将一步控制在 2 分钟内:对样本或每步的计数进行评分,并将完整套件或计时运行保留为守护或保留集。
- 目标是可以达到的。 找出当前设计能做到的最佳水平。如果目标超出了这个水平,在卡片上说明并提出一个你能达到的目标。
保护分数未衡量的所有方面:
- 守护(
--guard,退出码为 0 即通过)。列出你将修改的代码所接触的内容:调用者、其他平台、保存的数据、公共 API。每个都需要一个守护:今天能通过的测试、类型检查、lint、构建。当分数是失败计数时,将这些通过测试保留在它们自己的文件中作为守护,这样崩溃就不能伪装成“失败更少”。 - 锁定(
--lock)检查、测试、固定数据和保留集。try在锁定的文件有差异时拒绝评分。--lock路径下的新文件会成为攀登分支上的第一个提交。任何其他未提交的变更都会停止init。 - 保留集(
--holdout),当检查使用样本、基准或评判者时:攀登从未训练过的案例,保持在.belay/<name>/holdout/下。不要再打开它们。如果分数上升而保留集没有,说明攀登过拟合了。停下来并说明。
审计和开放式目标。 将目标涵盖的内容作为清单列在 .belay/<name>/notes.md 中。按区域寻找 Bug。根据规范或 Bug 报告,为每个真实发现编写一个失败测试。当后面的步骤发现新的 Bug 时,添加它的测试并运行 relock --why "found <bug>"。分数因正当理由上升,日志显示原因。需要用户决策的发现放在笔记和报告中,而不是分数中。
3. 卡片,然后初始化
发送一条只包含卡片的消息来决定:
攀登卡片:
build-speed目标: 冷构建快 2 倍,输出相同
分数 构建时间中位数,越低越好:当前 47.0,目标 23.5 检查 ./scripts/bench-build.sh,3 次运行,配对,最小增益 3%守护 npm test,npm run typecheck保留集 12 个样本应用中的 3 个 锁定 scripts/bench-build.sh,test/,samples/限制 无(每步约 2 分钟) 需要你的回答: 无
回复 go,或者更改一行。
用你的最佳选择填写每一行。如果用户给出了没有目标的目标,提出一个有野心且可检查的目标,比如“快 2 倍”或“每个样本都匹配”。等待“go”,这表示批准卡片、分支及其提交。当用户已表示无需询问即可运行,或者目标很小且目标没有疑问时,跳过等待并将卡片放在报告顶部。在那之后,不要询问你能自己找到或决定的事情。
python3 "$BELAY" init build-speed --goal "cold build 2x faster" \
--check ./scripts/bench-build.sh --direction min --target 2x \
--runs 3 --paired --min-gain 3% --guard "npm test" --guard "npm run typecheck" \
--holdout ./scripts/bench-holdout.sh --lock scripts --lock test \
--lock .belay/build-speed/holdout
跨后端和前端的审计:
python3 "$BELAY" init billing-audit --goal "every billing finding fixed" \
--check "pytest tests/billing -q" --check "cd ../web && npx vitest run billing" \
--score '(\d+) failed' --direction min --target 0 --repo ../web \
--guard "pytest tests/legacy -q" --guard "cd ../web && npx tsc --noEmit" \
--lock tests/billing --lock ../web/src/billing/__tests__
init 创建分支、测量基线,并在守护已失败时拒绝启动。在第一步之前修复它警告的问题。仅在用户指定限制时设置 --max-hours 或 --max-steps。没有限制时,攀登运行直到达到目标或卡住。
4. 攀登
每一步是一个想法,一个变更,一次 try:
- 选择想法。不要重试日志已经撤销的想法,除非你能说出现在有什么不同。如果使用了评判者,请阅读
.belay/judge-notes.md。 - 为某个原因做一个变更并猜测其效果。将猜测放入想法中:
"缓存解析器(预期 -10%)"。当猜测相差甚远时,你对代码的理解有误。在下一步之前进行性能分析。 python3 "$BELAY" try "<idea>"。它会提交成功或重置工作树。无论哪种情况,都继续前进。该步的完整检查输出在.belay/<name>/last.txt中,最佳步骤的在best.txt中。阅读它们,而不是再次运行检查。- 当检查运行时,不要编辑工作树。一次只能运行一个改变攀登状态的
belay.py命令。如果它说另一个仍在运行,请等待。 - 每 5 步,用几行更新
.belay/<name>/notes.md:什么有效,什么无效,下一步尝试什么。下一次会话从中恢复。
如果你相信一个想法被检查撤销了两次,说明检查看不到它的价值。使检查更精细并 relock。永远不要编辑锁定的文件来让一步通过。
永远不要欺骗检查
以下每一种行为都会提高分数并使工作变得更糟。即使 try 保留了它,也是一次失败的攀登:
- 特殊处理检查使用的输入,或硬编码或缓存预期输出。
- 跳过、削弱或删除测试,或捕获并隐藏错误。
- 让基准做更少的工作,而不是让代码更快。
- 使用
relock删除或放宽一个困难的案例以达到目标。Relock 只能让检查更正确。如果无法达到目标,报告你测量到的地板。 - 读取保留集。
一个真正的胜利会让相同的代码在检查从未见过的输入上表现得更好。如果唯一的前进方式是上述之一,说明检查有误。请说明。
持续重新评估
分数只是用户想要的东西的代理。检查它是否仍然如此:
- 每 10 个保留的步骤,以及在达到目标时: 亲自查看真实的东西。打开应用,阅读输出,查看截图。运行完整的测试套件和
holdout。如果分数和真实的东西有偏差,修复检查并relock --why "..."。 - 有东西坏了但没有守护抓住: 用
git bisect run <failing command>找到出问题的步骤,然后用try --fix "<what broke>"修复它,这会保留一个保持分数的变更。如果唯一的修复方法会放弃该步骤的增益,则用git revert还原该步骤并relock --why "reverted step N: broke X"。补充缺少的守护。 - 在报告之前: 用全新的视角,将
git diff <start>..HEAD作为一个整体变更来审查。寻找被删除的测试、被吞掉的错误、被关闭的功能、被改变的默认值以及数据或 API 变更。用try --fix修复,然后再次运行检查并holdout --why final,以便报告引用测量到的数字。
5. 当停滞时
try 和 status 在连续 5 步没有进展后打印 PLATEAU。停止尝试新想法并找到阻塞点。在猜测之前先测量:
- 时间或错误计数实际上来自哪里?进行性能分析。
- 检查是否不稳定、过于粗糙而无法看到小胜利,或者对问题视而不见?
- 是否有小步骤无法触及的更大想法?
- 什么工具、访问权限、数据或计算资源会改变接下来的十步?
用 python3 "$BELAY" review "<what you found>" 记录它,这会暂停 PLATEAU 5 步。修复你能修复的:更快的检查、更精细的指标、不同的方法。只有在所有其他想法都用尽之后,才向用户请求只有他们能提供的东西,比如访问权限或资金。
6. 停止并报告
当以下任一情况为真时停止:
try打印 BAR REACHED。它会为你运行保留集。一个平坦的保留集上的可见胜利不是胜利。status打印 STUCK:两次阻塞审查之间没有任何进展。try以“预算已用”拒绝。- 保留集与分数不一致。
- 下一步需要只有用户能提供的东西。
然后进行上述最终审查,删除 .belay/<name>/data/,并发送报告。让它在十秒内可读:
攀登
build-speed:达到目标47.1 秒 → 18.7 秒,快 2.5 倍(目标 23.5 秒,在第 41 步达到)
保留集 51.0 秒 → 20.2 秒,随分数移动 步骤 52 步中保留 19 步 安全性 每次保留步骤都通过了守护。完整套件和最终审查干净 分支 climb/build-speed,19 次提交有效的方法
- 增量类型缓存:−12 秒
尝试过但撤销的
- swc 替换:破坏了 3 个守护
你的行动
- 审查:
git log --oneline 1a2b3c4..climb/build-speed
标题是以下之一:达到目标、卡住、预算用尽、保留集不一致、需要你。引用脚本测量到的数字,永远不要用估计值。如果你在路上更改了检查或目标,请在标题中说明。最大的胜利排在最前面。每个部分最多 3 个要点。跳过空部分。
如果用户随后想要更多,用 raise 2x --why "..." 为同一个主张设置一个更难的障碍,然后继续攀登。永远不要切换到一个新的目标。
子代理
如果工具可以启动子代理,可以在设置期间使用它们进行并行阅读,并在阻塞审查和最终审查中获取新的视角。要求给出结论,而不是文件转储。只有你在攀登树中写入,因为 try 会重置它。助手读取,或在其自己的工作树中起草补丁,由你应用并 try。没有子代理时,内联完成。
命令
| 命令 | 功能 |
|---|---|
| `init --goal "..." --check "..." --direction min | max` |
try "<idea>" [--fix] | 对编辑进行评分,然后保留或撤销。--fix 保留一个保持分数的修复 |
status [name] | 分数,最近的步骤,笔记,PLATEAU 或 STUCK |
relock --why "..." | 接受一个更改后的检查并再次测量 |
holdout --why "..." | 立即运行保留集 |
review "<found>" | 记录一个阻塞审查 |
raise <bar> --why "..." | 在达到当前目标后设置一个更难的目标 |
budget [--hours N] [--steps N] | 用硬限制提醒自己 |
list | 列出此仓库中的所有攀登 |
note "<text>" | 将一行添加到笔记中 |
abandon <name> --why "..." | 删除分支和数据。报告为什么 |