Plan Mode 与 Todo
两个不同阶段的东西,放在一页讲是因为它们经常连着用:
- Plan Mode 管"动手之前"——只读地探索代码,写出方案,等你批准
- Todo 管"动手之后"——把长任务拆成清单,逐项推进,你能看到进度
快速上手
复杂任务先规划:
/plan/plan 本身只发一条提示,让模型调 enter_plan_mode 进入计划模式(也可以 /plan <任务描述>,把需求一并带进去)。进去之后正常提需求,它会读代码、写出方案, 然后停下来等你批。批了才开始改文件。
/plan status 看当前在不在计划模式;/plan exit(/plan quit 同义)让它退出。 注意这两个子命令同样是发提示,实际进出仍由 exit_plan_mode 那个状态机入口完成。
不进 Plan Mode 也行——遇到多步任务时它会自己调 enter_plan_mode。你可以直接说 "先给我方案,别动手",效果一样。
详细说明
Plan Mode 里它能做什么、不能做什么
代码级强制只读,不是靠提示词请求它"别改",而是权限层拦住写操作。
| 能 | 不能 |
|---|---|
读文件、grep、查符号(read / grep / glob / ls / read_many 这类只读工具) | 改文件、写文件 |
| 把方案写进计划文件 | 跑 bash——包括 ls、git log 这类只读命令 |
两条容易踩的边界:
- bash 整类被拒,权限层不按命令内容判断是否只读。想在计划阶段拿到命令输出, 让它改用 read / grep 工具,或者先退出计划模式再跑。
- 例外只有一条:开着沙箱自动放行时,bash 可能在更早的权限步骤就被放行, 那时不受 plan 模式约束。
计划文件是唯一例外——方案本身要落盘。放行是精确匹配当前计划文件路径, 不接受路径前缀匹配,所以不能借这个口子写到别处。
计划文件在 ~/.sid-code/plans/<项目名>/ 下,文件名是 YYYYMMDD-HHmm-<主题>.md。 三个细节:
<项目名>取 git 仓库根目录的名字(非 git 目录才退回当前目录名),限长 50 字符。- 主题由模型在
enter_plan_mode时给,可以省略——省略时文件名就是纯时间戳。 - 同一分钟内多次进入计划模式,会追加
-2、-3后缀避免覆盖。
好处是几天后想翻"上次那个方案"能找到。
三个状态
inactive → planning → awaiting_approvalawaiting_approval 是它调完 exit_plan_mode 之后的状态:方案已提交,球在你这。 你可以批准、可以拒绝并说明要改什么。拒绝会退回 planning,它带着你的反馈重写; 连续拒绝 5 次会强制退出计划模式(防无限拒绝循环),之后就是普通对话了。
退出 Plan Mode 的正路是状态机入口:批准方案、exit_plan_mode 工具,或审批框里选取消。 Shift+Tab 的权限模式循环会跳过 plan 档——键盘只改一个模式字符串会造出 "假 plan 态"(每轮的约束提醒不触发),所以那条路被显式堵住了。在 plan 态按 Shift+Tab 会提示你用 exit_plan_mode 退出。
另外两个容易误解的点:
exit_plan_mode在非 planning 态(已提交待审批、或早已在执行)不会报错,而是 返回一句"已提交 / 已进入执行阶段"的提示——这是防模型陷入报错重试空转的设计。- 企业策略禁用当前权限模式时,模式会被拨回
default,plan 态也随之结束。
什么任务值得 Plan Mode
值得:
- 改动跨多个文件,或要动你不熟的模块
- 有多种做法,你想先看它选哪条、为什么
- 改错了代价高(数据迁移、权限逻辑、发布脚本)
不值得:
- 单文件小改、修 typo、加个日志——规划的开销比改本身还大
- 你已经明确知道要改哪一行
判断标准很简单:你会不会想在它动手前看一眼计划。会就用。
Todo:执行阶段的进度
批准之后进执行阶段,模型用 todo_write 工具维护一份清单(TodoWrite 是它的类名, 工具实际叫 todo_write)。四个状态:
○ pending ◐ in_progress ● completed ◌ blocked字形取自输入框上方的 TodoPanel。blocked 表示"卡在你身上"——需要你输密码、在你自己 终端跑命令、提供信息或等外部系统,既不是"还没做"也不是"正在做"。剩余项全是 blocked 时, 正确行为是把要你做的事说清楚后收尾,而不是继续空转重试。
(同一份清单还会以文本形式回给模型,那份用的是另一套字形 ⬜ 🔄 ✅ ⏸, 是给模型读的,不是给你看的。)
任何时候看当前清单:
/todosTodo 不只是给你看的进度条,它是模型自己的工作记忆。长任务里模型容易"做到一半忘了 还有什么",清单让它每一步都能对照剩余项。这也是为什么执行阶段有了 Todo 之后, 模型不会再动不动想重新 enter_plan_mode——它有地方记进度了。
清单是全量替换的:每次更新都提交完整列表,不是增量打补丁。所以你看到的 永远是当前完整状态,不会出现半旧半新。全部完成时清单会被清空、面板收起——这是 刻意的,完成结论由模型的正文给出,不在屏幕上留一张全绿清单。
相关但不同的两个东西
容易和 Plan Mode 搞混,放一起说清楚:
| 命令 | 干什么 | 和 Plan Mode 的区别 |
|---|---|---|
/goal <完成条件> | 目标驱动:设定完成条件,达成前不停 | Plan 是"先批准再动手",goal 是"一直干到达标" |
/loop [间隔] <任务> | 按间隔重复跑同一个 prompt | 周期性任务,和规划无关,见定时与无人值守 |
下面展开讲 /goal——它有一套独立的评估机制,不只是"循环到满意"。
/goal:目标驱动持续执行
/goal 解决的是一类 Plan Mode 管不了的场景:你知道目标长什么样,但不想逐步盯着它怎么干。 设定一个完成条件,AI 自己干到达标为止,中间不需要你批准每一步。
典型用法:
/goal 把 src/legacy/ 下所有 callback 风格函数改成 async/await,测试全绿才算完成它和 Plan Mode 的根本区别:Plan Mode 是"先出方案等你批",/goal 是"直接干到达标"。 两者还可以配合——先 /plan 看方案,满意后退出 Plan Mode 再 /goal 执行。
子命令
/goal 的完整参数(packages/cli/src/command/commands/goal/goal.ts):
| 子命令 | 作用 |
|---|---|
/goal <完成条件> | 设定新目标(已有活跃目标会先确认替换) |
/goal status | 查看当前目标状态、轮次、预算、证据数、上次评估 |
/goal pause | 暂停目标 |
/goal resume | 恢复目标 |
/goal edit <新条件> | 编辑目标条件(会重置已收集的证据) |
/goal turns <n> | 调整最大轮次(1–1000) |
/goal budget <tokens> | 设置 Token 预算(支持 100k 这种写法) |
/goal clear 或 /goal cancel | 清除目标 |
补充两点:裸 /goal(不带参数)等同 /goal status;/goal turns <n> 除了 1–1000 的范围,还要求新上限不小于已用轮次——n < turnsUsed 会被拒绝并提示 "设置后会立即触发轮次超限",那种情况该用 /goal clear 收尾。
它怎么判断"达标了"
这是 /goal 的核心设计,不是靠模型自己说"我做完了"。每轮对话结束时,一套独立的机制介入 (packages/core/src/query/goal-gate.ts,在 end_turn 处理链的最末):
- 证据收集——自动从工具结果里提取证据(
packages/core/src/goal/evidence-collector.ts)。 测试结果、构建结果、文件变更、命令输出都会进证据日志,不依赖模型配合,也不受/compact影响(证据是结构化的,压缩对话不会丢)。 - 预算检查——先查 Token 预算,超了直接停(省下后面评估的调用费用)。
- 轮次检查——再查轮次上限。
- 前 2 轮跳过评估——模型刚起步不可能已完成,这一步省掉两次无意义的评估调用 (
packages/core/src/goal/config.ts的minTurnsBeforeEval)。所以设完目标后头两轮 不会被判完成,属正常现象。 - 快速路径——在调评估模型之前先看客观信号:测试全绿、构建成功,任一条命中就 直接判定满足,连 LLM 都不调(
packages/core/src/goal/evaluator.ts的tryFastPathEval)。 注意「报告型任务已交付」不在快速路径里:它被降级成兜底判据,只有评估器调用 失败时才启用(tryReportFallbackEval)——放在前面会让实干型目标靠一段长文本抢跑。 - 评估模型判定——用评估模型(512 token 输出、关闭思考)基于证据日志判定目标 是否达成(
packages/core/src/goal/evaluator.ts)。评估模型可用goal.evaluatorModel单独指定, 未指定时依次回退subAgentModels.default、主模型——建议配一个独立的轻量模型, 否则等于主模型给自己打分。设目标时若评估者就是主模型会提示一次,/goal status显示 当前评估者。 对话上下文只作补充,不是主判据——所以即使对话被压缩,评估也不受影响。
评估者返回结构化结果:{satisfied, reason, blockerKey, progress, impossible}。
双闸与自动停
| 闸 | 默认值 | 触发什么 |
|---|---|---|
| 最大轮次 | 150 轮(packages/core/src/goal/config.ts,"给长任务留足空间") | 到了 → turns_limited |
| Token 预算 | 可配(/goal budget) | 到了 → budget_limited |
还有一个卡住检测:连续 3 轮撞上同一个 blocker(packages/core/src/goal/blocked-detector.ts, 默认 threshold=3)→ 判 blocked。比如连续 3 轮都卡在"找不到某个依赖",它会停下来 而不是无限重试。
blocked 默认是软提醒,不是硬停止
blocked 和 impossible 默认降级为软提醒——告诉你"看起来卡住了",但不会强制终止, 你可以让它继续试或手动调整。想恢复"卡住即停"的旧行为,设环境变量 SID_ENABLE_GOAL_HARD_STOP=1(packages/core/src/query/goal-gate.ts)。
目标不会忘
长任务里模型容易"做着做着忘了目标是什么"。/goal 每 4 轮自动把目标状态回注一次 (回注逻辑在 packages/core/src/goal/reminder.ts,间隔常量 reminderInterval: 4 在 packages/core/src/goal/config.ts),首轮必注入、/compact 后强制注入。
回注走的是 reminderParts 管道,不进 system prompt——所以不影响 Prompt Cache 命中率 (cache 按前缀命中,system prompt 没变就还在)。这也是 /goal 能长跑而不贵的原因之一。
状态栏会显示进度:◎ 目标 N/M 轮,接近上限(≥80%)转黄预警;暂停时显示 ⏸ 目标已暂停 N/M 轮。目标处于其它状态(完成、blocked 等)时这一列不显示。
目标跨会话
目标状态会持久化到会话 metadata(packages/cli/src/app.ts 的 persistGoalState)。用 -c / --continue 或 -r / --resume 恢复会话时,非终态的目标(active / paused / blocked)会一起恢复,继续推进; complete 与 impossible 不恢复——已经结束了,恢复没有意义。
/clear 会结束当前会话、开一个新会话(新的 jsonl),当前进程里的目标随之清空。 旧目标仍留在 clear 之前那份会话里:恢复到那份会话时(-r <旧会话 id>,或 clear 后没发任何消息 就退出、再用 -c——新会话没落盘,最近的会话仍是旧的那份),目标会跟着那份会话的历史一起恢复。
/goal 与 Token Budget 续写互斥
消息里写 +500k 这类预算指令会触发续写(预算没花完就让模型接着干),与 /goal 互斥: goal 激活时跳过续写,由 goal 自己的 Token 预算接管。要给 /goal 任务封顶成本, 用 /goal budget <tokens>,或用 --max-budget-usd / quota.costLimit 做花费硬顶 (两者取更严的,见成本与用量)。
/goal、Plan Mode、/loop 三者怎么选
| 你要的 | 用哪个 | 为什么 |
|---|---|---|
| 先看方案再动手 | Plan Mode | 要你的批准才改文件 |
| 知道目标,干到达标为止 | /goal | 评估模型按证据判定,不需要逐步批准 |
| 按固定间隔重复同一件事 | /loop | 定时调度,不是"达标"驱动,见定时与无人值守 |
判断标准:你要不要在它动手前看一眼方案。要就用 Plan Mode;不要、只关心结果就用 /goal。
常见问题
在 Plan Mode 里它说要改文件,然后报权限错误。 预期行为。plan 模式下写操作被权限层拦住,这正是它的作用。它应该把改动写进方案 而不是直接改——如果它反复尝试写文件,说明它没意识到自己在 plan 态,直接提醒一句就行。
按了 Shift+Tab 想退出 Plan Mode,没反应。 见上,plan 不参与键盘循环。用 exit_plan_mode(让它把方案提交上来,你再批准或拒绝), 或者直接告诉它"退出计划模式"。
方案写得不对,我怎么让它重写。 在 awaiting_approval 时拒绝并说清哪里不对。别批准了再让它改——批准之后它就 开始动手了,改起来更麻烦。
批准了但它没按计划做。 批准之后,计划与实际工具调用的对齐度(fidelity)会记进轨迹。跑 /insights, L0 事实层里有一行 plan_fidelity:计划步数、实际调用数、偏离计划的调用数 (按计划文件聚合、每份计划取末条快照,所以同一会话批准过几份计划就有几行; 只规划、从未批准过计划的会话不会出现这一行)。发现偏离多的话,通常是计划写得太粗, 下次让它把步骤拆细。
偏离数是按「工具名或动作词 + 参数」与步骤描述做文本匹配算的,宁可漏记不虚报。 步骤写得越具体(带上文件名、命令),匹配越准。
计划文件堆了很多,能删吗。 能,~/.sid-code/plans/ 下的都是纯文本记录,删掉不影响任何运行时行为。
Plan Mode 里能用子代理探索吗。 能,但只允许 explore 类子代理(只读探索),其他类型在计划模式下会被拒绝。这在大仓库里很值——探索读的那一堆文件不进主上下文, 见子代理。
两个边界值得知道:不传 type 时不会被拦(判据只在拿到类型时才做比较),但子代理 内部同样受 plan 模式约束——它跑的还是同一个权限检查器,写操作照样被拒;只有 agent 定义里显式声明了别的 permissionMode 才会换一套规则。另外 plan / verify 虽然也是 只读类型,并不在 plan 模式的放行名单里,派它们会被拒。