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
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  mkdirSync,
3
- writeFileSync,
4
3
  existsSync,
5
- readFileSync,
6
4
  } from "node:fs";
7
5
  import { createHash, randomBytes } from "node:crypto";
8
6
  import { resolve, join } from "node:path";
@@ -15,10 +13,16 @@ import {
15
13
  import { guardPath } from "../security/pathGuard.js";
16
14
  import { PatchWardenError } from "../errors.js";
17
15
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
18
- import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
16
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
17
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
18
+ import {
19
+ mutateLockedJsonFileSync,
20
+ readJsonObjectFileSync,
21
+ withFileLock,
22
+ withFileLockSync,
23
+ } from "../utils/lockedJsonFile.js";
24
+ import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
19
25
  import {
20
- captureRepoSnapshot,
21
- buildChangeArtifacts,
22
26
  type RepoSnapshot,
23
27
  type ChangeArtifacts,
24
28
  } from "../runner/changeCapture.js";
@@ -29,7 +33,9 @@ export interface DirectSessionOperation {
29
33
  index: number;
30
34
  timestamp: string;
31
35
  path: string;
32
- before_sha256: string;
36
+ operation_type?: "patch" | "sync";
37
+ source_path?: string;
38
+ before_sha256: string | null;
33
39
  after_sha256: string;
34
40
  operations_applied: number;
35
41
  bytes_changed: number;
@@ -89,23 +95,52 @@ export function generateDirectSessionId(): string {
89
95
  return `direct_${timestamp}_${randomHex}`;
90
96
  }
91
97
 
98
+ /**
99
+ * Direct session IDs are directory names. Keep one shared, bounded grammar so
100
+ * MCP tools and Control Center routes cannot disagree about traversal input.
101
+ */
102
+ export function isValidDirectSessionId(sessionId: string): boolean {
103
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(sessionId);
104
+ }
105
+
106
+ function guardDirectSessionId(sessionId: string): void {
107
+ if (!isValidDirectSessionId(sessionId)) {
108
+ throw new PatchWardenError(
109
+ "invalid_session_id",
110
+ `Direct session ID "${sessionId}" is invalid.`,
111
+ "Use the session_id returned by create_direct_session.",
112
+ true,
113
+ { session_id: sessionId }
114
+ );
115
+ }
116
+ }
117
+
92
118
  // ── Directory management ───────────────────────────────────────────
93
119
 
94
- export function createDirectSessionDir(sessionId: string): string {
95
- const config = getConfig();
120
+ export function createDirectSessionDir(
121
+ sessionId: string,
122
+ config: PatchWardenConfig = getConfig()
123
+ ): string {
124
+ guardDirectSessionId(sessionId);
96
125
  const sessionsDir = getDirectSessionsDir(config);
97
126
  guardPath(sessionsDir, config.workspaceRoot, config.directSessionsDir);
98
127
  mkdirSync(sessionsDir, { recursive: true });
99
- const dir = resolve(sessionsDir, sessionId);
100
- guardPath(dir, config.workspaceRoot, config.directSessionsDir);
128
+ const dir = getDirectSessionDir(sessionId, config);
101
129
  mkdirSync(dir, { recursive: true });
102
130
  return dir;
103
131
  }
104
132
 
105
- export function getDirectSessionDir(sessionId: string): string {
106
- const config = getConfig();
133
+ export function getDirectSessionDir(
134
+ sessionId: string,
135
+ config: PatchWardenConfig = getConfig()
136
+ ): string {
137
+ guardDirectSessionId(sessionId);
107
138
  const sessionsDir = getDirectSessionsDir(config);
108
- return resolve(sessionsDir, sessionId);
139
+ return guardPath(
140
+ resolve(sessionsDir, sessionId),
141
+ config.workspaceRoot,
142
+ config.directSessionsDir
143
+ );
109
144
  }
110
145
 
111
146
  // ── CRUD ───────────────────────────────────────────────────────────
@@ -136,7 +171,11 @@ export function createDirectSession(
136
171
 
137
172
  const record: DirectSessionRecord = {
138
173
  session_id: sessionId,
139
- title: input.title || "",
174
+ title: redactSensitiveContent(input.title || "")
175
+ .content
176
+ .replace(/[\r\n\t]+/g, " ")
177
+ .trim()
178
+ .slice(0, 500),
140
179
  repo_path: input.repo_path,
141
180
  resolved_repo_path: input.resolved_repo_path,
142
181
  created_at: now.toISOString(),
@@ -155,21 +194,16 @@ export function createDirectSession(
155
194
  change_artifacts: null,
156
195
  };
157
196
 
158
- writeFileSync(
159
- join(dir, "session.json"),
160
- JSON.stringify(record, null, 2),
161
- "utf-8"
162
- );
197
+ atomicWriteJsonFileSync(join(dir, "session.json"), record);
163
198
 
164
199
  return record;
165
200
  }
166
201
 
167
- export function readDirectSession(sessionId: string): DirectSessionRecord {
168
- const config = getConfig();
169
- const sessionsDir = getDirectSessionsDir(config);
170
- const dir = resolve(sessionsDir, sessionId);
171
- const file = join(dir, "session.json");
172
- guardPath(file, config.workspaceRoot, config.directSessionsDir);
202
+ export function readDirectSession(
203
+ sessionId: string,
204
+ config: PatchWardenConfig = getConfig()
205
+ ): DirectSessionRecord {
206
+ const file = getDirectSessionFile(sessionId, config);
173
207
 
174
208
  if (!existsSync(file)) {
175
209
  throw new PatchWardenError(
@@ -181,40 +215,69 @@ export function readDirectSession(sessionId: string): DirectSessionRecord {
181
215
  );
182
216
  }
183
217
 
184
- return JSON.parse(readFileSync(file, "utf-8")) as DirectSessionRecord;
218
+ let value: unknown;
219
+ try {
220
+ value = readJsonObjectFileSync(file);
221
+ } catch {
222
+ throw invalidDirectSessionRecord(sessionId, "session.json is not valid JSON");
223
+ }
224
+ return validateDirectSessionRecord(sessionId, value);
185
225
  }
186
226
 
187
227
  export function updateDirectSession(
188
228
  sessionId: string,
189
229
  updates: Partial<DirectSessionRecord>
190
230
  ): DirectSessionRecord {
191
- const session = readDirectSession(sessionId);
192
- const updated: DirectSessionRecord = { ...session, ...updates };
193
- const dir = getDirectSessionDir(sessionId);
194
- writeFileSync(
195
- join(dir, "session.json"),
196
- JSON.stringify(updated, null, 2),
197
- "utf-8"
198
- );
199
- return updated;
231
+ return mutateDirectSessionRecord(sessionId, (session) => ({
232
+ ...session,
233
+ ...updates,
234
+ session_id: session.session_id,
235
+ }));
200
236
  }
201
237
 
202
238
  export function appendDirectSessionOperation(
203
239
  sessionId: string,
204
- operation: DirectSessionOperation
240
+ operation: DirectSessionOperation,
241
+ config: PatchWardenConfig = getConfig(),
205
242
  ): DirectSessionRecord {
206
- const session = readDirectSession(sessionId);
207
- const operations = [...session.operations, operation];
208
- return updateDirectSession(sessionId, { operations });
243
+ return mutateDirectSessionRecord(sessionId, (session) => ({
244
+ ...session,
245
+ operations: [
246
+ ...session.operations,
247
+ {
248
+ ...operation,
249
+ index: session.operations.reduce((max, entry) => Math.max(max, entry.index), -1) + 1,
250
+ },
251
+ ],
252
+ }), config);
253
+ }
254
+
255
+ export function withDirectSessionMutationLock<R>(
256
+ sessionId: string,
257
+ action: () => R,
258
+ config: PatchWardenConfig = getConfig(),
259
+ ): R {
260
+ const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
261
+ return withFileLockSync(lockTarget, action, directSessionBusyOptions(sessionId));
262
+ }
263
+
264
+ export function withDirectSessionMutationLockAsync<R>(
265
+ sessionId: string,
266
+ action: () => Promise<R>,
267
+ config: PatchWardenConfig = getConfig(),
268
+ ): Promise<R> {
269
+ const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
270
+ return withFileLock(lockTarget, action, directSessionBusyOptions(sessionId));
209
271
  }
210
272
 
211
273
  export function appendDirectSessionVerificationRun(
212
274
  sessionId: string,
213
275
  run: DirectSessionVerificationRun
214
276
  ): DirectSessionRecord {
215
- const session = readDirectSession(sessionId);
216
- const verification_runs = [...session.verification_runs, run];
217
- return updateDirectSession(sessionId, { verification_runs });
277
+ return mutateDirectSessionRecord(sessionId, (session) => ({
278
+ ...session,
279
+ verification_runs: [...session.verification_runs, run],
280
+ }));
218
281
  }
219
282
 
220
283
  // ── Validation ─────────────────────────────────────────────────────
@@ -261,32 +324,25 @@ export function validateDirectSessionFreshness(
261
324
 
262
325
  export function finalizeDirectSessionRecord(
263
326
  sessionId: string,
264
- afterSnapshot: RepoSnapshot,
265
327
  changeArtifacts: ChangeArtifacts
266
328
  ): DirectSessionRecord {
267
329
  const dir = getDirectSessionDir(sessionId);
268
330
 
269
331
  // Write change artifacts to session directory
270
- writeFileSync(
271
- join(dir, "changed-files.json"),
272
- JSON.stringify(changeArtifacts.changed_files, null, 2),
273
- "utf-8"
274
- );
275
- writeFileSync(join(dir, "diff.patch"), changeArtifacts.diff, "utf-8");
332
+ atomicWriteJsonFileSync(join(dir, "changed-files.json"), changeArtifacts.changed_files);
333
+ atomicWriteFileSync(join(dir, "diff.patch"), changeArtifacts.diff);
276
334
 
277
335
  const summary = {
278
336
  changed_files_total: changeArtifacts.changed_files.length,
279
337
  additions: changeArtifacts.additions,
280
338
  deletions: changeArtifacts.deletions,
281
339
  patch_mode: changeArtifacts.patch_mode,
340
+ diff_redacted: changeArtifacts.diff_redacted === true,
341
+ diff_redaction_categories: changeArtifacts.diff_redaction_categories ?? [],
282
342
  artifact_hygiene: changeArtifacts.artifact_hygiene,
283
343
  };
284
- writeFileSync(
285
- join(dir, "summary.json"),
286
- JSON.stringify(summary, null, 2),
287
- "utf-8"
288
- );
289
- writeFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts), "utf-8");
344
+ atomicWriteJsonFileSync(join(dir, "summary.json"), summary);
345
+ atomicWriteFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts));
290
346
 
291
347
  return updateDirectSession(sessionId, {
292
348
  finalized: true,
@@ -319,6 +375,105 @@ function extractReason(error: unknown): string {
319
375
  return String(error).slice(0, 80);
320
376
  }
321
377
 
378
+ function invalidDirectSessionRecord(
379
+ sessionId: string,
380
+ detail: string
381
+ ): PatchWardenError {
382
+ return new PatchWardenError(
383
+ "invalid_session_record",
384
+ `Direct session "${sessionId}" is invalid: ${detail}.`,
385
+ "Create a new direct session and do not modify its internal session.json file.",
386
+ true,
387
+ { session_id: sessionId }
388
+ );
389
+ }
390
+
391
+ function getDirectSessionFile(
392
+ sessionId: string,
393
+ config: PatchWardenConfig = getConfig()
394
+ ): string {
395
+ const dir = getDirectSessionDir(sessionId, config);
396
+ return guardPath(
397
+ join(dir, "session.json"),
398
+ config.workspaceRoot,
399
+ config.directSessionsDir
400
+ );
401
+ }
402
+
403
+ function mutateDirectSessionRecord(
404
+ sessionId: string,
405
+ mutation: (session: DirectSessionRecord) => DirectSessionRecord,
406
+ config: PatchWardenConfig = getConfig(),
407
+ ): DirectSessionRecord {
408
+ const file = getDirectSessionFile(sessionId, config);
409
+ if (!existsSync(file)) {
410
+ throw new PatchWardenError(
411
+ "session_not_found",
412
+ `Direct session "${sessionId}" not found.`,
413
+ "Call create_direct_session first to get a valid session_id.",
414
+ true,
415
+ { session_id: sessionId }
416
+ );
417
+ }
418
+ return mutateLockedJsonFileSync<DirectSessionRecord, DirectSessionRecord>(
419
+ file,
420
+ (current) => {
421
+ const session = validateDirectSessionRecord(sessionId, current);
422
+ const next = mutation(session);
423
+ return { next, result: next };
424
+ },
425
+ {
426
+ // Session-record appends are short serialized writes. Give Windows
427
+ // antivirus-delayed lock detachment enough time to finish instead of
428
+ // dropping concurrent operation or verification evidence.
429
+ waitMs: 10_000,
430
+ busyError: () => new PatchWardenError(
431
+ "direct_session_busy",
432
+ `Direct session "${sessionId}" is currently being updated.`,
433
+ "Retry after the current Direct operation completes.",
434
+ true,
435
+ { session_id: sessionId }
436
+ ),
437
+ }
438
+ );
439
+ }
440
+
441
+ function directSessionBusyOptions(sessionId: string) {
442
+ return {
443
+ waitMs: 0,
444
+ busyError: () => new PatchWardenError(
445
+ "direct_session_busy",
446
+ `Direct session "${sessionId}" already has a workspace operation in progress.`,
447
+ "Wait for the active patch, sync, verification, or finalization to finish, then retry.",
448
+ true,
449
+ { session_id: sessionId },
450
+ ),
451
+ };
452
+ }
453
+
454
+ function validateDirectSessionRecord(sessionId: string, value: unknown): DirectSessionRecord {
455
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
456
+ throw invalidDirectSessionRecord(sessionId, "session.json must contain an object");
457
+ }
458
+ const record = value as Partial<DirectSessionRecord>;
459
+ if (record.session_id !== sessionId) {
460
+ throw invalidDirectSessionRecord(sessionId, "session_id does not match its directory");
461
+ }
462
+ if (typeof record.resolved_repo_path !== "string" || record.resolved_repo_path.trim() === "") {
463
+ throw invalidDirectSessionRecord(sessionId, "resolved_repo_path is missing");
464
+ }
465
+ if (typeof record.expires_at !== "string" || !Number.isFinite(Date.parse(record.expires_at))) {
466
+ throw invalidDirectSessionRecord(sessionId, "expires_at is invalid");
467
+ }
468
+ if (typeof record.finalized !== "boolean") {
469
+ throw invalidDirectSessionRecord(sessionId, "finalized must be a boolean");
470
+ }
471
+ if (!Array.isArray(record.operations) || !Array.isArray(record.verification_runs)) {
472
+ throw invalidDirectSessionRecord(sessionId, "operation or verification history is invalid");
473
+ }
474
+ return record as DirectSessionRecord;
475
+ }
476
+
322
477
  function formatSummaryMd(artifacts: ChangeArtifacts): string {
323
478
  const lines: string[] = [
324
479
  "# Direct Session Change Summary",
@@ -327,6 +482,7 @@ function formatSummaryMd(artifacts: ChangeArtifacts): string {
327
482
  `**Additions:** ${artifacts.additions}`,
328
483
  `**Deletions:** ${artifacts.deletions}`,
329
484
  `**Patch mode:** ${artifacts.patch_mode}`,
485
+ `**Diff redacted:** ${artifacts.diff_redacted === true ? "yes" : "no"}`,
330
486
  "",
331
487
  "## Artifact Hygiene",
332
488
  "",
@@ -1,11 +1,12 @@
1
- import { writeFileSync, mkdirSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
  import { getConfig } from "../config.js";
4
3
  import { guardDirectCommand } from "../security/commandGuard.js";
5
4
  import { redactSensitiveContent } from "../security/contentRedaction.js";
6
5
  import { runSimpleProcess } from "../runner/simpleProcess.js";
6
+ import { resolveTrustedCommandLine } from "../runner/processSecurity.js";
7
7
  import { getDirectSessionDir } from "./directSessionStore.js";
8
8
  import type { DirectSessionVerificationRun } from "./directSessionStore.js";
9
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
9
10
 
10
11
  const MAX_TAIL_CHARS = 10_000;
11
12
  const DEFAULT_TIMEOUT_SECONDS = 120;
@@ -36,7 +37,7 @@ export async function runDirectVerification(
36
37
  const timeoutMs = timeoutSeconds * 1000;
37
38
 
38
39
  // Parse command into command + args
39
- const parts = parseCommand(allowedCommand);
40
+ const parts = parseCommand(allowedCommand, input.resolvedRepoPath);
40
41
  if (!parts) {
41
42
  throw new Error(`Failed to parse command: "${allowedCommand}"`);
42
43
  }
@@ -104,11 +105,7 @@ export async function runDirectVerification(
104
105
  spawn_error: result.spawnError,
105
106
  };
106
107
 
107
- writeFileSync(
108
- join(sessionDir, "verification.json"),
109
- JSON.stringify(verificationJson, null, 2),
110
- "utf-8"
111
- );
108
+ atomicWriteJsonFileSync(join(sessionDir, "verification.json"), verificationJson);
112
109
 
113
110
  return { run, log_path: logPath };
114
111
  }
@@ -116,26 +113,15 @@ export async function runDirectVerification(
116
113
  // ── Helpers ────────────────────────────────────────────────────────
117
114
 
118
115
  function parseCommand(
119
- command: string
116
+ command: string,
117
+ cwd: string,
120
118
  ): { command: string; args: string[] } | null {
121
119
  const trimmed = command.trim();
122
120
  if (!trimmed) return null;
123
-
124
- // Simple split by spaces - sufficient for allowlisted commands like "npm test"
125
- const parts = trimmed.split(/\s+/);
126
-
127
- // On Windows, npm/npx are batch files (.cmd) and spawn() cannot find them
128
- // without shell resolution. Wrap with cmd.exe /c for those commands.
129
- if (process.platform === "win32" && (parts[0] === "npm" || parts[0] === "npx")) {
130
- return {
131
- command: "cmd",
132
- args: ["/c", ...parts],
133
- };
134
- }
135
-
121
+ const invocation = resolveTrustedCommandLine(trimmed, cwd);
136
122
  return {
137
- command: parts[0],
138
- args: parts.slice(1),
123
+ command: invocation.command,
124
+ args: invocation.argsPrefix,
139
125
  };
140
126
  }
141
127
 
package/src/doctor.ts CHANGED
@@ -10,20 +10,22 @@
10
10
 
11
11
  import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
12
12
  import { isAbsolute, resolve, normalize, join } from "node:path";
13
- import { execSync } from "node:child_process";
14
13
  import { createServer } from "node:net";
15
14
  import { getConfig, type PatchWardenConfig } from "./config.js";
16
15
  import { guardPath, guardWorkspacePath } from "./security/pathGuard.js";
17
16
  import { isSensitivePath } from "./security/sensitiveGuard.js";
18
17
  import { guardPlanContent } from "./security/planGuard.js";
19
- import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
18
+ import { TASK_READ_ONLY_FILES } from "./tools/tasks/getTaskFile.js";
20
19
  import { getToolDefs } from "./tools/registry.js";
21
- import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
22
- import { buildToolRegistry } from "./tools/toolRegistry.js";
23
- import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
20
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/catalog/toolCatalog.js";
21
+ import { buildToolRegistry } from "./tools/catalog/toolRegistry.js";
22
+ import { runAllSchemaDriftChecks } from "./tools/diagnostics/schemaDriftCheck.js";
24
23
  import { runReleaseGateCheck } from "./release/releaseGate.js";
25
24
  import { PATCHWARDEN_VERSION } from "./version.js";
26
25
  import { logger } from "./logging.js";
26
+ import { unsafeWorkspaceRootLabel } from "./security/workspaceRootGuard.js";
27
+ import { runSimpleProcessSync } from "./runner/simpleProcess.js";
28
+ import { resolveAgentLaunch } from "./runner/agentInvocation.js";
27
29
 
28
30
  // ── Types ──────────────────────────────────────────────────────────
29
31
 
@@ -50,13 +52,18 @@ export interface DoctorContext {
50
52
 
51
53
  // ── Helpers ────────────────────────────────────────────────────────
52
54
 
53
- function cmd(cmdStr: string): string {
55
+ function cmd(command: string, args: readonly string[] = []): string {
54
56
  try {
55
- return execSync(cmdStr, {
56
- encoding: "utf-8",
57
- timeout: 5000,
58
- stdio: ["ignore", "pipe", "ignore"], // suppress stdin and stderr
59
- }).trim();
57
+ const result = runSimpleProcessSync({
58
+ command,
59
+ args: [...args],
60
+ cwd: process.cwd(),
61
+ timeoutMs: 5000,
62
+ maxStdoutBytes: 64 * 1024,
63
+ maxStderrBytes: 64 * 1024,
64
+ });
65
+ if (result.exitCode !== 0) return "";
66
+ return `${result.stdout}${result.stderr}`.trim();
60
67
  } catch {
61
68
  return "";
62
69
  }
@@ -109,7 +116,7 @@ const checkNpmAvailable: DoctorCheck = {
109
116
  id: "npm-available",
110
117
  description: "npm available",
111
118
  run() {
112
- const npmVer = cmd("npm --version");
119
+ const npmVer = cmd(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
113
120
  return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
114
121
  },
115
122
  };
@@ -144,7 +151,7 @@ const checkGitAvailable: DoctorCheck = {
144
151
  id: "git-available",
145
152
  description: "Git available",
146
153
  run() {
147
- const gitVer = cmd("git --version");
154
+ const gitVer = cmd("git", ["--version"]);
148
155
  return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
149
156
  },
150
157
  };
@@ -199,18 +206,9 @@ const checkWorkspaceRoot: DoctorCheck = {
199
206
  try { isDir = statSync(ws).isDirectory(); } catch {}
200
207
  results.push(checkResult("workspaceRoot is directory", isDir, ws));
201
208
 
202
- const dangerousRoots = [
203
- { pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
204
- { pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
205
- { pattern: /\\Desktop$/, label: "Desktop" },
206
- { pattern: /\\Downloads$/, label: "Downloads" },
207
- { pattern: /\\Documents$/, label: "Documents" },
208
- ];
209
-
210
- for (const { pattern, label } of dangerousRoots) {
211
- if (pattern.test(ws)) {
212
- results.push(warnResult(`workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`));
213
- }
209
+ const unsafeLabel = unsafeWorkspaceRootLabel(ws);
210
+ if (unsafeLabel) {
211
+ results.push(warnResult(`workspaceRoot is ${unsafeLabel}: ${ws} consider narrowing to a project directory`));
214
212
  }
215
213
  return results;
216
214
  },
@@ -312,11 +310,13 @@ const checkServerVersion: DoctorCheck = {
312
310
  packageJson.version === PATCHWARDEN_VERSION,
313
311
  `${PATCHWARDEN_VERSION} vs ${packageJson.version}`,
314
312
  ),
315
- checkResult(
316
- "Manifest preflight script exists",
317
- existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")),
318
- "scripts/checks/mcp-manifest-check.js",
319
- ),
313
+ process.env.PATCHWARDEN_DESKTOP_RUNTIME === "1"
314
+ ? okResult("Manifest preflight covered by compiled desktop runtime checks")
315
+ : checkResult(
316
+ "Manifest preflight script exists",
317
+ existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")),
318
+ "scripts/checks/mcp-manifest-check.js",
319
+ ),
320
320
  ];
321
321
  },
322
322
  };
@@ -331,8 +331,8 @@ const checkToolProfiles: DoctorCheck = {
331
331
  process.env.PATCHWARDEN_TOOL_PROFILE = "full";
332
332
  const fullTools = getToolDefs();
333
333
  const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", context.config?.enableDirectProfile);
334
- const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema as any;
335
- const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema as any;
334
+ const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
335
+ const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
336
336
  results.push(checkResult("Full tool profile exposes 66 tools", fullTools.length === 66, `${fullTools.length} tools`));
337
337
  results.push(
338
338
  checkResult(
@@ -463,23 +463,23 @@ const checkToolModules: DoctorCheck = {
463
463
  description: "Tool module checks",
464
464
  run() {
465
465
  const newTools = [
466
- "listTasks",
467
- "listAgents",
468
- "cancelTask",
469
- "killTask",
470
- "retryTask",
471
- "getTaskProgress",
472
- "getTaskSummary",
473
- "waitForTask",
474
- "getTaskStdoutTail",
475
- "healthCheck",
476
- "auditTask",
466
+ { name: "listTasks", subdir: "tasks" },
467
+ { name: "listAgents", subdir: "workspace" },
468
+ { name: "cancelTask", subdir: "tasks" },
469
+ { name: "killTask", subdir: "tasks" },
470
+ { name: "retryTask", subdir: "tasks" },
471
+ { name: "getTaskProgress", subdir: "tasks" },
472
+ { name: "getTaskSummary", subdir: "tasks" },
473
+ { name: "waitForTask", subdir: "tasks" },
474
+ { name: "getTaskStdoutTail", subdir: "tasks" },
475
+ { name: "healthCheck", subdir: "diagnostics" },
476
+ { name: "auditTask", subdir: "diagnostics" },
477
477
  ];
478
478
  const results: DoctorCheckResult[] = [];
479
479
  for (const t of newTools) {
480
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
480
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
481
481
  const exists = existsSync(compiled);
482
- results.push(checkResult(`Tool module: ${t}`, exists, exists ? "compiled" : "missing"));
482
+ results.push(checkResult(`Tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
483
483
  }
484
484
  return results;
485
485
  },
@@ -490,18 +490,18 @@ const checkDirectToolModules: DoctorCheck = {
490
490
  description: "Direct tool module checks",
491
491
  run() {
492
492
  const directToolModules = [
493
- "createDirectSession",
494
- "searchWorkspace",
495
- "applyPatch",
496
- "runVerification",
497
- "finalizeDirectSession",
498
- "auditSession",
493
+ { name: "createDirectSession", subdir: "direct" },
494
+ { name: "searchWorkspace", subdir: "workspace" },
495
+ { name: "applyPatch", subdir: "workspace" },
496
+ { name: "runVerification", subdir: "tasks" },
497
+ { name: "finalizeDirectSession", subdir: "direct" },
498
+ { name: "auditSession", subdir: "diagnostics" },
499
499
  ];
500
500
  const results: DoctorCheckResult[] = [];
501
501
  for (const t of directToolModules) {
502
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
502
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
503
503
  const exists = existsSync(compiled);
504
- results.push(checkResult(`Direct tool module: ${t}`, exists, exists ? "compiled" : "missing"));
504
+ results.push(checkResult(`Direct tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
505
505
  }
506
506
  return results;
507
507
  },
@@ -612,8 +612,7 @@ const checkAgentCommands: DoctorCheck = {
612
612
  run(context) {
613
613
  if (!context.config) return [];
614
614
  const results: DoctorCheckResult[] = [];
615
- const agents = context.config.agents || {};
616
- for (const [name, agentCfg] of Object.entries(agents) as [string, any][]) {
615
+ for (const [name, agentCfg] of Object.entries(context.config.agents)) {
617
616
  const cmdName = agentCfg.command;
618
617
  const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
619
618
  if (looksLikePath) {
@@ -621,11 +620,26 @@ const checkAgentCommands: DoctorCheck = {
621
620
  results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
622
621
  continue;
623
622
  }
624
- // Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
623
+ // Use an argumentized lookup utility; never interpolate config into a shell.
625
624
  const isWin = process.platform === "win32";
626
- const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
627
- const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
628
- const found = cmd(lookupCmd) || cmd(fallbackCmd);
625
+ let found = "";
626
+ if (isWin) {
627
+ try {
628
+ found = resolveAgentLaunch(
629
+ name,
630
+ cmdName,
631
+ process.platform,
632
+ process.env.PATH || "",
633
+ existsSync,
634
+ context.config.workspaceRoot,
635
+ typeof agentCfg.adapter === "string" ? agentCfg.adapter : name,
636
+ ).command;
637
+ } catch {
638
+ found = "";
639
+ }
640
+ } else {
641
+ found = cmd("which", [cmdName]);
642
+ }
629
643
  results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
630
644
  }
631
645
  return results;