逐日AI
第 1 周 · D4约 4 小时

一次只做一件事:feature 清单与 git 纪律

今天这一天从一个反转开始:手搓 Coding Agent 那门课明确裁定过不能用 git 提交,而本课的 git commit 就是进度的权威记录。两个结论相反而都对,因为前提不同。理解这个区别,才能理解清单驱动与绿点回滚。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能说清为什么交互式 Agent 不该碰用户的 git 历史、而无人值守 Agent 的 commit 就是权威进度,并指出区分二者的那个前提
  2. 能解释一次只做一条 feature 这条约束为什么比在提示词里写要求更有效,并说出放开它会出现的两种失败
  3. 能描述绿点的定义与回滚策略:什么情况该回到上一个绿点,什么情况该继续往前

今天从一个会让你困惑的冲突开始。读完回到页面顶部把三条目标勾掉。

小白版讲解

一个反转:同一件事,两门课给了相反的结论

如果你学过手搓 Coding Agent 那门课,会记得那里有一条明确的裁定:不要用 git 提交,不能污染用户的仓库历史,改用内容哈希做快照。

本课的结论正好相反:git commit 就是进度的权威记录,agent 自己提交。

两边都对。区分它们的前提只有一句话:这个仓库是谁的。

交互式 Coding Agent无人值守 harness
操作的是谁的仓库用户的仓库agent 自己的工作区
谁会读这段历史用户本人,还有他的同事、他的 code review只有 harness 自己,和早上来验收的你
往里面塞 commit 意味着越权——用户的 git log 是他自己的东西记账——那里面的 commit 就是进度本身
不提交的代价没有代价,用内容哈希快照一样能回退没有落点可退,出事只能整段重来

这个反转值得记住,不只是因为它解释了今天的设计,更因为它示范了一种判断方式:当你看到两个可信来源给出相反的结论时,先别急着判断谁错了,去找它们各自的前提。 大多数「最佳实践冲突」都是前提冲突。

工地上的验收单:签了字才能进下一项

先讲今天第二件事的类比。

装修工地上有一张验收单,每一项写清楚「做到什么算完成」,签了字才能进下一项。为什么是一项一项签,而不是整个工程做完一起验?

因为出问题时你要知道退回哪里。水电没验就封了墙,等到刷完漆才发现插座位置错了,那一刻你要拆的不是插座,是整面墙。

Agent 的 feature 清单就是这张验收单。而「一次只做一条」就是「签了字才能进下一项」。

一次只做一条:写进代码里,不是写进提示词里

多数人第一反应是在提示词里加一句「请一次只实现一个 feature」。这句话有用,但它不硬。

它不硬在哪:提示词是请求,代码是事实。 模型可能顺手把相邻的两条一起做了(它看得见那两条,而且它们确实很像);上下文被压缩之后那句要求可能被压没了;换个模型可能对这句话的理解不一样。这些都不会报错。

今天的做法是把这条约束变成结构上的不可能

  • harness 读磁盘上的清单挑出下一条
  • 上下文里只放这一条,其余三十九条模型根本看不到

看不到的东西,做不了。这不再是一个要求,是一个事实。

放开这条约束会出现两种失败,它们的形态很不一样:

第一种,跳着做。 模型挑了一条依赖还没满足的 feature——比如先做分页再做列表。它会写出一段引用了不存在的东西的代码,而浅检查看不出来。

第二种,一次做一批。 模型一口气实现三条。这看起来是效率提升,代价在出事的时候露出来:三条混在一次改动里,其中一条错了,你无法只退那一条。批量提交等于把三条的命运绑在一起。

判断依据从上下文搬到磁盘:退化从「不再发生」变成「不可能发生」

D1 到 D3 里其实一直是模型在挑任务:harness 把四十条清单全贴进上下文,模型按「第一条没有完成证据的」自己挑。那一路没出错,是因为证据一直是对的。

但这个设计有个结构性的问题:判断依据活在上下文里。 上下文被压缩、摘要写漏一条、某次证据格式对不上——任何一种情况下模型都会挑错,而且挑错之后没有任何东西会报错。

今天把依据搬到磁盘之后,出现了一个顺带的结果,值得单独说:

D1 那个退化从今天起不可能发生了。

D2 的对照靠的是「关掉状态层,模型在上下文里找不到证据于是重做」。而现在选谁做根本不看上下文。所以 lab 的自检里,状态层开与关都不再产生重复劳动——这不是 D2 白学了,是它被一个更强的结构取代了:D2 让退化不再发生,D4 让退化不可能发生。

这两者的差别在工程上很实在。「不再发生」依赖一条正确的信息通路(摘要必须拼对、约定词必须没被改),而 D2 的变异检验已经演示过那条通路有多容易被一个词改断。「不可能发生」不依赖任何通路。

绿点:通过验证并且已提交的那个点

