patchwarden 1.5.1 → 1.6.1

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 (573) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +152 -25
  3. package/README.md +120 -23
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentStore.js +42 -34
  6. package/dist/config.d.ts +15 -0
  7. package/dist/config.js +66 -13
  8. package/dist/control/fileManager.d.ts +2 -0
  9. package/dist/control/fileManager.js +39 -0
  10. package/dist/control/middleware/auth.d.ts +6 -0
  11. package/dist/control/middleware/auth.js +14 -5
  12. package/dist/control/middleware/static.js +16 -3
  13. package/dist/control/routeTable.d.ts +24 -0
  14. package/dist/control/routeTable.js +263 -0
  15. package/dist/control/routes/audit.js +17 -7
  16. package/dist/control/routes/evidence.js +1 -1
  17. package/dist/control/routes/lineage.js +1 -1
  18. package/dist/control/routes/process.d.ts +7 -0
  19. package/dist/control/routes/process.js +329 -42
  20. package/dist/control/routes/sessions.d.ts +9 -1
  21. package/dist/control/routes/sessions.js +84 -21
  22. package/dist/control/routes/status.d.ts +46 -1
  23. package/dist/control/routes/status.js +72 -15
  24. package/dist/control/routes/taskActions.js +24 -60
  25. package/dist/control/routes/tasks.d.ts +4 -0
  26. package/dist/control/routes/tasks.js +61 -62
  27. package/dist/control/routes/workspace.js +50 -23
  28. package/dist/control/runtime.d.ts +3 -2
  29. package/dist/control/runtime.js +63 -30
  30. package/dist/control/server.js +36 -365
  31. package/dist/control/shared.d.ts +25 -1
  32. package/dist/control/shared.js +64 -11
  33. package/dist/direct/directAudit.js +234 -225
  34. package/dist/direct/directGuards.d.ts +2 -1
  35. package/dist/direct/directGuards.js +42 -29
  36. package/dist/direct/directPatch.d.ts +6 -1
  37. package/dist/direct/directPatch.js +55 -8
  38. package/dist/direct/directSessionStore.d.ts +16 -6
  39. package/dist/direct/directSessionStore.js +127 -35
  40. package/dist/direct/directVerification.js +8 -16
  41. package/dist/doctor.js +62 -52
  42. package/dist/goal/acceptanceEngine.d.ts +1 -1
  43. package/dist/goal/goalProgress.js +75 -69
  44. package/dist/goal/goalReport.js +5 -12
  45. package/dist/goal/goalStatus.d.ts +2 -0
  46. package/dist/goal/goalStatus.js +10 -3
  47. package/dist/goal/goalStore.d.ts +9 -2
  48. package/dist/goal/goalStore.js +152 -45
  49. package/dist/goal/handoffExport.js +6 -6
  50. package/dist/goal/specKitImport.js +97 -96
  51. package/dist/goal/subgoalSync.js +11 -9
  52. package/dist/goal/worktreeManager.d.ts +2 -0
  53. package/dist/goal/worktreeManager.js +209 -136
  54. package/dist/httpServer.js +111 -32
  55. package/dist/logging.js +5 -3
  56. package/dist/policy/projectPolicy.js +5 -2
  57. package/dist/release/releaseGate.d.ts +1 -1
  58. package/dist/release/releaseGate.js +78 -25
  59. package/dist/runner/agentInvocation.d.ts +14 -0
  60. package/dist/runner/agentInvocation.js +96 -4
  61. package/dist/runner/changeCapture.d.ts +13 -5
  62. package/dist/runner/changeCapture.js +243 -61
  63. package/dist/runner/postTaskCleanup.js +48 -19
  64. package/dist/runner/processSecurity.d.ts +56 -0
  65. package/dist/runner/processSecurity.js +373 -0
  66. package/dist/runner/runTask.d.ts +1 -1
  67. package/dist/runner/runTask.js +115 -87
  68. package/dist/runner/simpleProcess.d.ts +3 -0
  69. package/dist/runner/simpleProcess.js +50 -36
  70. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  71. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  72. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  73. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  74. package/dist/runner/taskStatusStore.d.ts +11 -0
  75. package/dist/runner/taskStatusStore.js +24 -0
  76. package/dist/runner/watch.d.ts +14 -1
  77. package/dist/runner/watch.js +256 -48
  78. package/dist/security/contentRedaction.js +2 -2
  79. package/dist/security/discoveryTokenStore.d.ts +3 -2
  80. package/dist/security/discoveryTokenStore.js +17 -2
  81. package/dist/security/loopbackHost.d.ts +2 -0
  82. package/dist/security/loopbackHost.js +16 -0
  83. package/dist/security/pathGuard.js +32 -17
  84. package/dist/security/planGuard.js +2 -1
  85. package/dist/security/runtimeGuard.js +9 -6
  86. package/dist/security/secretComparison.d.ts +3 -0
  87. package/dist/security/secretComparison.js +11 -0
  88. package/dist/security/sensitiveGuard.d.ts +5 -0
  89. package/dist/security/sensitiveGuard.js +27 -8
  90. package/dist/security/toolInvocationGuard.d.ts +2 -2
  91. package/dist/security/toolInvocationGuard.js +1 -1
  92. package/dist/security/workspaceRootGuard.d.ts +8 -0
  93. package/dist/security/workspaceRootGuard.js +47 -0
  94. package/dist/smoke-test.js +24 -23
  95. package/dist/tools/catalog/index.d.ts +4 -0
  96. package/dist/tools/catalog/index.js +4 -0
  97. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  98. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  99. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  100. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  101. package/dist/tools/definitions/toolDefs.js +1177 -0
  102. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  103. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  104. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +1 -1
  105. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +99 -29
  106. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  107. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +7 -7
  108. package/dist/tools/diagnostics/index.d.ts +6 -0
  109. package/dist/tools/diagnostics/index.js +6 -0
  110. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  111. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  112. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  113. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  114. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  115. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  116. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  117. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  118. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  119. package/dist/tools/direct/index.d.ts +3 -0
  120. package/dist/tools/direct/index.js +3 -0
  121. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  122. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  123. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  124. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  125. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  126. package/dist/tools/discovery/index.d.ts +3 -0
  127. package/dist/tools/discovery/index.js +3 -0
  128. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  129. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  130. package/dist/tools/dispatch/coreDispatch.js +32 -31
  131. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  132. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  133. package/dist/tools/dispatch/directDispatch.js +11 -10
  134. package/dist/tools/dispatch/goalDispatch.js +5 -4
  135. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  136. package/dist/tools/dispatch/validation.d.ts +7 -0
  137. package/dist/tools/dispatch/validation.js +71 -0
  138. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  139. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  140. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  141. package/dist/tools/goals/index.d.ts +3 -0
  142. package/dist/tools/goals/index.js +3 -0
  143. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  144. package/dist/tools/registry.d.ts +7 -11
  145. package/dist/tools/registry.js +17 -1178
  146. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  147. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  148. package/dist/tools/release/index.d.ts +2 -0
  149. package/dist/tools/release/index.js +2 -0
  150. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  151. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  152. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  153. package/dist/tools/tasks/cancelTask.js +85 -0
  154. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +10 -2
  155. package/dist/tools/{createTask.js → tasks/createTask.js} +42 -27
  156. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  157. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  158. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  159. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  160. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  161. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  162. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  163. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  164. package/dist/tools/tasks/index.d.ts +18 -0
  165. package/dist/tools/tasks/index.js +18 -0
  166. package/dist/tools/tasks/killTask.d.ts +1 -0
  167. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  168. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  169. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  170. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  171. package/dist/tools/tasks/retryTask.js +60 -0
  172. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  173. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +58 -30
  174. package/dist/tools/tasks/runVerification.js +38 -0
  175. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  176. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  177. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  178. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  179. package/dist/tools/workspace/applyPatch.js +53 -0
  180. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  181. package/dist/tools/workspace/index.d.ts +10 -0
  182. package/dist/tools/workspace/index.js +10 -0
  183. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  184. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  185. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  186. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  187. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  188. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  189. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  190. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  191. package/dist/tools/workspace/syncFile.js +134 -0
  192. package/dist/utils/atomicFile.d.ts +7 -0
  193. package/dist/utils/atomicFile.js +48 -0
  194. package/dist/utils/boundedFile.d.ts +8 -0
  195. package/dist/utils/boundedFile.js +93 -0
  196. package/dist/utils/lockedJsonFile.d.ts +15 -0
  197. package/dist/utils/lockedJsonFile.js +224 -0
  198. package/dist/utils/platform.d.ts +11 -0
  199. package/dist/utils/platform.js +26 -0
  200. package/dist/utils/stableJson.d.ts +2 -0
  201. package/dist/utils/stableJson.js +12 -0
  202. package/dist/version.d.ts +3 -2
  203. package/dist/version.js +3 -2
  204. package/dist/watcherStatus.d.ts +2 -2
  205. package/dist/watcherStatus.js +18 -0
  206. package/docs/CODE_WIKI.md +808 -143
  207. package/docs/architecture.md +15 -55
  208. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  209. package/docs/control-center/README.md +4 -4
  210. package/docs/dashboard-overview.md +4 -4
  211. package/docs/desktop-app.md +175 -0
  212. package/docs/evidence-pack-schema.md +3 -3
  213. package/docs/open-source-application.md +17 -11
  214. package/docs/release-checklist.md +1 -1
  215. package/docs/release-evidence.md +39 -29
  216. package/docs/task-safe-review-workflow.md +5 -5
  217. package/docs/threat-model.md +19 -1
  218. package/examples/config.example.json +4 -2
  219. package/package.json +27 -10
  220. package/scripts/README.md +15 -6
  221. package/scripts/build.js +11 -0
  222. package/scripts/checks/brand-check.js +3 -0
  223. package/scripts/checks/build-output-check.js +37 -0
  224. package/scripts/checks/control-center-smoke.js +91 -6
  225. package/scripts/checks/control-smoke.js +19 -8
  226. package/scripts/checks/http-mcp-smoke.js +82 -4
  227. package/scripts/checks/lifecycle-smoke.js +10 -10
  228. package/scripts/checks/mcp-manifest-check.js +1 -1
  229. package/scripts/checks/mcp-smoke.js +27 -3
  230. package/scripts/checks/package-manifest-check.js +20 -7
  231. package/scripts/checks/unit-tests.js +3 -5
  232. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  233. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  234. package/scripts/control/manage-patchwarden.ps1 +69 -4
  235. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  236. package/scripts/control/run-background-supervisor.ps1 +41 -0
  237. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  238. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  239. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  240. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  241. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  242. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  243. package/scripts/lib/clean-generated-output.js +11 -0
  244. package/scripts/lib/file-discovery.js +12 -0
  245. package/scripts/release/desktop-preflight.js +211 -0
  246. package/scripts/release/pack-clean.js +59 -14
  247. package/src/assessments/agentAssessor.ts +26 -12
  248. package/src/assessments/assessmentStore.ts +52 -40
  249. package/src/config.ts +70 -13
  250. package/src/control/fileManager.ts +40 -0
  251. package/src/control/middleware/auth.ts +22 -4
  252. package/src/control/middleware/static.ts +16 -3
  253. package/src/control/routeTable.ts +301 -0
  254. package/src/control/routes/audit.ts +14 -6
  255. package/src/control/routes/evidence.ts +1 -1
  256. package/src/control/routes/lineage.ts +1 -1
  257. package/src/control/routes/process.ts +341 -40
  258. package/src/control/routes/sessions.ts +88 -25
  259. package/src/control/routes/status.ts +97 -17
  260. package/src/control/routes/taskActions.ts +24 -58
  261. package/src/control/routes/tasks.ts +66 -58
  262. package/src/control/routes/workspace.ts +56 -25
  263. package/src/control/runtime.ts +69 -33
  264. package/src/control/server.ts +36 -342
  265. package/src/control/shared.ts +77 -11
  266. package/src/direct/directAudit.ts +17 -9
  267. package/src/direct/directGuards.ts +72 -44
  268. package/src/direct/directPatch.ts +95 -8
  269. package/src/direct/directSessionStore.ts +211 -55
  270. package/src/direct/directVerification.ts +9 -23
  271. package/src/doctor.ts +73 -59
  272. package/src/goal/acceptanceEngine.ts +1 -1
  273. package/src/goal/goalProgress.ts +17 -13
  274. package/src/goal/goalReport.ts +5 -13
  275. package/src/goal/goalStatus.ts +19 -3
  276. package/src/goal/goalStore.ts +241 -54
  277. package/src/goal/handoffExport.ts +6 -6
  278. package/src/goal/specKitImport.ts +12 -12
  279. package/src/goal/subgoalSync.ts +11 -8
  280. package/src/goal/worktreeManager.ts +139 -22
  281. package/src/httpServer.ts +113 -30
  282. package/src/logging.ts +10 -3
  283. package/src/policy/projectPolicy.ts +5 -2
  284. package/src/release/releaseGate.ts +74 -27
  285. package/src/runner/agentInvocation.ts +126 -4
  286. package/src/runner/changeCapture.ts +291 -57
  287. package/src/runner/postTaskCleanup.ts +41 -17
  288. package/src/runner/processSecurity.ts +433 -0
  289. package/src/runner/runTask.ts +138 -91
  290. package/src/runner/simpleProcess.ts +66 -31
  291. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  292. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  293. package/src/runner/taskStatusStore.ts +54 -0
  294. package/src/runner/watch.ts +281 -51
  295. package/src/security/contentRedaction.ts +2 -2
  296. package/src/security/discoveryTokenStore.ts +18 -4
  297. package/src/security/loopbackHost.ts +19 -0
  298. package/src/security/pathGuard.ts +35 -16
  299. package/src/security/planGuard.ts +2 -1
  300. package/src/security/runtimeGuard.ts +9 -6
  301. package/src/security/secretComparison.ts +13 -0
  302. package/src/security/sensitiveGuard.ts +27 -8
  303. package/src/security/toolInvocationGuard.ts +2 -2
  304. package/src/security/workspaceRootGuard.ts +53 -0
  305. package/src/smoke-test.ts +25 -24
  306. package/src/tools/catalog/index.ts +4 -0
  307. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  308. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  309. package/src/tools/definitions/toolDefs.ts +1264 -0
  310. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  311. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +99 -32
  312. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +8 -8
  313. package/src/tools/diagnostics/index.ts +6 -0
  314. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  315. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  316. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  317. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  318. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  319. package/src/tools/direct/index.ts +3 -0
  320. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  321. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  322. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  323. package/src/tools/discovery/index.ts +3 -0
  324. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  325. package/src/tools/dispatch/coreDispatch.ts +36 -31
  326. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  327. package/src/tools/dispatch/directDispatch.ts +11 -10
  328. package/src/tools/dispatch/goalDispatch.ts +5 -4
  329. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  330. package/src/tools/dispatch/validation.ts +78 -0
  331. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  332. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  333. package/src/tools/goals/index.ts +3 -0
  334. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  335. package/src/tools/registry.ts +17 -1261
  336. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  337. package/src/tools/release/index.ts +2 -0
  338. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  339. package/src/tools/tasks/cancelTask.ts +109 -0
  340. package/src/tools/{createTask.ts → tasks/createTask.ts} +49 -26
  341. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  342. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  343. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  344. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  345. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  346. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  347. package/src/tools/tasks/index.ts +18 -0
  348. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  349. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  350. package/src/tools/tasks/retryTask.ts +69 -0
  351. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +69 -34
  352. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  353. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  354. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  355. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  356. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  357. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  358. package/src/tools/workspace/index.ts +10 -0
  359. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  360. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  361. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  362. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  363. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  364. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  365. package/src/tools/workspace/syncFile.ts +292 -0
  366. package/src/utils/atomicFile.ts +58 -0
  367. package/src/utils/boundedFile.ts +116 -0
  368. package/src/utils/lockedJsonFile.ts +251 -0
  369. package/src/utils/platform.ts +31 -0
  370. package/src/utils/stableJson.ts +12 -0
  371. package/src/version.ts +3 -2
  372. package/src/watcherStatus.ts +19 -1
  373. package/ui/desktop-bootstrap.js +11 -0
  374. package/ui/desktop-bridge.js +36 -0
  375. package/ui/desktop.css +160 -0
  376. package/ui/getting-started.js +93 -0
  377. package/ui/i18n.js +484 -0
  378. package/ui/log-parser.js +30 -0
  379. package/ui/pages/audit.html +12 -0
  380. package/ui/pages/dashboard.html +90 -147
  381. package/ui/pages/direct-sessions.html +123 -26
  382. package/ui/pages/getting-started.html +43 -0
  383. package/ui/pages/logs.html +105 -22
  384. package/ui/pages/settings.html +101 -0
  385. package/ui/pages/task-detail.html +12 -0
  386. package/ui/pages/tasks.html +54 -8
  387. package/ui/pages/workspace.html +38 -1
  388. package/ui/settings.js +245 -0
  389. package/PatchWarden-Control.cmd +0 -6
  390. package/Restart-PatchWarden-Control.cmd +0 -6
  391. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  392. package/dist/test/unit/acceptance-engine.test.js +0 -228
  393. package/dist/test/unit/agent-router.test.d.ts +0 -1
  394. package/dist/test/unit/agent-router.test.js +0 -287
  395. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  396. package/dist/test/unit/android-doctor.test.js +0 -118
  397. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  398. package/dist/test/unit/apply-patch.test.js +0 -225
  399. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  400. package/dist/test/unit/audit-checks.test.js +0 -350
  401. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  402. package/dist/test/unit/chinese-path.test.js +0 -91
  403. package/dist/test/unit/command-guard.test.d.ts +0 -1
  404. package/dist/test/unit/command-guard.test.js +0 -160
  405. package/dist/test/unit/create-task.test.d.ts +0 -1
  406. package/dist/test/unit/create-task.test.js +0 -197
  407. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  408. package/dist/test/unit/diagnose-task.test.js +0 -457
  409. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  410. package/dist/test/unit/direct-guards.test.js +0 -328
  411. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  412. package/dist/test/unit/discovery-token-store.test.js +0 -139
  413. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  414. package/dist/test/unit/evidence-pack.test.js +0 -224
  415. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  416. package/dist/test/unit/get-task-status.test.js +0 -174
  417. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  418. package/dist/test/unit/get-task-summary.test.js +0 -146
  419. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  420. package/dist/test/unit/goal-graph.test.js +0 -298
  421. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  422. package/dist/test/unit/goal-progress.test.js +0 -381
  423. package/dist/test/unit/goal-report.test.d.ts +0 -1
  424. package/dist/test/unit/goal-report.test.js +0 -159
  425. package/dist/test/unit/goal-status.test.d.ts +0 -1
  426. package/dist/test/unit/goal-status.test.js +0 -215
  427. package/dist/test/unit/goal-store.test.d.ts +0 -1
  428. package/dist/test/unit/goal-store.test.js +0 -253
  429. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  430. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  431. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  432. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  433. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  434. package/dist/test/unit/handoff-export.test.js +0 -263
  435. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  436. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  437. package/dist/test/unit/logging.test.d.ts +0 -1
  438. package/dist/test/unit/logging.test.js +0 -397
  439. package/dist/test/unit/path-guard.test.d.ts +0 -1
  440. package/dist/test/unit/path-guard.test.js +0 -133
  441. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  442. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  443. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  444. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  445. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  446. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  447. package/dist/test/unit/release-gate.test.d.ts +0 -1
  448. package/dist/test/unit/release-gate.test.js +0 -242
  449. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  450. package/dist/test/unit/run-task-loop.test.js +0 -380
  451. package/dist/test/unit/safe-status.test.d.ts +0 -1
  452. package/dist/test/unit/safe-status.test.js +0 -165
  453. package/dist/test/unit/safe-views.test.d.ts +0 -1
  454. package/dist/test/unit/safe-views.test.js +0 -171
  455. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  456. package/dist/test/unit/schema-drift-check.test.js +0 -176
  457. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  458. package/dist/test/unit/sensitive-guard.test.js +0 -104
  459. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  460. package/dist/test/unit/spec-kit-import.test.js +0 -341
  461. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  462. package/dist/test/unit/subgoal-sync.test.js +0 -183
  463. package/dist/test/unit/sync-file.test.d.ts +0 -1
  464. package/dist/test/unit/sync-file.test.js +0 -154
  465. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  466. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  467. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  468. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  469. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  470. package/dist/test/unit/toolSearch.test.js +0 -571
  471. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  472. package/dist/test/unit/wait-for-task.test.js +0 -144
  473. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  474. package/dist/test/unit/watcher-status.test.js +0 -169
  475. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  476. package/dist/test/unit/worktree-manager.test.js +0 -176
  477. package/dist/tools/applyPatch.js +0 -41
  478. package/dist/tools/cancelTask.d.ts +0 -30
  479. package/dist/tools/cancelTask.js +0 -64
  480. package/dist/tools/finalizeDirectSession.js +0 -84
  481. package/dist/tools/goalSubgoalTask.js +0 -110
  482. package/dist/tools/killTask.d.ts +0 -15
  483. package/dist/tools/retryTask.js +0 -45
  484. package/dist/tools/runVerification.js +0 -36
  485. package/dist/tools/syncFile.js +0 -65
  486. package/docs/control-center/control-center-daily-driver.md +0 -211
  487. package/docs/control-center/control-center-mvp.md +0 -205
  488. package/docs/control-center/control-center-phase2.md +0 -159
  489. package/docs/execution-plan-2026-07-09.md +0 -315
  490. package/docs/release-v0.6.0.md +0 -71
  491. package/docs/release-v0.6.1.md +0 -75
  492. package/docs/release-v0.6.4.md +0 -45
  493. package/docs/roadmap-execution-and-acceptance.md +0 -365
  494. package/src/test/unit/acceptance-engine.test.ts +0 -261
  495. package/src/test/unit/agent-router.test.ts +0 -342
  496. package/src/test/unit/android-doctor.test.ts +0 -158
  497. package/src/test/unit/apply-patch.test.ts +0 -293
  498. package/src/test/unit/audit-checks.test.ts +0 -567
  499. package/src/test/unit/chinese-path.test.ts +0 -106
  500. package/src/test/unit/command-guard.test.ts +0 -221
  501. package/src/test/unit/create-task.test.ts +0 -255
  502. package/src/test/unit/diagnose-task.test.ts +0 -544
  503. package/src/test/unit/direct-guards.test.ts +0 -467
  504. package/src/test/unit/discovery-token-store.test.ts +0 -181
  505. package/src/test/unit/evidence-pack.test.ts +0 -251
  506. package/src/test/unit/get-task-status.test.ts +0 -203
  507. package/src/test/unit/get-task-summary.test.ts +0 -173
  508. package/src/test/unit/goal-graph.test.ts +0 -347
  509. package/src/test/unit/goal-progress.test.ts +0 -538
  510. package/src/test/unit/goal-report.test.ts +0 -189
  511. package/src/test/unit/goal-status.test.ts +0 -270
  512. package/src/test/unit/goal-store.test.ts +0 -318
  513. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  514. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  515. package/src/test/unit/handoff-export.test.ts +0 -295
  516. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  517. package/src/test/unit/logging.test.ts +0 -497
  518. package/src/test/unit/path-guard.test.ts +0 -180
  519. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  520. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  521. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  522. package/src/test/unit/release-gate.test.ts +0 -314
  523. package/src/test/unit/run-task-loop.test.ts +0 -425
  524. package/src/test/unit/safe-status.test.ts +0 -187
  525. package/src/test/unit/safe-views.test.ts +0 -184
  526. package/src/test/unit/schema-drift-check.test.ts +0 -260
  527. package/src/test/unit/sensitive-guard.test.ts +0 -124
  528. package/src/test/unit/spec-kit-import.test.ts +0 -429
  529. package/src/test/unit/subgoal-sync.test.ts +0 -236
  530. package/src/test/unit/sync-file.test.ts +0 -231
  531. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  532. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  533. package/src/test/unit/toolSearch.test.ts +0 -756
  534. package/src/test/unit/wait-for-task.test.ts +0 -176
  535. package/src/test/unit/watcher-status.test.ts +0 -190
  536. package/src/test/unit/worktree-manager.test.ts +0 -247
  537. package/src/tools/cancelTask.ts +0 -70
  538. package/src/tools/retryTask.ts +0 -53
  539. package/src/tools/syncFile.ts +0 -122
  540. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  541. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  542. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  543. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  544. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  545. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  546. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  547. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  548. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  549. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  550. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  551. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  552. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  553. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  554. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  555. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  556. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  557. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  558. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  559. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  560. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  561. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  562. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  563. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  564. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  565. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  566. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  567. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  568. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  569. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  570. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  571. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  572. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  573. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
