@zhushanwen/pi-goal 0.2.0 → 0.3.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-goal",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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",
@@ -22,9 +22,9 @@
22
22
  "index.ts"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@mariozechner/pi-coding-agent": "*",
26
- "@earendil-works/pi-tui": "*",
27
25
  "@earendil-works/pi-ai": "*",
26
+ "@earendil-works/pi-tui": "*",
27
+ "@mariozechner/pi-coding-agent": "*",
28
28
  "@sinclair/typebox": "*"
29
29
  },
30
30
  "peerDependenciesMeta": {
@@ -36,6 +36,10 @@
36
36
  }
37
37
  },
38
38
  "scripts": {
39
- "typecheck": "npx tsc --noEmit"
39
+ "typecheck": "npx tsc --noEmit",
40
+ "test": "vitest run"
41
+ },
42
+ "devDependencies": {
43
+ "vitest": "^4.1.8"
40
44
  }
41
45
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * P1 测试:deserializeState — 旧格式向后兼容
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { deserializeState } from "../state";
7
+
8
+ describe("deserializeState — 向后兼容", () => {
9
+ it("旧数据无 verification 字段 → verification 为 undefined", () => {
10
+ const data = {
11
+ goalId: "g1",
12
+ objective: "test",
13
+ status: "active",
14
+ tasks: [{
15
+ id: 1,
16
+ description: "task 1",
17
+ status: "completed",
18
+ lastUpdatedTurn: 0,
19
+ }],
20
+ stallCount: 0,
21
+ tokensUsed: 0,
22
+ timeStartedAt: 1000,
23
+ timeUsedSeconds: 0,
24
+ budget: { maxStallTurns: 5, maxTurns: 50 },
25
+ };
26
+ const state = deserializeState(data);
27
+ expect(state.tasks[0]!.verification).toBeUndefined();
28
+ });
29
+
30
+ it("旧数据有 turnCount 字段 → 被忽略(不报错)", () => {
31
+ const data = {
32
+ goalId: "g1",
33
+ objective: "test",
34
+ status: "active",
35
+ tasks: [],
36
+ stallCount: 0,
37
+ tokensUsed: 0,
38
+ timeStartedAt: 1000,
39
+ timeUsedSeconds: 0,
40
+ budget: { maxStallTurns: 5, maxTurns: 50 },
41
+ turnCount: 42,
42
+ };
43
+ const state = deserializeState(data);
44
+ expect(state.currentTurnIndex).toBe(0);
45
+ // turnCount 不存在于 GoalRuntimeState 接口
46
+ expect("turnCount" in state).toBe(false);
47
+ });
48
+
49
+ it("新数据有 verification 字段 → 正确还原", () => {
50
+ const data = {
51
+ goalId: "g1",
52
+ objective: "test",
53
+ status: "active",
54
+ tasks: [{
55
+ id: 1,
56
+ description: "task 1",
57
+ status: "completed",
58
+ lastUpdatedTurn: 5,
59
+ verification: { method: "pnpm test", expected: "all pass", actual: "passed" },
60
+ }],
61
+ stallCount: 0,
62
+ tokensUsed: 0,
63
+ timeStartedAt: 1000,
64
+ timeUsedSeconds: 0,
65
+ budget: { maxStallTurns: 5, maxTurns: 50 },
66
+ };
67
+ const state = deserializeState(data);
68
+ expect(state.tasks[0]!.verification).toEqual({
69
+ method: "pnpm test",
70
+ expected: "all pass",
71
+ actual: "passed",
72
+ });
73
+ });
74
+
75
+ it("旧数据 verified 状态的 task → 正常加载", () => {
76
+ const data = {
77
+ goalId: "g1",
78
+ objective: "test",
79
+ status: "active",
80
+ tasks: [{
81
+ id: 1,
82
+ description: "task 1",
83
+ status: "verified",
84
+ lastUpdatedTurn: 3,
85
+ }],
86
+ stallCount: 0,
87
+ tokensUsed: 0,
88
+ timeStartedAt: 1000,
89
+ timeUsedSeconds: 0,
90
+ budget: { maxStallTurns: 5, maxTurns: 50 },
91
+ };
92
+ const state = deserializeState(data);
93
+ expect(state.tasks[0]!.status).toBe("verified");
94
+ });
95
+
96
+ it("缺少字段时给默认值", () => {
97
+ const data = {
98
+ goalId: "g1",
99
+ objective: "test",
100
+ status: "active",
101
+ tasks: [],
102
+ };
103
+ const state = deserializeState(data);
104
+ expect(state.stallCount).toBe(0);
105
+ expect(state.tokensUsed).toBe(0);
106
+ expect(state.budget.maxTurns).toBe(50);
107
+ expect(state.budget.maxStallTurns).toBe(5);
108
+ });
109
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * P0 测试:isTaskDone — 业务语义的"完成"判定
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { type GoalTask, isTaskDone } from "../state";
7
+
8
+ const makeTask = (overrides: Partial<GoalTask> = {}): GoalTask => ({
9
+ id: 1,
10
+ description: "test task",
11
+ status: "pending",
12
+ lastUpdatedTurn: 0,
13
+ ...overrides,
14
+ });
15
+
16
+ describe("isTaskDone", () => {
17
+ it("verified task → done", () => {
18
+ expect(isTaskDone(makeTask({ status: "verified" }))).toBe(true);
19
+ });
20
+
21
+ it("cancelled task → done", () => {
22
+ expect(isTaskDone(makeTask({ status: "cancelled" }))).toBe(true);
23
+ });
24
+
25
+ it("completed without verification → done", () => {
26
+ expect(isTaskDone(makeTask({ status: "completed" }))).toBe(true);
27
+ });
28
+
29
+ it("completed with verification awaiting → NOT done", () => {
30
+ expect(isTaskDone(makeTask({
31
+ status: "completed",
32
+ verification: { method: "pnpm test", expected: "all pass" },
33
+ }))).toBe(false);
34
+ });
35
+
36
+ it("pending → NOT done", () => {
37
+ expect(isTaskDone(makeTask({ status: "pending" }))).toBe(false);
38
+ });
39
+
40
+ it("in_progress → NOT done", () => {
41
+ expect(isTaskDone(makeTask({ status: "in_progress" }))).toBe(false);
42
+ });
43
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Pi SDK stub for vitest — 提供运行时 mock,避免 import 真实的 Pi 包
3
+ */
4
+
5
+ // StringEnum: 返回 values 的 union type 的 runtime 值(简化为第一个元素)
6
+ export function StringEnum<T extends readonly string[]>(values: T, _options?: Record<string, unknown>): T[number] {
7
+ return values[0] as T[number];
8
+ }
9
+
10
+ // Pi SDK types used in import chains — re-export as empty
11
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * typebox stub for vitest — 只需要 Type.Object/String/Array/Number/Optional/StringEnum 等
3
+ * 被 tool-handler.ts import 链拉入,测试中不实际调用 schema 验证
4
+ */
5
+ export const Type = {
6
+ Object: (_schema: unknown) => ({}),
7
+ String: (_opts?: unknown) => ({}),
8
+ Number: (_opts?: unknown) => ({}),
9
+ Array: (_item: unknown, _opts?: unknown) => ([]),
10
+ Optional: (_item: unknown) => ({}),
11
+ Boolean: (_opts?: unknown) => ({}),
12
+ };
13
+
14
+ export type Static<_T> = Record<string, unknown>;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * P0 测试:validateUpdateTasks — 状态转换验证
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import { validateUpdateTasks } from "../action-handlers";
7
+ import { type GoalRuntimeState, type GoalTask } from "../state";
8
+
9
+ const makeState = (tasks: GoalTask[]): GoalRuntimeState => ({
10
+ goalId: "test",
11
+ objective: "test",
12
+ status: "active",
13
+ tasks,
14
+ stallCount: 0,
15
+ tokensUsed: 0,
16
+ timeStartedAt: Date.now(),
17
+ timeUsedSeconds: 0,
18
+ budget: { maxStallTurns: 5, maxTurns: 50 },
19
+ lastProgressTurn: 0,
20
+ budgetLimitSteeringSent: false,
21
+ objectiveUpdatedAt: Date.now(),
22
+ lastBlockerReason: null,
23
+ budgetWarning70Sent: false,
24
+ budgetWarning90Sent: false,
25
+ lastTurnTokensUsed: 0,
26
+ currentTurnIndex: 1,
27
+ });
28
+
29
+ const makeTask = (overrides: Partial<GoalTask> = {}): GoalTask => ({
30
+ id: 1,
31
+ description: "test task",
32
+ status: "pending",
33
+ lastUpdatedTurn: 0,
34
+ ...overrides,
35
+ });
36
+
37
+ describe("validateUpdateTasks — 合法转换", () => {
38
+ it("pending → in_progress ✓", () => {
39
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
40
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "in_progress" }]);
41
+ expect(err).toBeNull();
42
+ });
43
+
44
+ it("pending → cancelled ✓", () => {
45
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
46
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "cancelled" }]);
47
+ expect(err).toBeNull();
48
+ });
49
+
50
+ it("in_progress → completed (有 evidence) ✓", () => {
51
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
52
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "completed", evidence: "done" }]);
53
+ expect(err).toBeNull();
54
+ });
55
+
56
+ it("in_progress → cancelled ✓", () => {
57
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
58
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "cancelled" }]);
59
+ expect(err).toBeNull();
60
+ });
61
+
62
+ it("completed(有 verification) → verified (有 actual) ✓", () => {
63
+ const state = makeState([makeTask({
64
+ id: 1,
65
+ status: "completed",
66
+ verification: { method: "pnpm test", expected: "pass" },
67
+ })]);
68
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "verified", actual: "all pass" }]);
69
+ expect(err).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe("validateUpdateTasks — 非法转换", () => {
74
+ it("pending → completed (应先走 in_progress)", () => {
75
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
76
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "completed", evidence: "done" }]);
77
+ expect(err).not.toBeNull();
78
+ });
79
+
80
+ it("pending → verified (不能跳过)", () => {
81
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
82
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "verified", actual: "ok" }]);
83
+ expect(err).not.toBeNull();
84
+ });
85
+
86
+ it("in_progress → verified (不能跳过 completed)", () => {
87
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
88
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "verified", actual: "ok" }]);
89
+ expect(err).not.toBeNull();
90
+ });
91
+
92
+ it("in_progress → pending (不能回退)", () => {
93
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
94
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "pending" }]);
95
+ expect(err).not.toBeNull();
96
+ });
97
+
98
+ it("completed(无 verification) → 任何状态 (终态不可变)", () => {
99
+ const state = makeState([makeTask({ id: 1, status: "completed" })]);
100
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "in_progress" }]);
101
+ expect(err).not.toBeNull();
102
+ });
103
+
104
+ it("completed(有 verification) → 非 verified 状态", () => {
105
+ const state = makeState([makeTask({
106
+ id: 1,
107
+ status: "completed",
108
+ verification: { method: "pnpm test", expected: "pass" },
109
+ })]);
110
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "cancelled" }]);
111
+ expect(err).not.toBeNull();
112
+ });
113
+
114
+ it("verified → 任何状态 (终态不可变)", () => {
115
+ const state = makeState([makeTask({ id: 1, status: "verified" })]);
116
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "in_progress" }]);
117
+ expect(err).not.toBeNull();
118
+ });
119
+
120
+ it("cancelled → 任何状态 (终态不可变)", () => {
121
+ const state = makeState([makeTask({ id: 1, status: "cancelled" })]);
122
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "in_progress" }]);
123
+ expect(err).not.toBeNull();
124
+ });
125
+ });
126
+
127
+ describe("validateUpdateTasks — 必填检查", () => {
128
+ it("completed 缺 evidence → 错误", () => {
129
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
130
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "completed" }]);
131
+ expect(err).not.toBeNull();
132
+ });
133
+
134
+ it("completed evidence 为空字符串 → 错误", () => {
135
+ const state = makeState([makeTask({ id: 1, status: "in_progress" })]);
136
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "completed", evidence: " " }]);
137
+ expect(err).not.toBeNull();
138
+ });
139
+
140
+ it("verified 缺 actual → 错误", () => {
141
+ const state = makeState([makeTask({
142
+ id: 1,
143
+ status: "completed",
144
+ verification: { method: "pnpm test", expected: "pass" },
145
+ })]);
146
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "verified" }]);
147
+ expect(err).not.toBeNull();
148
+ });
149
+
150
+ it("verified 但 task 无 verification 配置 → 错误", () => {
151
+ const state = makeState([makeTask({ id: 1, status: "completed" })]);
152
+ const err = validateUpdateTasks(state, [{ taskId: 1, status: "verified", actual: "ok" }]);
153
+ expect(err).not.toBeNull();
154
+ });
155
+ });
156
+
157
+ describe("validateUpdateTasks — 其他检查", () => {
158
+ it("重复 taskId → 错误", () => {
159
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
160
+ const err = validateUpdateTasks(state, [
161
+ { taskId: 1, status: "in_progress" },
162
+ { taskId: 1, status: "cancelled" },
163
+ ]);
164
+ expect(err).not.toBeNull();
165
+ });
166
+
167
+ it("不存在的 taskId → 错误", () => {
168
+ const state = makeState([makeTask({ id: 1, status: "pending" })]);
169
+ const err = validateUpdateTasks(state, [{ taskId: 99, status: "in_progress" }]);
170
+ expect(err).not.toBeNull();
171
+ });
172
+ });
@@ -10,16 +10,19 @@
10
10
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
11
11
  import type { Static } from "typebox";
