@zhushanwen/pi-plan 0.2.3 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-plan",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Lightweight plan mode for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -16,8 +16,9 @@
16
16
  "license": "MIT",
17
17
  "peerDependencies": {
18
18
  "@earendil-works/pi-coding-agent": ">=0.73.0",
19
- "@sinclair/typebox": "*",
20
- "@earendil-works/pi-ai": "*"
19
+ "typebox": "*",
20
+ "@earendil-works/pi-ai": "*",
21
+ "@zhushanwen/pi-goal": "0.7.0"
21
22
  },
22
23
  "peerDependenciesMeta": {
23
24
  "@earendil-works/pi-ai": {
@@ -78,6 +78,8 @@ describe("handlePlanComplete", () => {
78
78
  expect.any(String),
79
79
  undefined,
80
80
  ctx,
81
+ "plan",
82
+ "All 2 steps of plan.md executed and verified: Step one; Step two",
81
83
  );
82
84
  });
83
85
 
@@ -108,6 +110,8 @@ describe("handlePlanComplete", () => {
108
110
  expect.any(String),
109
111
  undefined,
110
112
  ctx,
113
+ "plan",
114
+ "All 2 steps of plan.md executed and verified: Step one; Step two",
111
115
  );
112
116
  });
113
117
  });
package/src/command.ts CHANGED
@@ -18,6 +18,17 @@ export function registerPlanCommand(
18
18
  "Enter plan mode: /plan [description]. " +
19
19
  "Subcommands: /plan abort, /plan status. " +
20
20
  "With no args, show status or detect existing plan.",
21
+ getArgumentCompletions(prefix: string) {
22
+ const parts = prefix.trimStart().split(/\s+/).filter(Boolean);
23
+ // More than 1 token means the user is typing a free-text requirement, no completion.
24
+ if (parts.length > 1) return null;
25
+ const trimmed = (parts[0] ?? "").toLowerCase();
26
+ const opts = [
27
+ { label: "abort", value: "abort", description: "取消活跃的 plan mode" },
28
+ { label: "status", value: "status", description: "查看 plan mode 状态" },
29
+ ];
30
+ return trimmed === "" ? opts : opts.filter((o) => o.label.startsWith(trimmed));
31
+ },
21
32
  handler: async (args: string, ctx: ExtensionContext) => {
22
33
  const trimmed = args.trim();
23
34
  const sessionId = ctx.sessionManager.getSessionId();
package/src/compact.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs";
2
+ import { basename } from "node:path";
2
3
 
3
4
  import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent, SessionBeforeTreeEvent } from "@earendil-works/pi-coding-agent";
5
+ import type { GoalInitFn } from "@zhushanwen/pi-goal";
4
6
 
5
7
  import type { PlanSessionMap, PlanState } from "./state.js";
6
8
  import { getPlanState } from "./state.js";
@@ -67,26 +69,45 @@ function readPlanFileSafe(planFilePath: string): string {
67
69
  /** Detect whether goal extension is available via its programming interface */
68
70
  export function detectGoalCapability(pi: ExtensionAPI): boolean {
69
71
  try {
70
- const api = pi as unknown as Record<string, unknown>;
72
+ // 交叉类型单步断言(ExtensionAPI 可赋给 ExtensionAPI & { __goalInit? })
73
+ const api = pi as ExtensionAPI & { __goalInit?: GoalInitFn };
71
74
  return typeof api.__goalInit === "function";
72
75
  } catch {
73
76
  return false;
74
77
  }
75
78
  }
76
79
 
80
+ /**
81
+ * 从 plan 文件路径推导 goal slug(kebab-case;无有效字符时 fallback)。
82
+ * 仅 widget 标题 + history 展示用,不注入 prompt。
83
+ */
84
+ function buildPlanSlug(planFilePath: string): string {
85
+ const stem = basename(planFilePath)
86
+ .replace(/\.md$/i, "")
87
+ .toLowerCase()
88
+ .replace(/[^a-z0-9]+/g, "-")
89
+ .replace(/^-+|-+$/g, "");
90
+ return stem || "plan-execution";
91
+ }
92
+
93
+ /** successCriteria 步骤预览条数上限 */
94
+ const STEP_PREVIEW_LIMIT = 3;
95
+
96
+ /**
97
+ * 从 plan 步骤构造可检查的 successCriteria(plan 完成 = 所有步骤执行并验证)。
98
+ * goal 的 complete 判定会对照本字段做证据审计。
99
+ */
100
+ function buildPlanSuccessCriteria(planFilePath: string, tasks: string[]): string {
101
+ const preview = tasks.slice(0, STEP_PREVIEW_LIMIT).join("; ");
102
+ const ellipsis = tasks.length > STEP_PREVIEW_LIMIT ? "; …" : "";
103
+ return `All ${tasks.length} steps of ${basename(planFilePath)} executed and verified: ${preview}${ellipsis}`;
104
+ }
105
+
77
106
  /** Try to initialize goal via programming interface */
78
107
  function tryGoalInit(pi: ExtensionAPI, planFilePath: string, ctx: ExtensionContext): boolean {
79
- // Inline alias mirrors @zhushanwen/pi-goal's GoalInitFn (exported from goal/src/index.ts).
80
- // Kept inline due to optional duck-typed coupling (pi.__goalInit) — update both if signature changes.
81
- type GoalInitFn = (
82
- objective: string,
83
- budget: { tokenBudget?: number; timeBudgetMinutes?: number } | undefined,
84
- ctx: ExtensionContext,
85
- ) => boolean;
86
-
87
108
  try {
88
- const api = pi as unknown as Record<string, unknown>;
89
- const goalInit = api.__goalInit as GoalInitFn | undefined;
109
+ const api = pi as ExtensionAPI & { __goalInit?: GoalInitFn };
110
+ const goalInit = api.__goalInit;
90
111
  if (typeof goalInit !== "function") return false;
91
112
 
92
113
  const planContent = readPlanFileSafe(planFilePath);
@@ -96,7 +117,13 @@ function tryGoalInit(pi: ExtensionAPI, planFilePath: string, ctx: ExtensionConte
96
117
  const tasks = extractPlanSteps(planContent);
97
118
  if (tasks.length === 0) return false;
98
119
 
99
- return goalInit(objective, undefined, ctx);
120
+ return goalInit(
121
+ objective,
122
+ undefined,
123
+ ctx,
124
+ buildPlanSlug(planFilePath),
125
+ buildPlanSuccessCriteria(planFilePath, tasks),
126
+ );
100
127
  } catch {
101
128
  return false;
102
129
  }
package/src/index.ts CHANGED
@@ -14,7 +14,8 @@ export default function planExtension(pi: ExtensionAPI) {
14
14
  registerPlanCommand(pi, sessions);
15
15
 
16
16
  // External API: __planStart(allow other extensions to start plan mode programmatically, #9)
17
- const api = pi as unknown as Record<string, unknown>;
17
+ // 交叉类型单步断言(ExtensionAPI 可赋给 ExtensionAPI & { __planStart? },无需 unknown 中转)
18
+ const api = pi as ExtensionAPI & { __planStart?: (requirement: string, ctx: ExtensionContext) => boolean };
18
19
  api.__planStart = (requirement: string, ctx: ExtensionContext): boolean => {
19
20
  return startPlanMode(pi, sessions, ctx, requirement);
20
21
  };
package/src/state.ts CHANGED
@@ -67,8 +67,13 @@ export function resetPlanState(
67
67
  }
68
68
 
69
69
  function isPlanStateEntry(entry: SessionEntry): entry is CustomEntry<Partial<PlanState>> & { customType: "plan-state" } {
70
- const e = entry as unknown as Record<string, unknown>;
71
- return e.type === "custom" && e.customType === "plan-state" && typeof e.data === "object" && e.data !== null;
70
+ // 判别式收窄(type === "custom")后可直接访问 customType/data,无需 cast
71
+ return (
72
+ entry.type === "custom" &&
73
+ entry.customType === "plan-state" &&
74
+ typeof entry.data === "object" &&
75
+ entry.data !== null
76
+ );
72
77
  }
73
78
 
74
79
  export function reconstructPlanState(ctx: ExtensionContext): PlanState {
@@ -76,15 +81,16 @@ export function reconstructPlanState(ctx: ExtensionContext): PlanState {
76
81
  const entries = ctx.sessionManager.getEntries();
77
82
 
78
83
  for (let i = entries.length - 1; i >= 0; i--) {
79
- if (isPlanStateEntry(entries[i])) {
80
- const data = (entries[i] as unknown as { data: Partial<PlanState> }).data;
81
- state.isActive = data.isActive ?? false;
82
- state.phase = data.phase ?? "idle";
83
- state.planFilePath = data.planFilePath ?? "";
84
- state.requirement = data.requirement ?? "";
85
- state.templateName = data.templateName ?? "";
86
- break;
87
- }
84
+ // entries[i] 是复杂表达式(TS 不收窄),守卫移到 const 变量上
85
+ const entry = entries[i];
86
+ if (!isPlanStateEntry(entry)) continue;
87
+ const data = entry.data;
88
+ state.isActive = data?.isActive ?? false;
89
+ state.phase = data?.phase ?? "idle";
90
+ state.planFilePath = data?.planFilePath ?? "";
91
+ state.requirement = data?.requirement ?? "";
92
+ state.templateName = data?.templateName ?? "";
93
+ break;
88
94
  }
89
95
 
90
96
  return state;