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,306 @@
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
+
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ renameSync,
22
+ writeFileSync,
23
+ statSync,
24
+ } from "node:fs";
25
+ import { join } from "node:path";
26
+ import { getConfig } from "../config.js";
27
+ import { guardWorkspacePath } from "../security/pathGuard.js";
28
+ import { PatchWardenError } from "../errors.js";
29
+ import {
30
+ type GoalStatus,
31
+ type Subgoal,
32
+ createInitialGoalStatus,
33
+ } from "./goalStatus.js";
34
+
35
+ // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
36
+
37
+ function resolveWorkspaceRoot(workspaceRoot?: string): string {
38
+ return workspaceRoot ?? getConfig().workspaceRoot;
39
+ }
40
+
41
+ // ── Goal ID 生成 ──────────────────────────────────────────────────
42
+
43
+ /**
44
+ * 从 title 生成 slug:小写、非字母数字字符替换为 `_`、合并连续 `_`、去除首尾 `_`、截断到 30 字符。
45
+ * 如果 slug 为空(title 全是符号),用 `untitled` 代替。
46
+ */
47
+ function titleToSlug(title: string): string {
48
+ const slug = title
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, "_")
51
+ .replace(/^_+|_+$/g, "")
52
+ .slice(0, 30);
53
+ return slug === "" ? "untitled" : slug;
54
+ }
55
+
56
+ /**
57
+ * 生成 `goal_{YYYYMMDD}_{slug}` 格式的 goal id。
58
+ * 冲突时追加 `_2`、`_3`... 直到唯一。
59
+ * 日期用本地时区(new Date())。
60
+ */
61
+ export function generateGoalId(title: string, existingIds: string[]): string {
62
+ const now = new Date();
63
+ const y = now.getFullYear();
64
+ const m = String(now.getMonth() + 1).padStart(2, "0");
65
+ const d = String(now.getDate()).padStart(2, "0");
66
+ const datePart = `${y}${m}${d}`;
67
+ const slug = titleToSlug(title);
68
+
69
+ const existing = new Set(existingIds);
70
+ const base = `goal_${datePart}_${slug}`;
71
+ if (!existing.has(base)) {
72
+ return base;
73
+ }
74
+
75
+ let counter = 2;
76
+ while (existing.has(`${base}_${counter}`)) {
77
+ counter++;
78
+ }
79
+ return `${base}_${counter}`;
80
+ }
81
+
82
+ // ── 目录路径解析 ──────────────────────────────────────────────────
83
+
84
+ /**
85
+ * 返回 `.patchwarden/goals/` 目录路径(相对于 workspaceRoot)。
86
+ * 不自动创建目录。
87
+ */
88
+ export function getGoalsDir(workspaceRoot?: string): string {
89
+ return join(resolveWorkspaceRoot(workspaceRoot), ".patchwarden", "goals");
90
+ }
91
+
92
+ /**
93
+ * 返回 `{getGoalsDir()}/{goalId}` 路径。
94
+ */
95
+ export function getGoalDir(goalId: string, workspaceRoot?: string): string {
96
+ return join(getGoalsDir(workspaceRoot), goalId);
97
+ }
98
+
99
+ // ── CRUD ──────────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * 创建一个新的 Goal Session。
103
+ * - 用 guardWorkspacePath 校验 repoPath 在 workspaceRoot 内
104
+ * - 扫描现有 goal 目录获取 existingIds,调用 generateGoalId
105
+ * - 创建目录结构:goal_dir/、tasks/、artifacts/
106
+ * - 写入 GOAL.md、GOALS.md、goal_status.json
107
+ * 返回 { goal_id, goal_dir }。
108
+ */
109
+ export function createGoal(
110
+ repoPath: string,
111
+ title: string,
112
+ description: string,
113
+ workspaceRoot?: string
114
+ ): { goal_id: string; goal_dir: string } {
115
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
116
+ const guardedRepo = guardWorkspacePath(repoPath, wsRoot);
117
+
118
+ const goalsDir = getGoalsDir(workspaceRoot);
119
+ const existingIds: string[] = [];
120
+ if (existsSync(goalsDir)) {
121
+ for (const entry of readdirSync(goalsDir)) {
122
+ const entryPath = join(goalsDir, entry);
123
+ try {
124
+ if (statSync(entryPath).isDirectory()) {
125
+ existingIds.push(entry);
126
+ }
127
+ } catch {
128
+ // 跳过无法 stat 的条目
129
+ }
130
+ }
131
+ }
132
+
133
+ const goalId = generateGoalId(title, existingIds);
134
+ const goalDir = join(goalsDir, goalId);
135
+
136
+ // 创建目录结构
137
+ mkdirSync(join(goalDir, "tasks"), { recursive: true });
138
+ mkdirSync(join(goalDir, "artifacts"), { recursive: true });
139
+
140
+ // 写入 GOAL.md
141
+ const now = new Date().toISOString();
142
+ const goalMd = [
143
+ `# ${title}`,
144
+ "",
145
+ description,
146
+ "",
147
+ `- Created: ${now}`,
148
+ `- Repo: ${guardedRepo}`,
149
+ "- Status: active",
150
+ "",
151
+ ].join("\n");
152
+ writeFileSync(join(goalDir, "GOAL.md"), goalMd, "utf-8");
153
+
154
+ // 写入 GOALS.md
155
+ const goalsMd = `# Subgoals: ${title}\n\n_No subgoals yet._\n`;
156
+ writeFileSync(join(goalDir, "GOALS.md"), goalsMd, "utf-8");
157
+
158
+ // 写入 goal_status.json
159
+ const status = createInitialGoalStatus(goalId, title, guardedRepo);
160
+ writeFileSync(
161
+ join(goalDir, "goal_status.json"),
162
+ JSON.stringify(status, null, 2) + "\n",
163
+ "utf-8"
164
+ );
165
+
166
+ return { goal_id: goalId, goal_dir: goalDir };
167
+ }
168
+
169
+ /**
170
+ * 列出所有 goal 的摘要信息,按 updated_at 降序排列。
171
+ * 无法解析的目录会被跳过。
172
+ */
173
+ export function listGoals(
174
+ workspaceRoot?: string
175
+ ): Array<{
176
+ goal_id: string;
177
+ title: string;
178
+ status: string;
179
+ subgoal_total: number;
180
+ subgoal_accepted: number;
181
+ subgoal_running: number;
182
+ updated_at: string;
183
+ }> {
184
+ const goalsDir = getGoalsDir(workspaceRoot);
185
+ if (!existsSync(goalsDir)) {
186
+ return [];
187
+ }
188
+
189
+ const results: Array<{
190
+ goal_id: string;
191
+ title: string;
192
+ status: string;
193
+ subgoal_total: number;
194
+ subgoal_accepted: number;
195
+ subgoal_running: number;
196
+ updated_at: string;
197
+ }> = [];
198
+
199
+ for (const entry of readdirSync(goalsDir)) {
200
+ const entryPath = join(goalsDir, entry);
201
+ try {
202
+ if (!statSync(entryPath).isDirectory()) {
203
+ continue;
204
+ }
205
+ const status = readGoalStatus(entry, workspaceRoot);
206
+ results.push({
207
+ goal_id: status.goal_id,
208
+ title: status.title,
209
+ status: status.status,
210
+ subgoal_total: status.subgoals.length,
211
+ subgoal_accepted: status.subgoals.filter((s) => s.status === "accepted").length,
212
+ subgoal_running: status.subgoals.filter((s) => s.status === "running").length,
213
+ updated_at: status.updated_at,
214
+ });
215
+ } catch {
216
+ // 跳过无法解析的目录
217
+ }
218
+ }
219
+
220
+ results.sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
221
+ return results;
222
+ }
223
+
224
+ /**
225
+ * 读取 goal 的完整详情:goal_status.json + GOAL.md 内容。
226
+ * 如果 goal 目录不存在或 goal_status.json 不存在,抛出 PatchWardenError("goal_not_found")。
227
+ */
228
+ export function readGoal(
229
+ goalId: string,
230
+ workspaceRoot?: string
231
+ ): {
232
+ goal_id: string;
233
+ title: string;
234
+ status: string;
235
+ repo_path: string;
236
+ created_at: string;
237
+ updated_at: string;
238
+ goal_description: string;
239
+ subgoals: Subgoal[];
240
+ } {
241
+ const goalDir = getGoalDir(goalId, workspaceRoot);
242
+ if (!existsSync(goalDir)) {
243
+ throw new PatchWardenError(
244
+ "goal_not_found",
245
+ `Goal directory not found: "${goalDir}"`,
246
+ "Ensure the goal id exists before reading.",
247
+ true,
248
+ { goal_id: goalId, goal_dir: goalDir }
249
+ );
250
+ }
251
+
252
+ const status = readGoalStatus(goalId, workspaceRoot);
253
+
254
+ let goalDescription = "";
255
+ const goalMdPath = join(goalDir, "GOAL.md");
256
+ if (existsSync(goalMdPath)) {
257
+ goalDescription = readFileSync(goalMdPath, "utf-8");
258
+ }
259
+
260
+ return {
261
+ goal_id: status.goal_id,
262
+ title: status.title,
263
+ status: status.status,
264
+ repo_path: status.repo_path,
265
+ created_at: status.created_at,
266
+ updated_at: status.updated_at,
267
+ goal_description: goalDescription,
268
+ subgoals: status.subgoals,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
274
+ * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
275
+ */
276
+ export function writeGoalStatus(
277
+ goalId: string,
278
+ status: GoalStatus,
279
+ workspaceRoot?: string
280
+ ): void {
281
+ const goalDir = getGoalDir(goalId, workspaceRoot);
282
+ const finalPath = join(goalDir, "goal_status.json");
283
+ const tmpPath = join(goalDir, "goal_status.json.tmp");
284
+ writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
285
+ renameSync(tmpPath, finalPath);
286
+ }
287
+
288
+ /**
289
+ * 读取 goal_status.json 并 JSON.parse。
290
+ * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
291
+ */
292
+ export function readGoalStatus(goalId: string, workspaceRoot?: string): GoalStatus {
293
+ const goalDir = getGoalDir(goalId, workspaceRoot);
294
+ const statusPath = join(goalDir, "goal_status.json");
295
+ if (!existsSync(statusPath)) {
296
+ throw new PatchWardenError(
297
+ "goal_not_found",
298
+ `goal_status.json not found for goal "${goalId}" at "${statusPath}"`,
299
+ "Ensure the goal has been created via createGoal before reading its status.",
300
+ true,
301
+ { goal_id: goalId, status_path: statusPath }
302
+ );
303
+ }
304
+ const raw = readFileSync(statusPath, "utf-8");
305
+ return JSON.parse(raw) as GoalStatus;
306
+ }
@@ -0,0 +1,211 @@
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
+
10
+ import type { GoalStatus } from "./goalStatus.js";
11
+ import { suggestNextSubgoal } from "./goalGraph.js";
12
+ import { getConfig } from "../config.js";
13
+ import { mkdirSync, writeFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+
16
+ // ── Markdown 生成 ────────────────────────────────────────────────
17
+
18
+ /**
19
+ * 生成 Goal Session 交接文档(Markdown 字符串)。
20
+ *
21
+ * 包含章节:当前 Goal、版本目标、已完成/未完成/已拒绝子目标、最近 diff 摘要、
22
+ * 最近测试结果、当前阻塞点、下一步建议、风险提醒、接手说明。
23
+ *
24
+ * @param goalId Goal 标识(用于接手说明中的引用)
25
+ * @param goalStatus 当前 GoalStatus 快照
26
+ * @param recentDiff 最近一次 git diff 摘要(可选)
27
+ * @param recentTestResult 最近一次测试结果(可选)
28
+ * @returns Markdown 格式的交接文档
29
+ */
30
+ export function generateHandoff(
31
+ goalId: string,
32
+ goalStatus: GoalStatus,
33
+ recentDiff?: string,
34
+ recentTestResult?: string
35
+ ): string {
36
+ const lines: string[] = [];
37
+ const now = new Date().toISOString();
38
+
39
+ lines.push("# Goal Session Handoff");
40
+ lines.push("");
41
+ lines.push("Generated: " + now);
42
+ lines.push("");
43
+
44
+ // ── 当前 Goal ───────────────────────────────────────────
45
+ lines.push("## 当前 Goal");
46
+ lines.push("");
47
+ lines.push("- **goal_id**: " + goalStatus.goal_id);
48
+ lines.push("- **title**: " + goalStatus.title);
49
+ lines.push("- **status**: " + goalStatus.status);
50
+ lines.push("- **repo_path**: " + goalStatus.repo_path);
51
+ lines.push("");
52
+
53
+ // ── 版本目标 ────────────────────────────────────────────
54
+ lines.push("## 版本目标");
55
+ lines.push("");
56
+ lines.push(goalStatus.title);
57
+ lines.push("");
58
+
59
+ // ── 已完成子目标 ────────────────────────────────────────
60
+ lines.push("## 已完成子目标");
61
+ lines.push("");
62
+ const accepted = goalStatus.subgoals.filter((s) => s.status === "accepted");
63
+ if (accepted.length === 0) {
64
+ lines.push("无");
65
+ } else {
66
+ for (const s of accepted) {
67
+ lines.push("- **" + s.id + "**: " + s.title + " (accepted_at: " + (s.accepted_at ?? "N/A") + ")");
68
+ }
69
+ }
70
+ lines.push("");
71
+
72
+ // ── 未完成子目标 ────────────────────────────────────────
73
+ lines.push("## 未完成子目标");
74
+ lines.push("");
75
+ const incomplete = goalStatus.subgoals.filter(
76
+ (s) => s.status !== "accepted" && s.status !== "rejected"
77
+ );
78
+ if (incomplete.length === 0) {
79
+ lines.push("无");
80
+ } else {
81
+ for (const s of incomplete) {
82
+ const deps = s.depends_on.length > 0 ? s.depends_on.join(", ") : "无";
83
+ lines.push("- **" + s.id + "**: " + s.title + " (status: " + s.status + ", depends_on: " + deps + ")");
84
+ }
85
+ }
86
+ lines.push("");
87
+
88
+ // ── 已拒绝子目标 ────────────────────────────────────────
89
+ lines.push("## 已拒绝子目标");
90
+ lines.push("");
91
+ const rejected = goalStatus.subgoals.filter((s) => s.status === "rejected");
92
+ if (rejected.length === 0) {
93
+ lines.push("无");
94
+ } else {
95
+ for (const s of rejected) {
96
+ lines.push("- **" + s.id + "**: " + s.title + " (rejected_reason: " + (s.rejected_reason ?? "N/A") + ")");
97
+ }
98
+ }
99
+ lines.push("");
100
+
101
+ // ── 最近一次 diff 摘要 ──────────────────────────────────
102
+ lines.push("## 最近一次 diff 摘要");
103
+ lines.push("");
104
+ if (recentDiff) {
105
+ lines.push(recentDiff);
106
+ } else {
107
+ lines.push("暂无");
108
+ }
109
+ lines.push("");
110
+
111
+ // ── 最近一次测试结果 ────────────────────────────────────
112
+ lines.push("## 最近一次测试结果");
113
+ lines.push("");
114
+ if (recentTestResult) {
115
+ lines.push(recentTestResult);
116
+ } else {
117
+ lines.push("暂无");
118
+ }
119
+ lines.push("");
120
+
121
+ // ── 当前阻塞点 ─────────────────────────────────────────
122
+ lines.push("## 当前阻塞点");
123
+ lines.push("");
124
+ const needsFix = goalStatus.subgoals.filter((s) => s.status === "needs_fix");
125
+ if (needsFix.length === 0) {
126
+ lines.push("无");
127
+ } else {
128
+ for (const s of needsFix) {
129
+ lines.push("- **" + s.id + "**: " + s.title);
130
+ }
131
+ }
132
+ lines.push("");
133
+
134
+ // ── 下一步建议 ─────────────────────────────────────────
135
+ lines.push("## 下一步建议");
136
+ lines.push("");
137
+ const suggestion = suggestNextSubgoal(goalStatus);
138
+ if (suggestion.subgoal_id) {
139
+ lines.push("建议执行子目标 **" + suggestion.subgoal_id + "**:" + (suggestion.title ?? ""));
140
+ } else if (suggestion.reason === "dependencies_not_met") {
141
+ lines.push("当前存在被阻塞的子目标,建议先处理以下依赖:");
142
+ const blockedList = suggestion.blocked_by ?? [];
143
+ for (const id of blockedList) {
144
+ lines.push("- " + id);
145
+ }
146
+ } else {
147
+ lines.push("当前无可执行的 ready 子目标,建议检查所有子目标状态或等待任务完成。");
148
+ }
149
+ lines.push("");
150
+
151
+ // ── 风险提醒 ───────────────────────────────────────────
152
+ lines.push("## 风险提醒");
153
+ lines.push("");
154
+ const risks = goalStatus.subgoals.filter(
155
+ (s) => s.status === "needs_fix" || s.status === "running"
156
+ );
157
+ if (risks.length === 0) {
158
+ lines.push("无");
159
+ } else {
160
+ for (const s of risks) {
161
+ if (s.status === "needs_fix") {
162
+ lines.push("- **" + s.id + "**: " + s.title + " (needs_fix — 需要修复)");
163
+ } else {
164
+ lines.push("- **" + s.id + "**: " + s.title + " (running — 长期运行中)");
165
+ }
166
+ }
167
+ }
168
+ lines.push("");
169
+
170
+ // ── 接手说明 ───────────────────────────────────────────
171
+ lines.push("## 接手说明");
172
+ lines.push("");
173
+ lines.push(
174
+ "新会话接手时,请先调用 read_goal 查看 " + goalId + " 的完整状态,然后调用 suggest_next_subgoal 获取下一个可执行子目标。"
175
+ );
176
+ lines.push("");
177
+
178
+ return lines.join("\n");
179
+ }
180
+
181
+ // ── 落盘导出 ─────────────────────────────────────────────────────
182
+
183
+ /**
184
+ * 生成交接文档并写入 {workspaceRoot}/.patchwarden/goals/{goalId}/handoff.md。
185
+ *
186
+ * @param goalId Goal 标识
187
+ * @param goalStatus 当前 GoalStatus 快照(由调用方读取后传入)
188
+ * @param workspaceRoot 工作区根目录(可选,默认从 getConfig().workspaceRoot 获取)
189
+ * @returns handoff_path(绝对路径)和 content_preview(前 500 字符,超出则追加 "...")
190
+ */
191
+ export function exportHandoff(
192
+ goalId: string,
193
+ goalStatus: GoalStatus,
194
+ workspaceRoot?: string
195
+ ): { handoff_path: string; content_preview: string } {
196
+ const content = generateHandoff(goalId, goalStatus);
197
+
198
+ const root = workspaceRoot ?? getConfig().workspaceRoot;
199
+ const goalDir = resolve(root, ".patchwarden", "goals", goalId);
200
+ mkdirSync(goalDir, { recursive: true });
201
+
202
+ const handoffPath = join(goalDir, "handoff.md");
203
+ writeFileSync(handoffPath, content, "utf-8");
204
+
205
+ const preview = content.length > 500 ? content.slice(0, 500) + "..." : content;
206
+
207
+ return {
208
+ handoff_path: handoffPath,
209
+ content_preview: preview,
210
+ };
211
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * v0.8.0: Subgoal 状态同步 — 当任务状态变为 done_by_agent 时回写关联 subgoal 状态。
3
+ *
4
+ * 设计原则:
5
+ * - 向后兼容:无 goal_id/subgoal_id 关联的任务直接返回,不影响现有流程。
6
+ * - 错误隔离:subgoal 同步失败只记录到 stderr,不阻断任务完成主流程。
7
+ * - 仅同步 running → done_by_agent 转换;其他状态由 audit_task / 显式 API 处理。
8
+ */
9
+
10
+ import { readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
13
+ import { updateSubgoalStatus } from "./goalStatus.js";
14
+
15
+ // ── 同步函数 ──────────────────────────────────────────────────────
16
+
17
+ /**
18
+ * v0.8.0: 当任务状态变为 done_by_agent 时,同步更新关联的 subgoal 状态。
19
+ *
20
+ * 行为:
21
+ * - 如果 taskMeta.subgoal_id 或 goal_id 为 null/undefined,直接返回(向后兼容)。
22
+ * - 读取 goal_status.json,找到对应 subgoal。
23
+ * - 仅当 subgoal 当前状态为 "running" 时,更新为 "done_by_agent"。
24
+ * - 其他状态(ready/accepted/rejected/needs_fix/done_by_agent)保持不变。
25
+ * - 所有操作用 try/catch 包裹,失败时只记录到 stderr 不阻断主流程。
26
+ *
27
+ * @param taskId 任务 id(仅用于日志)
28
+ * @param taskMeta 任务的 goal 关联元信息(goal_id / subgoal_id)
29
+ * @param workspaceRoot 可选的工作区根目录(测试用),默认从 getConfig() 读取
30
+ */
31
+ export function syncSubgoalOnTaskDone(
32
+ taskId: string,
33
+ taskMeta: { goal_id?: string | null; subgoal_id?: string | null },
34
+ workspaceRoot?: string
35
+ ): void {
36
+ const goalId = taskMeta.goal_id;
37
+ const subgoalId = taskMeta.subgoal_id;
38
+ if (!goalId || !subgoalId) return;
39
+
40
+ try {
41
+ const goalStatus = readGoalStatus(goalId, workspaceRoot);
42
+ const subgoal = goalStatus.subgoals.find((s) => s.id === subgoalId);
43
+ if (!subgoal) return;
44
+ if (subgoal.status !== "running") return;
45
+
46
+ const updated = updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent");
47
+ writeGoalStatus(goalId, updated, workspaceRoot);
48
+ } catch (err) {
49
+ // subgoal 同步失败不应影响任务完成流程,只记录到 stderr
50
+ console.error(
51
+ `[goal] syncSubgoalOnTaskDone failed for task ${taskId}: ${err instanceof Error ? err.message : String(err)}`
52
+ );
53
+ }
54
+ }
55
+
56
+ // ── Task 元信息读取 ───────────────────────────────────────────────
57
+
58
+ /**
59
+ * v0.8.0: 从 task 的 status.json 读取 goal_id/subgoal_id 关联信息。
60
+ *
61
+ * 行为:
62
+ * - 读取 {taskDir}/status.json,解析其中的 goal_id / subgoal_id 字段。
63
+ * - 任何错误(文件不存在、JSON 解析失败等)都返回 { goal_id: null, subgoal_id: null },
64
+ * 不抛错,保证调用方流程不受影响。
65
+ *
66
+ * @param taskDir 任务目录路径(包含 status.json)
67
+ */
68
+ export function readTaskGoalMeta(
69
+ taskDir: string
70
+ ): { goal_id: string | null; subgoal_id: string | null } {
71
+ try {
72
+ const statusFile = join(taskDir, "status.json");
73
+ const raw = readFileSync(statusFile, "utf-8");
74
+ const data = JSON.parse(raw) as Record<string, unknown>;
75
+ return {
76
+ goal_id: typeof data.goal_id === "string" ? data.goal_id : null,
77
+ subgoal_id: typeof data.subgoal_id === "string" ? data.subgoal_id : null,
78
+ };
79
+ } catch {
80
+ return { goal_id: null, subgoal_id: null };
81
+ }
82
+ }