12
12
 
13
- import { SECONDS_PER_MINUTE } from "./constants";
13
+ import { ELLIPSIS_LENGTH, SECONDS_PER_MINUTE, TASK_DESC_MAX_LENGTH } from "./constants";
14
14
  import {
15
15
  getCompletedCount,
16
16
  getElapsedTimeSeconds,
17
17
  getIncompleteTasks,
18
+ getNextTaskId,
18
19
  type GoalRuntimeState,
19
20
  type GoalTask,
20
21
  isTerminalStatus,
21
22
  isTerminalTaskStatus,
23
+ isTaskDone,
22
24
  type Subtask,
25
+ type TaskVerification,
23
26
  transitionStatus,
24
27
  } from "./state";
25
28
  import { formatTaskList } from "./templates";
@@ -31,6 +34,7 @@ import {
31
34
  type GoalSession,
32
35
  makeGoalResult,
33
36
  persistGoalState,
37
+ sendGoalContextMessage,
34
38
  writeGoalHistoryEntry,
35
39
  } from "./tool-handler";
36
40
 
@@ -60,15 +64,17 @@ export const handleCreateTasks: ActionHandler = ({ state, params, pi, session, c
60
64
  `Already has ${state.tasks.length} tasks (${existingIncomplete.length} incomplete). Use add_tasks to append, or /goal update to re-plan.`,
61
65
  );
62
66
  }
