One Thing at a Time: The Feature List and git Discipline
Today opens on a reversal. The hand-built Coding Agent course ruled explicitly that the agent must never commit to git, while in this course a git commit is the authoritative record of progress. Both rulings are right, because the premises differ. Get that distinction and the rest follows: list-driven task selection, and rolling back to the last verified commit.
Today's Goals
- Can explain why an interactive agent must not touch the user's git history while an unattended agent's commits are its authoritative progress, and point at the premise that separates the two cases
- Can explain why the one-feature-at-a-time rule works better as code than as a line in the prompt, and name the two failures that appear once you relax it
- Can state what a verified commit is, and when to roll back to the last one versus when to push forward
Today starts with a conflict that is supposed to confuse you for a moment. When you finish, scroll back up and tick off the three goals.
Plain-Language Walkthrough
A reversal: the same question, two courses, opposite answers
If you took Build Your Own Coding Agent in 21 Days, you will remember an explicit ruling there: never use git to commit, because the user's repository history is not yours to pollute. Snapshots were taken by content hash instead.
This course concludes the opposite: a git commit is the authoritative record of progress, and the agent makes those commits itself.
Both are correct. One sentence separates them: whose repository is this.
| Interactive Coding Agent | Unattended harness | |
|---|---|---|
| Whose repository it operates on | The user's repository | The agent's own workspace |
| Who reads that history | The user, their colleagues, their code review | Only the harness, and you when you check the work in the morning |
| What a new commit in it means | Overreach. The user's git log belongs to the user | Bookkeeping. Those commits are the progress itself |
| Cost of not committing | None. Hash snapshots roll back just as well | No landing point to return to, so an incident means redoing the whole stretch |
The reversal is worth remembering for more than today's design. It demonstrates a way of reading advice: when two sources you trust reach opposite conclusions, do not start by deciding which one is wrong. Go find the premise each of them is standing on. Most conflicting best practices are conflicting premises.
A construction sign-off sheet: nothing proceeds until the last item is signed
Now the second idea of the day, and its analogy.
A building site keeps a sign-off sheet. Every line says what counts as done, and nothing proceeds until the previous line is signed. Why sign line by line instead of inspecting everything once at the end?
Because when something is wrong, you need to know how far back to go. Close up the wall before the wiring is signed off, discover after the paint is dry that an outlet is in the wrong place, and what you are tearing out is not an outlet. It is the wall.
The agent's feature list is that sheet. "One at a time" is the rule that nothing proceeds until the previous line is signed.
One at a time: put it in the code, not in the prompt
Most people reach first for a line in the prompt: "please implement only one feature at a time." That line helps. It is not firm.
Where it is soft: a prompt is a request, and code is a fact. The model may do two neighboring items together, because it can see both and they really do look alike. The line may be dropped when the context is compacted. A different model may read it differently. None of those raise an error.
Today's approach makes the constraint structurally impossible to violate:
- the harness reads the list on disk and picks the next item
- the context carries that one item only, and the other thirty-nine are not visible to the model at all
You cannot act on what you cannot see. This stops being a request and becomes a fact.
Relax the constraint and two failures appear. They look nothing alike.
First, jumping ahead. The model picks an item whose dependencies are not satisfied yet, pagination before the listing it paginates. It writes code that refers to something that does not exist, and a shallow check cannot tell.
Second, batching. The model implements three items in one go. That looks like a speedup, and the price only shows up during an incident: three items are tangled into one change, one of them is wrong, and you cannot take back only that one. Batching a commit binds the fate of every item inside it.
Moving the criterion from the context to the disk
Through D1 to D3 it was really the model choosing the task. The harness pasted all forty list items into the context and the model picked the first one without evidence of completion. Nothing went wrong on that path, because the evidence happened to be right every time.
The design has a structural problem all the same: the criterion lives in the context. Compact the context, drop one line while summarizing, get one piece of evidence in an unexpected format, and the model picks wrong. Nothing raises an error when it does.
Moving the criterion to disk has a side effect worth stating on its own:
As of today, the D1 degradation cannot happen.
The D2 comparison relied on switching off the state layer so the model would find no evidence in the context and redo work. Selection no longer consults the context at all. So in today's self-test, the state layer on and off both produce zero duplicated work. That does not make D2 wasted. It means D2 has been superseded by something stronger: D2 made the degradation stop happening, D4 makes it impossible.
The difference is very concrete in engineering terms. "Stops happening" depends on one correct information path staying correct, with the summary phrased right and the convention token untouched, and the D2 mutation check already showed how easily one word breaks that path. "Impossible" depends on no path at all.
The verified commit: passed verification, and already committed
Here is the precise definition of the term that will come back every remaining day:
A verified commit is a point that passed end-to-end verification and has already been committed.
Both halves are required, and the order matters. An unverified commit is not a verified commit, because rolling back to it means treating an unknown state as a known one. A verified state that was never committed is not one either, because it does not exist anywhere you can return to.
The difference is not output, it is exposure
Today's lab runs the same code twice. The only difference is commit granularity: one commit per item, or one commit at the end of each window. The run prints something like this. The lab prints in Chinese, because both editions of this course share one codebase; the numbers are what matter:
completed items 9 -> 9 (unchanged)
per-feature verified commits 0 -> 9
maximum exposure of uncommitted work 3 -> 0The amount of work finished is identical. That matters, because otherwise you would read batching as a dumber implementation. It is not dumber. It merely accumulates risk.
So the lab goes on to stage a disaster: eight items done, the ninth in flight, and the workspace gets corrupted, whether a patch was half written when the process died or a change mangled a file. Both runs fall back to their own most recent commit where everything had passed:
per-window: back to "end of window 2, 6 items done"
keeps 6, loses 2 items of finished and verified work
per-feature: back to "feat(F08): search note titles ..."
keeps 8, loses 0One thing gets misreported often, so be careful with it: the batching run is not without rollback points. Its end-of-window commits roll back perfectly well. The problem is granularity: there is one landing point every three items, so going back takes the two neighbors down with it.
"Maximum exposure of uncommitted work" is the amount of work an incident destroys, and it is the price tag on the one-thing-at-a-time discipline. One commit per item drives it to zero.
Rollback policy: when to go back, when to push forward
Having verified commits is not enough. You need rules for using them. Today sets the criteria, and D5 makes the trigger automatic.
| Situation | What to do | Why |
|---|---|---|
| The workspace is broken, code will not run, files are mangled | Roll back to the last verified commit | Pushing forward means building on a foundation known to be damaged |
| One feature keeps failing, but the workspace is fine | Do not roll back. Switch to another item | Rolling back throws away good work, and that item does not get easier because you did |
| One feature failed, and it dirtied the workspace | Roll back, then switch to another item | Restore the foundation first, then route around the item |
| The list and the code disagree | Roll back, and find out why they diverged | This is the dangerous one. It means something is lying |
The second row is the one people get wrong: rolling back on every failure is an overreaction. A rollback exists to restore a trustworthy foundation, not to punish a failure. If the foundation is intact, stay where you are.
Source Reading
Today adds src/plan/features.ts and extends src/core/git.ts. Three places repay a slow read.
Position one: task selection. The rule is deliberately boring.
// Pick the next item: in id order, the first one that has not passed yet.
// It is enough because every dependency among the target's forty items points at a
// lower-numbered item (the dependency lives in the wording of the description), so
// working in id order is always a legal order. A real project would swap in a
// topological sort here, and the shape of the interface -- one item out at a time --
// would not change. That shape is the thing being established today.
function selectNext(features, state) {
const done = new Set(state.done)
return features.find((f) => !f.passes && !done.has(f.id)) ?? null
}
// Once picked, the context carries only this item. The model cannot see any other
// task, so jumping ahead is not something it is able to do.
const task = selectNext(loadFeatureList(repoDir), state)
const context = `${opening}\n\nThe one thing to do in this step:\n- ${task.id} [${task.category}] ${task.description}`# Pick the next item: in id order, the first one that has not passed yet.
# It is enough because every dependency among the target's forty items points at a
# lower-numbered item (the dependency lives in the wording of the description), so
# working in id order is always a legal order. A real project would swap in a
# topological sort here, and the shape of the interface -- one item out at a time --
# would not change. That shape is the thing being established today.
def select_next(features, state):
done = set(state.done)
return next((f for f in features if not f.passes and f.id not in done), None)
# Once picked, the context carries only this item. The model cannot see any other
# task, so jumping ahead is not something it is able to do.
task = select_next(load_feature_list(repo_dir), state)
context = f"{opening}\n\nThe one thing to do in this step:\n- {task.id} [{task.category}] {task.description}"Notice that selectNext reads loadFeatureList(repoDir), the list on disk, not the array already in memory. This is the same discipline as reading state from disk on D2 and reading init.sh from disk on D3: anything that has to survive a process boundary on D6 must travel through the disk starting today.
Position two: the list file and the code go into the same commit. This is the easiest thing to get wrong today.
// Two things happen after verification passes. The order does not matter,
// but they must land in the same commit
markPassed(repoDir, features, feature.id) // edit the list, and write it back to disk
state.done.push(feature.id)
// The verified commit: the code change (server.mjs) and the list change
// (features.json) are committed together. Roll back to this point and both go back,
// so you never get "the code reverted while the list still claims it is done"
commitAll(repoDir, `feat(${feature.id}): ${feature.description.slice(0, 40)}`)
// The broken version: edit memory, never touch the disk. The program still runs,
// still finishes nine items, and every assertion from D1 to D3 still passes.
// Only the list on disk stays frozen at "nothing passed"
function markPassedBroken(repoDir, features, featureId) {
features.find((f) => f.id === featureId).passes = true
// missing: saveFeatureList(repoDir, features)
}# Two things happen after verification passes. The order does not matter,
# but they must land in the same commit
mark_passed(repo_dir, features, feature.id) # edit the list, and write it back to disk
state.done.append(feature.id)
# The verified commit: the code change (server.mjs) and the list change
# (features.json) are committed together. Roll back to this point and both go back,
# so you never get "the code reverted while the list still claims it is done"
commit_all(repo_dir, f"feat({feature.id}): {feature.description[:40]}")
# The broken version: edit memory, never touch the disk. The program still runs,
# still finishes nine items, and every assertion from D1 to D3 still passes.
# Only the list on disk stays frozen at "nothing passed"
def mark_passed_broken(repo_dir, features, feature_id):
next(f for f in features if f.id == feature_id).passes = True
# missing: save_feature_list(repo_dir, features)That broken version is today's mutation check. Among the lines it turns red, this is the one that says the most:
FAIL rollback leaves code and list consistent
per-feature: 9 patches on disk / 0 items in the listNine implementations are sitting in the code, and the list says none of them was done. The failure raises nothing and warns about nothing. It surfaces when you roll back, when you restart, or when somebody else opens the repository, and at that moment the harness starts again from the first item and writes all nine a second time.
Position three: verified commits are read from git log, not tracked separately.
// Verified commits are read straight out of git log, never stored a second time
// inside RunState. Two copies means two chances to disagree, and git is already a
// reliable, ordered, revertible ledger.
function greenPoints(workdir) {
// Deliberately the default basic regular expression. Do not add --extended-regexp:
// in extended syntax an opening parenthesis is a group, and git reports
// 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') }
})
}# Verified commits are read straight out of git log, never stored a second time
# inside RunState. Two copies means two chances to disagree, and git is already a
# reliable, ordered, revertible ledger.
def green_points(workdir):
# Deliberately the default basic regular expression. Do not add --extended-regexp:
# in extended syntax an opening parenthesis is a group, and git reports
# parentheses not balanced
raw = run_git(workdir, ["log", "--grep=^feat(", "--format=%H%x09%s"])
out = []
for line in filter(None, raw.split("\n")):
sha, _, subject = line.partition("\t")
out.append({"sha": sha, "subject": subject})
return outThere is a judgment here worth more than the code: when an authoritative record already answers the question, do not build a second record to answer it again. Every extra copy is one more chance to diverge from the truth, and divergence itself is not the frightening part. Divergence nobody can see is.
On that regular expression: switch to extended syntax and git treats the opening parenthesis as a group, then refuses with unbalanced parentheses. The lab hit this while it was being written, which is why the comment stayed in the code.
Hands-On Lab
Four exercises: one missing write-to-disk line in markPassed in plan/features.ts, plus commitAll, greenPoints and rollbackTo in core/git.ts. The frozen files, the D2 state layer and the D3 initialization phase all carry over verbatim.
- Read the task selection block in src/core/loop.ts first, and see why the context ends up holding exactly one list line.
- Restore the write-to-disk line in markPassed, and work out the exact moment its absence would surface.
- Fill in commitAll and greenPoints. Do not commit when nothing changed, and do not add the extended regular expression flag.
- Fill in rollbackTo, guarding against an empty target before reset --hard.
- Run MOCK=1 pnpm selftest until all 57 self-test assertions are green, then MOCK=1 pnpm start to watch the disaster simulation.
Today's mutation check is to comment out the write-to-disk line inside markPassed. The run still reports nine of forty items complete, while features.json on disk reports zero passed. Five self-test assertions turn red, and the one reading 9 patches on disk / 0 items in the list is the closest thing to a real production incident this course has shown so far.
Interview Questions
Today's four questions circle the reversal, the structural constraint behind list-driven work, and the definition of a verified commit together with the rollback policy.
The first one shows up in some form almost every time: the interviewer hands you two opposite practices and asks which you would choose. The right answer is not to pick one. It is to ask about the premise first. That habit is the most valuable thing in today's material.
Checklist and Tomorrow
- Can explain why an interactive agent must not touch the user's git history while an unattended agent's commits are its authoritative progress, and point at the premise that separates the two cases
- Can explain why the one-feature-at-a-time rule works better as code than as a line in the prompt, and name the two failures that appear once you relax it
- Can state what a verified commit is, and when to roll back to the last one versus when to push forward
- Can say why the code change and the list change must land in the same commit, and name the exact moment splitting them fails
- Got all 57 self-test assertions green with
MOCK=1 pnpm selftest, and saw how much each granularity loses in the disaster simulation - Ran the commented-out write-to-disk mutation check once, and can describe the shape of a right-in-memory, wrong-on-disk failure
- Can answer at least three of the four interview questions without looking at the key points
Tomorrow is D5, Self-Verification: End-to-End Gates, Premature Completion Claims and Automatic Intervention. Today left an obvious hole open: verification is still a shallow check, and a written patch counts as a pass. So a verified commit is still only half honored, committed but never verified. Tomorrow supplies the other half, and two phenomena emerge on their own along the way: premature completion claims, where the service starts, the end-to-end check is red, and the model reports the work as finished anyway, and the doom loop, where the same item is attempted over and over while the list does not move. Then comes the harder half, which is not detection but what to do automatically once something is detected. Tomorrow turns today's rollback policy table into code.
Interview questions
When may an agent run git commit on its own, and when must it never?什么情况下 Agent 可以自己执行 git commit,什么情况下绝对不行?
Common in ChinaCommon overseasIntermediate#git#state-management#boundariesHow to reason about it · think before answering
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
分析过程 · 先想清楚再作答
- 这题表面在问 git 规范,实际考的是**一条工程结论能不能被前提推翻**。直接背「Agent 不许碰 git」或者「Agent 当然该自己提交」都会被反例打穿,因为两种说法各有一门课的明确裁定撑着:手搓 Coding Agent 那门课裁定过不能用 git 提交、改用内容哈希做快照,而无人值守场景里 commit 就是进度的权威记录。面试官等的不是站队,是那个能把两边分开的判据。
- 拆法是先问两句话:**这个仓库是谁的?谁会读它的历史?** 交互式 Coding Agent 操作的是**用户的仓库**,那份 git log 是用户自己的东西,工具往里塞机器生成的记录属于越权;而且不提交并没有代价——内容哈希快照一样能回退。既然收益为零、代价是污染别人的历史,结论自然是不提交。
- 反过来,无人值守时 agent 操作的是**自己的工作区**。一整夜没有第二个人会读那份历史,读它的只有下一个窗口和第二天早上的你。这时 commit 不是污染而是记账:提交点就是进度本身,而不提交的代价非常实在——没有可回退的点,出事只能整段重来。
- 所以结论可以压成一句话直接答出去:**判据不是「能不能提交」,是「这个仓库是谁的、谁会读它的历史」。** 同一个动作在两个前提下有两个相反的正解,这不是矛盾,是前提不同。能主动指出前提的人,通常也能把这套判断迁移到别的场景。
- 顺带一条值得主动说的设计取向:既然提交历史已经是权威记录,就不要在自己的状态文件里再存一份「已完成点列表」,直接按提交信息前缀查 git log 就行。两份记录就有两份不一致的可能,而 git 本身已经是一个可靠的、带顺序的、可回退的账本。通用判据是——**能让现成的权威记录回答的问题,不要另建一套记录。**
- 可预期的追问是「那 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 自己的工作区:提交就是记账,不提交才有代价——没有可回退的点。
- 两门课结论相反而都对,因为前提不同,不是其中一边写错了。
- 既然提交历史已是权威记录,就不要在状态文件里另存一份完成点列表。
- 通用判据:能让现成的权威记录回答的问题,不要另建一套记录。
- 边界模糊的后果:工作目录解析错一次,commit 就打进真实仓库的历史。
How do you force an agent to do one thing at a time? Is putting it in the prompt enough?怎么强制一个 Agent 一次只做一件事?写在提示词里够吗?
Common in ChinaCommon overseasIntermediate#feature-list#constraints#harness-designHow to reason about it · think before answering
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
分析过程 · 先想清楚再作答
- 这题考的是**约束写在哪里**。最常见的答案是「在提示词里把『一次只做一条』写清楚、写重一点」,它也是最不可靠的答案:提示词是一个请求,不是一个约束。模型可以不照办,而且不照办之后没有任何东西会报错——你早上拿到的是一堆做了一半的条目,而不是一条错误信息。
- 拆的第一步是问:**现在是谁在挑任务?** 常见做法是把整张 feature 清单贴进上下文,让模型按「第一条没有完成证据的」自己挑。这条路平时不出错,靠的是证据一直是对的;但判断依据活在上下文里,压缩一次、摘要漏写一条、某次证据格式对不上,模型就会挑错,而且它只是安静地做了一条不该做的。
- 正解是把任务选取从模型手里收回到 harness: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.
答题要点
- 提示词是请求不是约束:模型不照办时没有任何东西会报错。
- 先问谁在挑任务:贴整张清单等于把判断依据放在上下文里。
- 上下文会被压缩、摘要会漏写,依据一坏模型就安静地做错一条。
- 正解是 harness 读磁盘上的清单挑一条,只把这一条渲染进上下文。
- 四十条里模型只看得见一条,跳着做没有表达它的入口,这才叫强制。
- 判断依据从上下文搬到磁盘,约束就从不被鼓励变成不可能发生。
- 连带后果:退化不再依赖状态层挡着,摘要降级为可读性而非正确性依赖。
An agent botches one feature - do you roll back or let it keep fixing? On what basis?Agent 做坏了一条 feature,你会让它回滚还是继续修?依据是什么?
Common in ChinaCommon overseasDeep dive#rollback#green-point#risk-exposureHow to reason about it · think before answering
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 条、正在做第 9 条时工作区被写坏,两边各退到自己最近一个全部验证过的提交。按窗口提交的那边退到「窗口 2 收尾,已完成 6 条」,保住 6 条丢掉 2 条已经做完并验证过的工作;一条一提交的那边退到第 8 条对应的那个提交,保住 8 条丢 0 条。注意**按窗口提交并不是没有可回退点**,它的收尾提交同样能退,差的是**粒度**:可回退点每三条才有一个,退回去就要连累旁边两条。
- 于是判据出来了:先看这一条的失败有没有污染已经验证过的工作。没污染就继续修(回退会连带丢掉旁边的成果);一旦工作区状态不可信,就退到最近的绿点。而让这个决定变得廉价的前提是**绿点足够密**——「一次只做一件事」不是风格偏好,它是把这个损失压到零的唯一办法。
- 还有一条诚实的边界值得主动交代:在把端到端验证接进循环之前,这些提交点严格说**还没有资格叫绿点**。绿点的定义是「通过端到端验证并且已提交」,验证还是浅检查(补丁写进去就算过)时,前半句是空的。面试里主动区分「提交点」与「绿点」,比把两者混着叫更有说服力。
- 可预期的追问是「退回去之后清单怎么办」。清单必须和代码改动进**同一个 commit**,所以它跟着一起回到那个点,两者在任何一个提交上都同时正确。如果它们分在两个提交里,回退就会退出「代码退了、清单还记着已完成」这种最难查的不一致——下一步做什么会从一份错的清单里挑出来。
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.
答题要点
- 回退目标只能是绿点:通过端到端验证并且已提交,退到未验证的点更糟。
- 比较的量是风险敞口不是产出:两种粒度完成条数都是 9,一模一样。
- 差的是未提交工作的最大暴露量:3 条降到 0 条。
- 同一场灾难:已完成 8 条时出事,按窗口提交丢 2 条,一条一提交丢 0 条。
- 按窗口提交不是没有可回退点,差的是粒度——退一次连累旁边两条。
- 判据:没污染已验证的工作就继续修,工作区不可信就退到最近绿点。
- 诚实边界:验证还是浅检查时,那些点只是提交点,还不配叫绿点。
What can go wrong when an agent drives git, and how do you prevent it in code?让 Agent 操作 git 有哪些具体的危险?你会怎么在代码层面防住?
Common in ChinaCommon overseasDeep dive#git#safety-invariants#assertionsHow to reason about it · think before answering
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
分析过程 · 先想清楚再作答
- 这题问的是危险**加**防法,两半都要给,只给其中一半都算没答完。只答「小心一点」「加人工 review」拿不到分——无人值守的前提就是没有人在旁边 review,任何依赖人当场把关的方案在这里都不成立。要拿出的是几条能写进代码、并且能被断言钉住的不变量。
- 第一个危险最大也最容易被忽略:**工作目录解析错。** agent 自己提交本身没问题,前提是它提交到自己的工作区;可一旦 cwd 被继承成别的目录,那些 commit 就打进了你真正的仓库历史,那一瞬间它做的正是交互式场景里明令禁止的越权。防法:所有 git 调用收口到一个函数,工作目录是**必填**的第一个参数,每次显式传 `-C`,禁止依赖继承的 cwd;再配一条静态检查,断言 git 这个字面量不出现在那个文件之外。
- 第二个危险是**代码改动与清单改动分在两个 commit 里**。本课做过一次变异实验:把标记通过时那一行写盘去掉,其它一个字不动,程序照样跑、照样做完九条、前几天的断言照样全绿,只有磁盘上的清单一直停在零条已通过;回退实验里那一边直接变成「9 条补丁 / 0 条清单」。防法很简单——把改清单与提交绑成同一个动作,并加一条断言核对磁盘清单与权威状态对不对得上。
- 这类故障的共同形状值得单独记住:**内存里对、磁盘上错。** 它不报错也不崩,只在你回退、重启、或者换个人来看这个仓库的时候才暴露。所以凡是「内存里有一份、磁盘上也有一份」的地方,都得有人负责发现两者分叉——分叉本身不可怕,**没人看得见的分叉**才可怕。
- 第三个危险是硬回退的目标选错。它会丢掉未提交的改动,这是回退想要的效果,但也意味着没有第二次机会。防法是把「一个可回退目标都没有」写成显式抛错,而不是默默退到当前 HEAD 假装成功——后者会让一次失败的回退看起来像一次成功的回退。
- 再给一条设计取向,它同时降低了前面几类风险:绿点直接查提交历史,**不在自己的状态文件里另存一份**。两份记录就有两份不一致的可能,而 git 已经是一个可靠的、带顺序的、可回退的账本。实现上还有个能提的小坑——按提交信息前缀过滤时不要开扩展正则,左括号会被当成分组符,git 直接报括号不配对。
- 可预期的追问是「这些不变量怎么保证不随时间退化」。靠断言,不靠纪律:每一条危险配一条能跑的检查——收口那条用静态扫描,一致性那条用磁盘清单与状态对照,回退那条用「人为弄坏一条再看它退到哪」。写完之后一定要做变异检验,把防线关掉确认对应的检查真的转红,否则你钉住的可能只是一条恒真的断言。
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.
答题要点
- 最大危险是工作目录解析错:commit 会打进真实仓库的历史。
- 防法:git 调用收口到一个函数,工作目录必填、每次显式传,并加静态扫描断言。
- 第二个危险是代码与清单分在两个 commit:回退会留下最难查的不一致。
- 实测变异:去掉写盘那一行,程序照跑照绿,回退实验变成 9 条补丁 / 0 条清单。
- 共同形状是内存里对、磁盘上错——不报错,只在回退或换人接手时暴露。
- 硬回退没有目标时必须显式抛错,不能默默退到 HEAD 假装成功。
- 绿点查提交历史不另存一份;过滤提交信息前缀时别开扩展正则。