@@ -14,7 +14,7 @@ import { existsSync, readFileSync } from "node:fs";
14
14
  import { isAbsolute, join } from "node:path";
15
15
  import { getConfig } from "../config.js";
16
16
  import { PatchWardenError } from "../errors.js";
17
- import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
17
+ import { mutateGoalStatus, readGoalStatus } from "./goalStore.js";
18
18
  import { updateSubgoalStatus } from "./goalStatus.js";
19
19
  import { getBlockedSubgoals } from "./goalGraph.js";
20
20
  // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
@@ -66,42 +66,45 @@ function readTaskStatus(taskId, workspaceRoot) {
66
66
  * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "subgoal_not_ready" | "invalid_status_transition"
67
67
  */
68
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",
69
+ return mutateGoalStatus(goalId, (goalStatus) => {
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,
86
94
  });
87
95
  }
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
- };
96
+ // 校验 3:通过状态机转换(要求 subgoal 当前为 done_by_agent)
97
+ const updatedGoalStatus = updateSubgoalStatus(goalStatus, subgoalId, "accepted");
98
+ const updatedSubgoal = updatedGoalStatus.subgoals.find((s) => s.id === subgoalId);
99
+ return {
100
+ next: updatedGoalStatus,
101
+ result: {
102
+ subgoal_id: subgoalId,
103
+ status: "accepted",
104
+ accepted_at: updatedSubgoal.accepted_at,
105
+ },
106
+ };
107
+ }, workspaceRoot);
105
108
  }
