@zhushanwen/pi-plan 0.5.1 → 1.0.0
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 +9 -9
- package/package.json +7 -5
- package/skills/plan-ext-config/SKILL.md +33 -18
- package/src/__tests__/active-path.test.ts +204 -0
- package/src/__tests__/command.test.ts +55 -32
- package/src/__tests__/enter.test.ts +58 -20
- package/src/__tests__/exec-options-alignment.test.ts +56 -0
- package/src/__tests__/execution-notice.test.ts +197 -0
- package/src/__tests__/{compact.test.ts → extract-plan-steps.test.ts} +24 -15
- package/src/__tests__/fsm-consumption.test.ts +388 -0
- package/src/__tests__/index.test.ts +175 -75
- package/src/__tests__/prompts.test.ts +66 -0
- package/src/__tests__/review-contract-boundary.test.ts +123 -0
- package/src/__tests__/review.test.ts +264 -97
- package/src/__tests__/skills.test.ts +14 -5
- package/src/__tests__/state.test.ts +254 -52
- package/src/__tests__/{compact-criteria-array.test.ts → success-criteria-array.test.ts} +29 -70
- package/src/__tests__/templates.test.ts +8 -25
- package/src/__tests__/tool.test.ts +269 -121
- package/src/command.ts +26 -29
- package/src/enter.ts +31 -50
- package/src/{compact.ts → execution-notice.ts} +29 -117
- package/src/index.ts +100 -88
- package/src/prompts.ts +42 -12
- package/src/state.ts +290 -87
- package/src/templates.ts +0 -15
- package/src/tool.ts +664 -256
- package/src/__tests__/compact-handler.test.ts +0 -348
- package/src/__tests__/exec-skills.test.ts +0 -397
- package/src/exec-skills.ts +0 -407
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zhushanwen/pi-plan
|
|
2
2
|
|
|
3
|
-
轻量级规划模式 pi extension:`/plan [描述]` 触发,只读探索并产出结构化计划文档。与 coding-workflow 的区别:无 gate
|
|
3
|
+
轻量级规划模式 pi extension:`/plan [描述]` 触发,只读探索并产出结构化计划文档。与 coding-workflow 的区别:无 retrospect/回顾环节与自动 gate,只负责「想清楚再动手」的计划产出 + 执行前的用户审批(ADR pi-ext-021 prompt-only readonly / pi-ext-022 session-manager state)。
|
|
4
4
|
|
|
5
5
|
## 用法
|
|
6
6
|
|
|
@@ -18,21 +18,21 @@
|
|
|
18
18
|
|
|
19
19
|
## 计划态生命周期
|
|
20
20
|
|
|
21
|
-
进入后 agent 限只读工具集(read/bash/grep/find/ls/plan),按注入提示词产出文档:
|
|
21
|
+
进入后 agent 限只读工具集(read/bash/grep/find/ls/plan/ask_user——ask_user 供探索期向用户提问,D10),按注入提示词产出文档:
|
|
22
22
|
|
|
23
23
|
- **技能流程**(挂载 `--skills`):按技能 SKILL.md 流程产出文档(AI 自行 read 技能文件)。
|
|
24
24
|
- **模板流程**(未挂载):从三源发现的模板清单中选型后撰写 plan.md(见下节)。
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
文档全部就绪后先自审(对照需求逐条核覆盖 / 假设审计 / 章节完整性 / 验收场景可执行,自审发现的问题先修文档),再调 `submit-review`(必带 `selfReview` 自审结论)提交审阅;taiji rpc 宿主(`TAIJI_AGENT_EXT_LOG=1` 且 rpc 模式)挂 `PLAN_REVIEW_MARKER` select 弹 GUI 审批三键(approve 确认执行 / revise 提交评论修订 / dismiss 搁置——非破坏协议级决策,plan 模式保持、进度不变、被搁置的审批不复活),独立 pi / 非 rpc 形态返回文本软门(对话中反馈评论,满意后调 `complete`)。注意区分「dismiss 搁置」(用户第三键决策)与「取消/超时」(选择框被解散,非批准、不丢状态)。
|
|
27
27
|
|
|
28
28
|
## plan 工具
|
|
29
29
|
|
|
30
30
|
模型经 `plan` tool 驱动状态流转,actions 六项:`enter` / `select-template` / `register-doc` / `submit-review` / `complete` / `abort`。
|
|
31
31
|
|
|
32
32
|
- `register-doc`:登记一份产物文档(同 fileName 重登 = version+1 覆盖),供 GUI 产物 tab 展示与内容刷新。
|
|
33
|
-
- `submit-review
|
|
34
|
-
- `complete
|
|
35
|
-
- `abort
|
|
33
|
+
- `submit-review`:全部文档就绪后请求审阅。**必带非空 `selfReview`(自审硬门,无豁免,含修订后重挂)**:缺失/空 → 纠偏错误不挂审批;文档已变而 selfReview 与上次逐字节相同 → 防照抄袭拒收(必须对新版本重做自审)。docs 为空或计划态已退出时返回错误提示(E6 双守卫)。
|
|
34
|
+
- `complete`:经审批闸口进入执行方式选择(approve 边 → dispatching;未经审批直调被拒并指回 submit-review)。有 plan-exec 技能时弹执行方式选择(GUI 宿主经统一表单协议单 choice 问题):选项 = ≤2 个技能档(label `Execute via skill: <name>`)+ Execute 档(goal 跟踪已整合:可用时先建 goal 跟踪,再按复杂度派发 subagent 并行、小步/紧耦合本会话直执)+ 暂不执行(later 边:状态落「已批准」,可再调 `complete` 重新选择)。**无 plan-exec 技能(含检测失败降级)时不弹表单,直通 execute**(工具结果明示「无 plan-exec 技能,直接执行」)。无选择解散归口(dissolvedBy 解散来源直传判别):外部解散(取消/超时/channel-error/non-json)→ review_aborted 边落「已批准」(批准事实保留,可再调 `complete`);命令解散(`/plan abort` 等 reset 已介入)→ 归口 no-op 不覆写终态。headless 无 UI 默认 execute 不弹选择。
|
|
35
|
+
- `abort`:直接退出。complete/abort 退出后恢复工具集;状态落终态两值 `completed`(批准并派发执行)/ `exited`(主动退出)+ isActive=false,docs 保留(产物跨重开留存),selfReview/resumeHint/指纹快照随退出失效。
|
|
36
36
|
|
|
37
37
|
## 执行方式与 plan-exec skill
|
|
38
38
|
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
|
|
41
41
|
四根扫描(单根枚举复用 pi 公开导出的 `loadSkillsFromDir`,跨根组装同构 pi 本体加载序):project `.pi/skills` → 祖先链 `.agents/skills`(近→远,git root 级含)→ `<agentDir>/skills` → `~/.agents/skills`;同名 first-writer-wins + realPath 去重;untrusted 项目跳过前两族。complete 时现扫无缓存(技能热装即见)。
|
|
42
42
|
|
|
43
|
-
检测失败降级为空集,绝不炸 complete 交互闭环:每根 existsSync 守卫 + 整根 try/catch(EACCES 等 fs 错 → 跳过该根 + warn),单 skill frontmatter
|
|
43
|
+
检测失败降级为空集,绝不炸 complete 交互闭环:每根 existsSync 守卫 + 整根 try/catch(EACCES 等 fs 错 → 跳过该根 + warn),单 skill frontmatter 读/解析失败跳过该项。检测为空集(含失败降级)时不弹执行方式表单,直通 execute(D7②)。
|
|
44
44
|
|
|
45
45
|
## Plan File
|
|
46
46
|
|
|
47
|
-
模板流程的产出物,存储在
|
|
47
|
+
模板流程的产出物,存储在 `<project>/.tmp/plans/<slug>/plan.md`(slug 截断 30 字符)。含 YAML frontmatter 与模板章节。进入 plan mode 时自动扫描既有 plan 文件供续写。技能流程的产物文档同写在该目录下,经 `register-doc` 登记。
|
|
48
48
|
|
|
49
49
|
## 模板
|
|
50
50
|
|
|
@@ -60,4 +60,4 @@
|
|
|
60
60
|
|
|
61
61
|
## 依赖
|
|
62
62
|
|
|
63
|
-
依赖:`@zhushanwen/extension-protocol`(PLAN_REVIEW_MARKER + PlanReviewRequest/Response 契约 + ui-form(`uiFormInteract` / `FormQuestion`,统一提问表单协议),dependencies);peer 依赖:`@zhushanwen/pi-goal`(plan 完成后衔接 goal 驱动执行)。
|
|
63
|
+
依赖:`@zhushanwen/extension-protocol`(PLAN_REVIEW_MARKER + PlanReviewRequest/Response 契约 + plan 生命周期状态机与审阅值域契约(`src/extensions/plan/`:transition/derivePhase + 值域守卫/selfReview 截断)+ ui-form(`uiFormInteract` / `FormQuestion`,统一提问表单协议),dependencies);`@zhushanwen/pi-exec-skills`(plan-exec 技能发现/执行门禁单源,ADR-0074,dependencies);peer 依赖:`@zhushanwen/pi-goal`(plan 完成后衔接 goal 驱动执行)。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-plan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Lightweight plan mode for Pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
26
26
|
"typebox": "*",
|
|
27
27
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
28
|
-
"@zhushanwen/pi-goal": "0.14.
|
|
28
|
+
"@zhushanwen/pi-goal": "0.14.10"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@earendil-works/pi-tui": {
|
|
@@ -50,10 +50,12 @@
|
|
|
50
50
|
"templates/"
|
|
51
51
|
],
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@zhushanwen/extension-protocol": "0.
|
|
54
|
-
"@zhushanwen/pi-
|
|
53
|
+
"@zhushanwen/extension-protocol": "0.17.0",
|
|
54
|
+
"@zhushanwen/pi-exec-skills": "0.2.0",
|
|
55
|
+
"@zhushanwen/pi-ext-guards": "0.4.2",
|
|
55
56
|
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
56
|
-
"@zhushanwen/pi-llm-shared": "0.
|
|
57
|
+
"@zhushanwen/pi-llm-shared": "0.11.0",
|
|
58
|
+
"@zhushanwen/pi-session-path": "0.1.0"
|
|
57
59
|
},
|
|
58
60
|
"scripts": {
|
|
59
61
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: plan-ext-config
|
|
3
|
-
description: "使用或排查 @zhushanwen/pi-plan(计划模式 plan mode
|
|
3
|
+
description: "使用或排查 @zhushanwen/pi-plan(计划模式 plan mode)时加载。涵盖 /plan 命令用法、plan 工具六 action、审阅三键裁决与自审硬门、执行方式选项集、session JSONL 存储模型(无独立配置文件)与按 state 崩溃恢复——细节见正文各节。触发词:plan mode、计划模式、/plan、plan 工具、submit-review、selfReview、dismiss、搁置、register-doc、plan-state、计划文档登记、审阅挂起、计划模式排查、plan-ext-config。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# plan 使用与存储指南
|
|
@@ -28,28 +28,39 @@ description: "使用或排查 @zhushanwen/pi-plan(计划模式 plan mode)时
|
|
|
28
28
|
- **`enter`**:agent 自助进入(无需用户确认——plan mode 只读无害,进入事实经 GUI PlanModeBar 显形,用户随时可退出)。参数 `requirement` + 可选 `skills`。
|
|
29
29
|
- **`select-template`**:模板流程选型(`--template` 直传后调用会报错)。返回 content 携带胜者模板全文;错名报错自带可用清单自愈。
|
|
30
30
|
- **`register-doc`**:登记一份产物文档(参数 `fileName` 必须是 plan 目录内纯文件名,路径分隔符/`..` 被拒;同 fileName 重登 = version+1 原位覆盖,UI tab 顺序不跳动)。absPath 由扩展从 plan 目录推导,不信任 LLM 申报路径。
|
|
31
|
-
- **`submit-review`**:全部文档就绪后请求审阅(docs
|
|
32
|
-
- **`complete`**:用户 approve
|
|
31
|
+
- **`submit-review`**:全部文档就绪后请求审阅(docs 为空或已退出计划态返回错误提示,不挂交互)。**必带 `selfReview` 参数(自审硬门,无豁免)**:提交前对照需求逐条核覆盖 / 假设审计([UNVERIFIED] 清零或显式列出)/ 章节完整性对照模板 / 验收场景真实可执行,自审发现的问题先修文档再提交;缺失/空 → tool result 纠偏不挂交互;文档已变而 selfReview 与上次逐字节相同 → 防照抄袭拒收(必须对新版本重做自审)。
|
|
32
|
+
- **`complete`**:用户 approve 后退出计划态,先弹执行方式选择(见下节)。未经审批闸口(如 planning 态直调)会被拒并指回 submit-review——「执行前的用户审批」是结构性保证。
|
|
33
33
|
- **`abort`**:直接退出。
|
|
34
34
|
|
|
35
35
|
退出(complete/abort)恢复完整工具集;`plan-state` 落 isActive=false,docs 清单保留(产物文档跨重开可回看,至下次 /plan 同 slug 覆写)。
|
|
36
36
|
|
|
37
|
-
## 审阅闭环(submit-review →
|
|
37
|
+
## 审阅闭环(submit-review → 三键裁决)
|
|
38
38
|
|
|
39
|
-
- **taiji rpc 宿主**(`TAIJI_AGENT_EXT_LOG=1` 且 `mode==='rpc'`):挂 `PLAN_REVIEW_MARKER` select 弹 GUI
|
|
39
|
+
- **taiji rpc 宿主**(`TAIJI_AGENT_EXT_LOG=1` 且 `mode==='rpc'`):挂 `PLAN_REVIEW_MARKER` select 弹 GUI 审批条,payload 携带 docs + selfReview(写侧 4KB 截断),用户三键裁决——**approve**(确认执行,自动走 complete 流程,状态进 dispatching)/ **revise**(提交 `{quote, comment}[]` 评论,注入对话要求修订,状态进 revising)/ **dismiss 搁置**(D3 协议级决策,非破坏:不杀 turn、不丢状态,状态回 planning,被搁置的审批不复活;tool result 指示 agent 告知用户已搁置并询问下一步,不实施改动)。选择框被解散(turn abort / TUI 取消)非批准:按归口判别走 review_aborted 边或 no-op,result 明确禁止实施。
|
|
40
40
|
- **独立 pi / 非 rpc 形态**:文本软门——result 指示 agent 请用户直接在对话中反馈;用户满意确认后 agent 调 `complete`。
|
|
41
|
-
- **修订闭环**:revise 后逐条评论 rewrite 文件 + 重新 `register-doc`(version+1
|
|
41
|
+
- **修订闭环**:revise 后逐条评论 rewrite 文件 + 重新 `register-doc`(version+1),**对新版本重做自审**后带新 selfReview 再调 `submit-review` 重挂,循环直至 approve。
|
|
42
42
|
- **重提交无变化检测**:上次 submit-review 后无任何 register-doc(docs 指纹相同)再次 submit-review 时,result 末行追加确定性警告,提示必须先 rewrite + re-register。
|
|
43
|
+
- **降级恢复(重新提交审批)**:state=reviewing 而无挂起审批(会话重启等)时,宿主降级分支提供「重新提交审批」按钮——点击注入一条固定文案的用户可见消息,agent 收到后**直接** `submit-review` 重挂(提示词已带此纪律);会话重启时 E3 steer 会携带上轮 selfReview 全文供原样回传(该文档集的自审不重做,但过门义务不豁免)。
|
|
43
44
|
|
|
44
45
|
## 执行方式选项集(complete 时)
|
|
45
46
|
|
|
46
47
|
选项 = **≤2 个检测到的 plan-exec skill 档**(label `Execute via skill: <name>`)+ **Execute 档** + **暂不执行(Not now)**:
|
|
47
48
|
|
|
48
|
-
- **plan-exec skill
|
|
49
|
+
- **plan-exec skill 档(标记写法)**:skill 作者在 SKILL.md **frontmatter 加一行 `plan-exec: true`**(严格布尔 true,字符串 "true" 不算)即被提议,例如:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
---
|
|
53
|
+
name: dev-flow
|
|
54
|
+
description: ...
|
|
55
|
+
plan-exec: true
|
|
56
|
+
---
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
四根扫描(project `.pi/skills` → 祖先链 `.agents/skills` → `<agentDir>/skills` → `~/.agents/skills`,同构 pi 本体加载序,untrusted 项目跳过前两族),root 序同名 first-writer-wins,取前 2 个。complete 时现扫无缓存(技能热装即见);检测失败降级为空集,绝不阻断交互。无任何 plan-exec 技能时不弹执行方式表单(无技能时确认执行直通,不弹恒两项的死表单)。
|
|
49
60
|
- **Execute 档**:goal 跟踪已整合——goal 扩展可用时先建 goal 跟踪(`/goal`),再按复杂度执行:独立可并行任务派发 subagent、小步/紧耦合在本会话直执;goal 不可用/启动失败降级为直接执行(notify 提示 + result 带恢复动作)。
|
|
50
|
-
- **暂不执行**:留在 plan mode
|
|
61
|
+
- **暂不执行**:留在 plan mode,状态落「已批准」(later 边),不派发实施——需要时说『执行』或再调 `complete` 重新选执行方式。
|
|
51
62
|
|
|
52
|
-
headless(json/print 无 UI)默认 execute
|
|
63
|
+
headless(json/print 无 UI)默认 execute 不弹选择;交互框被解散(取消 / 超时 / channel-error / non-json)按「无选择解散」归口:无命令介入时状态落「已批准」(批准事实保留)+ result 可再调 complete;plan abort 等命令已退出时归口 no-op(不覆写终态)。
|
|
53
64
|
|
|
54
65
|
**goal 桥结构建议**:计划文档建议包含 `## Implementation Steps` 编号步骤节——Execute 档的 goal 步骤提取硬依赖该节;缺失会退化 fallback(扫全部编号项)或提取失败(no-steps,有恢复文案)。
|
|
55
66
|
|
|
@@ -58,22 +69,26 @@ headless(json/print 无 UI)默认 execute 不弹选择;通道失败(chan
|
|
|
58
69
|
**当前版本采用 session JSONL 的 append-only event sourcing,没有独立数据文件。**
|
|
59
70
|
|
|
60
71
|
- 每次状态变更调 `pi.appendEntry('plan-state', {...})`,customType 固定为 `plan-state`;重建时取 session 内**最后一条** plan-state entry 为当前态(逐字段白名单降级读,旧版 entry 缺字段归一为空/无值)。
|
|
61
|
-
- 字段:`isActive` / `planFilePath` / `requirement` / `templateName` / `
|
|
72
|
+
- 字段:`isActive` / `planFilePath` / `requirement` / `templateName` / `templateProvided`(布尔,--template 直传标记;旧字段 `templateProvidedPath` 已停写,读侧映射 readTemplateProvided)/ `skills`(挂载技能名)/ `docs`(产物清单 `PlanDocMeta[]`:fileName + absPath + sourceSkill + version)/ `state`(生命周期状态八值:idle/planning/reviewing/revising/approved/dispatching/completed/exited——取代式演进,旧字段 `reviewState`/`reviewStateSource` 已停写,读侧映射:awaiting→reviewing、revising→revising、无→planning|idle 按 isActive,reviewStateSource:'resubmit'→resumeHint:'resubmit')/ `selfReview`(上次提交的自审结论,≤4KB——E3 回传源 + 防照抄比较基线)/ `resumeHint`(降级等待原因:`resubmit` 会话重启待重提交,清除点三处,不跨轮残留)/ `lastSubmitReviewDocsFingerprint`。
|
|
62
73
|
- **产物文档本体**写在 `<cwd>/.tmp/plans/<slug>/`(slug 截断 30 字符)——文档是普通文件,agent 经 bash 写入;`register-doc` 只登记元数据。未产任何文档即退出会清理空 slug 目录(非空一律保留)。
|
|
63
74
|
- **模板三源发现**(同名 last-writer-wins,项目 > 用户 > 内置):内置(包内 `templates/`,5 个)+ 用户级 `~/.agents/plans/*.md` + 项目级 `<project>/.agents/plans/*.md`。进入时清单以 `<available-plans>` 段一次性注入。
|
|
64
75
|
|
|
65
|
-
##
|
|
76
|
+
## 崩溃恢复(E3,按 state 查表)
|
|
66
77
|
|
|
67
|
-
|
|
78
|
+
挂起的交互(审批 select / 执行方式 form)随 pi 进程消亡,但持久态经 entry 存活。session 重启(session_start)时按 `state` 查表恢复:
|
|
68
79
|
|
|
69
|
-
-
|
|
70
|
-
- `
|
|
71
|
-
-
|
|
80
|
+
- `state='reviewing'`:steer 指示 agent 重调 `submit-review` 重挂审批(**携带上轮 selfReview 全文**供原样回传——该文档集的自审不重做,过门义务不豁免),并落 `resumeHint='resubmit'`(GUI 据此渲染「会话已重启,尚未重新提交」降级态;submit-review 重挂起时清空)。
|
|
81
|
+
- `state='revising'`:steer 指示继续按已注入的评论修订文档并重新提交。
|
|
82
|
+
- `state='dispatching'`:执行方式选择窗口中断恢复(D5 E3 新支)——先按 review_aborted 边落 `approved`(批准事实保留),再 steer 指示重调 `complete` 重新发起执行方式选择。
|
|
83
|
+
- 其余态(planning/approved/终态)不打扰。
|
|
84
|
+
- 旧 entry(无 state 字段)经 reviewState 映射后同样落本表。
|
|
72
85
|
|
|
73
86
|
## 排障
|
|
74
87
|
|
|
75
88
|
- **submit-review 返回「taiji host does not understand the plan review marker」**:taiji 宿主版本过旧(select 回显 payload 的确定性识别)。不要重挂(会同样回显循环),指引升级 taiji 或固定 plan 扩展版本。
|
|
76
|
-
- **submit-review 返回
|
|
89
|
+
- **submit-review 返回 no-self-review / stale-self-review**:自审硬门拒收(D9①)——前者补自审后带 selfReview 重调;后者是文档已变而自审照抄,必须对新版本重做自审。
|
|
90
|
+
- **submit-review 回传 decision 不在值域(unknown-decision)**:落 bad-response 出口、result 引导重挂(重挂即恢复);版本错配信号只在日志 warn 留痕——原子发版下生产不可达(唯一窗口 = dev-link 版本错开),生产出现 = 错配组合真实可达的反证,恢复动作 = dev-link 对齐版本后重试。
|
|
91
|
+
- **submit-review 返回 cancelled / review-interrupted**:审批选择框被解散——非批准,禁止实施,等用户指示;cancelled = plan mode 已退出(命令解散),review-interrupted = plan 模式保持在规划态(外部解散,可按用户要求重挂)。
|
|
77
92
|
- **重提交总带「no documents changed」警告**:必须先 rewrite 文件 + `register-doc`(version+1)再 submit-review,警告按 docs 指纹逐次检测。
|
|
78
93
|
- **plan-exec skill 没出现在执行选项里**:确认 frontmatter 有 `plan-exec: true`(严格布尔 true,字符串 "true" 不算);确认 skill 过了 pi 的 description 必填门且未被 settings overrides(`-`/`!`)禁用;untrusted 项目不扫 `.pi/skills` 与祖先链两族。
|
|
79
94
|
- **goal 跟踪未启动(no-steps / plan-unreadable 等)**:result 文本带逐原因恢复动作(如补 `## Implementation Steps` 节后重调 complete);goal 扩展未装时属正常降级(直接执行)。
|
|
@@ -81,6 +96,6 @@ headless(json/print 无 UI)默认 execute 不弹选择;通道失败(chan
|
|
|
81
96
|
|
|
82
97
|
## 备注
|
|
83
98
|
|
|
84
|
-
- **宿主信号**:taiji runtime 对托管 pi 恒注入 `TAIJI_AGENT_EXT_LOG=1
|
|
85
|
-
- **计划态工具白名单**:read / bash / grep / find / ls / plan
|
|
99
|
+
- **宿主信号**:taiji runtime 对托管 pi 恒注入 `TAIJI_AGENT_EXT_LOG=1`(扩展日志落盘开关 + marker 交互宿主分流判据);marker 交互另需 `mode==='rpc'`,env 泄漏到独立 pi TUI 时回落文本软门(不挂乱码 select)。
|
|
100
|
+
- **计划态工具白名单**:read / bash / grep / find / ls / plan / ask_user(D10:探索期提问可用;ask-user 扩展被禁时 pi 静默跳过,回退对话流提问)——bash 在白名单内,文件写约束来自注入的计划模式提示词(产物只写 plan 目录)。
|
|
86
101
|
- **无配置 schema 可编辑**:plan 的所有状态都由运行时命令/工具产生,没有可手动编辑的配置文件。
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// index.ts 只做装配:mock 掉子模块注册函数,只捕获 pi.on 的 hook、sessions 缓存与
|
|
5
|
+
// widget 刷新(session_tree handler 纯重建断言的观察面)
|
|
6
|
+
vi.mock("../tool.js", () => ({
|
|
7
|
+
registerPlanTool: vi.fn(
|
|
8
|
+
(_pi: unknown, planCtx: { states: Map<string, unknown> }) => {
|
|
9
|
+
captured.sessions = planCtx.states as Map<string, import("../state.js").PlanState>;
|
|
10
|
+
},
|
|
11
|
+
),
|
|
12
|
+
PLAN_MODE_TOOLS: ["read", "bash", "grep", "find", "ls", "plan"],
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../command.js", () => ({ registerPlanCommand: vi.fn() }));
|
|
15
|
+
vi.mock("../widget.js", () => ({ updatePlanWidget: vi.fn() }));
|
|
16
|
+
|
|
17
|
+
import planExtension from "../index.js";
|
|
18
|
+
import { updatePlanWidget } from "../widget.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_PLAN_STATE,
|
|
21
|
+
getPlanState,
|
|
22
|
+
type PlanSessionMap,
|
|
23
|
+
type PlanState,
|
|
24
|
+
reconstructPlanState,
|
|
25
|
+
} from "../state.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* U6b:reconstructPlanState 接活跃路径裁剪 + session_tree handler 即时重建。
|
|
29
|
+
* 被撤子树的 plan-state entry 不进重建态;handler 为纯重建体(无 steer / 写盘副作用)
|
|
30
|
+
* 且缓存同步更新(getPlanState 缓存命中 = 最新重建态)。
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const captured: { sessions?: PlanSessionMap } = {};
|
|
34
|
+
|
|
35
|
+
type Handler = (event: unknown, ctx: ExtensionContext) => Promise<unknown> | unknown;
|
|
36
|
+
|
|
37
|
+
function planStateEntry(id: string, parentId: string | null, data: Record<string, unknown>) {
|
|
38
|
+
return { type: "custom", id, parentId, timestamp: "2026-09-24T00:00:00.000Z", customType: "plan-state", data };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 撤回后真实形态:label entry 落文件尾(parentId = 回退后叶子) */
|
|
42
|
+
function labelEntry(id: string, parentId: string | null) {
|
|
43
|
+
return { type: "label", id, parentId, timestamp: "2026-09-24T00:00:01.000Z", targetId: "u-x", label: "taiji:revoked" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeCtx(entries: unknown[], leafId: string | null): ExtensionContext {
|
|
47
|
+
return {
|
|
48
|
+
sessionManager: {
|
|
49
|
+
getSessionId: () => "test-session",
|
|
50
|
+
getEntries: () => entries,
|
|
51
|
+
getLeafId: () => leafId,
|
|
52
|
+
},
|
|
53
|
+
} as unknown as ExtensionContext;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("reconstructPlanState 活跃路径裁剪(U6b)", () => {
|
|
57
|
+
it("有分支 fixture:被撤子树的 plan-state entry 不进重建态——逆序取活跃路径内最后一条", () => {
|
|
58
|
+
// 树:e1(base) → e2(active-branch);e1 → e3(revoked-branch,物理后写=被撤子树);
|
|
59
|
+
// 撤回落 label 锚 L(parentId = 回退后叶子 e2),leafId = L → 活跃路径 {L, e2, e1}
|
|
60
|
+
const entries = [
|
|
61
|
+
planStateEntry("e1", null, { isActive: true, planFilePath: "/p/base.md", requirement: "base", templateName: "" }),
|
|
62
|
+
planStateEntry("e2", "e1", { isActive: true, planFilePath: "/p/active.md", requirement: "active-branch", templateName: "" }),
|
|
63
|
+
planStateEntry("e3", "e1", { isActive: true, planFilePath: "/p/revoked.md", requirement: "revoked-branch", templateName: "" }),
|
|
64
|
+
labelEntry("L", "e2"),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const state = reconstructPlanState(makeCtx(entries, "L"));
|
|
68
|
+
|
|
69
|
+
// 逆序扫描在裁剪后 entries 内取第一条:e2(active-branch),非物理最后的 e3
|
|
70
|
+
expect(state.requirement).toBe("active-branch");
|
|
71
|
+
expect(state.planFilePath).toBe("/p/active.md");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("活跃路径内无 plan-state(plan 进入随子树整个被撤)→ 重建 DEFAULT,不回退到旧分支 entry", () => {
|
|
75
|
+
// 无裁剪的旧实现逆序全量扫描会命中 e2(被撤子树)——这正是撤回要封堵的注入面
|
|
76
|
+
const entries = [
|
|
77
|
+
{ type: "message", id: "e1", parentId: null, timestamp: "t", message: { role: "user", content: "hi" } },
|
|
78
|
+
planStateEntry("e2", "e1", { isActive: true, planFilePath: "/p/revoked.md", requirement: "revoked", templateName: "" }),
|
|
79
|
+
labelEntry("L", "e1"),
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const state = reconstructPlanState(makeCtx(entries, "L"));
|
|
83
|
+
|
|
84
|
+
expect(state.isActive).toBe(false);
|
|
85
|
+
expect(state.requirement).toBe("");
|
|
86
|
+
expect(state).toEqual({ ...DEFAULT_PLAN_STATE });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("无分支回归:leafId = 文件尾 → 逆序全量取最后一条(现行为不变)", () => {
|
|
90
|
+
const entries = [
|
|
91
|
+
planStateEntry("e1", null, { isActive: true, requirement: "first", templateName: "" }),
|
|
92
|
+
planStateEntry("e2", "e1", { isActive: false, requirement: "second-final", templateName: "" }),
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const state = reconstructPlanState(makeCtx(entries, "e2"));
|
|
96
|
+
|
|
97
|
+
expect(state.requirement).toBe("second-final");
|
|
98
|
+
expect(state.isActive).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("leafId 防御:null / 指向不存在 entry → 回退文件尾全量逆序(不静默清空重建态)", () => {
|
|
102
|
+
const entries = [
|
|
103
|
+
planStateEntry("e1", null, { isActive: true, requirement: "first", templateName: "" }),
|
|
104
|
+
planStateEntry("e2", "e1", { isActive: true, requirement: "tail-snapshot", templateName: "" }),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
for (const leafId of [null, "missing-id"]) {
|
|
108
|
+
const state = reconstructPlanState(makeCtx(entries, leafId));
|
|
109
|
+
expect(state.requirement).toBe("tail-snapshot");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("环状 parentId 防御(损坏文件 / 非 pi 产出写入)", () => {
|
|
115
|
+
it("环状 parentId(a↔b,leafId 指向环内)→ 回溯终止不挂死,重建完成且不抛错", () => {
|
|
116
|
+
// 环:a.parentId=b、b.parentId=a;leafId=a → 回溯 a→b→(二次命中 a) 守卫终止
|
|
117
|
+
const entries = [
|
|
118
|
+
planStateEntry("a", "b", { isActive: true, planFilePath: "/p/cycle-a.md", requirement: "cycle-a", templateName: "" }),
|
|
119
|
+
planStateEntry("b", "a", { isActive: true, planFilePath: "/p/cycle-b.md", requirement: "cycle-b", templateName: "" }),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const state = reconstructPlanState(makeCtx(entries, "a"));
|
|
123
|
+
|
|
124
|
+
// 终止形态(实现语义):activeIds = {a, b},逆序扫描命中回溯链上最后一条 plan-state b
|
|
125
|
+
expect(state.requirement).toBe("cycle-b");
|
|
126
|
+
expect(state.planFilePath).toBe("/p/cycle-b.md");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("session_tree handler 即时重建(U6b:纯重建体 + 缓存同步)", () => {
|
|
131
|
+
type PiMock = ExtensionAPI & {
|
|
132
|
+
sendMessage: ReturnType<typeof vi.fn>;
|
|
133
|
+
appendEntry: ReturnType<typeof vi.fn>;
|
|
134
|
+
setActiveTools: ReturnType<typeof vi.fn>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
function setup(): { handlers: Map<string, Handler>; pi: PiMock } {
|
|
138
|
+
const handlers = new Map<string, Handler>();
|
|
139
|
+
const pi = {
|
|
140
|
+
on: vi.fn((event: string, handler: Handler) => {
|
|
141
|
+
handlers.set(event, handler);
|
|
142
|
+
}),
|
|
143
|
+
sendMessage: vi.fn(),
|
|
144
|
+
setActiveTools: vi.fn(),
|
|
145
|
+
appendEntry: vi.fn(),
|
|
146
|
+
} as unknown as PiMock;
|
|
147
|
+
planExtension(pi);
|
|
148
|
+
return { handlers, pi };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const branchedEntries = [
|
|
152
|
+
planStateEntry("e1", null, { isActive: true, planFilePath: "/p/base.md", requirement: "base", templateName: "" }),
|
|
153
|
+
planStateEntry("e2", "e1", { isActive: true, planFilePath: "/p/active.md", requirement: "active-branch", templateName: "" }),
|
|
154
|
+
planStateEntry("e3", "e1", { isActive: true, planFilePath: "/p/revoked.md", requirement: "revoked-branch", templateName: "" }),
|
|
155
|
+
labelEntry("L", "e2"),
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
vi.clearAllMocks();
|
|
160
|
+
captured.sessions = undefined;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("纯重建:无 sendMessage / appendEntry / setActiveTools 副作用,只重建内存态 + widget 刷新", async () => {
|
|
164
|
+
const { handlers, pi } = setup();
|
|
165
|
+
// 预填 stale 缓存:模拟撤回前已缓存的旧分支态(getPlanState 缓存命中短路的 stale 窗口)
|
|
166
|
+
captured.sessions!.set("test-session", {
|
|
167
|
+
...DEFAULT_PLAN_STATE,
|
|
168
|
+
isActive: true,
|
|
169
|
+
requirement: "stale-revoked",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await handlers.get("session_tree")!({ type: "session_tree" }, makeCtx(branchedEntries, "L"));
|
|
173
|
+
|
|
174
|
+
// 纯重建体:绝不复刻 session_start 块的 E3 steer / persist / 工具限制副作用
|
|
175
|
+
// (本事件由撤回编排触发——steer 会让撤回编排自己注入消息,persist 会污染新分支)
|
|
176
|
+
expect(pi.sendMessage).not.toHaveBeenCalled();
|
|
177
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
178
|
+
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
179
|
+
// widget 刷新(显示面与重建态一致)
|
|
180
|
+
expect(updatePlanWidget).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("缓存同步:sessions 缓存更新为重建态——getPlanState 缓存命中返回活跃路径态而非 stale", async () => {
|
|
184
|
+
const { handlers } = setup();
|
|
185
|
+
captured.sessions!.set("test-session", {
|
|
186
|
+
...DEFAULT_PLAN_STATE,
|
|
187
|
+
isActive: true,
|
|
188
|
+
requirement: "stale-revoked",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await handlers.get("session_tree")!({ type: "session_tree" }, makeCtx(branchedEntries, "L"));
|
|
192
|
+
|
|
193
|
+
// 「缓存命中 = 最新重建态」:stale 窗口消除(压缩摘要注入面读 getPlanState 不再拿到被撤 requirement)
|
|
194
|
+
const cached = captured.sessions!.get("test-session");
|
|
195
|
+
expect(cached?.requirement).toBe("active-branch");
|
|
196
|
+
expect(cached?.requirement).not.toBe("stale-revoked");
|
|
197
|
+
|
|
198
|
+
// 缓存命中路径直接返回重建态对象(不再触发 reconstruct)
|
|
199
|
+
const ctx = makeCtx(branchedEntries, "L");
|
|
200
|
+
const hit = getPlanState(captured.sessions!, "test-session", ctx);
|
|
201
|
+
expect(hit).toBe(cached);
|
|
202
|
+
expect(hit.requirement).toBe("active-branch");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -24,13 +24,14 @@ import * as path from "node:path";
|
|
|
24
24
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
25
|
|
|
26
26
|
import { parsePlanArgs, registerPlanCommand, resolveTemplateFile } from "../command.js";
|
|
27
|
-
import {
|
|
27
|
+
import type { PlanAbortControllers } from "../state.js";
|
|
28
|
+
import { createPlanCtx, freshPendingSelect, MAX_PLAN_REQUIREMENT_LENGTH, PLAN_CONTEXT_CUSTOM_TYPE } from "../state.js";
|
|
28
29
|
|
|
29
30
|
const ALL_TOOL_NAMES = ["read", "bash", "grep", "find", "ls", "plan", "write", "edit"];
|
|
30
31
|
|
|
31
32
|
function createMocks() {
|
|
32
33
|
let capturedHandler: (args: string, ctx: ExtensionContext) => Promise<void>;
|
|
33
|
-
const
|
|
34
|
+
const planCtx = createPlanCtx();
|
|
34
35
|
|
|
35
36
|
const pi = {
|
|
36
37
|
registerCommand: vi.fn((_name: string, def: { handler: (args: string, ctx: ExtensionContext) => Promise<void> }) => {
|
|
@@ -47,6 +48,7 @@ function createMocks() {
|
|
|
47
48
|
cwd: "/tmp/test-project",
|
|
48
49
|
sessionManager: {
|
|
49
50
|
getSessionId: () => "test-session",
|
|
51
|
+
getLeafId: () => null,
|
|
50
52
|
getEntries: () => [] as unknown[],
|
|
51
53
|
},
|
|
52
54
|
ui: {
|
|
@@ -60,7 +62,8 @@ function createMocks() {
|
|
|
60
62
|
return {
|
|
61
63
|
pi,
|
|
62
64
|
ctx,
|
|
63
|
-
|
|
65
|
+
planCtx,
|
|
66
|
+
controllers: planCtx.controllers,
|
|
64
67
|
getHandler: () => capturedHandler!,
|
|
65
68
|
};
|
|
66
69
|
}
|
|
@@ -74,7 +77,7 @@ describe("registerPlanCommand", () => {
|
|
|
74
77
|
let pi: ExtensionAPI;
|
|
75
78
|
let ctx: ExtensionContext;
|
|
76
79
|
let handler: (args: string, ctx: ExtensionContext) => Promise<void>;
|
|
77
|
-
let controllers:
|
|
80
|
+
let controllers: PlanAbortControllers;
|
|
78
81
|
|
|
79
82
|
beforeEach(() => {
|
|
80
83
|
vi.clearAllMocks();
|
|
@@ -82,8 +85,7 @@ describe("registerPlanCommand", () => {
|
|
|
82
85
|
pi = mocks.pi;
|
|
83
86
|
ctx = mocks.ctx;
|
|
84
87
|
controllers = mocks.controllers;
|
|
85
|
-
|
|
86
|
-
registerPlanCommand(pi, sessions, controllers);
|
|
88
|
+
registerPlanCommand(pi, mocks.planCtx);
|
|
87
89
|
handler = mocks.getHandler();
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -93,38 +95,54 @@ describe("registerPlanCommand", () => {
|
|
|
93
95
|
|
|
94
96
|
// --- abort subcommand ---
|
|
95
97
|
|
|
96
|
-
it("abort
|
|
98
|
+
it("abort(idle 常态格): warn 纠偏文案 + 幂等退出——不落盘、工具集幂等恢复(D-B1-1 单入口守卫极性,原 info no-op 取消)", async () => {
|
|
97
99
|
await handler("abort", ctx);
|
|
98
|
-
expect((ctx as ReturnType<typeof createMocks>["ctx"]).ui.notify).toHaveBeenCalledWith("No active plan mode.", "
|
|
99
|
-
|
|
100
|
+
expect((ctx as ReturnType<typeof createMocks>["ctx"]).ui.notify).toHaveBeenCalledWith("No active plan mode.", "warning");
|
|
101
|
+
// ④ 工具集照常幂等恢复(idle 格已是全量,重设全量 = 可观察零变化)
|
|
102
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(ALL_TOOL_NAMES);
|
|
103
|
+
// ② 跳过:idle 删 exit 边后 ok:false 不落盘(无噪音 exited entry)
|
|
104
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
100
105
|
});
|
|
101
106
|
|
|
102
|
-
it("abort
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
it("abort(终态格 isActive=false): warn 纠偏指令文案 + 不落盘(V1 ⑤ 幂等退出锚)", async () => {
|
|
108
|
+
// 注入终态格:上一轮已 complete,缓存重建自 entries(空)→ 直接预置 sessions 难以
|
|
109
|
+
// 穿透 handler 闭包,改走 entries 注入 completed 残留形态
|
|
110
|
+
const terminalCtx = {
|
|
111
|
+
...ctx,
|
|
112
|
+
sessionManager: {
|
|
113
|
+
getSessionId: () => "test-session",
|
|
114
|
+
getEntries: () => [
|
|
115
|
+
{
|
|
116
|
+
type: "custom",
|
|
117
|
+
customType: "plan-state",
|
|
118
|
+
data: { isActive: false, state: "completed", planFilePath: "", requirement: "", templateName: "" },
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
} as unknown as ExtensionContext;
|
|
123
|
+
await handler("abort", terminalCtx);
|
|
124
|
+
expect(terminalCtx.ui.notify).toHaveBeenCalledWith(
|
|
125
|
+
expect.stringContaining("Plan mode is not active"),
|
|
126
|
+
"warning",
|
|
127
|
+
);
|
|
128
|
+
// completed 终态记录不被 exited 覆写(ok:false 跳过状态值写入,isActive=false 非坏格)
|
|
129
|
+
expect(pi.appendEntry).not.toHaveBeenCalled();
|
|
113
130
|
});
|
|
114
131
|
|
|
115
|
-
it("abort 联动(E10):controller.abort() 先于 resetPlanState 的 entry
|
|
132
|
+
it("abort 联动(E10):controller.abort() 先于 resetPlanState 的 entry 落盘,markDissolved('self') 先行打标", async () => {
|
|
116
133
|
await handler("implement user auth", ctx);
|
|
117
134
|
vi.clearAllMocks();
|
|
118
135
|
|
|
119
|
-
// 模拟一个挂起 select 的
|
|
120
|
-
const
|
|
121
|
-
const abortSpy = vi.spyOn(controller, "abort");
|
|
122
|
-
controllers.set("test-session", controller);
|
|
136
|
+
// 模拟一个挂起 select 的 PendingSelect(submit-review / complete 执行方式两处共用注册表)
|
|
137
|
+
const pending = freshPendingSelect(controllers, "test-session");
|
|
138
|
+
const abortSpy = vi.spyOn(pending.controller, "abort");
|
|
123
139
|
|
|
124
140
|
await handler("abort", ctx);
|
|
125
141
|
|
|
126
|
-
// 挂起 select 被 abort(→ resolve undefined → tool execute
|
|
127
|
-
expect(controller.signal.aborted).toBe(true);
|
|
142
|
+
// 挂起 select 被 abort(→ resolve undefined → tool execute 归口分支 → turn 结束 → settled → busy defer 恢复)
|
|
143
|
+
expect(pending.controller.signal.aborted).toBe(true);
|
|
144
|
+
// D-B1-2:入口解散先打 'self' 标(同步先于 abort,等待处归口按来源直传判别)
|
|
145
|
+
expect(pending.dissolvedBy).toBe("self");
|
|
128
146
|
// 顺序断言:abort 必须先于 reset entry 落盘(反序 = 挂起 select 无人 resolve → session 卡死)
|
|
129
147
|
const resetCallIndex = (pi.appendEntry as ReturnType<typeof vi.fn>).mock.calls.findIndex(
|
|
130
148
|
(c) => (c[1] as { isActive?: boolean }).isActive === false,
|
|
@@ -168,7 +186,7 @@ describe("registerPlanCommand", () => {
|
|
|
168
186
|
"/tmp/test-project/.tmp/plans/implement-user-auth",
|
|
169
187
|
{ recursive: true },
|
|
170
188
|
);
|
|
171
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
189
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan", "ask_user"]);
|
|
172
190
|
expect(pi.sendMessage).toHaveBeenCalledWith(...planContextMessage(expect.stringContaining("[PLAN MODE]")));
|
|
173
191
|
expect(pi.sendMessage).toHaveBeenCalledWith(...planContextMessage(expect.stringContaining("Implement User Auth")));
|
|
174
192
|
expect(pi.appendEntry).toHaveBeenCalledWith("plan-state", {
|
|
@@ -176,9 +194,13 @@ describe("registerPlanCommand", () => {
|
|
|
176
194
|
planFilePath: "/tmp/test-project/.tmp/plans/implement-user-auth/plan.md",
|
|
177
195
|
requirement: "Implement User Auth",
|
|
178
196
|
templateName: "",
|
|
197
|
+
templateProvided: undefined,
|
|
179
198
|
skills: [],
|
|
180
199
|
docs: [],
|
|
181
|
-
|
|
200
|
+
state: "planning",
|
|
201
|
+
selfReview: undefined,
|
|
202
|
+
resumeHint: undefined,
|
|
203
|
+
lastSubmitReviewDocsFingerprint: undefined,
|
|
182
204
|
});
|
|
183
205
|
});
|
|
184
206
|
|
|
@@ -221,6 +243,7 @@ describe("registerPlanCommand", () => {
|
|
|
221
243
|
...ctx,
|
|
222
244
|
sessionManager: {
|
|
223
245
|
getSessionId: () => "test-session",
|
|
246
|
+
getLeafId: () => null,
|
|
224
247
|
getEntries: () => [
|
|
225
248
|
{
|
|
226
249
|
type: "custom",
|
|
@@ -327,11 +350,11 @@ describe("registerPlanCommand", () => {
|
|
|
327
350
|
isActive: true,
|
|
328
351
|
requirement: "retro meeting",
|
|
329
352
|
templateName: "retro-template",
|
|
330
|
-
|
|
353
|
+
templateProvided: true,
|
|
331
354
|
skills: [],
|
|
332
355
|
}),
|
|
333
356
|
);
|
|
334
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan"]);
|
|
357
|
+
expect(pi.setActiveTools).toHaveBeenCalledWith(["read", "bash", "grep", "find", "ls", "plan", "ask_user"]);
|
|
335
358
|
const prompt = sentMessage();
|
|
336
359
|
// 直传声明 + 全文内嵌(文件内容直达模型,不赌自发 read——D5)
|
|
337
360
|
expect(prompt).toContain("template was provided via --template");
|
|
@@ -349,7 +372,7 @@ describe("registerPlanCommand", () => {
|
|
|
349
372
|
|
|
350
373
|
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
351
374
|
"plan-state",
|
|
352
|
-
expect.objectContaining({ isActive: true, templateName: "retro template",
|
|
375
|
+
expect.objectContaining({ isActive: true, templateName: "retro template", templateProvided: true }),
|
|
353
376
|
);
|
|
354
377
|
const prompt = sentMessage();
|
|
355
378
|
expect(prompt).toContain(spacedAbs);
|