@zhushanwen/pi-goal 0.12.0 → 0.12.2

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-goal",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token budget, agent-reported blocking, and steering templates.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -29,10 +29,10 @@
29
29
  "@earendil-works/pi-coding-agent": "^0.84.4",
30
30
  "@earendil-works/pi-tui": "^0.84.4",
31
31
  "typebox": "*",
32
- "@zhushanwen/pi-pending-notifications": "0.5.2"
32
+ "@zhushanwen/pi-pending-notifications": "0.5.3"
33
33
  },
34
34
  "dependencies": {
35
- "@xyz-agent/extension-protocol": "0.8.0"
35
+ "@xyz-agent/extension-protocol": "0.8.2"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@earendil-works/pi-tui": {
@@ -422,6 +422,66 @@ describe("parseGoalArgs — --criteria 分号拆分(U20/U21)", () => {
422
422
  });
423
423
  });
424
424
 
425
+ // ── parseGoalArgs — 子命令路由(表驱动 + update/set 分支)──
426
+
427
+ describe("parseGoalArgs — 子命令路由", () => {
428
+ it("空串 / status → status", () => {
429
+ expect(parseGoalArgs("")).toEqual({ action: "status" });
430
+ expect(parseGoalArgs(" ")).toEqual({ action: "status" });
431
+ expect(parseGoalArgs("STATUS")).toEqual({ action: "status" });
432
+ });
433
+
434
+ it.each(["resume", "pause", "clear", "history"] as const)(
435
+ "%s(含大小写混写)→ 对应 action",
436
+ (action) => {
437
+ expect(parseGoalArgs(action)).toEqual({ action });
438
+ expect(parseGoalArgs(` ${action.toUpperCase()} `)).toEqual({ action });
439
+ },
440
+ );
441
+
442
+ it("update 无参 → { action: update }(无 objective,上层报错)", () => {
443
+ expect(parseGoalArgs("update")).toEqual({ action: "update" });
444
+ expect(parseGoalArgs("UPDATE")).toEqual({ action: "update" });
445
+ });
446
+
447
+ it("update 带 objective(无 --criteria)→ objective 全量保留", () => {
448
+ expect(parseGoalArgs("update ship v2 --draft")).toEqual({
449
+ action: "update",
450
+ objective: "ship v2 --draft",
451
+ });
452
+ });
453
+
454
+ it("update 仅接 --criteria(分隔标记要求前导空白)→ 整体作 objective(锚定现状)", () => {
455
+ const parsed = parseGoalArgs("update --criteria a;b");
456
+ expect(parsed.action).toBe("update");
457
+ expect(parsed.objective).toBe("--criteria a;b");
458
+ expect(parsed.criteria).toBeUndefined();
459
+ });
460
+
461
+ it("未知文本 → set 路径:objective 剥 --tokens flag + budget 透传", () => {
462
+ expect(parseGoalArgs("write docs --tokens 3000")).toEqual({
463
+ action: "set",
464
+ objective: "write docs",
465
+ budget: { tokenBudget: 3000 },
466
+ });
467
+ expect(parseGoalArgs("write docs")).toEqual({
468
+ action: "set",
469
+ objective: "write docs",
470
+ budget: {},
471
+ });
472
+ });
473
+
474
+ it("仅 --tokens(剥空 objective)→ status(非 set)", () => {
475
+ expect(parseGoalArgs("--tokens 500")).toEqual({ action: "status" });
476
+ });
477
+
478
+ it("原型链键名(constructor 等)不命中路由表 → set 路径", () => {
479
+ const parsed = parseGoalArgs("constructor");
480
+ expect(parsed.action).toBe("set");
481
+ expect(parsed.objective).toBe("constructor");
482
+ });
483
+ });
484
+
425
485
  // ── /goal set(提示词触发器:sendUserMessage 让 AI 调 goal_control create)──
426
486
 
427
487
  describe("handleGoalCommand — set (提示词触发器 + #11/D25 拒绝非终态)", () => {
@@ -252,7 +252,7 @@ describe("E1 全链路集成:goal_control create 数组 → 落盘 → 重载
252
252
  expect(prompt).toContain("1. e2e cond a");
253
253
  expect(prompt).toContain("2. e2e cond b");
254
254
  } finally {
255
- rmSync(dir, { recursive: true, force: true });
255
+ rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
256
256
  }
257
257
  });
258
258
  });
package/src/commands.ts CHANGED
@@ -13,42 +13,33 @@ export interface GoalCommandArgs {
13
13
  criteria?: string[];
14
14
  }
15
15
 
