patchwarden 1.5.1 → 1.6.2

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 (591) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +158 -26
  3. package/README.md +124 -24
  4. package/dist/assessments/agentAssessor.js +21 -12
  5. package/dist/assessments/assessmentDiagnostics.d.ts +29 -0
  6. package/dist/assessments/assessmentDiagnostics.js +132 -0
  7. package/dist/assessments/assessmentStore.d.ts +39 -0
  8. package/dist/assessments/assessmentStore.js +207 -45
  9. package/dist/assessments/securitySnapshot.d.ts +50 -0
  10. package/dist/assessments/securitySnapshot.js +158 -0
  11. package/dist/config.d.ts +15 -0
  12. package/dist/config.js +66 -13
  13. package/dist/control/fileManager.d.ts +2 -0
  14. package/dist/control/fileManager.js +39 -0
  15. package/dist/control/middleware/auth.d.ts +6 -0
  16. package/dist/control/middleware/auth.js +14 -5
  17. package/dist/control/middleware/static.js +16 -3
  18. package/dist/control/routeTable.d.ts +24 -0
  19. package/dist/control/routeTable.js +263 -0
  20. package/dist/control/routes/audit.js +17 -7
  21. package/dist/control/routes/evidence.js +1 -1
  22. package/dist/control/routes/lineage.js +1 -1
  23. package/dist/control/routes/process.d.ts +7 -0
  24. package/dist/control/routes/process.js +329 -42
  25. package/dist/control/routes/sessions.d.ts +9 -1
  26. package/dist/control/routes/sessions.js +84 -21
  27. package/dist/control/routes/status.d.ts +60 -1
  28. package/dist/control/routes/status.js +94 -15
  29. package/dist/control/routes/taskActions.js +24 -60
  30. package/dist/control/routes/tasks.d.ts +4 -0
  31. package/dist/control/routes/tasks.js +61 -62
  32. package/dist/control/routes/workspace.js +50 -23
  33. package/dist/control/runtime.d.ts +3 -2
  34. package/dist/control/runtime.js +65 -40
  35. package/dist/control/server.js +37 -365
  36. package/dist/control/shared.d.ts +25 -1
  37. package/dist/control/shared.js +64 -11
  38. package/dist/diagnostics/allowedTestCommandSafety.d.ts +1 -0
  39. package/dist/diagnostics/allowedTestCommandSafety.js +11 -0
  40. package/dist/direct/directAudit.js +234 -225
  41. package/dist/direct/directGuards.d.ts +2 -1
  42. package/dist/direct/directGuards.js +42 -29
  43. package/dist/direct/directPatch.d.ts +6 -1
  44. package/dist/direct/directPatch.js +55 -8
  45. package/dist/direct/directSessionStore.d.ts +16 -6
  46. package/dist/direct/directSessionStore.js +128 -35
  47. package/dist/direct/directVerification.js +8 -16
  48. package/dist/doctor.js +65 -57
  49. package/dist/goal/acceptanceEngine.d.ts +1 -1
  50. package/dist/goal/goalProgress.js +75 -69
  51. package/dist/goal/goalReport.js +5 -12
  52. package/dist/goal/goalStatus.d.ts +2 -0
  53. package/dist/goal/goalStatus.js +10 -3
  54. package/dist/goal/goalStore.d.ts +9 -2
  55. package/dist/goal/goalStore.js +152 -45
  56. package/dist/goal/handoffExport.js +6 -6
  57. package/dist/goal/specKitImport.js +97 -96
  58. package/dist/goal/subgoalSync.js +11 -9
  59. package/dist/goal/worktreeManager.d.ts +2 -0
  60. package/dist/goal/worktreeManager.js +209 -136
  61. package/dist/httpServer.js +111 -32
  62. package/dist/logging.js +5 -3
  63. package/dist/policy/projectPolicy.js +5 -2
  64. package/dist/release/releaseGate.d.ts +1 -1
  65. package/dist/release/releaseGate.js +78 -25
  66. package/dist/runner/agentInvocation.d.ts +14 -0
  67. package/dist/runner/agentInvocation.js +96 -4
  68. package/dist/runner/changeCapture.d.ts +13 -5
  69. package/dist/runner/changeCapture.js +243 -61
  70. package/dist/runner/postTaskCleanup.js +48 -19
  71. package/dist/runner/processSecurity.d.ts +56 -0
  72. package/dist/runner/processSecurity.js +373 -0
  73. package/dist/runner/runTask.d.ts +2 -1
  74. package/dist/runner/runTask.js +162 -93
  75. package/dist/runner/simpleProcess.d.ts +3 -0
  76. package/dist/runner/simpleProcess.js +50 -36
  77. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  78. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  79. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +3 -1
  80. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  81. package/dist/runner/taskStatusStore.d.ts +11 -0
  82. package/dist/runner/taskStatusStore.js +35 -0
  83. package/dist/runner/watch.d.ts +14 -1
  84. package/dist/runner/watch.js +320 -49
  85. package/dist/security/contentRedaction.js +2 -2
  86. package/dist/security/discoveryTokenStore.d.ts +3 -2
  87. package/dist/security/discoveryTokenStore.js +17 -2
  88. package/dist/security/loopbackHost.d.ts +2 -0
  89. package/dist/security/loopbackHost.js +16 -0
  90. package/dist/security/pathGuard.js +32 -17
  91. package/dist/security/planGuard.js +2 -1
  92. package/dist/security/runtimeGuard.js +9 -6
  93. package/dist/security/secretComparison.d.ts +3 -0
  94. package/dist/security/secretComparison.js +11 -0
  95. package/dist/security/sensitiveGuard.d.ts +5 -0
  96. package/dist/security/sensitiveGuard.js +27 -8
  97. package/dist/security/toolInvocationGuard.d.ts +2 -2
  98. package/dist/security/toolInvocationGuard.js +1 -1
  99. package/dist/security/workspaceRootGuard.d.ts +8 -0
  100. package/dist/security/workspaceRootGuard.js +47 -0
  101. package/dist/smoke-test.js +27 -26
  102. package/dist/tools/catalog/index.d.ts +4 -0
  103. package/dist/tools/catalog/index.js +4 -0
  104. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  105. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  106. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +6 -15
  107. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  108. package/dist/tools/definitions/toolDefs.js +1177 -0
  109. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  110. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  111. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +2 -1
  112. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +134 -36
  113. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +18 -6
  114. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +36 -8
  115. package/dist/tools/diagnostics/index.d.ts +6 -0
  116. package/dist/tools/diagnostics/index.js +6 -0
  117. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  118. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  119. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +12 -8
  120. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +23 -13
  121. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  122. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  123. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +6 -6
  124. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +1 -1
  125. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  126. package/dist/tools/direct/index.d.ts +3 -0
  127. package/dist/tools/direct/index.js +3 -0
  128. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  129. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  130. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  131. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  132. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  133. package/dist/tools/discovery/index.d.ts +3 -0
  134. package/dist/tools/discovery/index.js +3 -0
  135. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  136. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  137. package/dist/tools/dispatch/coreDispatch.js +32 -31
  138. package/dist/tools/dispatch/diagnosticDispatch.d.ts +3 -5
  139. package/dist/tools/dispatch/diagnosticDispatch.js +70 -68
  140. package/dist/tools/dispatch/directDispatch.js +11 -10
  141. package/dist/tools/dispatch/goalDispatch.js +5 -4
  142. package/dist/tools/dispatch/releaseDispatch.js +3 -2
  143. package/dist/tools/dispatch/validation.d.ts +7 -0
  144. package/dist/tools/dispatch/validation.js +71 -0
  145. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  146. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +1 -1
  147. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  148. package/dist/tools/goals/index.d.ts +3 -0
  149. package/dist/tools/goals/index.js +3 -0
  150. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  151. package/dist/tools/registry.d.ts +7 -11
  152. package/dist/tools/registry.js +17 -1178
  153. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  154. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  155. package/dist/tools/release/index.d.ts +2 -0
  156. package/dist/tools/release/index.js +2 -0
  157. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  158. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  159. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  160. package/dist/tools/tasks/cancelTask.js +86 -0
  161. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  162. package/dist/tools/{createTask.js → tasks/createTask.js} +110 -34
  163. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  164. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +12 -16
  165. package/dist/tools/{evidencePack.js → tasks/evidencePack.js} +34 -22
  166. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  167. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  168. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +11 -6
  169. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  170. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +35 -36
  171. package/dist/tools/tasks/index.d.ts +18 -0
  172. package/dist/tools/tasks/index.js +18 -0
  173. package/dist/tools/tasks/killTask.d.ts +1 -0
  174. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  175. package/dist/tools/{listTasks.js → tasks/listTasks.js} +5 -4
  176. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +3 -2
  177. package/dist/tools/tasks/reconcileTasks.js +496 -0
  178. package/dist/tools/tasks/retryTask.js +60 -0
  179. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  180. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +63 -41
  181. package/dist/tools/tasks/runVerification.js +38 -0
  182. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  183. package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +2 -2
  184. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  185. package/dist/tools/tasks/taskStates.d.ts +6 -0
  186. package/dist/tools/tasks/taskStates.js +52 -0
  187. package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +3 -11
  188. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  189. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  190. package/dist/tools/workspace/applyPatch.js +53 -0
  191. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  192. package/dist/tools/workspace/index.d.ts +10 -0
  193. package/dist/tools/workspace/index.js +10 -0
  194. package/dist/tools/workspace/listAgents.d.ts +23 -0
  195. package/dist/tools/workspace/listAgents.js +56 -0
  196. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  197. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  198. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  199. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  200. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  201. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  202. package/dist/tools/workspace/syncFile.js +134 -0
  203. package/dist/utils/atomicFile.d.ts +7 -0
  204. package/dist/utils/atomicFile.js +67 -0
  205. package/dist/utils/boundedFile.d.ts +8 -0
  206. package/dist/utils/boundedFile.js +93 -0
  207. package/dist/utils/lockedJsonFile.d.ts +15 -0
  208. package/dist/utils/lockedJsonFile.js +224 -0
  209. package/dist/utils/platform.d.ts +11 -0
  210. package/dist/utils/platform.js +26 -0
  211. package/dist/utils/stableJson.d.ts +2 -0
  212. package/dist/utils/stableJson.js +12 -0
  213. package/dist/version.d.ts +3 -2
  214. package/dist/version.js +3 -2
  215. package/dist/watcherStatus.d.ts +2 -2
  216. package/dist/watcherStatus.js +18 -0
  217. package/docs/CODE_WIKI.md +808 -143
  218. package/docs/architecture.md +15 -55
  219. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  220. package/docs/control-center/README.md +4 -4
  221. package/docs/dashboard-overview.md +4 -4
  222. package/docs/desktop-app.md +175 -0
  223. package/docs/evidence-pack-schema.md +3 -3
  224. package/docs/open-source-application.md +16 -11
  225. package/docs/release-checklist.md +1 -1
  226. package/docs/release-evidence.md +35 -39
  227. package/docs/task-safe-review-workflow.md +5 -5
  228. package/docs/threat-model.md +19 -1
  229. package/examples/config.example.json +4 -2
  230. package/package.json +27 -10
  231. package/scripts/README.md +15 -6
  232. package/scripts/build.js +11 -0
  233. package/scripts/checks/brand-check.js +3 -0
  234. package/scripts/checks/build-output-check.js +37 -0
  235. package/scripts/checks/control-center-smoke.js +102 -10
  236. package/scripts/checks/control-smoke.js +19 -8
  237. package/scripts/checks/http-mcp-smoke.js +82 -4
  238. package/scripts/checks/lifecycle-smoke.js +15 -12
  239. package/scripts/checks/mcp-manifest-check.js +1 -1
  240. package/scripts/checks/mcp-smoke.js +58 -4
  241. package/scripts/checks/package-manifest-check.js +20 -7
  242. package/scripts/checks/unit-tests.js +3 -5
  243. package/scripts/checks/watcher-supervisor-smoke.js +38 -1
  244. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  245. package/scripts/control/manage-patchwarden.ps1 +69 -4
  246. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  247. package/scripts/control/run-background-supervisor.ps1 +41 -0
  248. package/scripts/control/start-patchwarden-tunnel.ps1 +191 -33
  249. package/scripts/e2e/demo-runtime.mjs +153 -0
  250. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  251. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  252. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  253. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  254. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  255. package/scripts/lib/clean-generated-output.js +11 -0
  256. package/scripts/lib/file-discovery.js +12 -0
  257. package/scripts/release/desktop-preflight.js +211 -0
  258. package/scripts/release/pack-clean.js +59 -14
  259. package/src/assessments/agentAssessor.ts +26 -12
  260. package/src/assessments/assessmentDiagnostics.ts +172 -0
  261. package/src/assessments/assessmentStore.ts +288 -51
  262. package/src/assessments/securitySnapshot.ts +231 -0
  263. package/src/config.ts +70 -13
  264. package/src/control/fileManager.ts +40 -0
  265. package/src/control/middleware/auth.ts +22 -4
  266. package/src/control/middleware/static.ts +16 -3
  267. package/src/control/routeTable.ts +301 -0
  268. package/src/control/routes/audit.ts +14 -6
  269. package/src/control/routes/evidence.ts +1 -1
  270. package/src/control/routes/lineage.ts +1 -1
  271. package/src/control/routes/process.ts +341 -40
  272. package/src/control/routes/sessions.ts +88 -25
  273. package/src/control/routes/status.ts +134 -17
  274. package/src/control/routes/taskActions.ts +24 -58
  275. package/src/control/routes/tasks.ts +66 -58
  276. package/src/control/routes/workspace.ts +56 -25
  277. package/src/control/runtime.ts +71 -43
  278. package/src/control/server.ts +37 -342
  279. package/src/control/shared.ts +77 -11
  280. package/src/diagnostics/allowedTestCommandSafety.ts +12 -0
  281. package/src/direct/directAudit.ts +17 -9
  282. package/src/direct/directGuards.ts +72 -44
  283. package/src/direct/directPatch.ts +95 -8
  284. package/src/direct/directSessionStore.ts +213 -55
  285. package/src/direct/directVerification.ts +9 -23
  286. package/src/doctor.ts +76 -64
  287. package/src/goal/acceptanceEngine.ts +1 -1
  288. package/src/goal/goalProgress.ts +17 -13
  289. package/src/goal/goalReport.ts +5 -13
  290. package/src/goal/goalStatus.ts +19 -3
  291. package/src/goal/goalStore.ts +241 -54
  292. package/src/goal/handoffExport.ts +6 -6
  293. package/src/goal/specKitImport.ts +12 -12
  294. package/src/goal/subgoalSync.ts +11 -8
  295. package/src/goal/worktreeManager.ts +139 -22
  296. package/src/httpServer.ts +113 -30
  297. package/src/logging.ts +10 -3
  298. package/src/policy/projectPolicy.ts +5 -2
  299. package/src/release/releaseGate.ts +74 -27
  300. package/src/runner/agentInvocation.ts +126 -4
  301. package/src/runner/changeCapture.ts +291 -57
  302. package/src/runner/postTaskCleanup.ts +41 -17
  303. package/src/runner/processSecurity.ts +433 -0
  304. package/src/runner/runTask.ts +197 -97
  305. package/src/runner/simpleProcess.ts +66 -31
  306. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  307. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +6 -3
  308. package/src/runner/taskStatusStore.ts +69 -0
  309. package/src/runner/watch.ts +348 -52
  310. package/src/security/contentRedaction.ts +2 -2
  311. package/src/security/discoveryTokenStore.ts +18 -4
  312. package/src/security/loopbackHost.ts +19 -0
  313. package/src/security/pathGuard.ts +35 -16
  314. package/src/security/planGuard.ts +2 -1
  315. package/src/security/runtimeGuard.ts +9 -6
  316. package/src/security/secretComparison.ts +13 -0
  317. package/src/security/sensitiveGuard.ts +27 -8
  318. package/src/security/toolInvocationGuard.ts +2 -2
  319. package/src/security/workspaceRootGuard.ts +53 -0
  320. package/src/smoke-test.ts +27 -26
  321. package/src/tools/catalog/index.ts +4 -0
  322. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  323. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +7 -16
  324. package/src/tools/definitions/toolDefs.ts +1264 -0
  325. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  326. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +136 -39
  327. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +37 -9
  328. package/src/tools/diagnostics/index.ts +6 -0
  329. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  330. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +27 -17
  331. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  332. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +6 -6
  333. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +7 -5
  334. package/src/tools/direct/index.ts +3 -0
  335. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  336. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  337. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  338. package/src/tools/discovery/index.ts +3 -0
  339. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  340. package/src/tools/dispatch/coreDispatch.ts +36 -31
  341. package/src/tools/dispatch/diagnosticDispatch.ts +26 -21
  342. package/src/tools/dispatch/directDispatch.ts +11 -10
  343. package/src/tools/dispatch/goalDispatch.ts +5 -4
  344. package/src/tools/dispatch/releaseDispatch.ts +3 -2
  345. package/src/tools/dispatch/validation.ts +78 -0
  346. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  347. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +41 -26
  348. package/src/tools/goals/index.ts +3 -0
  349. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  350. package/src/tools/registry.ts +17 -1261
  351. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  352. package/src/tools/release/index.ts +2 -0
  353. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  354. package/src/tools/tasks/cancelTask.ts +110 -0
  355. package/src/tools/{createTask.ts → tasks/createTask.ts} +161 -33
  356. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +12 -16
  357. package/src/tools/{evidencePack.ts → tasks/evidencePack.ts} +36 -22
  358. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  359. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +13 -6
  360. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  361. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +46 -44
  362. package/src/tools/tasks/index.ts +18 -0
  363. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +6 -5
  364. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +228 -77
  365. package/src/tools/tasks/retryTask.ts +69 -0
  366. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +73 -46
  367. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  368. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  369. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  370. package/src/tools/tasks/taskStates.ts +54 -0
  371. package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +3 -12
  372. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  373. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  374. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  375. package/src/tools/workspace/index.ts +10 -0
  376. package/src/tools/workspace/listAgents.ts +75 -0
  377. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  378. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  379. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  380. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  381. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  382. package/src/tools/workspace/syncFile.ts +292 -0
  383. package/src/utils/atomicFile.ts +74 -0
  384. package/src/utils/boundedFile.ts +116 -0
  385. package/src/utils/lockedJsonFile.ts +251 -0
  386. package/src/utils/platform.ts +31 -0
  387. package/src/utils/stableJson.ts +12 -0
  388. package/src/version.ts +3 -2
  389. package/src/watcherStatus.ts +19 -1
  390. package/ui/desktop-bootstrap.js +11 -0
  391. package/ui/desktop-bridge.js +36 -0
  392. package/ui/desktop.css +160 -0
  393. package/ui/getting-started.js +93 -0
  394. package/ui/i18n.js +484 -0
  395. package/ui/log-parser.js +30 -0
  396. package/ui/pages/audit.html +12 -0
  397. package/ui/pages/dashboard.html +94 -147
  398. package/ui/pages/direct-sessions.html +123 -26
  399. package/ui/pages/getting-started.html +43 -0
  400. package/ui/pages/logs.html +134 -22
  401. package/ui/pages/settings.html +101 -0
  402. package/ui/pages/task-detail.html +12 -0
  403. package/ui/pages/tasks.html +54 -8
  404. package/ui/pages/workspace.html +38 -1
  405. package/ui/settings.js +254 -0
  406. package/PatchWarden-Control.cmd +0 -6
  407. package/Restart-PatchWarden-Control.cmd +0 -6
  408. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  409. package/dist/test/unit/acceptance-engine.test.js +0 -228
  410. package/dist/test/unit/agent-router.test.d.ts +0 -1
  411. package/dist/test/unit/agent-router.test.js +0 -287
  412. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  413. package/dist/test/unit/android-doctor.test.js +0 -118
  414. package/dist/test/unit/apply-patch.test.d.ts +0 -1
  415. package/dist/test/unit/apply-patch.test.js +0 -225
  416. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  417. package/dist/test/unit/audit-checks.test.js +0 -350
  418. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  419. package/dist/test/unit/chinese-path.test.js +0 -91
  420. package/dist/test/unit/command-guard.test.d.ts +0 -1
  421. package/dist/test/unit/command-guard.test.js +0 -160
  422. package/dist/test/unit/create-task.test.d.ts +0 -1
  423. package/dist/test/unit/create-task.test.js +0 -197
  424. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  425. package/dist/test/unit/diagnose-task.test.js +0 -457
  426. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  427. package/dist/test/unit/direct-guards.test.js +0 -328
  428. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  429. package/dist/test/unit/discovery-token-store.test.js +0 -139
  430. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  431. package/dist/test/unit/evidence-pack.test.js +0 -224
  432. package/dist/test/unit/get-task-status.test.d.ts +0 -1
  433. package/dist/test/unit/get-task-status.test.js +0 -174
  434. package/dist/test/unit/get-task-summary.test.d.ts +0 -1
  435. package/dist/test/unit/get-task-summary.test.js +0 -146
  436. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  437. package/dist/test/unit/goal-graph.test.js +0 -298
  438. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  439. package/dist/test/unit/goal-progress.test.js +0 -381
  440. package/dist/test/unit/goal-report.test.d.ts +0 -1
  441. package/dist/test/unit/goal-report.test.js +0 -159
  442. package/dist/test/unit/goal-status.test.d.ts +0 -1
  443. package/dist/test/unit/goal-status.test.js +0 -215
  444. package/dist/test/unit/goal-store.test.d.ts +0 -1
  445. package/dist/test/unit/goal-store.test.js +0 -253
  446. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  447. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  448. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  449. package/dist/test/unit/goal-tools-registry.test.js +0 -192
  450. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  451. package/dist/test/unit/handoff-export.test.js +0 -263
  452. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  453. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  454. package/dist/test/unit/logging.test.d.ts +0 -1
  455. package/dist/test/unit/logging.test.js +0 -397
  456. package/dist/test/unit/path-guard.test.d.ts +0 -1
  457. package/dist/test/unit/path-guard.test.js +0 -133
  458. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  459. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  460. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  461. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  462. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  463. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  464. package/dist/test/unit/release-gate.test.d.ts +0 -1
  465. package/dist/test/unit/release-gate.test.js +0 -242
  466. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  467. package/dist/test/unit/run-task-loop.test.js +0 -380
  468. package/dist/test/unit/safe-status.test.d.ts +0 -1
  469. package/dist/test/unit/safe-status.test.js +0 -165
  470. package/dist/test/unit/safe-views.test.d.ts +0 -1
  471. package/dist/test/unit/safe-views.test.js +0 -171
  472. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  473. package/dist/test/unit/schema-drift-check.test.js +0 -176
  474. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  475. package/dist/test/unit/sensitive-guard.test.js +0 -104
  476. package/dist/test/unit/spec-kit-import.test.d.ts +0 -1
  477. package/dist/test/unit/spec-kit-import.test.js +0 -341
  478. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  479. package/dist/test/unit/subgoal-sync.test.js +0 -183
  480. package/dist/test/unit/sync-file.test.d.ts +0 -1
  481. package/dist/test/unit/sync-file.test.js +0 -154
  482. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  483. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  484. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  485. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  486. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  487. package/dist/test/unit/toolSearch.test.js +0 -571
  488. package/dist/test/unit/wait-for-task.test.d.ts +0 -1
  489. package/dist/test/unit/wait-for-task.test.js +0 -144
  490. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  491. package/dist/test/unit/watcher-status.test.js +0 -169
  492. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  493. package/dist/test/unit/worktree-manager.test.js +0 -176
  494. package/dist/tools/applyPatch.js +0 -41
  495. package/dist/tools/cancelTask.d.ts +0 -30
  496. package/dist/tools/cancelTask.js +0 -64
  497. package/dist/tools/finalizeDirectSession.js +0 -84
  498. package/dist/tools/goalSubgoalTask.js +0 -110
  499. package/dist/tools/killTask.d.ts +0 -15
  500. package/dist/tools/listAgents.d.ts +0 -12
  501. package/dist/tools/listAgents.js +0 -42
  502. package/dist/tools/reconcileTasks.js +0 -352
  503. package/dist/tools/retryTask.js +0 -45
  504. package/dist/tools/runVerification.js +0 -36
  505. package/dist/tools/syncFile.js +0 -65
  506. package/docs/control-center/control-center-daily-driver.md +0 -211
  507. package/docs/control-center/control-center-mvp.md +0 -205
  508. package/docs/control-center/control-center-phase2.md +0 -159
  509. package/docs/execution-plan-2026-07-09.md +0 -315
  510. package/docs/release-v0.6.0.md +0 -71
  511. package/docs/release-v0.6.1.md +0 -75
  512. package/docs/release-v0.6.4.md +0 -45
  513. package/docs/roadmap-execution-and-acceptance.md +0 -365
  514. package/src/test/unit/acceptance-engine.test.ts +0 -261
  515. package/src/test/unit/agent-router.test.ts +0 -342
  516. package/src/test/unit/android-doctor.test.ts +0 -158
  517. package/src/test/unit/apply-patch.test.ts +0 -293
  518. package/src/test/unit/audit-checks.test.ts +0 -567
  519. package/src/test/unit/chinese-path.test.ts +0 -106
  520. package/src/test/unit/command-guard.test.ts +0 -221
  521. package/src/test/unit/create-task.test.ts +0 -255
  522. package/src/test/unit/diagnose-task.test.ts +0 -544
  523. package/src/test/unit/direct-guards.test.ts +0 -467
  524. package/src/test/unit/discovery-token-store.test.ts +0 -181
  525. package/src/test/unit/evidence-pack.test.ts +0 -251
  526. package/src/test/unit/get-task-status.test.ts +0 -203
  527. package/src/test/unit/get-task-summary.test.ts +0 -173
  528. package/src/test/unit/goal-graph.test.ts +0 -347
  529. package/src/test/unit/goal-progress.test.ts +0 -538
  530. package/src/test/unit/goal-report.test.ts +0 -189
  531. package/src/test/unit/goal-status.test.ts +0 -270
  532. package/src/test/unit/goal-store.test.ts +0 -318
  533. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  534. package/src/test/unit/goal-tools-registry.test.ts +0 -245
  535. package/src/test/unit/handoff-export.test.ts +0 -295
  536. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  537. package/src/test/unit/logging.test.ts +0 -497
  538. package/src/test/unit/path-guard.test.ts +0 -180
  539. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  540. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  541. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  542. package/src/test/unit/release-gate.test.ts +0 -314
  543. package/src/test/unit/run-task-loop.test.ts +0 -425
  544. package/src/test/unit/safe-status.test.ts +0 -187
  545. package/src/test/unit/safe-views.test.ts +0 -184
  546. package/src/test/unit/schema-drift-check.test.ts +0 -260
  547. package/src/test/unit/sensitive-guard.test.ts +0 -124
  548. package/src/test/unit/spec-kit-import.test.ts +0 -429
  549. package/src/test/unit/subgoal-sync.test.ts +0 -236
  550. package/src/test/unit/sync-file.test.ts +0 -231
  551. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  552. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  553. package/src/test/unit/toolSearch.test.ts +0 -756
  554. package/src/test/unit/wait-for-task.test.ts +0 -176
  555. package/src/test/unit/watcher-status.test.ts +0 -190
  556. package/src/test/unit/worktree-manager.test.ts +0 -247
  557. package/src/tools/cancelTask.ts +0 -70
  558. package/src/tools/listAgents.ts +0 -54
  559. package/src/tools/retryTask.ts +0 -53
  560. package/src/tools/syncFile.ts +0 -122
  561. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  562. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  563. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  564. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  565. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  566. /package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +0 -0
  567. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  568. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  569. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  570. /package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +0 -0
  571. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  572. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  573. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  574. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  575. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  576. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  577. /package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +0 -0
  578. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  579. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  580. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  581. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  582. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  583. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  584. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  585. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  586. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  587. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  588. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  589. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  590. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  591. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
@@ -12,7 +12,7 @@
12
12
  * 3. 存在 warn 级检查项或文档不同步 → NEEDS_FIX
13
13
  * 4. 所有检查通过 → ACCEPTED
14
14
  */
15
- import type { AcceptanceStatus } from "../tools/createTask.js";
15
+ import type { AcceptanceStatus } from "../tools/tasks/createTask.js";
16
16
  export type AcceptanceVerdict = "ACCEPTED" | "NEEDS_FIX" | "REJECTED" | "BLOCKED_BY_APPROVAL";
17
17
  export interface AcceptanceCheck {
18
18
  name: string;
@@ -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,