@zhushanwen/pi-plan 1.0.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-plan",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Lightweight plan mode for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -21,11 +21,11 @@
21
21
  ],
22
22
  "license": "MIT",
23
23
  "peerDependencies": {
24
- "@earendil-works/pi-coding-agent": "^0.84.4",
25
- "@earendil-works/pi-tui": "^0.84.4",
26
- "typebox": "*",
27
- "@earendil-works/pi-ai": "^0.84.4",
28
- "@zhushanwen/pi-goal": "0.14.10"
24
+ "@earendil-works/pi-coding-agent": "^1.0.0",
25
+ "@earendil-works/pi-tui": "^1.0.0",
26
+ "typebox": "1.3.27",
27
+ "@earendil-works/pi-ai": "^1.0.0",
28
+ "@zhushanwen/pi-goal": "0.14.11"
29
29
  },
30
30
  "peerDependenciesMeta": {
31
31
  "@earendil-works/pi-tui": {
@@ -39,7 +39,7 @@
39
39
  }
40
40
  },
41
41
  "devDependencies": {
42
- "@earendil-works/pi-tui": "^0.84.4",
42
+ "@earendil-works/pi-tui": "^1.0.0",
43
43
  "@vitest/coverage-v8": "^4.1.9",
44
44
  "vitest": "^4.1.8"
45
45
  },
@@ -50,11 +50,11 @@
50
50
  "templates/"
51
51
  ],
52
52
  "dependencies": {
53
- "@zhushanwen/extension-protocol": "0.17.0",
54
- "@zhushanwen/pi-exec-skills": "0.2.0",
55
- "@zhushanwen/pi-ext-guards": "0.4.2",
56
- "@zhushanwen/pi-extension-logger": "0.6.1",
57
- "@zhushanwen/pi-llm-shared": "0.11.0",
53
+ "@zhushanwen/extension-protocol": "0.18.0",
54
+ "@zhushanwen/pi-exec-skills": "0.2.1",
55
+ "@zhushanwen/pi-ext-guards": "0.4.3",
56
+ "@zhushanwen/pi-extension-logger": "0.6.2",
57
+ "@zhushanwen/pi-llm-shared": "0.11.1",
58
58
  "@zhushanwen/pi-session-path": "0.1.0"
59
59
  },
