patchwarden 0.6.4 → 1.1.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 (212) hide show
  1. package/README.en.md +18 -18
  2. package/README.md +20 -20
  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 +15 -2
  8. package/dist/doctor.js +32 -1
  9. package/dist/goal/acceptanceEngine.d.ts +62 -0
  10. package/dist/goal/acceptanceEngine.js +103 -0
  11. package/dist/goal/acceptanceTemplate.d.ts +10 -0
  12. package/dist/goal/acceptanceTemplate.js +104 -0
  13. package/dist/goal/goalGraph.d.ts +58 -0
  14. package/dist/goal/goalGraph.js +189 -0
  15. package/dist/goal/goalProgress.d.ts +81 -0
  16. package/dist/goal/goalProgress.js +216 -0
  17. package/dist/goal/goalStatus.d.ts +60 -0
  18. package/dist/goal/goalStatus.js +137 -0
  19. package/dist/goal/goalStore.d.ts +79 -0
  20. package/dist/goal/goalStore.js +211 -0
  21. package/dist/goal/handoffExport.d.ts +34 -0
  22. package/dist/goal/handoffExport.js +183 -0
  23. package/dist/goal/subgoalSync.d.ts +40 -0
  24. package/dist/goal/subgoalSync.js +72 -0
  25. package/dist/goal/worktreeManager.d.ts +96 -0
  26. package/dist/goal/worktreeManager.js +292 -0
  27. package/dist/logging.d.ts +44 -0
  28. package/dist/logging.js +68 -0
  29. package/dist/release/releaseGate.d.ts +99 -0
  30. package/dist/release/releaseGate.js +475 -0
  31. package/dist/runner/postTaskCleanup.d.ts +13 -0
  32. package/dist/runner/postTaskCleanup.js +147 -0
  33. package/dist/runner/runTask.js +50 -10
  34. package/dist/security/discoveryTokenStore.d.ts +62 -0
  35. package/dist/security/discoveryTokenStore.js +100 -0
  36. package/dist/security/toolInvocationGuard.d.ts +35 -0
  37. package/dist/security/toolInvocationGuard.js +153 -0
  38. package/dist/smoke-test.js +18 -10
  39. package/dist/taskRuntime.d.ts +17 -0
  40. package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
  41. package/dist/test/unit/acceptance-engine.test.js +228 -0
  42. package/dist/test/unit/agent-router.test.d.ts +1 -0
  43. package/dist/test/unit/agent-router.test.js +287 -0
  44. package/dist/test/unit/audit-checks.test.d.ts +1 -0
  45. package/dist/test/unit/audit-checks.test.js +350 -0
  46. package/dist/test/unit/diagnose-task.test.d.ts +1 -0
  47. package/dist/test/unit/diagnose-task.test.js +457 -0
  48. package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
  49. package/dist/test/unit/discovery-token-store.test.js +139 -0
  50. package/dist/test/unit/goal-graph.test.d.ts +1 -0
  51. package/dist/test/unit/goal-graph.test.js +298 -0
  52. package/dist/test/unit/goal-progress.test.d.ts +1 -0
  53. package/dist/test/unit/goal-progress.test.js +381 -0
  54. package/dist/test/unit/goal-status.test.d.ts +1 -0
  55. package/dist/test/unit/goal-status.test.js +215 -0
  56. package/dist/test/unit/goal-store.test.d.ts +1 -0
  57. package/dist/test/unit/goal-store.test.js +253 -0
  58. package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
  59. package/dist/test/unit/goal-subgoal-task.test.js +55 -0
  60. package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
  61. package/dist/test/unit/goal-tools-registry.test.js +190 -0
  62. package/dist/test/unit/handoff-export.test.d.ts +1 -0
  63. package/dist/test/unit/handoff-export.test.js +263 -0
  64. package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
  65. package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
  66. package/dist/test/unit/logging.test.js +127 -5
  67. package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
  68. package/dist/test/unit/post-task-cleanup.test.js +48 -0
  69. package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
  70. package/dist/test/unit/reconcile-tasks.test.js +456 -0
  71. package/dist/test/unit/release-gate.test.d.ts +1 -0
  72. package/dist/test/unit/release-gate.test.js +242 -0
  73. package/dist/test/unit/safe-views.test.d.ts +1 -0
  74. package/dist/test/unit/safe-views.test.js +171 -0
  75. package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
  76. package/dist/test/unit/schema-drift-check.test.js +175 -0
  77. package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
  78. package/dist/test/unit/subgoal-sync.test.js +183 -0
  79. package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
  80. package/dist/test/unit/tool-invocation-guard.test.js +432 -0
  81. package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
  82. package/dist/test/unit/tool-usage-stats.test.js +300 -0
  83. package/dist/test/unit/toolSearch.test.d.ts +1 -0
  84. package/dist/test/unit/toolSearch.test.js +571 -0
  85. package/dist/test/unit/worktree-manager.test.d.ts +1 -0
  86. package/dist/test/unit/worktree-manager.test.js +176 -0
  87. package/dist/tools/auditTask.d.ts +103 -1
  88. package/dist/tools/auditTask.js +461 -8
  89. package/dist/tools/cancelTask.js +1 -1
  90. package/dist/tools/checkReleaseGate.d.ts +21 -0
  91. package/dist/tools/checkReleaseGate.js +22 -0
  92. package/dist/tools/createTask.d.ts +18 -2
  93. package/dist/tools/createTask.js +29 -1
  94. package/dist/tools/diagnoseTask.d.ts +45 -0
  95. package/dist/tools/diagnoseTask.js +347 -0
  96. package/dist/tools/discardWorktree.d.ts +24 -0
  97. package/dist/tools/discardWorktree.js +27 -0
  98. package/dist/tools/discoverTools.d.ts +11 -0
  99. package/dist/tools/discoverTools.js +39 -0
  100. package/dist/tools/explainTool.d.ts +11 -0
  101. package/dist/tools/explainTool.js +21 -0
  102. package/dist/tools/getTaskSummary.js +2 -1
  103. package/dist/tools/goalSubgoalTask.d.ts +51 -0
  104. package/dist/tools/goalSubgoalTask.js +110 -0
  105. package/dist/tools/healthCheck.d.ts +1 -0
  106. package/dist/tools/healthCheck.js +5 -1
  107. package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
  108. package/dist/tools/invokeDiscoveredTool.js +112 -0
  109. package/dist/tools/listTasks.d.ts +3 -1
  110. package/dist/tools/listTasks.js +14 -1
  111. package/dist/tools/mergeWorktree.d.ts +24 -0
  112. package/dist/tools/mergeWorktree.js +27 -0
  113. package/dist/tools/reconcileTasks.d.ts +41 -0
  114. package/dist/tools/reconcileTasks.js +352 -0
  115. package/dist/tools/registry.js +550 -0
  116. package/dist/tools/safeStatus.d.ts +31 -1
  117. package/dist/tools/safeStatus.js +43 -1
  118. package/dist/tools/safeViews.d.ts +256 -0
  119. package/dist/tools/safeViews.js +250 -0
  120. package/dist/tools/schemaDriftCheck.d.ts +46 -0
  121. package/dist/tools/schemaDriftCheck.js +80 -0
  122. package/dist/tools/toolCatalog.d.ts +4 -3
  123. package/dist/tools/toolCatalog.js +33 -11
  124. package/dist/tools/toolRegistry.d.ts +61 -0
  125. package/dist/tools/toolRegistry.js +724 -0
  126. package/dist/tools/toolSearch.d.ts +110 -0
  127. package/dist/tools/toolSearch.js +331 -0
  128. package/dist/tools/toolUsageStats.d.ts +41 -0
  129. package/dist/tools/toolUsageStats.js +116 -0
  130. package/dist/tools/waitForTask.js +1 -0
  131. package/dist/version.d.ts +2 -2
  132. package/dist/version.js +2 -2
  133. package/dist/watcherStatus.d.ts +29 -0
  134. package/dist/watcherStatus.js +92 -1
  135. package/package.json +3 -2
  136. package/scripts/checks/control-smoke.js +2 -2
  137. package/scripts/checks/doctor-smoke.js +1 -0
  138. package/scripts/checks/lifecycle-smoke.js +13 -9
  139. package/scripts/checks/mcp-manifest-check.js +53 -6
  140. package/scripts/checks/mcp-smoke.js +36 -9
  141. package/scripts/checks/watcher-supervisor-smoke.js +2 -2
  142. package/src/agents/agentRouter.ts +149 -0
  143. package/src/config.ts +9 -3
  144. package/src/controlCenter.ts +17 -3
  145. package/src/doctor.ts +36 -1
  146. package/src/goal/acceptanceEngine.ts +160 -0
  147. package/src/goal/acceptanceTemplate.ts +121 -0
  148. package/src/goal/goalGraph.ts +225 -0
  149. package/src/goal/goalProgress.ts +301 -0
  150. package/src/goal/goalStatus.ts +234 -0
  151. package/src/goal/goalStore.ts +306 -0
  152. package/src/goal/handoffExport.ts +211 -0
  153. package/src/goal/subgoalSync.ts +82 -0
  154. package/src/goal/worktreeManager.ts +404 -0
  155. package/src/logging.ts +91 -0
  156. package/src/release/releaseGate.ts +567 -0
  157. package/src/runner/postTaskCleanup.ts +154 -0
  158. package/src/runner/runTask.ts +49 -10
  159. package/src/security/discoveryTokenStore.ts +157 -0
  160. package/src/security/toolInvocationGuard.ts +251 -0
  161. package/src/smoke-test.ts +15 -7
  162. package/src/taskRuntime.ts +17 -0
  163. package/src/test/unit/acceptance-engine.test.ts +261 -0
  164. package/src/test/unit/agent-router.test.ts +342 -0
  165. package/src/test/unit/audit-checks.test.ts +567 -0
  166. package/src/test/unit/diagnose-task.test.ts +544 -0
  167. package/src/test/unit/discovery-token-store.test.ts +181 -0
  168. package/src/test/unit/goal-graph.test.ts +347 -0
  169. package/src/test/unit/goal-progress.test.ts +538 -0
  170. package/src/test/unit/goal-status.test.ts +270 -0
  171. package/src/test/unit/goal-store.test.ts +318 -0
  172. package/src/test/unit/goal-subgoal-task.test.ts +72 -0
  173. package/src/test/unit/goal-tools-registry.test.ts +243 -0
  174. package/src/test/unit/handoff-export.test.ts +295 -0
  175. package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
  176. package/src/test/unit/logging.test.ts +177 -5
  177. package/src/test/unit/post-task-cleanup.test.ts +53 -0
  178. package/src/test/unit/reconcile-tasks.test.ts +551 -0
  179. package/src/test/unit/release-gate.test.ts +314 -0
  180. package/src/test/unit/safe-views.test.ts +184 -0
  181. package/src/test/unit/schema-drift-check.test.ts +258 -0
  182. package/src/test/unit/subgoal-sync.test.ts +236 -0
  183. package/src/test/unit/tool-invocation-guard.test.ts +542 -0
  184. package/src/test/unit/tool-usage-stats.test.ts +384 -0
  185. package/src/test/unit/toolSearch.test.ts +756 -0
  186. package/src/test/unit/worktree-manager.test.ts +247 -0
  187. package/src/tools/auditTask.ts +831 -402
  188. package/src/tools/cancelTask.ts +1 -1
  189. package/src/tools/checkReleaseGate.ts +45 -0
  190. package/src/tools/createTask.ts +64 -6
  191. package/src/tools/diagnoseTask.ts +460 -0
  192. package/src/tools/discardWorktree.ts +42 -0
  193. package/src/tools/discoverTools.ts +51 -0
  194. package/src/tools/explainTool.ts +26 -0
  195. package/src/tools/getTaskSummary.ts +2 -1
  196. package/src/tools/goalSubgoalTask.ts +170 -0
  197. package/src/tools/healthCheck.ts +4 -1
  198. package/src/tools/invokeDiscoveredTool.ts +163 -0
  199. package/src/tools/listTasks.ts +16 -2
  200. package/src/tools/mergeWorktree.ts +42 -0
  201. package/src/tools/reconcileTasks.ts +464 -0
  202. package/src/tools/registry.ts +618 -1
  203. package/src/tools/safeStatus.ts +73 -2
  204. package/src/tools/safeViews.ts +271 -0
  205. package/src/tools/schemaDriftCheck.ts +120 -0
  206. package/src/tools/toolCatalog.ts +34 -11
  207. package/src/tools/toolRegistry.ts +786 -0
  208. package/src/tools/toolSearch.ts +464 -0
  209. package/src/tools/toolUsageStats.ts +151 -0
  210. package/src/tools/waitForTask.ts +1 -0
  211. package/src/version.ts +2 -2
  212. package/src/watcherStatus.ts +97 -1
