patchwarden 0.6.4 → 1.5.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.
Files changed (254) hide show
  1. package/README.en.md +54 -20
  2. package/README.md +48 -21
  3. package/dist/agents/agentRouter.d.ts +40 -0
  4. package/dist/agents/agentRouter.js +95 -0
  5. package/dist/config.d.ts +1 -1
  6. package/dist/config.js +6 -2
  7. package/dist/controlCenter.js +212 -3
  8. package/dist/direct/directSessionStore.d.ts +2 -0
  9. package/dist/direct/directVerification.js +7 -0
  10. package/dist/doctor.js +32 -1
  11. package/dist/goal/acceptanceEngine.d.ts +62 -0
  12. package/dist/goal/acceptanceEngine.js +103 -0
  13. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  14. package/dist/goal/acceptanceTemplate.js +104 -0
  15. package/dist/goal/goalGraph.d.ts +58 -0
  16. package/dist/goal/goalGraph.js +189 -0
  17. package/dist/goal/goalProgress.d.ts +81 -0
  18. package/dist/goal/goalProgress.js +216 -0
  19. package/dist/goal/goalStatus.d.ts +60 -0
  20. package/dist/goal/goalStatus.js +137 -0
  21. package/dist/goal/goalStore.d.ts +79 -0
  22. package/dist/goal/goalStore.js +211 -0
  23. package/dist/goal/handoffExport.d.ts +34 -0
  24. package/dist/goal/handoffExport.js +183 -0
  25. package/dist/goal/subgoalSync.d.ts +40 -0
  26. package/dist/goal/subgoalSync.js +72 -0
  27. package/dist/goal/worktreeManager.d.ts +96 -0
  28. package/dist/goal/worktreeManager.js +292 -0
  29. package/dist/logging.d.ts +44 -0
  30. package/dist/logging.js +68 -0
  31. package/dist/policy/projectPolicy.d.ts +55 -0
  32. package/dist/policy/projectPolicy.js +286 -0
  33. package/dist/release/releaseGate.d.ts +99 -0
  34. package/dist/release/releaseGate.js +475 -0
  35. package/dist/runner/postTaskCleanup.d.ts +13 -0
  36. package/dist/runner/postTaskCleanup.js +147 -0
  37. package/dist/runner/runTask.js +50 -10
  38. package/dist/security/discoveryTokenStore.d.ts +62 -0
  39. package/dist/security/discoveryTokenStore.js +100 -0
  40. package/dist/security/toolInvocationGuard.d.ts +35 -0
  41. package/dist/security/toolInvocationGuard.js +153 -0
  42. package/dist/smoke-test.js +18 -10
  43. package/dist/taskRuntime.d.ts +17 -0
  44. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  45. package/dist/test/unit/acceptance-engine.test.js +228 -0
  46. package/dist/test/unit/agent-router.test.d.ts +1 -0
  47. package/dist/test/unit/agent-router.test.js +287 -0
  48. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  49. package/dist/test/unit/audit-checks.test.js +350 -0
  50. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  51. package/dist/test/unit/diagnose-task.test.js +457 -0
  52. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  53. package/dist/test/unit/discovery-token-store.test.js +139 -0
  54. package/dist/test/unit/evidence-pack.test.d.ts +1 -0
  55. package/dist/test/unit/evidence-pack.test.js +130 -0
  56. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  57. package/dist/test/unit/goal-graph.test.js +298 -0
  58. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  59. package/dist/test/unit/goal-progress.test.js +381 -0
  60. package/dist/test/unit/goal-status.test.d.ts +1 -0
  61. package/dist/test/unit/goal-status.test.js +215 -0
  62. package/dist/test/unit/goal-store.test.d.ts +1 -0
  63. package/dist/test/unit/goal-store.test.js +253 -0
  64. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  65. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  66. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  67. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  68. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  69. package/dist/test/unit/handoff-export.test.js +263 -0
  70. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  71. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  72. package/dist/test/unit/logging.test.js +127 -5
  73. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  74. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  75. package/dist/test/unit/project-policy-release-mode.test.d.ts +1 -0
  76. package/dist/test/unit/project-policy-release-mode.test.js +125 -0
  77. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  78. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  79. package/dist/test/unit/release-gate.test.d.ts +1 -0
  80. package/dist/test/unit/release-gate.test.js +242 -0
  81. package/dist/test/unit/run-task-loop.test.d.ts +1 -0
  82. package/dist/test/unit/run-task-loop.test.js +380 -0
  83. package/dist/test/unit/safe-views.test.d.ts +1 -0
  84. package/dist/test/unit/safe-views.test.js +171 -0
  85. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  86. package/dist/test/unit/schema-drift-check.test.js +176 -0
  87. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  88. package/dist/test/unit/subgoal-sync.test.js +183 -0
  89. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  90. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  91. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  92. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  93. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  94. package/dist/test/unit/toolSearch.test.js +571 -0
  95. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  96. package/dist/test/unit/worktree-manager.test.js +176 -0
  97. package/dist/tools/auditTask.d.ts +103 -1
  98. package/dist/tools/auditTask.js +461 -8
  99. package/dist/tools/cancelTask.js +1 -1
  100. package/dist/tools/checkReleaseGate.d.ts +21 -0
  101. package/dist/tools/checkReleaseGate.js +22 -0
  102. package/dist/tools/createTask.d.ts +18 -2
  103. package/dist/tools/createTask.js +29 -1
  104. package/dist/tools/diagnoseTask.d.ts +45 -0
  105. package/dist/tools/diagnoseTask.js +347 -0
  106. package/dist/tools/discardWorktree.d.ts +24 -0
  107. package/dist/tools/discardWorktree.js +27 -0
  108. package/dist/tools/discoverTools.d.ts +11 -0
  109. package/dist/tools/discoverTools.js +39 -0
  110. package/dist/tools/evidencePack.d.ts +39 -0
  111. package/dist/tools/evidencePack.js +168 -0
  112. package/dist/tools/explainTool.d.ts +11 -0
  113. package/dist/tools/explainTool.js +21 -0
  114. package/dist/tools/getTaskSummary.js +2 -1
  115. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  116. package/dist/tools/goalSubgoalTask.js +110 -0
  117. package/dist/tools/healthCheck.d.ts +1 -0
  118. package/dist/tools/healthCheck.js +5 -1
  119. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  120. package/dist/tools/invokeDiscoveredTool.js +112 -0
  121. package/dist/tools/listTasks.d.ts +3 -1
  122. package/dist/tools/listTasks.js +14 -1
  123. package/dist/tools/mergeWorktree.d.ts +24 -0
  124. package/dist/tools/mergeWorktree.js +27 -0
  125. package/dist/tools/recommendAgentForTask.d.ts +19 -0
  126. package/dist/tools/recommendAgentForTask.js +56 -0
  127. package/dist/tools/reconcileTasks.d.ts +41 -0
  128. package/dist/tools/reconcileTasks.js +352 -0
  129. package/dist/tools/registry.js +926 -2
  130. package/dist/tools/releaseMode.d.ts +50 -0
  131. package/dist/tools/releaseMode.js +370 -0
  132. package/dist/tools/runDirectVerificationBundle.d.ts +26 -0
  133. package/dist/tools/runDirectVerificationBundle.js +64 -0
  134. package/dist/tools/runTaskLoop.d.ts +57 -0
  135. package/dist/tools/runTaskLoop.js +417 -0
  136. package/dist/tools/runVerification.d.ts +4 -0
  137. package/dist/tools/runVerification.js +4 -0
  138. package/dist/tools/safeStatus.d.ts +31 -1
  139. package/dist/tools/safeStatus.js +43 -1
  140. package/dist/tools/safeViews.d.ts +262 -0
  141. package/dist/tools/safeViews.js +252 -0
  142. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  143. package/dist/tools/schemaDriftCheck.js +80 -0
  144. package/dist/tools/taskLineage.d.ts +91 -0
  145. package/dist/tools/taskLineage.js +175 -0
  146. package/dist/tools/toolCatalog.d.ts +4 -3
  147. package/dist/tools/toolCatalog.js +39 -11
  148. package/dist/tools/toolRegistry.d.ts +61 -0
  149. package/dist/tools/toolRegistry.js +834 -0
  150. package/dist/tools/toolSearch.d.ts +110 -0
  151. package/dist/tools/toolSearch.js +331 -0
  152. package/dist/tools/toolUsageStats.d.ts +41 -0
  153. package/dist/tools/toolUsageStats.js +116 -0
  154. package/dist/tools/waitForTask.js +1 -0
  155. package/dist/version.d.ts +2 -2
  156. package/dist/version.js +2 -2
  157. package/dist/watcherStatus.d.ts +29 -0
  158. package/dist/watcherStatus.js +92 -1
  159. package/docs/chatgpt-usage.md +31 -0
  160. package/docs/control-center/README.md +9 -0
  161. package/package.json +3 -2
  162. package/scripts/checks/control-center-smoke.js +87 -0
  163. package/scripts/checks/control-smoke.js +2 -2
  164. package/scripts/checks/doctor-smoke.js +1 -0
  165. package/scripts/checks/lifecycle-smoke.js +13 -9
  166. package/scripts/checks/mcp-manifest-check.js +65 -6
  167. package/scripts/checks/mcp-smoke.js +60 -9
  168. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  169. package/src/agents/agentRouter.ts +149 -0
  170. package/src/config.ts +9 -3
  171. package/src/controlCenter.ts +215 -4
  172. package/src/direct/directSessionStore.ts +2 -0
  173. package/src/direct/directVerification.ts +7 -0
  174. package/src/doctor.ts +36 -1
  175. package/src/goal/acceptanceEngine.ts +160 -0
  176. package/src/goal/acceptanceTemplate.ts +121 -0
  177. package/src/goal/goalGraph.ts +225 -0
  178. package/src/goal/goalProgress.ts +301 -0
  179. package/src/goal/goalStatus.ts +234 -0
  180. package/src/goal/goalStore.ts +306 -0
  181. package/src/goal/handoffExport.ts +211 -0
  182. package/src/goal/subgoalSync.ts +82 -0
  183. package/src/goal/worktreeManager.ts +404 -0
  184. package/src/logging.ts +91 -0
  185. package/src/policy/projectPolicy.ts +344 -0
  186. package/src/release/releaseGate.ts +567 -0
  187. package/src/runner/postTaskCleanup.ts +154 -0
  188. package/src/runner/runTask.ts +49 -10
  189. package/src/security/discoveryTokenStore.ts +157 -0
  190. package/src/security/toolInvocationGuard.ts +251 -0
  191. package/src/smoke-test.ts +15 -7
  192. package/src/taskRuntime.ts +17 -0
  193. package/src/test/unit/acceptance-engine.test.ts +261 -0
  194. package/src/test/unit/agent-router.test.ts +342 -0
  195. package/src/test/unit/audit-checks.test.ts +567 -0
  196. package/src/test/unit/diagnose-task.test.ts +544 -0
  197. package/src/test/unit/discovery-token-store.test.ts +181 -0
  198. package/src/test/unit/evidence-pack.test.ts +142 -0
  199. package/src/test/unit/goal-graph.test.ts +347 -0
  200. package/src/test/unit/goal-progress.test.ts +538 -0
  201. package/src/test/unit/goal-status.test.ts +270 -0
  202. package/src/test/unit/goal-store.test.ts +318 -0
  203. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  204. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  205. package/src/test/unit/handoff-export.test.ts +295 -0
  206. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  207. package/src/test/unit/logging.test.ts +177 -5
  208. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  209. package/src/test/unit/project-policy-release-mode.test.ts +156 -0
  210. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  211. package/src/test/unit/release-gate.test.ts +314 -0
  212. package/src/test/unit/run-task-loop.test.ts +425 -0
  213. package/src/test/unit/safe-views.test.ts +184 -0
  214. package/src/test/unit/schema-drift-check.test.ts +260 -0
  215. package/src/test/unit/subgoal-sync.test.ts +236 -0
  216. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  217. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  218. package/src/test/unit/toolSearch.test.ts +756 -0
  219. package/src/test/unit/worktree-manager.test.ts +247 -0
  220. package/src/tools/auditTask.ts +831 -402
  221. package/src/tools/cancelTask.ts +1 -1
  222. package/src/tools/checkReleaseGate.ts +45 -0
  223. package/src/tools/createTask.ts +64 -6
  224. package/src/tools/diagnoseTask.ts +460 -0
  225. package/src/tools/discardWorktree.ts +42 -0
  226. package/src/tools/discoverTools.ts +51 -0
  227. package/src/tools/evidencePack.ts +205 -0
  228. package/src/tools/explainTool.ts +26 -0
  229. package/src/tools/getTaskSummary.ts +2 -1
  230. package/src/tools/goalSubgoalTask.ts +170 -0
  231. package/src/tools/healthCheck.ts +4 -1
  232. package/src/tools/invokeDiscoveredTool.ts +163 -0
  233. package/src/tools/listTasks.ts +16 -2
  234. package/src/tools/listWorkspace.ts +71 -71
  235. package/src/tools/mergeWorktree.ts +42 -0
  236. package/src/tools/recommendAgentForTask.ts +79 -0
  237. package/src/tools/reconcileTasks.ts +464 -0
  238. package/src/tools/registry.ts +1093 -73
  239. package/src/tools/releaseMode.ts +450 -0
  240. package/src/tools/runDirectVerificationBundle.ts +98 -0
  241. package/src/tools/runTaskLoop.ts +526 -0
  242. package/src/tools/runVerification.ts +8 -0
  243. package/src/tools/safeStatus.ts +73 -2
  244. package/src/tools/safeViews.ts +273 -0
  245. package/src/tools/schemaDriftCheck.ts +120 -0
  246. package/src/tools/taskLineage.ts +300 -0
  247. package/src/tools/toolCatalog.ts +40 -11
  248. package/src/tools/toolRegistry.ts +896 -0
  249. package/src/tools/toolSearch.ts +464 -0
  250. package/src/tools/toolUsageStats.ts +151 -0
  251. package/src/tools/waitForTask.ts +1 -0
  252. package/src/version.ts +2 -2
  253. package/src/watcherStatus.ts +97 -1
  254. package/ui/pages/dashboard.html +143 -2