67
+ const verifications = params.verifications;
63
68
  state.tasks = params.tasks.map((desc: string, i: number) => ({
64
69
  id: i + 1,
65
70
  description: normalizeDescription(desc),
66
71
  status: "pending" as const,
72
+ verification: verifications?.[i] as TaskVerification | undefined,
67
73
  lastUpdatedTurn: state.currentTurnIndex,
68
74
  }));
69
75
  persistGoalState(pi, session, ctx);
70
76
  return makeGoalResult(session,
71
- `Created ${state.tasks.length} tasks:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
77
+ `Created ${state.tasks.length} tasks:\n${state.tasks.map((t) => ` #${t.id}: ${t.description}${t.verification ? ` [验证: ${t.verification.method}]` : ""}`).join("\n")}`,
72
78
  );
73
79
  };
74
80
 
@@ -78,19 +84,19 @@ export const handleAddTasks: ActionHandler = ({ state, params, pi, session, ctx
78
84
  if (!params.tasks || params.tasks.length === 0) {
79
85
  return errorResult("add_tasks requires a non-empty tasks array");
80
86
  }
81
- const startId = state.tasks.length > 0
82
- ? Math.max(...state.tasks.map((t) => t.id)) + 1
83
- : 1;
87
+ const startId = getNextTaskId(state.tasks);
88
+ const verifications = params.verifications;
84
89
  const newTasks: GoalTask[] = params.tasks.map((desc: string, i: number) => ({
85
90
  id: startId + i,
86
91
  description: normalizeDescription(desc),
87
92
  status: "pending" as const,
93
+ verification: verifications?.[i] as TaskVerification | undefined,
88
94
  lastUpdatedTurn: state.currentTurnIndex,
89
95
  }));
90
96
  state.tasks.push(...newTasks);
91
97
  persistGoalState(pi, session, ctx);
92
98
  return makeGoalResult(session,
93
- `Appended ${newTasks.length} tasks:\n${newTasks.map((t) => ` #${t.id}: ${t.description}`).join("\n")}`,
99
+ `Appended ${newTasks.length} tasks:\n${newTasks.map((t) => ` #${t.id}: ${t.description}${t.verification ? ` [验证: ${t.verification.method}]` : ""}`).join("\n")}`,
94
100
  );
95
101
  };
96
102
 
@@ -102,7 +108,10 @@ export const handleUpdateTasks: ActionHandler = ({ state, params, pi, session, c
102
108
  }
103
109
  const validationErr = validateUpdateTasks(state, params.updates);
104
110
  if (validationErr) return validationErr;
111
+
105
112
  const results: string[] = [];
113
+ const tasksNeedingVerification: GoalTask[] = [];
114
+
106
115
  for (const u of params.updates) {
107
116
  const task = state.tasks.find((t) => t.id === u.taskId)!;
108
117
  const prev = task.status;
@@ -111,17 +120,41 @@ export const handleUpdateTasks: ActionHandler = ({ state, params, pi, session, c
111
120
  task.status = "completed";
112
121
  task.evidence = u.evidence;
113
122
  results.push(`#${task.id}: ${prev} → completed (${u.evidence})`);
123
+ if (task.verification) {
124
+ tasksNeedingVerification.push(task);
125
+ }
126
+ } else if (u.status === "verified") {
127
+ task.status = "verified";
128
+ task.verification!.actual = u.actual;
129
+ results.push(`#${task.id}: ${prev} → verified (actual: ${u.actual})`);
114
130
  } else {
115
131
  task.status = u.status;
116
132
  results.push(`#${task.id}: ${prev} → ${u.status}`);
117
133
  }