16
+ /**
17
+ * 无 objective 的简单子命令 → action(表驱动分发;空串 = 纯 /goal,归 status)。
18
+ * 用 Map 而非普通对象:输入是外部字符串,普通对象查表会命中 Object.prototype 上的键
19
+ *(如 "constructor"),导致与原 if 链不同的错误路由。
20
+ */
21
+ const SIMPLE_SUBCOMMAND_ACTIONS = new Map<string, GoalCommandArgs["action"]>([
22
+ ["", "status"],
23
+ ["status", "status"],
24
+ ["resume", "resume"],
25
+ ["pause", "pause"],
26
+ ["clear", "clear"],
27
+ ["history", "history"],
28
+ ]);
29
+
16
30
  export function parseGoalArgs(raw: string): GoalCommandArgs {
17
31
  const trimmed = raw.trim().toLowerCase();
18
32
  const fullRaw = raw.trim();
19
33
 
20
- // Subcommands without objective
21
- if (trimmed === "" || trimmed === "status") {
22
- return { action: "status" };
23
- }
24
- if (trimmed === "resume") {
25
- return { action: "resume" };
26
- }
27
- if (trimmed === "pause") {
28
- return { action: "pause" };
29
- }
30
- if (trimmed === "clear") {
31
- return { action: "clear" };
32
- }
33
- if (trimmed === "history") {
34
- return { action: "history" };
34
+ // Subcommands without objective(表驱动分发)
35
+ const simpleAction = SIMPLE_SUBCOMMAND_ACTIONS.get(trimmed);
36
+ if (simpleAction) {
37
+ return { action: simpleAction };
35
38
  }
36
39
 
37
40
  // /goal update <new objective> [--criteria <a;b;c>]
38
41
  if (trimmed.startsWith("update ")) {
39
- const rest = fullRaw.slice(UPDATE_PREFIX_LENGTH).trim();
40
- // --criteria 作分隔标记(要求两侧空白,避免误切 objective 内文本)
41
- const criteriaSep = rest.match(/\s--criteria\s+/);
42
- if (criteriaSep) {
43
- const sepStart = criteriaSep.index ?? 0;
44
- const objective = rest.slice(0, sepStart).trim();
45
- const criteriaText = rest.slice(sepStart + criteriaSep[0].length).trim();
46
- // 分号拆分 + 逐段 trim + 去空段("a;;b" → ["a","b"]);全空段 → undefined
47
- // (决策④:`--criteria " "` / `";;"` 不产出空数组,handleUpdate 保留旧值)
48
- const criteria = criteriaText.split(";").map((s) => s.trim()).filter(Boolean);
49
- return { action: "update", objective, criteria: criteria.length > 0 ? criteria : undefined };
50
- }
51
- return { action: "update", objective: rest };
42
+ return parseUpdateArgs(fullRaw);
52
43
  }
53
44
  // /goal update (without argument) → 报错
54
45
  if (trimmed === "update") {
@@ -56,20 +47,54 @@ export function parseGoalArgs(raw: string): GoalCommandArgs {
56
47
  }
57
48
 
58
49
  // /goal <objective> [--tokens N]
59
- // 只匹配已知 flag,避免误删 objective 中的 -- 文本
60
- const knownFlags = /--tokens\s+\d+/g;
61
- const objective = fullRaw.replace(knownFlags, "").trim();
62
- const budget: Partial<BudgetConfig> = {};
50
+ return parseSetArgs(fullRaw);
51
+ }
63
52
 
64
- const tokenMatch = fullRaw.match(/--tokens\s+(\d+)/);
65
- if (tokenMatch) {
66
- const val = parseInt(tokenMatch[1]!, 10);
67
- if (!isNaN(val) && val > 0) budget.tokenBudget = val;
53
+ /** /goal update 分支:--criteria 分隔标记切出 objective + criteria 列表 */
54
+ function parseUpdateArgs(fullRaw: string): GoalCommandArgs {
55
+ const rest = fullRaw.slice(UPDATE_PREFIX_LENGTH).trim();
56
+ // --criteria 作分隔标记(要求两侧空白,避免误切 objective 内文本)
57
+ const criteriaSep = rest.match(/\s--criteria\s+/);
58
+ if (!criteriaSep) {
59
+ return { action: "update", objective: rest };
68
60
  }
61
+ const sepStart = criteriaSep.index ?? 0;
62
+ const objective = rest.slice(0, sepStart).trim();
63
+ const criteriaText = rest.slice(sepStart + criteriaSep[0].length).trim();
64
+ return { action: "update", objective, criteria: parseCriteriaList(criteriaText) };
65
+ }
66
+
67
+ /**
68
+ * 分号拆分 + 逐段 trim + 去空段("a;;b" → ["a","b"]);全空段 → undefined
69
+ * (决策④:`--criteria " "` / `";;"` 不产出空数组,handleUpdate 保留旧值)
70
+ */
71
+ function parseCriteriaList(criteriaText: string): string[] | undefined {
72
+ const criteria = criteriaText.split(";").map((s) => s.trim()).filter(Boolean);
73
+ return criteria.length > 0 ? criteria : undefined;
74
+ }
69
75
 
76
+ /** /goal <objective> [--tokens N] 分支:objective 剥已知 flag + budget 解析 */
77
+ function parseSetArgs(fullRaw: string): GoalCommandArgs {
78
+ const objective = stripKnownTokenFlag(fullRaw);
79
+ const budget = parseTokenBudget(fullRaw);
70
80
  if (!objective) {
71
81
  return { action: "status" };
72
82
  }
73
-
74
83
  return { action: "set", objective, budget };
75
84
  }
85
+
86
+ /** 只匹配已知 flag,避免误删 objective 中的 -- 文本 */
87
+ function stripKnownTokenFlag(fullRaw: string): string {
88
+ return fullRaw.replace(/--tokens\s+\d+/g, "").trim();
89
+ }
90
+
91
+ /** 提取 --tokens N 的数值;缺失 / 非法(isNaN 或 ≤0)→ 空 budget */
92
+ function parseTokenBudget(fullRaw: string): Partial<BudgetConfig> {
93
+ const budget: Partial<BudgetConfig> = {};
94
+ const tokenMatch = fullRaw.match(/--tokens\s+(\d+)/);
95
+ if (tokenMatch) {
96
+ const val = parseInt(tokenMatch[1]!, 10);
97
+ if (!isNaN(val) && val > 0) budget.tokenBudget = val;
98
+ }
99
+ return budget;
100
+ }