106
109
  /**
107
110
  * 拒绝子目标:从任意非终态(ready/running/done_by_agent/needs_fix)拒绝 subgoal。
@@ -117,40 +120,43 @@ export function acceptSubgoal(goalId, subgoalId, workspaceRoot) {
117
120
  * @throws {PatchWardenError} reason="goal_not_found" | "subgoal_not_found" | "invalid_status_transition"
118
121
  */
119
122
  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
- };
123
+ return mutateGoalStatus(goalId, (goalStatus) => {
124
+ const index = goalStatus.subgoals.findIndex((s) => s.id === subgoalId);
125
+ if (index === -1) {
126
+ 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 });
127
+ }
128
+ const current = goalStatus.subgoals[index];
129
+ // 终态检查:accepted 和 rejected 不允许再 reject
130
+ if (current.status === "accepted" || current.status === "rejected") {
131
+ 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, {
132
+ subgoal_id: subgoalId,
133
+ goal_id: goalId,
134
+ from_status: current.status,
135
+ to_status: "rejected",
136
+ });
137
+ }
138
+ // 手动更新 status 和 rejected_reason(绕过状态机)
139
+ const updatedSubgoal = {
140
+ ...current,
141
+ status: "rejected",
142
+ rejected_reason: reason,
143
+ };
144
+ const newSubgoals = [...goalStatus.subgoals];
145
+ newSubgoals[index] = updatedSubgoal;
146
+ const updatedGoalStatus = {
147
+ ...goalStatus,
148
+ subgoals: newSubgoals,
149
+ updated_at: new Date().toISOString(),
150
+ };
151
+ return {
152
+ next: updatedGoalStatus,
153
+ result: {
154
+ subgoal_id: subgoalId,
155
+ status: "rejected",
156
+ rejected_reason: reason,
157
+ },
158
+ };
159
+ }, workspaceRoot);
154
160
  }
155
161
  /**
156
162
  * 汇总 Goal 进度:统计各状态数量、完成率、阻塞子目标、风险项。
@@ -5,25 +5,18 @@
5
5
  * 生成 REPORT.md 和 report.json 到 {workspaceRoot}/.patchwarden/goals/{goalId}/report/。
6
6
  * 所有写入操作使用原子写(.tmp + rename),所有字符串内容经 redactSensitiveValue 脱敏。
7
7
  */
8
- import { mkdirSync, renameSync, writeFileSync } from "node:fs";
8
+ import { mkdirSync } from "node:fs";
9
9
  import { join, resolve } from "node:path";
10
10
  import { getConfig } from "../config.js";
11
11
  import { redactSensitiveValue } from "../security/contentRedaction.js";
12
- import { listEvidencePacks } from "../tools/evidencePack.js";
12
+ import { listEvidencePacks } from "../tools/tasks/evidencePack.js";
13
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
13
14
  import { summarizeGoalProgress } from "./goalProgress.js";
14
15
  import { readGoalStatus } from "./goalStore.js";
15
16
  // ── 辅助函数 ──────────────────────────────────────────────────────
16
17
  function resolveWorkspaceRoot(workspaceRoot) {
17
18
  return workspaceRoot ?? getConfig().workspaceRoot;
18
19
  }
19
- /**
20
- * 原子写文件:先写到 .tmp 文件,再 renameSync 到目标路径。
21
- */
22
- function atomicWriteFile(filePath, content) {
23
- const tmpPath = filePath + ".tmp";
24
- writeFileSync(tmpPath, content, "utf-8");
25
- renameSync(tmpPath, filePath);
26
- }
27
20
  /**
28
21
  * 将百分比字符串(如 "100%")解析为数字。
29
22
  */
@@ -196,9 +189,9 @@ export function exportGoalReport(goalId, options) {
196
189
  // 脱敏所有字符串内容
197
190
  const safeReport = redactSensitiveValue(rawReport).value;
198
191
  // 原子写 JSON(机器可读)
199
- atomicWriteFile(reportJsonPath, JSON.stringify(safeReport, null, 2) + "\n");
192
+ atomicWriteFileSync(reportJsonPath, JSON.stringify(safeReport, null, 2) + "\n");
200
193
  // 原子写 Markdown(人类可读,从脱敏后的对象生成)
201
194
  const markdown = buildReportMarkdown(safeReport, { noSubgoals, incomplete });
202
- atomicWriteFile(reportMdPath, markdown);
195
+ atomicWriteFileSync(reportMdPath, markdown);
203
196
  return safeReport;
204
197
  }
@@ -23,6 +23,8 @@ export interface Subgoal {
23
23
  external_ref?: string;
24
24
  /** Spec Kit task.files 提示(作用域文件路径列表) */
25
25
  scope_hints?: string[];
26
+ /** Managed git worktree associated with an isolated subgoal task. */
27
+ worktree_id?: string;
26
28
  }
27
29
  export interface GoalStatus {
28
30
  goal_id: string;
@@ -11,6 +11,7 @@
11
11
  * 所有更新函数返回新的 GoalStatus 对象,不修改原对象。
12
12
  */
13
13
  import { PatchWardenError } from "../errors.js";
14
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
14
15
  // ── 合法状态转换表 ────────────────────────────────────────────────
15
16
  const LEGAL_TRANSITIONS = {
16
17
  ready: ["running"],
@@ -29,7 +30,7 @@ export function createInitialGoalStatus(goalId, title, repoPath) {
29
30
  const now = new Date().toISOString();
30
31
  return {
31
32
  goal_id: goalId,
32
- title,
33
+ title: redactGoalText(title, 500),
33
34
  status: "active",
34
35
  repo_path: repoPath,
35
36
  created_at: now,
@@ -44,6 +45,9 @@ export function createInitialGoalStatus(goalId, title, repoPath) {
44
45
  * 不可变更新:返回新的 GoalStatus 对象。
45
46
  */
46
47
  export function addSubgoal(goalStatus, title, dependsOn = []) {
48
+ if (goalStatus.status !== "active") {
49
+ throw new PatchWardenError("goal_not_active", `Goal "${goalStatus.goal_id}" is ${goalStatus.status} and cannot accept new subgoals.`, "Create a new Goal Session for additional work.", true, { goal_id: goalStatus.goal_id, status: goalStatus.status });
50
+ }
47
51
  const existingIds = new Set(goalStatus.subgoals.map((s) => s.id));
48
52
  for (const depId of dependsOn) {
49
53
  if (!existingIds.has(depId)) {
@@ -54,7 +58,7 @@ export function addSubgoal(goalStatus, title, dependsOn = []) {
54
58
  const subgoalId = `subgoal-${String(nextNumber).padStart(3, "0")}`;
55
59
  const newSubgoal = {
56
60
  id: subgoalId,
57
- title,
61
+ title: redactGoalText(title, 500),
58
62
  status: "ready",
59
63
  depends_on: [...dependsOn],
60
64
  task_ids: [],
@@ -99,7 +103,7 @@ export function updateSubgoalStatus(goalStatus, subgoalId, newStatus, options) {
99
103
  if (!options || !options.rejected_reason) {
100
104
  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
105
  }
102
- updatedSubgoal.rejected_reason = options.rejected_reason;
106
+ updatedSubgoal.rejected_reason = redactGoalText(options.rejected_reason, 2000);
103
107
  }
104
108
  const newSubgoals = [...goalStatus.subgoals];
105
109
  newSubgoals[index] = updatedSubgoal;
@@ -109,6 +113,9 @@ export function updateSubgoalStatus(goalStatus, subgoalId, newStatus, options) {
109
113
  updated_at: new Date().toISOString(),
110
114
  };
111
115
  }
116
+ function redactGoalText(value, maxChars) {
117
+ return redactSensitiveContent(String(value || "")).content.slice(0, maxChars);
118
+ }
112
119
  /**
113
120
  * 将 taskId 关联到指定 subgoal(去重)。
114
121
  * 不可变更新:返回新的 GoalStatus 对象。
@@ -13,6 +13,8 @@
13
13
  * 默认从 getConfig().workspaceRoot 读取。
14
14
  */
15
15
  import { type GoalStatus, type Subgoal } from "./goalStatus.js";
16
+ /** Validate the storage key before it can participate in path construction. */
17
+ export declare function assertValidGoalId(goalId: string): void;
16
18
  /**
17
19
  * 生成 `goal_{YYYYMMDD}_{slug}` 格式的 goal id。
18
20
  * 冲突时追加 `_2`、`_3`... 直到唯一。
@@ -68,8 +70,7 @@ export declare function readGoal(goalId: string, workspaceRoot?: string): {
68
70
  subgoals: Subgoal[];
69
71
  };
70
72
  /**
71
- * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
72
- * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
73
+ * Replace goal_status.json under the shared cross-process mutation lock.
73
74
  */
74
75
  export declare function writeGoalStatus(goalId: string, status: GoalStatus, workspaceRoot?: string): void;
75
76
  /**
@@ -77,3 +78,9 @@ export declare function writeGoalStatus(goalId: string, status: GoalStatus, work
77
78
  * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
78
79
  */
79
80
  export declare function readGoalStatus(goalId: string, workspaceRoot?: string): GoalStatus;
81
+ export interface GoalStatusMutation<R> {
82
+ next?: GoalStatus;
83
+ result: R;
84
+ }
85
+ export declare function mutateGoalStatus<R>(goalId: string, mutation: (current: GoalStatus) => GoalStatusMutation<R>, workspaceRoot?: string): R;
86
+ export declare function mutateGoalStatusAsync<R>(goalId: string, mutation: (current: GoalStatus) => Promise<GoalStatusMutation<R>>, workspaceRoot?: string): Promise<R>;
@@ -12,16 +12,29 @@
12
12
  * 所有文件系统函数都接受可选的 workspaceRoot 参数用于测试;
13
13
  * 默认从 getConfig().workspaceRoot 读取。
14
14
  */
15
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, statSync, } from "node:fs";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, } from "node:fs";
16
16
  import { join } from "node:path";
17
17
  import { getConfig } from "../config.js";
18
- import { guardWorkspacePath } from "../security/pathGuard.js";
18
+ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
19
19
  import { PatchWardenError } from "../errors.js";
20
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
21
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
22
+ import { mutateLockedJsonFileSync, readJsonObjectFileSync, withFileLock, withFileLockSync, } from "../utils/lockedJsonFile.js";
20
23
  import { createInitialGoalStatus, } from "./goalStatus.js";
21
24
  // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
22
25
  function resolveWorkspaceRoot(workspaceRoot) {
23
26
  return workspaceRoot ?? getConfig().workspaceRoot;
24
27
  }
28
+ const GOALS_PREFIX = join(".patchwarden", "goals");
29
+ const GOAL_ID_MAX_LENGTH = 128;
30
+ const GOAL_ID_PATTERN = /^goal_[A-Za-z0-9][A-Za-z0-9_-]*$/;
31
+ /** Validate the storage key before it can participate in path construction. */
32
+ export function assertValidGoalId(goalId) {
33
+ if (goalId.length > GOAL_ID_MAX_LENGTH ||
34
+ !GOAL_ID_PATTERN.test(goalId)) {
35
+ throw new PatchWardenError("invalid_goal_id", `Invalid goal id. Expected "goal_" followed by at most ${GOAL_ID_MAX_LENGTH - 5} ASCII letters, digits, underscores, or hyphens.`, "Use the goal_id returned by create_goal or list_goals.", true, { goal_id_length: goalId.length });
36
+ }
37
+ }
25
38
  // ── Goal ID 生成 ──────────────────────────────────────────────────
26
39
  /**
27
40
  * 从 title 生成 slug:小写、非字母数字字符替换为 `_`、合并连续 `_`、去除首尾 `_`、截断到 30 字符。
@@ -64,13 +77,16 @@ export function generateGoalId(title, existingIds) {
64
77
  * 不自动创建目录。
65
78
  */
66
79
  export function getGoalsDir(workspaceRoot) {
67
- return join(resolveWorkspaceRoot(workspaceRoot), ".patchwarden", "goals");
80
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
81
+ return guardPath(join(wsRoot, GOALS_PREFIX), wsRoot, GOALS_PREFIX);
68
82
  }
69
83
  /**
70
84
  * 返回 `{getGoalsDir()}/{goalId}` 路径。
71
85
  */
72
86
  export function getGoalDir(goalId, workspaceRoot) {
73
- return join(getGoalsDir(workspaceRoot), goalId);
87
+ assertValidGoalId(goalId);
88
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
89
+ return guardPath(join(getGoalsDir(wsRoot), goalId), wsRoot, GOALS_PREFIX);
74
90
  }
75
91
  // ── CRUD ──────────────────────────────────────────────────────────
76
92
  /**
@@ -82,11 +98,21 @@ export function getGoalDir(goalId, workspaceRoot) {
82
98
  * 返回 { goal_id, goal_dir }。
83
99
  */
84
100
  export function createGoal(repoPath, title, description, workspaceRoot) {
101
+ const safeTitle = redactSensitiveContent(title)
102
+ .content
103
+ .replace(/[\r\n\t]+/g, " ")
104
+ .trim()
105
+ .slice(0, 500) || "Untitled goal";
106
+ const safeDescription = redactSensitiveContent(description).content;
107
+ if (Buffer.byteLength(safeDescription, "utf-8") > 1024 * 1024) {
108
+ throw new PatchWardenError("goal_description_too_large", "Goal description exceeds the 1 MiB limit.", "Split the work into smaller Goal Sessions.");
109
+ }
85
110
  const wsRoot = resolveWorkspaceRoot(workspaceRoot);
86
111
  const guardedRepo = guardWorkspacePath(repoPath, wsRoot);
87
112
  const goalsDir = getGoalsDir(workspaceRoot);
88
- const existingIds = [];
89
- if (existsSync(goalsDir)) {
113
+ mkdirSync(goalsDir, { recursive: true });
114
+ return withFileLockSync(join(goalsDir, "goal-creation"), () => {
115
+ const existingIds = [];
90
116
  for (const entry of readdirSync(goalsDir)) {
91
117
  const entryPath = join(goalsDir, entry);
92
118
  try {
@@ -95,35 +121,32 @@ export function createGoal(repoPath, title, description, workspaceRoot) {
95
121
  }
96
122
  }
97
123
  catch {
98
- // 跳过无法 stat 的条目
124
+ // Skip entries that cannot be inspected.
99
125
  }
100
126
  }
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
+ const goalId = generateGoalId(safeTitle, existingIds);
128
+ const goalDir = getGoalDir(goalId, wsRoot);
129
+ mkdirSync(goalDir);
130
+ mkdirSync(join(goalDir, "tasks"));
131
+ mkdirSync(join(goalDir, "artifacts"));
132
+ const now = new Date().toISOString();
133
+ const goalMd = [
134
+ `# ${safeTitle}`,
135
+ "",
136
+ safeDescription,
137
+ "",
138
+ `- Created: ${now}`,
139
+ `- Repo: ${guardedRepo}`,
140
+ "- Status: active",
141
+ "",
142
+ ].join("\n");
143
+ atomicWriteFileSync(join(goalDir, "GOAL.md"), goalMd);
144
+ const goalsMd = `# Subgoals: ${safeTitle}\n\n_No subgoals yet._\n`;
145
+ atomicWriteFileSync(join(goalDir, "GOALS.md"), goalsMd);
146
+ const status = createInitialGoalStatus(goalId, safeTitle, guardedRepo);
147
+ atomicWriteJsonFileSync(join(goalDir, "goal_status.json"), status);
148
+ return { goal_id: goalId, goal_dir: goalDir };
149
+ }, goalBusyOptions("creation"));
127
150
  }
128
151
  /**
129
152
  * 列出所有 goal 的摘要信息,按 updated_at 降序排列。
@@ -136,8 +159,8 @@ export function listGoals(workspaceRoot) {
136
159
  }
137
160
  const results = [];
138
161
  for (const entry of readdirSync(goalsDir)) {
139
- const entryPath = join(goalsDir, entry);
140
162
  try {
163
+ const entryPath = getGoalDir(entry, workspaceRoot);
141
164
  if (!statSync(entryPath).isDirectory()) {
142
165
  continue;
143
166
  }
@@ -172,7 +195,8 @@ export function readGoal(goalId, workspaceRoot) {
172
195
  let goalDescription = "";
173
196
  const goalMdPath = join(goalDir, "GOAL.md");
174
197
  if (existsSync(goalMdPath)) {
175
- goalDescription = readFileSync(goalMdPath, "utf-8");
198
+ const safeGoalMdPath = guardPath(goalMdPath, resolveWorkspaceRoot(workspaceRoot), GOALS_PREFIX);
199
+ goalDescription = readFileSync(safeGoalMdPath, "utf-8");
176
200
  }
177
201
  return {
178
202
  goal_id: status.goal_id,
@@ -186,26 +210,109 @@ export function readGoal(goalId, workspaceRoot) {
186
210
  };
187
211
  }
188
212
  /**
189
- * 原子写入 goal_status.json:先写到 .tmp 文件,再 renameSync。
190
- * JSON 序列化用 JSON.stringify(status, null, 2) + 末尾换行。
213
+ * Replace goal_status.json under the shared cross-process mutation lock.
191
214
  */
192
215
  export function writeGoalStatus(goalId, status, workspaceRoot) {
193
216
  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);
217
+ if (!existsSync(goalDir)) {
218
+ throw new PatchWardenError("goal_not_found", `Goal directory not found: "${goalDir}"`, "Ensure the goal has been created before writing its status.", true, { goal_id: goalId, goal_dir: goalDir });
219
+ }
220
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
221
+ if (!existsSync(statusPath)) {
222
+ withFileLockSync(statusPath, () => {
223
+ atomicWriteJsonFileSync(statusPath, normalizeGoalLifecycle(validateGoalStatus(goalId, status, workspaceRoot)));
224
+ }, goalBusyOptions(goalId));
225
+ return;
226
+ }
227
+ mutateGoalStatus(goalId, () => ({ next: status, result: undefined }), workspaceRoot);
198
228
  }
199
229
  /**
200
230
  * 读取 goal_status.json 并 JSON.parse。
201
231
  * 如果文件不存在,抛出 PatchWardenError("goal_not_found")。
202
232
  */
203
233
  export function readGoalStatus(goalId, workspaceRoot) {
204
- const goalDir = getGoalDir(goalId, workspaceRoot);
205
- const statusPath = join(goalDir, "goal_status.json");
234
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
206
235
  if (!existsSync(statusPath)) {
207
236
  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
237
  }
209
- const raw = readFileSync(statusPath, "utf-8");
210
- return JSON.parse(raw);
238
+ let parsed;
239
+ try {
240
+ parsed = readJsonObjectFileSync(statusPath);
241
+ }
242
+ catch {
243
+ throw invalidGoalStatus(goalId, "goal_status.json is not valid JSON");
244
+ }
245
+ return validateGoalStatus(goalId, parsed, workspaceRoot);
246
+ }
247
+ export function mutateGoalStatus(goalId, mutation, workspaceRoot) {
248
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
249
+ ensureGoalStatusExists(goalId, statusPath);
250
+ return mutateLockedJsonFileSync(statusPath, (raw) => {
251
+ const current = validateGoalStatus(goalId, raw, workspaceRoot);
252
+ const outcome = mutation(current);
253
+ if (!outcome.next)
254
+ return outcome;
255
+ return {
256
+ next: normalizeGoalLifecycle(validateGoalStatus(goalId, outcome.next, workspaceRoot)),
257
+ result: outcome.result,
258
+ };
259
+ }, goalBusyOptions(goalId));
260
+ }
261
+ export async function mutateGoalStatusAsync(goalId, mutation, workspaceRoot) {
262
+ const statusPath = getGoalStatusPath(goalId, workspaceRoot);
263
+ ensureGoalStatusExists(goalId, statusPath);
264
+ return withFileLock(statusPath, async () => {
265
+ const current = validateGoalStatus(goalId, readJsonObjectFileSync(statusPath), workspaceRoot);
266
+ const outcome = await mutation(current);
267
+ if (outcome.next) {
268
+ atomicWriteJsonFileSync(statusPath, normalizeGoalLifecycle(validateGoalStatus(goalId, outcome.next, workspaceRoot)));
269
+ }
270
+ return outcome.result;
271
+ }, goalBusyOptions(goalId));
272
+ }
273
+ function getGoalStatusPath(goalId, workspaceRoot) {
274
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
275
+ return guardPath(join(getGoalDir(goalId, wsRoot), "goal_status.json"), wsRoot, GOALS_PREFIX);
276
+ }
277
+ function ensureGoalStatusExists(goalId, statusPath) {
278
+ if (!existsSync(statusPath)) {
279
+ 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 updating it.", true, { goal_id: goalId, status_path: statusPath });
280
+ }
281
+ }
282
+ function validateGoalStatus(goalId, value, workspaceRoot) {
283
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
284
+ throw invalidGoalStatus(goalId, "expected a JSON object");
285
+ }
286
+ const candidate = value;
287
+ if (candidate.goal_id !== goalId) {
288
+ throw invalidGoalStatus(goalId, "goal_id does not match its directory");
289
+ }
290
+ if (typeof candidate.title !== "string" || typeof candidate.repo_path !== "string") {
291
+ throw invalidGoalStatus(goalId, "title or repo_path is missing");
292
+ }
293
+ if (candidate.status !== "active" && candidate.status !== "completed" && candidate.status !== "abandoned") {
294
+ throw invalidGoalStatus(goalId, "status is invalid");
295
+ }
296
+ if (!Array.isArray(candidate.subgoals)) {
297
+ throw invalidGoalStatus(goalId, "subgoals must be an array");
298
+ }
299
+ guardWorkspacePath(candidate.repo_path, resolveWorkspaceRoot(workspaceRoot));
300
+ return candidate;
301
+ }
302
+ function normalizeGoalLifecycle(status) {
303
+ if (status.status === "active" &&
304
+ status.subgoals.length > 0 &&
305
+ status.subgoals.every((subgoal) => subgoal.status === "accepted")) {
306
+ return { ...status, status: "completed", updated_at: new Date().toISOString() };
307
+ }
308
+ return status;
309
+ }
310
+ function invalidGoalStatus(goalId, detail) {
311
+ return new PatchWardenError("invalid_goal_status", `Invalid goal status for "${goalId}": ${detail}.`, "Recreate the Goal Session or restore a valid goal_status.json artifact.", true, { goal_id: goalId });
312
+ }
313
+ function goalBusyOptions(goalId) {
314
+ return {
315
+ waitMs: 0,
316
+ busyError: () => new PatchWardenError("goal_busy", `Goal "${goalId}" is currently being updated.`, "Retry after the active Goal operation finishes.", true, { goal_id: goalId }),
317
+ };
211
318
  }
@@ -7,9 +7,10 @@
7
7
  * 以避免与 goalStore 形成循环依赖。
8
8
  */
9
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";
10
+ import { getGoalDir } from "./goalStore.js";
11
+ import { mkdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
13
14
  // ── Markdown 生成 ────────────────────────────────────────────────
14
15
  /**
15
16
  * 生成 Goal Session 交接文档(Markdown 字符串)。
@@ -170,11 +171,10 @@ export function generateHandoff(goalId, goalStatus, recentDiff, recentTestResult
170
171
  */
171
172
  export function exportHandoff(goalId, goalStatus, workspaceRoot) {
172
173
  const content = generateHandoff(goalId, goalStatus);
173
- const root = workspaceRoot ?? getConfig().workspaceRoot;
174
- const goalDir = resolve(root, ".patchwarden", "goals", goalId);
174
+ const goalDir = getGoalDir(goalId, workspaceRoot);
175
175
  mkdirSync(goalDir, { recursive: true });
176
176
  const handoffPath = join(goalDir, "handoff.md");
177
- writeFileSync(handoffPath, content, "utf-8");
177
+ atomicWriteFileSync(handoffPath, content);
178
178
  const preview = content.length > 500 ? content.slice(0, 500) + "..." : content;
179
179
  return {
180
180
  handoff_path: handoffPath,