118
134
  }
135
+
119
136
  persistGoalState(pi, session, ctx);
120
- return makeGoalResult(session, `Updated ${results.length} tasks:\n${results.join("\n")}`);
137
+ if (tasksNeedingVerification.length > 0) {
138
+ injectVerificationSteering(pi, tasksNeedingVerification);
139
+ }
140
+ return makeGoalResult(session, `Updated ${results.length} task actions:\n${results.join("\n")}`);
121
141
  };
122
142
 
143
+ /** 注入 steering 提示 AI 对已完成的 task 执行验证命令。 */
144
+ function injectVerificationSteering(pi: ExtensionAPI, tasks: GoalTask[]): void {
145
+ const lines = tasks.map((t) =>
146
+ `Task #${t.id} requires verification. Run: ${t.verification!.method} (expected: ${t.verification!.expected})\n` +
147
+ `Then call update_tasks with taskId=${t.id}, status="verified", actual=<result>.`
148
+ ).join("\n\n");
149
+ sendGoalContextMessage(
150
+ pi,
151
+ `[GOAL Verification] Task(s) completed with verification pending:\n${lines}`,
152
+ "steer",
153
+ );
154
+ }
155
+
123
156
  /** 验证 update_tasks 的所有更新项;返回首个错误或 null。 */
