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
@@ -1,7 +1,8 @@
1
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
1
+ import { closeSync, existsSync, fstatSync, openSync, readFileSync } from "node:fs";
2
2
  import { createHash } from "node:crypto";
3
- import { dirname } from "node:path";
4
3
  import { PatchWardenError } from "../errors.js";
4
+ import { atomicWriteFileSync } from "../utils/atomicFile.js";
5
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
5
6
  // ── Hash helpers ───────────────────────────────────────────────────
6
7
  export function computeFileSha256(filePath) {
7
8
  const content = readFileSync(filePath);
@@ -21,12 +22,36 @@ export function validateExpectedSha256(filePath, expectedSha256) {
21
22
  return currentHash;
22
23
  }
23
24
  // ── Patch application ──────────────────────────────────────────────
24
- export function applyPatchOperations(filePath, operations) {
25
+ export function applyPatchOperations(filePath, operations, options = {}) {
25
26
  if (!existsSync(filePath)) {
26
27
  throw new PatchWardenError("patch_anchor_not_found", `File does not exist: "${filePath}".`, "Ensure the file exists before applying a patch. Direct mode does not support creating new files.", true, { path: filePath });
27
28
  }
28
- const beforeContent = readFileSync(filePath, "utf-8");
29
- const beforeSha256 = computeContentSha256(beforeContent);
29
+ let descriptor = null;
30
+ let beforeBytes;
31
+ let existingMode;
32
+ try {
33
+ descriptor = openSync(filePath, "r");
34
+ const metadata = fstatSync(descriptor);
35
+ if (!metadata.isFile()) {
36
+ throw new PatchWardenError("direct_target_not_file", `Patch target is not a regular file: "${filePath}".`, "Choose an existing text file inside the Direct session repository.");
37
+ }
38
+ guardFileByteLimit(metadata.size, options.maxFileBytes);
39
+ existingMode = metadata.mode;
40
+ beforeBytes = readFileSync(descriptor);
41
+ }
42
+ finally {
43
+ if (descriptor !== null)
44
+ closeSync(descriptor);
45
+ }
46
+ guardFileByteLimit(beforeBytes.length, options.maxFileBytes);
47
+ const beforeSha256 = createHash("sha256").update(beforeBytes).digest("hex");
48
+ if (options.expectedSha256 && beforeSha256 !== options.expectedSha256) {
49
+ throw fileHashMismatch(options.expectedSha256, beforeSha256);
50
+ }
51
+ const beforeContent = beforeBytes.toString("utf-8");
52
+ if (!Buffer.from(beforeContent, "utf-8").equals(beforeBytes)) {
53
+ throw new PatchWardenError("unsupported_text_encoding", `File "${filePath}" is not valid UTF-8 text.`, "Use a normal task or convert the file to UTF-8 before Direct editing.", true, { path: filePath });
54
+ }
30
55
  let content = beforeContent;
31
56
  let operationsApplied = 0;
32
57
  for (const op of operations) {
@@ -34,10 +59,19 @@ export function applyPatchOperations(filePath, operations) {
34
59
  operationsApplied++;
35
60
  }
36
61
  const afterSha256 = computeContentSha256(content);
62
+ guardFileByteLimit(Buffer.byteLength(content, "utf-8"), options.maxFileBytes);
63
+ const sensitive = redactSensitiveContent(content);
64
+ if (sensitive.redacted) {
65
+ throw new PatchWardenError("sensitive_content_blocked", `Patch result contains credential-like content (${sensitive.redaction_categories.join(", ")}).`, "Remove the sensitive value and retry with placeholders or environment-variable references.", true, { path: filePath, redaction_categories: sensitive.redaction_categories });
66
+ }
37
67
  const bytesChanged = Math.abs(Buffer.byteLength(content, "utf-8") - Buffer.byteLength(beforeContent, "utf-8"));
38
- // Ensure parent directory exists (should already exist for existing files)
39
- mkdirSync(dirname(filePath), { recursive: true });
40
- writeFileSync(filePath, content, "utf-8");
68
+ if (options.revalidatePath && !samePath(options.revalidatePath(), filePath)) {
69
+ throw new PatchWardenError("direct_path_changed_during_write", `Patch target path changed while preparing "${filePath}".`, "Retry only after concurrent filesystem changes stop.", true, { path: filePath, operation: "apply_patch" });
70
+ }
71
+ const currentSha256 = computeFileSha256(filePath);
72
+ if (currentSha256 !== beforeSha256)
73
+ throw fileHashMismatch(beforeSha256, currentSha256);
74
+ atomicWriteFileSync(filePath, content, { mode: existingMode });
41
75
  return {
42
76
  before_sha256: beforeSha256,
43
77
  after_sha256: afterSha256,
@@ -45,6 +79,19 @@ export function applyPatchOperations(filePath, operations) {
45
79
  bytes_changed: bytesChanged,
46
80
  };
47
81
  }
82
+ function guardFileByteLimit(fileBytes, maxFileBytes) {
83
+ if (maxFileBytes === undefined || fileBytes <= maxFileBytes)
84
+ return;
85
+ throw new PatchWardenError("file_too_large", `File size ${fileBytes} bytes exceeds maximum ${maxFileBytes} bytes.`, "Use a normal task for larger files.", true, { file_bytes: fileBytes, max_bytes: maxFileBytes });
86
+ }
87
+ function fileHashMismatch(expectedSha256, actualSha256) {
88
+ return new PatchWardenError("file_hash_mismatch", `File hash mismatch. Expected "${expectedSha256}" but got "${actualSha256}".`, "Re-read the file to get the current sha256, then retry the patch.", true, { expected_sha256: expectedSha256, actual_sha256: actualSha256 });
89
+ }
90
+ function samePath(left, right) {
91
+ return process.platform === "win32"
92
+ ? left.toLowerCase() === right.toLowerCase()
93
+ : left === right;
94
+ }
48
95
  function applySingleOperation(content, op) {
49
96
  switch (op.type) {
50
97
  case "replace_exact":
@@ -1,9 +1,12 @@
1
+ import { PatchWardenConfig } from "../config.js";
1
2
  import { type RepoSnapshot, type ChangeArtifacts } from "../runner/changeCapture.js";
2
3
  export interface DirectSessionOperation {
3
4
  index: number;
4
5
  timestamp: string;
5
6
  path: string;
6
- before_sha256: string;
7
+ operation_type?: "patch" | "sync";
8
+ source_path?: string;
9
+ before_sha256: string | null;
7
10
  after_sha256: string;
8
11
  operations_applied: number;
9
12
  bytes_changed: number;
@@ -48,12 +51,19 @@ export interface DirectSessionCreateInput {
48
51
  snapshot: RepoSnapshot;
49
52
  }
50
53
  export declare function generateDirectSessionId(): string;
51
- export declare function createDirectSessionDir(sessionId: string): string;
52
- export declare function getDirectSessionDir(sessionId: string): string;
54
+ /**
55
+ * Direct session IDs are directory names. Keep one shared, bounded grammar so
56
+ * MCP tools and Control Center routes cannot disagree about traversal input.
57
+ */
58
+ export declare function isValidDirectSessionId(sessionId: string): boolean;
59
+ export declare function createDirectSessionDir(sessionId: string, config?: PatchWardenConfig): string;
60
+ export declare function getDirectSessionDir(sessionId: string, config?: PatchWardenConfig): string;
53
61
  export declare function createDirectSession(input: DirectSessionCreateInput): DirectSessionRecord;
54
- export declare function readDirectSession(sessionId: string): DirectSessionRecord;
62
+ export declare function readDirectSession(sessionId: string, config?: PatchWardenConfig): DirectSessionRecord;
55
63
  export declare function updateDirectSession(sessionId: string, updates: Partial<DirectSessionRecord>): DirectSessionRecord;
56
- export declare function appendDirectSessionOperation(sessionId: string, operation: DirectSessionOperation): DirectSessionRecord;
64
+ export declare function appendDirectSessionOperation(sessionId: string, operation: DirectSessionOperation, config?: PatchWardenConfig): DirectSessionRecord;
65
+ export declare function withDirectSessionMutationLock<R>(sessionId: string, action: () => R, config?: PatchWardenConfig): R;
66
+ export declare function withDirectSessionMutationLockAsync<R>(sessionId: string, action: () => Promise<R>, config?: PatchWardenConfig): Promise<R>;
57
67
  export declare function appendDirectSessionVerificationRun(sessionId: string, run: DirectSessionVerificationRun): DirectSessionRecord;
58
68
  export interface DirectSessionValidationResult {
59
69
  valid: boolean;
@@ -61,5 +71,5 @@ export interface DirectSessionValidationResult {
61
71
  session: DirectSessionRecord | null;
62
72
  }
63
73
  export declare function validateDirectSessionFreshness(sessionId: string): DirectSessionValidationResult;
64
- export declare function finalizeDirectSessionRecord(sessionId: string, afterSnapshot: RepoSnapshot, changeArtifacts: ChangeArtifacts): DirectSessionRecord;
74
+ export declare function finalizeDirectSessionRecord(sessionId: string, changeArtifacts: ChangeArtifacts): DirectSessionRecord;
65
75
  export declare function computeWorkspaceFingerprint(snapshot: RepoSnapshot): string;
@@ -1,11 +1,18 @@
1
- import { mkdirSync, writeFileSync, existsSync, readFileSync, } from "node:fs";
1
+ import { mkdirSync, existsSync, } from "node:fs";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { resolve, join } from "node:path";
4
4
  import { getDirectSessionsDir, getConfig, getRepoDirectAllowedCommands, } from "../config.js";
5
5
  import { guardPath } from "../security/pathGuard.js";
6
6
  import { PatchWardenError } from "../errors.js";
7
7
  import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
8
- import { getLastToolCatalogSnapshot } from "../tools/toolCatalog.js";
8
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
9
+ import { redactSensitiveContent } from "../security/contentRedaction.js";
10
+ import { mutateLockedJsonFileSync, readJsonObjectFileSync, withFileLock, withFileLockSync, } from "../utils/lockedJsonFile.js";
11
+ import { getLastToolCatalogSnapshot } from "../tools/catalog/toolCatalog.js";
12
+ // Session history writes are metadata-only and may queue behind concurrent
13
+ // Direct workers. Keep the wait bounded, while workspace mutation locks remain
14
+ // fail-fast so an active patch is never hidden by retrying.
15
+ const DIRECT_SESSION_RECORD_LOCK_WAIT_MS = 10_000;
9
16
  // ── ID generation ──────────────────────────────────────────────────
10
17
  export function generateDirectSessionId() {
11
18
  const timestamp = new Date()
@@ -16,21 +23,32 @@ export function generateDirectSessionId() {
16
23
  const randomHex = randomBytes(16).toString("hex");
17
24
  return `direct_${timestamp}_${randomHex}`;
18
25
  }
26
+ /**
27
+ * Direct session IDs are directory names. Keep one shared, bounded grammar so
28
+ * MCP tools and Control Center routes cannot disagree about traversal input.
29
+ */
30
+ export function isValidDirectSessionId(sessionId) {
31
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(sessionId);
32
+ }
33
+ function guardDirectSessionId(sessionId) {
34
+ if (!isValidDirectSessionId(sessionId)) {
35
+ throw new PatchWardenError("invalid_session_id", `Direct session ID "${sessionId}" is invalid.`, "Use the session_id returned by create_direct_session.", true, { session_id: sessionId });
36
+ }
37
+ }
19
38
  // ── Directory management ───────────────────────────────────────────
20
- export function createDirectSessionDir(sessionId) {
21
- const config = getConfig();
39
+ export function createDirectSessionDir(sessionId, config = getConfig()) {
40
+ guardDirectSessionId(sessionId);
22
41
  const sessionsDir = getDirectSessionsDir(config);
23
42
  guardPath(sessionsDir, config.workspaceRoot, config.directSessionsDir);
24
43
  mkdirSync(sessionsDir, { recursive: true });
25
- const dir = resolve(sessionsDir, sessionId);
26
- guardPath(dir, config.workspaceRoot, config.directSessionsDir);
44
+ const dir = getDirectSessionDir(sessionId, config);
27
45
  mkdirSync(dir, { recursive: true });
28
46
  return dir;
29
47
  }
30
- export function getDirectSessionDir(sessionId) {
31
- const config = getConfig();
48
+ export function getDirectSessionDir(sessionId, config = getConfig()) {
49
+ guardDirectSessionId(sessionId);
32
50
  const sessionsDir = getDirectSessionsDir(config);
33
- return resolve(sessionsDir, sessionId);
51
+ return guardPath(resolve(sessionsDir, sessionId), config.workspaceRoot, config.directSessionsDir);
34
52
  }
35
53
  // ── CRUD ───────────────────────────────────────────────────────────
36
54
  export function createDirectSession(input) {
@@ -48,7 +66,11 @@ export function createDirectSession(input) {
48
66
  const expiresAt = new Date(now.getTime() + config.directSessionTtlSeconds * 1000).toISOString();
49
67
  const record = {
50
68
  session_id: sessionId,
51
- title: input.title || "",
69
+ title: redactSensitiveContent(input.title || "")
70
+ .content
71
+ .replace(/[\r\n\t]+/g, " ")
72
+ .trim()
73
+ .slice(0, 500),
52
74
  repo_path: input.repo_path,
53
75
  resolved_repo_path: input.resolved_repo_path,
54
76
  created_at: now.toISOString(),
@@ -66,36 +88,55 @@ export function createDirectSession(input) {
66
88
  audited: false,
67
89
  change_artifacts: null,
68
90
  };
69
- writeFileSync(join(dir, "session.json"), JSON.stringify(record, null, 2), "utf-8");
91
+ atomicWriteJsonFileSync(join(dir, "session.json"), record);
70
92
  return record;
71
93
  }
72
- export function readDirectSession(sessionId) {
73
- const config = getConfig();
74
- const sessionsDir = getDirectSessionsDir(config);
75
- const dir = resolve(sessionsDir, sessionId);
76
- const file = join(dir, "session.json");
77
- guardPath(file, config.workspaceRoot, config.directSessionsDir);
94
+ export function readDirectSession(sessionId, config = getConfig()) {
95
+ const file = getDirectSessionFile(sessionId, config);
78
96
  if (!existsSync(file)) {
79
97
  throw new PatchWardenError("session_not_found", `Direct session "${sessionId}" not found.`, "Call create_direct_session first to get a valid session_id.", true, { session_id: sessionId });
80
98
  }
81
- return JSON.parse(readFileSync(file, "utf-8"));
99
+ let value;
100
+ try {
101
+ value = readJsonObjectFileSync(file);
102
+ }
103
+ catch {
104
+ throw invalidDirectSessionRecord(sessionId, "session.json is not valid JSON");
105
+ }
106
+ return validateDirectSessionRecord(sessionId, value);
82
107
  }
83
108
  export function updateDirectSession(sessionId, updates) {
84
- const session = readDirectSession(sessionId);
85
- const updated = { ...session, ...updates };
86
- const dir = getDirectSessionDir(sessionId);
87
- writeFileSync(join(dir, "session.json"), JSON.stringify(updated, null, 2), "utf-8");
88
- return updated;
109
+ return mutateDirectSessionRecord(sessionId, (session) => ({
110
+ ...session,
111
+ ...updates,
112
+ session_id: session.session_id,
113
+ }));
114
+ }
115
+ export function appendDirectSessionOperation(sessionId, operation, config = getConfig()) {
116
+ return mutateDirectSessionRecord(sessionId, (session) => ({
117
+ ...session,
118
+ operations: [
119
+ ...session.operations,
120
+ {
121
+ ...operation,
122
+ index: session.operations.reduce((max, entry) => Math.max(max, entry.index), -1) + 1,
123
+ },
124
+ ],
125
+ }), config);
89
126
  }
90
- export function appendDirectSessionOperation(sessionId, operation) {
91
- const session = readDirectSession(sessionId);
92
- const operations = [...session.operations, operation];
93
- return updateDirectSession(sessionId, { operations });
127
+ export function withDirectSessionMutationLock(sessionId, action, config = getConfig()) {
128
+ const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
129
+ return withFileLockSync(lockTarget, action, directSessionBusyOptions(sessionId));
130
+ }
131
+ export function withDirectSessionMutationLockAsync(sessionId, action, config = getConfig()) {
132
+ const lockTarget = join(getDirectSessionDir(sessionId, config), "workspace-mutation");
133
+ return withFileLock(lockTarget, action, directSessionBusyOptions(sessionId));
94
134
  }
95
135
  export function appendDirectSessionVerificationRun(sessionId, run) {
96
- const session = readDirectSession(sessionId);
97
- const verification_runs = [...session.verification_runs, run];
98
- return updateDirectSession(sessionId, { verification_runs });
136
+ return mutateDirectSessionRecord(sessionId, (session) => ({
137
+ ...session,
138
+ verification_runs: [...session.verification_runs, run],
139
+ }));
99
140
  }
100
141
  export function validateDirectSessionFreshness(sessionId) {
101
142
  let session;
@@ -124,20 +165,22 @@ export function validateDirectSessionFreshness(sessionId) {
124
165
  return { valid: true, failure_reason: null, session };
125
166
  }
126
167
  // ── Finalization ───────────────────────────────────────────────────
127
- export function finalizeDirectSessionRecord(sessionId, afterSnapshot, changeArtifacts) {
168
+ export function finalizeDirectSessionRecord(sessionId, changeArtifacts) {
128
169
  const dir = getDirectSessionDir(sessionId);
129
170
  // Write change artifacts to session directory
130
- writeFileSync(join(dir, "changed-files.json"), JSON.stringify(changeArtifacts.changed_files, null, 2), "utf-8");
131
- writeFileSync(join(dir, "diff.patch"), changeArtifacts.diff, "utf-8");
171
+ atomicWriteJsonFileSync(join(dir, "changed-files.json"), changeArtifacts.changed_files);
172
+ atomicWriteFileSync(join(dir, "diff.patch"), changeArtifacts.diff);
132
173
  const summary = {
133
174
  changed_files_total: changeArtifacts.changed_files.length,
134
175
  additions: changeArtifacts.additions,
135
176
  deletions: changeArtifacts.deletions,
136
177
  patch_mode: changeArtifacts.patch_mode,
178
+ diff_redacted: changeArtifacts.diff_redacted === true,
179
+ diff_redaction_categories: changeArtifacts.diff_redaction_categories ?? [],
137
180
  artifact_hygiene: changeArtifacts.artifact_hygiene,
138
181
  };
139
- writeFileSync(join(dir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
140
- writeFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts), "utf-8");
182
+ atomicWriteJsonFileSync(join(dir, "summary.json"), summary);
183
+ atomicWriteFileSync(join(dir, "summary.md"), formatSummaryMd(changeArtifacts));
141
184
  return updateDirectSession(sessionId, {
142
185
  finalized: true,
143
186
  finalized_at: new Date().toISOString(),
@@ -166,6 +209,55 @@ function extractReason(error) {
166
209
  return error.message.slice(0, 80);
167
210
  return String(error).slice(0, 80);
168
211
  }
212
+ function invalidDirectSessionRecord(sessionId, detail) {
213
+ return new PatchWardenError("invalid_session_record", `Direct session "${sessionId}" is invalid: ${detail}.`, "Create a new direct session and do not modify its internal session.json file.", true, { session_id: sessionId });
214
+ }
215
+ function getDirectSessionFile(sessionId, config = getConfig()) {
216
+ const dir = getDirectSessionDir(sessionId, config);
217
+ return guardPath(join(dir, "session.json"), config.workspaceRoot, config.directSessionsDir);
218
+ }
219
+ function mutateDirectSessionRecord(sessionId, mutation, config = getConfig()) {
220
+ const file = getDirectSessionFile(sessionId, config);
221
+ if (!existsSync(file)) {
222
+ throw new PatchWardenError("session_not_found", `Direct session "${sessionId}" not found.`, "Call create_direct_session first to get a valid session_id.", true, { session_id: sessionId });
223
+ }
224
+ return mutateLockedJsonFileSync(file, (current) => {
225
+ const session = validateDirectSessionRecord(sessionId, current);
226
+ const next = mutation(session);
227
+ return { next, result: next };
228
+ }, {
229
+ waitMs: DIRECT_SESSION_RECORD_LOCK_WAIT_MS,
230
+ busyError: () => new PatchWardenError("direct_session_busy", `Direct session "${sessionId}" is currently being updated.`, "Retry after the current Direct operation completes.", true, { session_id: sessionId }),
231
+ });
232
+ }
233
+ function directSessionBusyOptions(sessionId) {
234
+ return {
235
+ waitMs: 0,
236
+ busyError: () => new PatchWardenError("direct_session_busy", `Direct session "${sessionId}" already has a workspace operation in progress.`, "Wait for the active patch, sync, verification, or finalization to finish, then retry.", true, { session_id: sessionId }),
237
+ };
238
+ }
239
+ function validateDirectSessionRecord(sessionId, value) {
240
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
241
+ throw invalidDirectSessionRecord(sessionId, "session.json must contain an object");
242
+ }
243
+ const record = value;
244
+ if (record.session_id !== sessionId) {
245
+ throw invalidDirectSessionRecord(sessionId, "session_id does not match its directory");
246
+ }
247
+ if (typeof record.resolved_repo_path !== "string" || record.resolved_repo_path.trim() === "") {
248
+ throw invalidDirectSessionRecord(sessionId, "resolved_repo_path is missing");
249
+ }
250
+ if (typeof record.expires_at !== "string" || !Number.isFinite(Date.parse(record.expires_at))) {
251
+ throw invalidDirectSessionRecord(sessionId, "expires_at is invalid");
252
+ }
253
+ if (typeof record.finalized !== "boolean") {
254
+ throw invalidDirectSessionRecord(sessionId, "finalized must be a boolean");
255
+ }
256
+ if (!Array.isArray(record.operations) || !Array.isArray(record.verification_runs)) {
257
+ throw invalidDirectSessionRecord(sessionId, "operation or verification history is invalid");
258
+ }
259
+ return record;
260
+ }
169
261
  function formatSummaryMd(artifacts) {
170
262
  const lines = [
171
263
  "# Direct Session Change Summary",
@@ -174,6 +266,7 @@ function formatSummaryMd(artifacts) {
174
266
  `**Additions:** ${artifacts.additions}`,
175
267
  `**Deletions:** ${artifacts.deletions}`,
176
268
  `**Patch mode:** ${artifacts.patch_mode}`,
269
+ `**Diff redacted:** ${artifacts.diff_redacted === true ? "yes" : "no"}`,
177
270
  "",
178
271
  "## Artifact Hygiene",
179
272
  "",
@@ -1,10 +1,11 @@
1
- import { writeFileSync } 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
+ import { atomicWriteJsonFileSync } from "../utils/atomicFile.js";
8
9
  const MAX_TAIL_CHARS = 10_000;
9
10
  const DEFAULT_TIMEOUT_SECONDS = 120;
10
11
  export async function runDirectVerification(input) {
@@ -13,7 +14,7 @@ export async function runDirectVerification(input) {
13
14
  const timeoutSeconds = input.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
14
15
  const timeoutMs = timeoutSeconds * 1000;
15
16
  // Parse command into command + args
16
- const parts = parseCommand(allowedCommand);
17
+ const parts = parseCommand(allowedCommand, input.resolvedRepoPath);
17
18
  if (!parts) {
18
19
  throw new Error(`Failed to parse command: "${allowedCommand}"`);
19
20
  }
@@ -73,27 +74,18 @@ export async function runDirectVerification(input) {
73
74
  ],
74
75
  spawn_error: result.spawnError,
75
76
  };
76
- writeFileSync(join(sessionDir, "verification.json"), JSON.stringify(verificationJson, null, 2), "utf-8");
77
+ atomicWriteJsonFileSync(join(sessionDir, "verification.json"), verificationJson);
77
78
  return { run, log_path: logPath };
78
79
  }
79
80
  // ── Helpers ────────────────────────────────────────────────────────
80
- function parseCommand(command) {
81
+ function parseCommand(command, cwd) {
81
82
  const trimmed = command.trim();
82
83
  if (!trimmed)
83
84
  return null;
84
- // Simple split by spaces - sufficient for allowlisted commands like "npm test"
85
- const parts = trimmed.split(/\s+/);
86
- // On Windows, npm/npx are batch files (.cmd) and spawn() cannot find them
87
- // without shell resolution. Wrap with cmd.exe /c for those commands.
88
- if (process.platform === "win32" && (parts[0] === "npm" || parts[0] === "npx")) {
89
- return {
90
- command: "cmd",
91
- args: ["/c", ...parts],
92
- };
93
- }
85
+ const invocation = resolveTrustedCommandLine(trimmed, cwd);
94
86
  return {
95
- command: parts[0],
96
- args: parts.slice(1),
87
+ command: invocation.command,
88
+ args: invocation.argsPrefix,
97
89
  };
98
90
  }
99
91
  function truncateTail(content, maxChars) {
package/dist/doctor.js CHANGED
@@ -9,28 +9,37 @@
9
9
  */
10
10
  import { existsSync, statSync, readFileSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
11
11
  import { isAbsolute, resolve, normalize, join } from "node:path";
12
- import { execSync } from "node:child_process";
13
12
  import { createServer } from "node:net";
14
13
  import { getConfig } from "./config.js";
15
14
  import { guardPath, guardWorkspacePath } from "./security/pathGuard.js";
16
15
  import { isSensitivePath } from "./security/sensitiveGuard.js";
17
16
  import { guardPlanContent } from "./security/planGuard.js";
18
- import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
17
+ import { TASK_READ_ONLY_FILES } from "./tools/tasks/getTaskFile.js";
19
18
  import { getToolDefs } from "./tools/registry.js";
20
- import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
21
- import { buildToolRegistry } from "./tools/toolRegistry.js";
22
- import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
19
+ import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/catalog/toolCatalog.js";
20
+ import { buildToolRegistry } from "./tools/catalog/toolRegistry.js";
21
+ import { runAllSchemaDriftChecks } from "./tools/diagnostics/schemaDriftCheck.js";
23
22
  import { runReleaseGateCheck } from "./release/releaseGate.js";
24
23
  import { PATCHWARDEN_VERSION } from "./version.js";
25
24
  import { logger } from "./logging.js";
25
+ import { unsafeWorkspaceRootLabel } from "./security/workspaceRootGuard.js";
26
+ import { runSimpleProcessSync } from "./runner/simpleProcess.js";
27
+ import { resolveAgentLaunch } from "./runner/agentInvocation.js";
28
+ import { isDangerousAllowedTestCommand } from "./diagnostics/allowedTestCommandSafety.js";
26
29
  // ── Helpers ────────────────────────────────────────────────────────
27
- function cmd(cmdStr) {
30
+ function cmd(command, args = []) {
28
31
  try {
29
- return execSync(cmdStr, {
30
- encoding: "utf-8",
31
- timeout: 5000,
32
- stdio: ["ignore", "pipe", "ignore"], // suppress stdin and stderr
33
- }).trim();
32
+ const result = runSimpleProcessSync({
33
+ command,
34
+ args: [...args],
35
+ cwd: process.cwd(),
36
+ timeoutMs: 5000,
37
+ maxStdoutBytes: 64 * 1024,
38
+ maxStderrBytes: 64 * 1024,
39
+ });
40
+ if (result.exitCode !== 0)
41
+ return "";
42
+ return `${result.stdout}${result.stderr}`.trim();
34
43
  }
35
44
  catch {
36
45
  return "";
@@ -75,7 +84,7 @@ const checkNpmAvailable = {
75
84
  id: "npm-available",
76
85
  description: "npm available",
77
86
  run() {
78
- const npmVer = cmd("npm --version");
87
+ const npmVer = cmd(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
79
88
  return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
80
89
  },
81
90
  };
@@ -101,7 +110,7 @@ const checkGitAvailable = {
101
110
  id: "git-available",
102
111
  description: "Git available",
103
112
  run() {
104
- const gitVer = cmd("git --version");
113
+ const gitVer = cmd("git", ["--version"]);
105
114
  return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
106
115
  },
107
116
  };
@@ -154,17 +163,9 @@ const checkWorkspaceRoot = {
154
163
  }
155
164
  catch { }
156
165
  results.push(checkResult("workspaceRoot is directory", isDir, ws));
157
- const dangerousRoots = [
158
- { pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
159
- { pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
160
- { pattern: /\\Desktop$/, label: "Desktop" },
161
- { pattern: /\\Downloads$/, label: "Downloads" },
162
- { pattern: /\\Documents$/, label: "Documents" },
163
- ];
164
- for (const { pattern, label } of dangerousRoots) {
165
- if (pattern.test(ws)) {
166
- results.push(warnResult(`workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`));
167
- }
166
+ const unsafeLabel = unsafeWorkspaceRootLabel(ws);
167
+ if (unsafeLabel) {
168
+ results.push(warnResult(`workspaceRoot is ${unsafeLabel}: ${ws} consider narrowing to a project directory`));
168
169
  }
169
170
  return results;
170
171
  },
@@ -258,7 +259,9 @@ const checkServerVersion = {
258
259
  const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
259
260
  return [
260
261
  checkResult("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`),
261
- checkResult("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js"),
262
+ process.env.PATCHWARDEN_DESKTOP_RUNTIME === "1"
263
+ ? okResult("Manifest preflight covered by compiled desktop runtime checks")
264
+ : checkResult("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js"),
262
265
  ];
263
266
  },
264
267
  };
@@ -375,23 +378,23 @@ const checkToolModules = {
375
378
  description: "Tool module checks",
376
379
  run() {
377
380
  const newTools = [
378
- "listTasks",
379
- "listAgents",
380
- "cancelTask",
381
- "killTask",
382
- "retryTask",
383
- "getTaskProgress",
384
- "getTaskSummary",
385
- "waitForTask",
386
- "getTaskStdoutTail",
387
- "healthCheck",
388
- "auditTask",
381
+ { name: "listTasks", subdir: "tasks" },
382
+ { name: "listAgents", subdir: "workspace" },
383
+ { name: "cancelTask", subdir: "tasks" },
384
+ { name: "killTask", subdir: "tasks" },
385
+ { name: "retryTask", subdir: "tasks" },
386
+ { name: "getTaskProgress", subdir: "tasks" },
387
+ { name: "getTaskSummary", subdir: "tasks" },
388
+ { name: "waitForTask", subdir: "tasks" },
389
+ { name: "getTaskStdoutTail", subdir: "tasks" },
390
+ { name: "healthCheck", subdir: "diagnostics" },
391
+ { name: "auditTask", subdir: "diagnostics" },
389
392
  ];
390
393
  const results = [];
391
394
  for (const t of newTools) {
392
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
395
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
393
396
  const exists = existsSync(compiled);
394
- results.push(checkResult(`Tool module: ${t}`, exists, exists ? "compiled" : "missing"));
397
+ results.push(checkResult(`Tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
395
398
  }
396
399
  return results;
397
400
  },
@@ -401,18 +404,18 @@ const checkDirectToolModules = {
401
404
  description: "Direct tool module checks",
402
405
  run() {
403
406
  const directToolModules = [
404
- "createDirectSession",
405
- "searchWorkspace",
406
- "applyPatch",
407
- "runVerification",
408
- "finalizeDirectSession",
409
- "auditSession",
407
+ { name: "createDirectSession", subdir: "direct" },
408
+ { name: "searchWorkspace", subdir: "workspace" },
409
+ { name: "applyPatch", subdir: "workspace" },
410
+ { name: "runVerification", subdir: "tasks" },
411
+ { name: "finalizeDirectSession", subdir: "direct" },
412
+ { name: "auditSession", subdir: "diagnostics" },
410
413
  ];
411
414
  const results = [];
412
415
  for (const t of directToolModules) {
413
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
416
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
414
417
  const exists = existsSync(compiled);
415
- results.push(checkResult(`Direct tool module: ${t}`, exists, exists ? "compiled" : "missing"));
418
+ results.push(checkResult(`Direct tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
416
419
  }
417
420
  return results;
418
421
  },
@@ -514,8 +517,7 @@ const checkAgentCommands = {
514
517
  if (!context.config)
515
518
  return [];
516
519
  const results = [];
517
- const agents = context.config.agents || {};
518
- for (const [name, agentCfg] of Object.entries(agents)) {
520
+ for (const [name, agentCfg] of Object.entries(context.config.agents)) {
519
521
  const cmdName = agentCfg.command;
520
522
  const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
521
523
  if (looksLikePath) {
@@ -523,11 +525,20 @@ const checkAgentCommands = {
523
525
  results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
524
526
  continue;
525
527
  }
526
- // Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
528
+ // Use an argumentized lookup utility; never interpolate config into a shell.
527
529
  const isWin = process.platform === "win32";
528
- const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
529
- const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
530
- const found = cmd(lookupCmd) || cmd(fallbackCmd);
530
+ let found = "";
531
+ if (isWin) {
532
+ try {
533
+ found = resolveAgentLaunch(name, cmdName, process.platform, process.env.PATH || "", existsSync, context.config.workspaceRoot, typeof agentCfg.adapter === "string" ? agentCfg.adapter : name).command;
534
+ }
535
+ catch {
536
+ found = "";
537
+ }
538
+ }
539
+ else {
540
+ found = cmd("which", [cmdName]);
541
+ }
531
542
  results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
532
543
  }
533
544
  return results;
@@ -542,12 +553,9 @@ const checkAllowedTestCommandsSafety = {
542
553
  const results = [];
543
554
  const testCmds = context.config.allowedTestCommands || [];
544
555
  results.push(checkResult("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured"));
545
- const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
546
556
  for (const cmdStr of testCmds) {
547
- for (const danger of dangerous) {
548
- if (cmdStr.toLowerCase().includes(danger)) {
549
- results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
550
- }
557
+ if (isDangerousAllowedTestCommand(cmdStr)) {
558
+ results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
551
559
  }
552
560
  }
553
561
  return results;