@zhushanwen/pi-goal 0.1.6 → 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 +8 -4
- package/src/__tests__/deserialize-state.test.ts +109 -0
- package/src/__tests__/is-task-done.test.ts +43 -0
- package/src/__tests__/stubs/pi-sdk.ts +11 -0
- package/src/__tests__/stubs/typebox.ts +14 -0
- package/src/__tests__/validate-update-tasks.test.ts +172 -0
- package/src/action-handlers.ts +92 -21
- package/src/agent-end-handler.ts +43 -41
- package/src/before-agent-start-handler.ts +4 -6
- package/src/budget.ts +3 -3
- package/src/command-handler.ts +32 -13
- package/src/commands.ts +4 -1
- package/src/constants.ts +5 -0
- package/src/index.ts +106 -36
- package/src/state.ts +51 -8
- package/src/templates.ts +59 -17
- package/src/tool-handler.ts +40 -1
- package/src/widget.ts +69 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-goal",
|
|
3
|
-
"version": "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
|
+
});
|
package/src/action-handlers.ts
CHANGED
|
@@ -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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
`${
|
|
230
|
+
`${notDone.length} tasks not done: ${notDone.map((t) => `#${t.id}`).join(", ")}.${hint} Complete them first.`,
|
|
162
231
|
);
|
|
163
232
|
}
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
}
|