@@ -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;
@@ -0,0 +1,137 @@
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
+ import { PatchWardenError } from "../errors.js";
14
+ // ── 合法状态转换表 ────────────────────────────────────────────────
15
+ const LEGAL_TRANSITIONS = {
16
+ ready: ["running"],
17
+ running: ["done_by_agent"],
18
+ done_by_agent: ["accepted", "rejected", "needs_fix"],
19
+ accepted: [],
20
+ rejected: [],
21
+ needs_fix: ["running"],
22
+ };
23
+ // ── 函数实现 ──────────────────────────────────────────────────────
24
+ /**
25
+ * 创建初始 GoalStatus,用于新 goal。
26
+ * status 为 "active",subgoals 为空数组,时间戳为当前 ISO 时间。
27
+ */
28
+ export function createInitialGoalStatus(goalId, title, repoPath) {
29
+ const now = new Date().toISOString();
30
+ return {
31
+ goal_id: goalId,
32
+ title,
33
+ status: "active",
34
+ repo_path: repoPath,
35
+ created_at: now,
36
+ updated_at: now,
37
+ subgoals: [],
38
+ };
39
+ }
40
+ /**
41
+ * 向 goalStatus 添加一个新 subgoal。
42
+ * subgoal_id 格式:`subgoal-{NNN}`,NNN 三位补零,从 001 开始递增。
43
+ * 校验 dependsOn 中每个 id 是否存在于现有 subgoals。
44
+ * 不可变更新:返回新的 GoalStatus 对象。
45
+ */
46
+ export function addSubgoal(goalStatus, title, dependsOn = []) {
47
+ const existingIds = new Set(goalStatus.subgoals.map((s) => s.id));
48
+ for (const depId of dependsOn) {
49
+ if (!existingIds.has(depId)) {
50
+ throw new PatchWardenError("invalid_dependency", `Subgoal dependency "${depId}" does not exist`, "Ensure depends_on references only existing subgoal ids.", true, { subgoal_id: depId, existing_ids: [...existingIds] });
51
+ }
52
+ }
53
+ const nextNumber = goalStatus.subgoals.length + 1;
54
+ const subgoalId = `subgoal-${String(nextNumber).padStart(3, "0")}`;
55
+ const newSubgoal = {
56
+ id: subgoalId,
57
+ title,
58
+ status: "ready",
59
+ depends_on: [...dependsOn],
60
+ task_ids: [],
61
+ };
62
+ const now = new Date().toISOString();
63
+ const updated = {
64
+ ...goalStatus,
65
+ subgoals: [...goalStatus.subgoals, newSubgoal],
66
+ updated_at: now,
67
+ };
68
+ return { goalStatus: updated, subgoalId };
69
+ }
70
+ /**
71
+ * 更新指定 subgoal 的状态,校验状态机合法转换。
72
+ * - accepted:设置 accepted_at
73
+ * - rejected:需要 options.rejected_reason
74
+ * 不可变更新:返回新的 GoalStatus 对象。
75
+ */
76
+ export function updateSubgoalStatus(goalStatus, subgoalId, newStatus, options) {
77
+ const index = goalStatus.subgoals.findIndex((s) => s.id === subgoalId);
78
+ if (index === -1) {
79
+ throw new PatchWardenError("subgoal_not_found", `Subgoal "${subgoalId}" not found in goal "${goalStatus.goal_id}"`, "Ensure the subgoal id exists before updating its status.", true, { subgoal_id: subgoalId, goal_id: goalStatus.goal_id });
80
+ }
81
+ const current = goalStatus.subgoals[index];
82
+ const legal = LEGAL_TRANSITIONS[current.status];
83
+ if (!legal.includes(newStatus)) {
84
+ throw new PatchWardenError("invalid_status_transition", `Invalid status transition for subgoal "${subgoalId}": "${current.status}" -> "${newStatus}"`, `Valid transitions from "${current.status}": ${legal.length > 0 ? legal.join(", ") : "(none, terminal state)"}.`, true, {
85
+ subgoal_id: subgoalId,
86
+ from_status: current.status,
87
+ to_status: newStatus,
88
+ legal_transitions: legal,
89
+ });
90
+ }
91
+ const updatedSubgoal = {
92
+ ...current,
93
+ status: newStatus,
94
+ };
95
+ if (newStatus === "accepted") {
96
+ updatedSubgoal.accepted_at = new Date().toISOString();
97
+ }
98
+ if (newStatus === "rejected") {
99
+ if (!options || !options.rejected_reason) {
100
+ throw new PatchWardenError("invalid_status_transition", `Rejecting subgoal "${subgoalId}" requires rejected_reason`, "Provide options.rejected_reason when transitioning to \"rejected\".", true, { subgoal_id: subgoalId, from_status: current.status, to_status: newStatus });
101
+ }
102
+ updatedSubgoal.rejected_reason = options.rejected_reason;
103
+ }
104
+ const newSubgoals = [...goalStatus.subgoals];
105
+ newSubgoals[index] = updatedSubgoal;
106
+ return {
107
+ ...goalStatus,
108
+ subgoals: newSubgoals,
109
+ updated_at: new Date().toISOString(),
110
+ };
111
+ }
112
+ /**
113
+ * 将 taskId 关联到指定 subgoal(去重)。
114
+ * 不可变更新:返回新的 GoalStatus 对象。
115
+ */
116
+ export function linkTaskToSubgoal(goalStatus, subgoalId, taskId) {
117
+ const index = goalStatus.subgoals.findIndex((s) => s.id === subgoalId);
118
+ if (index === -1) {
119
+ throw new PatchWardenError("subgoal_not_found", `Subgoal "${subgoalId}" not found in goal "${goalStatus.goal_id}"`, "Ensure the subgoal id exists before linking a task.", true, { subgoal_id: subgoalId, goal_id: goalStatus.goal_id });
120
+ }
121
+ const current = goalStatus.subgoals[index];
122
+ if (current.task_ids.includes(taskId)) {
123
+ // 已存在,不重复添加,但仍返回等价的新对象以保持不可变契约
124
+ return { ...goalStatus, updated_at: new Date().toISOString() };
125
+ }
126
+ const updatedSubgoal = {
127
+ ...current,
128
+ task_ids: [...current.task_ids, taskId],
129
+ };
130
+ const newSubgoals = [...goalStatus.subgoals];
131
+ newSubgoals[index] = updatedSubgoal;
132
+ return {
133
+ ...goalStatus,
134
+ subgoals: newSubgoals,
135
+ updated_at: new Date().toISOString(),
136
+ };
137
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * v0.8.0: Goal Session 目录 CRUD — 在 workspaceRoot 下管理 .patchwarden/goals/ 目录。
3
+ *
4
+ * 目录结构:
5
+ * {workspaceRoot}/.patchwarden/goals/{goal_id}/
6
+ * ├── GOAL.md 人类可读的 goal 描述
7
+ * ├── GOALS.md 子目标列表(人类可读)
8
+ * ├── goal_status.json 机器可读的状态文件(原子写)
9
+ * ├── tasks/ 任务产物
10
+ * └── artifacts/ 其他产物
11
+ *
12
+ * 所有文件系统函数都接受可选的 workspaceRoot 参数用于测试;
13
+ * 默认从 getConfig().workspaceRoot 读取。
14
+ */
15
+ import { type GoalStatus, type Subgoal } from "./goalStatus.js";
16
+ /**
17
+ * 生成 `goal_{YYYYMMDD}_{slug}` 格式的 goal id。
18
+ * 冲突时追加 `_2`、`_3`... 直到唯一。
19
+ * 日期用本地时区(new Date())。
20
+ */
21
+ export declare function generateGoalId(title: string, existingIds: string[]): string;
22
+ /**
23
+ * 返回 `.patchwarden/goals/` 目录路径(相对于 workspaceRoot)。
24
+ * 不自动创建目录。
25
+ */
26
+ export declare function getGoalsDir(workspaceRoot?: string): string;
27
+ /**
28
+ * 返回 `{getGoalsDir()}/{goalId}` 路径。
29
+ */
30
+ export declare function getGoalDir(goalId: string, workspaceRoot?: string): string;
31
+ /**
32
+ * 创建一个新的 Goal Session。
33
+ * - 用 guardWorkspacePath 校验 repoPath 在 workspaceRoot 内
34
+ * - 扫描现有 goal 目录获取 existingIds,调用 generateGoalId
35
+ * - 创建目录结构:goal_dir/、tasks/、artifacts/
36
+ * - 写入 GOAL.md、GOALS.md、goal_status.json
37
+ * 返回 { goal_id, goal_dir }。
38
+ */
39
+ export declare function createGoal(repoPath: string, title: string, description: string, workspaceRoot?: string): {
40
+ goal_id: string;
41
+ goal_dir: string;
42
+ };
43
+ /**
44
+ * 列出所有 goal 的摘要信息,按 updated_at 降序排列。
45
+ * 无法解析的目录会被跳过。
46
+ */
47
+ export declare function listGoals(workspaceRoot?: string): Array<{
48
+ goal_id: string;
49
+ title: string;
50
+ status: string;
51
+ subgoal_total: number;
52
+ subgoal_accepted: number;
53
+ subgoal_running: number;
54
+ updated_at: string;
55
+ }>;
56
+ /**
57
+ * 读取 goal 的完整详情:goal_status.json + GOAL.md 内容。
58
+ * 如果 goal 目录不存在或 goal_status.json 不存在,抛出 PatchWardenError("goal_not_found")。
59
+ */
60
+ export declare function readGoal(goalId: string, workspaceRoot?: string): {
61
+ goal_id: string;
62
+ title: string;
63
+ status: string;
64
+ repo_path: string;
65
+ created_at: string;
66
+ updated_at: string;
67
+ goal_description: string;
68
+ subgoals: Subgoal[];
69
+ };
70
+ /**
71
+ * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
72
+ * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
73
+ */
74
+ export declare function writeGoalStatus(goalId: string, status: GoalStatus, workspaceRoot?: string): void;
75
+ /**
76
+ * 读取 goal_status.json 并 JSON.parse。
77
+ * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
78
+ */
79
+ export declare function readGoalStatus(goalId: string, workspaceRoot?: string): GoalStatus;