现在给「绿点」一个准确定义,它在后面三天会反复出现:

绿点 = 通过端到端验证,并且已经提交的那个点。

两个条件缺一不可,而且顺序不能反。没验证过的提交不是绿点——回退到那里等于把一个未知状态当成已知状态;验证过但没提交的点也不是绿点——它不存在于任何可以回去的地方。

差的不是产出,是风险敞口

今天的实验跑两遍,唯一的差是提交粒度:一条一提交,还是攒到窗口收尾一起提交。

TextText
完成条数                 9 → 9(一样)
按 feature 的绿点个数    0 → 9
未提交工作的最大暴露量   3 条 → 0 条

最后做完的条数一模一样。 这一点很重要,否则你会误以为攒着提交是个更笨的实现——它不笨,它只是把风险攒起来了。

所以接着模拟一场灾难:已完成八条、正在做第九条时工作区被写坏(补丁写到一半进程被杀、某次改动把文件改坏了,这些在跑一整夜时都会真的发生)。两边都回退到各自最近一个「全部验证过」的提交:

TextText
per-window:退到「窗口 2 收尾,已完成 6 条」
  保住 6 条,丢掉 2 条已经做完并验证过的工作
per-feature:退到「feat(F08): 搜索标题……」
  保住 8 条,丢掉 0 条

注意一个容易被讲歪的地方:攒着提交的那一趟不是没有可回退点。 它的窗口收尾提交同样能退,问题是粒度——可回退点每三条才有一个,所以退回去要连累旁边两条。

「未提交工作的最大暴露量」这个数就是出事时会丢掉的工作量,也是「一次只做一件事」这条纪律的价格标签。一条一提交把它压到零。

回滚策略:什么时候该退,什么时候该继续往前

绿点有了,还得有用它的规矩。今天先把判据立起来(自动触发要到 D5):

情况该怎么办为什么
工作区坏了(代码跑不起来、文件被改坏)退到上一个绿点继续往前是在一个已知损坏的地基上盖楼
某条 feature 反复失败,但工作区是好的不退,换一条做退了会把好的工作一起丢掉,而那条的难度不会因为回退而变低
某条 feature 失败,且它把工作区弄脏了退到上一个绿点,然后换一条先恢复地基,再绕开这条
清单与代码对不上退,并且查为什么会分叉这是最危险的一种,它说明有个地方在说谎

第二行是最容易做错的:看到失败就回退是一种过度反应。 回退的目的是恢复一个可信的地基,不是惩罚失败。地基没坏就不该退。

源码导读

今天新增 src/plan/features.ts,并且扩写了 src/core/git.ts。三个位置值得细读。

第一个位置:任务选取。 规则刻意简单到无聊。

features.js
// 挑下一条:按编号顺序,第一条还没通过的。
// 够用是因为靶子的四十条依赖全部指向编号更小的条目(依赖写在描述的措辞里),
// 所以顺着编号做永远是合法顺序。真实项目里这里会换成拓扑排序,
// 但「一次只吐一条」这个接口形状不变——那才是今天要立的东西。
function selectNext(features, state) {
  const done = new Set(state.done)
  return features.find((f) => !f.passes && !done.has(f.id)) ?? null
}
 
// 挑出来之后,上下文里只放这一条。模型看不到别的任务,所以它不可能跳着做。
const task = selectNext(loadFeatureList(repoDir), state)
const context = `${opening}\n\n本步要做的(只有这一条):\n- ${task.id} [${task.category}] ${task.description}`

注意 selectNext 读的是 loadFeatureList(repoDir)——磁盘上的清单,不是内存里那个数组。这和 D2 读磁盘状态、D3 读磁盘上的 init.sh 是同一条纪律:凡是要在 D6 跨进程存活的通路,今天就得从磁盘走。

第二个位置:清单文件与代码进同一个 commit。 这是今天最容易被写错的地方。

green-point.js
// 验证通过之后做两件事,顺序不重要,但它们必须进同一个 commit
markPassed(repoDir, features, feature.id) // 改清单,并且写回磁盘
state.done.push(feature.id)
 
// 绿点:代码改动(server.mjs)与清单改动(features.json)一起提交。
// 回退到这里,两者一起回去,不会出现「代码退了、清单还记着已完成」。
commitAll(repoDir, `feat(${feature.id}): ${feature.description.slice(0, 40)}`)
 
// 反面写法:只改内存不写盘。程序照样跑、照样做完九条、
// D1 到 D3 的断言照样全绿,只有磁盘上的清单一直停在「全未通过」
function markPassedBroken(repoDir, features, featureId) {
  features.find((f) => f.id === featureId).passes = true
  // 少了 saveFeatureList(repoDir, features)
}

那个反面写法就是今天的变异检验。跑出来的结果里,最能说明问题的是这一行:

TextText
✗ 回退之后代码与清单仍然一致 — per-feature 9 条补丁 / 0 条清单

