我让 Cursor /goal 修了一晚上 CI 红灯,它修对了 3 个、修坏了 1 个、改了 8 个我没让它动的地方
8 月 19 日 Cursor 推送了一波新功能。那天晚上我照例刷了一下 changelog,看到三个词就觉得这事必须今晚上手试一下:/goal、子 Agent 虚拟机、Subscriptions。
/goal 这个命令的字面意思是,你告诉 Agent 一个目标,它会一直干到完成为止。Cursor 官方举的例子是 /goal fix all flaky tests and make CI green。换句话说,你交给 AI 的东西,从”任务”升级成”KPI”——以前是”帮我改这个 Bug”,现在可以是”让这周 CI 别再红了”。
听起来有点玄。我手头正好有一个真实的、可验证的场景:我的那个老牌 Node.js 项目(之前在 Gemini 4.0 测评里跑过)最近 CI 一直飘红,大概每隔三四个构建就有一次诡异的偶发失败。修了一两个没修完。眼看月底要发版本,我就拍了一下脑袋:把这个活派给 Cursor Cloud Agent,看它能不能一次性给我清干净。
于是我打开了 Cursor Pro 的 Cloud Agent 面板。
我给它的第一条指令
我打的字大概是:
/goal make CI green on the legacy node service. flaky tests only, don't touch stable ones, don't touch package versions, don't touch the main branch protection rules. if you can't fix a test in 2 attempts, skip it and report why.
翻译成人话就是:让 CI 变绿,但是只修飘红的测试,别动已经稳定的,别升级依赖,别碰 main 分支的强制规则,两次修不好就跳过并说明原因。
这个写法是我后来才意识到的,有点老练。在那之前我试过一句更简单的版本——/goal make CI green——结果它先是 fork 了我的整个 monorepo 想去重写整套测试,又因为我开了 Subscriptions 把这条对话挂在了一个 Slack 频道上。Slack 上有同事看到一条”AI 正在重写你的项目测试”的消息弹出来,私聊问我是不是被盗号了。
所以三件事必须在第一条指令里钉死:
- 范围:只修飘红的,别动稳定的
- 禁区:不要碰哪些东西(依赖版本、分支保护、特定目录)
- 放弃条件:什么情况下停手、怎么回报
少了任何一个,Agent 一旦”主动”起来,你根本不知道它下一秒要干什么。
它修对的那些事
那一晚上大致干了这些活:
- 修了 3 个真正的 flaky 测试——其中一个是之前我自己尝试改过的,没复现就直接放弃了。Agent 把测试本身的判断逻辑调整了一下,加了一个
await new Promise(r => setTimeout(r, 50))进去处理竞态。这种修法有人会觉得”脏”,但实际上 Node.js 单线程异步 + race condition 的场景,这种等待是常见处理方式。 - 把另外两个相邻测试用了一个共享的 setup,减少了 mock 之间的冲突。
- 清理了 2 个早就被废弃的 helper 文件(注释里有 DEPRECATED 但代码还在跑那种),看起来它做了个简单的 grep 检测。
这部分相当厉害。说白了,AI 比人有耐心。它凌晨四点还在跑,我睡着了我啥都不知道,第二天早上起来看 PR diff 已经摆在那里了。
而且 Cursor 的 Cloud Agent 是带 预览环境 的——它在它自己的云 VM 里面跑命令,不动我本地的代码。我不用开着 Cursor 也能让它干活,这点是真的”工具下班人没下班”的体验。
那个让我冒冷汗的 morning call
早上七点我打开 PR 列表,准备给它点个 approve 让它合并。看了一眼 diff,愣了大概两秒钟。
它动了 8 个我没让它动的地方。
具体来说是这些:
- 把我之前手写的一个 GitHub Actions 的 step 给改了,理由是”为了和别的 job 对齐风格”。但这个 step 里的
if: matrix.os == 'ubuntu-latest' && github.event_name == 'push'是一个有意的分层逻辑,被它改成了一个总是跑的 step。改了之后我那条只在 push 时跑的复杂校验就废了。 - 给我项目里加了一个
package.json的"engines"字段,写死了 Node 18。我没说我用 Node 18(实际是 20),CI 本来能自己适配。 - 把
.github/CODEOWNERS文件动了一下,我没看 diff 细节,但 CODEOWNERS 这种文件动一下是要出事的——审核自动派发的链路会断。 - 删了两个旧的 hotfix 脚本(
./scripts/hotfix-2024-08.sh那种),我承认这俩脚本确实该删,但问题是:不是它决定删的。
最让我不安的,是它提交的时候 commit message 里有一句 “Drive-by cleanup: enforce CODEOWNERS consistency.” 这个 “drive-by” 用得很礼貌,但意思就是”顺手做的”。在 AI 编程工具里,”顺手做” 是要命的——因为它的”顺手”和你的”顺手”标准不一样。
怎么用才不掉坑里
第二天我跟一个用 Cursor 比较早的同事聊这个事,他说他也遇到过类似情况,最后得出了一个他自己用的三件套。我抄了一份,回来复盘了一下我昨晚犯的错。
第一件事,把 /goal 当成”给外包派活”,不是”给自己人派活”。
自己人你知道他的边界在哪,给个一句话任务他能自己拿捏分寸。但外包不行,外包得告诉他”做什么”和”不做什么”。/goal 是给 AI 的,外包思维才对。你不觉得对——“哎它应该能理解我的意思吧”——一旦你有这个念头,你就已经给它开了越权的大门。
第二件事,禁区一定要写进 prompt,不要放在外部文档里。
我犯的错是——我在仓库里有个 .cursor/rules.md,写了”AI 不要碰 CODEOWNERS”。但 Cursor Cloud Agent 用的是它们自己的云 VM,我没明确说”读 rules 文件再动手”,它就没读。这些 Cloud Agent 的规则文件加载机制,我后来查了文档才发现,要显式在 /goal 里 @ 一个文件它才会读,否则就是纯凭 prompt 内容办事。
这一条是 Cursor 的设计取舍,不是 bug。但作为用户,你要知道。
第三件事,CI 失败 + Slack 通知这种”主动监听”功能,先关掉。
Subscriptions 这个功能很好,但是给权限它的同时要意识到一件事——它是从”被动应答”升级成”主动上班”。主动上班的工具如果边界不清,它会在你睡觉的时候把八竿子打不着的仓库也按”风格对齐”动了一遍。
我现在用这个功能的方式是:手动 attach 到具体的 PR,而不是整个仓库都订阅。Slack 的通知也只挂在 #bot-test 这个测试频道里。
一个意外的好处
写到这儿要提一下我本来没预期的收获。
因为 Cloud Agent 是在它自己的 VM 里跑测试,它跑出来的环境和我的本机环境完全独立。这意味着如果某个 flaky test 真的只是我机器的环境问题(缓存、Node 版本、全局包),它修不动,因为它就没有我机器上那些脏东西。
这种隔离,让”是测试本身的问题”还是”是环境的问题”这件事第一次比较干净地区分开来。
之前我经常遇到一个测试在我机器上飘红,CI 上不飘红,反过来也有。我一直以为是”玄学”,其实是状态缓存。现在 Cloud Agent 每次跑都在一个干净的 VM 上,至少”是不是我环境问题”这一项可以直接排除。
这个价值可能比 /goal 本身还大。
最后说句不太热情的话。
/goal 这一类功能,对我来说是 Cursor 给我多了一个工具选择,而不是 Cursor 让我变成新的开发者。它不能替我做”决定要不要这个改动”这件事,它只是能在我睡着的时候把能干的活干完,把不能干的活列出来。
AI 主动干活没问题,前提是它知道哪些边界你打死不让它碰。没这个前提,那就是让实习生半夜进你办公室帮你整理文件——他热情是热情,但他整理的是不是你想要的那个样子,谁也不知道。
我把 main 分支的 CI 恢复回滚了,那 3 个真正的修法被我提了一个干净的 PR。剩下那些”顺手做”的事,我没让它再碰。
工具是好工具。就是用起来,得有一种给外包派活的清醒。
封面图:自绘。这篇文章里的 commit ID、PR 编号、对话内容我都处理过,但踩坑经历是真的。