patchwarden 1.5.0 → 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 (597) hide show
  1. package/PatchWarden.cmd +32 -2
  2. package/README.en.md +194 -26
  3. package/README.md +160 -22
  4. package/dist/assessments/agentAssessor.d.ts +1 -1
  5. package/dist/assessments/agentAssessor.js +24 -15
  6. package/dist/assessments/assessmentStore.d.ts +1 -1
  7. package/dist/assessments/assessmentStore.js +44 -36
  8. package/dist/assessments/confirmCli.js +5 -4
  9. package/dist/config.d.ts +22 -0
  10. package/dist/config.js +98 -11
  11. package/dist/control/fileManager.d.ts +2 -0
  12. package/dist/control/fileManager.js +39 -0
  13. package/dist/control/middleware/auth.d.ts +16 -0
  14. package/dist/control/middleware/auth.js +17 -0
  15. package/dist/control/middleware/static.d.ts +3 -0
  16. package/dist/control/middleware/static.js +78 -0
  17. package/dist/control/routeTable.d.ts +24 -0
  18. package/dist/control/routeTable.js +263 -0
  19. package/dist/control/routes/audit.d.ts +15 -0
  20. package/dist/control/routes/audit.js +287 -0
  21. package/dist/control/routes/evidence.d.ts +4 -0
  22. package/dist/control/routes/evidence.js +96 -0
  23. package/dist/control/routes/lineage.d.ts +3 -0
  24. package/dist/control/routes/lineage.js +71 -0
  25. package/dist/control/routes/policy.d.ts +3 -0
  26. package/dist/control/routes/policy.js +81 -0
  27. package/dist/control/routes/process.d.ts +12 -0
  28. package/dist/control/routes/process.js +487 -0
  29. package/dist/control/routes/sessions.d.ts +30 -0
  30. package/dist/control/routes/sessions.js +287 -0
  31. package/dist/control/routes/status.d.ts +51 -0
  32. package/dist/control/routes/status.js +307 -0
  33. package/dist/control/routes/taskActions.d.ts +21 -0
  34. package/dist/control/routes/taskActions.js +197 -0
  35. package/dist/control/routes/tasks.d.ts +24 -0
  36. package/dist/control/routes/tasks.js +309 -0
  37. package/dist/control/routes/workspace.d.ts +15 -0
  38. package/dist/control/routes/workspace.js +220 -0
  39. package/dist/control/runtime.d.ts +92 -0
  40. package/dist/control/runtime.js +425 -0
  41. package/dist/control/server.d.ts +13 -0
  42. package/dist/control/server.js +150 -0
  43. package/dist/control/shared.d.ts +59 -0
  44. package/dist/control/shared.js +341 -0
  45. package/dist/controlCenter.d.ts +6 -0
  46. package/dist/controlCenter.js +7 -2197
  47. package/dist/direct/directAudit.js +234 -225
  48. package/dist/direct/directGuards.d.ts +2 -1
  49. package/dist/direct/directGuards.js +72 -37
  50. package/dist/direct/directPatch.d.ts +6 -1
  51. package/dist/direct/directPatch.js +55 -8
  52. package/dist/direct/directSessionStore.d.ts +16 -6
  53. package/dist/direct/directSessionStore.js +127 -35
  54. package/dist/direct/directVerification.js +8 -16
  55. package/dist/doctor.d.ts +18 -1
  56. package/dist/doctor.js +614 -373
  57. package/dist/goal/acceptanceEngine.d.ts +1 -1
  58. package/dist/goal/goalProgress.js +75 -69
  59. package/dist/goal/goalReport.d.ts +54 -0
  60. package/dist/goal/goalReport.js +197 -0
  61. package/dist/goal/goalStatus.d.ts +8 -0
  62. package/dist/goal/goalStatus.js +10 -3
  63. package/dist/goal/goalStore.d.ts +9 -2
  64. package/dist/goal/goalStore.js +152 -45
  65. package/dist/goal/handoffExport.js +6 -6
  66. package/dist/goal/specKitImport.d.ts +63 -0
  67. package/dist/goal/specKitImport.js +221 -0
  68. package/dist/goal/subgoalSync.js +13 -10
  69. package/dist/goal/worktreeManager.d.ts +2 -0
  70. package/dist/goal/worktreeManager.js +209 -136
  71. package/dist/httpServer.js +126 -44
  72. package/dist/index.js +7 -4
  73. package/dist/logging.d.ts +7 -1
  74. package/dist/logging.js +13 -3
  75. package/dist/policy/projectPolicy.js +5 -2
  76. package/dist/release/releaseGate.d.ts +1 -1
  77. package/dist/release/releaseGate.js +78 -25
  78. package/dist/runner/agentInvocation.d.ts +14 -0
  79. package/dist/runner/agentInvocation.js +96 -4
  80. package/dist/runner/changeCapture.d.ts +14 -6
  81. package/dist/runner/changeCapture.js +287 -81
  82. package/dist/runner/cli.js +7 -6
  83. package/dist/runner/postTaskCleanup.js +60 -7
  84. package/dist/runner/processSecurity.d.ts +56 -0
  85. package/dist/runner/processSecurity.js +373 -0
  86. package/dist/runner/runTask.d.ts +1 -1
  87. package/dist/runner/runTask.js +339 -287
  88. package/dist/runner/simpleProcess.d.ts +3 -0
  89. package/dist/runner/simpleProcess.js +52 -38
  90. package/dist/{taskProgress.d.ts → runner/taskProgress.d.ts} +1 -1
  91. package/dist/{taskProgress.js → runner/taskProgress.js} +2 -2
  92. package/dist/{taskRuntime.d.ts → runner/taskRuntime.d.ts} +1 -1
  93. package/dist/{taskRuntime.js → runner/taskRuntime.js} +3 -2
  94. package/dist/runner/taskStatusStore.d.ts +11 -0
  95. package/dist/runner/taskStatusStore.js +24 -0
  96. package/dist/runner/watch.d.ts +14 -1
  97. package/dist/runner/watch.js +265 -54
  98. package/dist/security/contentRedaction.d.ts +6 -0
  99. package/dist/security/contentRedaction.js +24 -2
  100. package/dist/security/discoveryTokenStore.d.ts +3 -2
  101. package/dist/security/discoveryTokenStore.js +17 -2
  102. package/dist/security/loopbackHost.d.ts +2 -0
  103. package/dist/security/loopbackHost.js +16 -0
  104. package/dist/security/pathGuard.js +32 -17
  105. package/dist/security/planGuard.js +2 -1
  106. package/dist/security/runtimeGuard.js +9 -6
  107. package/dist/security/secretComparison.d.ts +3 -0
  108. package/dist/security/secretComparison.js +11 -0
  109. package/dist/security/sensitiveGuard.d.ts +5 -0
  110. package/dist/security/sensitiveGuard.js +27 -8
  111. package/dist/security/toolInvocationGuard.d.ts +2 -2
  112. package/dist/security/toolInvocationGuard.js +1 -1
  113. package/dist/security/workspaceRootGuard.d.ts +8 -0
  114. package/dist/security/workspaceRootGuard.js +47 -0
  115. package/dist/smoke-test.js +280 -273
  116. package/dist/tools/catalog/index.d.ts +4 -0
  117. package/dist/tools/catalog/index.js +4 -0
  118. package/dist/tools/{toolCatalog.js → catalog/toolCatalog.js} +3 -12
  119. package/dist/tools/{toolRegistry.d.ts → catalog/toolRegistry.d.ts} +3 -3
  120. package/dist/tools/{toolRegistry.js → catalog/toolRegistry.js} +28 -15
  121. package/dist/tools/definitions/toolDefs.d.ts +18 -0
  122. package/dist/tools/definitions/toolDefs.js +1177 -0
  123. package/dist/tools/{auditSession.d.ts → diagnostics/auditSession.d.ts} +1 -1
  124. package/dist/tools/{auditSession.js → diagnostics/auditSession.js} +2 -2
  125. package/dist/tools/{auditTask.d.ts → diagnostics/auditTask.d.ts} +8 -63
  126. package/dist/tools/{auditTask.js → diagnostics/auditTask.js} +111 -37
  127. package/dist/tools/{healthCheck.d.ts → diagnostics/healthCheck.d.ts} +6 -6
  128. package/dist/tools/{healthCheck.js → diagnostics/healthCheck.js} +10 -10
  129. package/dist/tools/diagnostics/index.d.ts +6 -0
  130. package/dist/tools/diagnostics/index.js +6 -0
  131. package/dist/tools/{safeStatus.d.ts → diagnostics/safeStatus.d.ts} +3 -3
  132. package/dist/tools/{safeStatus.js → diagnostics/safeStatus.js} +6 -6
  133. package/dist/tools/{safeViews.d.ts → diagnostics/safeViews.d.ts} +14 -10
  134. package/dist/tools/{safeViews.js → diagnostics/safeViews.js} +25 -15
  135. package/dist/tools/{schemaDriftCheck.d.ts → diagnostics/schemaDriftCheck.d.ts} +1 -1
  136. package/dist/tools/{schemaDriftCheck.js → diagnostics/schemaDriftCheck.js} +2 -2
  137. package/dist/tools/{createDirectSession.d.ts → direct/createDirectSession.d.ts} +1 -1
  138. package/dist/tools/{createDirectSession.js → direct/createDirectSession.js} +8 -8
  139. package/dist/tools/{finalizeDirectSession.d.ts → direct/finalizeDirectSession.d.ts} +2 -2
  140. package/dist/tools/direct/finalizeDirectSession.js +86 -0
  141. package/dist/tools/direct/index.d.ts +3 -0
  142. package/dist/tools/direct/index.js +3 -0
  143. package/dist/tools/{runDirectVerificationBundle.js → direct/runDirectVerificationBundle.js} +2 -2
  144. package/dist/tools/{discoverTools.d.ts → discovery/discoverTools.d.ts} +2 -2
  145. package/dist/tools/{discoverTools.js → discovery/discoverTools.js} +5 -5
  146. package/dist/tools/{explainTool.d.ts → discovery/explainTool.d.ts} +2 -2
  147. package/dist/tools/{explainTool.js → discovery/explainTool.js} +2 -2
  148. package/dist/tools/discovery/index.d.ts +3 -0
  149. package/dist/tools/discovery/index.js +3 -0
  150. package/dist/tools/{invokeDiscoveredTool.d.ts → discovery/invokeDiscoveredTool.d.ts} +2 -2
  151. package/dist/tools/{invokeDiscoveredTool.js → discovery/invokeDiscoveredTool.js} +5 -5
  152. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  153. package/dist/tools/dispatch/coreDispatch.js +283 -0
  154. package/dist/tools/dispatch/diagnosticDispatch.d.ts +12 -0
  155. package/dist/tools/dispatch/diagnosticDispatch.js +80 -0
  156. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  157. package/dist/tools/dispatch/directDispatch.js +116 -0
  158. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  159. package/dist/tools/dispatch/goalDispatch.js +92 -0
  160. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  161. package/dist/tools/dispatch/releaseDispatch.js +46 -0
  162. package/dist/tools/dispatch/types.d.ts +23 -0
  163. package/dist/tools/dispatch/types.js +15 -0
  164. package/dist/tools/dispatch/validation.d.ts +7 -0
  165. package/dist/tools/dispatch/validation.js +71 -0
  166. package/dist/tools/{getPlan.js → goals/getPlan.js} +4 -4
  167. package/dist/tools/{goalSubgoalTask.d.ts → goals/goalSubgoalTask.d.ts} +2 -2
  168. package/dist/tools/goals/goalSubgoalTask.js +118 -0
  169. package/dist/tools/goals/index.d.ts +3 -0
  170. package/dist/tools/goals/index.js +3 -0
  171. package/dist/tools/{savePlan.js → goals/savePlan.js} +28 -8
  172. package/dist/tools/registry.d.ts +10 -14
  173. package/dist/tools/registry.js +47 -1651
  174. package/dist/tools/{checkReleaseGate.d.ts → release/checkReleaseGate.d.ts} +1 -1
  175. package/dist/tools/{checkReleaseGate.js → release/checkReleaseGate.js} +3 -3
  176. package/dist/tools/release/index.d.ts +2 -0
  177. package/dist/tools/release/index.js +2 -0
  178. package/dist/tools/{releaseMode.d.ts → release/releaseMode.d.ts} +2 -2
  179. package/dist/tools/{releaseMode.js → release/releaseMode.js} +46 -26
  180. package/dist/tools/tasks/cancelTask.d.ts +11 -0
  181. package/dist/tools/tasks/cancelTask.js +85 -0
  182. package/dist/tools/{createTask.d.ts → tasks/createTask.d.ts} +12 -4
  183. package/dist/tools/{createTask.js → tasks/createTask.js} +46 -31
  184. package/dist/tools/{diagnoseTask.d.ts → tasks/diagnoseTask.d.ts} +1 -1
  185. package/dist/tools/{diagnoseTask.js → tasks/diagnoseTask.js} +8 -8
  186. package/dist/tools/{evidencePack.d.ts → tasks/evidencePack.d.ts} +6 -0
  187. package/dist/tools/tasks/evidencePack.js +387 -0
  188. package/dist/tools/{getTaskFile.js → tasks/getTaskFile.js} +4 -4
  189. package/dist/tools/{getTaskStatus.d.ts → tasks/getTaskStatus.d.ts} +6 -1
  190. package/dist/tools/{getTaskStatus.js → tasks/getTaskStatus.js} +5 -5
  191. package/dist/tools/{getTaskStdoutTail.js → tasks/getTaskStdoutTail.js} +13 -11
  192. package/dist/tools/{getTaskSummary.js → tasks/getTaskSummary.js} +33 -26
  193. package/dist/tools/tasks/index.d.ts +18 -0
  194. package/dist/tools/tasks/index.js +18 -0
  195. package/dist/tools/tasks/killTask.d.ts +1 -0
  196. package/dist/tools/{listTasks.d.ts → tasks/listTasks.d.ts} +1 -1
  197. package/dist/tools/{listTasks.js → tasks/listTasks.js} +3 -3
  198. package/dist/tools/{reconcileTasks.d.ts → tasks/reconcileTasks.d.ts} +1 -1
  199. package/dist/tools/{reconcileTasks.js → tasks/reconcileTasks.js} +64 -66
  200. package/dist/tools/{retryTask.d.ts → tasks/retryTask.d.ts} +2 -2
  201. package/dist/tools/tasks/retryTask.js +60 -0
  202. package/dist/tools/{runTaskLoop.d.ts → tasks/runTaskLoop.d.ts} +6 -6
  203. package/dist/tools/{runTaskLoop.js → tasks/runTaskLoop.js} +59 -31
  204. package/dist/tools/tasks/runVerification.js +38 -0
  205. package/dist/tools/{taskLineage.js → tasks/taskLineage.js} +8 -7
  206. package/dist/tools/{taskOutputs.js → tasks/taskOutputs.js} +17 -16
  207. package/dist/tools/{androidDoctor.js → workspace/androidDoctor.js} +16 -10
  208. package/dist/tools/{applyPatch.d.ts → workspace/applyPatch.d.ts} +1 -1
  209. package/dist/tools/workspace/applyPatch.js +53 -0
  210. package/dist/tools/{discardWorktree.js → workspace/discardWorktree.js} +3 -3
  211. package/dist/tools/workspace/index.d.ts +10 -0
  212. package/dist/tools/workspace/index.js +10 -0
  213. package/dist/tools/{listAgents.d.ts → workspace/listAgents.d.ts} +5 -0
  214. package/dist/tools/{listAgents.js → workspace/listAgents.js} +8 -4
  215. package/dist/tools/{listWorkspace.js → workspace/listWorkspace.js} +3 -3
  216. package/dist/tools/{mergeWorktree.js → workspace/mergeWorktree.js} +3 -3
  217. package/dist/tools/{readWorkspaceFile.js → workspace/readWorkspaceFile.js} +8 -8
  218. package/dist/tools/{recommendAgentForTask.js → workspace/recommendAgentForTask.js} +3 -3
  219. package/dist/tools/{searchWorkspace.js → workspace/searchWorkspace.js} +5 -5
  220. package/dist/tools/{syncFile.d.ts → workspace/syncFile.d.ts} +1 -1
  221. package/dist/tools/workspace/syncFile.js +134 -0
  222. package/dist/utils/atomicFile.d.ts +7 -0
  223. package/dist/utils/atomicFile.js +48 -0
  224. package/dist/utils/boundedFile.d.ts +8 -0
  225. package/dist/utils/boundedFile.js +93 -0
  226. package/dist/utils/lockedJsonFile.d.ts +15 -0
  227. package/dist/utils/lockedJsonFile.js +224 -0
  228. package/dist/utils/platform.d.ts +11 -0
  229. package/dist/utils/platform.js +26 -0
  230. package/dist/utils/stableJson.d.ts +2 -0
  231. package/dist/utils/stableJson.js +12 -0
  232. package/dist/version.d.ts +3 -2
  233. package/dist/version.js +3 -2
  234. package/dist/watcherStatus.d.ts +2 -2
  235. package/dist/watcherStatus.js +18 -0
  236. package/docs/CODE_WIKI.md +1648 -0
  237. package/docs/agentseal-integration.md +150 -0
  238. package/docs/architecture.md +23 -0
  239. package/docs/assets/patchwarden-desktop-onboarding.png +0 -0
  240. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  241. package/docs/control-center/README.md +4 -4
  242. package/docs/dashboard-overview.md +86 -0
  243. package/docs/demo.md +8 -0
  244. package/docs/desktop-app.md +175 -0
  245. package/docs/direct-session-workflow.md +98 -0
  246. package/docs/evidence-pack-schema.md +215 -0
  247. package/docs/lineage-evidence-pack-workflow.md +127 -0
  248. package/docs/mcp-inspector-testing.md +200 -0
  249. package/docs/open-source-application.md +168 -0
  250. package/docs/opencode-worker.md +151 -0
  251. package/docs/openhands-worker.md +181 -0
  252. package/docs/release-checklist.md +1 -1
  253. package/docs/release-evidence.md +82 -0
  254. package/docs/spec-kit-integration.md +131 -0
  255. package/docs/task-safe-review-workflow.md +98 -0
  256. package/docs/threat-model.md +97 -0
  257. package/docs/user-feedback.md +40 -0
  258. package/docs/why-patchwarden.md +110 -0
  259. package/examples/config.example.json +4 -2
  260. package/package.json +28 -11
  261. package/scripts/README.md +15 -6
  262. package/scripts/build.js +11 -0
  263. package/scripts/checks/brand-check.js +3 -0
  264. package/scripts/checks/build-output-check.js +37 -0
  265. package/scripts/checks/control-center-smoke.js +447 -6
  266. package/scripts/checks/control-smoke.js +19 -8
  267. package/scripts/checks/http-mcp-smoke.js +82 -4
  268. package/scripts/checks/lifecycle-smoke.js +39 -33
  269. package/scripts/checks/mcp-manifest-check.js +1 -1
  270. package/scripts/checks/mcp-smoke.js +30 -4
  271. package/scripts/checks/package-manifest-check.js +20 -7
  272. package/scripts/checks/unit-tests.js +3 -5
  273. package/scripts/checks/watcher-supervisor-smoke.js +9 -0
  274. package/scripts/control/get-patchwarden-health.ps1 +1 -1
  275. package/scripts/control/manage-patchwarden.ps1 +69 -4
  276. package/scripts/control/provision-patchwarden-tunnel.ps1 +127 -0
  277. package/scripts/control/run-background-supervisor.ps1 +41 -0
  278. package/scripts/control/start-patchwarden-tunnel.ps1 +126 -32
  279. package/scripts/generate-demo-gif.py +320 -0
  280. package/{PatchWarden-Control-Tray.cmd → scripts/launchers/PatchWarden-Control-Tray.cmd} +3 -3
  281. package/scripts/launchers/PatchWarden-Control.cmd +6 -0
  282. package/{PatchWarden-Desktop.cmd → scripts/launchers/PatchWarden-Desktop.cmd} +2 -2
  283. package/scripts/launchers/Restart-PatchWarden-Control.cmd +6 -0
  284. package/{Stop-PatchWarden.cmd → scripts/launchers/Stop-PatchWarden.cmd} +2 -2
  285. package/scripts/lib/clean-generated-output.js +11 -0
  286. package/scripts/lib/file-discovery.js +12 -0
  287. package/scripts/release/desktop-preflight.js +211 -0
  288. package/scripts/release/pack-clean.js +59 -14
  289. package/src/assessments/agentAssessor.ts +29 -15
  290. package/src/assessments/assessmentStore.ts +55 -43
  291. package/src/assessments/confirmCli.ts +5 -4
  292. package/src/config.ts +105 -11
  293. package/src/control/fileManager.ts +40 -0
  294. package/src/control/middleware/auth.ts +35 -0
  295. package/src/control/middleware/static.ts +84 -0
  296. package/src/control/routeTable.ts +301 -0
  297. package/src/control/routes/audit.ts +329 -0
  298. package/src/control/routes/evidence.ts +107 -0
  299. package/src/control/routes/lineage.ts +92 -0
  300. package/src/control/routes/policy.ts +81 -0
  301. package/src/control/routes/process.ts +505 -0
  302. package/src/control/routes/sessions.ts +314 -0
  303. package/src/control/routes/status.ts +405 -0
  304. package/src/control/routes/taskActions.ts +214 -0
  305. package/src/control/routes/tasks.ts +331 -0
  306. package/src/control/routes/workspace.ts +234 -0
  307. package/src/control/runtime.ts +508 -0
  308. package/src/control/server.ts +165 -0
  309. package/src/control/shared.ts +360 -0
  310. package/src/controlCenter.ts +7 -2347
  311. package/src/direct/directAudit.ts +17 -9
  312. package/src/direct/directGuards.ts +100 -51
  313. package/src/direct/directPatch.ts +95 -8
  314. package/src/direct/directSessionStore.ts +211 -55
  315. package/src/direct/directVerification.ts +9 -23
  316. package/src/doctor.ts +766 -492
  317. package/src/goal/acceptanceEngine.ts +1 -1
  318. package/src/goal/goalProgress.ts +17 -13
  319. package/src/goal/goalReport.ts +263 -0
  320. package/src/goal/goalStatus.ts +25 -3
  321. package/src/goal/goalStore.ts +241 -54
  322. package/src/goal/handoffExport.ts +6 -6
  323. package/src/goal/specKitImport.ts +355 -0
  324. package/src/goal/subgoalSync.ts +15 -10
  325. package/src/goal/worktreeManager.ts +139 -22
  326. package/src/httpServer.ts +130 -44
  327. package/src/index.ts +7 -4
  328. package/src/logging.ts +20 -4
  329. package/src/policy/projectPolicy.ts +5 -2
  330. package/src/release/releaseGate.ts +74 -27
  331. package/src/runner/agentInvocation.ts +126 -4
  332. package/src/runner/changeCapture.ts +342 -80
  333. package/src/runner/cli.ts +7 -6
  334. package/src/runner/postTaskCleanup.ts +55 -7
  335. package/src/runner/processSecurity.ts +433 -0
  336. package/src/runner/runTask.ts +441 -292
  337. package/src/runner/simpleProcess.ts +68 -33
  338. package/src/{taskProgress.ts → runner/taskProgress.ts} +3 -3
  339. package/src/{taskRuntime.ts → runner/taskRuntime.ts} +4 -3
  340. package/src/runner/taskStatusStore.ts +54 -0
  341. package/src/runner/watch.ts +290 -57
  342. package/src/security/contentRedaction.ts +31 -2
  343. package/src/security/discoveryTokenStore.ts +18 -4
  344. package/src/security/loopbackHost.ts +19 -0
  345. package/src/security/pathGuard.ts +35 -16
  346. package/src/security/planGuard.ts +2 -1
  347. package/src/security/runtimeGuard.ts +9 -6
  348. package/src/security/secretComparison.ts +13 -0
  349. package/src/security/sensitiveGuard.ts +27 -8
  350. package/src/security/toolInvocationGuard.ts +2 -2
  351. package/src/security/workspaceRootGuard.ts +53 -0
  352. package/src/smoke-test.ts +276 -273
  353. package/src/tools/catalog/index.ts +4 -0
  354. package/src/tools/{toolCatalog.ts → catalog/toolCatalog.ts} +3 -12
  355. package/src/tools/{toolRegistry.ts → catalog/toolRegistry.ts} +29 -16
  356. package/src/tools/definitions/toolDefs.ts +1264 -0
  357. package/src/tools/{auditSession.ts → diagnostics/auditSession.ts} +2 -2
  358. package/src/tools/{auditTask.ts → diagnostics/auditTask.ts} +196 -89
  359. package/src/tools/{healthCheck.ts → diagnostics/healthCheck.ts} +11 -11
  360. package/src/tools/diagnostics/index.ts +6 -0
  361. package/src/tools/{safeStatus.ts → diagnostics/safeStatus.ts} +7 -7
  362. package/src/tools/{safeViews.ts → diagnostics/safeViews.ts} +29 -19
  363. package/src/tools/{schemaDriftCheck.ts → diagnostics/schemaDriftCheck.ts} +3 -3
  364. package/src/tools/{createDirectSession.ts → direct/createDirectSession.ts} +9 -9
  365. package/src/tools/{finalizeDirectSession.ts → direct/finalizeDirectSession.ts} +11 -9
  366. package/src/tools/direct/index.ts +3 -0
  367. package/src/tools/{runDirectVerificationBundle.ts → direct/runDirectVerificationBundle.ts} +2 -2
  368. package/src/tools/{discoverTools.ts → discovery/discoverTools.ts} +6 -6
  369. package/src/tools/{explainTool.ts → discovery/explainTool.ts} +3 -3
  370. package/src/tools/discovery/index.ts +3 -0
  371. package/src/tools/{invokeDiscoveredTool.ts → discovery/invokeDiscoveredTool.ts} +7 -7
  372. package/src/tools/dispatch/coreDispatch.ts +379 -0
  373. package/src/tools/dispatch/diagnosticDispatch.ts +106 -0
  374. package/src/tools/dispatch/directDispatch.ts +168 -0
  375. package/src/tools/dispatch/goalDispatch.ts +128 -0
  376. package/src/tools/dispatch/releaseDispatch.ts +66 -0
  377. package/src/tools/dispatch/types.ts +24 -0
  378. package/src/tools/dispatch/validation.ts +78 -0
  379. package/src/tools/{getPlan.ts → goals/getPlan.ts} +4 -4
  380. package/src/tools/{goalSubgoalTask.ts → goals/goalSubgoalTask.ts} +43 -28
  381. package/src/tools/goals/index.ts +3 -0
  382. package/src/tools/{savePlan.ts → goals/savePlan.ts} +38 -8
  383. package/src/tools/registry.ts +54 -1858
  384. package/src/tools/{checkReleaseGate.ts → release/checkReleaseGate.ts} +3 -3
  385. package/src/tools/release/index.ts +2 -0
  386. package/src/tools/{releaseMode.ts → release/releaseMode.ts} +42 -24
  387. package/src/tools/tasks/cancelTask.ts +109 -0
  388. package/src/tools/{createTask.ts → tasks/createTask.ts} +56 -33
  389. package/src/tools/{diagnoseTask.ts → tasks/diagnoseTask.ts} +8 -8
  390. package/src/tools/tasks/evidencePack.ts +504 -0
  391. package/src/tools/{getTaskFile.ts → tasks/getTaskFile.ts} +4 -4
  392. package/src/tools/{getTaskStatus.ts → tasks/getTaskStatus.ts} +7 -5
  393. package/src/tools/{getTaskStdoutTail.ts → tasks/getTaskStdoutTail.ts} +13 -11
  394. package/src/tools/{getTaskSummary.ts → tasks/getTaskSummary.ts} +44 -33
  395. package/src/tools/tasks/index.ts +18 -0
  396. package/src/tools/{listTasks.ts → tasks/listTasks.ts} +4 -4
  397. package/src/tools/{reconcileTasks.ts → tasks/reconcileTasks.ts} +68 -66
  398. package/src/tools/tasks/retryTask.ts +69 -0
  399. package/src/tools/{runTaskLoop.ts → tasks/runTaskLoop.ts} +70 -35
  400. package/src/tools/{runVerification.ts → tasks/runVerification.ts} +6 -3
  401. package/src/tools/{taskLineage.ts → tasks/taskLineage.ts} +8 -7
  402. package/src/tools/{taskOutputs.ts → tasks/taskOutputs.ts} +27 -20
  403. package/src/tools/{androidDoctor.ts → workspace/androidDoctor.ts} +15 -10
  404. package/src/tools/{applyPatch.ts → workspace/applyPatch.ts} +38 -16
  405. package/src/tools/{discardWorktree.ts → workspace/discardWorktree.ts} +3 -3
  406. package/src/tools/workspace/index.ts +10 -0
  407. package/src/tools/{listAgents.ts → workspace/listAgents.ts} +11 -4
  408. package/src/tools/{listWorkspace.ts → workspace/listWorkspace.ts} +3 -3
  409. package/src/tools/{mergeWorktree.ts → workspace/mergeWorktree.ts} +3 -3
  410. package/src/tools/{readWorkspaceFile.ts → workspace/readWorkspaceFile.ts} +8 -8
  411. package/src/tools/{recommendAgentForTask.ts → workspace/recommendAgentForTask.ts} +3 -3
  412. package/src/tools/{searchWorkspace.ts → workspace/searchWorkspace.ts} +5 -5
  413. package/src/tools/workspace/syncFile.ts +292 -0
  414. package/src/utils/atomicFile.ts +58 -0
  415. package/src/utils/boundedFile.ts +116 -0
  416. package/src/utils/lockedJsonFile.ts +251 -0
  417. package/src/utils/platform.ts +31 -0
  418. package/src/utils/stableJson.ts +12 -0
  419. package/src/version.ts +3 -2
  420. package/src/watcherStatus.ts +19 -1
  421. package/ui/desktop-bootstrap.js +11 -0
  422. package/ui/desktop-bridge.js +36 -0
  423. package/ui/desktop.css +160 -0
  424. package/ui/getting-started.js +93 -0
  425. package/ui/i18n.js +484 -0
  426. package/ui/log-parser.js +30 -0
  427. package/ui/pages/audit.html +204 -3
  428. package/ui/pages/dashboard.html +937 -79
  429. package/ui/pages/direct-sessions.html +609 -60
  430. package/ui/pages/getting-started.html +43 -0
  431. package/ui/pages/logs.html +105 -22
  432. package/ui/pages/settings.html +101 -0
  433. package/ui/pages/task-detail.html +468 -438
  434. package/ui/pages/tasks.html +648 -65
  435. package/ui/pages/workspace.html +38 -1
  436. package/ui/settings.js +245 -0
  437. package/PatchWarden-Control.cmd +0 -6
  438. package/Restart-PatchWarden-Control.cmd +0 -6
  439. package/dist/test/unit/acceptance-engine.test.d.ts +0 -1
  440. package/dist/test/unit/acceptance-engine.test.js +0 -228
  441. package/dist/test/unit/agent-router.test.d.ts +0 -1
  442. package/dist/test/unit/agent-router.test.js +0 -287
  443. package/dist/test/unit/android-doctor.test.d.ts +0 -1
  444. package/dist/test/unit/android-doctor.test.js +0 -118
  445. package/dist/test/unit/audit-checks.test.d.ts +0 -1
  446. package/dist/test/unit/audit-checks.test.js +0 -350
  447. package/dist/test/unit/chinese-path.test.d.ts +0 -1
  448. package/dist/test/unit/chinese-path.test.js +0 -91
  449. package/dist/test/unit/command-guard.test.d.ts +0 -1
  450. package/dist/test/unit/command-guard.test.js +0 -160
  451. package/dist/test/unit/diagnose-task.test.d.ts +0 -1
  452. package/dist/test/unit/diagnose-task.test.js +0 -457
  453. package/dist/test/unit/direct-guards.test.d.ts +0 -1
  454. package/dist/test/unit/direct-guards.test.js +0 -213
  455. package/dist/test/unit/discovery-token-store.test.d.ts +0 -1
  456. package/dist/test/unit/discovery-token-store.test.js +0 -139
  457. package/dist/test/unit/evidence-pack.test.d.ts +0 -1
  458. package/dist/test/unit/evidence-pack.test.js +0 -130
  459. package/dist/test/unit/goal-graph.test.d.ts +0 -1
  460. package/dist/test/unit/goal-graph.test.js +0 -298
  461. package/dist/test/unit/goal-progress.test.d.ts +0 -1
  462. package/dist/test/unit/goal-progress.test.js +0 -381
  463. package/dist/test/unit/goal-status.test.d.ts +0 -1
  464. package/dist/test/unit/goal-status.test.js +0 -215
  465. package/dist/test/unit/goal-store.test.d.ts +0 -1
  466. package/dist/test/unit/goal-store.test.js +0 -253
  467. package/dist/test/unit/goal-subgoal-task.test.d.ts +0 -1
  468. package/dist/test/unit/goal-subgoal-task.test.js +0 -55
  469. package/dist/test/unit/goal-tools-registry.test.d.ts +0 -1
  470. package/dist/test/unit/goal-tools-registry.test.js +0 -190
  471. package/dist/test/unit/handoff-export.test.d.ts +0 -1
  472. package/dist/test/unit/handoff-export.test.js +0 -263
  473. package/dist/test/unit/invoke-discovered-tool.test.d.ts +0 -1
  474. package/dist/test/unit/invoke-discovered-tool.test.js +0 -167
  475. package/dist/test/unit/logging.test.d.ts +0 -1
  476. package/dist/test/unit/logging.test.js +0 -397
  477. package/dist/test/unit/path-guard.test.d.ts +0 -1
  478. package/dist/test/unit/path-guard.test.js +0 -109
  479. package/dist/test/unit/post-task-cleanup.test.d.ts +0 -1
  480. package/dist/test/unit/post-task-cleanup.test.js +0 -48
  481. package/dist/test/unit/project-policy-release-mode.test.d.ts +0 -1
  482. package/dist/test/unit/project-policy-release-mode.test.js +0 -125
  483. package/dist/test/unit/reconcile-tasks.test.d.ts +0 -1
  484. package/dist/test/unit/reconcile-tasks.test.js +0 -456
  485. package/dist/test/unit/release-gate.test.d.ts +0 -1
  486. package/dist/test/unit/release-gate.test.js +0 -242
  487. package/dist/test/unit/run-task-loop.test.d.ts +0 -1
  488. package/dist/test/unit/run-task-loop.test.js +0 -380
  489. package/dist/test/unit/safe-status.test.d.ts +0 -1
  490. package/dist/test/unit/safe-status.test.js +0 -165
  491. package/dist/test/unit/safe-views.test.d.ts +0 -1
  492. package/dist/test/unit/safe-views.test.js +0 -171
  493. package/dist/test/unit/schema-drift-check.test.d.ts +0 -1
  494. package/dist/test/unit/schema-drift-check.test.js +0 -176
  495. package/dist/test/unit/sensitive-guard.test.d.ts +0 -1
  496. package/dist/test/unit/sensitive-guard.test.js +0 -104
  497. package/dist/test/unit/subgoal-sync.test.d.ts +0 -1
  498. package/dist/test/unit/subgoal-sync.test.js +0 -183
  499. package/dist/test/unit/sync-file.test.d.ts +0 -1
  500. package/dist/test/unit/sync-file.test.js +0 -154
  501. package/dist/test/unit/tool-invocation-guard.test.d.ts +0 -1
  502. package/dist/test/unit/tool-invocation-guard.test.js +0 -432
  503. package/dist/test/unit/tool-usage-stats.test.d.ts +0 -1
  504. package/dist/test/unit/tool-usage-stats.test.js +0 -300
  505. package/dist/test/unit/toolSearch.test.d.ts +0 -1
  506. package/dist/test/unit/toolSearch.test.js +0 -571
  507. package/dist/test/unit/watcher-status.test.d.ts +0 -1
  508. package/dist/test/unit/watcher-status.test.js +0 -169
  509. package/dist/test/unit/worktree-manager.test.d.ts +0 -1
  510. package/dist/test/unit/worktree-manager.test.js +0 -176
  511. package/dist/tools/applyPatch.js +0 -41
  512. package/dist/tools/cancelTask.d.ts +0 -30
  513. package/dist/tools/cancelTask.js +0 -64
  514. package/dist/tools/evidencePack.js +0 -168
  515. package/dist/tools/finalizeDirectSession.js +0 -84
  516. package/dist/tools/goalSubgoalTask.js +0 -110
  517. package/dist/tools/killTask.d.ts +0 -15
  518. package/dist/tools/retryTask.js +0 -45
  519. package/dist/tools/runVerification.js +0 -36
  520. package/dist/tools/syncFile.js +0 -65
  521. package/docs/control-center/control-center-daily-driver.md +0 -211
  522. package/docs/control-center/control-center-mvp.md +0 -205
  523. package/docs/control-center/control-center-phase2.md +0 -159
  524. package/docs/release-v0.6.0.md +0 -71
  525. package/docs/release-v0.6.1.md +0 -75
  526. package/docs/release-v0.6.4.md +0 -45
  527. package/src/test/unit/acceptance-engine.test.ts +0 -261
  528. package/src/test/unit/agent-router.test.ts +0 -342
  529. package/src/test/unit/android-doctor.test.ts +0 -158
  530. package/src/test/unit/audit-checks.test.ts +0 -567
  531. package/src/test/unit/chinese-path.test.ts +0 -106
  532. package/src/test/unit/command-guard.test.ts +0 -221
  533. package/src/test/unit/diagnose-task.test.ts +0 -544
  534. package/src/test/unit/direct-guards.test.ts +0 -297
  535. package/src/test/unit/discovery-token-store.test.ts +0 -181
  536. package/src/test/unit/evidence-pack.test.ts +0 -142
  537. package/src/test/unit/goal-graph.test.ts +0 -347
  538. package/src/test/unit/goal-progress.test.ts +0 -538
  539. package/src/test/unit/goal-status.test.ts +0 -270
  540. package/src/test/unit/goal-store.test.ts +0 -318
  541. package/src/test/unit/goal-subgoal-task.test.ts +0 -72
  542. package/src/test/unit/goal-tools-registry.test.ts +0 -243
  543. package/src/test/unit/handoff-export.test.ts +0 -295
  544. package/src/test/unit/invoke-discovered-tool.test.ts +0 -216
  545. package/src/test/unit/logging.test.ts +0 -497
  546. package/src/test/unit/path-guard.test.ts +0 -150
  547. package/src/test/unit/post-task-cleanup.test.ts +0 -53
  548. package/src/test/unit/project-policy-release-mode.test.ts +0 -156
  549. package/src/test/unit/reconcile-tasks.test.ts +0 -551
  550. package/src/test/unit/release-gate.test.ts +0 -314
  551. package/src/test/unit/run-task-loop.test.ts +0 -425
  552. package/src/test/unit/safe-status.test.ts +0 -187
  553. package/src/test/unit/safe-views.test.ts +0 -184
  554. package/src/test/unit/schema-drift-check.test.ts +0 -260
  555. package/src/test/unit/sensitive-guard.test.ts +0 -124
  556. package/src/test/unit/subgoal-sync.test.ts +0 -236
  557. package/src/test/unit/sync-file.test.ts +0 -231
  558. package/src/test/unit/tool-invocation-guard.test.ts +0 -542
  559. package/src/test/unit/tool-usage-stats.test.ts +0 -384
  560. package/src/test/unit/toolSearch.test.ts +0 -756
  561. package/src/test/unit/watcher-status.test.ts +0 -190
  562. package/src/test/unit/worktree-manager.test.ts +0 -247
  563. package/src/tools/cancelTask.ts +0 -70
  564. package/src/tools/evidencePack.ts +0 -205
  565. package/src/tools/retryTask.ts +0 -53
  566. package/src/tools/syncFile.ts +0 -122
  567. /package/dist/tools/{toolCatalog.d.ts → catalog/toolCatalog.d.ts} +0 -0
  568. /package/dist/tools/{toolSearch.d.ts → catalog/toolSearch.d.ts} +0 -0
  569. /package/dist/tools/{toolSearch.js → catalog/toolSearch.js} +0 -0
  570. /package/dist/tools/{toolUsageStats.d.ts → catalog/toolUsageStats.d.ts} +0 -0
  571. /package/dist/tools/{toolUsageStats.js → catalog/toolUsageStats.js} +0 -0
  572. /package/dist/tools/{runDirectVerificationBundle.d.ts → direct/runDirectVerificationBundle.d.ts} +0 -0
  573. /package/dist/tools/{getPlan.d.ts → goals/getPlan.d.ts} +0 -0
  574. /package/dist/tools/{savePlan.d.ts → goals/savePlan.d.ts} +0 -0
  575. /package/dist/tools/{getTaskFile.d.ts → tasks/getTaskFile.d.ts} +0 -0
  576. /package/dist/tools/{getTaskProgress.d.ts → tasks/getTaskProgress.d.ts} +0 -0
  577. /package/dist/tools/{getTaskProgress.js → tasks/getTaskProgress.js} +0 -0
  578. /package/dist/tools/{getTaskStdoutTail.d.ts → tasks/getTaskStdoutTail.d.ts} +0 -0
  579. /package/dist/tools/{getTaskSummary.d.ts → tasks/getTaskSummary.d.ts} +0 -0
  580. /package/dist/tools/{killTask.js → tasks/killTask.js} +0 -0
  581. /package/dist/tools/{runVerification.d.ts → tasks/runVerification.d.ts} +0 -0
  582. /package/dist/tools/{taskLineage.d.ts → tasks/taskLineage.d.ts} +0 -0
  583. /package/dist/tools/{taskOutputs.d.ts → tasks/taskOutputs.d.ts} +0 -0
  584. /package/dist/tools/{waitForTask.d.ts → tasks/waitForTask.d.ts} +0 -0
  585. /package/dist/tools/{waitForTask.js → tasks/waitForTask.js} +0 -0
  586. /package/dist/tools/{androidDoctor.d.ts → workspace/androidDoctor.d.ts} +0 -0
  587. /package/dist/tools/{discardWorktree.d.ts → workspace/discardWorktree.d.ts} +0 -0
  588. /package/dist/tools/{listWorkspace.d.ts → workspace/listWorkspace.d.ts} +0 -0
  589. /package/dist/tools/{mergeWorktree.d.ts → workspace/mergeWorktree.d.ts} +0 -0
  590. /package/dist/tools/{readWorkspaceFile.d.ts → workspace/readWorkspaceFile.d.ts} +0 -0
  591. /package/dist/tools/{recommendAgentForTask.d.ts → workspace/recommendAgentForTask.d.ts} +0 -0
  592. /package/dist/tools/{searchWorkspace.d.ts → workspace/searchWorkspace.d.ts} +0 -0
  593. /package/src/tools/{toolSearch.ts → catalog/toolSearch.ts} +0 -0
  594. /package/src/tools/{toolUsageStats.ts → catalog/toolUsageStats.ts} +0 -0
  595. /package/src/tools/{getTaskProgress.ts → tasks/getTaskProgress.ts} +0 -0
  596. /package/src/tools/{killTask.ts → tasks/killTask.ts} +0 -0
  597. /package/src/tools/{waitForTask.ts → tasks/waitForTask.ts} +0 -0
