@zhushanwen/pi-plan 0.5.2 → 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 +6 -4
- 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 +663 -255
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* plan/
|
|
2
|
+
* plan/execution-notice.ts — buildPlanSuccessCriteria 数组形态测试
|
|
3
3
|
*
|
|
4
4
|
* 形态契约(U25):1 条总述 `All N steps of <basename> executed and verified`
|
|
5
5
|
* + 前 3 条 step preview(编号前缀、单条截断 ≤80 chars),合计 ≤4 条
|
|
@@ -11,7 +11,7 @@ vi.mock("node:fs", () => ({
|
|
|
11
11
|
readFileSync: vi.fn(),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
|
-
import { buildPlanSuccessCriteria, handlePlanComplete } from "../
|
|
14
|
+
import { buildPlanSuccessCriteria, handlePlanComplete } from "../execution-notice.js";
|
|
15
15
|
import { PLAN_CONTEXT_CUSTOM_TYPE } from "../state.js";
|
|
16
16
|
|
|
17
17
|
const fsMock = vi.mocked(await import("node:fs"));
|
|
@@ -25,25 +25,18 @@ function makePi() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function makeCtx() {
|
|
28
|
-
const onCompleteFns: Array<() => void> = [];
|
|
29
|
-
const onErrorFns: Array<(e: Error) => void> = [];
|
|
30
|
-
|
|
31
28
|
return {
|
|
32
|
-
sessionManager: { getSessionId: () => "test-session", getEntries: () => [] as unknown[] },
|
|
29
|
+
sessionManager: { getSessionId: () => "test-session", getLeafId: () => null, getEntries: () => [] as unknown[] },
|
|
33
30
|
ui: { notify: vi.fn() },
|
|
34
|
-
compact: vi.fn((opts: { onComplete?: () => void; onError?: (e: Error) => void }) => {
|
|
35
|
-
if (opts.onComplete) onCompleteFns.push(opts.onComplete);
|
|
36
|
-
if (opts.onError) onErrorFns.push(opts.onError);
|
|
37
|
-
}),
|
|
38
|
-
_onCompleteFns: onCompleteFns,
|
|
39
|
-
_onErrorFns: onErrorFns,
|
|
40
31
|
};
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
function makeActiveState() {
|
|
44
35
|
return {
|
|
45
36
|
isActive: true,
|
|
46
|
-
|
|
37
|
+
// 生产真实形态(enter.ts:<project>/.tmp/plans/<slug>/plan.md)——buildPlanSlug 取
|
|
38
|
+
// 目录名(requirement slug 段),合成单层路径会让 slug 派生在测试数据上失真
|
|
39
|
+
planFilePath: "/tmp/test-project/.tmp/plans/login-page/plan.md",
|
|
47
40
|
requirement: "Add login page",
|
|
48
41
|
templateName: "default",
|
|
49
42
|
};
|
|
@@ -66,46 +59,37 @@ function expectSingleLineArray(value: unknown): string[] {
|
|
|
66
59
|
// --- buildPlanSuccessCriteria 单元 ---
|
|
67
60
|
|
|
68
61
|
describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
|
|
62
|
+
// 生产真实形态(enter.ts:<project>/.tmp/plans/<slug>/plan.md)——总述身份取目录名
|
|
63
|
+
const PLAN_PATH = "/tmp/test-project/.tmp/plans/login-page/plan.md";
|
|
64
|
+
|
|
69
65
|
it("5 步 plan → 4 条:总述 + 前 3 条 preview", () => {
|
|
70
66
|
const steps = ["Alpha", "Bravo", "Charlie", "Delta", "Echo"];
|
|
71
|
-
const items = expectSingleLineArray(buildPlanSuccessCriteria(
|
|
67
|
+
const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, steps));
|
|
72
68
|
|
|
73
69
|
expect(items).toHaveLength(4);
|
|
74
|
-
expect(items[0]).toBe("All 5 steps of
|
|
70
|
+
expect(items[0]).toBe("All 5 steps of login-page executed and verified");
|
|
75
71
|
expect(items.slice(1)).toEqual(["1. Alpha", "2. Bravo", "3. Charlie"]);
|
|
76
72
|
});
|
|
77
73
|
|
|
78
|
-
it("1 步 plan → 2 条", () => {
|
|
79
|
-
const items = expectSingleLineArray(buildPlanSuccessCriteria("/tmp/plan.md", ["Only step"]));
|
|
80
|
-
|
|
81
|
-
expect(items).toHaveLength(2);
|
|
82
|
-
expect(items[0]).toBe("All 1 steps of plan executed and verified");
|
|
83
|
-
expect(items[1]).toBe("1. Only step");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
74
|
it("0 步 → 仅总述 1 条", () => {
|
|
87
|
-
const items = expectSingleLineArray(buildPlanSuccessCriteria(
|
|
75
|
+
const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, []));
|
|
88
76
|
|
|
89
|
-
expect(items).toEqual(["All 0 steps of
|
|
77
|
+
expect(items).toEqual(["All 0 steps of login-page executed and verified"]);
|
|
90
78
|
});
|
|
91
79
|
|
|
92
|
-
it("
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(items).toHaveLength(4);
|
|
97
|
-
expect(items[0]).toBe("All 12 steps of plan executed and verified");
|
|
98
|
-
expect(items.slice(1)).toEqual(["1. S1", "2. S2", "3. S3"]);
|
|
80
|
+
it("目录名混合大小写 → kebab 链小写折叠(非生产形态防御)", () => {
|
|
81
|
+
const items = buildPlanSuccessCriteria("/work/FeatLogin/plan.md", ["S1"]);
|
|
82
|
+
expect(items[0]).toBe("All 1 steps of featlogin executed and verified");
|
|
99
83
|
});
|
|
100
84
|
|
|
101
|
-
it("
|
|
102
|
-
const items = buildPlanSuccessCriteria("/
|
|
103
|
-
expect(items[0]).toBe("All 1 steps of
|
|
85
|
+
it("非生产形态裸文件路径 → 提取为空回兜底名", () => {
|
|
86
|
+
const items = buildPlanSuccessCriteria("/plan-x.md", ["S1"]);
|
|
87
|
+
expect(items[0]).toBe("All 1 steps of plan-execution executed and verified");
|
|
104
88
|
});
|
|
105
89
|
|
|
106
90
|
it("超长 step → 截断至 ≤80 chars 且以 ... 结尾,保留编号前缀", () => {
|
|
107
91
|
const long = "x".repeat(120);
|
|
108
|
-
const items = expectSingleLineArray(buildPlanSuccessCriteria(
|
|
92
|
+
const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, [long]));
|
|
109
93
|
|
|
110
94
|
expect(items[1]).toHaveLength(80);
|
|
111
95
|
expect(items[1].endsWith("...")).toBe(true);
|
|
@@ -114,7 +98,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
|
|
|
114
98
|
|
|
115
99
|
it("恰好 80 chars 的条目 → 不截断、不加省略号", () => {
|
|
116
100
|
const exact = "y".repeat(77); // "1. " 前缀 + 77 = 80
|
|
117
|
-
const items = buildPlanSuccessCriteria(
|
|
101
|
+
const items = buildPlanSuccessCriteria(PLAN_PATH, [exact]);
|
|
118
102
|
|
|
119
103
|
expect(items[1]).toBe(`1. ${exact}`);
|
|
120
104
|
expect(items[1]).toHaveLength(80);
|
|
@@ -122,7 +106,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
|
|
|
122
106
|
});
|
|
123
107
|
|
|
124
108
|
it("step 文本含换行符 → 折叠为单行空格分隔(goal handler 拒 \r\n)", () => {
|
|
125
|
-
const items = expectSingleLineArray(buildPlanSuccessCriteria(
|
|
109
|
+
const items = expectSingleLineArray(buildPlanSuccessCriteria(PLAN_PATH, ["line1\nline2\r\nline3"]));
|
|
126
110
|
|
|
127
111
|
expect(items[1]).toBe("1. line1 line2 line3");
|
|
128
112
|
});
|
|
@@ -131,7 +115,7 @@ describe("buildPlanSuccessCriteria — 1 总述 + 前 3 条 preview", () => {
|
|
|
131
115
|
// --- handlePlanComplete → tryGoalInit 端到端 ---
|
|
132
116
|
|
|
133
117
|
/**
|
|
134
|
-
* goal 桥 slot key——与
|
|
118
|
+
* goal 桥 slot key——与 execution-notice.ts / goal 侧 index.ts 的字符串一致(本地声明,
|
|
135
119
|
* 不 import 对方包:pi-goal 是 optional peer)。mock 挂 slot 与真实通道同构。
|
|
136
120
|
*/
|
|
137
121
|
const GOAL_INIT_SLOT_KEY = Symbol.for("@zhushanwen/pi-goal.goalInit");
|
|
@@ -158,51 +142,26 @@ describe("handlePlanComplete — goalInit slot 第 5 参数为新形态 string[]
|
|
|
158
142
|
return expectSingleLineArray(goalInitMock.mock.calls[0][4]);
|
|
159
143
|
}
|
|
160
144
|
|
|
161
|
-
it("
|
|
145
|
+
it("直接投递:3 步 plan → 总述 + 3 条 preview", () => {
|
|
162
146
|
fsMock.readFileSync.mockReturnValue(makePlanContent(["Step A", "Step B", "Step C"]));
|
|
163
147
|
|
|
164
|
-
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "
|
|
148
|
+
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
|
|
165
149
|
|
|
166
150
|
expect(getCriteriaArg()).toEqual([
|
|
167
|
-
"All 3 steps of
|
|
151
|
+
"All 3 steps of login-page executed and verified",
|
|
168
152
|
"1. Step A",
|
|
169
153
|
"2. Step B",
|
|
170
154
|
"3. Step C",
|
|
171
155
|
]);
|
|
172
156
|
});
|
|
173
157
|
|
|
174
|
-
it("compact isolation: onComplete 后 goalInit 收到同形态数组", () => {
|
|
175
|
-
fsMock.readFileSync.mockReturnValue(makePlanContent(["Step one", "Step two"]));
|
|
176
|
-
|
|
177
|
-
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "compact", "execute");
|
|
178
|
-
ctx._onCompleteFns[0]();
|
|
179
|
-
|
|
180
|
-
expect(getCriteriaArg()).toEqual([
|
|
181
|
-
"All 2 steps of plan executed and verified",
|
|
182
|
-
"1. Step one",
|
|
183
|
-
"2. Step two",
|
|
184
|
-
]);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("12 步 plan → 数组长度固定 4(1 总述 + 3 preview),不再按 8 条上限截断", () => {
|
|
188
|
-
const steps = Array.from({ length: 12 }, (_, i) => `Step ${i + 1}`);
|
|
189
|
-
fsMock.readFileSync.mockReturnValue(makePlanContent(steps));
|
|
190
|
-
|
|
191
|
-
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "direct", "execute");
|
|
192
|
-
|
|
193
|
-
const items = getCriteriaArg();
|
|
194
|
-
expect(items).toHaveLength(4);
|
|
195
|
-
expect(items[0]).toContain("12 steps");
|
|
196
|
-
expect(items[0]).toContain("plan");
|
|
197
|
-
});
|
|
198
|
-
|
|
199
158
|
it("CRLF plan 文件 → 每条 criteria 仍单行不含 \\r \\n", () => {
|
|
200
159
|
fsMock.readFileSync.mockReturnValue("## 实现步骤\r\n1. Step one\r\n2. Step two\r\n3. Step three");
|
|
201
160
|
|
|
202
|
-
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "
|
|
161
|
+
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
|
|
203
162
|
|
|
204
163
|
expect(getCriteriaArg()).toEqual([
|
|
205
|
-
"All 3 steps of
|
|
164
|
+
"All 3 steps of login-page executed and verified",
|
|
206
165
|
"1. Step one",
|
|
207
166
|
"2. Step two",
|
|
208
167
|
"3. Step three",
|
|
@@ -212,7 +171,7 @@ describe("handlePlanComplete — goalInit slot 第 5 参数为新形态 string[]
|
|
|
212
171
|
it("0 步 plan → tryGoalInit 提前退出,goalInit 不被调用", () => {
|
|
213
172
|
(fsMock.readFileSync as ReturnType<typeof vi.fn>).mockReturnValue("## Overview\nNo numbered steps here.");
|
|
214
173
|
|
|
215
|
-
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "
|
|
174
|
+
handlePlanComplete(pi as never, ctx as never, makeActiveState(), "execute");
|
|
216
175
|
|
|
217
176
|
expect(goalInitMock).not.toHaveBeenCalled();
|
|
218
177
|
// steer 仍发出(执行流程不因 goal 缺席中断)——custom message 三要素 + steer options(A6)
|
|
@@ -7,12 +7,11 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
|
|
8
8
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
9
9
|
|
|
10
|
-
import { extractPlanSteps } from "../
|
|
10
|
+
import { extractPlanSteps } from "../execution-notice.js";
|
|
11
11
|
import {
|
|
12
12
|
formatAvailablePlans,
|
|
13
13
|
getBuiltinTemplateDir,
|
|
14
14
|
listTemplates,
|
|
15
|
-
loadTemplate,
|
|
16
15
|
} from "../templates.js";
|
|
17
16
|
|
|
18
17
|
const BUILTIN_TEMPLATE_NAMES = [
|
|
@@ -46,7 +45,7 @@ function absentDir(label: string): string {
|
|
|
46
45
|
return join(tmpdir(), `plan-absent-${label}-${randomUUID()}`);
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
/**
|
|
48
|
+
/** 发现视图用隔离源(用户级缺位 → 只剩内置源,不受运行机真实 ~/.agents/plans 干扰) */
|
|
50
49
|
function isolatedSources(): { userPlansDir: string } {
|
|
51
50
|
return { userPlansDir: absentDir("user") };
|
|
52
51
|
}
|
|
@@ -170,24 +169,6 @@ describe("空发现 warn(bundle 布局事故信号)", () => {
|
|
|
170
169
|
});
|
|
171
170
|
});
|
|
172
171
|
|
|
173
|
-
describe("loadTemplate(发现视图的读取侧)", () => {
|
|
174
|
-
it("returns content for existing builtin template(隔离用户源)", () => {
|
|
175
|
-
const content = loadTemplate("feature-plan", isolatedSources());
|
|
176
|
-
expect(content).not.toBeNull();
|
|
177
|
-
expect(content).toContain("## ");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("returns null for non-existent template", () => {
|
|
181
|
-
expect(loadTemplate("non-existent-template", isolatedSources())).toBeNull();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("从合并视图解析:同名时用户级胜者内容被加载", () => {
|
|
185
|
-
const userDir = mkTmpDir("plan-lt-user-");
|
|
186
|
-
writeTemplate(userDir, "feature-plan.md", "# user-owned skeleton\n");
|
|
187
|
-
expect(loadTemplate("feature-plan", { userPlansDir: userDir })).toBe("# user-owned skeleton\n");
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
172
|
describe("<available-plans> 段拼装纯函数(D6)", () => {
|
|
192
173
|
it("空清单返回空串(不注入段)", () => {
|
|
193
174
|
expect(formatAvailablePlans([])).toBe("");
|
|
@@ -233,12 +214,14 @@ describe("template ↔ extractPlanSteps alignment guard (D4)", () => {
|
|
|
233
214
|
// ① 模板标题改名 → 恰一节断言失败;
|
|
234
215
|
// ② 解析正则与标题脱节 → 提取退化到 fallback 收进其他节的噪音项 → toEqual 失败。
|
|
235
216
|
it.each(BUILTIN_TEMPLATE_NAMES)("template '%s' has exactly one '## Implementation Steps' section that extractPlanSteps consumes", (name) => {
|
|
236
|
-
|
|
237
|
-
|
|
217
|
+
// 胜者 path 直读(读取侧已随 select-template 单次扫描复用收口——不另有按名读取 API)
|
|
218
|
+
const winner = listTemplates(isolatedSources()).find((t) => t.name === name);
|
|
219
|
+
expect(winner).toBeDefined();
|
|
220
|
+
const content = winner ? fs.readFileSync(winner.path, "utf-8") : "";
|
|
238
221
|
|
|
239
|
-
expect(content
|
|
222
|
+
expect(content.match(/^## Implementation Steps$/gm)).toHaveLength(1);
|
|
240
223
|
|
|
241
|
-
const plan = content
|
|
224
|
+
const plan = content
|
|
242
225
|
// 在第一个非步骤节标题后放噪音编号项(模拟 Requirements 等节含编号列表)
|
|
243
226
|
.replace(/^(## (?!Implementation Steps).+)$/m, "$1\n1. noise-from-other-section")
|
|
244
227
|
// 在步骤节标题后填编号步骤(模拟 AI 按模板写 plan.md)
|