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
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
- import { readGoalStatus, writeGoalStatus } from "./goalStore.js";
12
+ import { mutateGoalStatus } from "./goalStore.js";
13
13
  import { updateSubgoalStatus } from "./goalStatus.js";
14
14
  import { logger } from "../logging.js";
15
15
 
@@ -39,13 +39,16 @@ export function syncSubgoalOnTaskDone(
39
39
  if (!goalId || !subgoalId) return;
40
40
 
41
41
  try {
42
- const goalStatus = readGoalStatus(goalId, workspaceRoot);
43
- const subgoal = goalStatus.subgoals.find((s) => s.id === subgoalId);
44
- if (!subgoal) return;
45
- if (subgoal.status !== "running") return;
46
-
47
- const updated = updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent");
48
- writeGoalStatus(goalId, updated, workspaceRoot);
42
+ mutateGoalStatus(goalId, (goalStatus) => {
43
+ const subgoal = goalStatus.subgoals.find((entry) => entry.id === subgoalId);
44
+ if (!subgoal || subgoal.status !== "running") {
45
+ return { result: undefined };
46
+ }
47
+ return {
48
+ next: updateSubgoalStatus(goalStatus, subgoalId, "done_by_agent"),
49
+ result: undefined,
50
+ };
51
+ }, workspaceRoot);
49
52
  } catch (err) {
50
53
  // subgoal 同步失败不应影响任务完成流程,只记录到 stderr
51
54
  logger.error(
@@ -23,17 +23,18 @@ import {
23
23
  existsSync,
24
24
  mkdirSync,
25
25
  readFileSync,
26
- renameSync,
27
- writeFileSync,
28
26
  rmSync,
29
27
  } from "node:fs";
30
- import { join } from "node:path";
28
+ import { join, normalize } from "node:path";
31
29
  import { execFileSync } from "node:child_process";
32
30
  import { randomBytes } from "node:crypto";
33
31
  import { getConfig } from "../config.js";
34
- import { guardWorkspacePath } from "../security/pathGuard.js";
32
+ import { guardPath, guardWorkspacePath } from "../security/pathGuard.js";
35
33
  import { guardSensitivePath } from "../security/sensitiveGuard.js";
36
34
  import { PatchWardenError } from "../errors.js";
35
+ import { buildGitEnvironment, resolveTrustedExecutable } from "../runner/processSecurity.js";
36
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
37
+ import { withFileLockSync } from "../utils/lockedJsonFile.js";
37
38
 
38
39
  // ── 类型定义 ──────────────────────────────────────────────────────
39
40
 
@@ -56,9 +57,12 @@ export const WorktreeDir = "_workspacetrees";
56
57
 
57
58
  /** 归档目录名(discard 后保留状态),位于 .patchwarden 下,始终为安全路径。 */
58
59
  const WORKTREE_ARCHIVE_DIR = ".patchwarden/worktree-archive";
60
+ const WORKTREE_LOCK_DIR = ".patchwarden/worktree-locks";
59
61
 
60
62
  const GIT_TIMEOUT_MS = 30000;
61
63
  const GIT_BRANCH_TIMEOUT_MS = 15000;
64
+ const WORKTREE_ID_MAX_LENGTH = 32;
65
+ const WORKTREE_ID_PATTERN = /^wt_[a-z0-9]{1,16}_[a-f0-9]{12}$/;
62
66
 
63
67
  // ── 辅助:解析 workspaceRoot ──────────────────────────────────────
64
68
 
@@ -66,20 +70,43 @@ function resolveWorkspaceRoot(workspaceRoot?: string): string {
66
70
  return workspaceRoot ?? getConfig().workspaceRoot;
67
71
  }
68
72
 
73
+ /** Accept only IDs emitted by generateWorktreeId(). */
74
+ export function assertValidWorktreeId(worktreeId: string): void {
75
+ if (
76
+ worktreeId.length > WORKTREE_ID_MAX_LENGTH ||
77
+ !WORKTREE_ID_PATTERN.test(worktreeId)
78
+ ) {
79
+ throw new PatchWardenError(
80
+ "invalid_worktree_id",
81
+ "Invalid worktree id. Expected the wt_<base36 timestamp>_<12 hex chars> format.",
82
+ "Use the worktree_id returned by create_subgoal_task.",
83
+ true,
84
+ { worktree_id_length: worktreeId.length }
85
+ );
86
+ }
87
+ }
88
+
69
89
  // ── 目录路径解析 ──────────────────────────────────────────────────
70
90
 
71
91
  /**
72
92
  * 返回 `<workspaceRoot>/_workspacetrees` 目录路径。不自动创建目录。
73
93
  */
74
94
  export function getWorktreesDir(workspaceRoot?: string): string {
75
- return join(resolveWorkspaceRoot(workspaceRoot), WorktreeDir);
95
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
96
+ return guardPath(join(wsRoot, WorktreeDir), wsRoot, WorktreeDir);
76
97
  }
77
98
 
78
99
  /**
79
100
  * 返回 `<getWorktreesDir()>/<worktreeId>` 路径。
80
101
  */
81
102
  export function getWorktreeDir(worktreeId: string, workspaceRoot?: string): string {
82
- return join(getWorktreesDir(workspaceRoot), worktreeId);
103
+ assertValidWorktreeId(worktreeId);
104
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
105
+ return guardPath(
106
+ join(getWorktreesDir(wsRoot), worktreeId),
107
+ wsRoot,
108
+ WorktreeDir
109
+ );
83
110
  }
84
111
 
85
112
  // ── ID 与 branch 生成 ─────────────────────────────────────────────
@@ -105,23 +132,110 @@ function sanitizeBranchSegment(value: string): string {
105
132
  return cleaned === "" ? "x" : cleaned;
106
133
  }
107
134
 
135
+ function samePath(left: string, right: string): boolean {
136
+ const normalizedLeft = normalize(left);
137
+ const normalizedRight = normalize(right);
138
+ return process.platform === "win32"
139
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
140
+ : normalizedLeft === normalizedRight;
141
+ }
142
+
143
+ function withWorktreeRepositoryLock<R>(workspaceRoot: string, action: () => R): R {
144
+ const wsRoot = resolveWorkspaceRoot(workspaceRoot);
145
+ const lockDir = guardPath(join(wsRoot, WORKTREE_LOCK_DIR), wsRoot, WORKTREE_LOCK_DIR);
146
+ mkdirSync(lockDir, { recursive: true });
147
+ return withFileLockSync(join(lockDir, "repository-mutation"), action, {
148
+ waitMs: 0,
149
+ busyError: () => new PatchWardenError(
150
+ "worktree_busy",
151
+ "Another managed worktree operation is already changing this repository.",
152
+ "Retry after the active create, merge, or discard operation finishes.",
153
+ true,
154
+ ),
155
+ });
156
+ }
157
+
158
+ function invalidWorktreeStatus(
159
+ worktreeId: string,
160
+ message: string
161
+ ): never {
162
+ throw new PatchWardenError(
163
+ "invalid_worktree_status",
164
+ `Invalid worktree status for "${worktreeId}": ${message}`,
165
+ "Do not edit worktree_status.json manually; recreate the isolated worktree.",
166
+ true,
167
+ { worktree_id: worktreeId }
168
+ );
169
+ }
170
+
171
+ function validateWorktreeStatus(
172
+ value: unknown,
173
+ worktreeId: string,
174
+ workspaceRoot: string,
175
+ expectedPath: string
176
+ ): WorktreeStatus {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
178
+ return invalidWorktreeStatus(worktreeId, "expected a JSON object");
179
+ }
180
+
181
+ const candidate = value as Record<string, unknown>;
182
+ const statusValues = new Set(["active", "merged", "discarded"]);
183
+ if (
184
+ typeof candidate.worktree_id !== "string" ||
185
+ typeof candidate.goal_id !== "string" ||
186
+ typeof candidate.subgoal_id !== "string" ||
187
+ typeof candidate.path !== "string" ||
188
+ typeof candidate.created_at !== "string" ||
189
+ typeof candidate.status !== "string" ||
190
+ !statusValues.has(candidate.status) ||
191
+ typeof candidate.branch !== "string"
192
+ ) {
193
+ return invalidWorktreeStatus(worktreeId, "missing or invalid required fields");
194
+ }
195
+
196
+ if (candidate.worktree_id !== worktreeId) {
197
+ return invalidWorktreeStatus(worktreeId, "worktree_id does not match its directory");
198
+ }
199
+
200
+ let guardedStatusPath: string;
201
+ try {
202
+ guardedStatusPath = guardPath(candidate.path, workspaceRoot, WorktreeDir);
203
+ } catch {
204
+ return invalidWorktreeStatus(worktreeId, "path is outside the managed worktree root");
205
+ }
206
+ if (!samePath(guardedStatusPath, expectedPath)) {
207
+ return invalidWorktreeStatus(worktreeId, "path does not match its directory");
208
+ }
209
+
210
+ const expectedBranch = `pw-${sanitizeBranchSegment(candidate.goal_id)}-${sanitizeBranchSegment(candidate.subgoal_id)}`;
211
+ if (candidate.branch !== expectedBranch) {
212
+ return invalidWorktreeStatus(worktreeId, "branch does not match goal and subgoal metadata");
213
+ }
214
+
215
+ if (
216
+ (candidate.merged_at !== undefined && typeof candidate.merged_at !== "string") ||
217
+ (candidate.discarded_at !== undefined && typeof candidate.discarded_at !== "string")
218
+ ) {
219
+ return invalidWorktreeStatus(worktreeId, "invalid lifecycle timestamp");
220
+ }
221
+
222
+ return candidate as unknown as WorktreeStatus;
223
+ }
224
+
108
225
  // ── 原子写 ────────────────────────────────────────────────────────
109
226
 
110
- /**
111
- * 原子写入 status 文件:先写到 `.tmp` 文件,再 renameSync。
112
- * 参考 goalStore.ts writeGoalStatus 的模式。
113
- */
114
227
  function writeStatusAtomic(statusFilePath: string, status: WorktreeStatus): void {
115
- const tmpPath = statusFilePath + ".tmp";
116
- writeFileSync(tmpPath, JSON.stringify(status, null, 2) + "\n", "utf-8");
117
- renameSync(tmpPath, statusFilePath);
228
+ atomicWriteJsonFileSync(statusFilePath, status);
118
229
  }
119
230
 
120
231
  function runGit(args: string[], cwd: string, timeoutMs: number): void {
121
- execFileSync("git", args, {
232
+ const env = buildGitEnvironment(cwd);
233
+ execFileSync(resolveTrustedExecutable("git", cwd, { pathValue: env.PATH }), args, {
122
234
  cwd,
123
235
  timeout: timeoutMs,
124
236
  stdio: ["pipe", "pipe", "pipe"],
237
+ windowsHide: true,
238
+ env,
125
239
  });
126
240
  }
127
241
 
@@ -157,6 +271,7 @@ export function createWorktree(
157
271
  subgoalId: string,
158
272
  workspaceRoot: string
159
273
  ): { worktreeId: string; worktreePath: string; branch: string } {
274
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
160
275
  const wsRoot = resolveWorkspaceRoot(workspaceRoot);
161
276
 
162
277
  const worktreeId = generateWorktreeId();
@@ -222,6 +337,7 @@ export function createWorktree(
222
337
  }
223
338
  );
224
339
  }
340
+ });
225
341
  }