package/src/doctor.ts CHANGED
@@ -10,560 +10,834 @@
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
- import { loadConfig, getConfig } from "./config.js";
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";
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";
26
29
 
27
- // ── State ──────────────────────────────────────────────────────────
30
+ // ── Types ──────────────────────────────────────────────────────────
28
31
 
29
- let ok = 0;
30
- let warn = 0;
31
- let fail = 0;
32
- const results: string[] = [];
32
+ export type DoctorCheckLevel = "OK" | "WARN" | "FAIL";
33
33
 
34
- function check(name: string, condition: boolean, detail?: string) {
35
- if (condition) {
36
- results.push(`[OK] ${name}`);
37
- ok++;
38
- } else {
39
- results.push(`[FAIL] ${name}${detail ? " — " + detail : ""}`);
40
- fail++;
41
- }
34
+ export interface DoctorCheckResult {
35
+ level: DoctorCheckLevel;
36
+ message: string;
37
+ details?: string;
42
38
  }
43
39
 
44
- function warnCheck(name: string, condition: boolean, detail?: string) {
45
- if (condition) {
46
- results.push(`[OK] ${name}`);
47
- ok++;
48
- } else {
49
- results.push(`[WARN] ${name}${detail ? " — " + detail : ""}`);
50
- warn++;
51
- }
40
+ export interface DoctorCheck {
41
+ id: string;
42
+ description: string;
43
+ run(context: DoctorContext): DoctorCheckResult[] | Promise<DoctorCheckResult[]>;
52
44
  }
53
45
 
54
- function cmd(cmdStr: string): string {
46
+ export interface DoctorContext {
47
+ config: PatchWardenConfig | null;
48
+ configError: string | null;
49
+ allowDefaultConfig: boolean;
50
+ configPathUsed: string;
51
+ }
52
+
53
+ // ── Helpers ────────────────────────────────────────────────────────
54
+
55
+ function cmd(command: string, args: readonly string[] = []): string {
55
56
  try {
56
- return execSync(cmdStr, {
57
- encoding: "utf-8",
58
- timeout: 5000,
59
- stdio: ["ignore", "pipe", "ignore"], // suppress stdin and stderr
60
- }).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();
61
67
  } catch {
62
68
  return "";
63
69
  }
64
70
  }
65
71
 
66
- // ══════════════════════════════════════════════════════════════════
67
-
68
- async function main() {
69
-
70
- console.log("PatchWarden Doctor\n");
71
- const allowDefaultConfig = process.argv.includes("--allow-default-config");
72
-
73
- // 1. Node version
74
- const nodeVer = process.version;
75
- const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
76
- check("Node.js version", nodeMajor >= 18,
77
- nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`);
78
-
79
- // 2. npm
80
- const npmVer = cmd("npm --version");
81
- check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
82
-
83
- // Local-only medium-risk confirmation entrypoint. It must exist as a package
84
- // binary but must not be exposed as an MCP tool.
85
- try {
86
- const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
87
- check(
88
- "patchwarden-confirm package binary",
89
- packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js",
90
- packageJson.bin?.["patchwarden-confirm"] || "missing"
91
- );
92
- check(
93
- "patchwarden-confirm compiled entrypoint",
94
- existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")),
95
- "run npm.cmd run build"
96
- );
97
- } catch (error) {
98
- check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
72
+ function okResult(message: string, details?: string): DoctorCheckResult {
73
+ return { level: "OK", message, details };
99
74
  }
100
75
 
101
- // 3. Git
102
- const gitVer = cmd("git --version");
103
- warnCheck("Git available", gitVer !== "",
104
- gitVer || "git not found — runner git.diff will not work");
105
-
106
- // 4. Config file exists
107
- const configPaths = [
108
- resolve(process.cwd(), "patchwarden.config.json"),
109
- process.env.PATCHWARDEN_CONFIG || "",
110
- ].filter(Boolean);
111
-
112
- let configPathUsed = "";
113
- for (const p of configPaths) {
114
- if (existsSync(p)) { configPathUsed = p; break; }
76
+ function warnResult(message: string, details?: string): DoctorCheckResult {
77
+ return { level: "WARN", message, details };
115
78
  }
116
79
 
117
- const configDetail = configPathUsed
118
- ? configPathUsed
119
- : 'Create one: cp examples/config.example.json patchwarden.config.json';
120
- if (allowDefaultConfig) {
121
- warnCheck("Config file exists", configPathUsed !== "", configDetail);
122
- } else {
123
- check("Config file exists", configPathUsed !== "", configDetail);
80
+ function failResult(message: string, details?: string): DoctorCheckResult {
81
+ return { level: "FAIL", message, details };
124
82
  }
125
83
 
126
- // 5. PATCHWARDEN_CONFIG env
127
- if (process.env.PATCHWARDEN_CONFIG) {
128
- results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
129
- ok++;
130
- } else {
131
- results.push(`[OK] PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)`);
132
- ok++;
84
+ /** Mirrors original check(): OK if condition true, FAIL otherwise. */
85
+ function checkResult(name: string, condition: boolean, detail?: string): DoctorCheckResult {
86
+ return condition ? okResult(name, detail) : failResult(name, detail);
133
87
  }
134
88
 
135
- // Load config (may fail)
136
- let config: any = null;
137
- try {
138
- config = getConfig();
139
- check("Config parseable", true, `workspaceRoot: ${config.workspaceRoot}`);
140
- } catch (err) {
141
- check("Config parseable", false, err instanceof Error ? err.message : String(err));
89
+ /** Mirrors original warnCheck(): OK if condition true, WARN otherwise. */
90
+ function warnCheckResult(name: string, condition: boolean, detail?: string): DoctorCheckResult {
91
+ return condition ? okResult(name, detail) : warnResult(name, detail);
142
92
  }
143
93
 
144
- // 6. workspaceRoot checks
145
- if (config) {
146
- const ws = normalize(resolve(config.workspaceRoot));
147
- const exists = existsSync(ws);
148
- check("workspaceRoot exists", exists, ws);
149
-
150
- let isDir = false;
151
- try { isDir = statSync(ws).isDirectory(); } catch {}
152
- check("workspaceRoot is directory", isDir, ws);
153
-
154
- // Danger checks
155
- const dangerousRoots = [
156
- { pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
157
- { pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
158
- { pattern: /\\Desktop$/, label: "Desktop" },
159
- { pattern: /\\Downloads$/, label: "Downloads" },
160
- { pattern: /\\Documents$/, label: "Documents" },
161
- ];
162
-
163
- for (const { pattern, label } of dangerousRoots) {
164
- const matches = pattern.test(ws);
165
- if (matches) {
166
- results.push(`[WARN] workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`);
167
- warn++;
168
- }
94
+ function formatResult(result: DoctorCheckResult): string {
95
+ const tag = result.level === "OK" ? "[OK] " : result.level === "WARN" ? "[WARN] " : "[FAIL] ";
96
+ if (result.level !== "OK" && result.details) {
97
+ return `${tag}${result.message} ${result.details}`;
169
98
  }
99
+ return `${tag}${result.message}`;
170
100
  }
171
101
 
172
- // 7. Path guard test
173
- if (config) {
174
- try {
175
- guardPath("test-file.txt", config.workspaceRoot);
176
- results.push(`[OK] pathGuard allows workspace-internal path`);
177
- ok++;
178
- } catch (err) {
179
- results.push(`[FAIL] pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`);
180
- fail++;
181
- }
182
-
183
- try {
184
- guardPath("../outside", config.workspaceRoot);
185
- results.push(`[FAIL] pathGuard should have blocked ../escape`);
186
- fail++;
187
- } catch {
188
- results.push(`[OK] pathGuard blocks ../ path escape`);
189
- ok++;
190
- }
191
-
192
- try {
193
- const relativeRepo = guardWorkspacePath(".", config.workspaceRoot);
194
- const absoluteRepo = guardWorkspacePath(config.workspaceRoot, config.workspaceRoot);
195
- check("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo);
196
- } catch (error) {
197
- check("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error));
198
- }
199
- }
102
+ // ── Checks ─────────────────────────────────────────────────────────
103
+
104
+ const checkNodeVersion: DoctorCheck = {
105
+ id: "node-version",
106
+ description: "Node.js version",
107
+ run() {
108
+ const nodeVer = process.version;
109
+ const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
110
+ const detail = nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`;
111
+ return [checkResult("Node.js version", nodeMajor >= 18, detail)];
112
+ },
113
+ };
114
+
115
+ const checkNpmAvailable: DoctorCheck = {
116
+ id: "npm-available",
117
+ description: "npm available",
118
+ run() {
119
+ const npmVer = cmd(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
120
+ return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
121
+ },
122
+ };
200
123
 
201
- // 8. Sensitive file guard test
202
- const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
203
- for (const sp of sensitivePaths) {
204
- const blocked = isSensitivePath(sp);
205
- if (blocked) {
206
- results.push(`[OK] sensitiveGuard blocks "${sp}"`);
207
- ok++;
208
- } else {
209
- results.push(`[FAIL] sensitiveGuard does NOT block "${sp}"`);
210
- fail++;
211
- }
212
- }
124
+ // Local-only medium-risk confirmation entrypoint. It must exist as a package
125
+ // binary but must not be exposed as an MCP tool.
126
+ const checkConfirmPackage: DoctorCheck = {
127
+ id: "confirm-package",
128
+ description: "patchwarden-confirm package",
129
+ run() {
130
+ try {
131
+ const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
132
+ return [
133
+ checkResult(
134
+ "patchwarden-confirm package binary",
135
+ packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js",
136
+ packageJson.bin?.["patchwarden-confirm"] || "missing",
137
+ ),
138
+ checkResult(
139
+ "patchwarden-confirm compiled entrypoint",
140
+ existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")),
141
+ "run npm.cmd run build",
142
+ ),
143
+ ];
144
+ } catch (error) {
145
+ return [checkResult("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error))];
146
+ }
147
+ },
148
+ };
149
+
150
+ const checkGitAvailable: DoctorCheck = {
151
+ id: "git-available",
152
+ description: "Git available",
153
+ run() {
154
+ const gitVer = cmd("git", ["--version"]);
155
+ return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
156
+ },
157
+ };
158
+
159
+ const checkConfigFile: DoctorCheck = {
160
+ id: "config-file",
161
+ description: "Config file exists",
162
+ run(context) {
163
+ const configDetail = context.configPathUsed
164
+ ? context.configPathUsed
165
+ : 'Create one: cp examples/config.example.json patchwarden.config.json';
166
+ if (context.allowDefaultConfig) {
167
+ return [warnCheckResult("Config file exists", context.configPathUsed !== "", configDetail)];
168
+ }
169
+ return [checkResult("Config file exists", context.configPathUsed !== "", configDetail)];
170
+ },
171
+ };
172
+
173
+ const checkPatchwardenConfigEnv: DoctorCheck = {
174
+ id: "patchwarden-config-env",
175
+ description: "PATCHWARDEN_CONFIG env",
176
+ run() {
177
+ if (process.env.PATCHWARDEN_CONFIG) {
178
+ return [okResult(`PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`)];
179
+ }
180
+ return [okResult("PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)")];
181
+ },
182
+ };
183
+
184
+ const checkConfigParseable: DoctorCheck = {
185
+ id: "config-parseable",
186
+ description: "Config parseable",
187
+ run(context) {
188
+ if (context.config) {
189
+ return [checkResult("Config parseable", true, `workspaceRoot: ${context.config.workspaceRoot}`)];
190
+ }
191
+ return [checkResult("Config parseable", false, context.configError || "unknown error")];
192
+ },
193
+ };
194
+
195
+ const checkWorkspaceRoot: DoctorCheck = {
196
+ id: "workspace-root",
197
+ description: "workspaceRoot checks",
198
+ run(context) {
199
+ if (!context.config) return [];
200
+ const results: DoctorCheckResult[] = [];
201
+ const ws = normalize(resolve(context.config.workspaceRoot));
202
+ const exists = existsSync(ws);
203
+ results.push(checkResult("workspaceRoot exists", exists, ws));
204
+
205
+ let isDir = false;
206
+ try { isDir = statSync(ws).isDirectory(); } catch {}
207
+ results.push(checkResult("workspaceRoot is directory", isDir, ws));
208
+
209
+ const unsafeLabel = unsafeWorkspaceRootLabel(ws);
210
+ if (unsafeLabel) {
211
+ results.push(warnResult(`workspaceRoot is ${unsafeLabel}: ${ws} — consider narrowing to a project directory`));
212
+ }
213
+ return results;
214
+ },
215
+ };
216
+
217
+ const checkPathGuard: DoctorCheck = {
218
+ id: "path-guard",
219
+ description: "Path guard test",
220
+ run(context) {
221
+ if (!context.config) return [];
222
+ const results: DoctorCheckResult[] = [];
223
+ try {
224
+ guardPath("test-file.txt", context.config.workspaceRoot);
225
+ results.push(okResult("pathGuard allows workspace-internal path"));
226
+ } catch (err) {
227
+ results.push(failResult(`pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`));
228
+ }
213
229
 
214
- try {
215
- guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
216
- results.push("[OK] save_plan allows normal development plans");
217
- ok++;
218
- } catch {
219
- results.push("[FAIL] save_plan incorrectly blocks a normal development plan");
220
- fail++;
221
- }
222
- try {
223
- guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
224
- results.push("[FAIL] save_plan security rule did not block credential access");
225
- fail++;
226
- } catch {
227
- results.push("[OK] save_plan security rules loaded");
228
- ok++;
229
- }
230
+ try {
231
+ guardPath("../outside", context.config.workspaceRoot);
232
+ results.push(failResult("pathGuard should have blocked ../escape"));
233
+ } catch {
234
+ results.push(okResult("pathGuard blocks ../ path escape"));
235
+ }
230
236
 
231
- const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
232
- check(
233
- "Read-only task artifact allowlist",
234
- requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)),
235
- requiredReadOnlyFiles.join(", ")
236
- );
237
-
238
- const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
239
- check("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION,
240
- `${PATCHWARDEN_VERSION} vs ${packageJson.version}`);
241
- check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")),
242
- "scripts/checks/mcp-manifest-check.js");
243
-
244
- const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
245
- try {
246
- process.env.PATCHWARDEN_TOOL_PROFILE = "full";
247
- const fullTools = getToolDefs();
248
- const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
249
- const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema as any;
250
- const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema as any;
251
- check("Full tool profile exposes 64 tools", fullTools.length === 64, `${fullTools.length} tools`);
252
- check(
253
- `chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
254
- JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
255
- coreTools.map((tool) => tool.name).join(", ")
256
- );
257
- check(
258
- "Core task schemas expose inline_plan, verify_commands, and wait aliases",
259
- Boolean(
260
- createSchema?.properties?.inline_plan &&
261
- createSchema?.properties?.verify_commands &&
262
- waitSchema?.properties?.timeout_seconds &&
263
- waitSchema?.properties?.wait_seconds
264
- )
265
- );
266
-
267
- // chatgpt_direct profile checks
268
- const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
269
- check(
270
- "chatgpt_direct disabled exposes only health_check (degraded mode)",
271
- directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check",
272
- `${directDisabledTools.map((t) => t.name).join(", ")}`
273
- );
274
-
275
- if (config?.enableDirectProfile) {
276
- const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
277
- check(
278
- `chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`,
279
- JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES),
280
- directEnabledTools.map((tool) => tool.name).join(", ")
281
- );
282
- } else {
283
- results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
284
- ok++;
285
- }
237
+ try {
238
+ const relativeRepo = guardWorkspacePath(".", context.config.workspaceRoot);
239
+ const absoluteRepo = guardWorkspacePath(context.config.workspaceRoot, context.config.workspaceRoot);
240
+ results.push(checkResult("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo));
241
+ } catch (error) {
242
+ results.push(checkResult("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error)));
243
+ }
244
+ return results;
245
+ },
246
+ };
247
+
248
+ const checkSensitiveGuard: DoctorCheck = {
249
+ id: "sensitive-guard",
250
+ description: "Sensitive file guard test",
251
+ run() {
252
+ const results: DoctorCheckResult[] = [];
253
+ const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
254
+ for (const sp of sensitivePaths) {
255
+ const blocked = isSensitivePath(sp);
256
+ if (blocked) {
257
+ results.push(okResult(`sensitiveGuard blocks "${sp}"`));
258
+ } else {
259
+ results.push(failResult(`sensitiveGuard does NOT block "${sp}"`));
260
+ }
261
+ }
262
+ return results;
263
+ },
264
+ };
265
+
266
+ const checkSavePlanRules: DoctorCheck = {
267
+ id: "save-plan-rules",
268
+ description: "save_plan guard test",
269
+ run() {
270
+ const results: DoctorCheckResult[] = [];
271
+ try {
272
+ guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
273
+ results.push(okResult("save_plan allows normal development plans"));
274
+ } catch {
275
+ results.push(failResult("save_plan incorrectly blocks a normal development plan"));
276
+ }
277
+ try {
278
+ guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
279
+ results.push(failResult("save_plan security rule did not block credential access"));
280
+ } catch {
281
+ results.push(okResult("save_plan security rules loaded"));
282
+ }
283
+ return results;
284
+ },
285
+ };
286
+
287
+ const checkTaskArtifactAllowlist: DoctorCheck = {
288
+ id: "task-artifact-allowlist",
289
+ description: "Read-only task artifact allowlist",
290
+ run() {
291
+ const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
292
+ return [
293
+ checkResult(
294
+ "Read-only task artifact allowlist",
295
+ requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)),
296
+ requiredReadOnlyFiles.join(", "),
297
+ ),
298
+ ];
299
+ },
300
+ };
301
+
302
+ const checkServerVersion: DoctorCheck = {
303
+ id: "server-version",
304
+ description: "Server version and manifest preflight",
305
+ run() {
306
+ const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
307
+ return [
308
+ checkResult(
309
+ "Server version matches package.json",
310
+ packageJson.version === PATCHWARDEN_VERSION,
311
+ `${PATCHWARDEN_VERSION} vs ${packageJson.version}`,
312
+ ),
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
+ ];
321
+ },
322
+ };
323
+
324
+ const checkToolProfiles: DoctorCheck = {
325
+ id: "tool-profiles",
326
+ description: "Tool profile and schema drift checks",
327
+ run(context) {
328
+ const results: DoctorCheckResult[] = [];
329
+ const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
330
+ try {
331
+ process.env.PATCHWARDEN_TOOL_PROFILE = "full";
332
+ const fullTools = getToolDefs();
333
+ const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", context.config?.enableDirectProfile);
334
+ const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
335
+ const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
336
+ results.push(checkResult("Full tool profile exposes 66 tools", fullTools.length === 66, `${fullTools.length} tools`));
337
+ results.push(
338
+ checkResult(
339
+ `chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
340
+ JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
341
+ coreTools.map((tool) => tool.name).join(", "),
342
+ ),
343
+ );
344
+ results.push(
345
+ checkResult(
346
+ "Core task schemas expose inline_plan, verify_commands, and wait aliases",
347
+ Boolean(
348
+ createSchema?.properties?.inline_plan &&
349
+ createSchema?.properties?.verify_commands &&
350
+ waitSchema?.properties?.timeout_seconds &&
351
+ waitSchema?.properties?.wait_seconds,
352
+ ),
353
+ ),
354
+ );
355
+
356
+ // chatgpt_direct profile checks
357
+ const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
358
+ results.push(
359
+ checkResult(
360
+ "chatgpt_direct disabled exposes only health_check (degraded mode)",
361
+ directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check",
362
+ `${directDisabledTools.map((t) => t.name).join(", ")}`,
363
+ ),
364
+ );
365
+
366
+ if (context.config?.enableDirectProfile) {
367
+ const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
368
+ results.push(
369
+ checkResult(
370
+ `chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`,
371
+ JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES),
372
+ directEnabledTools.map((tool) => tool.name).join(", "),
373
+ ),
374
+ );
375
+ } else {
376
+ results.push(okResult("chatgpt_direct enabled check skipped (enableDirectProfile=false)"));
377
+ }
286
378
 
287
- // 16. Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
288
- // fullTools 同时构建 registry 和 toolDefs Map,确保 schema digest 比对基准一致。
289
- const driftRegistry = buildToolRegistry(fullTools);
290
- const driftToolDefs = new Map<string, { inputSchema: unknown }>();
291
- for (const tool of fullTools) {
292
- driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
293
- }
294
- const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
295
- if (driftResult.ok) {
296
- results.push(`[OK] Schema drift check — no drift detected`);
297
- ok++;
298
- } else {
299
- // 每个 warning 单独输出为 WARN,与其他自检项的多警告模式一致
300
- for (const w of driftResult.warnings) {
301
- results.push(`[WARN] Schema drift: ${w}`);
302
- warn++;
379
+ // Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
380
+ const driftRegistry = buildToolRegistry(fullTools);
381
+ const driftToolDefs = new Map<string, { inputSchema: unknown }>();
382
+ for (const tool of fullTools) {
383
+ driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
384
+ }
385
+ const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
386
+ if (driftResult.ok) {
387
+ results.push(okResult("Schema drift check — no drift detected"));
388
+ } else {
389
+ for (const w of driftResult.warnings) {
390
+ results.push(warnResult(`Schema drift: ${w}`));
391
+ }
392
+ }
393
+ } finally {
394
+ if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
395
+ else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
303
396
  }
304
- }
305
- } finally {
306
- if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
307
- else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
308
- }
397
+ return results;
398
+ },
399
+ };
309
400
 