60
60
  "scripts": {
@@ -295,6 +295,53 @@ describe("registerPlanCommand", () => {
295
295
  return payload.content;
296
296
  }
297
297
 
298
+ // --- D10②:命令反馈 notify(display:false 注入仅 LLM 可见,用户可见性由 notify 承担) ---
299
+
300
+ it("重入分支(存在既有 plan 文件):notify 提示 + 选项清单仍走 display:false 注入", async () => {
301
+ vi.mocked(fs.readdirSync).mockReturnValue(["old-plan"] as unknown as ReturnType<typeof fs.readdirSync>);
302
+ vi.mocked(fs.statSync).mockImplementation(() => ({ isDirectory: () => true }) as unknown as fs.Stats);
303
+ vi.mocked(fs.existsSync).mockImplementation((p) => String(p).endsWith("/.tmp/plans/old-plan/plan.md"));
304
+ try {
305
+ await handler("", ctx);
306
+ } finally {
307
+ vi.mocked(fs.existsSync).mockImplementation(() => false);
308
+ }
309
+
310
+ const notify = (ctx as unknown as { ui: { notify: ReturnType<typeof vi.fn> } }).ui.notify;
311
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("1 existing plan file(s)"), "info");
312
+ // LLM 注入通路不变:custom message display:false + 选项清单全文
313
+ const message = sentMessage();
314
+ expect(message).toContain("[PLAN MODE] Found existing plan files:");
315
+ const calls = (pi.sendMessage as ReturnType<typeof vi.fn>).mock.calls;
316
+ const payload = calls[0][0] as { display: boolean };
317
+ expect(payload.display).toBe(false);
318
+ });
319
+
320
+ it("--skills 未知技能 fail-fast:notify error + 注入通路保留", async () => {
321
+ await handler("复盘 --skills nope", ctx);
322
+
323
+ const notify = (ctx as unknown as { ui: { notify: ReturnType<typeof vi.fn> } }).ui.notify;
324
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("unknown skill(s): nope"), "error");
325
+ expect(sentMessage()).toContain("Failed to enter: unknown skill(s): nope");
326
+ });
327
+
328
+ it("--template 文件缺失 fail-fast:notify error", async () => {
329
+ await handler("复盘 --template tpl.md", ctx);
330
+
331
+ const notify = (ctx as unknown as { ui: { notify: ReturnType<typeof vi.fn> } }).ui.notify;
332
+ expect(notify).toHaveBeenCalledWith(expect.stringContaining("Template file not found"), "error");
333
+ });
334
+
335
+ it("进入成功:notify info 携带 planFilePath(命令反馈通道,widget 之外补可达)", async () => {
336
+ await handler("复盘需求", ctx);
337
+
338
+ const notify = (ctx as unknown as { ui: { notify: ReturnType<typeof vi.fn> } }).ui.notify;
339
+ expect(notify).toHaveBeenCalledWith(
340
+ expect.stringContaining("Plan mode entered: /tmp/test-project/.tmp/plans/"),
341
+ "info",
342
+ );
343
+ });
344
+
298
345
  describe("--template via /plan handler", () => {
299
346
  afterEach(() => {
300
347
  // 还原默认 existsSync=false,避免路径实现泄漏到后续用例
package/src/command.ts CHANGED
@@ -149,6 +149,9 @@ export function registerPlanCommand(
149
149
  const plansDir = path.join(projectDir, ".tmp", "plans");
150
150
  const existingPlans = findExistingPlans(plansDir);
151
151
  if (existingPlans.length > 0) {
152
+ // D10②:选项清单经 display:false 注入(LLM 消费),用户可见性由 notify 承担
153
+ // (现状 display:false 消息用户零可见——反馈矩阵修复)
154
+ ctx.ui.notify(`Found ${existingPlans.length} existing plan file(s) in .tmp/plans — options sent to the model.`, "info");
152
155
  sendPlanContextMessage(
153
156
  pi,
154
157
  `[PLAN MODE] Found existing plan files:\n${existingPlans.map((p, i) => ` ${i + 1}. ${p}`).join("\n")}\n\n` +
@@ -229,13 +232,15 @@ function sendPlanContextMessage(pi: ExtensionAPI, content: string): void {
229
232
  }
230
233
 
231
234
  /** E1 fail-fast 回复:不进入计划模式(不写 entry / 不限制工具 / 不注入计划提示词),回复可用技能清单与纠正命令 */
232
- function reportUnknownSkills(pi: ExtensionAPI, resolution: Extract<SkillResolution, { ok: false }>): void {
235
+ function reportUnknownSkills(pi: ExtensionAPI, ctx: ExtensionContext, resolution: Extract<SkillResolution, { ok: false }>): void {
233
236
  const available = resolution.available.length > 0
234
237
  ? resolution.available.map((n) => ` - ${n}`).join("\n")
235
238
  : " (no skills installed)";
236
239
  const problem = resolution.missing.length > 0
237
240
  ? `unknown skill(s): ${resolution.missing.join(", ")}`
238
241
  : "--skills was given but no skill names followed it";
242
+ // D10②:fail-fast 错误的用户可见性由 notify 承担(display:false 注入仅 LLM 可见)
243
+ ctx.ui.notify(`Plan mode not entered: ${problem}`, "error");
239
244
  sendPlanContextMessage(
240
245
  pi,
241
246
  `[PLAN MODE] Failed to enter: ${problem}.\n\n` +
@@ -249,7 +254,9 @@ function reportUnknownSkills(pi: ExtensionAPI, resolution: Extract<SkillResoluti
249
254
  * 不限制工具 / 不注入计划提示词),回复问题句(互斥/不存在/非 md,problem 内
250
255
  * 已带用法样例)。
251
256
  */
252
- function reportTemplateFlagError(pi: ExtensionAPI, problem: string): void {
257
+ function reportTemplateFlagError(pi: ExtensionAPI, ctx: ExtensionContext, problem: string): void {
258
+ // D10②:fail-fast 错误的用户可见性由 notify 承担(display:false 注入仅 LLM 可见)
259
+ ctx.ui.notify(`Plan mode not entered: ${problem}`, "error");
253
260
  sendPlanContextMessage(
254
261
  pi,
255
262
  `[PLAN MODE] Failed to enter: ${problem}.\n\n` +
@@ -274,6 +281,7 @@ function handleEnterPlanMode(
274
281
  if (parsed.skills !== undefined && parsed.templatePath !== undefined) {
275
282
  reportTemplateFlagError(
276
283
  pi,
284
+ ctx,
277
285
  "--template and --skills are mutually exclusive. Use one: /plan <requirement> --skills a,b | /plan <requirement> --template <path>",
278
286
  );
279
287
  return;
@@ -284,7 +292,7 @@ function handleEnterPlanMode(
284
292
  if (parsed.templatePath !== undefined) {
285
293
  const resolution = resolveTemplateFile(parsed.templatePath, ctx.cwd);
286
294
  if (!resolution.ok) {
287
- reportTemplateFlagError(pi, resolution.problem);
295
+ reportTemplateFlagError(pi, ctx, resolution.problem);
288
296
  return;
289
297
  }
290
298
  // 全文在校验通过后、任何进入副作用(entry/工具限制)之前读好——读失败
@@ -293,7 +301,7 @@ function handleEnterPlanMode(
293
301
  try {
294
302
  templateContent = fs.readFileSync(resolution.absPath, "utf-8");
295
303
  } catch {
296
- reportTemplateFlagError(pi, `Template file not readable: ${resolution.absPath}. ${TEMPLATE_USAGE_SAMPLE}`);
304
+ reportTemplateFlagError(pi, ctx, `Template file not readable: ${resolution.absPath}. ${TEMPLATE_USAGE_SAMPLE}`);
297
305
  return;
298
306
  }
299
307
  templateAbsPath = resolution.absPath;
@@ -309,14 +317,14 @@ function handleEnterPlanMode(
309
317
  if (failed.ok) {
310
318
  throw new Error("plan: empty --skills must fail skill resolution (invariant)");
311
319
  }
312
- reportUnknownSkills(pi, failed);
320
+ reportUnknownSkills(pi, ctx, failed);
313
321
  return;
314
322
  }
315
323
  let resolved: SkillRef[] = [];
316
324
  if (requested.length > 0) {
317
325
  const resolution = resolveSkills(pi, requested);
318
326
  if (!resolution.ok) {
319
- reportUnknownSkills(pi, resolution);
327
+ reportUnknownSkills(pi, ctx, resolution);
320
328
  return;
321
329
  }
322
330
  resolved = resolution.resolved;
@@ -326,7 +334,7 @@ function handleEnterPlanMode(
326
334
  // 进入核心收敛到 enter.ts(plan(enter) tool 与 slash 命令共用);本入口只负责
327
335
  // flag 解析/校验(上方)与提示词投递(下方 sendMessage custom message 注入)。
328
336
  // state 由 activatePlanMode 就地改 + persist(getPlanState 缓存同一对象)。
329
- const { prompt } = activatePlanMode(pi, planCtx.states, sessionId, ctx, {
337
+ const { state: activated, prompt } = activatePlanMode(pi, planCtx.states, sessionId, ctx, {
330
338
  requirement,
331
339
  skills: resolved,
332
340
  projectDir: ctx.cwd,
@@ -334,6 +342,9 @@ function handleEnterPlanMode(
334
342
  ? { template: { absPath: templateAbsPath, content: templateContent } }
335
343
  : {}),
336
344
  });
345
+ // D10②:进入成功的用户可见性由 notify 承担(提示词注入 display:false 仅 LLM 可见;
346
+ // widget 已呈激活态,notify 补命令反馈通道)
347
+ ctx.ui.notify(`Plan mode entered: ${activated.planFilePath}`, "info");
337
348
 
338
349
  // Inject plan mode prompt as custom message(display/triggerTurn 语义见 sendPlanContextMessage。
339
350
  // 非 streaming 直调 _runAgentPrompt 跳过 prompt() 前置链,首轮 systemPrompt 叠加
@@ -41,7 +41,7 @@ const GOAL_INIT_SLOT_KEY = Symbol.for("@zhushanwen/pi-goal.goalInit");
41
41
 
42
42
  /**
43
43
  * goal 桥的单一断言点:goal 扩展挂在 globalThis slot 上的编程式接口(发现 7——
44
- * 桥通道从 pi API 对象挂载迁到 slot:pi 0.84.4 per-extension API 隔离使
44
+ * 桥通道从 pi API 对象挂载迁到 slot:pi per-extension API 隔离(1.0.0 复核)使
45
45
  * pi.__goalInit 形态跨扩展恒不可见,slot 是 C-ext-06 惯例的进程级共享形态)。
46
46
  */
47
47
  function getGoalInit(): GoalInitFn | undefined {
package/src/state.ts CHANGED
@@ -91,9 +91,9 @@ export const DEFAULT_PLAN_STATE: PlanState = {
91
91
  *
92
92
  * ask_user(D10/F9):提示词 Phase B 本就指示「Use ask_user tool if available」,白名单
93
93
  * 曾把它排除(结构性禁言)。feature-tier 依赖降级:ask-user 是可禁扩展,pi setActiveTools
94
- * 对不存在的工具名静默跳过——pi 实装锚点:dist/core/agent-session.js:655(0.84.4,
94
+ * 对不存在的工具名静默跳过——pi 实装锚点(1.0.0 复核):dist/core/agent-session.js
95
95
  * setActiveToolsByName docstring "Only tools in the registry can be enabled. Unknown
96
- * tool names are ignored.")——被禁时不报错不生效,提示词 "if available"
96
+ * and hidden tool names are ignored."——被禁时不报错不生效,提示词 "if available"
97
97
  * 条件语义即降级(回退对话流提问)。
98
98
  */
99
99
  export const PLAN_MODE_TOOLS = ["read", "bash", "grep", "find", "ls", "plan", "ask_user"];
@@ -176,7 +176,7 @@ export type PlanAbortControllers = Map<string, PendingSelect>;
176
176
  * 发挂起 select 前新建 PendingSelect 并登记。禁复用已 abort 的 controller——
177
177
  * pi 实装对已 abort 的 signal 在 createDialogPromise 首行短路立即 resolve undefined,
178
178
  * 复用会让退出后再入 plan 的 submit-review 瞬时静默取消。
179
- * pi 实装锚点:dist/modes/rpc/rpc-mode.js:48(0.84.4)——createDialogPromise 首行
179
+ * pi 实装锚点:dist/modes/rpc/rpc-mode.js createDialogPromise 首行
180
180
  * `opts?.signal?.aborted` 即 `return Promise.resolve(defaultValue)`,select 的
181
181
  * defaultValue = undefined(E10 生命周期设计依据)。
182
182
  */
package/src/tool.ts CHANGED
@@ -895,8 +895,8 @@ async function executeSubmitReview(
895
895
 
896
896
  case "revise": {
897
897
  // custom message 形态注入:streaming 时显式
898
- // deliverAs:'steer' 排队至下一次 LLM 调用(pi 实装锚点:dist/core/agent-session.js
899
- // :859-868(0.84.4)——isStreaming 分支 steer 走 :868 _queueSteer;sendMessage 缺省
898
+ // deliverAs:'steer' 排队至下一次 LLM 调用(语义登记 PS-58,verifiedWith 以
899
+ // pi-semantics.json 为准——isStreaming 分支 steer 走 _queueSteer;sendMessage 缺省
900
900
  // deliverAs 同为 steer,显式传保持排队语义自明);非 streaming 由 triggerTurn:true 开轮
901
901
  // 状态写走 transition()(D1 'revise' 边:reviewing → revising)且先于 steer 注入:
902
902
  // ok:false = 审批已被交错操作推进,此时注入评论并报成功会让 agent 空跑一轮——
@@ -1072,7 +1072,7 @@ async function resolveCompleteChoice(
1072
1072
  // 泛型组件工厂签名比 GuiContext 的宽松形状窄,整 ctx 直传类型不兼容)。
1073
1073
  // select 必须 .bind(ctx.ui)(对齐 ask-user / scheduler 同协议形态):callMarkerRpc 先
1074
1074
  // 解构再裸调用,this 依赖 pi 实装 select 为箭头闭包——显式 bind 消除该隐式依赖
1075
- // (pi 实装锚点:dist/modes/rpc/rpc-mode.js:84(0.84.4)——select 为箭头函数闭包)。
1075
+ // (pi 实装锚点:dist/modes/rpc/rpc-mode.js select 为箭头函数闭包)。
1076
1076
  const form = await uiFormInteract(
1077
1077
  { mode: ctx.mode, hasUI: ctx.hasUI, ui: { select: ctx.ui.select.bind(ctx.ui) } },
1078
1078
  [question],
@@ -1279,11 +1279,11 @@ export function registerPlanTool(
1279
1279
  name: "plan",
1280
1280
  label: "Plan Mode",
1281
1281
  // 串行声明(D-B1-4):全部 action 就地突变共享 PlanState,声明串行消除并行交错类
1282
- //(A1 死锁机理的调度半边)。pi 0.84.4 声明粒度是工具级——同批任一 sequential 工具
1283
- // 使整批工具顺序执行。pi 实装锚点:dist/core/extensions/types.d.ts:363-370(0.84.4,
1284
- // executionMode 是 per-tool 声明,"sequential" = this tool must execute one at a time
1285
- // with other tool calls)+ @earendil-works/pi-agent-core dist/agent-loop.js:287-288
1286
- //(0.84.4,hasSequentialToolCall = toolCalls.some(...) 命中即整批走
1282
+ //(A1 死锁机理的调度半边)。pi 声明粒度是工具级——同批任一 sequential 工具
1283
+ // 使整批工具顺序执行。pi 实装锚点(1.0.0 复核):dist/core/extensions/types.d.ts
1284
+ // executionMode(per-tool 声明,"sequential" = this tool must execute one at a time
1285
+ // with other tool calls)+ @earendil-works/pi-agent-core dist/agent-loop.js
1286
+ //(hasSequentialToolCall = toolCalls.some(...) 命中即整批走
1287
1287
  // executeToolCallsSequential)。单行回退通道 = 移除本声明即回默认并行。
1288
1288
  executionMode: "sequential",
1289
1289
  description: