@zhushanwen/pi-goal 0.5.0 → 0.5.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.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Codex-style /goal command for Pi — persistent goal-driven autonomous loop with evidence-based completion, token/time budgets, blocked detection, and steering templates.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -24,7 +24,7 @@
24
24
  "peerDependencies": {
25
25
  "@earendil-works/pi-ai": "*",
26
26
  "@earendil-works/pi-tui": "*",
27
- "@mariozechner/pi-coding-agent": "*",
27
+ "@earendil-works/pi-coding-agent": "*",
28
28
  "@sinclair/typebox": "*"
29
29
  },
30
30
  "dependencies": {
@@ -9,7 +9,7 @@
9
9
  * 用 fake pi + fake ctx(不 import Pi SDK 真实实现)。
10
10
  * handleGoalCommand(pi, session, args, ctx) → Promise<void>。
11
11
  */
12
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
13
  import { describe, expect, it } from "vitest";
14
14
 
15
15
  import { handleGoalCommand } from "../adapters/command-adapter";
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * 用 fake pi + fake ctx(不 import Pi SDK)。handler 签名 (pi, session, ctx) → void/result。
11
11
  */
12
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
13
  import { describe, expect, it, vi } from "vitest";
14
14
 
15
15
  import { handleAgentEnd } from "../adapters/event-handlers/agent-end";
@@ -0,0 +1,79 @@
1
+ // 提示词质量回归:goal_control tool description + runtime 错误文案必须是
2
+ // "可纠正的行为约束器",而非干巴巴的错误。
3
+ //
4
+ // agent(LLM)唯一能看到的 tool 元信息就是 description + 报错文案。弱模型
5
+ // (如 step-3.7-flash)会在首次调用时省略条件必填字段(create 漏 slug、
6
+ // complete 漏 evidence——complete 需模型主动生成内容,省略动因最强)。
7
+ // description 补完整 JSON 正例 + 结构反例,runtime throw 补 "Correct:" 纠正
8
+ // 正例,能让弱模型在首轮或报错后无需猜测即可对齐。
9
+ //
10
+ // 本测试用源码断言(读 .ts 文件文本)锁定这些约束,防止后续重构把正例/反例
11
+ // 或纠错文案删掉。读源码而非 import,避免 mock 链(goal-control-adapter.ts
12
+ // 依赖 pi-ai/typebox/pi-tui/ExtensionAPI 等值导入)。
13
+ //
14
+ // 模式参考:subagent-workflow/src/interface/__tests__/subagent-tool-prompt.test.ts。
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ import { describe, expect, it } from "vitest";
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const ADAPTER_SRC = readFileSync(
24
+ join(__dirname, "../adapters/goal-control-adapter.ts"),
25
+ "utf-8",
26
+ );
27
+
28
+ /** 提取 description: `...` 模板字符串的原始内容。 */
29
+ function extractDescription(src: string): string {
30
+ const m = src.match(/description:\s*`([\s\S]*?)`/);
31
+ if (!m) throw new Error("description template literal not found");
32
+ return m[1];
33
+ }
34
+
35
+ const DESCRIPTION = extractDescription(ADAPTER_SRC);
36
+
37
+ describe("goal_control description — 含完整 JSON 正例(弱模型首轮误用防御)", () => {
38
+ it("含 create 正例子串", () => {
39
+ // create 是弱模型最易漏 slug 的 action;完整 JSON 正例给出可直接复用的形态。
40
+ expect(DESCRIPTION).toContain('{"action":"create"');
41
+ });
42
+
43
+ it("含 complete 正例子串", () => {
44
+ // complete 需模型主动生成 evidence,省略动因最强;正例锁定必填字段。
45
+ expect(DESCRIPTION).toContain('{"action":"complete"');
46
+ });
47
+
48
+ it("含 report_blocked 正例子串", () => {
49
+ expect(DESCRIPTION).toContain('{"action":"report_blocked"');
50
+ });
51
+ });
52
+
53
+ describe("goal_control description — 含参数结构反例(Don't 段)", () => {
54
+ it("含 Don't 段", () => {
55
+ expect(DESCRIPTION).toMatch(/Don't/);
56
+ });
57
+
58
+ it("Don't 段含 evidence 结构反例关键词(complete 漏 evidence)", () => {
59
+ // complete 漏 evidence 是核心失败模式之一;反例文案必须提及 evidence。
60
+ expect(DESCRIPTION).toMatch(/evidence/);
61
+ });
62
+ });
63
+
64
+ describe("goal_control runtime 错误文案 — 含 'Correct:' 纠正正例(≥4 处)", () => {
65
+ it("源码含 ≥4 处 'Correct:' 纠错文案(4 条 required throw 各一带正例)", () => {
66
+ // objective/slug/evidence/reason 四条必填 throw 各应带完整 JSON 正例,
67
+ // 让弱模型在报错后无需猜测即可纠正。读整份源码统计出现次数。
68
+ const matches = ADAPTER_SRC.match(/Correct:/g) || [];
69
+ expect(matches.length).toBeGreaterThanOrEqual(4);
70
+ });
71
+
72
+ it("objective 报错带 Correct 正例", () => {
73
+ expect(ADAPTER_SRC).toMatch(/'objective' is required[\s\S]*?Correct:/);
74
+ });
75
+
76
+ it("evidence 报错带 Correct 正例", () => {
77
+ expect(ADAPTER_SRC).toMatch(/'evidence' is required[\s\S]*?Correct:/);
78
+ });
79
+ });
@@ -15,7 +15,7 @@
15
15
  * 不 mock handleCreate:走真实 createGoal 让 session.state 含完整字段(slug/objective/budget),
16
16
  * 避免 hand-rolled state 与生产路径不一致。pi/ctx 用最小 fake(对齐 index.test.ts makeFactoryFixture)。
17
17
  */
18
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
19
  import { describe, expect, it } from "vitest";
20
20
 
21
21
  import { registerGoalControlTool, type GoalControlDetails } from "../adapters/goal-control-adapter";
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * 用最小 fake pi + fake ctx 实例化 goalExtension 工厂,再调 pi.__goalInit。
8
8
  */
9
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
9
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { describe, expect, it } from "vitest";
11
11
 
12
12
  import goalExtension from "../index";
@@ -14,7 +14,7 @@
14
14
  * adapters 层可 import Pi 类型(桥接 Pi 和 service)。
15
15
  */
16
16
 
17
- import type { CustomEntry, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
17
+ import type { CustomEntry, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
18
 
19
19
  import { parseGoalArgs } from "../commands";
20
20
  import {
@@ -18,7 +18,7 @@
18
18
  * 检查之后——终态 goal 仍走终态 notify(不被 ESC 影响),非 active 状态直接返回。
19
19
  */
20
20
 
21
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
21
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
22
22
 
23
23
  import { checkBudgetOnTurnEnd } from "../../engine/budget";
24
24
  import { isActiveStatus, isTerminalStatus } from "../../engine/goal";
@@ -15,7 +15,7 @@
15
15
  * contextInjectionPrompt 恒定建议 plan mode(AI 自行决定是否用)。
16
16
  */
17
17
 
18
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
19
 
20
20
  import { AUTO_CLEAR_TURNS, CONTEXT_USAGE_RATIO_LIMIT } from "../../constants";
21
21
  import { isActiveStatus, isTerminalStatus } from "../../engine/goal";
@@ -7,7 +7,7 @@
7
7
  * 不 persist / updateWidget。
8
8
  */
9
9
 
10
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
11
 
12
12
  import { applyEvent } from "../../service";
13
13
  import type { GoalSession } from "../../session";
@@ -4,7 +4,7 @@
4
4
  * 调 reconstructGoalState 重建持久化状态 + updateWidget。
5
5
  */
6
6
 
7
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
8
 
9
9
  import { updateWidget } from "../../projection/widget";
10
10
  import type { GoalSession } from "../../session";
@@ -10,7 +10,7 @@
10
10
  * 不 persist(与旧 index.ts 行为对齐——turn_end 只内存变更 + widget)。
11
11
  */
12
12
 
13
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
13
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
14
14
 
15
15
  import { updateWidget } from "../../projection/widget";
16
16
  import { applyEvent } from "../../service";
@@ -25,8 +25,8 @@
25
25
  */
26
26
 
27
27
  import { StringEnum } from "@earendil-works/pi-ai";
28
- import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
29
- import { Text } from "@mariozechner/pi-tui";
28
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
29
+ import { Text } from "@earendil-works/pi-tui";
30
30
  import { type GuiComponent, guiComponent, type GuiRenderResult,guiResult } from "@xyz-agent/extension-protocol";
31
31
  import { type Static, Type } from "typebox";
32
32
 
@@ -118,11 +118,15 @@ export function handleCreate(
118
118
  ): GoalControlDetails {
119
119
  const objective = params.objective?.trim();
120
120
  if (!objective) {
121
- throw new Error("'objective' is required for create. Describe the concrete objective to pursue.");
121
+ throw new Error(
122
+ "'objective' is required for create. Describe the concrete objective to pursue. Correct: {\"action\":\"create\",\"slug\":\"<kebab-case>\",\"objective\":\"<concrete objective>\"}",
123
+ );
122
124
  }
123
125
  const slug = params.slug?.trim();
124
126
  if (!slug) {
125
- throw new Error("'slug' is required for create. Provide a short kebab-case identifier (e.g. 'refactor-auth').");
127
+ throw new Error(
128
+ "'slug' is required for create. Provide a short kebab-case identifier (e.g. 'refactor-auth'). Correct: {\"action\":\"create\",\"slug\":\"refactor-auth\",\"objective\":\"<concrete objective>\"}",
129
+ );
126
130
  }
127
131
 
128
132
  // D25 严格守卫:非终态旧 goal(active/paused/blocked)→ 拒绝创建(防静默覆盖未完成工作)
@@ -181,7 +185,9 @@ export function handleComplete(
181
185
  }
182
186
  const evidence = params.evidence?.trim();
183
187
  if (!evidence) {
184
- throw new Error("'evidence' is required for complete. Provide concrete completion evidence.");
188
+ throw new Error(
189
+ "'evidence' is required for complete. Provide concrete completion evidence. Correct: {\"action\":\"complete\",\"evidence\":\"Modified src/auth.ts; pnpm test auth passed (12/12); tsc --noEmit clean.\"}",
190
+ );
185
191
  }
186
192
 
187
193
  // FR-3.3: 唯一终态序列入口(内部:tickState → finalizeGoal(transition+history) → persist)
@@ -210,7 +216,9 @@ export function handleReportBlocked(
210
216
  }
211
217
  const reason = params.reason?.trim();
212
218
  if (!reason) {
213
- throw new Error("'reason' is required for report_blocked. Describe the blocking condition.");
219
+ throw new Error(
220
+ "'reason' is required for report_blocked. Describe the blocking condition. Correct: {\"action\":\"report_blocked\",\"reason\":\"<blocker + what was tried (at least 3 approaches)>\"}",
221
+ );
214
222
  }
215
223
 
216
224
  state.lastBlockerReason = reason;
@@ -237,6 +245,15 @@ export function handleReportBlocked(
237
245
  *
238
246
  * 对齐 projection/widget.ts 的 getBudgetColor 语义——终态预算耗尽渲染为 error。
239
247
  */
248
+ /** renderResult 的 result 是否含 details 字段(类型守卫,替代全可选结构断言 as {details?})。
249
+ * 收紧:除检查 "details" in r 外,还验证其值为 object 或 undefined(防 details 是 string/number
250
+ * 时下游读 d.status 得到 undefined 却被类型系统当作 GoalControlDetails)。 */
251
+ function hasGoalDetails(r: unknown): r is { details?: GoalControlDetails } {
252
+ if (typeof r !== "object" || r === null || !("details" in r)) return false;
253
+ const d = (r as Record<string, unknown>).details;
254
+ return d === undefined || typeof d === "object";
255
+ }
256
+
240
257
  function goalStatusSeverity(status: GoalStatus): "ok" | "warn" | "danger" {
241
258
  switch (status) {
242
259
  case "active":
@@ -341,7 +358,22 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
341
358
  name: "goal_control",
342
359
  label: "Goal Control",
343
360
  description:
344
- "Manage the goal for this thread.\n\nActions:\n- create: start a new goal. Requires `slug` (a short kebab-case identifier you generate) and `objective` (the full description). Only use when the user explicitly asks to start a goal; do not infer goals from ordinary tasks. Fails if a goal is already active/paused/blocked (use /goal resume or /goal clear first).\n- complete: mark the active goal complete. Requires `evidence` with concrete proof (files/tests/commands). Recommend finishing all todos (including verification todos) first, but you decide.\n- report_blocked: mark the active goal blocked by a real blocker. Requires `reason` describing the block and what was tried. Only after genuine exhaustion of alternatives.",
361
+ `Manage the goal for this thread.
362
+
363
+ Actions:
364
+ - create: start a new goal. Requires 'slug' (a short kebab-case identifier you generate) and 'objective' (the full description). Only use when the user explicitly asks to start a goal; do not infer goals from ordinary tasks. Fails if a goal is already active/paused/blocked (use /goal resume or /goal clear first).
365
+ - complete: mark the active goal complete. Requires 'evidence' with concrete proof (files/tests/commands). Recommend finishing all todos (including verification todos) first, but you decide.
366
+ - report_blocked: mark the active goal blocked by a real blocker. Requires 'reason' describing the block and what was tried. Only after genuine exhaustion of alternatives.
367
+
368
+ Examples:
369
+ {"action":"create","slug":"refactor-auth","objective":"Refactor the auth module to use JWT and add integration tests"}
370
+ {"action":"complete","evidence":"Modified src/auth.ts; pnpm test auth passed (12/12); tsc --noEmit clean."}
371
+ {"action":"report_blocked","reason":"Blocked: DB migration API changed mid-task (tried: regenerate client, pin old version, rewrite queries)."}
372
+
373
+ Don't:
374
+ - create without 'slug': {"action":"create","objective":"..."} — slug is required, generate a kebab-case id.
375
+ - complete without 'evidence': {"action":"complete"} — must provide concrete completion proof (files/tests/commands).
376
+ - complete when no goal is active — create or resume one first (fails with 'Goal mode not active').`,
345
377
  promptSnippet:
346
378
  "Use goal_control to manage the thread goal: create (with slug + objective, only when user asks) or complete (with evidence) or report_blocked (with reason, after trying alternatives).",
347
379
  executionMode: "sequential",
@@ -396,7 +428,7 @@ export function registerGoalControlTool(pi: ExtensionAPI, session: GoalSession):
396
428
  },
397
429
 
398
430
  renderResult(result: unknown, _options: { expanded: boolean }, theme: Theme): Text {
399
- const d = (result as { details?: GoalControlDetails }).details;
431
+ const d = hasGoalDetails(result) ? result.details : undefined;
400
432
  if (!d) return new Text(theme.fg("dim", "goal_control"), 0, 0);
401
433
  const statusColor =
402
434
  d.status === "active"
@@ -9,7 +9,7 @@
9
9
  * - session: ctx.sessionManager.getEntries + ctx.getContextUsage + ctx.signal
10
10
  */
11
11
 
12
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
12
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
13
13
 
14
14
  import { ENTRY_TYPE, HISTORY_ENTRY_TYPE } from "../persistence";
15
15
  import type { MessagingPort, PersistencePort, SessionPort, UiPort } from "../ports";
package/src/index.ts CHANGED
@@ -17,8 +17,8 @@
17
17
  * FR-6.7:移除 pendingPause(ESC 改用 ctx.signal.aborted 守卫,在 event-adapter)。
18
18
  */
19
19
 
20
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
21
- import { Text } from "@mariozechner/pi-tui";
20
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
21
+ import { Text } from "@earendil-works/pi-tui";
22
22
 
23
23
  import { handleGoalCommand } from "./adapters/command-adapter";
24
24
  import {