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
@@ -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;