310
- // 17. Release gate module loadable (v1.0.0) — module integrity only.
401
+ // Release gate module loadable (v1.0.0) — module integrity only.
311
402
  // Does NOT execute local_ready (would recurse into doctor:ci).
312
- try {
313
- const releaseGateReady = typeof runReleaseGateCheck === "function";
314
- check(
315
- "Release gate module loadable",
316
- releaseGateReady,
317
- releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid",
318
- );
319
- } catch (error) {
320
- check("Release gate module loadable", false, error instanceof Error ? error.message : String(error));
321
- }
322
-
323
- // 9. HTTP port check
324
- const httpPort = (config as any)?.http?.port || 7331;
325
- try {
326
- const server = createServer();
327
- await new Promise<void>((resolvePort, rejectPort) => {
328
- server.once("error", rejectPort);
329
- server.listen(httpPort, "127.0.0.1", () => {
330
- server.close();
331
- resolvePort();
332
- });
333
- });
334
- results.push(`[OK] HTTP port ${httpPort} is free`);
335
- ok++;
336
- } catch {
337
- results.push(`[WARN] HTTP port ${httpPort} is in use — change http.port in config`);
338
- warn++;
339
- }
403
+ const checkReleaseGate: DoctorCheck = {
404
+ id: "release-gate",
405
+ description: "Release gate module loadable",
406
+ run() {
407
+ try {
408
+ const releaseGateReady = typeof runReleaseGateCheck === "function";
409
+ return [
410
+ checkResult(
411
+ "Release gate module loadable",
412
+ releaseGateReady,
413
+ releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid",
414
+ ),
415
+ ];
416
+ } catch (error) {
417
+ return [checkResult("Release gate module loadable", false, error instanceof Error ? error.message : String(error))];
418
+ }
419
+ },
420
+ };
421
+
422
+ const checkHttpPort: DoctorCheck = {
423
+ id: "http-port",
424
+ description: "HTTP port check",
425
+ async run(context) {
426
+ const httpPort = context.config?.http?.port || context.config?.httpPort || 7331;
427
+ try {
428
+ const server = createServer();
429
+ await new Promise<void>((resolvePort, rejectPort) => {
430
+ server.once("error", rejectPort);
431
+ server.listen(httpPort, "127.0.0.1", () => {
432
+ server.close();
433
+ resolvePort();
434
+ });
435
+ });
436
+ return [okResult(`HTTP port ${httpPort} is free`)];
437
+ } catch {
438
+ return [warnResult(`HTTP port ${httpPort} is in use — change http.port in config`)];
439
+ }
440
+ },
441
+ };
442
+
443
+ const checkDistFiles: DoctorCheck = {
444
+ id: "dist-files",
445
+ description: "dist file checks",
446
+ run() {
447
+ const distChecks = [
448
+ { file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
449
+ { file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
450
+ { file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
451
+ ];
452
+ const results: DoctorCheckResult[] = [];
453
+ for (const { file, label, cmd: buildCmd } of distChecks) {
454
+ const exists = existsSync(resolve(process.cwd(), file));
455
+ results.push(checkResult(`${label} exists`, exists, exists ? file : `Missing — run: ${buildCmd}`));
456
+ }
457
+ return results;
458
+ },
459
+ };
460
+
461
+ const checkToolModules: DoctorCheck = {
462
+ id: "tool-modules",
463
+ description: "Tool module checks",
464
+ run() {
465
+ const newTools = [
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
+ ];
478
+ const results: DoctorCheckResult[] = [];
479
+ for (const t of newTools) {
480
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
481
+ const exists = existsSync(compiled);
482
+ results.push(checkResult(`Tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
483
+ }
484
+ return results;
485
+ },
486
+ };
487
+
488
+ const checkDirectToolModules: DoctorCheck = {
489
+ id: "direct-tool-modules",
490
+ description: "Direct tool module checks",
491
+ run() {
492
+ const directToolModules = [
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
+ ];
500
+ const results: DoctorCheckResult[] = [];
501
+ for (const t of directToolModules) {
502
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
503
+ const exists = existsSync(compiled);
504
+ results.push(checkResult(`Direct tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
505
+ }
506
+ return results;
507
+ },
508
+ };
509
+
510
+ const checkDirectSupportModules: DoctorCheck = {
511
+ id: "direct-support-modules",
512
+ description: "Direct support module checks",
513
+ run() {
514
+ const directSupportModules = [
515
+ "directSessionStore",
516
+ "directGuards",
517
+ "directPatch",
518
+ "directVerification",
519
+ "directAudit",
520
+ ];
521
+ const results: DoctorCheckResult[] = [];
522
+ for (const t of directSupportModules) {
523
+ const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
524
+ const exists = existsSync(compiled);
525
+ results.push(checkResult(`Direct support module: ${t}`, exists, exists ? "compiled" : "missing"));
526
+ }
527
+ return results;
528
+ },
529
+ };
530
+
531
+ const checkTaskDirectoryWritable: DoctorCheck = {
532
+ id: "task-directory-writable",
533
+ description: "Task directory and workspaceRoot writable",
534
+ run(context) {
535
+ if (!context.config) return [];
536
+ const results: DoctorCheckResult[] = [];
537
+ const tasksDir = resolve(context.config.workspaceRoot, context.config.tasksDir);
538
+ try {
539
+ mkdirSync(tasksDir, { recursive: true });
540
+ const testFile = join(tasksDir, ".doctor-write-test");
541
+ writeFileSync(testFile, "ok", "utf-8");
542
+ rmSync(testFile);
543
+ results.push(checkResult("Task directory writable", true, tasksDir));
544
+
545
+ const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
546
+ mkdirSync(sampleTaskDir, { recursive: true });
547
+ const sampleStatus = join(sampleTaskDir, "status.json");
548
+ writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
549
+ const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
550
+ rmSync(sampleTaskDir, { recursive: true, force: true });
551
+ results.push(checkResult("Example task directory read/write", sampleReadable, sampleTaskDir));
552
+ } catch {
553
+ results.push(warnCheckResult("Task directory writable", false, tasksDir));
554
+ }
340
555
 
341
- // 10-12. dist file checks
342
- const distChecks = [
343
- { file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
344
- { file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
345
- { file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
556
+ // workspaceRoot writable
557
+ try {
558
+ const testFile = resolve(context.config.workspaceRoot, ".doctor-write-test");
559
+ writeFileSync(testFile, "ok", "utf-8");
560
+ rmSync(testFile);
561
+ results.push(checkResult("workspaceRoot writable", true, context.config.workspaceRoot));
562
+ } catch {
563
+ results.push(warnCheckResult("workspaceRoot writable", false, context.config.workspaceRoot));
564
+ }
565
+ return results;
566
+ },
567
+ };
568
+
569
+ const checkWatcherStaleThreshold: DoctorCheck = {
570
+ id: "watcher-stale-threshold",
571
+ description: "Watcher stale threshold is valid",
572
+ run(context) {
573
+ if (!context.config) return [];
574
+ return [
575
+ checkResult(
576
+ "Watcher stale threshold is valid",
577
+ context.config.watcherStaleSeconds >= 5 && context.config.watcherStaleSeconds <= 3600,
578
+ `${context.config.watcherStaleSeconds}s`,
579
+ ),
580
+ ];
581
+ },
582
+ };
583
+
584
+ const checkAllowedTestCommandsIncludesNpmTest: DoctorCheck = {
585
+ id: "allowed-test-commands-npm-test",
586
+ description: "allowedTestCommands includes npm test",
587
+ run(context) {
588
+ if (!context.config) return [];
589
+ const hasNpmTest = context.config.allowedTestCommands.some((c: string) => c === "npm test" || c === "npm run test");
590
+ return [warnCheckResult("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands")];
591
+ },
592
+ };
593
+
594
+ const checkTaskTimeoutDefaults: DoctorCheck = {
595
+ id: "task-timeout-defaults",
596
+ description: "Task timeout defaults are valid",
597
+ run(context) {
598
+ if (!context.config) return [];
599
+ return [
600
+ checkResult(
601
+ "Task timeout defaults are valid",
602
+ context.config.defaultTaskTimeoutSeconds > 0 && context.config.defaultTaskTimeoutSeconds <= context.config.maxTaskTimeoutSeconds,
603
+ `default ${context.config.defaultTaskTimeoutSeconds}s, max ${context.config.maxTaskTimeoutSeconds}s`,
604
+ ),
605
+ ];
606
+ },
607
+ };
608
+
609
+ const checkAgentCommands: DoctorCheck = {
610
+ id: "agent-commands",
611
+ description: "Agent command check",
612
+ run(context) {
613
+ if (!context.config) return [];
614
+ const results: DoctorCheckResult[] = [];
615
+ for (const [name, agentCfg] of Object.entries(context.config.agents)) {
616
+ const cmdName = agentCfg.command;
617
+ const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
618
+ if (looksLikePath) {
619
+ const agentExists = existsSync(cmdName);
620
+ results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
621
+ continue;
622
+ }
623
+ // Use an argumentized lookup utility; never interpolate config into a shell.
624
+ const isWin = process.platform === "win32";
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
+ }
643
+ results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
644
+ }
645
+ return results;
646
+ },
647
+ };
648
+
649
+ const checkAllowedTestCommandsSafety: DoctorCheck = {
650
+ id: "allowed-test-commands-safety",
651
+ description: "allowedTestCommands safety check",
652
+ run(context) {
653
+ if (!context.config) return [];
654
+ const results: DoctorCheckResult[] = [];
655
+ const testCmds = context.config.allowedTestCommands || [];
656
+ results.push(checkResult("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured"));
657
+
658
+ const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
659
+ for (const cmdStr of testCmds) {
660
+ for (const danger of dangerous) {
661
+ if (cmdStr.toLowerCase().includes(danger)) {
662
+ results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
663
+ }
664
+ }
665
+ }
666
+ return results;
667
+ },
668
+ };
669
+
670
+ // Direct profile config checks
671
+ const checkDirectProfileConfig: DoctorCheck = {
672
+ id: "direct-profile-config",
673
+ description: "Direct profile config checks",
674
+ run(context) {
675
+ if (!context.config) return [];
676
+ const results: DoctorCheckResult[] = [];
677
+ const directCmds = context.config.directAllowedCommands || [];
678
+ results.push(warnCheckResult("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured"));
679
+
680
+ // npm run doctor should NOT be in default Direct whitelist
681
+ const hasDoctor = directCmds.some((c: string) => c === "npm run doctor");
682
+ results.push(checkResult("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present"));
683
+
684
+ results.push(checkResult("directSessionTtlSeconds is valid", context.config.directSessionTtlSeconds >= 60 && context.config.directSessionTtlSeconds <= 86400, `${context.config.directSessionTtlSeconds}s`));
685
+
686
+ results.push(checkResult("directMaxPatchBytes is positive", context.config.directMaxPatchBytes > 0, `${context.config.directMaxPatchBytes}`));
687
+
688
+ results.push(checkResult("directMaxFileBytes is positive", context.config.directMaxFileBytes > 0, `${context.config.directMaxFileBytes}`));
689
+ return results;
690
+ },
691
+ };
692
+
693
+ // Tunnel example files check
694
+ const checkTunnelExamples: DoctorCheck = {
695
+ id: "tunnel-examples",
696
+ description: "Tunnel example files check",
697
+ run() {
698
+ const tunnelFiles = [
699
+ "examples/openai-tunnel/README.md",
700
+ "examples/openai-tunnel/tunnel-client.example.yaml",
701
+ "examples/openai-tunnel/chatgpt-test-prompt.md",
702
+ "scripts/mcp/patchwarden-mcp-direct.cmd",
703
+ "PatchWarden.cmd",
704
+ "scripts/control/manage-patchwarden.ps1",
705
+ "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
706
+ ];
707
+ const results: DoctorCheckResult[] = [];
708
+ for (const tf of tunnelFiles) {
709
+ const full = resolve(process.cwd(), tf);
710
+ const exists = existsSync(full);
711
+ results.push(checkResult(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing"));
712
+
713
+ // Check for leaked secrets in example files
714
+ if (exists) {
715
+ const content = readFileSync(full, "utf-8");
716
+ // Only flag actual key-value assignments, not comments/mentions
717
+ const leaked = /(?:api_key|sk-[a-zA-Z0-9]{10,}|token\s*[:=]\s*\S{4,}|secret\s*[:=]\s*\S{4,}|password\s*[:=]\s*\S{4,})/gi.test(
718
+ // Strip comment lines first
719
+ content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"),
720
+ );
721
+ if (leaked) {
722
+ results.push(warnResult(`${tf} may contain secrets`));
723
+ } else {
724
+ results.push(okResult(`${tf} — no real secrets`));
725
+ }
726
+ }
727
+ }
728
+ return results;
729
+ },
730
+ };
731
+
732
+ // ── Check registry ─────────────────────────────────────────────────
733
+
734
+ const checks: DoctorCheck[] = [
735
+ checkNodeVersion,
736
+ checkNpmAvailable,
737
+ checkConfirmPackage,
738
+ checkGitAvailable,
739
+ checkConfigFile,
740
+ checkPatchwardenConfigEnv,
741
+ checkConfigParseable,
742
+ checkWorkspaceRoot,
743
+ checkPathGuard,
744
+ checkSensitiveGuard,
745
+ checkSavePlanRules,
746
+ checkTaskArtifactAllowlist,
747
+ checkServerVersion,
748
+ checkToolProfiles,
749
+ checkReleaseGate,
750
+ checkHttpPort,
751
+ checkDistFiles,
752
+ checkToolModules,
753
+ checkDirectToolModules,
754
+ checkDirectSupportModules,
755
+ checkTaskDirectoryWritable,
756
+ checkWatcherStaleThreshold,
757
+ checkAllowedTestCommandsIncludesNpmTest,
758
+ checkTaskTimeoutDefaults,
759
+ checkAgentCommands,
760
+ checkAllowedTestCommandsSafety,
761
+ checkDirectProfileConfig,
762
+ checkTunnelExamples,
346
763
  ];
347
- for (const { file, label, cmd: buildCmd } of distChecks) {
348
- check(`${label} exists`, existsSync(resolve(process.cwd(), file)),
349
- existsSync(resolve(process.cwd(), file)) ? file : `Missing — run: ${buildCmd}`);
350
- }
351
764
 
352
- // New tool registrations check
353
- const newTools = [
354
- "listTasks",
355
- "listAgents",
356
- "cancelTask",
357
- "killTask",
358
- "retryTask",
359
- "getTaskProgress",
360
- "getTaskSummary",
361
- "waitForTask",
362
- "getTaskStdoutTail",
363
- "healthCheck",
364
- "auditTask",
365
- ];
366
- for (const t of newTools) {
367
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
368
- check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
369
- }
765
+ // ── Context preparation ────────────────────────────────────────────
370
766
 
371
- // Direct tool module checks
372
- const directToolModules = [
373
- "createDirectSession",
374
- "searchWorkspace",
375
- "applyPatch",
376
- "runVerification",
377
- "finalizeDirectSession",
378
- "auditSession",
379
- ];
380
- for (const t of directToolModules) {
381
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
382
- check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
383
- }
767
+ function prepareContext(): DoctorContext {
768
+ const allowDefaultConfig = process.argv.includes("--allow-default-config");
384
769
 
385
- // Direct support module checks
386
- const directSupportModules = [
387
- "directSessionStore",
388
- "directGuards",
389
- "directPatch",
390
- "directVerification",
391
- "directAudit",
392
- ];
393
- for (const t of directSupportModules) {
394
- const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
395
- check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
396
- }
770
+ const configPaths = [
771
+ resolve(process.cwd(), "patchwarden.config.json"),
772
+ process.env.PATCHWARDEN_CONFIG || "",
773
+ ].filter(Boolean);
397
774
 
398
- // Task directory writable
399
- if (config) {
400
- const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
401
- try {
402
- mkdirSync(tasksDir, { recursive: true });
403
- const testFile = join(tasksDir, ".doctor-write-test");
404
- writeFileSync(testFile, "ok", "utf-8");
405
- rmSync(testFile);
406
- check("Task directory writable", true, tasksDir);
407
-
408
- const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
409
- mkdirSync(sampleTaskDir, { recursive: true });
410
- const sampleStatus = join(sampleTaskDir, "status.json");
411
- writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
412
- const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
413
- rmSync(sampleTaskDir, { recursive: true, force: true });
414
- check("Example task directory read/write", sampleReadable, sampleTaskDir);
415
- } catch {
416
- warnCheck("Task directory writable", false, tasksDir);
775
+ let configPathUsed = "";
776
+ for (const p of configPaths) {
777
+ if (existsSync(p)) { configPathUsed = p; break; }
417
778
  }
418
779
 
419
- // workspaceRoot writable
780
+ // Load config (may fail)
781
+ let config: PatchWardenConfig | null = null;
782
+ let configError: string | null = null;
420
783
  try {
421
- const testFile = resolve(config.workspaceRoot, ".doctor-write-test");
422
- writeFileSync(testFile, "ok", "utf-8");
423
- rmSync(testFile);
424
- check("workspaceRoot writable", true, config.workspaceRoot);
425
- } catch {
426
- warnCheck("workspaceRoot writable", false, config.workspaceRoot);
784
+ config = getConfig();
785
+ } catch (err) {
786
+ configError = err instanceof Error ? err.message : String(err);
427
787
  }
428
- }
429
-
430
- if (config) {
431
- check("Watcher stale threshold is valid",
432
- config.watcherStaleSeconds >= 5 && config.watcherStaleSeconds <= 3600,
433
- `${config.watcherStaleSeconds}s`);
434
- }
435
-
436
- // allowedTestCommands has npm test
437
- if (config) {
438
- const hasNpmTest = config.allowedTestCommands.some((c: string) => c === "npm test" || c === "npm run test");
439
- warnCheck("allowedTestCommands includes npm test", hasNpmTest,
440
- hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands");
441
- }
442
788
 
443
- if (config) {
444
- check("Task timeout defaults are valid",
445
- config.defaultTaskTimeoutSeconds > 0 && config.defaultTaskTimeoutSeconds <= config.maxTaskTimeoutSeconds,
446
- `default ${config.defaultTaskTimeoutSeconds}s, max ${config.maxTaskTimeoutSeconds}s`);
789
+ return { config, configError, allowDefaultConfig, configPathUsed };
447
790
  }
448
791
 
449
- // 13. Agent command check
450
- if (config) {
451
- const agents = config.agents || {};
452
- for (const [name, agentCfg] of Object.entries(agents) as [string, any][]) {
453
- const cmdName = agentCfg.command;
454
- const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
455
- if (looksLikePath) {
456
- const agentExists = existsSync(cmdName);
457
- warnCheck(`Agent "${name}" command available`, agentExists,
458
- agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`);
459
- continue;
460
- }
461
- // Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
462
- const isWin = process.platform === "win32";
463
- const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
464
- const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
465
- const found = cmd(lookupCmd) || cmd(fallbackCmd);
466
- warnCheck(`Agent "${name}" command available`, found !== "",
467
- found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`);
468
- }
469
- }
792
+ // ══════════════════════════════════════════════════════════════════
793
+ // Main
794
+ // ══════════════════════════════════════════════════════════════════
470
795
 
471
- // 14. allowedTestCommands safety check
472
- if (config) {
473
- const testCmds = config.allowedTestCommands || [];
474
- check("allowedTestCommands is non-empty", testCmds.length > 0,
475
- testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured");
476
-
477
- const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
478
- for (const cmdStr of testCmds) {
479
- for (const danger of dangerous) {
480
- if (cmdStr.toLowerCase().includes(danger)) {
481
- results.push(`[WARN] allowedTestCommands contains dangerous pattern: "${cmdStr}"`);
482
- warn++;
483
- }
796
+ async function main() {
797
+ console.log("PatchWarden Doctor\n");
798
+ const context = prepareContext();
799
+
800
+ const allResults: DoctorCheckResult[] = [];
801
+ for (const check of checks) {
802
+ try {
803
+ const results = await check.run(context);
804
+ allResults.push(...results);
805
+ } catch (err) {
806
+ allResults.push(failResult(check.description, err instanceof Error ? err.message : String(err)));
484
807
  }
485
808
  }
486
- }
487
-
488
- // 14b. Direct profile config checks
489
- if (config) {
490
- const directCmds = config.directAllowedCommands || [];
491
- warnCheck("directAllowedCommands is non-empty", directCmds.length > 0,
492
- directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
493
-
494
- // npm run doctor should NOT be in default Direct whitelist
495
- const hasDoctor = directCmds.some((c: string) => c === "npm run doctor");
496
- check("directAllowedCommands does not include npm run doctor", !hasDoctor,
497
- hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
498
809
 
499
- check("directSessionTtlSeconds is valid",
500
- config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400,
501
- `${config.directSessionTtlSeconds}s`);
810
+ console.log(allResults.map(formatResult).join("\n"));
502
811
 
503
- check("directMaxPatchBytes is positive",
504
- config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
505
-
506
- check("directMaxFileBytes is positive",
507
- config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
508
- }
509
-
510
- // 15. Tunnel example files check
511
- const tunnelFiles = [
512
- "examples/openai-tunnel/README.md",
513
- "examples/openai-tunnel/tunnel-client.example.yaml",
514
- "examples/openai-tunnel/chatgpt-test-prompt.md",
515
- "scripts/mcp/patchwarden-mcp-direct.cmd",
516
- "PatchWarden.cmd",
517
- "scripts/control/manage-patchwarden.ps1",
518
- "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
519
- ];
520
- for (const tf of tunnelFiles) {
521
- const full = resolve(process.cwd(), tf);
522
- const exists = existsSync(full);
523
- check(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing");
524
-
525
- // Check for leaked secrets in example files
526
- if (exists) {
527
- const content = readFileSync(full, "utf-8");
528
- // Only flag actual key-value assignments, not comments/mentions
529
- const leaked = /(?:api_key|sk-[a-zA-Z0-9]{10,}|token\s*[:=]\s*\S{4,}|secret\s*[:=]\s*\S{4,}|password\s*[:=]\s*\S{4,})/gi.test(
530
- // Strip comment lines first
531
- content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n")
532
- );
533
- if (leaked) {
534
- results.push(`[WARN] ${tf} may contain secrets`);
535
- warn++;
536
- } else {
537
- results.push(`[OK] ${tf} — no real secrets`);
538
- ok++;
539
- }
812
+ let ok = 0;
813
+ let warn = 0;
814
+ let fail = 0;
815
+ for (const r of allResults) {
816
+ if (r.level === "OK") ok++;
817
+ else if (r.level === "WARN") warn++;
818
+ else fail++;
540
819
  }
541
- }
542
820
 
543
- // ══════════════════════════════════════════════════════════════════
544
- // Summary
545
- // ══════════════════════════════════════════════════════════════════
546
-
547
- console.log(results.join("\n"));
548
- console.log(`\n${"=".repeat(50)}`);
549
- console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
550
- console.log(`${"=".repeat(50)}`);
551
-
552
- if (fail > 0) {
553
- console.log("\n❌ Doctor found issues that need attention.");
554
- console.log(" Fix FAIL items before using PatchWarden.");
555
- process.exit(1);
556
- } else if (warn > 0) {
557
- console.log("\n⚠️ Doctor found warnings — review before production use.");
558
- process.exit(0);
559
- } else {
560
- console.log("\n✅ All checks passed.");
561
- process.exit(0);
821
+ console.log(`\n${"=".repeat(50)}`);
822
+ console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
823
+ console.log(`${"=".repeat(50)}`);
824
+
825
+ if (fail > 0) {
826
+ console.log("\n❌ Doctor found issues that need attention.");
827
+ console.log(" Fix FAIL items before using PatchWarden.");
828
+ process.exit(1);
829
+ } else if (warn > 0) {
830
+ console.log("\n⚠️ Doctor found warnings — review before production use.");
831
+ process.exit(0);
832
+ } else {
833
+ console.log("\n✅ All checks passed.");
834
+ process.exit(0);
835
+ }
562
836
  }
563
837
 
564
- } // end async main
565
-
566
838
  main().catch((err) => {
567
- console.error("Doctor crashed:", err);
839
+ logger.fatal("Doctor crashed", {
840
+ error: err instanceof Error ? err.message : String(err),
841
+ });
568
842
  process.exit(1);
569
843
  });