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,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;
@@ -0,0 +1,211 @@
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 { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, statSync, } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { getConfig } from "../config.js";
18
+ import { guardWorkspacePath } from "../security/pathGuard.js";
19
+ import { PatchWardenError } from "../errors.js";
20
+ import { createInitialGoalStatus, } from "./goalStatus.js";
21
+ // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
22
+ function resolveWorkspaceRoot(workspaceRoot) {
23
+ return workspaceRoot ?? getConfig().workspaceRoot;
24
+ }
25
+ // ── Goal ID 生成 ──────────────────────────────────────────────────
26
+ /**
27
+ * 从 title 生成 slug:小写、非字母数字字符替换为 `_`、合并连续 `_`、去除首尾 `_`、截断到 30 字符。
28
+ * 如果 slug 为空(title 全是符号),用 `untitled` 代替。
29
+ */
30
+ function titleToSlug(title) {
31
+ const slug = title
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, "_")
34
+ .replace(/^_+|_+$/g, "")
35
+ .slice(0, 30);
36
+ return slug === "" ? "untitled" : slug;
37
+ }
38
+ /**
39
+ * 生成 `goal_{YYYYMMDD}_{slug}` 格式的 goal id。
40
+ * 冲突时追加 `_2`、`_3`... 直到唯一。
41
+ * 日期用本地时区(new Date())。
42
+ */
43
+ export function generateGoalId(title, existingIds) {
44
+ const now = new Date();
45
+ const y = now.getFullYear();
46
+ const m = String(now.getMonth() + 1).padStart(2, "0");
47
+ const d = String(now.getDate()).padStart(2, "0");
48
+ const datePart = `${y}${m}${d}`;
49
+ const slug = titleToSlug(title);
50
+ const existing = new Set(existingIds);
51
+ const base = `goal_${datePart}_${slug}`;
52
+ if (!existing.has(base)) {
53
+ return base;
54
+ }
55
+ let counter = 2;
56
+ while (existing.has(`${base}_${counter}`)) {
57
+ counter++;
58
+ }
59
+ return `${base}_${counter}`;
60
+ }
61
+ // ── 目录路径解析 ──────────────────────────────────────────────────
62
+ /**
63
+ * 返回 `.patchwarden/goals/` 目录路径(相对于 workspaceRoot)。
64
+ * 不自动创建目录。
65
+ */
66
+ export function getGoalsDir(workspaceRoot) {
67
+ return join(resolveWorkspaceRoot(workspaceRoot), ".patchwarden", "goals");
68
+ }
69
+ /**
70
+ * 返回 `{getGoalsDir()}/{goalId}` 路径。
71
+ */
72
+ export function getGoalDir(goalId, workspaceRoot) {
73
+ return join(getGoalsDir(workspaceRoot), goalId);
74
+ }
75
+ // ── CRUD ──────────────────────────────────────────────────────────
76
+ /**
77
+ * 创建一个新的 Goal Session。
78
+ * - 用 guardWorkspacePath 校验 repoPath 在 workspaceRoot 内
79
+ * - 扫描现有 goal 目录获取 existingIds,调用 generateGoalId
80
+ * - 创建目录结构:goal_dir/、tasks/、artifacts/
81
+ * - 写入 GOAL.md、GOALS.md、goal_status.json
82
+ * 返回 { goal_id, goal_dir }。
83
+ */
84
+ export function createGoal(repoPath, title, description, workspaceRoot) {
85
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
86
+ const guardedRepo = guardWorkspacePath(repoPath, wsRoot);
87
+ const goalsDir = getGoalsDir(workspaceRoot);
88
+ const existingIds = [];
89
+ if (existsSync(goalsDir)) {
90
+ for (const entry of readdirSync(goalsDir)) {
91
+ const entryPath = join(goalsDir, entry);
92
+ try {
93
+ if (statSync(entryPath).isDirectory()) {
94
+ existingIds.push(entry);
95
+ }
96
+ }
97
+ catch {
98
+ // 跳过无法 stat 的条目
99
+ }
100
+ }
101
+ }
102
+ const goalId = generateGoalId(title, existingIds);
103
+ const goalDir = join(goalsDir, goalId);
104
+ // 创建目录结构
105
+ mkdirSync(join(goalDir, "tasks"), { recursive: true });
106
+ mkdirSync(join(goalDir, "artifacts"), { recursive: true });
107
+ // 写入 GOAL.md
108
+ const now = new Date().toISOString();
109
+ const goalMd = [
110
+ `# ${title}`,
111
+ "",
112
+ description,
113
+ "",
114
+ `- Created: ${now}`,
115
+ `- Repo: ${guardedRepo}`,
116
+ "- Status: active",
117
+ "",
118
+ ].join("\n");
119
+ writeFileSync(join(goalDir, "GOAL.md"), goalMd, "utf-8");
120
+ // 写入 GOALS.md
121
+ const goalsMd = `# Subgoals: ${title}\n\n_No subgoals yet._\n`;
122
+ writeFileSync(join(goalDir, "GOALS.md"), goalsMd, "utf-8");
123
+ // 写入 goal_status.json
124
+ const status = createInitialGoalStatus(goalId, title, guardedRepo);
125
+ writeFileSync(join(goalDir, "goal_status.json"), JSON.stringify(status, null, 2) + "\n", "utf-8");
126
+ return { goal_id: goalId, goal_dir: goalDir };
127
+ }
128
+ /**
129
+ * 列出所有 goal 的摘要信息,按 updated_at 降序排列。
130
+ * 无法解析的目录会被跳过。
131
+ */
132
+ export function listGoals(workspaceRoot) {
133
+ const goalsDir = getGoalsDir(workspaceRoot);
134
+ if (!existsSync(goalsDir)) {
135
+ return [];
136
+ }
137
+ const results = [];
138
+ for (const entry of readdirSync(goalsDir)) {
139
+ const entryPath = join(goalsDir, entry);
140
+ try {
141
+ if (!statSync(entryPath).isDirectory()) {
142
+ continue;
143
+ }
144
+ const status = readGoalStatus(entry, workspaceRoot);
145
+ results.push({
146
+ goal_id: status.goal_id,
147
+ title: status.title,
148
+ status: status.status,
149
+ subgoal_total: status.subgoals.length,
150
+ subgoal_accepted: status.subgoals.filter((s) => s.status === "accepted").length,
151
+ subgoal_running: status.subgoals.filter((s) => s.status === "running").length,
152
+ updated_at: status.updated_at,
153
+ });
154
+ }
155
+ catch {
156
+ // 跳过无法解析的目录
157
+ }
158
+ }
159
+ results.sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
160
+ return results;
161
+ }
162
+ /**
163
+ * 读取 goal 的完整详情:goal_status.json + GOAL.md 内容。
164
+ * 如果 goal 目录不存在或 goal_status.json 不存在,抛出 PatchWardenError("goal_not_found")。
165
+ */
166
+ export function readGoal(goalId, workspaceRoot) {
167
+ const goalDir = getGoalDir(goalId, workspaceRoot);
168
+ if (!existsSync(goalDir)) {
169
+ throw new PatchWardenError("goal_not_found", `Goal directory not found: "${goalDir}"`, "Ensure the goal id exists before reading.", true, { goal_id: goalId, goal_dir: goalDir });
170
+ }
171
+ const status = readGoalStatus(goalId, workspaceRoot);
172
+ let goalDescription = "";
173
+ const goalMdPath = join(goalDir, "GOAL.md");
174
+ if (existsSync(goalMdPath)) {
175
+ goalDescription = readFileSync(goalMdPath, "utf-8");
176
+ }
177
+ return {
178
+ goal_id: status.goal_id,
179
+ title: status.title,
180
+ status: status.status,
181
+ repo_path: status.repo_path,
182
+ created_at: status.created_at,
183
+ updated_at: status.updated_at,
184
+ goal_description: goalDescription,
185
+ subgoals: status.subgoals,
186
+ };
187
+ }
188
+ /**
189
+ * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
190
+ * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
191
+ */
192
+ export function writeGoalStatus(goalId, status, workspaceRoot) {
193
+ const goalDir = getGoalDir(goalId, workspaceRoot);
194
+ const finalPath = join(goalDir, "goal_status.json");
195
+ const tmpPath = join(goalDir, "goal_status.json.tmp");
196
+ writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
197
+ renameSync(tmpPath, finalPath);
198
+ }
199
+ /**
200
+ * 读取 goal_status.json 并 JSON.parse。
201
+ * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
202
+ */
203
+ export function readGoalStatus(goalId, workspaceRoot) {
204
+ const goalDir = getGoalDir(goalId, workspaceRoot);
205
+ const statusPath = join(goalDir, "goal_status.json");
206
+ if (!existsSync(statusPath)) {
207
+ throw new PatchWardenError("goal_not_found", `goal_status.json not found for goal "${goalId}" at "${statusPath}"`, "Ensure the goal has been created via createGoal before reading its status.", true, { goal_id: goalId, status_path: statusPath });
208
+ }
209
+ const raw = readFileSync(statusPath, "utf-8");
210
+ return JSON.parse(raw);
211
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * v0.8.0: Goal Session 交接文档生成 — 将当前 goal 状态渲染为 Markdown 交接报告。
3
+ *
4
+ * generateHandoff 生成 Markdown 字符串;exportHandoff 将其落盘到
5
+ * {workspaceRoot}/.patchwarden/goals/{goalId}/handoff.md。
6
+ * exportHandoff 接受调用方传入的 goalStatus,不自行读取 goal_status.json,
7
+ * 以避免与 goalStore 形成循环依赖。
8
+ */
9
+ import type { GoalStatus } from "./goalStatus.js";
10
+ /**
11
+ * 生成 Goal Session 交接文档(Markdown 字符串)。
12
+ *
13
+ * 包含章节:当前 Goal、版本目标、已完成/未完成/已拒绝子目标、最近 diff 摘要、
14
+ * 最近测试结果、当前阻塞点、下一步建议、风险提醒、接手说明。
15
+ *
16
+ * @param goalId Goal 标识(用于接手说明中的引用)
17
+ * @param goalStatus 当前 GoalStatus 快照
18
+ * @param recentDiff 最近一次 git diff 摘要(可选)
19
+ * @param recentTestResult 最近一次测试结果(可选)
20
+ * @returns Markdown 格式的交接文档
21
+ */
22
+ export declare function generateHandoff(goalId: string, goalStatus: GoalStatus, recentDiff?: string, recentTestResult?: string): string;
23
+ /**
24
+ * 生成交接文档并写入 {workspaceRoot}/.patchwarden/goals/{goalId}/handoff.md。
25
+ *
26
+ * @param goalId Goal 标识
27
+ * @param goalStatus 当前 GoalStatus 快照(由调用方读取后传入)
28
+ * @param workspaceRoot 工作区根目录(可选,默认从 getConfig().workspaceRoot 获取)
29
+ * @returns handoff_path(绝对路径)和 content_preview(前 500 字符,超出则追加 "...")
30
+ */
31
+ export declare function exportHandoff(goalId: string, goalStatus: GoalStatus, workspaceRoot?: string): {
32
+ handoff_path: string;
33
+ content_preview: string;
34
+ };
@@ -0,0 +1,183 @@
1
+ /**
2
+ * v0.8.0: Goal Session 交接文档生成 — 将当前 goal 状态渲染为 Markdown 交接报告。
3
+ *
4
+ * generateHandoff 生成 Markdown 字符串;exportHandoff 将其落盘到
5
+ * {workspaceRoot}/.patchwarden/goals/{goalId}/handoff.md。
6
+ * exportHandoff 接受调用方传入的 goalStatus,不自行读取 goal_status.json,
7
+ * 以避免与 goalStore 形成循环依赖。
8
+ */
9
+ import { suggestNextSubgoal } from "./goalGraph.js";
10
+ import { getConfig } from "../config.js";
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { join, resolve } from "node:path";
13
+ // ── Markdown 生成 ────────────────────────────────────────────────
14
+ /**
15
+ * 生成 Goal Session 交接文档(Markdown 字符串)。
16
+ *
17
+ * 包含章节:当前 Goal、版本目标、已完成/未完成/已拒绝子目标、最近 diff 摘要、
18
+ * 最近测试结果、当前阻塞点、下一步建议、风险提醒、接手说明。
19
+ *
20
+ * @param goalId Goal 标识(用于接手说明中的引用)
21
+ * @param goalStatus 当前 GoalStatus 快照
22
+ * @param recentDiff 最近一次 git diff 摘要(可选)
23
+ * @param recentTestResult 最近一次测试结果(可选)
24
+ * @returns Markdown 格式的交接文档
25
+ */
26
+ export function generateHandoff(goalId, goalStatus, recentDiff, recentTestResult) {
27
+ const lines = [];
28
+ const now = new Date().toISOString();
29
+ lines.push("# Goal Session Handoff");
30
+ lines.push("");
31
+ lines.push("Generated: " + now);
32
+ lines.push("");
33
+ // ── 当前 Goal ───────────────────────────────────────────
34
+ lines.push("## 当前 Goal");
35
+ lines.push("");
36
+ lines.push("- **goal_id**: " + goalStatus.goal_id);
37
+ lines.push("- **title**: " + goalStatus.title);
38
+ lines.push("- **status**: " + goalStatus.status);
39
+ lines.push("- **repo_path**: " + goalStatus.repo_path);
40
+ lines.push("");
41
+ // ── 版本目标 ────────────────────────────────────────────
42
+ lines.push("## 版本目标");
43
+ lines.push("");
44
+ lines.push(goalStatus.title);
45
+ lines.push("");
46
+ // ── 已完成子目标 ────────────────────────────────────────
47
+ lines.push("## 已完成子目标");
48
+ lines.push("");
49
+ const accepted = goalStatus.subgoals.filter((s) => s.status === "accepted");
50
+ if (accepted.length === 0) {
51
+ lines.push("无");
52
+ }
53
+ else {
54
+ for (const s of accepted) {
55
+ lines.push("- **" + s.id + "**: " + s.title + " (accepted_at: " + (s.accepted_at ?? "N/A") + ")");
56
+ }
57
+ }
58
+ lines.push("");
59
+ // ── 未完成子目标 ────────────────────────────────────────
60
+ lines.push("## 未完成子目标");
61
+ lines.push("");
62
+ const incomplete = goalStatus.subgoals.filter((s) => s.status !== "accepted" && s.status !== "rejected");
63
+ if (incomplete.length === 0) {
64
+ lines.push("无");
65
+ }
66
+ else {
67
+ for (const s of incomplete) {
68
+ const deps = s.depends_on.length > 0 ? s.depends_on.join(", ") : "无";
69
+ lines.push("- **" + s.id + "**: " + s.title + " (status: " + s.status + ", depends_on: " + deps + ")");
70
+ }
71
+ }
72
+ lines.push("");
73
+ // ── 已拒绝子目标 ────────────────────────────────────────
74
+ lines.push("## 已拒绝子目标");
75
+ lines.push("");
76
+ const rejected = goalStatus.subgoals.filter((s) => s.status === "rejected");
77
+ if (rejected.length === 0) {
78
+ lines.push("无");
79
+ }
80
+ else {
81
+ for (const s of rejected) {
82
+ lines.push("- **" + s.id + "**: " + s.title + " (rejected_reason: " + (s.rejected_reason ?? "N/A") + ")");
83
+ }
84
+ }
85
+ lines.push("");
86
+ // ── 最近一次 diff 摘要 ──────────────────────────────────
87
+ lines.push("## 最近一次 diff 摘要");
88
+ lines.push("");
89
+ if (recentDiff) {
90
+ lines.push(recentDiff);
91
+ }
92
+ else {
93
+ lines.push("暂无");
94
+ }
95
+ lines.push("");
96
+ // ── 最近一次测试结果 ────────────────────────────────────
97
+ lines.push("## 最近一次测试结果");
98
+ lines.push("");
99
+ if (recentTestResult) {
100
+ lines.push(recentTestResult);
101
+ }
102
+ else {
103
+ lines.push("暂无");
104
+ }
105
+ lines.push("");
106
+ // ── 当前阻塞点 ─────────────────────────────────────────
107
+ lines.push("## 当前阻塞点");
108
+ lines.push("");
109
+ const needsFix = goalStatus.subgoals.filter((s) => s.status === "needs_fix");
110
+ if (needsFix.length === 0) {
111
+ lines.push("无");
112
+ }
113
+ else {
114
+ for (const s of needsFix) {
115
+ lines.push("- **" + s.id + "**: " + s.title);
116
+ }
117
+ }
118
+ lines.push("");
119
+ // ── 下一步建议 ─────────────────────────────────────────
120
+ lines.push("## 下一步建议");
121
+ lines.push("");
122
+ const suggestion = suggestNextSubgoal(goalStatus);
123
+ if (suggestion.subgoal_id) {
124
+ lines.push("建议执行子目标 **" + suggestion.subgoal_id + "**:" + (suggestion.title ?? ""));
125
+ }
126
+ else if (suggestion.reason === "dependencies_not_met") {
127
+ lines.push("当前存在被阻塞的子目标,建议先处理以下依赖:");
128
+ const blockedList = suggestion.blocked_by ?? [];
129
+ for (const id of blockedList) {
130
+ lines.push("- " + id);
131
+ }
132
+ }
133
+ else {
134
+ lines.push("当前无可执行的 ready 子目标,建议检查所有子目标状态或等待任务完成。");
135
+ }
136
+ lines.push("");
137
+ // ── 风险提醒 ───────────────────────────────────────────
138
+ lines.push("## 风险提醒");
139
+ lines.push("");
140
+ const risks = goalStatus.subgoals.filter((s) => s.status === "needs_fix" || s.status === "running");
141
+ if (risks.length === 0) {
142
+ lines.push("无");
143
+ }
144
+ else {
145
+ for (const s of risks) {
146
+ if (s.status === "needs_fix") {
147
+ lines.push("- **" + s.id + "**: " + s.title + " (needs_fix — 需要修复)");
148
+ }
149
+ else {
150
+ lines.push("- **" + s.id + "**: " + s.title + " (running — 长期运行中)");
151
+ }
152
+ }
153
+ }
154
+ lines.push("");
155
+ // ── 接手说明 ───────────────────────────────────────────
156
+ lines.push("## 接手说明");
157
+ lines.push("");
158
+ lines.push("新会话接手时,请先调用 read_goal 查看 " + goalId + " 的完整状态,然后调用 suggest_next_subgoal 获取下一个可执行子目标。");
159
+ lines.push("");
160
+ return lines.join("\n");
161
+ }
162
+ // ── 落盘导出 ─────────────────────────────────────────────────────
163
+ /**
164
+ * 生成交接文档并写入 {workspaceRoot}/.patchwarden/goals/{goalId}/handoff.md。
165
+ *
166
+ * @param goalId Goal 标识
167
+ * @param goalStatus 当前 GoalStatus 快照(由调用方读取后传入)
168
+ * @param workspaceRoot 工作区根目录(可选,默认从 getConfig().workspaceRoot 获取)
169
+ * @returns handoff_path(绝对路径)和 content_preview(前 500 字符,超出则追加 "...")
170
+ */
171
+ export function exportHandoff(goalId, goalStatus, workspaceRoot) {
172
+ const content = generateHandoff(goalId, goalStatus);
173
+ const root = workspaceRoot ?? getConfig().workspaceRoot;
174
+ const goalDir = resolve(root, ".patchwarden", "goals", goalId);
175
+ mkdirSync(goalDir, { recursive: true });
176
+ const handoffPath = join(goalDir, "handoff.md");
177
+ writeFileSync(handoffPath, content, "utf-8");
178
+ const preview = content.length > 500 ? content.slice(0, 500) + "..." : content;
179
+ return {
180
+ handoff_path: handoffPath,
181
+ content_preview: preview,
182
+ };
183
+ }