124
- function validateUpdateTasks(state: GoalRuntimeState, updates: NonNullable<Static<typeof GoalManagerParams>["updates"]>) {
157
+ export function validateUpdateTasks(state: GoalRuntimeState, updates: NonNullable<Static<typeof GoalManagerParams>["updates"]>) {
125
158
  const taskIds = updates.map((u: { taskId: number }) => u.taskId);
126
159
  const duplicateIds = taskIds.filter((id: number, i: number) => taskIds.indexOf(id) !== i);
127
160
  if (duplicateIds.length > 0) {
@@ -130,12 +163,44 @@ function validateUpdateTasks(state: GoalRuntimeState, updates: NonNullable<Stati
130
163
  for (const u of updates) {
131
164
  const task = state.tasks.find((t) => t.id === u.taskId);
132
165
  if (!task) return errorResult(`Task #${u.taskId} not found`);
133
- if (isTerminalTaskStatus(task.status)) {
134
- return errorResult(`Task #${task.id} already in terminal state (${task.status}), cannot be changed`);
166
+
167
+ // 终态检查
168
+ if (task.status === "verified" || task.status === "cancelled") {
169
+ return errorResult(`Task #${task.id} in terminal state (${task.status}), cannot be changed`);
135
170
  }
171
+ // completed 无 verification:不可变更
172
+ if (task.status === "completed" && !task.verification) {
173
+ return errorResult(`Task #${task.id} already completed, cannot be changed`);
174
+ }
175
+ // completed 有 verification:只允许 verified
176
+ if (task.status === "completed" && task.verification && u.status !== "verified") {
177
+ return errorResult(`Task #${task.id} completed but requires verification. Call update_tasks with status=verified.`);
178
+ }
179
+
180
+ // 合法转换检查
181
+ const validNext: Record<string, Set<string>> = {
182
+ pending: new Set(["in_progress", "cancelled"]),
183
+ in_progress: new Set(["completed", "cancelled"]),
184
+ completed: new Set(["verified"]),
185
+ };
186
+ const allowed = validNext[task.status];
187
+ if (!allowed || !allowed.has(u.status)) {
188
+ const hints: Record<string, string> = { pending: "allowed: in_progress or cancelled", in_progress: "allowed: completed or cancelled", completed: "allowed: verified (only if task has verification config)" };
189
+ return errorResult(`Task #${task.id}: invalid transition ${task.status} → ${u.status}. From ${task.status}, ${hints[task.status] ?? "no transitions allowed"}`);
190
+ }
191
+
192
+ // completed 必须有 evidence
136
193
  if (u.status === "completed" && (!u.evidence || u.evidence.trim() === "")) {
137
194
  return errorResult(`Task #${task.id}: completed requires evidence`);
138
195
  }
196
+ // verified 必须有 actual
197
+ if (u.status === "verified" && (!u.actual || u.actual.trim() === "")) {
198
+ return errorResult(`Task #${task.id}: verified requires actual verification result`);
199
+ }
200
+ // verified 要求 task 有 verification 配置
201
+ if (u.status === "verified" && !task.verification) {
202
+ return errorResult(`Task #${task.id}: cannot verify a task without verification config`);
203
+ }
139
204
  }
140
205
  return null;
141
206
  }
@@ -155,14 +220,20 @@ export const handleCompleteGoal: ActionHandler = ({ state, params, pi, session,
155
220
  if (state.tasks.length === 0) {
156
221
  return errorResult("Create a task list with create_tasks before completing the goal.");
157
222
  }
158
- const incomplete = getIncompleteTasks(state.tasks);
159
- if (incomplete.length > 0) {
223
+ // 检查所有 task 都已完成(含 verified、cancelled、无 verification 的 completed)
224
+ const notDone = state.tasks.filter((t) => !isTaskDone(t));
225
+ if (notDone.length > 0) {
226
+ const hint = notDone.some((t) => t.status === "completed" && t.verification)
227
+ ? " Some completed tasks still require verification (status=verified)."
228
+ : "";
160
229
  return errorResult(
161
- `${incomplete.length} tasks still incomplete: ${incomplete.map((t) => `#${t.id}`).join(", ")}. Complete them first or explain why they don't need completion.`,
230
+ `${notDone.length} tasks not done: ${notDone.map((t) => `#${t.id}`).join(", ")}.${hint} Complete them first.`,
162
231
  );
163
232
  }
164
- if (getCompletedCount(state.tasks) === 0) {
165
- return errorResult("At least one task must be completed. All-cancelled does not count.");
233
+ // 至少一个 task 必须是 completed 或 verified(不能全 cancelled)
234
+ const completedOrVerified = state.tasks.filter((t) => t.status === "completed" || t.status === "verified");
235
+ if (completedOrVerified.length === 0) {
236
+ return errorResult("At least one task must be completed or verified. All-cancelled does not count.");
166
237
  }
167
238
  state.status = transitionStatus(state.status, "complete");
168
239
  state.completedAtTurnIndex = state.currentTurnIndex;
@@ -237,8 +308,10 @@ export const handleAddSubtasks: ActionHandler = ({ state, params, pi, session, c
237
308
  }
238
309
  const parentTask = state.tasks.find((t) => t.id === params.taskId);
239
310
  if (!parentTask) return errorResult(`Task #${params.taskId} not found`);
240
- if (isTerminalTaskStatus(parentTask.status)) {
241
- return errorResult(`Task #${parentTask.id} already in terminal state (${parentTask.status}), cannot add subtask`);
311
+ // isTerminalTaskStatus 不含 completed(completed 有 verification 时需转为 verified)
312
+ // 这里的 || completed 是有意的业务决策:completed 任务不允许加 subtask
313
+ if (isTerminalTaskStatus(parentTask.status) || parentTask.status === "completed") {
314
+ return errorResult(`Task #${parentTask.id} in terminal state (${parentTask.status}), cannot add subtask`);
242
315
  }
243
316
  const subtasks = parentTask.subtasks ?? [];
244
317
  const startId = subtasks.length > 0 ? Math.max(...subtasks.map((s) => s.id)) + 1 : 1;
@@ -316,10 +389,8 @@ export const handleDeleteSubtasks: ActionHandler = ({ state, params, pi, session
316
389
  /** 将 AI 传入的 task description 标准化:去换行、截断 */
317
390
  function normalizeDescription(desc: string): string {
318
391
  const singleLine = desc.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
319
- const ELLIPSIS_LENGTH = 3;
320
- const MAX_TASK_DESC_LENGTH = 80;
321
- if (singleLine.length > MAX_TASK_DESC_LENGTH) {
322
- return singleLine.slice(0, MAX_TASK_DESC_LENGTH - ELLIPSIS_LENGTH) + "...";
392
+ if (singleLine.length > TASK_DESC_MAX_LENGTH) {
393
+ return singleLine.slice(0, TASK_DESC_MAX_LENGTH - ELLIPSIS_LENGTH) + "...";
323
394
  }
324
395
  return singleLine;
325
396
  }