@@ -0,0 +1,538 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import { strict as assert } from "node:assert";
3
+ import {
4
+ mkdtempSync,
5
+ rmSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+ import {
12
+ createGoal,
13
+ readGoalStatus,
14
+ writeGoalStatus,
15
+ } from "../../goal/goalStore.js";
16
+ import {
17
+ addSubgoal,
18
+ updateSubgoalStatus,
19
+ linkTaskToSubgoal,
20
+ type SubgoalStatus,
21
+ } from "../../goal/goalStatus.js";
22
+ import {
23
+ acceptSubgoal,
24
+ rejectSubgoal,
25
+ summarizeGoalProgress,
26
+ } from "../../goal/goalProgress.js";
27
+ import { PatchWardenError } from "../../errors.js";
28
+
29
+ // ── Helpers ───────────────────────────────────────────────────────
30
+
31
+ let tempDir: string;
32
+
33
+ beforeEach(() => {
34
+ tempDir = mkdtempSync(join(tmpdir(), "pw-goalprogress-"));
35
+ });
36
+
37
+ afterEach(() => {
38
+ rmSync(tempDir, { recursive: true, force: true });
39
+ });
40
+
41
+ /**
42
+ * 在 {workspaceRoot}/.patchwarden/tasks/{taskId}/status.json 创建任务状态文件。
43
+ */
44
+ function createTestTask(workspaceRoot: string, taskId: string, status: string): void {
45
+ const taskDir = join(workspaceRoot, ".patchwarden", "tasks", taskId);
46
+ mkdirSync(taskDir, { recursive: true });
47
+ writeFileSync(
48
+ join(taskDir, "status.json"),
49
+ JSON.stringify({ task_id: taskId, status }),
50
+ "utf-8"
51
+ );
52
+ }
53
+
54
+ /**
55
+ * 创建 goal 并添加一个 subgoal,可选地设置状态和关联 task。
56
+ */
57
+ function setupGoalWithSubgoal(
58
+ workspaceRoot: string,
59
+ options: {
60
+ title?: string;
61
+ subgoalTitle?: string;
62
+ subgoalStatus?: SubgoalStatus;
63
+ taskIds?: string[];
64
+ dependsOn?: string[];
65
+ } = {}
66
+ ): { goal_id: string; subgoal_id: string } {
67
+ const { goal_id } = createGoal(
68
+ "repo",
69
+ options.title ?? "Test Goal",
70
+ "description",
71
+ workspaceRoot
72
+ );
73
+
74
+ let status = readGoalStatus(goal_id, workspaceRoot);
75
+ const addResult = addSubgoal(status, options.subgoalTitle ?? "Subgoal 1", options.dependsOn ?? []);
76
+ status = addResult.goalStatus;
77
+ const subgoalId = addResult.subgoalId;
78
+
79
+ // 关联 task
80
+ for (const taskId of options.taskIds ?? []) {
81
+ status = linkTaskToSubgoal(status, subgoalId, taskId);
82
+ }
83
+
84
+ // 推进到目标状态
85
+ const targetStatus = options.subgoalStatus ?? "ready";
86
+ if (targetStatus !== "ready") {
87
+ if (targetStatus === "running" || targetStatus === "done_by_agent" ||
88
+ targetStatus === "accepted" || targetStatus === "rejected" || targetStatus === "needs_fix") {
89
+ status = updateSubgoalStatus(status, subgoalId, "running");
90
+ if (targetStatus === "done_by_agent" || targetStatus === "accepted" ||
91
+ targetStatus === "rejected" || targetStatus === "needs_fix") {
92
+ status = updateSubgoalStatus(status, subgoalId, "done_by_agent");
93
+ if (targetStatus === "accepted") {
94
+ status = updateSubgoalStatus(status, subgoalId, "accepted");
95
+ } else if (targetStatus === "rejected") {
96
+ status = updateSubgoalStatus(status, subgoalId, "rejected", {
97
+ rejected_reason: "pre-set rejected",
98
+ });
99
+ } else if (targetStatus === "needs_fix") {
100
+ status = updateSubgoalStatus(status, subgoalId, "needs_fix");
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ writeGoalStatus(goal_id, status, workspaceRoot);
107
+ return { goal_id, subgoal_id: subgoalId };
108
+ }
109
+
110
+ // ── Tests ─────────────────────────────────────────────────────────
111
+
112
+ describe("goalProgress", () => {
113
+
114
+ describe("acceptSubgoal", () => {
115
+
116
+ it("所有关联 task 为 accepted → 成功,subgoal.status 变为 accepted", () => {
117
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
118
+ subgoalStatus: "done_by_agent",
119
+ taskIds: ["task-001", "task-002"],
120
+ });
121
+ createTestTask(tempDir, "task-001", "accepted");
122
+ createTestTask(tempDir, "task-002", "accepted");
123
+
124
+ const result = acceptSubgoal(goal_id, subgoal_id, tempDir);
125
+
126
+ assert.equal(result.subgoal_id, subgoal_id);
127
+ assert.equal(result.status, "accepted");
128
+ assert.ok(result.accepted_at.length > 0);
129
+
130
+ // 验证已持久化
131
+ const status = readGoalStatus(goal_id, tempDir);
132
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
133
+ assert.equal(sub.status, "accepted");
134
+ assert.equal(sub.accepted_at, result.accepted_at);
135
+ });
136
+
137
+ it("有未 accepted 的 task(done_by_agent)→ 抛 subgoal_not_ready,detail 含 unaccepted_tasks", () => {
138
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
139
+ subgoalStatus: "done_by_agent",
140
+ taskIds: ["task-001", "task-002"],
141
+ });
142
+ createTestTask(tempDir, "task-001", "accepted");
143
+ createTestTask(tempDir, "task-002", "done_by_agent");
144
+
145
+ assert.throws(
146
+ () => acceptSubgoal(goal_id, subgoal_id, tempDir),
147
+ (err: unknown) => {
148
+ assert.ok(err instanceof PatchWardenError);
149
+ assert.equal(err.reason, "subgoal_not_ready");
150
+ const details = err.details as { unaccepted_tasks: Array<{ task_id: string; current_status: string }> };
151
+ assert.ok(Array.isArray(details.unaccepted_tasks));
152
+ assert.equal(details.unaccepted_tasks.length, 1);
153
+ assert.equal(details.unaccepted_tasks[0].task_id, "task-002");
154
+ assert.equal(details.unaccepted_tasks[0].current_status, "done_by_agent");
155
+ return true;
156
+ }
157
+ );
158
+
159
+ // 验证状态未改变
160
+ const status = readGoalStatus(goal_id, tempDir);
161
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
162
+ assert.equal(sub.status, "done_by_agent");
163
+ });
164
+
165
+ it("无关联 task(task_ids 为空)→ 抛 subgoal_not_ready", () => {
166
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
167
+ subgoalStatus: "done_by_agent",
168
+ taskIds: [],
169
+ });
170
+
171
+ assert.throws(
172
+ () => acceptSubgoal(goal_id, subgoal_id, tempDir),
173
+ (err: unknown) => {
174
+ assert.ok(err instanceof PatchWardenError);
175
+ assert.equal(err.reason, "subgoal_not_ready");
176
+ return true;
177
+ }
178
+ );
179
+ });
180
+
181
+ it("subgoal 不存在 → 抛 subgoal_not_found", () => {
182
+ const { goal_id } = setupGoalWithSubgoal(tempDir, {
183
+ subgoalStatus: "done_by_agent",
184
+ taskIds: ["task-001"],
185
+ });
186
+ createTestTask(tempDir, "task-001", "accepted");
187
+
188
+ assert.throws(
189
+ () => acceptSubgoal(goal_id, "subgoal-999", tempDir),
190
+ (err: unknown) => {
191
+ assert.ok(err instanceof PatchWardenError);
192
+ assert.equal(err.reason, "subgoal_not_found");
193
+ return true;
194
+ }
195
+ );
196
+ });
197
+
198
+ it("goal 不存在 → 抛 goal_not_found", () => {
199
+ assert.throws(
200
+ () => acceptSubgoal("goal_nonexistent", "subgoal-001", tempDir),
201
+ (err: unknown) => {
202
+ assert.ok(err instanceof PatchWardenError);
203
+ assert.equal(err.reason, "goal_not_found");
204
+ return true;
205
+ }
206
+ );
207
+ });
208
+
209
+ it("task status.json 不存在 → 视为未 accepted,抛 subgoal_not_ready", () => {
210
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
211
+ subgoalStatus: "done_by_agent",
212
+ taskIds: ["task-missing"],
213
+ });
214
+ // 不创建 task 文件
215
+
216
+ assert.throws(
217
+ () => acceptSubgoal(goal_id, subgoal_id, tempDir),
218
+ (err: unknown) => {
219
+ assert.ok(err instanceof PatchWardenError);
220
+ assert.equal(err.reason, "subgoal_not_ready");
221
+ const details = err.details as { unaccepted_tasks: Array<{ task_id: string; current_status: string }> };
222
+ assert.equal(details.unaccepted_tasks[0].current_status, "missing");
223
+ return true;
224
+ }
225
+ );
226
+ });
227
+ });
228
+
229
+ describe("rejectSubgoal", () => {
230
+
231
+ it("从 running 状态 reject → 成功,status 变为 rejected,rejected_reason 记录", () => {
232
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
233
+ subgoalStatus: "running",
234
+ });
235
+
236
+ const result = rejectSubgoal(goal_id, subgoal_id, "Tests failed", tempDir);
237
+
238
+ assert.equal(result.subgoal_id, subgoal_id);
239
+ assert.equal(result.status, "rejected");
240
+ assert.equal(result.rejected_reason, "Tests failed");
241
+
242
+ // 验证已持久化
243
+ const status = readGoalStatus(goal_id, tempDir);
244
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
245
+ assert.equal(sub.status, "rejected");
246
+ assert.equal(sub.rejected_reason, "Tests failed");
247
+ });
248
+
249
+ it("从 ready 状态 reject → 成功", () => {
250
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
251
+ subgoalStatus: "ready",
252
+ });
253
+
254
+ const result = rejectSubgoal(goal_id, subgoal_id, "Not needed", tempDir);
255
+
256
+ assert.equal(result.status, "rejected");
257
+ assert.equal(result.rejected_reason, "Not needed");
258
+
259
+ const status = readGoalStatus(goal_id, tempDir);
260
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
261
+ assert.equal(sub.status, "rejected");
262
+ });
263
+
264
+ it("从 done_by_agent 状态 reject → 成功", () => {
265
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
266
+ subgoalStatus: "done_by_agent",
267
+ });
268
+
269
+ const result = rejectSubgoal(goal_id, subgoal_id, "Quality issues", tempDir);
270
+
271
+ assert.equal(result.status, "rejected");
272
+ assert.equal(result.rejected_reason, "Quality issues");
273
+
274
+ const status = readGoalStatus(goal_id, tempDir);
275
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
276
+ assert.equal(sub.status, "rejected");
277
+ });
278
+
279
+ it("从 needs_fix 状态 reject → 成功", () => {
280
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
281
+ subgoalStatus: "needs_fix",
282
+ });
283
+
284
+ const result = rejectSubgoal(goal_id, subgoal_id, "Abandoning fix", tempDir);
285
+
286
+ assert.equal(result.status, "rejected");
287
+ assert.equal(result.rejected_reason, "Abandoning fix");
288
+ });
289
+
290
+ it("从 accepted 状态 reject → 抛 invalid_status_transition", () => {
291
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
292
+ subgoalStatus: "accepted",
293
+ });
294
+
295
+ assert.throws(
296
+ () => rejectSubgoal(goal_id, subgoal_id, "Try to reject", tempDir),
297
+ (err: unknown) => {
298
+ assert.ok(err instanceof PatchWardenError);
299
+ assert.equal(err.reason, "invalid_status_transition");
300
+ const details = err.details as { from_status: string; to_status: string };
301
+ assert.equal(details.from_status, "accepted");
302
+ assert.equal(details.to_status, "rejected");
303
+ return true;
304
+ }
305
+ );
306
+
307
+ // 验证状态未改变
308
+ const status = readGoalStatus(goal_id, tempDir);
309
+ const sub = status.subgoals.find((s) => s.id === subgoal_id)!;
310
+ assert.equal(sub.status, "accepted");
311
+ });
312
+
313
+ it("从 rejected 状态 reject → 抛 invalid_status_transition", () => {
314
+ const { goal_id, subgoal_id } = setupGoalWithSubgoal(tempDir, {
315
+ subgoalStatus: "rejected",
316
+ });
317
+
318
+ assert.throws(
319
+ () => rejectSubgoal(goal_id, subgoal_id, "Reject again", tempDir),
320
+ (err: unknown) => {
321
+ assert.ok(err instanceof PatchWardenError);
322
+ assert.equal(err.reason, "invalid_status_transition");
323
+ const details = err.details as { from_status: string };
324
+ assert.equal(details.from_status, "rejected");
325
+ return true;
326
+ }
327
+ );
328
+ });
329
+
330
+ it("subgoal 不存在 → 抛 subgoal_not_found", () => {
331
+ const { goal_id } = setupGoalWithSubgoal(tempDir);
332
+
333
+ assert.throws(
334
+ () => rejectSubgoal(goal_id, "subgoal-999", "reason", tempDir),
335
+ (err: unknown) => {
336
+ assert.ok(err instanceof PatchWardenError);
337
+ assert.equal(err.reason, "subgoal_not_found");
338
+ return true;
339
+ }
340
+ );
341
+ });
342
+
343
+ it("goal 不存在 → 抛 goal_not_found", () => {
344
+ assert.throws(
345
+ () => rejectSubgoal("goal_nonexistent", "subgoal-001", "reason", tempDir),
346
+ (err: unknown) => {
347
+ assert.ok(err instanceof PatchWardenError);
348
+ assert.equal(err.reason, "goal_not_found");
349
+ return true;
350
+ }
351
+ );
352
+ });
353
+ });
354
+
355
+ describe("summarizeGoalProgress", () => {
356
+
357
+ it("空 subgoals → total=0, completion_rate=0%", () => {
358
+ const { goal_id } = createGoal("repo", "Empty Goal", "desc", tempDir);
359
+
360
+ const summary = summarizeGoalProgress(goal_id, tempDir);
361
+
362
+ assert.equal(summary.goal_id, goal_id);
363
+ assert.equal(summary.title, "Empty Goal");
364
+ assert.equal(summary.total, 0);
365
+ assert.equal(summary.accepted, 0);
366
+ assert.equal(summary.rejected, 0);
367
+ assert.equal(summary.running, 0);
368
+ assert.equal(summary.ready, 0);
369
+ assert.equal(summary.needs_fix, 0);
370
+ assert.equal(summary.done_by_agent, 0);
371
+ assert.equal(summary.completion_rate, "0%");
372
+ assert.deepEqual(summary.blocked_subgoals, []);
373
+ assert.deepEqual(summary.risks, []);
374
+ });
375
+
376
+ it("混合状态 → 正确计数", () => {
377
+ const { goal_id } = createGoal("repo", "Mixed Goal", "desc", tempDir);
378
+ let status = readGoalStatus(goal_id, tempDir);
379
+
380
+ // subgoal-001: accepted
381
+ status = addSubgoal(status, "S1").goalStatus;
382
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
383
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
384
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
385
+
386
+ // subgoal-002: running
387
+ status = addSubgoal(status, "S2").goalStatus;
388
+ status = updateSubgoalStatus(status, "subgoal-002", "running");
389
+
390
+ // subgoal-003: ready
391
+ status = addSubgoal(status, "S3").goalStatus;
392
+
393
+ // subgoal-004: rejected
394
+ status = addSubgoal(status, "S4").goalStatus;
395
+ status = updateSubgoalStatus(status, "subgoal-004", "running");
396
+ status = updateSubgoalStatus(status, "subgoal-004", "done_by_agent");
397
+ status = updateSubgoalStatus(status, "subgoal-004", "rejected", { rejected_reason: "bad" });
398
+
399
+ // subgoal-005: needs_fix
400
+ status = addSubgoal(status, "S5").goalStatus;
401
+ status = updateSubgoalStatus(status, "subgoal-005", "running");
402
+ status = updateSubgoalStatus(status, "subgoal-005", "done_by_agent");
403
+ status = updateSubgoalStatus(status, "subgoal-005", "needs_fix");
404
+
405
+ // subgoal-006: done_by_agent
406
+ status = addSubgoal(status, "S6").goalStatus;
407
+ status = updateSubgoalStatus(status, "subgoal-006", "running");
408
+ status = updateSubgoalStatus(status, "subgoal-006", "done_by_agent");
409
+
410
+ writeGoalStatus(goal_id, status, tempDir);
411
+
412
+ const summary = summarizeGoalProgress(goal_id, tempDir);
413
+
414
+ assert.equal(summary.total, 6);
415
+ assert.equal(summary.accepted, 1);
416
+ assert.equal(summary.rejected, 1);
417
+ assert.equal(summary.running, 1);
418
+ assert.equal(summary.ready, 1);
419
+ assert.equal(summary.needs_fix, 1);
420
+ assert.equal(summary.done_by_agent, 1);
421
+ // 1/6 = 16.67% → round to 17%
422
+ assert.equal(summary.completion_rate, "17%");
423
+ });
424
+
425
+ it("有 blocked subgoal → blocked_subgoals 非空", () => {
426
+ const { goal_id } = createGoal("repo", "Blocked Goal", "desc", tempDir);
427
+ let status = readGoalStatus(goal_id, tempDir);
428
+
429
+ // subgoal-001: running(未 accepted,作为阻塞源)
430
+ status = addSubgoal(status, "Blocker").goalStatus;
431
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
432
+
433
+ // subgoal-002: ready 但依赖 subgoal-001(未 accepted)→ blocked
434
+ status = addSubgoal(status, "Blocked", ["subgoal-001"]).goalStatus;
435
+
436
+ writeGoalStatus(goal_id, status, tempDir);
437
+
438
+ const summary = summarizeGoalProgress(goal_id, tempDir);
439
+
440
+ assert.equal(summary.blocked_subgoals.length, 1);
441
+ assert.equal(summary.blocked_subgoals[0].subgoal_id, "subgoal-002");
442
+ assert.equal(summary.blocked_subgoals[0].title, "Blocked");
443
+ assert.deepEqual(summary.blocked_subgoals[0].blocked_by, ["subgoal-001"]);
444
+ });
445
+
446
+ it("有 needs_fix → risks 非空", () => {
447
+ const { goal_id } = createGoal("repo", "Risk Goal", "desc", tempDir);
448
+ let status = readGoalStatus(goal_id, tempDir);
449
+
450
+ status = addSubgoal(status, "NeedsFix").goalStatus;
451
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
452
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
453
+ status = updateSubgoalStatus(status, "subgoal-001", "needs_fix");
454
+
455
+ writeGoalStatus(goal_id, status, tempDir);
456
+
457
+ const summary = summarizeGoalProgress(goal_id, tempDir);
458
+
459
+ assert.equal(summary.risks.length, 1);
460
+ assert.equal(summary.risks[0].subgoal_id, "subgoal-001");
461
+ assert.equal(summary.risks[0].title, "NeedsFix");
462
+ assert.equal(summary.risks[0].status, "needs_fix");
463
+ assert.equal(summary.risks[0].reason, "needs_fix");
464
+ });
465
+
466
+ it("有 running → risks 非空", () => {
467
+ const { goal_id } = createGoal("repo", "Running Goal", "desc", tempDir);
468
+ let status = readGoalStatus(goal_id, tempDir);
469
+
470
+ status = addSubgoal(status, "Running").goalStatus;
471
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
472
+
473
+ writeGoalStatus(goal_id, status, tempDir);
474
+
475
+ const summary = summarizeGoalProgress(goal_id, tempDir);
476
+
477
+ assert.equal(summary.risks.length, 1);
478
+ assert.equal(summary.risks[0].subgoal_id, "subgoal-001");
479
+ assert.equal(summary.risks[0].status, "running");
480
+ assert.equal(summary.risks[0].reason, "running");
481
+ });
482
+
483
+ it("全部 accepted → completion_rate=100%", () => {
484
+ const { goal_id } = createGoal("repo", "All Accepted", "desc", tempDir);
485
+ let status = readGoalStatus(goal_id, tempDir);
486
+
487
+ status = addSubgoal(status, "S1").goalStatus;
488
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
489
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
490
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
491
+
492
+ status = addSubgoal(status, "S2").goalStatus;
493
+ status = updateSubgoalStatus(status, "subgoal-002", "running");
494
+ status = updateSubgoalStatus(status, "subgoal-002", "done_by_agent");
495
+ status = updateSubgoalStatus(status, "subgoal-002", "accepted");
496
+
497
+ writeGoalStatus(goal_id, status, tempDir);
498
+
499
+ const summary = summarizeGoalProgress(goal_id, tempDir);
500
+
501
+ assert.equal(summary.total, 2);
502
+ assert.equal(summary.accepted, 2);
503
+ assert.equal(summary.completion_rate, "100%");
504
+ assert.equal(summary.risks.length, 0);
505
+ assert.equal(summary.blocked_subgoals.length, 0);
506
+ });
507
+
508
+ it("goal 不存在 → 抛 goal_not_found", () => {
509
+ assert.throws(
510
+ () => summarizeGoalProgress("goal_nonexistent", tempDir),
511
+ (err: unknown) => {
512
+ assert.ok(err instanceof PatchWardenError);
513
+ assert.equal(err.reason, "goal_not_found");
514
+ return true;
515
+ }
516
+ );
517
+ });
518
+
519
+ it("completion_rate 取整正确(1/3 → 33%)", () => {
520
+ const { goal_id } = createGoal("repo", "Third Goal", "desc", tempDir);
521
+ let status = readGoalStatus(goal_id, tempDir);
522
+
523
+ // 1 accepted, 2 ready
524
+ status = addSubgoal(status, "S1").goalStatus;
525
+ status = updateSubgoalStatus(status, "subgoal-001", "running");
526
+ status = updateSubgoalStatus(status, "subgoal-001", "done_by_agent");
527
+ status = updateSubgoalStatus(status, "subgoal-001", "accepted");
528
+
529
+ status = addSubgoal(status, "S2").goalStatus;
530
+ status = addSubgoal(status, "S3").goalStatus;
531
+
532
+ writeGoalStatus(goal_id, status, tempDir);
533
+
534
+ const summary = summarizeGoalProgress(goal_id, tempDir);
535
+ assert.equal(summary.completion_rate, "33%");
536
+ });
537
+ });
538
+ });