代码里躺着九条实现,清单说一条都没做。 而且这种故障不报错、不打警告,只有在你回退、重启、或者换个人来看这个仓库的时候才暴露——那时候 harness 会从第一条重来一遍,把九条全做第二遍。

第三个位置:绿点查 git log,不另建记录。

greens.js
// 绿点直接从 git log 里查,不在 RunState 里再存一份。
// 存两份就有两份不一致的可能,而 git 本身已经是一个可靠的、
// 带顺序的、可回退的账本了。
function greenPoints(workdir) {
  // 刻意用 git 默认的基本正则,不要加 --extended-regexp:
  // 扩展正则里左括号是分组符,git 会直接报 parentheses not balanced
  const raw = runGit(workdir, ['log', '--grep=^feat(', '--format=%H%x09%s'])
  return raw.split('\n').filter(Boolean).map((line) => {
    const [sha, ...rest] = line.split('\t')
    return { sha, subject: rest.join('\t') }
  })
}

这里有一条比代码更值得带走的判断:能让现成的权威记录回答的问题,不要另建一套记录。 你每多一份副本,就多一个它和真相分叉的机会,而分叉本身不可怕——没人看得见的分叉才可怕

顺便说那个正则的坑:加上扩展正则开关之后 git 会把左括号当分组符,直接报括号不匹配。这个 lab 写的时候真中过一次,所以注释留在了代码里。

动手实验

🧪 D4 实验:一次只做一件事——清单驱动与 git 纪律

代码位置:labs/agent-harness-7days/day-04-feature-list

练习四处:plan/features.tsmarkPassed 少一行写盘,core/git.tscommitAllgreenPointsrollbackTo。冻结文件、D2 的状态层、D3 的初始化阶段全部原样带过来。

  1. 先读 src/core/loop.ts 里 harness 挑任务那一段,看清楚上下文里为什么只剩一条清单行。
  2. 补上 markPassed 的写盘那一行,想清楚少了它会在哪一刻暴露。
  3. 补上 commitAll 与 greenPoints,注意没有改动时不要提交、以及那个正则不能加扩展开关。
  4. 补上 rollbackTo,先挡住空目标再 reset --hard。
  5. 跑 MOCK=1 pnpm selftest 到五十七项全绿,再跑 MOCK=1 pnpm start 看灾难模拟。

今天的变异检验就是把 markPassed 里的写盘那一行注释掉。运行输出里「清单实际进度 9/40」照旧,但「磁盘上的 features.json:已通过 0 条」。自检转红五项,其中那条「9 条补丁 / 0 条清单」是全课到目前为止最贴近真实事故的一个画面。

面试题

今天四道题围绕那个反转、清单驱动的结构性约束,以及绿点与回滚策略。

第一道几乎一定会以某种形式出现:面试官给你两个相反的做法,问你选哪个。正确的回答不是选一个,是先问前提。 这个思路本身就是今天最值钱的东西。

检查清单与明日预告

  • 能说清为什么交互式 Agent 不该碰用户的 git 历史、而无人值守 Agent 的 commit 就是权威进度,并指出区分二者的那个前提
  • 能解释一次只做一条 feature 这条约束为什么比在提示词里写要求更有效,并说出放开它会出现的两种失败
  • 能描述绿点的定义与回滚策略:什么情况该回到上一个绿点,什么情况该继续往前
  • 能说出为什么代码改动与清单改动必须进同一个 commit,以及分开会在哪一刻出事
  • 让 MOCK=1 pnpm selftest 五十七项全绿,并看过灾难模拟里两种粒度各丢多少
  • 做过一次「注释掉写盘那一行」的变异检验,能说清「内存里对、磁盘上错」这类故障的形态
  • 四道面试题不看要点也能答出至少三道

明天是 D5《自验证:端到端闸门、谎报完成与打转的自动干预》。今天留了一个明摆着的破洞:验证仍然是浅检查,补丁写进去就算过。于是「绿点」到现在还只兑现了一半——已提交,但没验证过。明天补上前半句,并且会看到两个真实涌现的现象:谎报完成(服务能起来、端到端是红的,而模型照样回报做完了)与打转(同一条反复尝试而清单一动不动)。然后是比发现更难的那一半:发现之后自动怎么办。今天那张回滚策略表明天会变成代码。

