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,58 @@
1
+ /**
2
+ * v0.8.0: Goal Session 依赖图 — 就绪/阻塞判定、下一子目标建议、环检测、拓扑排序。
3
+ *
4
+ * 所有函数均为纯函数,不修改传入的 goalStatus 对象。
5
+ * 依赖语义:subgoal.depends_on 引用其他 subgoal 的 id,被引用者 status 必须为
6
+ * "accepted" 才算依赖满足。引用不存在的 id 视为数据不一致,按阻塞处理。
7
+ */
8
+ import type { GoalStatus, Subgoal } from "./goalStatus.js";
9
+ /**
10
+ * 返回所有 status 为 "ready" 且依赖全部满足的子目标。
11
+ * 依赖满足 = depends_on 中所有 id 都存在且对应 subgoal status 为 "accepted"。
12
+ * depends_on 为空数组视为依赖满足。
13
+ * depends_on 引用不存在的 subgoal 时,该子目标不算 ready(跳过)。
14
+ */
15
+ export declare function getReadySubgoals(goalStatus: GoalStatus): Subgoal[];
16
+ /**
17
+ * 返回所有 status 为 "ready" 但依赖未全部满足的子目标,附带阻塞它的依赖 id 列表。
18
+ * blocked_by 包含 depends_on 中 status 不为 "accepted" 的 subgoal_id,
19
+ * 以及引用不存在的 id。
20
+ */
21
+ export declare function getBlockedSubgoals(goalStatus: GoalStatus): Array<{
22
+ subgoal: Subgoal;
23
+ blocked_by: string[];
24
+ }>;
25
+ /**
26
+ * 建议下一个可执行的子目标。
27
+ *
28
+ * 优先级:
29
+ * 1. 有 ready 子目标 → 返回第一个(按 subgoal 添加顺序)的 { subgoal_id, title, depends_on }
30
+ * 2. 无 ready 但有 blocked → 返回 { subgoal_id: null, reason: "dependencies_not_met", blocked_by: [...] }
31
+ * 3. 无 ready 无 blocked → 返回 { subgoal_id: null, reason: "no_ready_subgoal" }
32
+ */
33
+ export declare function suggestNextSubgoal(goalStatus: GoalStatus): {
34
+ subgoal_id: string | null;
35
+ title?: string;
36
+ depends_on?: string[];
37
+ reason?: string;
38
+ blocked_by?: string[];
39
+ };
40
+ /**
41
+ * 检测 subgoal 依赖图是否有环(DFS 三色标记法)。
42
+ *
43
+ * 沿 depends_on 深度优先遍历,遇到当前路径上正在访问的节点即为环。
44
+ * depends_on 引用不存在的 subgoal 不算环(忽略)。
45
+ *
46
+ * @returns 有环时返回环上的 subgoal_id 数组(任一环即可);无环返回 null。
47
+ */
48
+ export declare function detectCycle(goalStatus: GoalStatus): string[] | null;
49
+ /**
50
+ * 对 subgoal 做拓扑排序(依赖在前)。
51
+ *
52
+ * 如果 B depends_on A,则 A 排在 B 之前。
53
+ * depends_on 引用不存在的 id 会被忽略(不影响排序)。
54
+ *
55
+ * @throws {PatchWardenError} reason="dependency_cycle" 当依赖图存在环时。
56
+ * @returns subgoal_id 数组,依赖项在前。
57
+ */
58
+ export declare function topologicalSort(goalStatus: GoalStatus): string[];
@@ -0,0 +1,189 @@
1
+ /**
2
+ * v0.8.0: Goal Session 依赖图 — 就绪/阻塞判定、下一子目标建议、环检测、拓扑排序。
3
+ *
4
+ * 所有函数均为纯函数,不修改传入的 goalStatus 对象。
5
+ * 依赖语义:subgoal.depends_on 引用其他 subgoal 的 id,被引用者 status 必须为
6
+ * "accepted" 才算依赖满足。引用不存在的 id 视为数据不一致,按阻塞处理。
7
+ */
8
+ import { PatchWardenError } from "../errors.js";
9
+ // ── 内部工具 ──────────────────────────────────────────────────────
10
+ /**
11
+ * 构建 subgoal_id → Subgoal 的查找表。
12
+ */
13
+ function buildSubgoalMap(goalStatus) {
14
+ const map = new Map();
15
+ for (const s of goalStatus.subgoals) {
16
+ map.set(s.id, s);
17
+ }
18
+ return map;
19
+ }
20
+ /**
21
+ * 判断单个 subgoal 的依赖是否全部满足(所有 depends_on 引用存在且 status 为 accepted)。
22
+ * depends_on 为空视为依赖满足。引用不存在的 id 视为未满足。
23
+ */
24
+ function areDependenciesMet(subgoal, subgoalMap) {
25
+ for (const depId of subgoal.depends_on) {
26
+ const dep = subgoalMap.get(depId);
27
+ if (!dep)
28
+ return false;
29
+ if (dep.status !== "accepted")
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ // ── 公共 API ──────────────────────────────────────────────────────
35
+ /**
36
+ * 返回所有 status 为 "ready" 且依赖全部满足的子目标。
37
+ * 依赖满足 = depends_on 中所有 id 都存在且对应 subgoal status 为 "accepted"。
38
+ * depends_on 为空数组视为依赖满足。
39
+ * depends_on 引用不存在的 subgoal 时,该子目标不算 ready(跳过)。
40
+ */
41
+ export function getReadySubgoals(goalStatus) {
42
+ const subgoalMap = buildSubgoalMap(goalStatus);
43
+ return goalStatus.subgoals.filter((s) => s.status === "ready" && areDependenciesMet(s, subgoalMap));
44
+ }
45
+ /**
46
+ * 返回所有 status 为 "ready" 但依赖未全部满足的子目标,附带阻塞它的依赖 id 列表。
47
+ * blocked_by 包含 depends_on 中 status 不为 "accepted" 的 subgoal_id,
48
+ * 以及引用不存在的 id。
49
+ */
50
+ export function getBlockedSubgoals(goalStatus) {
51
+ const subgoalMap = buildSubgoalMap(goalStatus);
52
+ const result = [];
53
+ for (const s of goalStatus.subgoals) {
54
+ if (s.status !== "ready")
55
+ continue;
56
+ const blockedBy = [];
57
+ for (const depId of s.depends_on) {
58
+ const dep = subgoalMap.get(depId);
59
+ if (!dep) {
60
+ blockedBy.push(depId);
61
+ }
62
+ else if (dep.status !== "accepted") {
63
+ blockedBy.push(depId);
64
+ }
65
+ }
66
+ if (blockedBy.length > 0) {
67
+ result.push({ subgoal: s, blocked_by: blockedBy });
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ /**
73
+ * 建议下一个可执行的子目标。
74
+ *
75
+ * 优先级:
76
+ * 1. 有 ready 子目标 → 返回第一个(按 subgoal 添加顺序)的 { subgoal_id, title, depends_on }
77
+ * 2. 无 ready 但有 blocked → 返回 { subgoal_id: null, reason: "dependencies_not_met", blocked_by: [...] }
78
+ * 3. 无 ready 无 blocked → 返回 { subgoal_id: null, reason: "no_ready_subgoal" }
79
+ */
80
+ export function suggestNextSubgoal(goalStatus) {
81
+ const ready = getReadySubgoals(goalStatus);
82
+ if (ready.length > 0) {
83
+ const first = ready[0];
84
+ return {
85
+ subgoal_id: first.id,
86
+ title: first.title,
87
+ depends_on: [...first.depends_on],
88
+ };
89
+ }
90
+ const blocked = getBlockedSubgoals(goalStatus);
91
+ if (blocked.length > 0) {
92
+ return {
93
+ subgoal_id: null,
94
+ reason: "dependencies_not_met",
95
+ blocked_by: blocked.map((b) => b.subgoal.id),
96
+ };
97
+ }
98
+ return {
99
+ subgoal_id: null,
100
+ reason: "no_ready_subgoal",
101
+ };
102
+ }
103
+ /**
104
+ * 检测 subgoal 依赖图是否有环(DFS 三色标记法)。
105
+ *
106
+ * 沿 depends_on 深度优先遍历,遇到当前路径上正在访问的节点即为环。
107
+ * depends_on 引用不存在的 subgoal 不算环(忽略)。
108
+ *
109
+ * @returns 有环时返回环上的 subgoal_id 数组(任一环即可);无环返回 null。
110
+ */
111
+ export function detectCycle(goalStatus) {
112
+ const subgoalMap = buildSubgoalMap(goalStatus);
113
+ const WHITE = 0;
114
+ const GRAY = 1;
115
+ const BLACK = 2;
116
+ const color = new Map();
117
+ for (const s of goalStatus.subgoals) {
118
+ color.set(s.id, WHITE);
119
+ }
120
+ let cycle = null;
121
+ const dfs = (id, path) => {
122
+ color.set(id, GRAY);
123
+ path.push(id);
124
+ const node = subgoalMap.get(id);
125
+ if (node) {
126
+ for (const depId of node.depends_on) {
127
+ if (!subgoalMap.has(depId))
128
+ continue; // 引用不存在,忽略
129
+ const depColor = color.get(depId);
130
+ if (depColor === GRAY) {
131
+ // 发现环:截取从依赖节点到当前节点的路径
132
+ const cycleStart = path.indexOf(depId);
133
+ cycle = path.slice(cycleStart);
134
+ return true;
135
+ }
136
+ if (depColor === WHITE) {
137
+ if (dfs(depId, path))
138
+ return true;
139
+ }
140
+ }
141
+ }
142
+ path.pop();
143
+ color.set(id, BLACK);
144
+ return false;
145
+ };
146
+ for (const s of goalStatus.subgoals) {
147
+ if (color.get(s.id) === WHITE) {
148
+ if (dfs(s.id, []))
149
+ return cycle;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ /**
155
+ * 对 subgoal 做拓扑排序(依赖在前)。
156
+ *
157
+ * 如果 B depends_on A,则 A 排在 B 之前。
158
+ * depends_on 引用不存在的 id 会被忽略(不影响排序)。
159
+ *
160
+ * @throws {PatchWardenError} reason="dependency_cycle" 当依赖图存在环时。
161
+ * @returns subgoal_id 数组,依赖项在前。
162
+ */
163
+ export function topologicalSort(goalStatus) {
164
+ const cycle = detectCycle(goalStatus);
165
+ if (cycle) {
166
+ throw new PatchWardenError("dependency_cycle", `Dependency cycle detected among subgoals: ${cycle.join(" -> ")}`, "Remove the circular dependency among subgoals before performing topological sort.", true, { cycle });
167
+ }
168
+ const subgoalMap = buildSubgoalMap(goalStatus);
169
+ const visited = new Set();
170
+ const result = [];
171
+ const visit = (id) => {
172
+ if (visited.has(id))
173
+ return;
174
+ visited.add(id);
175
+ const node = subgoalMap.get(id);
176
+ if (node) {
177
+ for (const depId of node.depends_on) {
178
+ if (subgoalMap.has(depId)) {
179
+ visit(depId);
180
+ }
181
+ }
182
+ }
183
+ result.push(id);
184
+ };
185
+ for (const s of goalStatus.subgoals) {
186
+ visit(s.id);
187
+ }
188
+ return result;
189
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * v0.8.0: Goal Session 子目标验收业务逻辑 — accept / reject / summarize。
3
+ *
4
+ * 三个公共函数:
5
+ * - acceptSubgoal:校验所有关联 task 已 accepted 后,将 subgoal 置为 accepted
6
+ * - rejectSubgoal:从任意非终态拒绝 subgoal,记录 rejected_reason
7
+ * - summarizeGoalProgress:统计 goal 进度,返回 completion_rate / blocked / risks
8
+ *
9
+ * 任务 status.json 路径解析:
10
+ * 优先用 getConfig().tasksDir(绝对路径直接用,相对路径 join workspaceRoot)。
11
+ * 所有函数接受可选的 workspaceRoot 参数用于测试。
12
+ */
13
+ export interface GoalProgressSummary {
14
+ goal_id: string;
15
+ title: string;
16
+ total: number;
17
+ accepted: number;
18
+ rejected: number;
19
+ running: number;
20
+ ready: number;
21
+ needs_fix: number;
22
+ done_by_agent: number;
23
+ completion_rate: string;
24
+ blocked_subgoals: Array<{
25
+ subgoal_id: string;
26
+ title: string;
27
+ blocked_by: string[];
28
+ }>;
29
+ risks: Array<{
30
+ subgoal_id: string;
31
+ title: string;
32
+ status: string;
33
+ reason: string;
34
+ }>;
35
+ }
36
+ /**
37
+ * 验收子目标:校验所有关联 task 已 accepted 后,将 subgoal 置为 accepted。
38
+ *
39
+ * 校验规则(理解 B — 更严格):
40
+ * 1. task_ids 不能为空(无关联任务无法验收)
41
+ * 2. 所有 task_ids 对应的 task status 必须为 "accepted"
42
+ * 3. 通过 updateSubgoalStatus 状态机校验(要求 subgoal 当前为 done_by_agent)
43
+ *
44
+ * @param goalId Goal 标识
45
+ * @param subgoalId Subgoal 标识
46
+ * @param workspaceRoot 可选,用于测试
47
+ * @returns { subgoal_id, status, accepted_at }
48
+ * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "subgoal_not_ready" | "invalid_status_transition"
49
+ */
50
+ export declare function acceptSubgoal(goalId: string, subgoalId: string, workspaceRoot?: string): {
51
+ subgoal_id: string;
52
+ status: "accepted";
53
+ accepted_at: string;
54
+ };
55
+ /**
56
+ * 拒绝子目标:从任意非终态(ready/running/done_by_agent/needs_fix)拒绝 subgoal。
57
+ *
58
+ * 不使用 updateSubgoalStatus 的状态机校验(因为状态机只允许 done_by_agent → rejected),
59
+ * 直接手动设置 status 和 rejected_reason。
60
+ *
61
+ * @param goalId Goal 标识
62
+ * @param subgoalId Subgoal 标识
63
+ * @param reason 拒绝原因
64
+ * @param workspaceRoot 可选,用于测试
65
+ * @returns { subgoal_id, status, rejected_reason }
66
+ * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "invalid_status_transition"
67
+ */
68
+ export declare function rejectSubgoal(goalId: string, subgoalId: string, reason: string, workspaceRoot?: string): {
69
+ subgoal_id: string;
70
+ status: "rejected";
71
+ rejected_reason: string;
72
+ };
73
+ /**
74
+ * 汇总 Goal 进度:统计各状态数量、完成率、阻塞子目标、风险项。
75
+ *
76
+ * @param goalId Goal 标识
77
+ * @param workspaceRoot 可选,用于测试
78
+ * @returns GoalProgressSummary
79
+ * @throws {PatchWardenError} reason="goal_not_found"
80
+ */
81
+ export declare function summarizeGoalProgress(goalId: string, workspaceRoot?: string): GoalProgressSummary;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * v0.8.0: Goal Session 子目标验收业务逻辑 — accept / reject / summarize。
3
+ *
4
+ * 三个公共函数:
5
+ * - acceptSubgoal:校验所有关联 task 已 accepted 后,将 subgoal 置为 accepted
6
+ * - rejectSubgoal:从任意非终态拒绝 subgoal,记录 rejected_reason
7
+ * - summarizeGoalProgress:统计 goal 进度,返回 completion_rate / blocked / risks
8
+ *
9
+ * 任务 status.json 路径解析:
10
+ * 优先用 getConfig().tasksDir(绝对路径直接用,相对路径 join workspaceRoot)。
11
+ * 所有函数接受可选的 workspaceRoot 参数用于测试。
12
+ */
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { isAbsolute, join } from "node:path";
15
+ import { getConfig } from "../config.js";
16
+ import { PatchWardenError } from "../errors.js";
17
+ import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
18
+ import { updateSubgoalStatus } from "./goalStatus.js";
19
+ import { getBlockedSubgoals } from "./goalGraph.js";
20
+ // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
21
+ function resolveWorkspaceRoot(workspaceRoot) {
22
+ return workspaceRoot ?? getConfig().workspaceRoot;
23
+ }
24
+ /**
25
+ * 解析任务 status.json 路径。
26
+ * 读取 getConfig().tasksDir:绝对路径直接用,相对路径 join workspaceRoot。
27
+ */
28
+ function resolveTaskStatusPath(taskId, workspaceRoot) {
29
+ const config = getConfig();
30
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
31
+ const tasksDir = config.tasksDir;
32
+ const resolvedTasksDir = isAbsolute(tasksDir) ? tasksDir : join(wsRoot, tasksDir);
33
+ return join(resolvedTasksDir, taskId, "status.json");
34
+ }
35
+ /**
36
+ * 读取单个任务的 status 字段。
37
+ * 如果 status.json 不存在或无法解析,返回 null(视为未 accepted)。
38
+ */
39
+ function readTaskStatus(taskId, workspaceRoot) {
40
+ const statusPath = resolveTaskStatusPath(taskId, workspaceRoot);
41
+ if (!existsSync(statusPath)) {
42
+ return null;
43
+ }
44
+ try {
45
+ const raw = readFileSync(statusPath, "utf-8");
46
+ const parsed = JSON.parse(raw);
47
+ return typeof parsed.status === "string" ? parsed.status : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ // ── 公共 API ──────────────────────────────────────────────────────
54
+ /**
55
+ * 验收子目标:校验所有关联 task 已 accepted 后,将 subgoal 置为 accepted。
56
+ *
57
+ * 校验规则(理解 B — 更严格):
58
+ * 1. task_ids 不能为空(无关联任务无法验收)
59
+ * 2. 所有 task_ids 对应的 task status 必须为 "accepted"
60
+ * 3. 通过 updateSubgoalStatus 状态机校验(要求 subgoal 当前为 done_by_agent)
61
+ *
62
+ * @param goalId Goal 标识
63
+ * @param subgoalId Subgoal 标识
64
+ * @param workspaceRoot 可选,用于测试
65
+ * @returns { subgoal_id, status, accepted_at }
66
+ * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "subgoal_not_ready" | "invalid_status_transition"
67
+ */
68
+ export function acceptSubgoal(goalId, subgoalId, workspaceRoot) {
69
+ const goalStatus = readGoalStatus(goalId, workspaceRoot);
70
+ const subgoal = goalStatus.subgoals.find((s) => s.id === subgoalId);
71
+ if (!subgoal) {
72
+ throw new PatchWardenError("subgoal_not_found", `Subgoal "${subgoalId}" not found in goal "${goalId}"`, "Ensure the subgoal id exists before accepting.", true, { subgoal_id: subgoalId, goal_id: goalId });
73
+ }
74
+ // 校验 1:task_ids 不能为空
75
+ if (subgoal.task_ids.length === 0) {
76
+ throw new PatchWardenError("subgoal_not_ready", `Subgoal "${subgoalId}" has no associated tasks; cannot accept without task verification.`, "Link tasks to the subgoal and run audit_task on each before accepting.", true, { subgoal_id: subgoalId, goal_id: goalId, task_ids: [] });
77
+ }
78
+ // 校验 2:所有 task 必须为 accepted
79
+ const unacceptedTasks = [];
80
+ for (const taskId of subgoal.task_ids) {
81
+ const taskStatus = readTaskStatus(taskId, workspaceRoot);
82
+ if (taskStatus !== "accepted") {
83
+ unacceptedTasks.push({
84
+ task_id: taskId,
85
+ current_status: taskStatus ?? "missing",
86
+ });
87
+ }
88
+ }
89
+ if (unacceptedTasks.length > 0) {
90
+ throw new PatchWardenError("subgoal_not_ready", `Subgoal "${subgoalId}" cannot be accepted: ${unacceptedTasks.length} task(s) are not in "accepted" status.`, "Run audit_task on each unaccepted task first.", true, {
91
+ subgoal_id: subgoalId,
92
+ goal_id: goalId,
93
+ unaccepted_tasks: unacceptedTasks,
94
+ });
95
+ }
96
+ // 校验 3:通过状态机转换(要求 subgoal 当前为 done_by_agent)
97
+ const updatedGoalStatus = updateSubgoalStatus(goalStatus, subgoalId, "accepted");
98
+ writeGoalStatus(goalId, updatedGoalStatus, workspaceRoot);
99
+ const updatedSubgoal = updatedGoalStatus.subgoals.find((s) => s.id === subgoalId);
100
+ return {
101
+ subgoal_id: subgoalId,
102
+ status: "accepted",
103
+ accepted_at: updatedSubgoal.accepted_at,
104
+ };
105
+ }
106
+ /**
107
+ * 拒绝子目标:从任意非终态(ready/running/done_by_agent/needs_fix)拒绝 subgoal。
108
+ *
109
+ * 不使用 updateSubgoalStatus 的状态机校验(因为状态机只允许 done_by_agent → rejected),
110
+ * 直接手动设置 status 和 rejected_reason。
111
+ *
112
+ * @param goalId Goal 标识
113
+ * @param subgoalId Subgoal 标识
114
+ * @param reason 拒绝原因
115
+ * @param workspaceRoot 可选,用于测试
116
+ * @returns { subgoal_id, status, rejected_reason }
117
+ * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "invalid_status_transition"
118
+ */
119
+ export function rejectSubgoal(goalId, subgoalId, reason, workspaceRoot) {
120
+ const goalStatus = readGoalStatus(goalId, workspaceRoot);
121
+ const index = goalStatus.subgoals.findIndex((s) => s.id === subgoalId);
122
+ if (index === -1) {
123
+ throw new PatchWardenError("subgoal_not_found", `Subgoal "${subgoalId}" not found in goal "${goalId}"`, "Ensure the subgoal id exists before rejecting.", true, { subgoal_id: subgoalId, goal_id: goalId });
124
+ }
125
+ const current = goalStatus.subgoals[index];
126
+ // 终态检查:accepted 和 rejected 不允许再 reject
127
+ if (current.status === "accepted" || current.status === "rejected") {
128
+ throw new PatchWardenError("invalid_status_transition", `Cannot reject subgoal "${subgoalId}" in terminal state "${current.status}".`, "Reject is only allowed from non-terminal states (ready, running, done_by_agent, needs_fix).", true, {
129
+ subgoal_id: subgoalId,
130
+ goal_id: goalId,
131
+ from_status: current.status,
132
+ to_status: "rejected",
133
+ });
134
+ }
135
+ // 手动更新 status 和 rejected_reason(绕过状态机)
136
+ const updatedSubgoal = {
137
+ ...current,
138
+ status: "rejected",
139
+ rejected_reason: reason,
140
+ };
141
+ const newSubgoals = [...goalStatus.subgoals];
142
+ newSubgoals[index] = updatedSubgoal;
143
+ const updatedGoalStatus = {
144
+ ...goalStatus,
145
+ subgoals: newSubgoals,
146
+ updated_at: new Date().toISOString(),
147
+ };
148
+ writeGoalStatus(goalId, updatedGoalStatus, workspaceRoot);
149
+ return {
150
+ subgoal_id: subgoalId,
151
+ status: "rejected",
152
+ rejected_reason: reason,
153
+ };
154
+ }
155
+ /**
156
+ * 汇总 Goal 进度:统计各状态数量、完成率、阻塞子目标、风险项。
157
+ *
158
+ * @param goalId Goal 标识
159
+ * @param workspaceRoot 可选,用于测试
160
+ * @returns GoalProgressSummary
161
+ * @throws {PatchWardenError} reason="goal_not_found"
162
+ */
163
+ export function summarizeGoalProgress(goalId, workspaceRoot) {
164
+ const goalStatus = readGoalStatus(goalId, workspaceRoot);
165
+ const subgoals = goalStatus.subgoals;
166
+ const total = subgoals.length;
167
+ const accepted = subgoals.filter((s) => s.status === "accepted").length;
168
+ const rejected = subgoals.filter((s) => s.status === "rejected").length;
169
+ const running = subgoals.filter((s) => s.status === "running").length;
170
+ const ready = subgoals.filter((s) => s.status === "ready").length;
171
+ const needsFix = subgoals.filter((s) => s.status === "needs_fix").length;
172
+ const doneByAgent = subgoals.filter((s) => s.status === "done_by_agent").length;
173
+ const completionRate = total > 0
174
+ ? `${Math.round((accepted / total) * 100)}%`
175
+ : "0%";
176
+ // 阻塞子目标
177
+ const blockedSubgoals = getBlockedSubgoals(goalStatus).map((b) => ({
178
+ subgoal_id: b.subgoal.id,
179
+ title: b.subgoal.title,
180
+ blocked_by: [...b.blocked_by],
181
+ }));
182
+ // 风险项:needs_fix 和 running
183
+ const risks = [];
184
+ for (const s of subgoals) {
185
+ if (s.status === "needs_fix") {
186
+ risks.push({
187
+ subgoal_id: s.id,
188
+ title: s.title,
189
+ status: s.status,
190
+ reason: "needs_fix",
191
+ });
192
+ }
193
+ else if (s.status === "running") {
194
+ risks.push({
195
+ subgoal_id: s.id,
196
+ title: s.title,
197
+ status: s.status,
198
+ reason: "running",
199
+ });
200
+ }
201
+ }
202
+ return {
203
+ goal_id: goalStatus.goal_id,
204
+ title: goalStatus.title,
205
+ total,
206
+ accepted,
207
+ rejected,
208
+ running,
209
+ ready,
210
+ needs_fix: needsFix,
211
+ done_by_agent: doneByAgent,
212
+ completion_rate: completionRate,
213
+ blocked_subgoals: blockedSubgoals,
214
+ risks,
215
+ };
216
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * v0.8.0: Goal Session 子目标状态机 — 不可变更新 + 状态机校验。
3
+ *
4
+ * 类型定义:SubgoalStatus / Subgoal / GoalStatus
5
+ * 状态机合法转换:
6
+ * ready → running
7
+ * running → done_by_agent
8
+ * done_by_agent → accepted | rejected | needs_fix
9
+ * needs_fix → running
10
+ *
11
+ * 所有更新函数返回新的 GoalStatus 对象,不修改原对象。
12
+ */
13
+ export type SubgoalStatus = "ready" | "running" | "done_by_agent" | "accepted" | "rejected" | "needs_fix";
14
+ export interface Subgoal {
15
+ id: string;
16
+ title: string;
17
+ status: SubgoalStatus;
18
+ depends_on: string[];
19
+ task_ids: string[];
20
+ accepted_at?: string;
21
+ rejected_reason?: string;
22
+ }
23
+ export interface GoalStatus {
24
+ goal_id: string;
25
+ title: string;
26
+ status: "active" | "completed" | "abandoned";
27
+ repo_path: string;
28
+ created_at: string;
29
+ updated_at: string;
30
+ subgoals: Subgoal[];
31
+ }
32
+ /**
33
+ * 创建初始 GoalStatus,用于新 goal。
34
+ * status 为 "active",subgoals 为空数组,时间戳为当前 ISO 时间。
35
+ */
36
+ export declare function createInitialGoalStatus(goalId: string, title: string, repoPath: string): GoalStatus;
37
+ /**
38
+ * 向 goalStatus 添加一个新 subgoal。
39
+ * subgoal_id 格式:`subgoal-{NNN}`,NNN 三位补零,从 001 开始递增。
40
+ * 校验 dependsOn 中每个 id 是否存在于现有 subgoals。
41
+ * 不可变更新:返回新的 GoalStatus 对象。
42
+ */
43
+ export declare function addSubgoal(goalStatus: GoalStatus, title: string, dependsOn?: string[]): {
44
+ goalStatus: GoalStatus;
45
+ subgoalId: string;
46
+ };
47
+ /**
48
+ * 更新指定 subgoal 的状态,校验状态机合法转换。
49
+ * - accepted:设置 accepted_at
50
+ * - rejected:需要 options.rejected_reason
51
+ * 不可变更新:返回新的 GoalStatus 对象。
52
+ */
53
+ export declare function updateSubgoalStatus(goalStatus: GoalStatus, subgoalId: string, newStatus: SubgoalStatus, options?: {
54
+ rejected_reason?: string;
55
+ }): GoalStatus;
56
+ /**
57
+ * 将 taskId 关联到指定 subgoal(去重)。
58
+ * 不可变更新:返回新的 GoalStatus 对象。
59
+ */
60
+ export declare function linkTaskToSubgoal(goalStatus: GoalStatus, subgoalId: string, taskId: string): GoalStatus;