@pencil-agent/nano-pencil 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -267
- package/dist/build-meta.json +3 -3
- package/dist/core/export-html/AGENT.md +11 -11
- package/dist/core/export-html/template.css +971 -971
- package/dist/core/export-html/template.html +54 -54
- package/dist/core/mcp/mcp-client.d.ts +3 -1
- package/dist/core/mcp/mcp-client.js +6 -6
- package/dist/core/mcp/mcp-config.d.ts +3 -3
- package/dist/core/mcp/mcp-config.js +1 -1
- package/dist/core/mcp/mcp-manager.d.ts +5 -1
- package/dist/core/mcp/mcp-manager.js +1 -1
- package/dist/core/platform/config/resource-loader.d.ts +2 -0
- package/dist/core/platform/config/resource-loader.js +2 -2
- package/dist/core/runtime/agent-session.d.ts +12 -0
- package/dist/core/runtime/agent-session.js +8 -8
- package/dist/core/runtime/sdk.d.ts +8 -0
- package/dist/core/runtime/sdk.js +1 -1
- package/dist/extensions/builtin/AGENT.md +115 -115
- package/dist/extensions/builtin/browser/AGENT.md +17 -17
- package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
- package/dist/extensions/builtin/browser/browser.md +73 -73
- package/dist/extensions/builtin/browser/install.md +142 -142
- package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
- package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
- package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
- package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
- package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
- package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
- package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
- package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
- package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
- package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
- package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
- package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
- package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
- package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
- package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
- package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
- package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
- package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
- package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
- package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
- package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
- package/dist/extensions/builtin/goal/README.md +67 -67
- package/dist/extensions/builtin/grub/README.md +112 -112
- package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
- package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
- package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
- package/dist/extensions/builtin/link-world/linkworld.md +313 -313
- package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
- package/dist/extensions/builtin/loop/README.md +92 -92
- package/dist/extensions/builtin/mcp/figma-design.md +68 -68
- package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
- package/dist/extensions/builtin/recap/AGENT.md +15 -15
- package/dist/extensions/builtin/sal/README.md +72 -72
- package/dist/extensions/builtin/security-audit/README.md +289 -289
- package/dist/extensions/builtin/team/AGENT.md +112 -112
- package/dist/extensions/builtin/team/TESTING.md +299 -299
- package/dist/extensions/builtin/token-save/README.md +56 -56
- package/dist/extensions/optional/AGENT.md +10 -10
- package/dist/modes/interactive/interactive-mode.js +36 -36
- package/dist/modes/interactive/theme/dark.json +85 -85
- package/dist/modes/interactive/theme/light.json +84 -84
- package/dist/modes/interactive/theme/theme-schema.json +335 -335
- package/dist/modes/interactive/theme/warm.json +81 -81
- package/dist/node_modules/@pencil-agent/agent-core/dist/agent-loop.js +3 -2
- package/dist/node_modules/@pencil-agent/agent-core/dist/structured-adaptive-agent-loop.js +2 -1
- package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
- package/docs/cc-agent-design.md +1297 -0
- package/docs/cc-tui-design.md +1333 -0
- package/docs/codex-goal-command-impl.md +1055 -1055
- package/docs/codex-goal-vs-grub.md +500 -500
- package/docs/custom-provider.md +27 -27
- package/docs/extensions.md +27 -27
- package/docs/keybindings.md +27 -27
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
- package/docs/loop-usage-examples.md +214 -214
- package/docs/models.md +27 -27
- package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +170 -0
- package/docs/packages.md +27 -27
- package/docs/pi-design-philosophy.md +457 -457
- package/docs/planmode.md +1987 -1987
- package/docs/prompt-templates.md +27 -27
- package/docs/providers.md +27 -27
- package/docs/scan-report.md +3820 -0
- package/docs/sdk.md +27 -27
- package/docs/skills.md +27 -27
- package/docs/themes.md +27 -27
- package/docs/tui.md +27 -27
- package/docs//345/257/271/346/240/207Claude-Code.md +1775 -0
- package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +261 -0
- package/package.json +190 -190
- package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +0 -851
- package/docs/SDK-TESTING.md +0 -364
- package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +0 -593
- package/docs/startup-performance-optimization.md +0 -301
- package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +0 -47
package/docs/planmode.md
CHANGED
|
@@ -1,1987 +1,1987 @@
|
|
|
1
|
-
# 06 | `/plan` 命令实现与一比一复现指南
|
|
2
|
-
|
|
3
|
-
> 基于 Claude Code v2.1.88 反编译源码的逆向分析文档。本文目标是把 Plan Mode 拆成可施工规格:让 GPT-4 级别模型也能按步骤实现出结构和行为接近源码的版本,而不是只理解概念。
|
|
4
|
-
|
|
5
|
-
## 目录
|
|
6
|
-
|
|
7
|
-
1. [一句话总结](#1-一句话总结)
|
|
8
|
-
2. [必须实现的能力边界](#2-必须实现的能力边界)
|
|
9
|
-
3. [核心文件和职责](#3-核心文件和职责)
|
|
10
|
-
4. [整体数据流](#4-整体数据流)
|
|
11
|
-
5. [最小状态模型](#5-最小状态模型)
|
|
12
|
-
6. [`/plan` local-jsx 命令](#6-plan-local-jsx-命令)
|
|
13
|
-
7. [Plan 文件系统](#7-plan-文件系统)
|
|
14
|
-
8. [权限模式切换](#8-权限模式切换)
|
|
15
|
-
9. [EnterPlanModeTool](#9-enterplanmodetool)
|
|
16
|
-
10. [ExitPlanModeV2Tool](#10-exitplanmodev2tool)
|
|
17
|
-
11. [Plan Mode 附件系统](#11-plan-mode-附件系统)
|
|
18
|
-
12. [Plan workflow prompt](#12-plan-workflow-prompt)
|
|
19
|
-
13. [Explore Agent 与 Plan Agent](#13-explore-agent-与-plan-agent)
|
|
20
|
-
14. [Auto mode 与 Plan mode 的交互](#14-auto-mode-与-plan-mode-的交互)
|
|
21
|
-
15. [Teammate 审批流](#15-teammate-审批流)
|
|
22
|
-
16. [从零实现顺序](#16-从零实现顺序)
|
|
23
|
-
17. [最小可用版本](#17-最小可用版本)
|
|
24
|
-
18. [完整版本增强项](#18-完整版本增强项)
|
|
25
|
-
19. [测试清单](#19-测试清单)
|
|
26
|
-
20. [常见错误](#20-常见错误)
|
|
27
|
-
21. [关键结论](#21-关键结论)
|
|
28
|
-
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
## 1. 一句话总结
|
|
32
|
-
|
|
33
|
-
Plan Mode 是一个权限受限的规划状态。进入后,模型只能读取代码和编辑唯一的 plan 文件;完成规划后必须调用 `ExitPlanMode` 请求用户批准,批准后恢复进入前的权限模式并开始实现。
|
|
34
|
-
|
|
35
|
-
它由六层组成:
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
用户 /plan 或模型 EnterPlanMode
|
|
39
|
-
│
|
|
40
|
-
▼
|
|
41
|
-
切换 toolPermissionContext.mode = "plan"
|
|
42
|
-
│
|
|
43
|
-
▼
|
|
44
|
-
记录 prePlanMode,准备退出时恢复
|
|
45
|
-
│
|
|
46
|
-
▼
|
|
47
|
-
附件系统注入 plan_mode 工作流 prompt
|
|
48
|
-
│
|
|
49
|
-
▼
|
|
50
|
-
模型只读探索 + 写 plan 文件
|
|
51
|
-
│
|
|
52
|
-
▼
|
|
53
|
-
模型调用 ExitPlanMode
|
|
54
|
-
│
|
|
55
|
-
▼
|
|
56
|
-
用户或 team lead 审批
|
|
57
|
-
│
|
|
58
|
-
▼
|
|
59
|
-
恢复 prePlanMode,注入 plan_mode_exit 附件
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
最重要的设计点:**`/plan` 本身只切换状态;真正指导模型如何规划的是附件系统注入的 plan mode prompt;真正退出和审批的是 `ExitPlanModeV2Tool`。**
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## 2. 必须实现的能力边界
|
|
67
|
-
|
|
68
|
-
Plan Mode 必须支持:
|
|
69
|
-
|
|
70
|
-
1. 用户输入 `/plan` 进入 plan mode。
|
|
71
|
-
2. 用户输入 `/plan <description>` 进入 plan mode,并把描述作为下一轮模型查询。
|
|
72
|
-
3. 模型主动调用 `EnterPlanMode` 进入 plan mode。
|
|
73
|
-
4. 已在 plan mode 时,`/plan` 显示当前 plan 文件内容。
|
|
74
|
-
5. 已在 plan mode 时,`/plan open` 用外部编辑器打开 plan 文件。
|
|
75
|
-
6. 进入 plan mode 后,只允许读操作和编辑 plan 文件。
|
|
76
|
-
7. 进入 plan mode 后,附件系统注入 plan workflow 指令。
|
|
77
|
-
8. 模型必须把计划写到 plan 文件。
|
|
78
|
-
9. 模型必须用 `ExitPlanMode` 请求批准,不能用普通文本问“可以开始吗”。
|
|
79
|
-
10. 用户批准后恢复进入前的权限模式。
|
|
80
|
-
11. 退出后注入 `plan_mode_exit` 附件,提醒模型现在可以修改文件。
|
|
81
|
-
12. 会话恢复、fork、远程会话尽量保留 plan 文件。
|
|
82
|
-
13. teammate 可走 leader 审批流。
|
|
83
|
-
|
|
84
|
-
Plan Mode 不应该:
|
|
85
|
-
|
|
86
|
-
1. 直接修改业务文件。
|
|
87
|
-
2. 在 plan mode 内运行写操作 shell 命令。
|
|
88
|
-
3. 让 subagent 调用 `EnterPlanMode`。
|
|
89
|
-
4. 让模型在未写 plan 的情况下对 plan-required teammate 退出。
|
|
90
|
-
5. 在 channels 环境中进入一个无法退出的 plan mode。
|
|
91
|
-
|
|
92
|
-
---
|
|
93
|
-
|
|
94
|
-
## 3. 核心文件和职责
|
|
95
|
-
|
|
96
|
-
| 文件 | 职责 |
|
|
97
|
-
|------|------|
|
|
98
|
-
| `src/commands/plan/index.ts` | 注册 `/plan` local-jsx 命令 |
|
|
99
|
-
| `src/commands/plan/plan.tsx` | `/plan` 命令主逻辑:进入、显示、打开 plan |
|
|
100
|
-
| `src/bootstrap/state.ts` | 保存 plan mode 附件标志、plan slug cache、退出标志 |
|
|
101
|
-
| `src/utils/permissions/permissionSetup.ts` | 进入 plan mode 前准备权限上下文 |
|
|
102
|
-
| `src/utils/permissions/PermissionUpdate.ts` | 应用 `{ type: 'setMode', mode: 'plan' }` |
|
|
103
|
-
| `src/utils/plans.ts` | plan 文件路径、读写、slug、resume/fork 恢复 |
|
|
104
|
-
| `src/tools/EnterPlanModeTool/EnterPlanModeTool.ts` | 模型主动进入 plan mode 的工具 |
|
|
105
|
-
| `src/tools/EnterPlanModeTool/prompt.ts` | 告诉模型何时应该使用 EnterPlanMode |
|
|
106
|
-
| `src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts` | 退出、审批、恢复权限、返回 approved plan |
|
|
107
|
-
| `src/tools/ExitPlanModeTool/prompt.ts` | 告诉模型如何调用 ExitPlanMode |
|
|
108
|
-
| `src/utils/attachments.ts` | 根据状态生成 `plan_mode` / `plan_mode_exit` 附件 |
|
|
109
|
-
| `src/utils/messages.ts` | 把附件转换成模型可见 workflow prompt |
|
|
110
|
-
| `src/utils/planModeV2.ts` | Explore/Plan agent 数量、interview phase、实验变体 |
|
|
111
|
-
| `src/tools/AgentTool/built-in/exploreAgent.ts` | Explore agent 定义,只读搜索 |
|
|
112
|
-
| `src/tools/AgentTool/built-in/planAgent.ts` | Plan agent 定义,只读规划 |
|
|
113
|
-
| `src/tools.ts` | 注册 EnterPlanMode 和 ExitPlanMode 工具 |
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 4. 整体数据流
|
|
118
|
-
|
|
119
|
-
### 4.1 用户输入 `/plan`
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
用户输入 /plan [description]
|
|
123
|
-
│
|
|
124
|
-
▼
|
|
125
|
-
processSlashCommand
|
|
126
|
-
│
|
|
127
|
-
▼
|
|
128
|
-
找到 local-jsx command: plan
|
|
129
|
-
│
|
|
130
|
-
▼
|
|
131
|
-
load() -> import('./plan.js')
|
|
132
|
-
│
|
|
133
|
-
▼
|
|
134
|
-
call(onDone, context, args)
|
|
135
|
-
│
|
|
136
|
-
├─ 当前不在 plan mode
|
|
137
|
-
│ ├─ handlePlanModeTransition(currentMode, 'plan')
|
|
138
|
-
│ ├─ prepareContextForPlanMode(...)
|
|
139
|
-
│ ├─ applyPermissionUpdate(... setMode plan ...)
|
|
140
|
-
│ └─ onDone('Enabled plan mode', { shouldQuery: args 非空且不是 open })
|
|
141
|
-
│
|
|
142
|
-
└─ 当前已在 plan mode
|
|
143
|
-
├─ 没有 plan 文件 -> onDone('Already in plan mode. No plan written yet.')
|
|
144
|
-
├─ args[0] === 'open' -> editFileInEditor(planPath)
|
|
145
|
-
└─ 否则 renderToString(<PlanDisplay />)
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### 4.2 模型主动调用 `EnterPlanMode`
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
模型判断任务复杂
|
|
152
|
-
│
|
|
153
|
-
▼
|
|
154
|
-
调用 EnterPlanMode({})
|
|
155
|
-
│
|
|
156
|
-
▼
|
|
157
|
-
validate: 非 agent context,channels 未启用
|
|
158
|
-
│
|
|
159
|
-
▼
|
|
160
|
-
handlePlanModeTransition(currentMode, 'plan')
|
|
161
|
-
│
|
|
162
|
-
▼
|
|
163
|
-
setAppState(toolPermissionContext.mode = 'plan')
|
|
164
|
-
│
|
|
165
|
-
▼
|
|
166
|
-
tool_result 返回 plan mode 指令摘要
|
|
167
|
-
│
|
|
168
|
-
▼
|
|
169
|
-
下一轮 attachment 注入完整 workflow prompt
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### 4.3 模型退出 plan mode
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
模型写完 plan 文件
|
|
176
|
-
│
|
|
177
|
-
▼
|
|
178
|
-
调用 ExitPlanMode({})
|
|
179
|
-
│
|
|
180
|
-
▼
|
|
181
|
-
validate: 非 teammate 必须当前 mode === 'plan'
|
|
182
|
-
│
|
|
183
|
-
▼
|
|
184
|
-
checkPermissions
|
|
185
|
-
│
|
|
186
|
-
├─ teammate -> allow
|
|
187
|
-
└─ 普通用户 -> ask "Exit plan mode?"
|
|
188
|
-
│
|
|
189
|
-
▼
|
|
190
|
-
call()
|
|
191
|
-
│
|
|
192
|
-
├─ 读取 plan 文件
|
|
193
|
-
├─ 如果 input.plan 存在,写回 plan 文件
|
|
194
|
-
├─ teammate required -> 发 leader mailbox,等待审批
|
|
195
|
-
└─ 普通用户 -> 恢复 prePlanMode
|
|
196
|
-
│
|
|
197
|
-
▼
|
|
198
|
-
tool_result:
|
|
199
|
-
User has approved your plan. You can now start coding.
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## 5. 最小状态模型
|
|
205
|
-
|
|
206
|
-
要实现 Plan Mode,至少需要以下状态字段。
|
|
207
|
-
|
|
208
|
-
### 5.1 ToolPermissionContext
|
|
209
|
-
|
|
210
|
-
伪类型:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
type ToolPermissionContext = {
|
|
214
|
-
mode: 'default' | 'plan' | 'auto' | 'acceptEdits' | 'bypassPermissions'
|
|
215
|
-
prePlanMode?: 'default' | 'auto' | 'acceptEdits' | 'bypassPermissions'
|
|
216
|
-
strippedDangerousRules?: PermissionRule[]
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
字段含义:
|
|
221
|
-
|
|
222
|
-
| 字段 | 作用 |
|
|
223
|
-
|------|------|
|
|
224
|
-
| `mode` | 当前权限模式 |
|
|
225
|
-
| `prePlanMode` | 进入 plan mode 前的模式,退出时恢复 |
|
|
226
|
-
| `strippedDangerousRules` | auto/plan 期间临时移除的危险 allow rules |
|
|
227
|
-
|
|
228
|
-
### 5.2 Bootstrap state
|
|
229
|
-
|
|
230
|
-
伪类型:
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
type BootstrapState = {
|
|
234
|
-
needsPlanModeExitAttachment: boolean
|
|
235
|
-
hasExitedPlanModeInSession: boolean
|
|
236
|
-
planSlugCache: Map<SessionId, string>
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
字段含义:
|
|
241
|
-
|
|
242
|
-
| 字段 | 作用 |
|
|
243
|
-
|------|------|
|
|
244
|
-
| `needsPlanModeExitAttachment` | 刚退出 plan mode 时置 true,附件系统消费后清零 |
|
|
245
|
-
| `hasExitedPlanModeInSession` | 用于判断重新进入 plan mode 是否需要 reentry 指令 |
|
|
246
|
-
| `planSlugCache` | session id -> plan slug,保证同一 session 使用同一 plan 文件 |
|
|
247
|
-
|
|
248
|
-
### 5.3 关键状态函数
|
|
249
|
-
|
|
250
|
-
必须提供:
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
function handlePlanModeTransition(fromMode: string, toMode: string): void
|
|
254
|
-
function needsPlanModeExitAttachment(): boolean
|
|
255
|
-
function setNeedsPlanModeExitAttachment(value: boolean): void
|
|
256
|
-
function hasExitedPlanModeInSession(): boolean
|
|
257
|
-
function setHasExitedPlanMode(value: boolean): void
|
|
258
|
-
function getPlanSlugCache(): Map<SessionId, string>
|
|
259
|
-
function getSessionId(): SessionId
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
`handlePlanModeTransition` 逻辑:
|
|
263
|
-
|
|
264
|
-
```typescript
|
|
265
|
-
if (toMode === 'plan' && fromMode !== 'plan') {
|
|
266
|
-
STATE.needsPlanModeExitAttachment = false
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (fromMode === 'plan' && toMode !== 'plan') {
|
|
270
|
-
STATE.needsPlanModeExitAttachment = true
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
注意:源码中 `ExitPlanModeV2Tool.call()` 也会显式设置:
|
|
275
|
-
|
|
276
|
-
```typescript
|
|
277
|
-
setHasExitedPlanMode(true)
|
|
278
|
-
setNeedsPlanModeExitAttachment(true)
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
---
|
|
282
|
-
|
|
283
|
-
## 6. `/plan` local-jsx 命令
|
|
284
|
-
|
|
285
|
-
### 6.1 注册文件
|
|
286
|
-
|
|
287
|
-
文件:`src/commands/plan/index.ts`
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
const plan = {
|
|
291
|
-
type: 'local-jsx',
|
|
292
|
-
name: 'plan',
|
|
293
|
-
description: 'Enable plan mode or view the current session plan',
|
|
294
|
-
argumentHint: '[open|<description>]',
|
|
295
|
-
load: () => import('./plan.js'),
|
|
296
|
-
} satisfies Command
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
实现要求:
|
|
300
|
-
|
|
301
|
-
1. `type` 必须是 `local-jsx`。
|
|
302
|
-
2. `name` 必须是 `plan`。
|
|
303
|
-
3. `load` 动态 import `./plan.js`。
|
|
304
|
-
4. 该 command 必须加入全局 commands 列表。
|
|
305
|
-
|
|
306
|
-
### 6.2 `call()` 函数签名
|
|
307
|
-
|
|
308
|
-
文件:`src/commands/plan/plan.tsx`
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
export async function call(
|
|
312
|
-
onDone: LocalJSXCommandOnDone,
|
|
313
|
-
context: LocalJSXCommandContext,
|
|
314
|
-
args: string,
|
|
315
|
-
): Promise<React.ReactNode>
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
`context` 至少需要:
|
|
319
|
-
|
|
320
|
-
```typescript
|
|
321
|
-
type LocalJSXCommandContext = {
|
|
322
|
-
getAppState(): AppState
|
|
323
|
-
setAppState(updater: (prev: AppState) => AppState): void
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### 6.3 当前不在 plan mode
|
|
328
|
-
|
|
329
|
-
完整伪代码:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
const appState = getAppState()
|
|
333
|
-
const currentMode = appState.toolPermissionContext.mode
|
|
334
|
-
|
|
335
|
-
if (currentMode !== 'plan') {
|
|
336
|
-
handlePlanModeTransition(currentMode, 'plan')
|
|
337
|
-
|
|
338
|
-
setAppState(prev => ({
|
|
339
|
-
...prev,
|
|
340
|
-
toolPermissionContext: applyPermissionUpdate(
|
|
341
|
-
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
342
|
-
{
|
|
343
|
-
type: 'setMode',
|
|
344
|
-
mode: 'plan',
|
|
345
|
-
destination: 'session',
|
|
346
|
-
},
|
|
347
|
-
),
|
|
348
|
-
}))
|
|
349
|
-
|
|
350
|
-
const description = args.trim()
|
|
351
|
-
if (description && description !== 'open') {
|
|
352
|
-
onDone('Enabled plan mode', { shouldQuery: true })
|
|
353
|
-
} else {
|
|
354
|
-
onDone('Enabled plan mode')
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return null
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
分支含义:
|
|
362
|
-
|
|
363
|
-
| 输入 | 行为 |
|
|
364
|
-
|------|------|
|
|
365
|
-
| `/plan` | 进入 plan mode,不立即 query |
|
|
366
|
-
| `/plan open` 且当前不在 plan mode | 只进入 plan mode,不打开文件 |
|
|
367
|
-
| `/plan 重构认证模块` | 进入 plan mode,并 `shouldQuery: true` |
|
|
368
|
-
|
|
369
|
-
为什么 `/plan <description>` 要 `shouldQuery: true`:用户提供了任务描述,进入 plan mode 后要立刻让模型开始规划。
|
|
370
|
-
|
|
371
|
-
### 6.4 当前已在 plan mode
|
|
372
|
-
|
|
373
|
-
完整伪代码:
|
|
374
|
-
|
|
375
|
-
```typescript
|
|
376
|
-
const planContent = getPlan()
|
|
377
|
-
const planPath = getPlanFilePath()
|
|
378
|
-
|
|
379
|
-
if (!planContent) {
|
|
380
|
-
onDone('Already in plan mode. No plan written yet.')
|
|
381
|
-
return null
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const argList = args.trim().split(/\s+/)
|
|
385
|
-
|
|
386
|
-
if (argList[0] === 'open') {
|
|
387
|
-
const result = await editFileInEditor(planPath)
|
|
388
|
-
if (result.error) {
|
|
389
|
-
onDone(`Failed to open plan in editor: ${result.error}`)
|
|
390
|
-
} else {
|
|
391
|
-
onDone(`Opened plan in editor: ${planPath}`)
|
|
392
|
-
}
|
|
393
|
-
return null
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const editor = getExternalEditor()
|
|
397
|
-
const editorName = editor ? toIDEDisplayName(editor) : undefined
|
|
398
|
-
const display = (
|
|
399
|
-
<PlanDisplay
|
|
400
|
-
planContent={planContent}
|
|
401
|
-
planPath={planPath}
|
|
402
|
-
editorName={editorName}
|
|
403
|
-
/>
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
const output = await renderToString(display)
|
|
407
|
-
onDone(output)
|
|
408
|
-
return null
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### 6.5 PlanDisplay 组件
|
|
412
|
-
|
|
413
|
-
显示结构:
|
|
414
|
-
|
|
415
|
-
```tsx
|
|
416
|
-
<Box flexDirection="column">
|
|
417
|
-
<Text bold>Current Plan</Text>
|
|
418
|
-
<Text dimColor>{planPath}</Text>
|
|
419
|
-
<Box marginTop={1}>
|
|
420
|
-
<Text>{planContent}</Text>
|
|
421
|
-
</Box>
|
|
422
|
-
{editorName && (
|
|
423
|
-
<Box marginTop={1}>
|
|
424
|
-
<Text dimColor>"/plan open"</Text>
|
|
425
|
-
<Text dimColor> to edit this plan in </Text>
|
|
426
|
-
<Text bold dimColor>{editorName}</Text>
|
|
427
|
-
</Box>
|
|
428
|
-
)}
|
|
429
|
-
</Box>
|
|
430
|
-
```
|
|
431
|
-
|
|
432
|
-
注意:源码使用 React compiler cache,复现时不需要实现缓存。
|
|
433
|
-
|
|
434
|
-
---
|
|
435
|
-
|
|
436
|
-
## 7. Plan 文件系统
|
|
437
|
-
|
|
438
|
-
核心文件:`src/utils/plans.ts`
|
|
439
|
-
|
|
440
|
-
### 7.1 文件位置
|
|
441
|
-
|
|
442
|
-
默认:
|
|
443
|
-
|
|
444
|
-
```text
|
|
445
|
-
~/.claude/plans/{slug}.md
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
如果是 subagent:
|
|
449
|
-
|
|
450
|
-
```text
|
|
451
|
-
~/.claude/plans/{slug}-agent-{agentId}.md
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
如果用户设置了 `plansDirectory`:
|
|
455
|
-
|
|
456
|
-
```json
|
|
457
|
-
{
|
|
458
|
-
"plansDirectory": ".claude/plans"
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
则相对于项目根目录解析,且必须位于项目根目录内。
|
|
463
|
-
|
|
464
|
-
### 7.2 必须实现的 API
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
function getPlanSlug(sessionId?: SessionId): string
|
|
468
|
-
function setPlanSlug(sessionId: SessionId, slug: string): void
|
|
469
|
-
function clearPlanSlug(sessionId?: SessionId): void
|
|
470
|
-
function clearAllPlanSlugs(): void
|
|
471
|
-
function getPlansDirectory(): string
|
|
472
|
-
function getPlanFilePath(agentId?: AgentId): string
|
|
473
|
-
function getPlan(agentId?: AgentId): string | null
|
|
474
|
-
async function copyPlanForResume(log: LogOption, targetSessionId?: SessionId): Promise<boolean>
|
|
475
|
-
async function copyPlanForFork(log: LogOption, targetSessionId: SessionId): Promise<boolean>
|
|
476
|
-
async function persistFileSnapshotIfRemote(): Promise<void>
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### 7.3 `getPlansDirectory`
|
|
480
|
-
|
|
481
|
-
伪代码:
|
|
482
|
-
|
|
483
|
-
```typescript
|
|
484
|
-
function getPlansDirectory(): string {
|
|
485
|
-
const settingsDir = getInitialSettings().plansDirectory
|
|
486
|
-
|
|
487
|
-
if (settingsDir) {
|
|
488
|
-
const cwd = getCwd()
|
|
489
|
-
const resolved = resolve(cwd, settingsDir)
|
|
490
|
-
|
|
491
|
-
if (!resolved.startsWith(cwd + sep) && resolved !== cwd) {
|
|
492
|
-
logError(new Error(`plansDirectory must be within project root: ${settingsDir}`))
|
|
493
|
-
return join(getClaudeConfigHomeDir(), 'plans')
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
mkdirSync(resolved, { recursive: true })
|
|
497
|
-
return resolved
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const fallback = join(getClaudeConfigHomeDir(), 'plans')
|
|
501
|
-
mkdirSync(fallback, { recursive: true })
|
|
502
|
-
return fallback
|
|
503
|
-
}
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
源码中该函数用 `memoize` 包裹,因为渲染和权限检查会频繁调用。
|
|
507
|
-
|
|
508
|
-
### 7.4 `getPlanSlug`
|
|
509
|
-
|
|
510
|
-
伪代码:
|
|
511
|
-
|
|
512
|
-
```typescript
|
|
513
|
-
function getPlanSlug(sessionId = getSessionId()) {
|
|
514
|
-
const cache = getPlanSlugCache()
|
|
515
|
-
let slug = cache.get(sessionId)
|
|
516
|
-
|
|
517
|
-
if (!slug) {
|
|
518
|
-
const plansDir = getPlansDirectory()
|
|
519
|
-
for (let i = 0; i < 10; i++) {
|
|
520
|
-
slug = generateWordSlug()
|
|
521
|
-
const filePath = join(plansDir, `${slug}.md`)
|
|
522
|
-
if (!existsSync(filePath)) break
|
|
523
|
-
}
|
|
524
|
-
cache.set(sessionId, slug)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return slug
|
|
528
|
-
}
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
关键规则:
|
|
532
|
-
|
|
533
|
-
1. slug 延迟生成。
|
|
534
|
-
2. 同一 session id 重复调用返回同一个 slug。
|
|
535
|
-
3. 最多重试 10 次避免文件名冲突。
|
|
536
|
-
4. 生成结果放进 `planSlugCache`。
|
|
537
|
-
|
|
538
|
-
### 7.5 `getPlanFilePath`
|
|
539
|
-
|
|
540
|
-
```typescript
|
|
541
|
-
function getPlanFilePath(agentId?: AgentId): string {
|
|
542
|
-
const planSlug = getPlanSlug(getSessionId())
|
|
543
|
-
|
|
544
|
-
if (!agentId) {
|
|
545
|
-
return join(getPlansDirectory(), `${planSlug}.md`)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return join(getPlansDirectory(), `${planSlug}-agent-${agentId}.md`)
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
### 7.6 `getPlan`
|
|
553
|
-
|
|
554
|
-
```typescript
|
|
555
|
-
function getPlan(agentId?: AgentId): string | null {
|
|
556
|
-
const filePath = getPlanFilePath(agentId)
|
|
557
|
-
try {
|
|
558
|
-
return readFileSync(filePath, 'utf-8')
|
|
559
|
-
} catch (error) {
|
|
560
|
-
if (isENOENT(error)) return null
|
|
561
|
-
logError(error)
|
|
562
|
-
return null
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### 7.7 Resume 恢复
|
|
568
|
-
|
|
569
|
-
`copyPlanForResume(log, targetSessionId)`:
|
|
570
|
-
|
|
571
|
-
1. 从消息历史中找 `slug`。
|
|
572
|
-
2. `setPlanSlug(targetSessionId, slug)`。
|
|
573
|
-
3. 如果 plan 文件还在,返回 true。
|
|
574
|
-
4. 如果文件丢失且不是远程环境,返回 false。
|
|
575
|
-
5. 如果是远程环境,尝试恢复:
|
|
576
|
-
- 优先从 `file_snapshot` 中找 `key === 'plan'`
|
|
577
|
-
- 否则从消息历史中找 `ExitPlanMode` tool_use input 的 `plan`
|
|
578
|
-
- 否则找 user message 的 `planContent`
|
|
579
|
-
- 否则找 `plan_file_reference` attachment
|
|
580
|
-
6. 恢复成功后写回 plan 文件。
|
|
581
|
-
|
|
582
|
-
### 7.8 Fork 恢复
|
|
583
|
-
|
|
584
|
-
`copyPlanForFork(log, targetSessionId)`:
|
|
585
|
-
|
|
586
|
-
1. 从原日志取 original slug。
|
|
587
|
-
2. 为 target session 生成新 slug。
|
|
588
|
-
3. 把原 plan 文件复制到新 plan 文件。
|
|
589
|
-
4. 不复用原 slug,避免 fork 会话互相覆盖。
|
|
590
|
-
|
|
591
|
-
### 7.9 远程会话 snapshot
|
|
592
|
-
|
|
593
|
-
`persistFileSnapshotIfRemote()`:
|
|
594
|
-
|
|
595
|
-
1. 只在远程环境中运行。
|
|
596
|
-
2. 读取当前 plan 文件。
|
|
597
|
-
3. 写入一条 `system` / `file_snapshot` transcript message。
|
|
598
|
-
4. 用于 remote resume 时恢复 plan 文件。
|
|
599
|
-
|
|
600
|
-
---
|
|
601
|
-
|
|
602
|
-
## 8. 权限模式切换
|
|
603
|
-
|
|
604
|
-
核心文件:`src/utils/permissions/permissionSetup.ts`
|
|
605
|
-
|
|
606
|
-
### 8.1 `prepareContextForPlanMode`
|
|
607
|
-
|
|
608
|
-
完整伪代码:
|
|
609
|
-
|
|
610
|
-
```typescript
|
|
611
|
-
function prepareContextForPlanMode(context: ToolPermissionContext) {
|
|
612
|
-
const currentMode = context.mode
|
|
613
|
-
if (currentMode === 'plan') return context
|
|
614
|
-
|
|
615
|
-
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
|
616
|
-
const planAutoMode = shouldPlanUseAutoMode()
|
|
617
|
-
|
|
618
|
-
if (currentMode === 'auto') {
|
|
619
|
-
if (planAutoMode) {
|
|
620
|
-
return { ...context, prePlanMode: 'auto' }
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
autoModeStateModule?.setAutoModeActive(false)
|
|
624
|
-
setNeedsAutoModeExitAttachment(true)
|
|
625
|
-
return {
|
|
626
|
-
...restoreDangerousPermissions(context),
|
|
627
|
-
prePlanMode: 'auto',
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (planAutoMode && currentMode !== 'bypassPermissions') {
|
|
632
|
-
autoModeStateModule?.setAutoModeActive(true)
|
|
633
|
-
return {
|
|
634
|
-
...stripDangerousPermissionsForAutoMode(context),
|
|
635
|
-
prePlanMode: currentMode,
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
return { ...context, prePlanMode: currentMode }
|
|
641
|
-
}
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### 8.2 行为表
|
|
645
|
-
|
|
646
|
-
| 进入前模式 | planAutoMode | 结果 |
|
|
647
|
-
|------------|--------------|------|
|
|
648
|
-
| `plan` | 任意 | 原样返回 |
|
|
649
|
-
| `auto` | true | 保持 auto active,记录 `prePlanMode: 'auto'` |
|
|
650
|
-
| `auto` | false | 关闭 auto active,恢复危险权限,记录 `prePlanMode: 'auto'` |
|
|
651
|
-
| `default` | true | 打开 auto active,剥离危险权限,记录 `prePlanMode: 'default'` |
|
|
652
|
-
| `acceptEdits` | true | 打开 auto active,剥离危险权限,记录 `prePlanMode: 'acceptEdits'` |
|
|
653
|
-
| `bypassPermissions` | true | 不打开 auto,记录 `prePlanMode: 'bypassPermissions'` |
|
|
654
|
-
| 其他 | false | 只记录 `prePlanMode: currentMode` |
|
|
655
|
-
|
|
656
|
-
### 8.3 应用 mode 更新
|
|
657
|
-
|
|
658
|
-
进入 plan mode 时不能只改 `mode`,必须先 `prepareContextForPlanMode`,再 `applyPermissionUpdate`:
|
|
659
|
-
|
|
660
|
-
```typescript
|
|
661
|
-
toolPermissionContext: applyPermissionUpdate(
|
|
662
|
-
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
663
|
-
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
|
664
|
-
)
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
这样才能保留 `prePlanMode` 和 auto mode side effects。
|
|
668
|
-
|
|
669
|
-
### 8.4 Plan mode 权限规则
|
|
670
|
-
|
|
671
|
-
复现时至少要保证:
|
|
672
|
-
|
|
673
|
-
| 工具类别 | Plan mode 行为 |
|
|
674
|
-
|----------|----------------|
|
|
675
|
-
| FileRead | 允许 |
|
|
676
|
-
| Glob/Grep | 允许 |
|
|
677
|
-
| FileWrite | 只允许写 plan 文件 |
|
|
678
|
-
| FileEdit | 只允许编辑 plan 文件 |
|
|
679
|
-
| NotebookEdit | 禁止 |
|
|
680
|
-
| Bash/PowerShell | 只允许只读命令 |
|
|
681
|
-
| Agent | 允许 Explore/Plan 等只读 agent |
|
|
682
|
-
| ExitPlanMode | 允许主线程调用;部分 agent 场景特殊处理 |
|
|
683
|
-
| EnterPlanMode | agent context 禁止 |
|
|
684
|
-
|
|
685
|
-
实现上可以在权限检查层判断:
|
|
686
|
-
|
|
687
|
-
```typescript
|
|
688
|
-
if (context.mode === 'plan') {
|
|
689
|
-
if (tool is readOnly) allow
|
|
690
|
-
if (tool edits file && target === getPlanFilePath(agentId)) allow
|
|
691
|
-
if (tool === ExitPlanMode) allow
|
|
692
|
-
otherwise deny or ask
|
|
693
|
-
}
|
|
694
|
-
```
|
|
695
|
-
|
|
696
|
-
---
|
|
697
|
-
|
|
698
|
-
## 9. EnterPlanModeTool
|
|
699
|
-
|
|
700
|
-
核心文件:`src/tools/EnterPlanModeTool/EnterPlanModeTool.ts`
|
|
701
|
-
|
|
702
|
-
### 9.1 工具定义
|
|
703
|
-
|
|
704
|
-
```typescript
|
|
705
|
-
export const EnterPlanModeTool = buildTool({
|
|
706
|
-
name: 'EnterPlanMode',
|
|
707
|
-
searchHint: 'switch to plan mode to design an approach before coding',
|
|
708
|
-
shouldDefer: true,
|
|
709
|
-
isConcurrencySafe: true,
|
|
710
|
-
isReadOnly: true,
|
|
711
|
-
})
|
|
712
|
-
```
|
|
713
|
-
|
|
714
|
-
### 9.2 Schema
|
|
715
|
-
|
|
716
|
-
输入是 strict empty object:
|
|
717
|
-
|
|
718
|
-
```typescript
|
|
719
|
-
const inputSchema = z.strictObject({})
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
输出:
|
|
723
|
-
|
|
724
|
-
```typescript
|
|
725
|
-
{
|
|
726
|
-
message: string
|
|
727
|
-
}
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
### 9.3 isEnabled
|
|
731
|
-
|
|
732
|
-
channels 场景禁用:
|
|
733
|
-
|
|
734
|
-
```typescript
|
|
735
|
-
if ((feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
|
736
|
-
getAllowedChannels().length > 0) {
|
|
737
|
-
return false
|
|
738
|
-
}
|
|
739
|
-
return true
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
原因:channels 用户可能不在 TUI 前,`ExitPlanMode` 审批弹窗会卡住。
|
|
743
|
-
|
|
744
|
-
### 9.4 call
|
|
745
|
-
|
|
746
|
-
完整伪代码:
|
|
747
|
-
|
|
748
|
-
```typescript
|
|
749
|
-
async call(_input, context) {
|
|
750
|
-
if (context.agentId) {
|
|
751
|
-
throw new Error('EnterPlanMode tool cannot be used in agent contexts')
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const appState = context.getAppState()
|
|
755
|
-
handlePlanModeTransition(appState.toolPermissionContext.mode, 'plan')
|
|
756
|
-
|
|
757
|
-
context.setAppState(prev => ({
|
|
758
|
-
...prev,
|
|
759
|
-
toolPermissionContext: applyPermissionUpdate(
|
|
760
|
-
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
761
|
-
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
|
762
|
-
),
|
|
763
|
-
}))
|
|
764
|
-
|
|
765
|
-
return {
|
|
766
|
-
data: {
|
|
767
|
-
message:
|
|
768
|
-
'Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.',
|
|
769
|
-
},
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
### 9.5 tool_result 映射
|
|
775
|
-
|
|
776
|
-
标准模式返回:
|
|
777
|
-
|
|
778
|
-
```text
|
|
779
|
-
Entered plan mode...
|
|
780
|
-
|
|
781
|
-
In plan mode, you should:
|
|
782
|
-
1. Thoroughly explore the codebase...
|
|
783
|
-
...
|
|
784
|
-
Remember: DO NOT write or edit any files yet.
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
interview phase 返回:
|
|
788
|
-
|
|
789
|
-
```text
|
|
790
|
-
Entered plan mode...
|
|
791
|
-
|
|
792
|
-
DO NOT write or edit any files except the plan file.
|
|
793
|
-
Detailed workflow instructions will follow.
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
注意:完整 workflow 不在这里,而在 attachment -> messages.ts 中注入。
|
|
797
|
-
|
|
798
|
-
---
|
|
799
|
-
|
|
800
|
-
## 10. ExitPlanModeV2Tool
|
|
801
|
-
|
|
802
|
-
核心文件:`src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts`
|
|
803
|
-
|
|
804
|
-
这是 Plan Mode 最复杂的模块。要一比一实现,必须按分支表写。
|
|
805
|
-
|
|
806
|
-
### 10.1 工具定义
|
|
807
|
-
|
|
808
|
-
```typescript
|
|
809
|
-
export const ExitPlanModeV2Tool = buildTool({
|
|
810
|
-
name: 'ExitPlanMode',
|
|
811
|
-
searchHint: 'present plan for approval and start coding (plan mode only)',
|
|
812
|
-
shouldDefer: true,
|
|
813
|
-
isConcurrencySafe: true,
|
|
814
|
-
isReadOnly: false,
|
|
815
|
-
})
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
### 10.2 Schema
|
|
819
|
-
|
|
820
|
-
源码的公开 input schema 是 strict empty object:
|
|
821
|
-
|
|
822
|
-
```typescript
|
|
823
|
-
const inputSchema = z.strictObject({})
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
但 `call()` 内部会检查:
|
|
827
|
-
|
|
828
|
-
```typescript
|
|
829
|
-
const inputPlan =
|
|
830
|
-
'plan' in input && typeof input.plan === 'string' ? input.plan : undefined
|
|
831
|
-
```
|
|
832
|
-
|
|
833
|
-
原因:CCR web UI 或 permission result 可能把用户编辑过的 plan 放进 `input.plan`。这个字段不是普通模型应该手写的 schema 字段。
|
|
834
|
-
|
|
835
|
-
输出包含:
|
|
836
|
-
|
|
837
|
-
```typescript
|
|
838
|
-
type Output = {
|
|
839
|
-
plan?: string | null
|
|
840
|
-
isAgent?: boolean
|
|
841
|
-
filePath?: string
|
|
842
|
-
hasTaskTool?: boolean
|
|
843
|
-
planWasEdited?: boolean
|
|
844
|
-
awaitingLeaderApproval?: boolean
|
|
845
|
-
requestId?: string
|
|
846
|
-
}
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
### 10.3 requiresUserInteraction
|
|
850
|
-
|
|
851
|
-
```typescript
|
|
852
|
-
requiresUserInteraction() {
|
|
853
|
-
if (isTeammate()) return false
|
|
854
|
-
return true
|
|
855
|
-
}
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
含义:
|
|
859
|
-
|
|
860
|
-
| 场景 | 是否需要本地用户弹窗 |
|
|
861
|
-
|------|----------------------|
|
|
862
|
-
| 普通用户 | 需要 |
|
|
863
|
-
| teammate plan required | 不需要本地弹窗,发给 leader 审批 |
|
|
864
|
-
| teammate voluntary plan | 不需要本地弹窗,直接退出 |
|
|
865
|
-
|
|
866
|
-
### 10.4 validateInput
|
|
867
|
-
|
|
868
|
-
```typescript
|
|
869
|
-
async validateInput(_input, { getAppState, options }) {
|
|
870
|
-
if (isTeammate()) {
|
|
871
|
-
return { result: true }
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const mode = getAppState().toolPermissionContext.mode
|
|
875
|
-
if (mode !== 'plan') {
|
|
876
|
-
logEvent('tengu_exit_plan_mode_called_outside_plan', ...)
|
|
877
|
-
return {
|
|
878
|
-
result: false,
|
|
879
|
-
message:
|
|
880
|
-
'You are not in plan mode. This tool is only for exiting plan mode after writing a plan. If your plan was already approved, continue with implementation.',
|
|
881
|
-
errorCode: 1,
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
return { result: true }
|
|
886
|
-
}
|
|
887
|
-
```
|
|
888
|
-
|
|
889
|
-
### 10.5 checkPermissions
|
|
890
|
-
|
|
891
|
-
```typescript
|
|
892
|
-
async checkPermissions(input, context) {
|
|
893
|
-
if (isTeammate()) {
|
|
894
|
-
return { behavior: 'allow', updatedInput: input }
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
return {
|
|
898
|
-
behavior: 'ask',
|
|
899
|
-
message: 'Exit plan mode?',
|
|
900
|
-
updatedInput: input,
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
```
|
|
904
|
-
|
|
905
|
-
### 10.6 call 总流程
|
|
906
|
-
|
|
907
|
-
伪代码:
|
|
908
|
-
|
|
909
|
-
```typescript
|
|
910
|
-
async call(input, context) {
|
|
911
|
-
const isAgent = !!context.agentId
|
|
912
|
-
const filePath = getPlanFilePath(context.agentId)
|
|
913
|
-
const inputPlan =
|
|
914
|
-
'plan' in input && typeof input.plan === 'string'
|
|
915
|
-
? input.plan
|
|
916
|
-
: undefined
|
|
917
|
-
const plan = inputPlan ?? getPlan(context.agentId)
|
|
918
|
-
|
|
919
|
-
if (inputPlan !== undefined && filePath) {
|
|
920
|
-
await writeFile(filePath, inputPlan, 'utf-8').catch(logError)
|
|
921
|
-
void persistFileSnapshotIfRemote()
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (isTeammate() && isPlanModeRequired()) {
|
|
925
|
-
return await submitPlanToLeader(...)
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
restorePermissionMode(context)
|
|
929
|
-
|
|
930
|
-
const hasTaskTool =
|
|
931
|
-
isAgentSwarmsEnabled() &&
|
|
932
|
-
context.options.tools.some(t => toolMatchesName(t, AGENT_TOOL_NAME))
|
|
933
|
-
|
|
934
|
-
return {
|
|
935
|
-
data: {
|
|
936
|
-
plan,
|
|
937
|
-
isAgent,
|
|
938
|
-
filePath,
|
|
939
|
-
hasTaskTool: hasTaskTool || undefined,
|
|
940
|
-
planWasEdited: inputPlan !== undefined || undefined,
|
|
941
|
-
},
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
```
|
|
945
|
-
|
|
946
|
-
### 10.7 teammate required 分支
|
|
947
|
-
|
|
948
|
-
如果 `isTeammate() && isPlanModeRequired()`:
|
|
949
|
-
|
|
950
|
-
1. 如果没有 plan,抛错:
|
|
951
|
-
|
|
952
|
-
```typescript
|
|
953
|
-
throw new Error(
|
|
954
|
-
`No plan file found at ${filePath}. Please write your plan to this file before calling ExitPlanMode.`,
|
|
955
|
-
)
|
|
956
|
-
```
|
|
957
|
-
|
|
958
|
-
2. 生成 `requestId`:
|
|
959
|
-
|
|
960
|
-
```typescript
|
|
961
|
-
generateRequestId('plan_approval', formatAgentId(agentName, teamName || 'default'))
|
|
962
|
-
```
|
|
963
|
-
|
|
964
|
-
3. 写 mailbox:
|
|
965
|
-
|
|
966
|
-
```typescript
|
|
967
|
-
const approvalRequest = {
|
|
968
|
-
type: 'plan_approval_request',
|
|
969
|
-
from: agentName,
|
|
970
|
-
timestamp: new Date().toISOString(),
|
|
971
|
-
planFilePath: filePath,
|
|
972
|
-
planContent: plan,
|
|
973
|
-
requestId,
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
await writeToMailbox(
|
|
977
|
-
'team-lead',
|
|
978
|
-
{
|
|
979
|
-
from: agentName,
|
|
980
|
-
text: jsonStringify(approvalRequest),
|
|
981
|
-
timestamp: new Date().toISOString(),
|
|
982
|
-
},
|
|
983
|
-
teamName,
|
|
984
|
-
)
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
4. 如果是 in-process teammate,更新 task 状态为 awaiting approval。
|
|
988
|
-
5. 返回:
|
|
989
|
-
|
|
990
|
-
```typescript
|
|
991
|
-
{
|
|
992
|
-
plan,
|
|
993
|
-
isAgent: true,
|
|
994
|
-
filePath,
|
|
995
|
-
awaitingLeaderApproval: true,
|
|
996
|
-
requestId,
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
### 10.8 普通退出时恢复权限
|
|
1001
|
-
|
|
1002
|
-
核心伪代码:
|
|
1003
|
-
|
|
1004
|
-
```typescript
|
|
1005
|
-
context.setAppState(prev => {
|
|
1006
|
-
if (prev.toolPermissionContext.mode !== 'plan') return prev
|
|
1007
|
-
|
|
1008
|
-
setHasExitedPlanMode(true)
|
|
1009
|
-
setNeedsPlanModeExitAttachment(true)
|
|
1010
|
-
|
|
1011
|
-
let restoreMode = prev.toolPermissionContext.prePlanMode ?? 'default'
|
|
1012
|
-
|
|
1013
|
-
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
|
1014
|
-
if (restoreMode === 'auto' && !isAutoModeGateEnabled()) {
|
|
1015
|
-
restoreMode = 'default'
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const restoringToAuto = restoreMode === 'auto'
|
|
1019
|
-
const autoWasUsedDuringPlan = autoModeStateModule?.isAutoModeActive() ?? false
|
|
1020
|
-
autoModeStateModule?.setAutoModeActive(restoringToAuto)
|
|
1021
|
-
|
|
1022
|
-
if (autoWasUsedDuringPlan && !restoringToAuto) {
|
|
1023
|
-
setNeedsAutoModeExitAttachment(true)
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
let baseContext = prev.toolPermissionContext
|
|
1028
|
-
|
|
1029
|
-
if (restoreMode === 'auto') {
|
|
1030
|
-
baseContext = stripDangerousPermissionsForAutoMode(baseContext)
|
|
1031
|
-
} else if (prev.toolPermissionContext.strippedDangerousRules) {
|
|
1032
|
-
baseContext = restoreDangerousPermissions(baseContext)
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
return {
|
|
1036
|
-
...prev,
|
|
1037
|
-
toolPermissionContext: {
|
|
1038
|
-
...baseContext,
|
|
1039
|
-
mode: restoreMode,
|
|
1040
|
-
prePlanMode: undefined,
|
|
1041
|
-
},
|
|
1042
|
-
}
|
|
1043
|
-
})
|
|
1044
|
-
```
|
|
1045
|
-
|
|
1046
|
-
### 10.9 gate fallback notification
|
|
1047
|
-
|
|
1048
|
-
如果进入 plan 前是 `auto`,但退出时 auto gate 关闭:
|
|
1049
|
-
|
|
1050
|
-
1. 恢复到 `default`。
|
|
1051
|
-
2. 添加 notification:
|
|
1052
|
-
|
|
1053
|
-
```typescript
|
|
1054
|
-
context.addNotification?.({
|
|
1055
|
-
key: 'auto-mode-gate-plan-exit-fallback',
|
|
1056
|
-
text: `plan exit -> default · ${gateFallbackNotification}`,
|
|
1057
|
-
priority: 'immediate',
|
|
1058
|
-
color: 'warning',
|
|
1059
|
-
timeoutMs: 10000,
|
|
1060
|
-
})
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
### 10.10 tool_result 映射
|
|
1064
|
-
|
|
1065
|
-
#### teammate 等待审批
|
|
1066
|
-
|
|
1067
|
-
```text
|
|
1068
|
-
Your plan has been submitted to the team lead for approval.
|
|
1069
|
-
|
|
1070
|
-
Plan file: <filePath>
|
|
1071
|
-
|
|
1072
|
-
What happens next:
|
|
1073
|
-
1. Wait for the team lead to review your plan
|
|
1074
|
-
2. You will receive a message in your inbox with approval/rejection
|
|
1075
|
-
3. If approved, you can proceed with implementation
|
|
1076
|
-
4. If rejected, refine your plan based on the feedback
|
|
1077
|
-
|
|
1078
|
-
Important: Do NOT proceed until you receive approval.
|
|
1079
|
-
|
|
1080
|
-
Request ID: <requestId>
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
#### agent 场景
|
|
1084
|
-
|
|
1085
|
-
```text
|
|
1086
|
-
User has approved the plan. There is nothing else needed from you now. Please respond with "ok"
|
|
1087
|
-
```
|
|
1088
|
-
|
|
1089
|
-
#### 空 plan
|
|
1090
|
-
|
|
1091
|
-
```text
|
|
1092
|
-
User has approved exiting plan mode. You can now proceed.
|
|
1093
|
-
```
|
|
1094
|
-
|
|
1095
|
-
#### 普通用户批准
|
|
1096
|
-
|
|
1097
|
-
```text
|
|
1098
|
-
User has approved your plan. You can now start coding. Start with updating your todo list if applicable
|
|
1099
|
-
|
|
1100
|
-
Your plan has been saved to: <filePath>
|
|
1101
|
-
You can refer back to it if needed during implementation.
|
|
1102
|
-
|
|
1103
|
-
## Approved Plan:
|
|
1104
|
-
<plan>
|
|
1105
|
-
```
|
|
1106
|
-
|
|
1107
|
-
如果用户编辑过 plan,标题变成:
|
|
1108
|
-
|
|
1109
|
-
```text
|
|
1110
|
-
## Approved Plan (edited by user):
|
|
1111
|
-
```
|
|
1112
|
-
|
|
1113
|
-
如果当前工具列表有 Agent/Team 工具,还会附加并行任务提示。
|
|
1114
|
-
|
|
1115
|
-
---
|
|
1116
|
-
|
|
1117
|
-
## 11. Plan Mode 附件系统
|
|
1118
|
-
|
|
1119
|
-
核心文件:`src/utils/attachments.ts` 和 `src/utils/messages.ts`
|
|
1120
|
-
|
|
1121
|
-
### 11.1 Attachment 类型
|
|
1122
|
-
|
|
1123
|
-
必须支持:
|
|
1124
|
-
|
|
1125
|
-
```typescript
|
|
1126
|
-
type Attachment =
|
|
1127
|
-
| {
|
|
1128
|
-
type: 'plan_mode'
|
|
1129
|
-
reminderType: 'full' | 'sparse'
|
|
1130
|
-
isSubAgent?: boolean
|
|
1131
|
-
planFilePath: string
|
|
1132
|
-
planExists: boolean
|
|
1133
|
-
}
|
|
1134
|
-
| {
|
|
1135
|
-
type: 'plan_mode_reentry'
|
|
1136
|
-
planFilePath: string
|
|
1137
|
-
}
|
|
1138
|
-
| {
|
|
1139
|
-
type: 'plan_mode_exit'
|
|
1140
|
-
planFilePath: string
|
|
1141
|
-
planExists: boolean
|
|
1142
|
-
}
|
|
1143
|
-
| {
|
|
1144
|
-
type: 'plan_file_reference'
|
|
1145
|
-
planFilePath: string
|
|
1146
|
-
planContent: string
|
|
1147
|
-
}
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
### 11.2 `getPlanModeAttachments`
|
|
1151
|
-
|
|
1152
|
-
生成条件:
|
|
1153
|
-
|
|
1154
|
-
1. 当前 `toolPermissionContext.mode !== 'plan'`,返回 `[]`。
|
|
1155
|
-
2. 如果已有 plan mode attachment 且距离上次不足阈值 human turns,返回 `[]`。
|
|
1156
|
-
3. 读取 `planFilePath = getPlanFilePath(agentId)`。
|
|
1157
|
-
4. 读取 `existingPlan = getPlan(agentId)`。
|
|
1158
|
-
5. 如果 `hasExitedPlanModeInSession()` 且 plan 存在,先加入 `plan_mode_reentry`,然后 `setHasExitedPlanMode(false)`。
|
|
1159
|
-
6. 计算 full/sparse reminder。
|
|
1160
|
-
7. 加入 `plan_mode` attachment。
|
|
1161
|
-
|
|
1162
|
-
伪代码:
|
|
1163
|
-
|
|
1164
|
-
```typescript
|
|
1165
|
-
async function getPlanModeAttachments(messages, toolUseContext) {
|
|
1166
|
-
const mode = toolUseContext.getAppState().toolPermissionContext.mode
|
|
1167
|
-
if (mode !== 'plan') return []
|
|
1168
|
-
|
|
1169
|
-
if (messages?.length) {
|
|
1170
|
-
const { turnCount, foundPlanModeAttachment } =
|
|
1171
|
-
getPlanModeAttachmentTurnCount(messages)
|
|
1172
|
-
|
|
1173
|
-
if (foundPlanModeAttachment &&
|
|
1174
|
-
turnCount < PLAN_MODE_ATTACHMENT_CONFIG.TURNS_BETWEEN_ATTACHMENTS) {
|
|
1175
|
-
return []
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
const planFilePath = getPlanFilePath(toolUseContext.agentId)
|
|
1180
|
-
const existingPlan = getPlan(toolUseContext.agentId)
|
|
1181
|
-
const attachments = []
|
|
1182
|
-
|
|
1183
|
-
if (hasExitedPlanModeInSession() && existingPlan !== null) {
|
|
1184
|
-
attachments.push({ type: 'plan_mode_reentry', planFilePath })
|
|
1185
|
-
setHasExitedPlanMode(false)
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const attachmentCount =
|
|
1189
|
-
countPlanModeAttachmentsSinceLastExit(messages ?? []) + 1
|
|
1190
|
-
|
|
1191
|
-
const reminderType =
|
|
1192
|
-
attachmentCount % FULL_REMINDER_EVERY_N_ATTACHMENTS === 1
|
|
1193
|
-
? 'full'
|
|
1194
|
-
: 'sparse'
|
|
1195
|
-
|
|
1196
|
-
attachments.push({
|
|
1197
|
-
type: 'plan_mode',
|
|
1198
|
-
reminderType,
|
|
1199
|
-
isSubAgent: !!toolUseContext.agentId,
|
|
1200
|
-
planFilePath,
|
|
1201
|
-
planExists: existingPlan !== null,
|
|
1202
|
-
})
|
|
1203
|
-
|
|
1204
|
-
return attachments
|
|
1205
|
-
}
|
|
1206
|
-
```
|
|
1207
|
-
|
|
1208
|
-
### 11.3 human turn 计数
|
|
1209
|
-
|
|
1210
|
-
`getPlanModeAttachmentTurnCount(messages)` 从后往前扫描:
|
|
1211
|
-
|
|
1212
|
-
1. 只数 human user turns。
|
|
1213
|
-
2. 不数 `isMeta` user message。
|
|
1214
|
-
3. 不数 tool_result user message。
|
|
1215
|
-
4. 遇到最近的 `plan_mode` 或 `plan_mode_reentry` attachment 停止。
|
|
1216
|
-
|
|
1217
|
-
这是为了避免 tool loop 中每次工具调用都重复注入 plan prompt。
|
|
1218
|
-
|
|
1219
|
-
### 11.4 full/sparse 规则
|
|
1220
|
-
|
|
1221
|
-
源码逻辑:
|
|
1222
|
-
|
|
1223
|
-
```typescript
|
|
1224
|
-
const attachmentCount =
|
|
1225
|
-
countPlanModeAttachmentsSinceLastExit(messages ?? []) + 1
|
|
1226
|
-
|
|
1227
|
-
const reminderType =
|
|
1228
|
-
attachmentCount % FULL_REMINDER_EVERY_N_ATTACHMENTS === 1
|
|
1229
|
-
? 'full'
|
|
1230
|
-
: 'sparse'
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
含义:第 1、N+1、2N+1 次注入 full,其余 sparse。
|
|
1234
|
-
|
|
1235
|
-
### 11.5 `getPlanModeExitAttachment`
|
|
1236
|
-
|
|
1237
|
-
退出后一次性注入:
|
|
1238
|
-
|
|
1239
|
-
```typescript
|
|
1240
|
-
async function getPlanModeExitAttachment(toolUseContext) {
|
|
1241
|
-
if (!needsPlanModeExitAttachment()) return []
|
|
1242
|
-
|
|
1243
|
-
const appState = toolUseContext.getAppState()
|
|
1244
|
-
if (appState.toolPermissionContext.mode === 'plan') {
|
|
1245
|
-
setNeedsPlanModeExitAttachment(false)
|
|
1246
|
-
return []
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
setNeedsPlanModeExitAttachment(false)
|
|
1250
|
-
|
|
1251
|
-
const planFilePath = getPlanFilePath(toolUseContext.agentId)
|
|
1252
|
-
const planExists = getPlan(toolUseContext.agentId) !== null
|
|
1253
|
-
|
|
1254
|
-
return [{ type: 'plan_mode_exit', planFilePath, planExists }]
|
|
1255
|
-
}
|
|
1256
|
-
```
|
|
1257
|
-
|
|
1258
|
-
### 11.6 messages.ts 中的渲染
|
|
1259
|
-
|
|
1260
|
-
`plan_mode`:
|
|
1261
|
-
|
|
1262
|
-
```typescript
|
|
1263
|
-
return getPlanModeInstructions(attachment)
|
|
1264
|
-
```
|
|
1265
|
-
|
|
1266
|
-
`plan_mode_reentry`:
|
|
1267
|
-
|
|
1268
|
-
```text
|
|
1269
|
-
You are returning to plan mode after having previously exited it.
|
|
1270
|
-
A plan file exists at <path>.
|
|
1271
|
-
|
|
1272
|
-
Before proceeding:
|
|
1273
|
-
1. Read existing plan
|
|
1274
|
-
2. Compare current request
|
|
1275
|
-
3. Different task -> overwrite
|
|
1276
|
-
4. Same task -> modify and clean stale parts
|
|
1277
|
-
5. Always edit plan file before ExitPlanMode
|
|
1278
|
-
```
|
|
1279
|
-
|
|
1280
|
-
`plan_mode_exit`:
|
|
1281
|
-
|
|
1282
|
-
```text
|
|
1283
|
-
## Exited Plan Mode
|
|
1284
|
-
|
|
1285
|
-
You have exited plan mode. You can now make edits, run tools, and take actions.
|
|
1286
|
-
The plan file is located at <path> if you need to reference it.
|
|
1287
|
-
```
|
|
1288
|
-
|
|
1289
|
-
---
|
|
1290
|
-
|
|
1291
|
-
## 12. Plan workflow prompt
|
|
1292
|
-
|
|
1293
|
-
核心文件:`src/utils/messages.ts`
|
|
1294
|
-
|
|
1295
|
-
### 12.1 标准 5 阶段 workflow
|
|
1296
|
-
|
|
1297
|
-
`getPlanModeV2Instructions(attachment)` 生成主要 prompt。
|
|
1298
|
-
|
|
1299
|
-
开头必须包含硬约束:
|
|
1300
|
-
|
|
1301
|
-
```text
|
|
1302
|
-
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
|
1303
|
-
```
|
|
1304
|
-
|
|
1305
|
-
然后给出 plan 文件信息:
|
|
1306
|
-
|
|
1307
|
-
```text
|
|
1308
|
-
No plan file exists yet. You should create your plan at <path> using FileWrite.
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
或:
|
|
1312
|
-
|
|
1313
|
-
```text
|
|
1314
|
-
A plan file already exists at <path>. You can read it and make incremental edits using FileEdit.
|
|
1315
|
-
```
|
|
1316
|
-
|
|
1317
|
-
### 12.2 Phase 1: Initial Understanding
|
|
1318
|
-
|
|
1319
|
-
要求:
|
|
1320
|
-
|
|
1321
|
-
1. 全面理解用户请求。
|
|
1322
|
-
2. 搜索可复用函数、工具、模式。
|
|
1323
|
-
3. 只能使用 `Explore` subagent 类型。
|
|
1324
|
-
4. 最多启动 `getPlanModeV2ExploreAgentCount()` 个 Explore agents。
|
|
1325
|
-
5. 单文件/小改动用 1 个 agent。
|
|
1326
|
-
6. 范围不确定或跨模块时用多个 agent。
|
|
1327
|
-
|
|
1328
|
-
### 12.3 Phase 2: Design
|
|
1329
|
-
|
|
1330
|
-
要求:
|
|
1331
|
-
|
|
1332
|
-
1. 启动 `Plan` agent 设计实现。
|
|
1333
|
-
2. 默认至少 1 个 Plan agent。
|
|
1334
|
-
3. trivial task 可以跳过。
|
|
1335
|
-
4. 最多 `getPlanModeV2AgentCount()` 个 Plan agents。
|
|
1336
|
-
5. agent prompt 必须包含 Phase 1 的完整背景、文件名、代码路径、约束。
|
|
1337
|
-
|
|
1338
|
-
### 12.4 Phase 3: Review
|
|
1339
|
-
|
|
1340
|
-
要求:
|
|
1341
|
-
|
|
1342
|
-
1. 读取 agents 识别出的关键文件。
|
|
1343
|
-
2. 确认方案符合用户原始意图。
|
|
1344
|
-
3. 有疑问时用 `AskUserQuestion`。
|
|
1345
|
-
|
|
1346
|
-
### 12.5 Phase 4: Final Plan
|
|
1347
|
-
|
|
1348
|
-
要求模型把 plan 写入 plan 文件。
|
|
1349
|
-
|
|
1350
|
-
默认 control 版本至少包括:
|
|
1351
|
-
|
|
1352
|
-
1. context:为什么要做这个改动。
|
|
1353
|
-
2. 推荐方案,不列无关替代方案。
|
|
1354
|
-
3. 需要修改的关键文件路径。
|
|
1355
|
-
4. 可复用的现有函数和工具,带文件路径。
|
|
1356
|
-
5. 验证方式,最好是端到端命令。
|
|
1357
|
-
|
|
1358
|
-
### 12.6 Phase 5: ExitPlanMode
|
|
1359
|
-
|
|
1360
|
-
关键规则:
|
|
1361
|
-
|
|
1362
|
-
1. 当计划完成后必须调用 `ExitPlanMode`。
|
|
1363
|
-
2. 当前 turn 只能以 `AskUserQuestion` 或 `ExitPlanMode` 结束。
|
|
1364
|
-
3. 不允许普通文本问“这个计划可以吗”。
|
|
1365
|
-
4. 不允许用 `AskUserQuestion` 请求 plan approval。
|
|
1366
|
-
5. 只有澄清需求或选择方案时才用 `AskUserQuestion`。
|
|
1367
|
-
|
|
1368
|
-
### 12.7 Interview phase
|
|
1369
|
-
|
|
1370
|
-
开关函数:
|
|
1371
|
-
|
|
1372
|
-
```typescript
|
|
1373
|
-
function isPlanModeInterviewPhaseEnabled() {
|
|
1374
|
-
if (process.env.USER_TYPE === 'ant') return true
|
|
1375
|
-
const env = process.env.CLAUDE_CODE_PLAN_MODE_INTERVIEW_PHASE
|
|
1376
|
-
if (isEnvTruthy(env)) return true
|
|
1377
|
-
if (isEnvDefinedFalsy(env)) return false
|
|
1378
|
-
return getFeatureValue_CACHED_MAY_BE_STALE(
|
|
1379
|
-
'tengu_plan_mode_interview_phase',
|
|
1380
|
-
false,
|
|
1381
|
-
)
|
|
1382
|
-
}
|
|
1383
|
-
```
|
|
1384
|
-
|
|
1385
|
-
interview phase 不强制 Explore/Plan agents,而是让模型:
|
|
1386
|
-
|
|
1387
|
-
1. 直接读取代码。
|
|
1388
|
-
2. 逐步写 plan 文件。
|
|
1389
|
-
3. 遇到决策就问用户。
|
|
1390
|
-
4. 反复迭代,直到所有歧义解决。
|
|
1391
|
-
5. 最后调用 `ExitPlanMode`。
|
|
1392
|
-
|
|
1393
|
-
首轮要求:
|
|
1394
|
-
|
|
1395
|
-
```text
|
|
1396
|
-
Start by quickly scanning a few key files to form an initial understanding.
|
|
1397
|
-
Then write a skeleton plan and ask the user your first round of questions.
|
|
1398
|
-
Don't explore exhaustively before engaging the user.
|
|
1399
|
-
```
|
|
1400
|
-
|
|
1401
|
-
### 12.8 PewterLedger 实验
|
|
1402
|
-
|
|
1403
|
-
文件:`src/utils/planModeV2.ts`
|
|
1404
|
-
|
|
1405
|
-
变体:
|
|
1406
|
-
|
|
1407
|
-
```typescript
|
|
1408
|
-
type PewterLedgerVariant = 'trim' | 'cut' | 'cap' | null
|
|
1409
|
-
```
|
|
1410
|
-
|
|
1411
|
-
| 变体 | 作用 |
|
|
1412
|
-
|------|------|
|
|
1413
|
-
| `null` | control,完整 plan |
|
|
1414
|
-
| `trim` | 轻度缩短 |
|
|
1415
|
-
| `cut` | 中度缩短,减少背景 |
|
|
1416
|
-
| `cap` | 强限制,40 行上限 |
|
|
1417
|
-
|
|
1418
|
-
复现最小版本可以先只实现 control。
|
|
1419
|
-
|
|
1420
|
-
---
|
|
1421
|
-
|
|
1422
|
-
## 13. Explore Agent 与 Plan Agent
|
|
1423
|
-
|
|
1424
|
-
### 13.1 Explore Agent
|
|
1425
|
-
|
|
1426
|
-
文件:`src/tools/AgentTool/built-in/exploreAgent.ts`
|
|
1427
|
-
|
|
1428
|
-
关键定义:
|
|
1429
|
-
|
|
1430
|
-
```typescript
|
|
1431
|
-
const EXPLORE_AGENT = {
|
|
1432
|
-
agentType: 'Explore',
|
|
1433
|
-
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
|
|
1434
|
-
omitClaudeMd: true,
|
|
1435
|
-
disallowedTools: [
|
|
1436
|
-
'Agent',
|
|
1437
|
-
'ExitPlanMode',
|
|
1438
|
-
'FileEdit',
|
|
1439
|
-
'FileWrite',
|
|
1440
|
-
'NotebookEdit',
|
|
1441
|
-
],
|
|
1442
|
-
}
|
|
1443
|
-
```
|
|
1444
|
-
|
|
1445
|
-
必须保证:
|
|
1446
|
-
|
|
1447
|
-
1. 只读。
|
|
1448
|
-
2. 不能嵌套 Agent。
|
|
1449
|
-
3. 不能退出 plan mode。
|
|
1450
|
-
4. 不能写文件。
|
|
1451
|
-
5. 主要用于快速搜索代码。
|
|
1452
|
-
|
|
1453
|
-
### 13.2 Plan Agent
|
|
1454
|
-
|
|
1455
|
-
文件:`src/tools/AgentTool/built-in/planAgent.ts`
|
|
1456
|
-
|
|
1457
|
-
关键定义:
|
|
1458
|
-
|
|
1459
|
-
```typescript
|
|
1460
|
-
const PLAN_AGENT = {
|
|
1461
|
-
agentType: 'Plan',
|
|
1462
|
-
model: 'inherit',
|
|
1463
|
-
omitClaudeMd: true,
|
|
1464
|
-
tools: EXPLORE_AGENT.tools,
|
|
1465
|
-
disallowedTools: EXPLORE_AGENT.disallowedTools,
|
|
1466
|
-
}
|
|
1467
|
-
```
|
|
1468
|
-
|
|
1469
|
-
Plan Agent 的 prompt 要求:
|
|
1470
|
-
|
|
1471
|
-
1. 作为软件架构师。
|
|
1472
|
-
2. 基于 Explore 结果设计方案。
|
|
1473
|
-
3. 输出分步实现策略。
|
|
1474
|
-
4. 输出 `Critical Files for Implementation`。
|
|
1475
|
-
5. 仍然只读,不能写文件。
|
|
1476
|
-
|
|
1477
|
-
### 13.3 agent 数量
|
|
1478
|
-
|
|
1479
|
-
文件:`src/utils/planModeV2.ts`
|
|
1480
|
-
|
|
1481
|
-
```typescript
|
|
1482
|
-
function getPlanModeV2ExploreAgentCount() {
|
|
1483
|
-
if (env CLAUDE_CODE_PLAN_V2_EXPLORE_AGENT_COUNT is 1..10) return env
|
|
1484
|
-
return 3
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
function getPlanModeV2AgentCount() {
|
|
1488
|
-
if (env CLAUDE_CODE_PLAN_V2_AGENT_COUNT is 1..10) return env
|
|
1489
|
-
if (subscriptionType === 'max' && rateLimitTier === 'default_claude_max_20x') return 3
|
|
1490
|
-
if (subscriptionType === 'enterprise' || subscriptionType === 'team') return 3
|
|
1491
|
-
return 1
|
|
1492
|
-
}
|
|
1493
|
-
```
|
|
1494
|
-
|
|
1495
|
-
---
|
|
1496
|
-
|
|
1497
|
-
## 14. Auto mode 与 Plan mode 的交互
|
|
1498
|
-
|
|
1499
|
-
如果实现项目没有 auto mode,可以跳过本节,直接把 `prePlanMode` 恢复为原 mode。
|
|
1500
|
-
|
|
1501
|
-
如果有 auto mode,必须实现:
|
|
1502
|
-
|
|
1503
|
-
1. `shouldPlanUseAutoMode()`
|
|
1504
|
-
2. `prepareContextForPlanMode()`
|
|
1505
|
-
3. `transitionPlanAutoMode()`
|
|
1506
|
-
4. ExitPlanMode 里的 auto restore/fallback
|
|
1507
|
-
|
|
1508
|
-
### 14.1 shouldPlanUseAutoMode
|
|
1509
|
-
|
|
1510
|
-
```typescript
|
|
1511
|
-
return (
|
|
1512
|
-
hasAutoModeOptIn() &&
|
|
1513
|
-
isAutoModeGateEnabled() &&
|
|
1514
|
-
getUseAutoModeDuringPlan()
|
|
1515
|
-
)
|
|
1516
|
-
```
|
|
1517
|
-
|
|
1518
|
-
### 14.2 settings 中途变化
|
|
1519
|
-
|
|
1520
|
-
`transitionPlanAutoMode(context)`:
|
|
1521
|
-
|
|
1522
|
-
1. 只在 `mode === 'plan'` 时生效。
|
|
1523
|
-
2. 如果 `prePlanMode === 'bypassPermissions'`,不激活 auto。
|
|
1524
|
-
3. 如果 want 和 have 都 true,重新 strip dangerous permissions。
|
|
1525
|
-
4. 如果 want true have false,打开 auto 并 strip。
|
|
1526
|
-
5. 如果 want false have true,关闭 auto 并 restore dangerous permissions。
|
|
1527
|
-
|
|
1528
|
-
---
|
|
1529
|
-
|
|
1530
|
-
## 15. Teammate 审批流
|
|
1531
|
-
|
|
1532
|
-
当 teammate 使用 plan mode,分两类。
|
|
1533
|
-
|
|
1534
|
-
### 15.1 plan_mode_required teammate
|
|
1535
|
-
|
|
1536
|
-
流程:
|
|
1537
|
-
|
|
1538
|
-
```
|
|
1539
|
-
teammate 调用 ExitPlanMode
|
|
1540
|
-
│
|
|
1541
|
-
├─ 如果 plan 文件不存在 -> 抛错
|
|
1542
|
-
│
|
|
1543
|
-
├─ 生成 plan_approval_request
|
|
1544
|
-
│
|
|
1545
|
-
├─ 写入 team-lead mailbox
|
|
1546
|
-
│
|
|
1547
|
-
├─ 标记 teammate task awaiting approval
|
|
1548
|
-
│
|
|
1549
|
-
└─ 返回 awaitingLeaderApproval: true
|
|
1550
|
-
```
|
|
1551
|
-
|
|
1552
|
-
teammate 收到 tool result 后必须停止实现,等待 inbox。
|
|
1553
|
-
|
|
1554
|
-
### 15.2 voluntary plan mode teammate
|
|
1555
|
-
|
|
1556
|
-
如果不是 required:
|
|
1557
|
-
|
|
1558
|
-
1. `requiresUserInteraction()` 返回 false。
|
|
1559
|
-
2. `checkPermissions()` allow。
|
|
1560
|
-
3. `call()` 直接恢复权限模式。
|
|
1561
|
-
|
|
1562
|
-
---
|
|
1563
|
-
|
|
1564
|
-
## 16. 从零实现顺序
|
|
1565
|
-
|
|
1566
|
-
如果让 GPT-4 级别模型实现,不要一次让它写全部。按这个顺序分任务。
|
|
1567
|
-
|
|
1568
|
-
### 步骤 1:实现状态字段
|
|
1569
|
-
|
|
1570
|
-
实现:
|
|
1571
|
-
|
|
1572
|
-
1. `toolPermissionContext.mode`
|
|
1573
|
-
2. `toolPermissionContext.prePlanMode`
|
|
1574
|
-
3. `needsPlanModeExitAttachment`
|
|
1575
|
-
4. `hasExitedPlanModeInSession`
|
|
1576
|
-
5. `planSlugCache`
|
|
1577
|
-
|
|
1578
|
-
### 步骤 2:实现 `plans.ts`
|
|
1579
|
-
|
|
1580
|
-
实现:
|
|
1581
|
-
|
|
1582
|
-
1. `getPlansDirectory`
|
|
1583
|
-
2. `getPlanSlug`
|
|
1584
|
-
3. `getPlanFilePath`
|
|
1585
|
-
4. `getPlan`
|
|
1586
|
-
5. `clearPlanSlug`
|
|
1587
|
-
|
|
1588
|
-
先不实现 resume/fork。
|
|
1589
|
-
|
|
1590
|
-
### 步骤 3:实现权限准备函数
|
|
1591
|
-
|
|
1592
|
-
实现:
|
|
1593
|
-
|
|
1594
|
-
1. `handlePlanModeTransition`
|
|
1595
|
-
2. `prepareContextForPlanMode`
|
|
1596
|
-
3. `applyPermissionUpdate({ type: 'setMode' })`
|
|
1597
|
-
|
|
1598
|
-
### 步骤 4:实现 `/plan` 命令
|
|
1599
|
-
|
|
1600
|
-
实现:
|
|
1601
|
-
|
|
1602
|
-
1. `src/commands/plan/index.ts`
|
|
1603
|
-
2. `src/commands/plan/plan.tsx`
|
|
1604
|
-
3. `PlanDisplay`
|
|
1605
|
-
4. `/plan open`
|
|
1606
|
-
|
|
1607
|
-
### 步骤 5:实现 Plan Mode 权限拦截
|
|
1608
|
-
|
|
1609
|
-
至少保证:
|
|
1610
|
-
|
|
1611
|
-
1. 读工具允许。
|
|
1612
|
-
2. 非 plan 文件写入禁止。
|
|
1613
|
-
3. plan 文件写入允许。
|
|
1614
|
-
4. ExitPlanMode 允许。
|
|
1615
|
-
|
|
1616
|
-
### 步骤 6:实现 EnterPlanModeTool
|
|
1617
|
-
|
|
1618
|
-
实现:
|
|
1619
|
-
|
|
1620
|
-
1. empty input schema
|
|
1621
|
-
2. channels disabled gate
|
|
1622
|
-
3. agent context 禁止
|
|
1623
|
-
4. call 切 mode
|
|
1624
|
-
5. tool_result prompt
|
|
1625
|
-
|
|
1626
|
-
### 步骤 7:实现 ExitPlanModeV2Tool
|
|
1627
|
-
|
|
1628
|
-
先实现普通用户版本:
|
|
1629
|
-
|
|
1630
|
-
1. validate mode 必须是 plan
|
|
1631
|
-
2. checkPermissions ask
|
|
1632
|
-
3. 读取 plan
|
|
1633
|
-
4. 恢复 prePlanMode
|
|
1634
|
-
5. 返回 approved plan
|
|
1635
|
-
|
|
1636
|
-
再补 teammate 和 input.plan。
|
|
1637
|
-
|
|
1638
|
-
### 步骤 8:实现 attachment
|
|
1639
|
-
|
|
1640
|
-
实现:
|
|
1641
|
-
|
|
1642
|
-
1. `plan_mode`
|
|
1643
|
-
2. `plan_mode_exit`
|
|
1644
|
-
3. `plan_mode_reentry`
|
|
1645
|
-
4. human turn throttle
|
|
1646
|
-
|
|
1647
|
-
### 步骤 9:实现 workflow prompt
|
|
1648
|
-
|
|
1649
|
-
先实现标准 5 阶段。
|
|
1650
|
-
|
|
1651
|
-
之后再补:
|
|
1652
|
-
|
|
1653
|
-
1. interview phase
|
|
1654
|
-
2. PewterLedger variants
|
|
1655
|
-
3. sparse reminders
|
|
1656
|
-
|
|
1657
|
-
### 步骤 10:实现 Explore/Plan agents
|
|
1658
|
-
|
|
1659
|
-
实现:
|
|
1660
|
-
|
|
1661
|
-
1. Explore agent 只读定义
|
|
1662
|
-
2. Plan agent 只读定义
|
|
1663
|
-
3. agent count env override
|
|
1664
|
-
|
|
1665
|
-
### 步骤 11:实现恢复和远程增强
|
|
1666
|
-
|
|
1667
|
-
实现:
|
|
1668
|
-
|
|
1669
|
-
1. `copyPlanForResume`
|
|
1670
|
-
2. `copyPlanForFork`
|
|
1671
|
-
3. `persistFileSnapshotIfRemote`
|
|
1672
|
-
4. `plan_file_reference`
|
|
1673
|
-
|
|
1674
|
-
---
|
|
1675
|
-
|
|
1676
|
-
## 17. 最小可用版本
|
|
1677
|
-
|
|
1678
|
-
如果只想先跑通 Plan Mode,保留这些:
|
|
1679
|
-
|
|
1680
|
-
1. `/plan` 命令。
|
|
1681
|
-
2. `toolPermissionContext.mode = 'plan'`。
|
|
1682
|
-
3. `prePlanMode`。
|
|
1683
|
-
4. `getPlanFilePath` 和 `getPlan`。
|
|
1684
|
-
5. 只允许写 plan 文件。
|
|
1685
|
-
6. `EnterPlanModeTool`。
|
|
1686
|
-
7. `ExitPlanModeTool` 普通用户流程。
|
|
1687
|
-
8. `plan_mode` 附件注入一个简单 workflow。
|
|
1688
|
-
|
|
1689
|
-
可以暂时省略:
|
|
1690
|
-
|
|
1691
|
-
1. auto mode。
|
|
1692
|
-
2. teammate。
|
|
1693
|
-
3. resume/fork。
|
|
1694
|
-
4. remote snapshot。
|
|
1695
|
-
5. PewterLedger。
|
|
1696
|
-
6. interview phase。
|
|
1697
|
-
7. full/sparse 节流。
|
|
1698
|
-
8. Explore/Plan agent 数量实验。
|
|
1699
|
-
|
|
1700
|
-
最小可用数据流:
|
|
1701
|
-
|
|
1702
|
-
```
|
|
1703
|
-
/plan task
|
|
1704
|
-
│
|
|
1705
|
-
▼
|
|
1706
|
-
mode = plan, prePlanMode = oldMode
|
|
1707
|
-
│
|
|
1708
|
-
▼
|
|
1709
|
-
attachment 注入:只能读代码,写 plan 文件
|
|
1710
|
-
│
|
|
1711
|
-
▼
|
|
1712
|
-
模型写 ~/.claude/plans/<slug>.md
|
|
1713
|
-
│
|
|
1714
|
-
▼
|
|
1715
|
-
模型调用 ExitPlanMode
|
|
1716
|
-
│
|
|
1717
|
-
▼
|
|
1718
|
-
用户批准
|
|
1719
|
-
│
|
|
1720
|
-
▼
|
|
1721
|
-
mode = prePlanMode
|
|
1722
|
-
```
|
|
1723
|
-
|
|
1724
|
-
---
|
|
1725
|
-
|
|
1726
|
-
## 18. 完整版本增强项
|
|
1727
|
-
|
|
1728
|
-
按优先级补:
|
|
1729
|
-
|
|
1730
|
-
1. `/plan open`。
|
|
1731
|
-
2. `plan_mode_exit` 附件。
|
|
1732
|
-
3. `plan_mode_reentry` 附件。
|
|
1733
|
-
4. full/sparse 注入节流。
|
|
1734
|
-
5. Explore/Plan agents。
|
|
1735
|
-
6. agent count env override。
|
|
1736
|
-
7. interview phase。
|
|
1737
|
-
8. auto mode integration。
|
|
1738
|
-
9. teammate approval。
|
|
1739
|
-
10. resume/fork plan recovery。
|
|
1740
|
-
11. remote file snapshot。
|
|
1741
|
-
12. PewterLedger variants。
|
|
1742
|
-
|
|
1743
|
-
---
|
|
1744
|
-
|
|
1745
|
-
## 19. 测试清单
|
|
1746
|
-
|
|
1747
|
-
### 19.1 `/plan` 基本行为
|
|
1748
|
-
|
|
1749
|
-
```text
|
|
1750
|
-
初始 mode = default
|
|
1751
|
-
输入 /plan
|
|
1752
|
-
期望:
|
|
1753
|
-
- mode = plan
|
|
1754
|
-
- prePlanMode = default
|
|
1755
|
-
- onDone('Enabled plan mode')
|
|
1756
|
-
- shouldQuery 不为 true
|
|
1757
|
-
```
|
|
1758
|
-
|
|
1759
|
-
```text
|
|
1760
|
-
初始 mode = default
|
|
1761
|
-
输入 /plan 重构认证模块
|
|
1762
|
-
期望:
|
|
1763
|
-
- mode = plan
|
|
1764
|
-
- prePlanMode = default
|
|
1765
|
-
- onDone('Enabled plan mode', { shouldQuery: true })
|
|
1766
|
-
```
|
|
1767
|
-
|
|
1768
|
-
```text
|
|
1769
|
-
初始 mode = plan
|
|
1770
|
-
没有 plan 文件
|
|
1771
|
-
输入 /plan
|
|
1772
|
-
期望:
|
|
1773
|
-
- onDone('Already in plan mode. No plan written yet.')
|
|
1774
|
-
```
|
|
1775
|
-
|
|
1776
|
-
```text
|
|
1777
|
-
初始 mode = plan
|
|
1778
|
-
有 plan 文件
|
|
1779
|
-
输入 /plan
|
|
1780
|
-
期望:
|
|
1781
|
-
- 输出 Current Plan
|
|
1782
|
-
- 输出 plan path
|
|
1783
|
-
- 输出 plan content
|
|
1784
|
-
```
|
|
1785
|
-
|
|
1786
|
-
```text
|
|
1787
|
-
初始 mode = plan
|
|
1788
|
-
有 plan 文件
|
|
1789
|
-
输入 /plan open
|
|
1790
|
-
期望:
|
|
1791
|
-
- 调用 editFileInEditor(planPath)
|
|
1792
|
-
- 成功时输出 Opened plan in editor
|
|
1793
|
-
- 失败时输出 Failed to open plan in editor
|
|
1794
|
-
```
|
|
1795
|
-
|
|
1796
|
-
### 19.2 Plan 文件
|
|
1797
|
-
|
|
1798
|
-
测试:
|
|
1799
|
-
|
|
1800
|
-
1. 同一 session 多次 `getPlanFilePath()` 返回同一路径。
|
|
1801
|
-
2. 不同 session slug 不同。
|
|
1802
|
-
3. `agentId` 存在时文件名包含 `-agent-{agentId}`。
|
|
1803
|
-
4. `plansDirectory` 越界时 fallback 到 `~/.claude/plans`。
|
|
1804
|
-
5. `getPlan()` 文件不存在返回 null。
|
|
1805
|
-
6. `getPlan()` 读错误时 logError 并返回 null。
|
|
1806
|
-
|
|
1807
|
-
### 19.3 EnterPlanModeTool
|
|
1808
|
-
|
|
1809
|
-
测试:
|
|
1810
|
-
|
|
1811
|
-
1. input schema 不接受任何字段。
|
|
1812
|
-
2. `context.agentId` 存在时抛错。
|
|
1813
|
-
3. channels active 时 `isEnabled() === false`。
|
|
1814
|
-
4. call 后 mode 变 plan。
|
|
1815
|
-
5. call 后 `prePlanMode` 记录旧 mode。
|
|
1816
|
-
|
|
1817
|
-
### 19.4 ExitPlanModeV2Tool
|
|
1818
|
-
|
|
1819
|
-
普通用户:
|
|
1820
|
-
|
|
1821
|
-
1. mode 不是 plan 时 validate 拒绝。
|
|
1822
|
-
2. mode 是 plan 时 validate 通过。
|
|
1823
|
-
3. checkPermissions 返回 ask。
|
|
1824
|
-
4. call 后 mode 恢复 prePlanMode。
|
|
1825
|
-
5. call 后 `prePlanMode` 清空。
|
|
1826
|
-
6. call 后 `hasExitedPlanModeInSession = true`。
|
|
1827
|
-
7. call 后 `needsPlanModeExitAttachment = true`。
|
|
1828
|
-
8. plan 为空时 tool_result 返回 “approved exiting plan mode”。
|
|
1829
|
-
9. plan 非空时 tool_result 包含 approved plan。
|
|
1830
|
-
|
|
1831
|
-
input.plan:
|
|
1832
|
-
|
|
1833
|
-
1. 传入 `input.plan` 时写回 plan 文件。
|
|
1834
|
-
2. `planWasEdited = true`。
|
|
1835
|
-
3. tool_result 标题是 `Approved Plan (edited by user)`。
|
|
1836
|
-
|
|
1837
|
-
teammate:
|
|
1838
|
-
|
|
1839
|
-
1. `requiresUserInteraction()` 返回 false。
|
|
1840
|
-
2. plan_required 且无 plan 时抛错。
|
|
1841
|
-
3. plan_required 且有 plan 时写 mailbox。
|
|
1842
|
-
4. 返回 `awaitingLeaderApproval: true`。
|
|
1843
|
-
|
|
1844
|
-
### 19.5 附件系统
|
|
1845
|
-
|
|
1846
|
-
测试:
|
|
1847
|
-
|
|
1848
|
-
1. mode 非 plan 时不注入 `plan_mode`。
|
|
1849
|
-
2. mode plan 时注入 `plan_mode`。
|
|
1850
|
-
3. plan 文件不存在时 `planExists = false`。
|
|
1851
|
-
4. plan 文件存在时 `planExists = true`。
|
|
1852
|
-
5. tool loop 中不因为 assistant/tool messages 反复注入。
|
|
1853
|
-
6. 退出后注入一次 `plan_mode_exit`。
|
|
1854
|
-
7. `plan_mode_exit` 注入后 flag 清零。
|
|
1855
|
-
8. 重新进入且 plan 存在时先注入 `plan_mode_reentry`。
|
|
1856
|
-
|
|
1857
|
-
### 19.6 权限
|
|
1858
|
-
|
|
1859
|
-
Plan mode 中:
|
|
1860
|
-
|
|
1861
|
-
| 操作 | 期望 |
|
|
1862
|
-
|------|------|
|
|
1863
|
-
| FileRead 任意文件 | 允许 |
|
|
1864
|
-
| Grep/Glob | 允许 |
|
|
1865
|
-
| FileWrite plan 文件 | 允许 |
|
|
1866
|
-
| FileWrite 业务文件 | 拒绝 |
|
|
1867
|
-
| FileEdit plan 文件 | 允许 |
|
|
1868
|
-
| FileEdit 业务文件 | 拒绝 |
|
|
1869
|
-
| Bash `ls` | 允许 |
|
|
1870
|
-
| Bash `rm file` | 拒绝 |
|
|
1871
|
-
| ExitPlanMode | 允许/ask |
|
|
1872
|
-
| EnterPlanMode in agent | 拒绝 |
|
|
1873
|
-
|
|
1874
|
-
### 19.7 workflow prompt
|
|
1875
|
-
|
|
1876
|
-
检查 `plan_mode` prompt 必须包含:
|
|
1877
|
-
|
|
1878
|
-
1. 不允许修改文件,除了 plan 文件。
|
|
1879
|
-
2. plan file path。
|
|
1880
|
-
3. Phase 1 Explore。
|
|
1881
|
-
4. Phase 2 Plan。
|
|
1882
|
-
5. Phase 3 Review。
|
|
1883
|
-
6. Phase 4 写 plan 文件。
|
|
1884
|
-
7. Phase 5 调用 ExitPlanMode。
|
|
1885
|
-
8. 禁止用文本请求 plan approval。
|
|
1886
|
-
|
|
1887
|
-
---
|
|
1888
|
-
|
|
1889
|
-
## 20. 常见错误
|
|
1890
|
-
|
|
1891
|
-
### 错误 1:只改 mode,不记录 prePlanMode
|
|
1892
|
-
|
|
1893
|
-
退出时无法恢复原权限模式。
|
|
1894
|
-
|
|
1895
|
-
正确:
|
|
1896
|
-
|
|
1897
|
-
```typescript
|
|
1898
|
-
prepareContextForPlanMode(prev.toolPermissionContext)
|
|
1899
|
-
```
|
|
1900
|
-
|
|
1901
|
-
### 错误 2:`/plan <description>` 没有 `shouldQuery: true`
|
|
1902
|
-
|
|
1903
|
-
用户输入任务描述后,模型不会开始规划。
|
|
1904
|
-
|
|
1905
|
-
### 错误 3:允许写任意文件
|
|
1906
|
-
|
|
1907
|
-
Plan mode 的核心是只读探索。唯一允许写的是 plan 文件。
|
|
1908
|
-
|
|
1909
|
-
### 错误 4:把完整 workflow 放进 EnterPlanMode tool_result
|
|
1910
|
-
|
|
1911
|
-
源码中 EnterPlanMode 只返回简短说明。完整 workflow 由 attachment 注入。这样 `/plan` 和 `EnterPlanMode` 两种入口能共享同一套指令。
|
|
1912
|
-
|
|
1913
|
-
### 错误 5:ExitPlanMode 不走用户审批
|
|
1914
|
-
|
|
1915
|
-
普通用户必须通过 `checkPermissions -> ask`。
|
|
1916
|
-
|
|
1917
|
-
### 错误 6:ExitPlanMode 不设置 exit attachment flag
|
|
1918
|
-
|
|
1919
|
-
退出后模型不知道自己已经可以写文件。
|
|
1920
|
-
|
|
1921
|
-
正确:
|
|
1922
|
-
|
|
1923
|
-
```typescript
|
|
1924
|
-
setHasExitedPlanMode(true)
|
|
1925
|
-
setNeedsPlanModeExitAttachment(true)
|
|
1926
|
-
```
|
|
1927
|
-
|
|
1928
|
-
### 错误 7:plan 文件 slug 每次重新生成
|
|
1929
|
-
|
|
1930
|
-
同一 session 必须复用同一个 slug,否则 `/plan` 读不到之前写的 plan。
|
|
1931
|
-
|
|
1932
|
-
### 错误 8:subagent 复用主 plan 文件名
|
|
1933
|
-
|
|
1934
|
-
subagent plan 文件必须带 `-agent-{agentId}`,否则会覆盖主会话 plan。
|
|
1935
|
-
|
|
1936
|
-
### 错误 9:普通文本询问 plan approval
|
|
1937
|
-
|
|
1938
|
-
prompt 必须明确禁止:
|
|
1939
|
-
|
|
1940
|
-
```text
|
|
1941
|
-
Do NOT ask about plan approval in text or AskUserQuestion.
|
|
1942
|
-
Use ExitPlanMode.
|
|
1943
|
-
```
|
|
1944
|
-
|
|
1945
|
-
### 错误 10:teammate required 没 plan 也允许退出
|
|
1946
|
-
|
|
1947
|
-
plan_mode_required teammate 必须先写 plan 文件,否则抛错。
|
|
1948
|
-
|
|
1949
|
-
---
|
|
1950
|
-
|
|
1951
|
-
## 21. 关键结论
|
|
1952
|
-
|
|
1953
|
-
要一比一实现 Plan Mode,不要把它当成一个 `/plan` 命令。它实际是一个状态机:
|
|
1954
|
-
|
|
1955
|
-
```text
|
|
1956
|
-
default/auto/acceptEdits/bypassPermissions
|
|
1957
|
-
│
|
|
1958
|
-
▼
|
|
1959
|
-
plan
|
|
1960
|
-
│
|
|
1961
|
-
├─ attachment 注入 workflow
|
|
1962
|
-
├─ 权限层限制只读 + plan 文件写入
|
|
1963
|
-
├─ model 写 plan
|
|
1964
|
-
└─ ExitPlanMode 审批
|
|
1965
|
-
│
|
|
1966
|
-
▼
|
|
1967
|
-
prePlanMode
|
|
1968
|
-
```
|
|
1969
|
-
|
|
1970
|
-
最小实现先抓住四件事:
|
|
1971
|
-
|
|
1972
|
-
1. `prePlanMode` 保存和恢复。
|
|
1973
|
-
2. plan 文件路径稳定。
|
|
1974
|
-
3. plan mode 权限只允许读和写 plan 文件。
|
|
1975
|
-
4. ExitPlanMode 是唯一 plan approval 出口。
|
|
1976
|
-
|
|
1977
|
-
完整实现再补:
|
|
1978
|
-
|
|
1979
|
-
1. attachment 节流。
|
|
1980
|
-
2. reentry/exit 附件。
|
|
1981
|
-
3. Explore/Plan agents。
|
|
1982
|
-
4. auto mode 交互。
|
|
1983
|
-
5. teammate 审批。
|
|
1984
|
-
6. resume/fork/remote recovery。
|
|
1985
|
-
7. 实验变体。
|
|
1986
|
-
|
|
1987
|
-
按本文顺序实现,GPT-4 级别模型可以把每一步当成独立小任务完成,并通过测试清单逐项验证。
|
|
1
|
+
# 06 | `/plan` 命令实现与一比一复现指南
|
|
2
|
+
|
|
3
|
+
> 基于 Claude Code v2.1.88 反编译源码的逆向分析文档。本文目标是把 Plan Mode 拆成可施工规格:让 GPT-4 级别模型也能按步骤实现出结构和行为接近源码的版本,而不是只理解概念。
|
|
4
|
+
|
|
5
|
+
## 目录
|
|
6
|
+
|
|
7
|
+
1. [一句话总结](#1-一句话总结)
|
|
8
|
+
2. [必须实现的能力边界](#2-必须实现的能力边界)
|
|
9
|
+
3. [核心文件和职责](#3-核心文件和职责)
|
|
10
|
+
4. [整体数据流](#4-整体数据流)
|
|
11
|
+
5. [最小状态模型](#5-最小状态模型)
|
|
12
|
+
6. [`/plan` local-jsx 命令](#6-plan-local-jsx-命令)
|
|
13
|
+
7. [Plan 文件系统](#7-plan-文件系统)
|
|
14
|
+
8. [权限模式切换](#8-权限模式切换)
|
|
15
|
+
9. [EnterPlanModeTool](#9-enterplanmodetool)
|
|
16
|
+
10. [ExitPlanModeV2Tool](#10-exitplanmodev2tool)
|
|
17
|
+
11. [Plan Mode 附件系统](#11-plan-mode-附件系统)
|
|
18
|
+
12. [Plan workflow prompt](#12-plan-workflow-prompt)
|
|
19
|
+
13. [Explore Agent 与 Plan Agent](#13-explore-agent-与-plan-agent)
|
|
20
|
+
14. [Auto mode 与 Plan mode 的交互](#14-auto-mode-与-plan-mode-的交互)
|
|
21
|
+
15. [Teammate 审批流](#15-teammate-审批流)
|
|
22
|
+
16. [从零实现顺序](#16-从零实现顺序)
|
|
23
|
+
17. [最小可用版本](#17-最小可用版本)
|
|
24
|
+
18. [完整版本增强项](#18-完整版本增强项)
|
|
25
|
+
19. [测试清单](#19-测试清单)
|
|
26
|
+
20. [常见错误](#20-常见错误)
|
|
27
|
+
21. [关键结论](#21-关键结论)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. 一句话总结
|
|
32
|
+
|
|
33
|
+
Plan Mode 是一个权限受限的规划状态。进入后,模型只能读取代码和编辑唯一的 plan 文件;完成规划后必须调用 `ExitPlanMode` 请求用户批准,批准后恢复进入前的权限模式并开始实现。
|
|
34
|
+
|
|
35
|
+
它由六层组成:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
用户 /plan 或模型 EnterPlanMode
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
切换 toolPermissionContext.mode = "plan"
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
记录 prePlanMode,准备退出时恢复
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
附件系统注入 plan_mode 工作流 prompt
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
模型只读探索 + 写 plan 文件
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
模型调用 ExitPlanMode
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
用户或 team lead 审批
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
恢复 prePlanMode,注入 plan_mode_exit 附件
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
最重要的设计点:**`/plan` 本身只切换状态;真正指导模型如何规划的是附件系统注入的 plan mode prompt;真正退出和审批的是 `ExitPlanModeV2Tool`。**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 2. 必须实现的能力边界
|
|
67
|
+
|
|
68
|
+
Plan Mode 必须支持:
|
|
69
|
+
|
|
70
|
+
1. 用户输入 `/plan` 进入 plan mode。
|
|
71
|
+
2. 用户输入 `/plan <description>` 进入 plan mode,并把描述作为下一轮模型查询。
|
|
72
|
+
3. 模型主动调用 `EnterPlanMode` 进入 plan mode。
|
|
73
|
+
4. 已在 plan mode 时,`/plan` 显示当前 plan 文件内容。
|
|
74
|
+
5. 已在 plan mode 时,`/plan open` 用外部编辑器打开 plan 文件。
|
|
75
|
+
6. 进入 plan mode 后,只允许读操作和编辑 plan 文件。
|
|
76
|
+
7. 进入 plan mode 后,附件系统注入 plan workflow 指令。
|
|
77
|
+
8. 模型必须把计划写到 plan 文件。
|
|
78
|
+
9. 模型必须用 `ExitPlanMode` 请求批准,不能用普通文本问“可以开始吗”。
|
|
79
|
+
10. 用户批准后恢复进入前的权限模式。
|
|
80
|
+
11. 退出后注入 `plan_mode_exit` 附件,提醒模型现在可以修改文件。
|
|
81
|
+
12. 会话恢复、fork、远程会话尽量保留 plan 文件。
|
|
82
|
+
13. teammate 可走 leader 审批流。
|
|
83
|
+
|
|
84
|
+
Plan Mode 不应该:
|
|
85
|
+
|
|
86
|
+
1. 直接修改业务文件。
|
|
87
|
+
2. 在 plan mode 内运行写操作 shell 命令。
|
|
88
|
+
3. 让 subagent 调用 `EnterPlanMode`。
|
|
89
|
+
4. 让模型在未写 plan 的情况下对 plan-required teammate 退出。
|
|
90
|
+
5. 在 channels 环境中进入一个无法退出的 plan mode。
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 3. 核心文件和职责
|
|
95
|
+
|
|
96
|
+
| 文件 | 职责 |
|
|
97
|
+
|------|------|
|
|
98
|
+
| `src/commands/plan/index.ts` | 注册 `/plan` local-jsx 命令 |
|
|
99
|
+
| `src/commands/plan/plan.tsx` | `/plan` 命令主逻辑:进入、显示、打开 plan |
|
|
100
|
+
| `src/bootstrap/state.ts` | 保存 plan mode 附件标志、plan slug cache、退出标志 |
|
|
101
|
+
| `src/utils/permissions/permissionSetup.ts` | 进入 plan mode 前准备权限上下文 |
|
|
102
|
+
| `src/utils/permissions/PermissionUpdate.ts` | 应用 `{ type: 'setMode', mode: 'plan' }` |
|
|
103
|
+
| `src/utils/plans.ts` | plan 文件路径、读写、slug、resume/fork 恢复 |
|
|
104
|
+
| `src/tools/EnterPlanModeTool/EnterPlanModeTool.ts` | 模型主动进入 plan mode 的工具 |
|
|
105
|
+
| `src/tools/EnterPlanModeTool/prompt.ts` | 告诉模型何时应该使用 EnterPlanMode |
|
|
106
|
+
| `src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts` | 退出、审批、恢复权限、返回 approved plan |
|
|
107
|
+
| `src/tools/ExitPlanModeTool/prompt.ts` | 告诉模型如何调用 ExitPlanMode |
|
|
108
|
+
| `src/utils/attachments.ts` | 根据状态生成 `plan_mode` / `plan_mode_exit` 附件 |
|
|
109
|
+
| `src/utils/messages.ts` | 把附件转换成模型可见 workflow prompt |
|
|
110
|
+
| `src/utils/planModeV2.ts` | Explore/Plan agent 数量、interview phase、实验变体 |
|
|
111
|
+
| `src/tools/AgentTool/built-in/exploreAgent.ts` | Explore agent 定义,只读搜索 |
|
|
112
|
+
| `src/tools/AgentTool/built-in/planAgent.ts` | Plan agent 定义,只读规划 |
|
|
113
|
+
| `src/tools.ts` | 注册 EnterPlanMode 和 ExitPlanMode 工具 |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 4. 整体数据流
|
|
118
|
+
|
|
119
|
+
### 4.1 用户输入 `/plan`
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
用户输入 /plan [description]
|
|
123
|
+
│
|
|
124
|
+
▼
|
|
125
|
+
processSlashCommand
|
|
126
|
+
│
|
|
127
|
+
▼
|
|
128
|
+
找到 local-jsx command: plan
|
|
129
|
+
│
|
|
130
|
+
▼
|
|
131
|
+
load() -> import('./plan.js')
|
|
132
|
+
│
|
|
133
|
+
▼
|
|
134
|
+
call(onDone, context, args)
|
|
135
|
+
│
|
|
136
|
+
├─ 当前不在 plan mode
|
|
137
|
+
│ ├─ handlePlanModeTransition(currentMode, 'plan')
|
|
138
|
+
│ ├─ prepareContextForPlanMode(...)
|
|
139
|
+
│ ├─ applyPermissionUpdate(... setMode plan ...)
|
|
140
|
+
│ └─ onDone('Enabled plan mode', { shouldQuery: args 非空且不是 open })
|
|
141
|
+
│
|
|
142
|
+
└─ 当前已在 plan mode
|
|
143
|
+
├─ 没有 plan 文件 -> onDone('Already in plan mode. No plan written yet.')
|
|
144
|
+
├─ args[0] === 'open' -> editFileInEditor(planPath)
|
|
145
|
+
└─ 否则 renderToString(<PlanDisplay />)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 4.2 模型主动调用 `EnterPlanMode`
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
模型判断任务复杂
|
|
152
|
+
│
|
|
153
|
+
▼
|
|
154
|
+
调用 EnterPlanMode({})
|
|
155
|
+
│
|
|
156
|
+
▼
|
|
157
|
+
validate: 非 agent context,channels 未启用
|
|
158
|
+
│
|
|
159
|
+
▼
|
|
160
|
+
handlePlanModeTransition(currentMode, 'plan')
|
|
161
|
+
│
|
|
162
|
+
▼
|
|
163
|
+
setAppState(toolPermissionContext.mode = 'plan')
|
|
164
|
+
│
|
|
165
|
+
▼
|
|
166
|
+
tool_result 返回 plan mode 指令摘要
|
|
167
|
+
│
|
|
168
|
+
▼
|
|
169
|
+
下一轮 attachment 注入完整 workflow prompt
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 4.3 模型退出 plan mode
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
模型写完 plan 文件
|
|
176
|
+
│
|
|
177
|
+
▼
|
|
178
|
+
调用 ExitPlanMode({})
|
|
179
|
+
│
|
|
180
|
+
▼
|
|
181
|
+
validate: 非 teammate 必须当前 mode === 'plan'
|
|
182
|
+
│
|
|
183
|
+
▼
|
|
184
|
+
checkPermissions
|
|
185
|
+
│
|
|
186
|
+
├─ teammate -> allow
|
|
187
|
+
└─ 普通用户 -> ask "Exit plan mode?"
|
|
188
|
+
│
|
|
189
|
+
▼
|
|
190
|
+
call()
|
|
191
|
+
│
|
|
192
|
+
├─ 读取 plan 文件
|
|
193
|
+
├─ 如果 input.plan 存在,写回 plan 文件
|
|
194
|
+
├─ teammate required -> 发 leader mailbox,等待审批
|
|
195
|
+
└─ 普通用户 -> 恢复 prePlanMode
|
|
196
|
+
│
|
|
197
|
+
▼
|
|
198
|
+
tool_result:
|
|
199
|
+
User has approved your plan. You can now start coding.
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## 5. 最小状态模型
|
|
205
|
+
|
|
206
|
+
要实现 Plan Mode,至少需要以下状态字段。
|
|
207
|
+
|
|
208
|
+
### 5.1 ToolPermissionContext
|
|
209
|
+
|
|
210
|
+
伪类型:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
type ToolPermissionContext = {
|
|
214
|
+
mode: 'default' | 'plan' | 'auto' | 'acceptEdits' | 'bypassPermissions'
|
|
215
|
+
prePlanMode?: 'default' | 'auto' | 'acceptEdits' | 'bypassPermissions'
|
|
216
|
+
strippedDangerousRules?: PermissionRule[]
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
字段含义:
|
|
221
|
+
|
|
222
|
+
| 字段 | 作用 |
|
|
223
|
+
|------|------|
|
|
224
|
+
| `mode` | 当前权限模式 |
|
|
225
|
+
| `prePlanMode` | 进入 plan mode 前的模式,退出时恢复 |
|
|
226
|
+
| `strippedDangerousRules` | auto/plan 期间临时移除的危险 allow rules |
|
|
227
|
+
|
|
228
|
+
### 5.2 Bootstrap state
|
|
229
|
+
|
|
230
|
+
伪类型:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
type BootstrapState = {
|
|
234
|
+
needsPlanModeExitAttachment: boolean
|
|
235
|
+
hasExitedPlanModeInSession: boolean
|
|
236
|
+
planSlugCache: Map<SessionId, string>
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
字段含义:
|
|
241
|
+
|
|
242
|
+
| 字段 | 作用 |
|
|
243
|
+
|------|------|
|
|
244
|
+
| `needsPlanModeExitAttachment` | 刚退出 plan mode 时置 true,附件系统消费后清零 |
|
|
245
|
+
| `hasExitedPlanModeInSession` | 用于判断重新进入 plan mode 是否需要 reentry 指令 |
|
|
246
|
+
| `planSlugCache` | session id -> plan slug,保证同一 session 使用同一 plan 文件 |
|
|
247
|
+
|
|
248
|
+
### 5.3 关键状态函数
|
|
249
|
+
|
|
250
|
+
必须提供:
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
function handlePlanModeTransition(fromMode: string, toMode: string): void
|
|
254
|
+
function needsPlanModeExitAttachment(): boolean
|
|
255
|
+
function setNeedsPlanModeExitAttachment(value: boolean): void
|
|
256
|
+
function hasExitedPlanModeInSession(): boolean
|
|
257
|
+
function setHasExitedPlanMode(value: boolean): void
|
|
258
|
+
function getPlanSlugCache(): Map<SessionId, string>
|
|
259
|
+
function getSessionId(): SessionId
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`handlePlanModeTransition` 逻辑:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
if (toMode === 'plan' && fromMode !== 'plan') {
|
|
266
|
+
STATE.needsPlanModeExitAttachment = false
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (fromMode === 'plan' && toMode !== 'plan') {
|
|
270
|
+
STATE.needsPlanModeExitAttachment = true
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
注意:源码中 `ExitPlanModeV2Tool.call()` 也会显式设置:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
setHasExitedPlanMode(true)
|
|
278
|
+
setNeedsPlanModeExitAttachment(true)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 6. `/plan` local-jsx 命令
|
|
284
|
+
|
|
285
|
+
### 6.1 注册文件
|
|
286
|
+
|
|
287
|
+
文件:`src/commands/plan/index.ts`
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const plan = {
|
|
291
|
+
type: 'local-jsx',
|
|
292
|
+
name: 'plan',
|
|
293
|
+
description: 'Enable plan mode or view the current session plan',
|
|
294
|
+
argumentHint: '[open|<description>]',
|
|
295
|
+
load: () => import('./plan.js'),
|
|
296
|
+
} satisfies Command
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
实现要求:
|
|
300
|
+
|
|
301
|
+
1. `type` 必须是 `local-jsx`。
|
|
302
|
+
2. `name` 必须是 `plan`。
|
|
303
|
+
3. `load` 动态 import `./plan.js`。
|
|
304
|
+
4. 该 command 必须加入全局 commands 列表。
|
|
305
|
+
|
|
306
|
+
### 6.2 `call()` 函数签名
|
|
307
|
+
|
|
308
|
+
文件:`src/commands/plan/plan.tsx`
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
export async function call(
|
|
312
|
+
onDone: LocalJSXCommandOnDone,
|
|
313
|
+
context: LocalJSXCommandContext,
|
|
314
|
+
args: string,
|
|
315
|
+
): Promise<React.ReactNode>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`context` 至少需要:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
type LocalJSXCommandContext = {
|
|
322
|
+
getAppState(): AppState
|
|
323
|
+
setAppState(updater: (prev: AppState) => AppState): void
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### 6.3 当前不在 plan mode
|
|
328
|
+
|
|
329
|
+
完整伪代码:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const appState = getAppState()
|
|
333
|
+
const currentMode = appState.toolPermissionContext.mode
|
|
334
|
+
|
|
335
|
+
if (currentMode !== 'plan') {
|
|
336
|
+
handlePlanModeTransition(currentMode, 'plan')
|
|
337
|
+
|
|
338
|
+
setAppState(prev => ({
|
|
339
|
+
...prev,
|
|
340
|
+
toolPermissionContext: applyPermissionUpdate(
|
|
341
|
+
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
342
|
+
{
|
|
343
|
+
type: 'setMode',
|
|
344
|
+
mode: 'plan',
|
|
345
|
+
destination: 'session',
|
|
346
|
+
},
|
|
347
|
+
),
|
|
348
|
+
}))
|
|
349
|
+
|
|
350
|
+
const description = args.trim()
|
|
351
|
+
if (description && description !== 'open') {
|
|
352
|
+
onDone('Enabled plan mode', { shouldQuery: true })
|
|
353
|
+
} else {
|
|
354
|
+
onDone('Enabled plan mode')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
分支含义:
|
|
362
|
+
|
|
363
|
+
| 输入 | 行为 |
|
|
364
|
+
|------|------|
|
|
365
|
+
| `/plan` | 进入 plan mode,不立即 query |
|
|
366
|
+
| `/plan open` 且当前不在 plan mode | 只进入 plan mode,不打开文件 |
|
|
367
|
+
| `/plan 重构认证模块` | 进入 plan mode,并 `shouldQuery: true` |
|
|
368
|
+
|
|
369
|
+
为什么 `/plan <description>` 要 `shouldQuery: true`:用户提供了任务描述,进入 plan mode 后要立刻让模型开始规划。
|
|
370
|
+
|
|
371
|
+
### 6.4 当前已在 plan mode
|
|
372
|
+
|
|
373
|
+
完整伪代码:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const planContent = getPlan()
|
|
377
|
+
const planPath = getPlanFilePath()
|
|
378
|
+
|
|
379
|
+
if (!planContent) {
|
|
380
|
+
onDone('Already in plan mode. No plan written yet.')
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const argList = args.trim().split(/\s+/)
|
|
385
|
+
|
|
386
|
+
if (argList[0] === 'open') {
|
|
387
|
+
const result = await editFileInEditor(planPath)
|
|
388
|
+
if (result.error) {
|
|
389
|
+
onDone(`Failed to open plan in editor: ${result.error}`)
|
|
390
|
+
} else {
|
|
391
|
+
onDone(`Opened plan in editor: ${planPath}`)
|
|
392
|
+
}
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const editor = getExternalEditor()
|
|
397
|
+
const editorName = editor ? toIDEDisplayName(editor) : undefined
|
|
398
|
+
const display = (
|
|
399
|
+
<PlanDisplay
|
|
400
|
+
planContent={planContent}
|
|
401
|
+
planPath={planPath}
|
|
402
|
+
editorName={editorName}
|
|
403
|
+
/>
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const output = await renderToString(display)
|
|
407
|
+
onDone(output)
|
|
408
|
+
return null
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### 6.5 PlanDisplay 组件
|
|
412
|
+
|
|
413
|
+
显示结构:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<Box flexDirection="column">
|
|
417
|
+
<Text bold>Current Plan</Text>
|
|
418
|
+
<Text dimColor>{planPath}</Text>
|
|
419
|
+
<Box marginTop={1}>
|
|
420
|
+
<Text>{planContent}</Text>
|
|
421
|
+
</Box>
|
|
422
|
+
{editorName && (
|
|
423
|
+
<Box marginTop={1}>
|
|
424
|
+
<Text dimColor>"/plan open"</Text>
|
|
425
|
+
<Text dimColor> to edit this plan in </Text>
|
|
426
|
+
<Text bold dimColor>{editorName}</Text>
|
|
427
|
+
</Box>
|
|
428
|
+
)}
|
|
429
|
+
</Box>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
注意:源码使用 React compiler cache,复现时不需要实现缓存。
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## 7. Plan 文件系统
|
|
437
|
+
|
|
438
|
+
核心文件:`src/utils/plans.ts`
|
|
439
|
+
|
|
440
|
+
### 7.1 文件位置
|
|
441
|
+
|
|
442
|
+
默认:
|
|
443
|
+
|
|
444
|
+
```text
|
|
445
|
+
~/.claude/plans/{slug}.md
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
如果是 subagent:
|
|
449
|
+
|
|
450
|
+
```text
|
|
451
|
+
~/.claude/plans/{slug}-agent-{agentId}.md
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
如果用户设置了 `plansDirectory`:
|
|
455
|
+
|
|
456
|
+
```json
|
|
457
|
+
{
|
|
458
|
+
"plansDirectory": ".claude/plans"
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
则相对于项目根目录解析,且必须位于项目根目录内。
|
|
463
|
+
|
|
464
|
+
### 7.2 必须实现的 API
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
function getPlanSlug(sessionId?: SessionId): string
|
|
468
|
+
function setPlanSlug(sessionId: SessionId, slug: string): void
|
|
469
|
+
function clearPlanSlug(sessionId?: SessionId): void
|
|
470
|
+
function clearAllPlanSlugs(): void
|
|
471
|
+
function getPlansDirectory(): string
|
|
472
|
+
function getPlanFilePath(agentId?: AgentId): string
|
|
473
|
+
function getPlan(agentId?: AgentId): string | null
|
|
474
|
+
async function copyPlanForResume(log: LogOption, targetSessionId?: SessionId): Promise<boolean>
|
|
475
|
+
async function copyPlanForFork(log: LogOption, targetSessionId: SessionId): Promise<boolean>
|
|
476
|
+
async function persistFileSnapshotIfRemote(): Promise<void>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### 7.3 `getPlansDirectory`
|
|
480
|
+
|
|
481
|
+
伪代码:
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
function getPlansDirectory(): string {
|
|
485
|
+
const settingsDir = getInitialSettings().plansDirectory
|
|
486
|
+
|
|
487
|
+
if (settingsDir) {
|
|
488
|
+
const cwd = getCwd()
|
|
489
|
+
const resolved = resolve(cwd, settingsDir)
|
|
490
|
+
|
|
491
|
+
if (!resolved.startsWith(cwd + sep) && resolved !== cwd) {
|
|
492
|
+
logError(new Error(`plansDirectory must be within project root: ${settingsDir}`))
|
|
493
|
+
return join(getClaudeConfigHomeDir(), 'plans')
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
mkdirSync(resolved, { recursive: true })
|
|
497
|
+
return resolved
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const fallback = join(getClaudeConfigHomeDir(), 'plans')
|
|
501
|
+
mkdirSync(fallback, { recursive: true })
|
|
502
|
+
return fallback
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
源码中该函数用 `memoize` 包裹,因为渲染和权限检查会频繁调用。
|
|
507
|
+
|
|
508
|
+
### 7.4 `getPlanSlug`
|
|
509
|
+
|
|
510
|
+
伪代码:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
function getPlanSlug(sessionId = getSessionId()) {
|
|
514
|
+
const cache = getPlanSlugCache()
|
|
515
|
+
let slug = cache.get(sessionId)
|
|
516
|
+
|
|
517
|
+
if (!slug) {
|
|
518
|
+
const plansDir = getPlansDirectory()
|
|
519
|
+
for (let i = 0; i < 10; i++) {
|
|
520
|
+
slug = generateWordSlug()
|
|
521
|
+
const filePath = join(plansDir, `${slug}.md`)
|
|
522
|
+
if (!existsSync(filePath)) break
|
|
523
|
+
}
|
|
524
|
+
cache.set(sessionId, slug)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return slug
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
关键规则:
|
|
532
|
+
|
|
533
|
+
1. slug 延迟生成。
|
|
534
|
+
2. 同一 session id 重复调用返回同一个 slug。
|
|
535
|
+
3. 最多重试 10 次避免文件名冲突。
|
|
536
|
+
4. 生成结果放进 `planSlugCache`。
|
|
537
|
+
|
|
538
|
+
### 7.5 `getPlanFilePath`
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
function getPlanFilePath(agentId?: AgentId): string {
|
|
542
|
+
const planSlug = getPlanSlug(getSessionId())
|
|
543
|
+
|
|
544
|
+
if (!agentId) {
|
|
545
|
+
return join(getPlansDirectory(), `${planSlug}.md`)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return join(getPlansDirectory(), `${planSlug}-agent-${agentId}.md`)
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### 7.6 `getPlan`
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
function getPlan(agentId?: AgentId): string | null {
|
|
556
|
+
const filePath = getPlanFilePath(agentId)
|
|
557
|
+
try {
|
|
558
|
+
return readFileSync(filePath, 'utf-8')
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (isENOENT(error)) return null
|
|
561
|
+
logError(error)
|
|
562
|
+
return null
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### 7.7 Resume 恢复
|
|
568
|
+
|
|
569
|
+
`copyPlanForResume(log, targetSessionId)`:
|
|
570
|
+
|
|
571
|
+
1. 从消息历史中找 `slug`。
|
|
572
|
+
2. `setPlanSlug(targetSessionId, slug)`。
|
|
573
|
+
3. 如果 plan 文件还在,返回 true。
|
|
574
|
+
4. 如果文件丢失且不是远程环境,返回 false。
|
|
575
|
+
5. 如果是远程环境,尝试恢复:
|
|
576
|
+
- 优先从 `file_snapshot` 中找 `key === 'plan'`
|
|
577
|
+
- 否则从消息历史中找 `ExitPlanMode` tool_use input 的 `plan`
|
|
578
|
+
- 否则找 user message 的 `planContent`
|
|
579
|
+
- 否则找 `plan_file_reference` attachment
|
|
580
|
+
6. 恢复成功后写回 plan 文件。
|
|
581
|
+
|
|
582
|
+
### 7.8 Fork 恢复
|
|
583
|
+
|
|
584
|
+
`copyPlanForFork(log, targetSessionId)`:
|
|
585
|
+
|
|
586
|
+
1. 从原日志取 original slug。
|
|
587
|
+
2. 为 target session 生成新 slug。
|
|
588
|
+
3. 把原 plan 文件复制到新 plan 文件。
|
|
589
|
+
4. 不复用原 slug,避免 fork 会话互相覆盖。
|
|
590
|
+
|
|
591
|
+
### 7.9 远程会话 snapshot
|
|
592
|
+
|
|
593
|
+
`persistFileSnapshotIfRemote()`:
|
|
594
|
+
|
|
595
|
+
1. 只在远程环境中运行。
|
|
596
|
+
2. 读取当前 plan 文件。
|
|
597
|
+
3. 写入一条 `system` / `file_snapshot` transcript message。
|
|
598
|
+
4. 用于 remote resume 时恢复 plan 文件。
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## 8. 权限模式切换
|
|
603
|
+
|
|
604
|
+
核心文件:`src/utils/permissions/permissionSetup.ts`
|
|
605
|
+
|
|
606
|
+
### 8.1 `prepareContextForPlanMode`
|
|
607
|
+
|
|
608
|
+
完整伪代码:
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
function prepareContextForPlanMode(context: ToolPermissionContext) {
|
|
612
|
+
const currentMode = context.mode
|
|
613
|
+
if (currentMode === 'plan') return context
|
|
614
|
+
|
|
615
|
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
|
616
|
+
const planAutoMode = shouldPlanUseAutoMode()
|
|
617
|
+
|
|
618
|
+
if (currentMode === 'auto') {
|
|
619
|
+
if (planAutoMode) {
|
|
620
|
+
return { ...context, prePlanMode: 'auto' }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
autoModeStateModule?.setAutoModeActive(false)
|
|
624
|
+
setNeedsAutoModeExitAttachment(true)
|
|
625
|
+
return {
|
|
626
|
+
...restoreDangerousPermissions(context),
|
|
627
|
+
prePlanMode: 'auto',
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (planAutoMode && currentMode !== 'bypassPermissions') {
|
|
632
|
+
autoModeStateModule?.setAutoModeActive(true)
|
|
633
|
+
return {
|
|
634
|
+
...stripDangerousPermissionsForAutoMode(context),
|
|
635
|
+
prePlanMode: currentMode,
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return { ...context, prePlanMode: currentMode }
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### 8.2 行为表
|
|
645
|
+
|
|
646
|
+
| 进入前模式 | planAutoMode | 结果 |
|
|
647
|
+
|------------|--------------|------|
|
|
648
|
+
| `plan` | 任意 | 原样返回 |
|
|
649
|
+
| `auto` | true | 保持 auto active,记录 `prePlanMode: 'auto'` |
|
|
650
|
+
| `auto` | false | 关闭 auto active,恢复危险权限,记录 `prePlanMode: 'auto'` |
|
|
651
|
+
| `default` | true | 打开 auto active,剥离危险权限,记录 `prePlanMode: 'default'` |
|
|
652
|
+
| `acceptEdits` | true | 打开 auto active,剥离危险权限,记录 `prePlanMode: 'acceptEdits'` |
|
|
653
|
+
| `bypassPermissions` | true | 不打开 auto,记录 `prePlanMode: 'bypassPermissions'` |
|
|
654
|
+
| 其他 | false | 只记录 `prePlanMode: currentMode` |
|
|
655
|
+
|
|
656
|
+
### 8.3 应用 mode 更新
|
|
657
|
+
|
|
658
|
+
进入 plan mode 时不能只改 `mode`,必须先 `prepareContextForPlanMode`,再 `applyPermissionUpdate`:
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
toolPermissionContext: applyPermissionUpdate(
|
|
662
|
+
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
663
|
+
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
|
664
|
+
)
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
这样才能保留 `prePlanMode` 和 auto mode side effects。
|
|
668
|
+
|
|
669
|
+
### 8.4 Plan mode 权限规则
|
|
670
|
+
|
|
671
|
+
复现时至少要保证:
|
|
672
|
+
|
|
673
|
+
| 工具类别 | Plan mode 行为 |
|
|
674
|
+
|----------|----------------|
|
|
675
|
+
| FileRead | 允许 |
|
|
676
|
+
| Glob/Grep | 允许 |
|
|
677
|
+
| FileWrite | 只允许写 plan 文件 |
|
|
678
|
+
| FileEdit | 只允许编辑 plan 文件 |
|
|
679
|
+
| NotebookEdit | 禁止 |
|
|
680
|
+
| Bash/PowerShell | 只允许只读命令 |
|
|
681
|
+
| Agent | 允许 Explore/Plan 等只读 agent |
|
|
682
|
+
| ExitPlanMode | 允许主线程调用;部分 agent 场景特殊处理 |
|
|
683
|
+
| EnterPlanMode | agent context 禁止 |
|
|
684
|
+
|
|
685
|
+
实现上可以在权限检查层判断:
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
if (context.mode === 'plan') {
|
|
689
|
+
if (tool is readOnly) allow
|
|
690
|
+
if (tool edits file && target === getPlanFilePath(agentId)) allow
|
|
691
|
+
if (tool === ExitPlanMode) allow
|
|
692
|
+
otherwise deny or ask
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## 9. EnterPlanModeTool
|
|
699
|
+
|
|
700
|
+
核心文件:`src/tools/EnterPlanModeTool/EnterPlanModeTool.ts`
|
|
701
|
+
|
|
702
|
+
### 9.1 工具定义
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
export const EnterPlanModeTool = buildTool({
|
|
706
|
+
name: 'EnterPlanMode',
|
|
707
|
+
searchHint: 'switch to plan mode to design an approach before coding',
|
|
708
|
+
shouldDefer: true,
|
|
709
|
+
isConcurrencySafe: true,
|
|
710
|
+
isReadOnly: true,
|
|
711
|
+
})
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### 9.2 Schema
|
|
715
|
+
|
|
716
|
+
输入是 strict empty object:
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
const inputSchema = z.strictObject({})
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
输出:
|
|
723
|
+
|
|
724
|
+
```typescript
|
|
725
|
+
{
|
|
726
|
+
message: string
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### 9.3 isEnabled
|
|
731
|
+
|
|
732
|
+
channels 场景禁用:
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
if ((feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
|
736
|
+
getAllowedChannels().length > 0) {
|
|
737
|
+
return false
|
|
738
|
+
}
|
|
739
|
+
return true
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
原因:channels 用户可能不在 TUI 前,`ExitPlanMode` 审批弹窗会卡住。
|
|
743
|
+
|
|
744
|
+
### 9.4 call
|
|
745
|
+
|
|
746
|
+
完整伪代码:
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
async call(_input, context) {
|
|
750
|
+
if (context.agentId) {
|
|
751
|
+
throw new Error('EnterPlanMode tool cannot be used in agent contexts')
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
const appState = context.getAppState()
|
|
755
|
+
handlePlanModeTransition(appState.toolPermissionContext.mode, 'plan')
|
|
756
|
+
|
|
757
|
+
context.setAppState(prev => ({
|
|
758
|
+
...prev,
|
|
759
|
+
toolPermissionContext: applyPermissionUpdate(
|
|
760
|
+
prepareContextForPlanMode(prev.toolPermissionContext),
|
|
761
|
+
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
|
762
|
+
),
|
|
763
|
+
}))
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
data: {
|
|
767
|
+
message:
|
|
768
|
+
'Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.',
|
|
769
|
+
},
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### 9.5 tool_result 映射
|
|
775
|
+
|
|
776
|
+
标准模式返回:
|
|
777
|
+
|
|
778
|
+
```text
|
|
779
|
+
Entered plan mode...
|
|
780
|
+
|
|
781
|
+
In plan mode, you should:
|
|
782
|
+
1. Thoroughly explore the codebase...
|
|
783
|
+
...
|
|
784
|
+
Remember: DO NOT write or edit any files yet.
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
interview phase 返回:
|
|
788
|
+
|
|
789
|
+
```text
|
|
790
|
+
Entered plan mode...
|
|
791
|
+
|
|
792
|
+
DO NOT write or edit any files except the plan file.
|
|
793
|
+
Detailed workflow instructions will follow.
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
注意:完整 workflow 不在这里,而在 attachment -> messages.ts 中注入。
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## 10. ExitPlanModeV2Tool
|
|
801
|
+
|
|
802
|
+
核心文件:`src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.ts`
|
|
803
|
+
|
|
804
|
+
这是 Plan Mode 最复杂的模块。要一比一实现,必须按分支表写。
|
|
805
|
+
|
|
806
|
+
### 10.1 工具定义
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
export const ExitPlanModeV2Tool = buildTool({
|
|
810
|
+
name: 'ExitPlanMode',
|
|
811
|
+
searchHint: 'present plan for approval and start coding (plan mode only)',
|
|
812
|
+
shouldDefer: true,
|
|
813
|
+
isConcurrencySafe: true,
|
|
814
|
+
isReadOnly: false,
|
|
815
|
+
})
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### 10.2 Schema
|
|
819
|
+
|
|
820
|
+
源码的公开 input schema 是 strict empty object:
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
const inputSchema = z.strictObject({})
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
但 `call()` 内部会检查:
|
|
827
|
+
|
|
828
|
+
```typescript
|
|
829
|
+
const inputPlan =
|
|
830
|
+
'plan' in input && typeof input.plan === 'string' ? input.plan : undefined
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
原因:CCR web UI 或 permission result 可能把用户编辑过的 plan 放进 `input.plan`。这个字段不是普通模型应该手写的 schema 字段。
|
|
834
|
+
|
|
835
|
+
输出包含:
|
|
836
|
+
|
|
837
|
+
```typescript
|
|
838
|
+
type Output = {
|
|
839
|
+
plan?: string | null
|
|
840
|
+
isAgent?: boolean
|
|
841
|
+
filePath?: string
|
|
842
|
+
hasTaskTool?: boolean
|
|
843
|
+
planWasEdited?: boolean
|
|
844
|
+
awaitingLeaderApproval?: boolean
|
|
845
|
+
requestId?: string
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### 10.3 requiresUserInteraction
|
|
850
|
+
|
|
851
|
+
```typescript
|
|
852
|
+
requiresUserInteraction() {
|
|
853
|
+
if (isTeammate()) return false
|
|
854
|
+
return true
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
含义:
|
|
859
|
+
|
|
860
|
+
| 场景 | 是否需要本地用户弹窗 |
|
|
861
|
+
|------|----------------------|
|
|
862
|
+
| 普通用户 | 需要 |
|
|
863
|
+
| teammate plan required | 不需要本地弹窗,发给 leader 审批 |
|
|
864
|
+
| teammate voluntary plan | 不需要本地弹窗,直接退出 |
|
|
865
|
+
|
|
866
|
+
### 10.4 validateInput
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
async validateInput(_input, { getAppState, options }) {
|
|
870
|
+
if (isTeammate()) {
|
|
871
|
+
return { result: true }
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const mode = getAppState().toolPermissionContext.mode
|
|
875
|
+
if (mode !== 'plan') {
|
|
876
|
+
logEvent('tengu_exit_plan_mode_called_outside_plan', ...)
|
|
877
|
+
return {
|
|
878
|
+
result: false,
|
|
879
|
+
message:
|
|
880
|
+
'You are not in plan mode. This tool is only for exiting plan mode after writing a plan. If your plan was already approved, continue with implementation.',
|
|
881
|
+
errorCode: 1,
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return { result: true }
|
|
886
|
+
}
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### 10.5 checkPermissions
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
async checkPermissions(input, context) {
|
|
893
|
+
if (isTeammate()) {
|
|
894
|
+
return { behavior: 'allow', updatedInput: input }
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return {
|
|
898
|
+
behavior: 'ask',
|
|
899
|
+
message: 'Exit plan mode?',
|
|
900
|
+
updatedInput: input,
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### 10.6 call 总流程
|
|
906
|
+
|
|
907
|
+
伪代码:
|
|
908
|
+
|
|
909
|
+
```typescript
|
|
910
|
+
async call(input, context) {
|
|
911
|
+
const isAgent = !!context.agentId
|
|
912
|
+
const filePath = getPlanFilePath(context.agentId)
|
|
913
|
+
const inputPlan =
|
|
914
|
+
'plan' in input && typeof input.plan === 'string'
|
|
915
|
+
? input.plan
|
|
916
|
+
: undefined
|
|
917
|
+
const plan = inputPlan ?? getPlan(context.agentId)
|
|
918
|
+
|
|
919
|
+
if (inputPlan !== undefined && filePath) {
|
|
920
|
+
await writeFile(filePath, inputPlan, 'utf-8').catch(logError)
|
|
921
|
+
void persistFileSnapshotIfRemote()
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (isTeammate() && isPlanModeRequired()) {
|
|
925
|
+
return await submitPlanToLeader(...)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
restorePermissionMode(context)
|
|
929
|
+
|
|
930
|
+
const hasTaskTool =
|
|
931
|
+
isAgentSwarmsEnabled() &&
|
|
932
|
+
context.options.tools.some(t => toolMatchesName(t, AGENT_TOOL_NAME))
|
|
933
|
+
|
|
934
|
+
return {
|
|
935
|
+
data: {
|
|
936
|
+
plan,
|
|
937
|
+
isAgent,
|
|
938
|
+
filePath,
|
|
939
|
+
hasTaskTool: hasTaskTool || undefined,
|
|
940
|
+
planWasEdited: inputPlan !== undefined || undefined,
|
|
941
|
+
},
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### 10.7 teammate required 分支
|
|
947
|
+
|
|
948
|
+
如果 `isTeammate() && isPlanModeRequired()`:
|
|
949
|
+
|
|
950
|
+
1. 如果没有 plan,抛错:
|
|
951
|
+
|
|
952
|
+
```typescript
|
|
953
|
+
throw new Error(
|
|
954
|
+
`No plan file found at ${filePath}. Please write your plan to this file before calling ExitPlanMode.`,
|
|
955
|
+
)
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
2. 生成 `requestId`:
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
generateRequestId('plan_approval', formatAgentId(agentName, teamName || 'default'))
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
3. 写 mailbox:
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
const approvalRequest = {
|
|
968
|
+
type: 'plan_approval_request',
|
|
969
|
+
from: agentName,
|
|
970
|
+
timestamp: new Date().toISOString(),
|
|
971
|
+
planFilePath: filePath,
|
|
972
|
+
planContent: plan,
|
|
973
|
+
requestId,
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
await writeToMailbox(
|
|
977
|
+
'team-lead',
|
|
978
|
+
{
|
|
979
|
+
from: agentName,
|
|
980
|
+
text: jsonStringify(approvalRequest),
|
|
981
|
+
timestamp: new Date().toISOString(),
|
|
982
|
+
},
|
|
983
|
+
teamName,
|
|
984
|
+
)
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
4. 如果是 in-process teammate,更新 task 状态为 awaiting approval。
|
|
988
|
+
5. 返回:
|
|
989
|
+
|
|
990
|
+
```typescript
|
|
991
|
+
{
|
|
992
|
+
plan,
|
|
993
|
+
isAgent: true,
|
|
994
|
+
filePath,
|
|
995
|
+
awaitingLeaderApproval: true,
|
|
996
|
+
requestId,
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
### 10.8 普通退出时恢复权限
|
|
1001
|
+
|
|
1002
|
+
核心伪代码:
|
|
1003
|
+
|
|
1004
|
+
```typescript
|
|
1005
|
+
context.setAppState(prev => {
|
|
1006
|
+
if (prev.toolPermissionContext.mode !== 'plan') return prev
|
|
1007
|
+
|
|
1008
|
+
setHasExitedPlanMode(true)
|
|
1009
|
+
setNeedsPlanModeExitAttachment(true)
|
|
1010
|
+
|
|
1011
|
+
let restoreMode = prev.toolPermissionContext.prePlanMode ?? 'default'
|
|
1012
|
+
|
|
1013
|
+
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
|
1014
|
+
if (restoreMode === 'auto' && !isAutoModeGateEnabled()) {
|
|
1015
|
+
restoreMode = 'default'
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const restoringToAuto = restoreMode === 'auto'
|
|
1019
|
+
const autoWasUsedDuringPlan = autoModeStateModule?.isAutoModeActive() ?? false
|
|
1020
|
+
autoModeStateModule?.setAutoModeActive(restoringToAuto)
|
|
1021
|
+
|
|
1022
|
+
if (autoWasUsedDuringPlan && !restoringToAuto) {
|
|
1023
|
+
setNeedsAutoModeExitAttachment(true)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let baseContext = prev.toolPermissionContext
|
|
1028
|
+
|
|
1029
|
+
if (restoreMode === 'auto') {
|
|
1030
|
+
baseContext = stripDangerousPermissionsForAutoMode(baseContext)
|
|
1031
|
+
} else if (prev.toolPermissionContext.strippedDangerousRules) {
|
|
1032
|
+
baseContext = restoreDangerousPermissions(baseContext)
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
return {
|
|
1036
|
+
...prev,
|
|
1037
|
+
toolPermissionContext: {
|
|
1038
|
+
...baseContext,
|
|
1039
|
+
mode: restoreMode,
|
|
1040
|
+
prePlanMode: undefined,
|
|
1041
|
+
},
|
|
1042
|
+
}
|
|
1043
|
+
})
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### 10.9 gate fallback notification
|
|
1047
|
+
|
|
1048
|
+
如果进入 plan 前是 `auto`,但退出时 auto gate 关闭:
|
|
1049
|
+
|
|
1050
|
+
1. 恢复到 `default`。
|
|
1051
|
+
2. 添加 notification:
|
|
1052
|
+
|
|
1053
|
+
```typescript
|
|
1054
|
+
context.addNotification?.({
|
|
1055
|
+
key: 'auto-mode-gate-plan-exit-fallback',
|
|
1056
|
+
text: `plan exit -> default · ${gateFallbackNotification}`,
|
|
1057
|
+
priority: 'immediate',
|
|
1058
|
+
color: 'warning',
|
|
1059
|
+
timeoutMs: 10000,
|
|
1060
|
+
})
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
### 10.10 tool_result 映射
|
|
1064
|
+
|
|
1065
|
+
#### teammate 等待审批
|
|
1066
|
+
|
|
1067
|
+
```text
|
|
1068
|
+
Your plan has been submitted to the team lead for approval.
|
|
1069
|
+
|
|
1070
|
+
Plan file: <filePath>
|
|
1071
|
+
|
|
1072
|
+
What happens next:
|
|
1073
|
+
1. Wait for the team lead to review your plan
|
|
1074
|
+
2. You will receive a message in your inbox with approval/rejection
|
|
1075
|
+
3. If approved, you can proceed with implementation
|
|
1076
|
+
4. If rejected, refine your plan based on the feedback
|
|
1077
|
+
|
|
1078
|
+
Important: Do NOT proceed until you receive approval.
|
|
1079
|
+
|
|
1080
|
+
Request ID: <requestId>
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
#### agent 场景
|
|
1084
|
+
|
|
1085
|
+
```text
|
|
1086
|
+
User has approved the plan. There is nothing else needed from you now. Please respond with "ok"
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
#### 空 plan
|
|
1090
|
+
|
|
1091
|
+
```text
|
|
1092
|
+
User has approved exiting plan mode. You can now proceed.
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
#### 普通用户批准
|
|
1096
|
+
|
|
1097
|
+
```text
|
|
1098
|
+
User has approved your plan. You can now start coding. Start with updating your todo list if applicable
|
|
1099
|
+
|
|
1100
|
+
Your plan has been saved to: <filePath>
|
|
1101
|
+
You can refer back to it if needed during implementation.
|
|
1102
|
+
|
|
1103
|
+
## Approved Plan:
|
|
1104
|
+
<plan>
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
如果用户编辑过 plan,标题变成:
|
|
1108
|
+
|
|
1109
|
+
```text
|
|
1110
|
+
## Approved Plan (edited by user):
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
如果当前工具列表有 Agent/Team 工具,还会附加并行任务提示。
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## 11. Plan Mode 附件系统
|
|
1118
|
+
|
|
1119
|
+
核心文件:`src/utils/attachments.ts` 和 `src/utils/messages.ts`
|
|
1120
|
+
|
|
1121
|
+
### 11.1 Attachment 类型
|
|
1122
|
+
|
|
1123
|
+
必须支持:
|
|
1124
|
+
|
|
1125
|
+
```typescript
|
|
1126
|
+
type Attachment =
|
|
1127
|
+
| {
|
|
1128
|
+
type: 'plan_mode'
|
|
1129
|
+
reminderType: 'full' | 'sparse'
|
|
1130
|
+
isSubAgent?: boolean
|
|
1131
|
+
planFilePath: string
|
|
1132
|
+
planExists: boolean
|
|
1133
|
+
}
|
|
1134
|
+
| {
|
|
1135
|
+
type: 'plan_mode_reentry'
|
|
1136
|
+
planFilePath: string
|
|
1137
|
+
}
|
|
1138
|
+
| {
|
|
1139
|
+
type: 'plan_mode_exit'
|
|
1140
|
+
planFilePath: string
|
|
1141
|
+
planExists: boolean
|
|
1142
|
+
}
|
|
1143
|
+
| {
|
|
1144
|
+
type: 'plan_file_reference'
|
|
1145
|
+
planFilePath: string
|
|
1146
|
+
planContent: string
|
|
1147
|
+
}
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
### 11.2 `getPlanModeAttachments`
|
|
1151
|
+
|
|
1152
|
+
生成条件:
|
|
1153
|
+
|
|
1154
|
+
1. 当前 `toolPermissionContext.mode !== 'plan'`,返回 `[]`。
|
|
1155
|
+
2. 如果已有 plan mode attachment 且距离上次不足阈值 human turns,返回 `[]`。
|
|
1156
|
+
3. 读取 `planFilePath = getPlanFilePath(agentId)`。
|
|
1157
|
+
4. 读取 `existingPlan = getPlan(agentId)`。
|
|
1158
|
+
5. 如果 `hasExitedPlanModeInSession()` 且 plan 存在,先加入 `plan_mode_reentry`,然后 `setHasExitedPlanMode(false)`。
|
|
1159
|
+
6. 计算 full/sparse reminder。
|
|
1160
|
+
7. 加入 `plan_mode` attachment。
|
|
1161
|
+
|
|
1162
|
+
伪代码:
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
async function getPlanModeAttachments(messages, toolUseContext) {
|
|
1166
|
+
const mode = toolUseContext.getAppState().toolPermissionContext.mode
|
|
1167
|
+
if (mode !== 'plan') return []
|
|
1168
|
+
|
|
1169
|
+
if (messages?.length) {
|
|
1170
|
+
const { turnCount, foundPlanModeAttachment } =
|
|
1171
|
+
getPlanModeAttachmentTurnCount(messages)
|
|
1172
|
+
|
|
1173
|
+
if (foundPlanModeAttachment &&
|
|
1174
|
+
turnCount < PLAN_MODE_ATTACHMENT_CONFIG.TURNS_BETWEEN_ATTACHMENTS) {
|
|
1175
|
+
return []
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const planFilePath = getPlanFilePath(toolUseContext.agentId)
|
|
1180
|
+
const existingPlan = getPlan(toolUseContext.agentId)
|
|
1181
|
+
const attachments = []
|
|
1182
|
+
|
|
1183
|
+
if (hasExitedPlanModeInSession() && existingPlan !== null) {
|
|
1184
|
+
attachments.push({ type: 'plan_mode_reentry', planFilePath })
|
|
1185
|
+
setHasExitedPlanMode(false)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const attachmentCount =
|
|
1189
|
+
countPlanModeAttachmentsSinceLastExit(messages ?? []) + 1
|
|
1190
|
+
|
|
1191
|
+
const reminderType =
|
|
1192
|
+
attachmentCount % FULL_REMINDER_EVERY_N_ATTACHMENTS === 1
|
|
1193
|
+
? 'full'
|
|
1194
|
+
: 'sparse'
|
|
1195
|
+
|
|
1196
|
+
attachments.push({
|
|
1197
|
+
type: 'plan_mode',
|
|
1198
|
+
reminderType,
|
|
1199
|
+
isSubAgent: !!toolUseContext.agentId,
|
|
1200
|
+
planFilePath,
|
|
1201
|
+
planExists: existingPlan !== null,
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
return attachments
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
### 11.3 human turn 计数
|
|
1209
|
+
|
|
1210
|
+
`getPlanModeAttachmentTurnCount(messages)` 从后往前扫描:
|
|
1211
|
+
|
|
1212
|
+
1. 只数 human user turns。
|
|
1213
|
+
2. 不数 `isMeta` user message。
|
|
1214
|
+
3. 不数 tool_result user message。
|
|
1215
|
+
4. 遇到最近的 `plan_mode` 或 `plan_mode_reentry` attachment 停止。
|
|
1216
|
+
|
|
1217
|
+
这是为了避免 tool loop 中每次工具调用都重复注入 plan prompt。
|
|
1218
|
+
|
|
1219
|
+
### 11.4 full/sparse 规则
|
|
1220
|
+
|
|
1221
|
+
源码逻辑:
|
|
1222
|
+
|
|
1223
|
+
```typescript
|
|
1224
|
+
const attachmentCount =
|
|
1225
|
+
countPlanModeAttachmentsSinceLastExit(messages ?? []) + 1
|
|
1226
|
+
|
|
1227
|
+
const reminderType =
|
|
1228
|
+
attachmentCount % FULL_REMINDER_EVERY_N_ATTACHMENTS === 1
|
|
1229
|
+
? 'full'
|
|
1230
|
+
: 'sparse'
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
含义:第 1、N+1、2N+1 次注入 full,其余 sparse。
|
|
1234
|
+
|
|
1235
|
+
### 11.5 `getPlanModeExitAttachment`
|
|
1236
|
+
|
|
1237
|
+
退出后一次性注入:
|
|
1238
|
+
|
|
1239
|
+
```typescript
|
|
1240
|
+
async function getPlanModeExitAttachment(toolUseContext) {
|
|
1241
|
+
if (!needsPlanModeExitAttachment()) return []
|
|
1242
|
+
|
|
1243
|
+
const appState = toolUseContext.getAppState()
|
|
1244
|
+
if (appState.toolPermissionContext.mode === 'plan') {
|
|
1245
|
+
setNeedsPlanModeExitAttachment(false)
|
|
1246
|
+
return []
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
setNeedsPlanModeExitAttachment(false)
|
|
1250
|
+
|
|
1251
|
+
const planFilePath = getPlanFilePath(toolUseContext.agentId)
|
|
1252
|
+
const planExists = getPlan(toolUseContext.agentId) !== null
|
|
1253
|
+
|
|
1254
|
+
return [{ type: 'plan_mode_exit', planFilePath, planExists }]
|
|
1255
|
+
}
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
### 11.6 messages.ts 中的渲染
|
|
1259
|
+
|
|
1260
|
+
`plan_mode`:
|
|
1261
|
+
|
|
1262
|
+
```typescript
|
|
1263
|
+
return getPlanModeInstructions(attachment)
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
`plan_mode_reentry`:
|
|
1267
|
+
|
|
1268
|
+
```text
|
|
1269
|
+
You are returning to plan mode after having previously exited it.
|
|
1270
|
+
A plan file exists at <path>.
|
|
1271
|
+
|
|
1272
|
+
Before proceeding:
|
|
1273
|
+
1. Read existing plan
|
|
1274
|
+
2. Compare current request
|
|
1275
|
+
3. Different task -> overwrite
|
|
1276
|
+
4. Same task -> modify and clean stale parts
|
|
1277
|
+
5. Always edit plan file before ExitPlanMode
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
`plan_mode_exit`:
|
|
1281
|
+
|
|
1282
|
+
```text
|
|
1283
|
+
## Exited Plan Mode
|
|
1284
|
+
|
|
1285
|
+
You have exited plan mode. You can now make edits, run tools, and take actions.
|
|
1286
|
+
The plan file is located at <path> if you need to reference it.
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
---
|
|
1290
|
+
|
|
1291
|
+
## 12. Plan workflow prompt
|
|
1292
|
+
|
|
1293
|
+
核心文件:`src/utils/messages.ts`
|
|
1294
|
+
|
|
1295
|
+
### 12.1 标准 5 阶段 workflow
|
|
1296
|
+
|
|
1297
|
+
`getPlanModeV2Instructions(attachment)` 生成主要 prompt。
|
|
1298
|
+
|
|
1299
|
+
开头必须包含硬约束:
|
|
1300
|
+
|
|
1301
|
+
```text
|
|
1302
|
+
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received.
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
然后给出 plan 文件信息:
|
|
1306
|
+
|
|
1307
|
+
```text
|
|
1308
|
+
No plan file exists yet. You should create your plan at <path> using FileWrite.
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
或:
|
|
1312
|
+
|
|
1313
|
+
```text
|
|
1314
|
+
A plan file already exists at <path>. You can read it and make incremental edits using FileEdit.
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
### 12.2 Phase 1: Initial Understanding
|
|
1318
|
+
|
|
1319
|
+
要求:
|
|
1320
|
+
|
|
1321
|
+
1. 全面理解用户请求。
|
|
1322
|
+
2. 搜索可复用函数、工具、模式。
|
|
1323
|
+
3. 只能使用 `Explore` subagent 类型。
|
|
1324
|
+
4. 最多启动 `getPlanModeV2ExploreAgentCount()` 个 Explore agents。
|
|
1325
|
+
5. 单文件/小改动用 1 个 agent。
|
|
1326
|
+
6. 范围不确定或跨模块时用多个 agent。
|
|
1327
|
+
|
|
1328
|
+
### 12.3 Phase 2: Design
|
|
1329
|
+
|
|
1330
|
+
要求:
|
|
1331
|
+
|
|
1332
|
+
1. 启动 `Plan` agent 设计实现。
|
|
1333
|
+
2. 默认至少 1 个 Plan agent。
|
|
1334
|
+
3. trivial task 可以跳过。
|
|
1335
|
+
4. 最多 `getPlanModeV2AgentCount()` 个 Plan agents。
|
|
1336
|
+
5. agent prompt 必须包含 Phase 1 的完整背景、文件名、代码路径、约束。
|
|
1337
|
+
|
|
1338
|
+
### 12.4 Phase 3: Review
|
|
1339
|
+
|
|
1340
|
+
要求:
|
|
1341
|
+
|
|
1342
|
+
1. 读取 agents 识别出的关键文件。
|
|
1343
|
+
2. 确认方案符合用户原始意图。
|
|
1344
|
+
3. 有疑问时用 `AskUserQuestion`。
|
|
1345
|
+
|
|
1346
|
+
### 12.5 Phase 4: Final Plan
|
|
1347
|
+
|
|
1348
|
+
要求模型把 plan 写入 plan 文件。
|
|
1349
|
+
|
|
1350
|
+
默认 control 版本至少包括:
|
|
1351
|
+
|
|
1352
|
+
1. context:为什么要做这个改动。
|
|
1353
|
+
2. 推荐方案,不列无关替代方案。
|
|
1354
|
+
3. 需要修改的关键文件路径。
|
|
1355
|
+
4. 可复用的现有函数和工具,带文件路径。
|
|
1356
|
+
5. 验证方式,最好是端到端命令。
|
|
1357
|
+
|
|
1358
|
+
### 12.6 Phase 5: ExitPlanMode
|
|
1359
|
+
|
|
1360
|
+
关键规则:
|
|
1361
|
+
|
|
1362
|
+
1. 当计划完成后必须调用 `ExitPlanMode`。
|
|
1363
|
+
2. 当前 turn 只能以 `AskUserQuestion` 或 `ExitPlanMode` 结束。
|
|
1364
|
+
3. 不允许普通文本问“这个计划可以吗”。
|
|
1365
|
+
4. 不允许用 `AskUserQuestion` 请求 plan approval。
|
|
1366
|
+
5. 只有澄清需求或选择方案时才用 `AskUserQuestion`。
|
|
1367
|
+
|
|
1368
|
+
### 12.7 Interview phase
|
|
1369
|
+
|
|
1370
|
+
开关函数:
|
|
1371
|
+
|
|
1372
|
+
```typescript
|
|
1373
|
+
function isPlanModeInterviewPhaseEnabled() {
|
|
1374
|
+
if (process.env.USER_TYPE === 'ant') return true
|
|
1375
|
+
const env = process.env.CLAUDE_CODE_PLAN_MODE_INTERVIEW_PHASE
|
|
1376
|
+
if (isEnvTruthy(env)) return true
|
|
1377
|
+
if (isEnvDefinedFalsy(env)) return false
|
|
1378
|
+
return getFeatureValue_CACHED_MAY_BE_STALE(
|
|
1379
|
+
'tengu_plan_mode_interview_phase',
|
|
1380
|
+
false,
|
|
1381
|
+
)
|
|
1382
|
+
}
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
interview phase 不强制 Explore/Plan agents,而是让模型:
|
|
1386
|
+
|
|
1387
|
+
1. 直接读取代码。
|
|
1388
|
+
2. 逐步写 plan 文件。
|
|
1389
|
+
3. 遇到决策就问用户。
|
|
1390
|
+
4. 反复迭代,直到所有歧义解决。
|
|
1391
|
+
5. 最后调用 `ExitPlanMode`。
|
|
1392
|
+
|
|
1393
|
+
首轮要求:
|
|
1394
|
+
|
|
1395
|
+
```text
|
|
1396
|
+
Start by quickly scanning a few key files to form an initial understanding.
|
|
1397
|
+
Then write a skeleton plan and ask the user your first round of questions.
|
|
1398
|
+
Don't explore exhaustively before engaging the user.
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
### 12.8 PewterLedger 实验
|
|
1402
|
+
|
|
1403
|
+
文件:`src/utils/planModeV2.ts`
|
|
1404
|
+
|
|
1405
|
+
变体:
|
|
1406
|
+
|
|
1407
|
+
```typescript
|
|
1408
|
+
type PewterLedgerVariant = 'trim' | 'cut' | 'cap' | null
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
| 变体 | 作用 |
|
|
1412
|
+
|------|------|
|
|
1413
|
+
| `null` | control,完整 plan |
|
|
1414
|
+
| `trim` | 轻度缩短 |
|
|
1415
|
+
| `cut` | 中度缩短,减少背景 |
|
|
1416
|
+
| `cap` | 强限制,40 行上限 |
|
|
1417
|
+
|
|
1418
|
+
复现最小版本可以先只实现 control。
|
|
1419
|
+
|
|
1420
|
+
---
|
|
1421
|
+
|
|
1422
|
+
## 13. Explore Agent 与 Plan Agent
|
|
1423
|
+
|
|
1424
|
+
### 13.1 Explore Agent
|
|
1425
|
+
|
|
1426
|
+
文件:`src/tools/AgentTool/built-in/exploreAgent.ts`
|
|
1427
|
+
|
|
1428
|
+
关键定义:
|
|
1429
|
+
|
|
1430
|
+
```typescript
|
|
1431
|
+
const EXPLORE_AGENT = {
|
|
1432
|
+
agentType: 'Explore',
|
|
1433
|
+
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
|
|
1434
|
+
omitClaudeMd: true,
|
|
1435
|
+
disallowedTools: [
|
|
1436
|
+
'Agent',
|
|
1437
|
+
'ExitPlanMode',
|
|
1438
|
+
'FileEdit',
|
|
1439
|
+
'FileWrite',
|
|
1440
|
+
'NotebookEdit',
|
|
1441
|
+
],
|
|
1442
|
+
}
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
必须保证:
|
|
1446
|
+
|
|
1447
|
+
1. 只读。
|
|
1448
|
+
2. 不能嵌套 Agent。
|
|
1449
|
+
3. 不能退出 plan mode。
|
|
1450
|
+
4. 不能写文件。
|
|
1451
|
+
5. 主要用于快速搜索代码。
|
|
1452
|
+
|
|
1453
|
+
### 13.2 Plan Agent
|
|
1454
|
+
|
|
1455
|
+
文件:`src/tools/AgentTool/built-in/planAgent.ts`
|
|
1456
|
+
|
|
1457
|
+
关键定义:
|
|
1458
|
+
|
|
1459
|
+
```typescript
|
|
1460
|
+
const PLAN_AGENT = {
|
|
1461
|
+
agentType: 'Plan',
|
|
1462
|
+
model: 'inherit',
|
|
1463
|
+
omitClaudeMd: true,
|
|
1464
|
+
tools: EXPLORE_AGENT.tools,
|
|
1465
|
+
disallowedTools: EXPLORE_AGENT.disallowedTools,
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
Plan Agent 的 prompt 要求:
|
|
1470
|
+
|
|
1471
|
+
1. 作为软件架构师。
|
|
1472
|
+
2. 基于 Explore 结果设计方案。
|
|
1473
|
+
3. 输出分步实现策略。
|
|
1474
|
+
4. 输出 `Critical Files for Implementation`。
|
|
1475
|
+
5. 仍然只读,不能写文件。
|
|
1476
|
+
|
|
1477
|
+
### 13.3 agent 数量
|
|
1478
|
+
|
|
1479
|
+
文件:`src/utils/planModeV2.ts`
|
|
1480
|
+
|
|
1481
|
+
```typescript
|
|
1482
|
+
function getPlanModeV2ExploreAgentCount() {
|
|
1483
|
+
if (env CLAUDE_CODE_PLAN_V2_EXPLORE_AGENT_COUNT is 1..10) return env
|
|
1484
|
+
return 3
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function getPlanModeV2AgentCount() {
|
|
1488
|
+
if (env CLAUDE_CODE_PLAN_V2_AGENT_COUNT is 1..10) return env
|
|
1489
|
+
if (subscriptionType === 'max' && rateLimitTier === 'default_claude_max_20x') return 3
|
|
1490
|
+
if (subscriptionType === 'enterprise' || subscriptionType === 'team') return 3
|
|
1491
|
+
return 1
|
|
1492
|
+
}
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
---
|
|
1496
|
+
|
|
1497
|
+
## 14. Auto mode 与 Plan mode 的交互
|
|
1498
|
+
|
|
1499
|
+
如果实现项目没有 auto mode,可以跳过本节,直接把 `prePlanMode` 恢复为原 mode。
|
|
1500
|
+
|
|
1501
|
+
如果有 auto mode,必须实现:
|
|
1502
|
+
|
|
1503
|
+
1. `shouldPlanUseAutoMode()`
|
|
1504
|
+
2. `prepareContextForPlanMode()`
|
|
1505
|
+
3. `transitionPlanAutoMode()`
|
|
1506
|
+
4. ExitPlanMode 里的 auto restore/fallback
|
|
1507
|
+
|
|
1508
|
+
### 14.1 shouldPlanUseAutoMode
|
|
1509
|
+
|
|
1510
|
+
```typescript
|
|
1511
|
+
return (
|
|
1512
|
+
hasAutoModeOptIn() &&
|
|
1513
|
+
isAutoModeGateEnabled() &&
|
|
1514
|
+
getUseAutoModeDuringPlan()
|
|
1515
|
+
)
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
### 14.2 settings 中途变化
|
|
1519
|
+
|
|
1520
|
+
`transitionPlanAutoMode(context)`:
|
|
1521
|
+
|
|
1522
|
+
1. 只在 `mode === 'plan'` 时生效。
|
|
1523
|
+
2. 如果 `prePlanMode === 'bypassPermissions'`,不激活 auto。
|
|
1524
|
+
3. 如果 want 和 have 都 true,重新 strip dangerous permissions。
|
|
1525
|
+
4. 如果 want true have false,打开 auto 并 strip。
|
|
1526
|
+
5. 如果 want false have true,关闭 auto 并 restore dangerous permissions。
|
|
1527
|
+
|
|
1528
|
+
---
|
|
1529
|
+
|
|
1530
|
+
## 15. Teammate 审批流
|
|
1531
|
+
|
|
1532
|
+
当 teammate 使用 plan mode,分两类。
|
|
1533
|
+
|
|
1534
|
+
### 15.1 plan_mode_required teammate
|
|
1535
|
+
|
|
1536
|
+
流程:
|
|
1537
|
+
|
|
1538
|
+
```
|
|
1539
|
+
teammate 调用 ExitPlanMode
|
|
1540
|
+
│
|
|
1541
|
+
├─ 如果 plan 文件不存在 -> 抛错
|
|
1542
|
+
│
|
|
1543
|
+
├─ 生成 plan_approval_request
|
|
1544
|
+
│
|
|
1545
|
+
├─ 写入 team-lead mailbox
|
|
1546
|
+
│
|
|
1547
|
+
├─ 标记 teammate task awaiting approval
|
|
1548
|
+
│
|
|
1549
|
+
└─ 返回 awaitingLeaderApproval: true
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
teammate 收到 tool result 后必须停止实现,等待 inbox。
|
|
1553
|
+
|
|
1554
|
+
### 15.2 voluntary plan mode teammate
|
|
1555
|
+
|
|
1556
|
+
如果不是 required:
|
|
1557
|
+
|
|
1558
|
+
1. `requiresUserInteraction()` 返回 false。
|
|
1559
|
+
2. `checkPermissions()` allow。
|
|
1560
|
+
3. `call()` 直接恢复权限模式。
|
|
1561
|
+
|
|
1562
|
+
---
|
|
1563
|
+
|
|
1564
|
+
## 16. 从零实现顺序
|
|
1565
|
+
|
|
1566
|
+
如果让 GPT-4 级别模型实现,不要一次让它写全部。按这个顺序分任务。
|
|
1567
|
+
|
|
1568
|
+
### 步骤 1:实现状态字段
|
|
1569
|
+
|
|
1570
|
+
实现:
|
|
1571
|
+
|
|
1572
|
+
1. `toolPermissionContext.mode`
|
|
1573
|
+
2. `toolPermissionContext.prePlanMode`
|
|
1574
|
+
3. `needsPlanModeExitAttachment`
|
|
1575
|
+
4. `hasExitedPlanModeInSession`
|
|
1576
|
+
5. `planSlugCache`
|
|
1577
|
+
|
|
1578
|
+
### 步骤 2:实现 `plans.ts`
|
|
1579
|
+
|
|
1580
|
+
实现:
|
|
1581
|
+
|
|
1582
|
+
1. `getPlansDirectory`
|
|
1583
|
+
2. `getPlanSlug`
|
|
1584
|
+
3. `getPlanFilePath`
|
|
1585
|
+
4. `getPlan`
|
|
1586
|
+
5. `clearPlanSlug`
|
|
1587
|
+
|
|
1588
|
+
先不实现 resume/fork。
|
|
1589
|
+
|
|
1590
|
+
### 步骤 3:实现权限准备函数
|
|
1591
|
+
|
|
1592
|
+
实现:
|
|
1593
|
+
|
|
1594
|
+
1. `handlePlanModeTransition`
|
|
1595
|
+
2. `prepareContextForPlanMode`
|
|
1596
|
+
3. `applyPermissionUpdate({ type: 'setMode' })`
|
|
1597
|
+
|
|
1598
|
+
### 步骤 4:实现 `/plan` 命令
|
|
1599
|
+
|
|
1600
|
+
实现:
|
|
1601
|
+
|
|
1602
|
+
1. `src/commands/plan/index.ts`
|
|
1603
|
+
2. `src/commands/plan/plan.tsx`
|
|
1604
|
+
3. `PlanDisplay`
|
|
1605
|
+
4. `/plan open`
|
|
1606
|
+
|
|
1607
|
+
### 步骤 5:实现 Plan Mode 权限拦截
|
|
1608
|
+
|
|
1609
|
+
至少保证:
|
|
1610
|
+
|
|
1611
|
+
1. 读工具允许。
|
|
1612
|
+
2. 非 plan 文件写入禁止。
|
|
1613
|
+
3. plan 文件写入允许。
|
|
1614
|
+
4. ExitPlanMode 允许。
|
|
1615
|
+
|
|
1616
|
+
### 步骤 6:实现 EnterPlanModeTool
|
|
1617
|
+
|
|
1618
|
+
实现:
|
|
1619
|
+
|
|
1620
|
+
1. empty input schema
|
|
1621
|
+
2. channels disabled gate
|
|
1622
|
+
3. agent context 禁止
|
|
1623
|
+
4. call 切 mode
|
|
1624
|
+
5. tool_result prompt
|
|
1625
|
+
|
|
1626
|
+
### 步骤 7:实现 ExitPlanModeV2Tool
|
|
1627
|
+
|
|
1628
|
+
先实现普通用户版本:
|
|
1629
|
+
|
|
1630
|
+
1. validate mode 必须是 plan
|
|
1631
|
+
2. checkPermissions ask
|
|
1632
|
+
3. 读取 plan
|
|
1633
|
+
4. 恢复 prePlanMode
|
|
1634
|
+
5. 返回 approved plan
|
|
1635
|
+
|
|
1636
|
+
再补 teammate 和 input.plan。
|
|
1637
|
+
|
|
1638
|
+
### 步骤 8:实现 attachment
|
|
1639
|
+
|
|
1640
|
+
实现:
|
|
1641
|
+
|
|
1642
|
+
1. `plan_mode`
|
|
1643
|
+
2. `plan_mode_exit`
|
|
1644
|
+
3. `plan_mode_reentry`
|
|
1645
|
+
4. human turn throttle
|
|
1646
|
+
|
|
1647
|
+
### 步骤 9:实现 workflow prompt
|
|
1648
|
+
|
|
1649
|
+
先实现标准 5 阶段。
|
|
1650
|
+
|
|
1651
|
+
之后再补:
|
|
1652
|
+
|
|
1653
|
+
1. interview phase
|
|
1654
|
+
2. PewterLedger variants
|
|
1655
|
+
3. sparse reminders
|
|
1656
|
+
|
|
1657
|
+
### 步骤 10:实现 Explore/Plan agents
|
|
1658
|
+
|
|
1659
|
+
实现:
|
|
1660
|
+
|
|
1661
|
+
1. Explore agent 只读定义
|
|
1662
|
+
2. Plan agent 只读定义
|
|
1663
|
+
3. agent count env override
|
|
1664
|
+
|
|
1665
|
+
### 步骤 11:实现恢复和远程增强
|
|
1666
|
+
|
|
1667
|
+
实现:
|
|
1668
|
+
|
|
1669
|
+
1. `copyPlanForResume`
|
|
1670
|
+
2. `copyPlanForFork`
|
|
1671
|
+
3. `persistFileSnapshotIfRemote`
|
|
1672
|
+
4. `plan_file_reference`
|
|
1673
|
+
|
|
1674
|
+
---
|
|
1675
|
+
|
|
1676
|
+
## 17. 最小可用版本
|
|
1677
|
+
|
|
1678
|
+
如果只想先跑通 Plan Mode,保留这些:
|
|
1679
|
+
|
|
1680
|
+
1. `/plan` 命令。
|
|
1681
|
+
2. `toolPermissionContext.mode = 'plan'`。
|
|
1682
|
+
3. `prePlanMode`。
|
|
1683
|
+
4. `getPlanFilePath` 和 `getPlan`。
|
|
1684
|
+
5. 只允许写 plan 文件。
|
|
1685
|
+
6. `EnterPlanModeTool`。
|
|
1686
|
+
7. `ExitPlanModeTool` 普通用户流程。
|
|
1687
|
+
8. `plan_mode` 附件注入一个简单 workflow。
|
|
1688
|
+
|
|
1689
|
+
可以暂时省略:
|
|
1690
|
+
|
|
1691
|
+
1. auto mode。
|
|
1692
|
+
2. teammate。
|
|
1693
|
+
3. resume/fork。
|
|
1694
|
+
4. remote snapshot。
|
|
1695
|
+
5. PewterLedger。
|
|
1696
|
+
6. interview phase。
|
|
1697
|
+
7. full/sparse 节流。
|
|
1698
|
+
8. Explore/Plan agent 数量实验。
|
|
1699
|
+
|
|
1700
|
+
最小可用数据流:
|
|
1701
|
+
|
|
1702
|
+
```
|
|
1703
|
+
/plan task
|
|
1704
|
+
│
|
|
1705
|
+
▼
|
|
1706
|
+
mode = plan, prePlanMode = oldMode
|
|
1707
|
+
│
|
|
1708
|
+
▼
|
|
1709
|
+
attachment 注入:只能读代码,写 plan 文件
|
|
1710
|
+
│
|
|
1711
|
+
▼
|
|
1712
|
+
模型写 ~/.claude/plans/<slug>.md
|
|
1713
|
+
│
|
|
1714
|
+
▼
|
|
1715
|
+
模型调用 ExitPlanMode
|
|
1716
|
+
│
|
|
1717
|
+
▼
|
|
1718
|
+
用户批准
|
|
1719
|
+
│
|
|
1720
|
+
▼
|
|
1721
|
+
mode = prePlanMode
|
|
1722
|
+
```
|
|
1723
|
+
|
|
1724
|
+
---
|
|
1725
|
+
|
|
1726
|
+
## 18. 完整版本增强项
|
|
1727
|
+
|
|
1728
|
+
按优先级补:
|
|
1729
|
+
|
|
1730
|
+
1. `/plan open`。
|
|
1731
|
+
2. `plan_mode_exit` 附件。
|
|
1732
|
+
3. `plan_mode_reentry` 附件。
|
|
1733
|
+
4. full/sparse 注入节流。
|
|
1734
|
+
5. Explore/Plan agents。
|
|
1735
|
+
6. agent count env override。
|
|
1736
|
+
7. interview phase。
|
|
1737
|
+
8. auto mode integration。
|
|
1738
|
+
9. teammate approval。
|
|
1739
|
+
10. resume/fork plan recovery。
|
|
1740
|
+
11. remote file snapshot。
|
|
1741
|
+
12. PewterLedger variants。
|
|
1742
|
+
|
|
1743
|
+
---
|
|
1744
|
+
|
|
1745
|
+
## 19. 测试清单
|
|
1746
|
+
|
|
1747
|
+
### 19.1 `/plan` 基本行为
|
|
1748
|
+
|
|
1749
|
+
```text
|
|
1750
|
+
初始 mode = default
|
|
1751
|
+
输入 /plan
|
|
1752
|
+
期望:
|
|
1753
|
+
- mode = plan
|
|
1754
|
+
- prePlanMode = default
|
|
1755
|
+
- onDone('Enabled plan mode')
|
|
1756
|
+
- shouldQuery 不为 true
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
```text
|
|
1760
|
+
初始 mode = default
|
|
1761
|
+
输入 /plan 重构认证模块
|
|
1762
|
+
期望:
|
|
1763
|
+
- mode = plan
|
|
1764
|
+
- prePlanMode = default
|
|
1765
|
+
- onDone('Enabled plan mode', { shouldQuery: true })
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
```text
|
|
1769
|
+
初始 mode = plan
|
|
1770
|
+
没有 plan 文件
|
|
1771
|
+
输入 /plan
|
|
1772
|
+
期望:
|
|
1773
|
+
- onDone('Already in plan mode. No plan written yet.')
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
```text
|
|
1777
|
+
初始 mode = plan
|
|
1778
|
+
有 plan 文件
|
|
1779
|
+
输入 /plan
|
|
1780
|
+
期望:
|
|
1781
|
+
- 输出 Current Plan
|
|
1782
|
+
- 输出 plan path
|
|
1783
|
+
- 输出 plan content
|
|
1784
|
+
```
|
|
1785
|
+
|
|
1786
|
+
```text
|
|
1787
|
+
初始 mode = plan
|
|
1788
|
+
有 plan 文件
|
|
1789
|
+
输入 /plan open
|
|
1790
|
+
期望:
|
|
1791
|
+
- 调用 editFileInEditor(planPath)
|
|
1792
|
+
- 成功时输出 Opened plan in editor
|
|
1793
|
+
- 失败时输出 Failed to open plan in editor
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
### 19.2 Plan 文件
|
|
1797
|
+
|
|
1798
|
+
测试:
|
|
1799
|
+
|
|
1800
|
+
1. 同一 session 多次 `getPlanFilePath()` 返回同一路径。
|
|
1801
|
+
2. 不同 session slug 不同。
|
|
1802
|
+
3. `agentId` 存在时文件名包含 `-agent-{agentId}`。
|
|
1803
|
+
4. `plansDirectory` 越界时 fallback 到 `~/.claude/plans`。
|
|
1804
|
+
5. `getPlan()` 文件不存在返回 null。
|
|
1805
|
+
6. `getPlan()` 读错误时 logError 并返回 null。
|
|
1806
|
+
|
|
1807
|
+
### 19.3 EnterPlanModeTool
|
|
1808
|
+
|
|
1809
|
+
测试:
|
|
1810
|
+
|
|
1811
|
+
1. input schema 不接受任何字段。
|
|
1812
|
+
2. `context.agentId` 存在时抛错。
|
|
1813
|
+
3. channels active 时 `isEnabled() === false`。
|
|
1814
|
+
4. call 后 mode 变 plan。
|
|
1815
|
+
5. call 后 `prePlanMode` 记录旧 mode。
|
|
1816
|
+
|
|
1817
|
+
### 19.4 ExitPlanModeV2Tool
|
|
1818
|
+
|
|
1819
|
+
普通用户:
|
|
1820
|
+
|
|
1821
|
+
1. mode 不是 plan 时 validate 拒绝。
|
|
1822
|
+
2. mode 是 plan 时 validate 通过。
|
|
1823
|
+
3. checkPermissions 返回 ask。
|
|
1824
|
+
4. call 后 mode 恢复 prePlanMode。
|
|
1825
|
+
5. call 后 `prePlanMode` 清空。
|
|
1826
|
+
6. call 后 `hasExitedPlanModeInSession = true`。
|
|
1827
|
+
7. call 后 `needsPlanModeExitAttachment = true`。
|
|
1828
|
+
8. plan 为空时 tool_result 返回 “approved exiting plan mode”。
|
|
1829
|
+
9. plan 非空时 tool_result 包含 approved plan。
|
|
1830
|
+
|
|
1831
|
+
input.plan:
|
|
1832
|
+
|
|
1833
|
+
1. 传入 `input.plan` 时写回 plan 文件。
|
|
1834
|
+
2. `planWasEdited = true`。
|
|
1835
|
+
3. tool_result 标题是 `Approved Plan (edited by user)`。
|
|
1836
|
+
|
|
1837
|
+
teammate:
|
|
1838
|
+
|
|
1839
|
+
1. `requiresUserInteraction()` 返回 false。
|
|
1840
|
+
2. plan_required 且无 plan 时抛错。
|
|
1841
|
+
3. plan_required 且有 plan 时写 mailbox。
|
|
1842
|
+
4. 返回 `awaitingLeaderApproval: true`。
|
|
1843
|
+
|
|
1844
|
+
### 19.5 附件系统
|
|
1845
|
+
|
|
1846
|
+
测试:
|
|
1847
|
+
|
|
1848
|
+
1. mode 非 plan 时不注入 `plan_mode`。
|
|
1849
|
+
2. mode plan 时注入 `plan_mode`。
|
|
1850
|
+
3. plan 文件不存在时 `planExists = false`。
|
|
1851
|
+
4. plan 文件存在时 `planExists = true`。
|
|
1852
|
+
5. tool loop 中不因为 assistant/tool messages 反复注入。
|
|
1853
|
+
6. 退出后注入一次 `plan_mode_exit`。
|
|
1854
|
+
7. `plan_mode_exit` 注入后 flag 清零。
|
|
1855
|
+
8. 重新进入且 plan 存在时先注入 `plan_mode_reentry`。
|
|
1856
|
+
|
|
1857
|
+
### 19.6 权限
|
|
1858
|
+
|
|
1859
|
+
Plan mode 中:
|
|
1860
|
+
|
|
1861
|
+
| 操作 | 期望 |
|
|
1862
|
+
|------|------|
|
|
1863
|
+
| FileRead 任意文件 | 允许 |
|
|
1864
|
+
| Grep/Glob | 允许 |
|
|
1865
|
+
| FileWrite plan 文件 | 允许 |
|
|
1866
|
+
| FileWrite 业务文件 | 拒绝 |
|
|
1867
|
+
| FileEdit plan 文件 | 允许 |
|
|
1868
|
+
| FileEdit 业务文件 | 拒绝 |
|
|
1869
|
+
| Bash `ls` | 允许 |
|
|
1870
|
+
| Bash `rm file` | 拒绝 |
|
|
1871
|
+
| ExitPlanMode | 允许/ask |
|
|
1872
|
+
| EnterPlanMode in agent | 拒绝 |
|
|
1873
|
+
|
|
1874
|
+
### 19.7 workflow prompt
|
|
1875
|
+
|
|
1876
|
+
检查 `plan_mode` prompt 必须包含:
|
|
1877
|
+
|
|
1878
|
+
1. 不允许修改文件,除了 plan 文件。
|
|
1879
|
+
2. plan file path。
|
|
1880
|
+
3. Phase 1 Explore。
|
|
1881
|
+
4. Phase 2 Plan。
|
|
1882
|
+
5. Phase 3 Review。
|
|
1883
|
+
6. Phase 4 写 plan 文件。
|
|
1884
|
+
7. Phase 5 调用 ExitPlanMode。
|
|
1885
|
+
8. 禁止用文本请求 plan approval。
|
|
1886
|
+
|
|
1887
|
+
---
|
|
1888
|
+
|
|
1889
|
+
## 20. 常见错误
|
|
1890
|
+
|
|
1891
|
+
### 错误 1:只改 mode,不记录 prePlanMode
|
|
1892
|
+
|
|
1893
|
+
退出时无法恢复原权限模式。
|
|
1894
|
+
|
|
1895
|
+
正确:
|
|
1896
|
+
|
|
1897
|
+
```typescript
|
|
1898
|
+
prepareContextForPlanMode(prev.toolPermissionContext)
|
|
1899
|
+
```
|
|
1900
|
+
|
|
1901
|
+
### 错误 2:`/plan <description>` 没有 `shouldQuery: true`
|
|
1902
|
+
|
|
1903
|
+
用户输入任务描述后,模型不会开始规划。
|
|
1904
|
+
|
|
1905
|
+
### 错误 3:允许写任意文件
|
|
1906
|
+
|
|
1907
|
+
Plan mode 的核心是只读探索。唯一允许写的是 plan 文件。
|
|
1908
|
+
|
|
1909
|
+
### 错误 4:把完整 workflow 放进 EnterPlanMode tool_result
|
|
1910
|
+
|
|
1911
|
+
源码中 EnterPlanMode 只返回简短说明。完整 workflow 由 attachment 注入。这样 `/plan` 和 `EnterPlanMode` 两种入口能共享同一套指令。
|
|
1912
|
+
|
|
1913
|
+
### 错误 5:ExitPlanMode 不走用户审批
|
|
1914
|
+
|
|
1915
|
+
普通用户必须通过 `checkPermissions -> ask`。
|
|
1916
|
+
|
|
1917
|
+
### 错误 6:ExitPlanMode 不设置 exit attachment flag
|
|
1918
|
+
|
|
1919
|
+
退出后模型不知道自己已经可以写文件。
|
|
1920
|
+
|
|
1921
|
+
正确:
|
|
1922
|
+
|
|
1923
|
+
```typescript
|
|
1924
|
+
setHasExitedPlanMode(true)
|
|
1925
|
+
setNeedsPlanModeExitAttachment(true)
|
|
1926
|
+
```
|
|
1927
|
+
|
|
1928
|
+
### 错误 7:plan 文件 slug 每次重新生成
|
|
1929
|
+
|
|
1930
|
+
同一 session 必须复用同一个 slug,否则 `/plan` 读不到之前写的 plan。
|
|
1931
|
+
|
|
1932
|
+
### 错误 8:subagent 复用主 plan 文件名
|
|
1933
|
+
|
|
1934
|
+
subagent plan 文件必须带 `-agent-{agentId}`,否则会覆盖主会话 plan。
|
|
1935
|
+
|
|
1936
|
+
### 错误 9:普通文本询问 plan approval
|
|
1937
|
+
|
|
1938
|
+
prompt 必须明确禁止:
|
|
1939
|
+
|
|
1940
|
+
```text
|
|
1941
|
+
Do NOT ask about plan approval in text or AskUserQuestion.
|
|
1942
|
+
Use ExitPlanMode.
|
|
1943
|
+
```
|
|
1944
|
+
|
|
1945
|
+
### 错误 10:teammate required 没 plan 也允许退出
|
|
1946
|
+
|
|
1947
|
+
plan_mode_required teammate 必须先写 plan 文件,否则抛错。
|
|
1948
|
+
|
|
1949
|
+
---
|
|
1950
|
+
|
|
1951
|
+
## 21. 关键结论
|
|
1952
|
+
|
|
1953
|
+
要一比一实现 Plan Mode,不要把它当成一个 `/plan` 命令。它实际是一个状态机:
|
|
1954
|
+
|
|
1955
|
+
```text
|
|
1956
|
+
default/auto/acceptEdits/bypassPermissions
|
|
1957
|
+
│
|
|
1958
|
+
▼
|
|
1959
|
+
plan
|
|
1960
|
+
│
|
|
1961
|
+
├─ attachment 注入 workflow
|
|
1962
|
+
├─ 权限层限制只读 + plan 文件写入
|
|
1963
|
+
├─ model 写 plan
|
|
1964
|
+
└─ ExitPlanMode 审批
|
|
1965
|
+
│
|
|
1966
|
+
▼
|
|
1967
|
+
prePlanMode
|
|
1968
|
+
```
|
|
1969
|
+
|
|
1970
|
+
最小实现先抓住四件事:
|
|
1971
|
+
|
|
1972
|
+
1. `prePlanMode` 保存和恢复。
|
|
1973
|
+
2. plan 文件路径稳定。
|
|
1974
|
+
3. plan mode 权限只允许读和写 plan 文件。
|
|
1975
|
+
4. ExitPlanMode 是唯一 plan approval 出口。
|
|
1976
|
+
|
|
1977
|
+
完整实现再补:
|
|
1978
|
+
|
|
1979
|
+
1. attachment 节流。
|
|
1980
|
+
2. reentry/exit 附件。
|
|
1981
|
+
3. Explore/Plan agents。
|
|
1982
|
+
4. auto mode 交互。
|
|
1983
|
+
5. teammate 审批。
|
|
1984
|
+
6. resume/fork/remote recovery。
|
|
1985
|
+
7. 实验变体。
|
|
1986
|
+
|
|
1987
|
+
按本文顺序实现,GPT-4 级别模型可以把每一步当成独立小任务完成,并通过测试清单逐项验证。
|