面试题库

  • 什么情况下 Agent 可以自己执行 git commit,什么情况下绝对不行?When may an agent run git commit on its own, and when must it never?
    国内高频海外高频进阶#git#state-management#boundaries

    分析过程 · 先想清楚再作答

    1. 这题表面在问 git 规范,实际考的是**一条工程结论能不能被前提推翻**。直接背「Agent 不许碰 git」或者「Agent 当然该自己提交」都会被反例打穿,因为两种说法各有一门课的明确裁定撑着:手搓 Coding Agent 那门课裁定过不能用 git 提交、改用内容哈希做快照,而无人值守场景里 commit 就是进度的权威记录。面试官等的不是站队,是那个能把两边分开的判据。
    2. 拆法是先问两句话:**这个仓库是谁的?谁会读它的历史?** 交互式 Coding Agent 操作的是**用户的仓库**,那份 git log 是用户自己的东西,工具往里塞机器生成的记录属于越权;而且不提交并没有代价——内容哈希快照一样能回退。既然收益为零、代价是污染别人的历史,结论自然是不提交。
    3. 反过来,无人值守时 agent 操作的是**自己的工作区**。一整夜没有第二个人会读那份历史,读它的只有下一个窗口和第二天早上的你。这时 commit 不是污染而是记账:提交点就是进度本身,而不提交的代价非常实在——没有可回退的点,出事只能整段重来。
    4. 所以结论可以压成一句话直接答出去:**判据不是「能不能提交」,是「这个仓库是谁的、谁会读它的历史」。** 同一个动作在两个前提下有两个相反的正解,这不是矛盾,是前提不同。能主动指出前提的人,通常也能把这套判断迁移到别的场景。
    5. 顺带一条值得主动说的设计取向:既然提交历史已经是权威记录,就不要在自己的状态文件里再存一份「已完成点列表」,直接按提交信息前缀查 git log 就行。两份记录就有两份不一致的可能,而 git 本身已经是一个可靠的、带顺序的、可回退的账本。通用判据是——**能让现成的权威记录回答的问题,不要另建一套记录。**
    6. 可预期的追问是「那 agent 就是要在用户的仓库里干活呢」。答案不是折中,是把前提改回来:给它一个自己的工作区副本,或者干脆退回快照方案。同时要点出这条边界一旦模糊的后果——工作目录只要解析错一次,本该进沙盒的 commit 就打进了真实仓库的历史,那一瞬间你做的就是另一门课明令禁止的事。

    How to reason about it · think before answering

    1. On the surface this is a question about git hygiene. What it actually tests is whether you can see a premise overturn an engineering conclusion. Reciting either 'agents must never touch git' or 'of course the agent should commit' gets shot down by a counterexample, because each has an explicit ruling behind it: the course where you build a coding agent by hand rules out git commits in favor of content-hash snapshots, while unattended runs treat the commit as the authoritative record of progress. The interviewer wants the criterion that separates the two, not a side.
    2. Break it open with two questions: whose repository is this, and who will read its history? An interactive coding agent works inside the user's repository. That git log belongs to the user, and stuffing machine-generated entries into it is overreach - and skipping the commits costs nothing, since content-hash snapshots roll back just as well. Zero upside, real downside: do not commit.
    3. Unattended, the agent works in its own workspace. Nobody else reads that history all night; its only readers are the next window and you in the morning. Here a commit is not pollution but bookkeeping - the commit point is the progress. And not committing has a very concrete cost: no point to fall back to, so any incident means redoing a whole stretch.
    4. The conclusion compresses into one deliverable line: the criterion is not whether committing is allowed, it is whose repository this is and who reads its history. The same action has opposite correct answers under two premises, which is not a contradiction - it is two premises. Someone who names the premise unprompted usually transfers the judgment to other situations too.
    5. One design stance worth volunteering: since the commit history is already the authoritative record, do not keep a second list of completed points inside your own state file - query the log by message prefix instead. Two records means two ways to disagree, and git is already a reliable, ordered, rewindable ledger. The portable rule: if an existing authoritative record can answer the question, do not build a second record to answer it.
    6. Expected follow-up: what if the agent really does have to work in the user's repository? The answer is not a compromise, it is restoring the premise - give it its own copy of the workspace, or fall back to snapshots. And name the consequence of letting that boundary blur: resolve the working directory wrongly once and a commit meant for the sandbox lands in the real repository's history, which is precisely the thing the other course forbids.

    答题要点

    • 判据不是能不能提交,是这个仓库是谁的、谁会读它的历史。
    • 用户的仓库:提交属于越权,而且不提交没有代价——快照一样能回退。
    • Agent 自己的工作区:提交就是记账,不提交才有代价——没有可回退的点。
    • 两门课结论相反而都对,因为前提不同,不是其中一边写错了。
    • 既然提交历史已是权威记录,就不要在状态文件里另存一份完成点列表。
    • 通用判据:能让现成的权威记录回答的问题,不要另建一套记录。
    • 边界模糊的后果:工作目录解析错一次,commit 就打进真实仓库的历史。

    Key points

    • The criterion is not whether committing is allowed but whose repository it is and who reads its history.
    • The user's repository: committing is overreach, and skipping it costs nothing since snapshots roll back fine.
    • The agent's own workspace: committing is bookkeeping, and not committing costs you every rollback point.
    • Two courses reach opposite conclusions and both are right, because the premises differ.
    • Since the commit history is already authoritative, do not keep a second list of completed points in state.
    • Portable rule: if an existing authoritative record answers the question, do not build a second record.
    • Blur the boundary and one mis-resolved working directory puts commits into a real repository's history.
  • 怎么强制一个 Agent 一次只做一件事?写在提示词里够吗?How do you force an agent to do one thing at a time? Is putting it in the prompt enough?
    国内高频海外高频进阶#feature-list#constraints#harness-design

    分析过程 · 先想清楚再作答

    1. 这题考的是**约束写在哪里**。最常见的答案是「在提示词里把『一次只做一条』写清楚、写重一点」,它也是最不可靠的答案:提示词是一个请求,不是一个约束。模型可以不照办,而且不照办之后没有任何东西会报错——你早上拿到的是一堆做了一半的条目,而不是一条错误信息。
    2. 拆的第一步是问:**现在是谁在挑任务?** 常见做法是把整张 feature 清单贴进上下文,让模型按「第一条没有完成证据的」自己挑。这条路平时不出错,靠的是证据一直是对的;但判断依据活在上下文里,压缩一次、摘要漏写一条、某次证据格式对不上,模型就会挑错,而且它只是安静地做了一条不该做的。
    3. 正解是把任务选取从模型手里收回到 harness:harness 读**磁盘上**的清单挑出下一条,然后**只把这一条渲染进上下文**。本课靶子有四十条,模型每一步只看得见一条,另外三十九条根本不在它眼前。于是「跳着做」不是被禁止了,而是没有表达它的入口——这才叫强制。
    4. 结论值得原样答出去:**判断依据从上下文搬到磁盘,约束就从「不被鼓励」变成「不可能发生」。** 提示词能做到的上限是前者。这个句式可以迁移到任何一条你不想让模型自由发挥的规则上:先问它现在活在上下文里还是活在代码里。
    5. 有一个连带后果值得主动说,因为它看起来像是前面的设计白做了:任务选取上收之后,**之前靠状态层挡住的重复劳动,现在把状态层关掉也不会重现**——选谁做已经不看上下文了。这不是前面白学了,是那个退化被一个更强的结构取代。跨窗口摘要仍然有用(让模型知道整体进度、让人能读),但它不再是正确性的依赖。
    6. 可预期的追问有两个。一是「那提示词里还要不要写」:要写,但它的角色降级成解释而不是保证,真正的闸门在循环里。二是「一次只做一条会不会把进度拖慢」:这条约束限制的是**每一步上下文里放几条任务**,不是每步能写多少代码,步数与完成节奏都没变——变的只是每一步挑谁做这件事有了一个不会漂移的依据。

    How to reason about it · think before answering

    1. This question is about where a constraint lives. The usual answer - write 'do one feature at a time' in the prompt, and write it emphatically - is also the least reliable one. A prompt is a request, not a constraint. The model can ignore it, and when it does nothing raises an error: what you find in the morning is a pile of half-done items, not an error message.
    2. Start by asking who is choosing the task right now. The common design pastes the whole feature list into the context and lets the model pick the first item without evidence of completion. It works day to day because the evidence happens to be correct - but the basis for the decision lives in the context. Compact it once, drop one line from a summary, change the shape of the evidence, and the model picks wrong and quietly builds something it should not have.
    3. The fix is to take task selection back from the model and give it to the harness: the harness reads the checklist on disk, picks the next item, and renders only that one item into the context. The target in this course has forty items; the model sees exactly one per step and the other thirty-nine are not in front of it. Skipping ahead is not forbidden - there is no longer any way to express it. That is what enforcement means.
    4. The conclusion is deliverable as is: move the basis for the decision from the context to disk and the constraint goes from discouraged to impossible. A prompt can only reach the first of those. The pattern transfers to any rule you do not want the model improvising around - ask first whether that rule currently lives in the context or in the code.
    5. One consequence is worth volunteering because it looks like earlier work was wasted: once selection moves to the harness, the duplicated work that the state layer used to prevent no longer reappears even with the state layer switched off, because selection does not consult the context any more. Nothing was wasted - that regression was replaced by a stronger structure. The cross-window summary still earns its place (it tells the model where the project stands and it is readable by a human), but correctness no longer depends on it.
    6. Two follow-ups to expect. Should the prompt still say it? Yes, but demoted from guarantee to explanation; the real gate is in the loop. Does one-at-a-time slow things down? The constraint limits how many tasks appear in a step's context, not how much code a step may write. Step count and pace are unchanged - what changed is that choosing the next item now rests on something that cannot drift.

    答题要点

    • 提示词是请求不是约束:模型不照办时没有任何东西会报错。
    • 先问谁在挑任务:贴整张清单等于把判断依据放在上下文里。
    • 上下文会被压缩、摘要会漏写,依据一坏模型就安静地做错一条。
    • 正解是 harness 读磁盘上的清单挑一条,只把这一条渲染进上下文。
    • 四十条里模型只看得见一条,跳着做没有表达它的入口,这才叫强制。
    • 判断依据从上下文搬到磁盘,约束就从不被鼓励变成不可能发生。
    • 连带后果:退化不再依赖状态层挡着,摘要降级为可读性而非正确性依赖。

    Key points

    • A prompt is a request, not a constraint: when it is ignored, nothing raises an error.
    • Ask who picks the task: pasting the whole list puts the decision basis in the context.
    • Contexts get compacted and summaries drop lines, so a bad basis silently builds the wrong item.
    • The fix: the harness reads the checklist on disk and renders only the chosen item.
    • One of forty items is visible per step, so skipping ahead has no way to be expressed.
    • Move the decision basis from context to disk and the constraint goes from discouraged to impossible.
    • Side effect: the regression no longer depends on the state layer; summaries serve readability, not correctness.
  • Agent 做坏了一条 feature,你会让它回滚还是继续修?依据是什么?An agent botches one feature - do you roll back or let it keep fixing? On what basis?
    国内高频海外高频深入#rollback#green-point#risk-exposure

    分析过程 · 先想清楚再作答

    1. 这题考的是**回滚的判据**,不是回滚这个动作。答「看情况」「能修就修、修不好就退」没有任何区分度,因为它对任何一次失败都成立。面试官想听的是两件事:你拿什么量去做这个比较,以及你回退过去的那个点凭什么可信。后一半比前一半更容易被忽略,而它恰恰是这题真正的分水岭。
    2. 先把回退目标定死:**回退的目标只能是绿点**——通过端到端验证**并且**已提交的那个点。硬回退会丢掉未提交的改动,这正是我们要的效果,因为回退的目的就是把工作区恢复成一个已经验证过的样子。但反过来,退到一个没验证过的提交,等于把一个未知状态当成已知状态,比不退更糟。
    3. 比较用的量不是产出,是**风险敞口**。本课实测跑了两种提交粒度的对照:一条一提交与一个窗口提交一次,最后完成条数都是 **9**,一模一样;差别在「未提交工作的最大暴露量」,从 **3 条**降到 **0 条**。如果只盯产出看,这一天的设计会显得毫无意义——这正是它值得考的原因。
    4. 把它变成一场灾难就看得见了:已完成 8 条、正在做第 9 条时工作区被写坏,两边各退到自己最近一个全部验证过的提交。按窗口提交的那边退到「窗口 2 收尾,已完成 6 条」,保住 6 条丢掉 2 条已经做完并验证过的工作;一条一提交的那边退到第 8 条对应的那个提交,保住 8 条丢 0 条。注意**按窗口提交并不是没有可回退点**,它的收尾提交同样能退,差的是**粒度**:可回退点每三条才有一个,退回去就要连累旁边两条。
    5. 于是判据出来了:先看这一条的失败有没有污染已经验证过的工作。没污染就继续修(回退会连带丢掉旁边的成果);一旦工作区状态不可信,就退到最近的绿点。而让这个决定变得廉价的前提是**绿点足够密**——「一次只做一件事」不是风格偏好,它是把这个损失压到零的唯一办法。
    6. 还有一条诚实的边界值得主动交代:在把端到端验证接进循环之前,这些提交点严格说**还没有资格叫绿点**。绿点的定义是「通过端到端验证并且已提交」,验证还是浅检查(补丁写进去就算过)时,前半句是空的。面试里主动区分「提交点」与「绿点」,比把两者混着叫更有说服力。
    7. 可预期的追问是「退回去之后清单怎么办」。清单必须和代码改动进**同一个 commit**,所以它跟着一起回到那个点,两者在任何一个提交上都同时正确。如果它们分在两个提交里,回退就会退出「代码退了、清单还记着已完成」这种最难查的不一致——下一步做什么会从一份错的清单里挑出来。

    How to reason about it · think before answering

    1. This tests the criterion for rolling back, not the act of rolling back. 'It depends' and 'fix it if you can, otherwise revert' earn nothing, because they hold for every failure equally. The interviewer wants two things: which quantity you compare, and why the point you revert to can be trusted. The second half is the one people skip, and it is where this question actually separates candidates.
    2. Pin the target first: the only legal rollback target is a green point - a point that passed end-to-end verification and has been committed. A hard reset discards uncommitted changes, which is exactly what you want, because the purpose is to restore the workspace to a state that was verified. Revert to an unverified commit instead and you have promoted an unknown state to a known one, which is worse than not reverting.
    3. The quantity to compare is not output, it is risk exposure. This course measured two commit granularities side by side: one commit per feature versus one commit per window. Both finished the same nine items - identical. What differed was the peak amount of completed-but-uncommitted work, which dropped from three items to zero. Judged on output alone the whole day looks pointless, which is precisely why it makes a good question.
    4. Stage a disaster and it becomes visible: eight items done, the ninth in flight, and the workspace gets corrupted. Each side reverts to its most recent fully verified commit. The per-window side lands on 'window 2 wrap-up, 6 items done' and keeps six, losing two items of finished, verified work. The per-feature side lands on the commit for item eight and keeps all eight, losing nothing. Note that per-window is not without rollback points - its wrap-up commits are perfectly revertible. What it lacks is granularity: a rollback point every three items means reverting drags two neighbors down with it.
    5. So the criterion: first ask whether this failure contaminated already-verified work. If it did not, keep fixing - reverting would throw away neighboring results for nothing. Once the workspace state cannot be trusted, revert to the nearest green point. What makes that decision cheap is green points being dense, which is why one-at-a-time is not a style preference: it is the only way to drive that loss to zero.
    6. One honest boundary is worth stating unprompted: before end-to-end verification is wired into the loop, these commit points do not yet qualify as green points. A green point is verified and committed; while verification is still a shallow check - the patch landed, therefore it passed - the first half is empty. Distinguishing commit point from green point in an interview reads far better than using the terms interchangeably.
    7. Expected follow-up: what happens to the checklist after a rollback? The checklist must ride in the same commit as the code change, so it returns to that point too and the two are correct together at every commit. Split them across two commits and a rollback produces the hardest inconsistency to trace - the code reverted while the checklist still claims the work is done - and the next item gets chosen from a checklist that is lying.

    答题要点

    • 回退目标只能是绿点:通过端到端验证并且已提交,退到未验证的点更糟。
    • 比较的量是风险敞口不是产出:两种粒度完成条数都是 9,一模一样。
    • 差的是未提交工作的最大暴露量:3 条降到 0 条。
    • 同一场灾难:已完成 8 条时出事,按窗口提交丢 2 条,一条一提交丢 0 条。
    • 按窗口提交不是没有可回退点,差的是粒度——退一次连累旁边两条。
    • 判据:没污染已验证的工作就继续修,工作区不可信就退到最近绿点。
    • 诚实边界:验证还是浅检查时,那些点只是提交点,还不配叫绿点。

    Key points

    • The only rollback target is a green point: verified end to end and committed.
    • Compare risk exposure, not output: both granularities finished the same nine items.
    • What differs is peak uncommitted work: three items down to zero.
    • Same disaster at eight items done: per-window loses two, per-feature loses none.
    • Per-window does have rollback points; it lacks granularity, so reverting drags neighbors down.
    • Criterion: keep fixing if verified work is uncontaminated, revert once the workspace is untrustworthy.
    • Honest boundary: while verification is shallow these are commit points, not yet green points.
  • 让 Agent 操作 git 有哪些具体的危险?你会怎么在代码层面防住?What can go wrong when an agent drives git, and how do you prevent it in code?
    国内高频海外高频深入#git#safety-invariants#assertions

    分析过程 · 先想清楚再作答

    1. 这题问的是危险**加**防法,两半都要给,只给其中一半都算没答完。只答「小心一点」「加人工 review」拿不到分——无人值守的前提就是没有人在旁边 review,任何依赖人当场把关的方案在这里都不成立。要拿出的是几条能写进代码、并且能被断言钉住的不变量。
    2. 第一个危险最大也最容易被忽略:**工作目录解析错。** agent 自己提交本身没问题,前提是它提交到自己的工作区;可一旦 cwd 被继承成别的目录,那些 commit 就打进了你真正的仓库历史,那一瞬间它做的正是交互式场景里明令禁止的越权。防法:所有 git 调用收口到一个函数,工作目录是**必填**的第一个参数,每次显式传 `-C`,禁止依赖继承的 cwd;再配一条静态检查,断言 git 这个字面量不出现在那个文件之外。
    3. 第二个危险是**代码改动与清单改动分在两个 commit 里**。本课做过一次变异实验:把标记通过时那一行写盘去掉,其它一个字不动,程序照样跑、照样做完九条、前几天的断言照样全绿,只有磁盘上的清单一直停在零条已通过;回退实验里那一边直接变成「9 条补丁 / 0 条清单」。防法很简单——把改清单与提交绑成同一个动作,并加一条断言核对磁盘清单与权威状态对不对得上。
    4. 这类故障的共同形状值得单独记住:**内存里对、磁盘上错。** 它不报错也不崩,只在你回退、重启、或者换个人来看这个仓库的时候才暴露。所以凡是「内存里有一份、磁盘上也有一份」的地方,都得有人负责发现两者分叉——分叉本身不可怕,**没人看得见的分叉**才可怕。
    5. 第三个危险是硬回退的目标选错。它会丢掉未提交的改动,这是回退想要的效果,但也意味着没有第二次机会。防法是把「一个可回退目标都没有」写成显式抛错,而不是默默退到当前 HEAD 假装成功——后者会让一次失败的回退看起来像一次成功的回退。
    6. 再给一条设计取向,它同时降低了前面几类风险:绿点直接查提交历史,**不在自己的状态文件里另存一份**。两份记录就有两份不一致的可能,而 git 已经是一个可靠的、带顺序的、可回退的账本。实现上还有个能提的小坑——按提交信息前缀过滤时不要开扩展正则,左括号会被当成分组符,git 直接报括号不配对。
    7. 可预期的追问是「这些不变量怎么保证不随时间退化」。靠断言,不靠纪律:每一条危险配一条能跑的检查——收口那条用静态扫描,一致性那条用磁盘清单与状态对照,回退那条用「人为弄坏一条再看它退到哪」。写完之后一定要做变异检验,把防线关掉确认对应的检查真的转红,否则你钉住的可能只是一条恒真的断言。

    How to reason about it · think before answering

    1. This asks for the dangers and the defenses, and you owe both halves - giving only one is an unfinished answer. 'Be careful' and 'add human review' score nothing, because the whole premise of unattended work is that nobody is there to review, so any plan resting on a human catching it in the moment is void here. What you should produce is a handful of invariants that can be written into code and pinned by assertions.
    2. The biggest danger is also the easiest to overlook: resolving the working directory wrongly. An agent committing for itself is fine as long as it commits into its own workspace, but let the cwd be inherited from somewhere else and those commits land in your real repository's history - which is exactly the overreach the interactive case forbids. The defense: funnel every git call through one function, make the working directory a required first argument, pass it explicitly on every invocation, never rely on an inherited cwd, and add a static check asserting the literal git appears nowhere outside that file.
    3. The second danger is splitting the code change and the checklist change across two commits. This course ran the mutation: remove the single line that writes the checklist back to disk when an item is marked passed, change nothing else, and the program still runs, still completes nine items, and every earlier assertion stays green - while the checklist on disk sits at zero passed. In the rollback experiment that side becomes nine patches against zero checklist entries. The defense is small: bind updating the checklist and committing into one action, and assert that the on-disk checklist agrees with authoritative state.
    4. The shape shared by this class of failure is worth memorizing on its own: right in memory, wrong on disk. Nothing errors, nothing crashes; it surfaces only when you roll back, restart, or hand the repository to somebody else. So anywhere a fact lives both in memory and on disk, something must be responsible for noticing divergence. Divergence is not the danger - divergence nobody can see is.
    5. The third danger is choosing the wrong target for a hard reset. It discards uncommitted changes, which is the point, but it also means there is no second chance. The defense is to make 'there is no rollback target at all' an explicit error rather than silently resetting to the current head and reporting success - that turns a failed rollback into something that looks like a successful one.
    6. One more design stance, which lowers several of these risks at once: read green points straight out of the commit history instead of keeping a second copy in your own state file. Two records means two ways to disagree, and git is already a reliable, ordered, rewindable ledger. A small implementation trap worth mentioning: when filtering by commit-message prefix, do not switch on extended regular expressions - the opening parenthesis becomes a grouping operator and git simply reports unbalanced parentheses.
    7. Expected follow-up: how do these invariants avoid decaying over time? Through assertions, not discipline. Each danger gets a runnable check - a static scan for the single git exit, a disk-versus-state comparison for consistency, and a deliberate corruption to see where a rollback actually lands. Then run the mutation: switch the defense off and confirm the matching check really turns red, or what you pinned may just be a tautology.

    答题要点

    • 最大危险是工作目录解析错:commit 会打进真实仓库的历史。
    • 防法:git 调用收口到一个函数,工作目录必填、每次显式传,并加静态扫描断言。
    • 第二个危险是代码与清单分在两个 commit:回退会留下最难查的不一致。
    • 实测变异:去掉写盘那一行,程序照跑照绿,回退实验变成 9 条补丁 / 0 条清单。
    • 共同形状是内存里对、磁盘上错——不报错,只在回退或换人接手时暴露。
    • 硬回退没有目标时必须显式抛错,不能默默退到 HEAD 假装成功。
    • 绿点查提交历史不另存一份;过滤提交信息前缀时别开扩展正则。

    Key points

    • The biggest danger is a mis-resolved working directory: commits land in the real repository.
    • Defense: one git entry point, a required explicit working directory, plus a static scan asserting it.
    • Second danger: code and checklist in separate commits leaves the hardest inconsistency to trace.
    • Measured mutation: drop the write-to-disk line and everything stays green, yet rollback gives nine patches and zero checklist entries.
    • Shared shape: right in memory, wrong on disk - silent until a rollback, a restart, or a new pair of eyes.
    • A hard reset with no target must raise an explicit error, never quietly reset to head and claim success.
    • Read green points from the commit history; and do not enable extended regex when filtering message prefixes.

评论