226
342
 
227
343
  /**
@@ -241,12 +357,13 @@ export function readWorktreeStatus(
241
357
  const statusFilePath = join(worktreePath, "worktree_status.json");
242
358
  if (!existsSync(statusFilePath)) return null;
243
359
 
360
+ let parsed: unknown;
244
361
  try {
245
- const raw = readFileSync(statusFilePath, "utf-8");
246
- return JSON.parse(raw) as WorktreeStatus;
362
+ parsed = JSON.parse(readFileSync(statusFilePath, "utf-8"));
247
363
  } catch {
248
- return null;
364
+ return invalidWorktreeStatus(worktreeId, "file is not valid JSON");
249
365
  }
366
+ return validateWorktreeStatus(parsed, worktreeId, wsRoot, worktreePath);
250
367
  }
251
368
 
252
369
  /**
@@ -264,6 +381,7 @@ export function mergeWorktree(
264
381
  worktreeId: string,
265
382
  workspaceRoot: string
266
383
  ): { status: "merged" } {
384
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
267
385
  const wsRoot = resolveWorkspaceRoot(workspaceRoot);
268
386
  const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
269
387
 
@@ -313,6 +431,7 @@ export function mergeWorktree(
313
431
  writeStatusAtomic(statusFilePath, updatedStatus);
314
432
 
315
433
  return { status: "merged" };
434
+ });
316
435
  }
317
436
 
318
437
  /**
@@ -333,6 +452,7 @@ export function discardWorktree(
333
452
  worktreeId: string,
334
453
  workspaceRoot: string
335
454
  ): { status: "discarded" } {
455
+ return withWorktreeRepositoryLock(workspaceRoot, () => {
336
456
  const wsRoot = resolveWorkspaceRoot(workspaceRoot);
337
457
  const worktreePath = getWorktreeDir(worktreeId, workspaceRoot);
338
458
 
@@ -364,10 +484,6 @@ export function discardWorktree(
364
484
  try {
365
485
  runGit(["worktree", "remove", "--force", worktreePath], wsRoot, GIT_TIMEOUT_MS);
366
486
  } catch (err) {
367
- // git remove 失败时再尝试直接删目录(best effort),然后抛错
368
- try {
369
- rmSync(worktreePath, { recursive: true, force: true });
370
- } catch { /* ignore */ }
371
487
  throw new PatchWardenError(
372
488
  "worktree_discard_failed",
373
489
  `Failed to remove worktree "${worktreeId}": ${gitErrorMessage(err)}`,
@@ -391,7 +507,7 @@ export function discardWorktree(
391
507
 
392
508
  const archiveDir = join(wsRoot, WORKTREE_ARCHIVE_DIR);
393
509
  guardWorkspacePath(archiveDir, wsRoot);
394
- // .patchwarden 路径在 sensitiveGuard 中始终视为安全(SAFE_PREFIX),无需额外校验
510
+ // The archive path is workspace-confined and contains only managed status metadata.
395
511
 
396
512
  try {
397
513
  mkdirSync(archiveDir, { recursive: true });
@@ -401,4 +517,5 @@ export function discardWorktree(
401
517
  writeStatusAtomic(archiveFilePath, archivedStatus);
402
518
 
403
519
  return { status: "discarded" };
520
+ });
404
521
  }
package/src/httpServer.ts CHANGED
@@ -16,16 +16,21 @@
16
16
  */
17
17
 
18
18
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
20
20
  import { join, resolve } from "node:path";
21
21
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
22
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
23
  import { loadConfig, getTasksDir } from "./config.js";
24
24
  import { registerTools } from "./tools/registry.js";
25
- import { healthCheck } from "./tools/healthCheck.js";
25
+ import { healthCheck } from "./tools/diagnostics/healthCheck.js";
26
26
  import { getToolCatalogSnapshot } from "./tools/registry.js";
27
27
  import { PATCHWARDEN_VERSION } from "./version.js";
28
28
  import { logger } from "./logging.js";
29
+ import { firstHeaderValue, timingSafeStringEqual } from "./security/secretComparison.js";
30
+ import { redactSensitiveContent } from "./security/contentRedaction.js";
31
+ import { atomicWriteJsonFileSync } from "./utils/atomicFile.js";
32
+ import { mutateTaskStatus } from "./runner/taskStatusStore.js";
33
+ import { isTrustedLoopbackHostHeader } from "./security/loopbackHost.js";
29
34
 
30
35
  // ── Bootstrap ─────────────────────────────────────────────────────
31
36
 
@@ -44,6 +49,16 @@ logger.info(`[patchwarden-http] ⚠️ Bound to 127.0.0.1 only — not exposed
44
49
  const httpCfg = config.http || {};
45
50
  const ownerTokenEnv = httpCfg.ownerTokenEnv || "PATCHWARDEN_OWNER_TOKEN";
46
51
  const ownerToken = process.env[ownerTokenEnv] || "";
52
+ const MAX_ADMIN_BODY_BYTES = 64 * 1024;
53
+ const MAX_ACCEPTANCE_NOTES_CHARS = 10_000;
54
+ const REVIEWABLE_TASK_STATUSES = new Set([
55
+ "done_by_agent",
56
+ "done",
57
+ "accepted",
58
+ "rejected",
59
+ "needs_fix",
60
+ "blocked",
61
+ ]);
47
62
 
48
63
  if (ownerToken) {
49
64
  logger.info(`[patchwarden-http] 🔒 Owner token required (env: ${ownerTokenEnv})`);
@@ -54,15 +69,15 @@ if (ownerToken) {
54
69
  function checkOwnerToken(req: IncomingMessage): boolean {
55
70
  if (!ownerToken) return true; // no token configured — allow all
56
71
 
57
- const authHeader = req.headers["authorization"] || "";
58
- const customHeader = req.headers["x-patchwarden-token"] || "";
72
+ const authHeader = firstHeaderValue(req.headers["authorization"]);
73
+ const customHeader = firstHeaderValue(req.headers["x-patchwarden-token"]);
59
74
 
60
75
  if (authHeader.startsWith("Bearer ")) {
61
- return authHeader.slice(7) === ownerToken;
76
+ return timingSafeStringEqual(authHeader.slice(7), ownerToken);
62
77
  }
63
78
 
64
- if (typeof customHeader === "string" && customHeader.length > 0) {
65
- return customHeader === ownerToken;
79
+ if (customHeader.length > 0) {
80
+ return timingSafeStringEqual(customHeader, ownerToken);
66
81
  }
67
82
 
68
83
  return false;
@@ -82,11 +97,22 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
82
97
  throw Object.assign(new Error(`Task "${taskId}" not found.`), { statusCode: 404 });
83
98
  }
84
99
  let notes = "";
85
- try {
86
- const parsed = JSON.parse(body || "{}");
87
- notes = String(parsed.notes || parsed.reason || "");
88
- } catch {
89
- // no body or invalid JSON — notes stays empty
100
+ if (body.trim()) {
101
+ let parsed: unknown;
102
+ try {
103
+ parsed = JSON.parse(body);
104
+ } catch {
105
+ throw httpError(400, "Acceptance body must be valid JSON.");
106
+ }
107
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
108
+ throw httpError(400, "Acceptance body must be a JSON object.");
109
+ }
110
+ const record = parsed as Record<string, unknown>;
111
+ const rawNotes = record.notes ?? record.reason ?? "";
112
+ if (typeof rawNotes !== "string") {
113
+ throw httpError(400, "Acceptance notes or reason must be a string.");
114
+ }
115
+ notes = redactSensitiveContent(rawNotes).content.slice(0, MAX_ACCEPTANCE_NOTES_CHARS);
90
116
  }
91
117
  const acceptance = {
92
118
  status,
@@ -94,23 +120,69 @@ function handleAcceptance(taskId: string, status: "accepted" | "rejected", body:
94
120
  reviewer: "human",
95
121
  notes,
96
122
  };
97
- writeFileSync(filePath, JSON.stringify(acceptance, null, 2), "utf-8");
98
-
99
- // Also update the task's status.json with acceptance_status
123
+ // Commit the review artifact and status annotation while holding the same
124
+ // status lock used by the runner, cancel, and audit paths.
100
125
  const statusFile = join(taskDir, "status.json");
101
- if (existsSync(statusFile)) {
102
- try {
103
- const current = JSON.parse(readFileSync(statusFile, "utf-8"));
104
- current.acceptance_status = status;
105
- current.acceptance_reviewed_at = acceptance.reviewed_at;
106
- writeFileSync(statusFile, JSON.stringify(current, null, 2), "utf-8");
107
- } catch {
108
- // status.json update is best-effort
109
- }
126
+ if (!existsSync(statusFile)) {
127
+ throw httpError(409, `Task "${taskId}" has no status.json to review.`);
110
128
  }
129
+ mutateTaskStatus(statusFile, (current) => {
130
+ if (!REVIEWABLE_TASK_STATUSES.has(String(current.status || ""))) {
131
+ throw httpError(409, `Task "${taskId}" is not in a reviewable terminal state.`);
132
+ }
133
+ atomicWriteJsonFileSync(filePath, acceptance);
134
+ const next = {
135
+ ...current,
136
+ acceptance_status: status,
137
+ acceptance_reviewed_at: acceptance.reviewed_at,
138
+ updated_at: new Date().toISOString(),
139
+ };
140
+ return { next, result: next };
141
+ });
111
142
  return acceptance;
112
143
  }
113
144
 
145
+ function httpError(statusCode: number, message: string): Error & { statusCode: number } {
146
+ return Object.assign(new Error(message), { statusCode });
147
+ }
148
+
149
+ function readBoundedRequestBody(req: IncomingMessage, maxBytes: number): Promise<string> {
150
+ const declaredLength = Number(firstHeaderValue(req.headers["content-length"]));
151
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
152
+ req.resume();
153
+ return Promise.reject(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
154
+ }
155
+ return new Promise((resolveBody, rejectBody) => {
156
+ const chunks: Buffer[] = [];
157
+ let total = 0;
158
+ let settled = false;
159
+ req.on("data", (chunk: Buffer | string) => {
160
+ if (settled) return;
161
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
162
+ total += buffer.length;
163
+ if (total > maxBytes) {
164
+ settled = true;
165
+ req.resume();
166
+ rejectBody(httpError(413, `Request body exceeds ${maxBytes} bytes.`));
167
+ return;
168
+ }
169
+ chunks.push(buffer);
170
+ });
171
+ req.on("end", () => {
172
+ if (!settled) {
173
+ settled = true;
174
+ resolveBody(Buffer.concat(chunks).toString("utf-8"));
175
+ }
176
+ });
177
+ req.on("error", (error) => {
178
+ if (!settled) {
179
+ settled = true;
180
+ rejectBody(error);
181
+ }
182
+ });
183
+ });
184
+ }
185
+
114
186
  function readAcceptance(taskId: string): object {
115
187
  const filePath = getAcceptancePath(taskId);
116
188
  if (!existsSync(filePath)) {
@@ -181,6 +253,16 @@ function parseAdminUrl(url: string) {
181
253
  // ── HTTP server ───────────────────────────────────────────────────
182
254
 
183
255
  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
256
+ if (!isTrustedLoopbackHostHeader(req.headers.host, port)) {
257
+ res.writeHead(421, {
258
+ "Content-Type": "application/json; charset=utf-8",
259
+ "Cache-Control": "no-store",
260
+ "X-Content-Type-Options": "nosniff",
261
+ });
262
+ res.end(JSON.stringify({ error: "Untrusted Host header" }));
263
+ return;
264
+ }
265
+
184
266
  // Health check endpoints
185
267
  if (req.method === "GET" && (req.url === "/healthz" || req.url === "/readyz")) {
186
268
  const health = healthCheck(getToolCatalogSnapshot());
@@ -206,9 +288,7 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
206
288
  return;
207
289
  }
208
290
  if ((admin.action === "accept" || admin.action === "reject") && req.method === "POST") {
209
- const chunks: Buffer[] = [];
210
- for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); }
211
- const body = Buffer.concat(chunks).toString("utf-8");
291
+ const body = await readBoundedRequestBody(req, MAX_ADMIN_BODY_BYTES);
212
292
  const acceptance = handleAcceptance(admin.taskId, admin.action === "accept" ? "accepted" : "rejected", body);
213
293
  res.writeHead(200, { "Content-Type": "application/json" });
214
294
  res.end(JSON.stringify(acceptance, null, 2));
@@ -216,10 +296,13 @@ const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse
216
296
  }
217
297
  res.writeHead(405, { "Content-Type": "application/json" });
218
298
  res.end(JSON.stringify({ error: "Method not allowed for this admin endpoint." }));
219
- } catch (err: any) {
220
- const statusCode = err.statusCode || 500;
299
+ } catch (err: unknown) {
300
+ const statusCode = err && typeof err === "object" && "statusCode" in err
301
+ ? Number((err as { statusCode?: unknown }).statusCode) || 500
302
+ : 500;
303
+ const message = err instanceof Error ? err.message : "Internal server error";
221
304
  res.writeHead(statusCode, { "Content-Type": "application/json" });
222
- res.end(JSON.stringify({ error: err.message || "Internal server error" }));
305
+ res.end(JSON.stringify({ error: message }));
223
306
  }
224
307
  return;
225
308
  }
package/src/logging.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
- import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { redactSensitiveContent } from "./security/contentRedaction.js";
5
- import { stableJsonStringify } from "./tools/toolRegistry.js";
5
+ import { stableJsonStringify } from "./utils/stableJson.js";
6
6
  import { getConfig } from "./config.js";
7
+ import { appendBoundedTextFileSync } from "./utils/boundedFile.js";
8
+
9
+ const MAX_INVOCATION_LOG_BYTES = 5 * 1024 * 1024;
7
10
 
8
11
  // ── Types ─────────────────────────────────────────────────────────
9
12
 
@@ -234,7 +237,11 @@ export function logToolInvocation(
234
237
 
235
238
  mkdirSync(logsDir, { recursive: true });
236
239
  const logFilePath = join(logsDir, "invocation.log");
237
- appendFileSync(logFilePath, JSON.stringify(entry) + "\n", "utf8");
240
+ appendBoundedTextFileSync(
241
+ logFilePath,
242
+ JSON.stringify(entry) + "\n",
243
+ MAX_INVOCATION_LOG_BYTES,
244
+ );
238
245
  } catch (err) {
239
246
  // 日志失败不应阻断主流程,仅向 stderr 记录错误
240
247
  try {
@@ -334,10 +334,13 @@ function pathMatchesPattern(relPath: string, pattern: string): boolean {
334
334
  }
335
335
 
336
336
  function parseGithubRepo(repository: unknown): string | null {
337
+ const repositoryObject = repository && typeof repository === "object" && !Array.isArray(repository)
338
+ ? repository as Record<string, unknown>
339
+ : null;
337
340
  const raw = typeof repository === "string"
338
341
  ? repository
339
- : repository && typeof repository === "object" && typeof (repository as any).url === "string"
340
- ? (repository as any).url
342
+ : typeof repositoryObject?.url === "string"
343
+ ? repositoryObject.url
341
344
  : "";
342
345
  const match = raw.match(/github\.com[:/](.+?\/.+?)(?:\.git)?(?:[#?].*)?$/i);
343
346
  return match ? match[1] : null;