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/dist/doctor.js CHANGED
@@ -9,461 +9,700 @@
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
- // ── State ──────────────────────────────────────────────────────────
26
- let ok = 0;
27
- let warn = 0;
28
- let fail = 0;
29
- const results = [];
30
- function check(name, condition, detail) {
31
- if (condition) {
32
- results.push(`[OK] ${name}`);
33
- ok++;
34
- }
35
- else {
36
- results.push(`[FAIL] ${name}${detail ? " — " + detail : ""}`);
37
- fail++;
38
- }
39
- }
40
- function warnCheck(name, condition, detail) {
41
- if (condition) {
42
- results.push(`[OK] ${name}`);
43
- ok++;
44
- }
45
- else {
46
- results.push(`[WARN] ${name}${detail ? " — " + detail : ""}`);
47
- warn++;
48
- }
49
- }
50
- function cmd(cmdStr) {
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
+ // ── Helpers ────────────────────────────────────────────────────────
29
+ function cmd(command, args = []) {
51
30
  try {
52
- return execSync(cmdStr, {
53
- encoding: "utf-8",
54
- timeout: 5000,
55
- stdio: ["ignore", "pipe", "ignore"], // suppress stdin and stderr
56
- }).trim();
31
+ const result = runSimpleProcessSync({
32
+ command,
33
+ args: [...args],
34
+ cwd: process.cwd(),
35
+ timeoutMs: 5000,
36
+ maxStdoutBytes: 64 * 1024,
37
+ maxStderrBytes: 64 * 1024,
38
+ });
39
+ if (result.exitCode !== 0)
40
+ return "";
41
+ return `${result.stdout}${result.stderr}`.trim();
57
42
  }
58
43
  catch {
59
44
  return "";
60
45
  }
61
46
  }
62
- // ══════════════════════════════════════════════════════════════════
63
- async function main() {
64
- console.log("PatchWarden Doctor\n");
65
- const allowDefaultConfig = process.argv.includes("--allow-default-config");
66
- // 1. Node version
67
- const nodeVer = process.version;
68
- const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
69
- check("Node.js version", nodeMajor >= 18, nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`);
70
- // 2. npm
71
- const npmVer = cmd("npm --version");
72
- check("npm available", npmVer !== "", npmVer || "npm not found in PATH");
73
- // Local-only medium-risk confirmation entrypoint. It must exist as a package
74
- // binary but must not be exposed as an MCP tool.
75
- try {
76
- const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
77
- check("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing");
78
- check("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build");
79
- }
80
- catch (error) {
81
- check("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error));
47
+ function okResult(message, details) {
48
+ return { level: "OK", message, details };
49
+ }
50
+ function warnResult(message, details) {
51
+ return { level: "WARN", message, details };
52
+ }
53
+ function failResult(message, details) {
54
+ return { level: "FAIL", message, details };
55
+ }
56
+ /** Mirrors original check(): OK if condition true, FAIL otherwise. */
57
+ function checkResult(name, condition, detail) {
58
+ return condition ? okResult(name, detail) : failResult(name, detail);
59
+ }
60
+ /** Mirrors original warnCheck(): OK if condition true, WARN otherwise. */
61
+ function warnCheckResult(name, condition, detail) {
62
+ return condition ? okResult(name, detail) : warnResult(name, detail);
63
+ }
64
+ function formatResult(result) {
65
+ const tag = result.level === "OK" ? "[OK] " : result.level === "WARN" ? "[WARN] " : "[FAIL] ";
66
+ if (result.level !== "OK" && result.details) {
67
+ return `${tag}${result.message} — ${result.details}`;
82
68
  }
83
- // 3. Git
84
- const gitVer = cmd("git --version");
85
- warnCheck("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work");
86
- // 4. Config file exists
87
- const configPaths = [
88
- resolve(process.cwd(), "patchwarden.config.json"),
89
- process.env.PATCHWARDEN_CONFIG || "",
90
- ].filter(Boolean);
91
- let configPathUsed = "";
92
- for (const p of configPaths) {
93
- if (existsSync(p)) {
94
- configPathUsed = p;
95
- break;
69
+ return `${tag}${result.message}`;
70
+ }
71
+ // ── Checks ─────────────────────────────────────────────────────────
72
+ const checkNodeVersion = {
73
+ id: "node-version",
74
+ description: "Node.js version",
75
+ run() {
76
+ const nodeVer = process.version;
77
+ const nodeMajor = parseInt(nodeVer.slice(1).split(".")[0]);
78
+ const detail = nodeMajor < 18 ? `v${nodeVer} — need >=18.0.0` : `v${nodeVer}`;
79
+ return [checkResult("Node.js version", nodeMajor >= 18, detail)];
80
+ },
81
+ };
82
+ const checkNpmAvailable = {
83
+ id: "npm-available",
84
+ description: "npm available",
85
+ run() {
86
+ const npmVer = cmd(process.platform === "win32" ? "npm.cmd" : "npm", ["--version"]);
87
+ return [checkResult("npm available", npmVer !== "", npmVer || "npm not found in PATH")];
88
+ },
89
+ };
90
+ // Local-only medium-risk confirmation entrypoint. It must exist as a package
91
+ // binary but must not be exposed as an MCP tool.
92
+ const checkConfirmPackage = {
93
+ id: "confirm-package",
94
+ description: "patchwarden-confirm package",
95
+ run() {
96
+ try {
97
+ const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
98
+ return [
99
+ checkResult("patchwarden-confirm package binary", packageJson.bin?.["patchwarden-confirm"] === "dist/assessments/confirmCli.js", packageJson.bin?.["patchwarden-confirm"] || "missing"),
100
+ checkResult("patchwarden-confirm compiled entrypoint", existsSync(resolve(process.cwd(), "dist/assessments/confirmCli.js")), "run npm.cmd run build"),
101
+ ];
96
102
  }
97
- }
98
- const configDetail = configPathUsed
99
- ? configPathUsed
100
- : 'Create one: cp examples/config.example.json patchwarden.config.json';
101
- if (allowDefaultConfig) {
102
- warnCheck("Config file exists", configPathUsed !== "", configDetail);
103
- }
104
- else {
105
- check("Config file exists", configPathUsed !== "", configDetail);
106
- }
107
- // 5. PATCHWARDEN_CONFIG env
108
- if (process.env.PATCHWARDEN_CONFIG) {
109
- results.push(`[OK] PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`);
110
- ok++;
111
- }
112
- else {
113
- results.push(`[OK] PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)`);
114
- ok++;
115
- }
116
- // Load config (may fail)
117
- let config = null;
118
- try {
119
- config = getConfig();
120
- check("Config parseable", true, `workspaceRoot: ${config.workspaceRoot}`);
121
- }
122
- catch (err) {
123
- check("Config parseable", false, err instanceof Error ? err.message : String(err));
124
- }
125
- // 6. workspaceRoot checks
126
- if (config) {
127
- const ws = normalize(resolve(config.workspaceRoot));
103
+ catch (error) {
104
+ return [checkResult("patchwarden-confirm package binary", false, error instanceof Error ? error.message : String(error))];
105
+ }
106
+ },
107
+ };
108
+ const checkGitAvailable = {
109
+ id: "git-available",
110
+ description: "Git available",
111
+ run() {
112
+ const gitVer = cmd("git", ["--version"]);
113
+ return [warnCheckResult("Git available", gitVer !== "", gitVer || "git not found — runner git.diff will not work")];
114
+ },
115
+ };
116
+ const checkConfigFile = {
117
+ id: "config-file",
118
+ description: "Config file exists",
119
+ run(context) {
120
+ const configDetail = context.configPathUsed
121
+ ? context.configPathUsed
122
+ : 'Create one: cp examples/config.example.json patchwarden.config.json';
123
+ if (context.allowDefaultConfig) {
124
+ return [warnCheckResult("Config file exists", context.configPathUsed !== "", configDetail)];
125
+ }
126
+ return [checkResult("Config file exists", context.configPathUsed !== "", configDetail)];
127
+ },
128
+ };
129
+ const checkPatchwardenConfigEnv = {
130
+ id: "patchwarden-config-env",
131
+ description: "PATCHWARDEN_CONFIG env",
132
+ run() {
133
+ if (process.env.PATCHWARDEN_CONFIG) {
134
+ return [okResult(`PATCHWARDEN_CONFIG = ${process.env.PATCHWARDEN_CONFIG}`)];
135
+ }
136
+ return [okResult("PATCHWARDEN_CONFIG not set (using default: patchwarden.config.json)")];
137
+ },
138
+ };
139
+ const checkConfigParseable = {
140
+ id: "config-parseable",
141
+ description: "Config parseable",
142
+ run(context) {
143
+ if (context.config) {
144
+ return [checkResult("Config parseable", true, `workspaceRoot: ${context.config.workspaceRoot}`)];
145
+ }
146
+ return [checkResult("Config parseable", false, context.configError || "unknown error")];
147
+ },
148
+ };
149
+ const checkWorkspaceRoot = {
150
+ id: "workspace-root",
151
+ description: "workspaceRoot checks",
152
+ run(context) {
153
+ if (!context.config)
154
+ return [];
155
+ const results = [];
156
+ const ws = normalize(resolve(context.config.workspaceRoot));
128
157
  const exists = existsSync(ws);
129
- check("workspaceRoot exists", exists, ws);
158
+ results.push(checkResult("workspaceRoot exists", exists, ws));
130
159
  let isDir = false;
131
160
  try {
132
161
  isDir = statSync(ws).isDirectory();
133
162
  }
134
163
  catch { }
135
- check("workspaceRoot is directory", isDir, ws);
136
- // Danger checks
137
- const dangerousRoots = [
138
- { pattern: /^[A-Za-z]:\\?$/, label: "drive root" },
139
- { pattern: /\\Users\\[^\\]+$/, label: "user home directory" },
140
- { pattern: /\\Desktop$/, label: "Desktop" },
141
- { pattern: /\\Downloads$/, label: "Downloads" },
142
- { pattern: /\\Documents$/, label: "Documents" },
143
- ];
144
- for (const { pattern, label } of dangerousRoots) {
145
- const matches = pattern.test(ws);
146
- if (matches) {
147
- results.push(`[WARN] workspaceRoot is ${label}: ${ws} — consider narrowing to a project directory`);
148
- warn++;
149
- }
164
+ results.push(checkResult("workspaceRoot is directory", isDir, ws));
165
+ const unsafeLabel = unsafeWorkspaceRootLabel(ws);
166
+ if (unsafeLabel) {
167
+ results.push(warnResult(`workspaceRoot is ${unsafeLabel}: ${ws} consider narrowing to a project directory`));
150
168
  }
151
- }
152
- // 7. Path guard test
153
- if (config) {
169
+ return results;
170
+ },
171
+ };
172
+ const checkPathGuard = {
173
+ id: "path-guard",
174
+ description: "Path guard test",
175
+ run(context) {
176
+ if (!context.config)
177
+ return [];
178
+ const results = [];
154
179
  try {
155
- guardPath("test-file.txt", config.workspaceRoot);
156
- results.push(`[OK] pathGuard allows workspace-internal path`);
157
- ok++;
180
+ guardPath("test-file.txt", context.config.workspaceRoot);
181
+ results.push(okResult("pathGuard allows workspace-internal path"));
158
182
  }
159
183
  catch (err) {
160
- results.push(`[FAIL] pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`);
161
- fail++;
184
+ results.push(failResult(`pathGuard rejects internal path: ${err instanceof Error ? err.message : String(err)}`));
162
185
  }
163
186
  try {
164
- guardPath("../outside", config.workspaceRoot);
165
- results.push(`[FAIL] pathGuard should have blocked ../escape`);
166
- fail++;
187
+ guardPath("../outside", context.config.workspaceRoot);
188
+ results.push(failResult("pathGuard should have blocked ../escape"));
167
189
  }
168
190
  catch {
169
- results.push(`[OK] pathGuard blocks ../ path escape`);
170
- ok++;
191
+ results.push(okResult("pathGuard blocks ../ path escape"));
171
192
  }
172
193
  try {
173
- const relativeRepo = guardWorkspacePath(".", config.workspaceRoot);
174
- const absoluteRepo = guardWorkspacePath(config.workspaceRoot, config.workspaceRoot);
175
- check("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo);
194
+ const relativeRepo = guardWorkspacePath(".", context.config.workspaceRoot);
195
+ const absoluteRepo = guardWorkspacePath(context.config.workspaceRoot, context.config.workspaceRoot);
196
+ results.push(checkResult("repo_path resolver supports relative and absolute paths", relativeRepo === absoluteRepo, relativeRepo));
176
197
  }
177
198
  catch (error) {
178
- check("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error));
199
+ results.push(checkResult("repo_path resolver supports relative and absolute paths", false, error instanceof Error ? error.message : String(error)));
179
200
  }
180
- }
181
- // 8. Sensitive file guard test
182
- const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
183
- for (const sp of sensitivePaths) {
184
- const blocked = isSensitivePath(sp);
185
- if (blocked) {
186
- results.push(`[OK] sensitiveGuard blocks "${sp}"`);
187
- ok++;
201
+ return results;
202
+ },
203
+ };
204
+ const checkSensitiveGuard = {
205
+ id: "sensitive-guard",
206
+ description: "Sensitive file guard test",
207
+ run() {
208
+ const results = [];
209
+ const sensitivePaths = [".env", ".ssh/id_rsa", "token.json", "credentials"];
210
+ for (const sp of sensitivePaths) {
211
+ const blocked = isSensitivePath(sp);
212
+ if (blocked) {
213
+ results.push(okResult(`sensitiveGuard blocks "${sp}"`));
214
+ }
215
+ else {
216
+ results.push(failResult(`sensitiveGuard does NOT block "${sp}"`));
217
+ }
188
218
  }
189
- else {
190
- results.push(`[FAIL] sensitiveGuard does NOT block "${sp}"`);
191
- fail++;
219
+ return results;
220
+ },
221
+ };
222
+ const checkSavePlanRules = {
223
+ id: "save-plan-rules",
224
+ description: "save_plan guard test",
225
+ run() {
226
+ const results = [];
227
+ try {
228
+ guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
229
+ results.push(okResult("save_plan allows normal development plans"));
192
230
  }
193
- }
194
- try {
195
- guardPlanContent("Normal build plan", "Run npm test, npm run lint, release check, and npm run dist.");
196
- results.push("[OK] save_plan allows normal development plans");
197
- ok++;
198
- }
199
- catch {
200
- results.push("[FAIL] save_plan incorrectly blocks a normal development plan");
201
- fail++;
202
- }
203
- try {
204
- guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
205
- results.push("[FAIL] save_plan security rule did not block credential access");
206
- fail++;
207
- }
208
- catch {
209
- results.push("[OK] save_plan security rules loaded");
210
- ok++;
211
- }
212
- const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
213
- check("Read-only task artifact allowlist", requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)), requiredReadOnlyFiles.join(", "));
214
- const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
215
- check("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`);
216
- check("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js");
217
- const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
218
- try {
219
- process.env.PATCHWARDEN_TOOL_PROFILE = "full";
220
- const fullTools = getToolDefs();
221
- const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
222
- const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
223
- const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
224
- check("Full tool profile exposes 64 tools", fullTools.length === 64, `${fullTools.length} tools`);
225
- check(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", "));
226
- check("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
227
- createSchema?.properties?.verify_commands &&
228
- waitSchema?.properties?.timeout_seconds &&
229
- waitSchema?.properties?.wait_seconds));
230
- // chatgpt_direct profile checks
231
- const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
232
- check("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`);
233
- if (config?.enableDirectProfile) {
234
- const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
235
- check(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", "));
236
- }
237
- else {
238
- results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
239
- ok++;
231
+ catch {
232
+ results.push(failResult("save_plan incorrectly blocks a normal development plan"));
240
233
  }
241
- // 16. Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
242
- // fullTools 同时构建 registry toolDefs Map,确保 schema digest 比对基准一致。
243
- const driftRegistry = buildToolRegistry(fullTools);
244
- const driftToolDefs = new Map();
245
- for (const tool of fullTools) {
246
- driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
234
+ try {
235
+ guardPlanContent("Unsafe plan", "Read the .env access token and export it.");
236
+ results.push(failResult("save_plan security rule did not block credential access"));
247
237
  }
248
- const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
249
- if (driftResult.ok) {
250
- results.push(`[OK] Schema drift check — no drift detected`);
251
- ok++;
238
+ catch {
239
+ results.push(okResult("save_plan security rules loaded"));
252
240
  }
253
- else {
254
- // 每个 warning 单独输出为 WARN,与其他自检项的多警告模式一致
255
- for (const w of driftResult.warnings) {
256
- results.push(`[WARN] Schema drift: ${w}`);
257
- warn++;
241
+ return results;
242
+ },
243
+ };
244
+ const checkTaskArtifactAllowlist = {
245
+ id: "task-artifact-allowlist",
246
+ description: "Read-only task artifact allowlist",
247
+ run() {
248
+ const requiredReadOnlyFiles = ["status.json", "result.md", "result.json", "diff.patch", "file-stats.json", "test.log", "verify.json"];
249
+ return [
250
+ checkResult("Read-only task artifact allowlist", requiredReadOnlyFiles.every((name) => TASK_READ_ONLY_FILES.includes(name)), requiredReadOnlyFiles.join(", ")),
251
+ ];
252
+ },
253
+ };
254
+ const checkServerVersion = {
255
+ id: "server-version",
256
+ description: "Server version and manifest preflight",
257
+ run() {
258
+ const packageJson = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
259
+ return [
260
+ checkResult("Server version matches package.json", packageJson.version === PATCHWARDEN_VERSION, `${PATCHWARDEN_VERSION} vs ${packageJson.version}`),
261
+ process.env.PATCHWARDEN_DESKTOP_RUNTIME === "1"
262
+ ? okResult("Manifest preflight covered by compiled desktop runtime checks")
263
+ : checkResult("Manifest preflight script exists", existsSync(resolve(process.cwd(), "scripts/checks/mcp-manifest-check.js")), "scripts/checks/mcp-manifest-check.js"),
264
+ ];
265
+ },
266
+ };
267
+ const checkToolProfiles = {
268
+ id: "tool-profiles",
269
+ description: "Tool profile and schema drift checks",
270
+ run(context) {
271
+ const results = [];
272
+ const previousProfile = process.env.PATCHWARDEN_TOOL_PROFILE;
273
+ try {
274
+ process.env.PATCHWARDEN_TOOL_PROFILE = "full";
275
+ const fullTools = getToolDefs();
276
+ const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", context.config?.enableDirectProfile);
277
+ const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema;
278
+ const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema;
279
+ results.push(checkResult("Full tool profile exposes 66 tools", fullTools.length === 66, `${fullTools.length} tools`));
280
+ results.push(checkResult(`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`, JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES), coreTools.map((tool) => tool.name).join(", ")));
281
+ results.push(checkResult("Core task schemas expose inline_plan, verify_commands, and wait aliases", Boolean(createSchema?.properties?.inline_plan &&
282
+ createSchema?.properties?.verify_commands &&
283
+ waitSchema?.properties?.timeout_seconds &&
284
+ waitSchema?.properties?.wait_seconds)));
285
+ // chatgpt_direct profile checks
286
+ const directDisabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", false);
287
+ results.push(checkResult("chatgpt_direct disabled exposes only health_check (degraded mode)", directDisabledTools.length === 1 && directDisabledTools[0].name === "health_check", `${directDisabledTools.map((t) => t.name).join(", ")}`));
288
+ if (context.config?.enableDirectProfile) {
289
+ const directEnabledTools = selectToolsForProfile(fullTools, "chatgpt_direct", true);
290
+ results.push(checkResult(`chatgpt_direct enabled exposes the exact ${CHATGPT_DIRECT_TOOL_NAMES.length}-tool manifest`, JSON.stringify(directEnabledTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_DIRECT_TOOL_NAMES), directEnabledTools.map((tool) => tool.name).join(", ")));
291
+ }
292
+ else {
293
+ results.push(okResult("chatgpt_direct enabled check skipped (enableDirectProfile=false)"));
294
+ }
295
+ // Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
296
+ const driftRegistry = buildToolRegistry(fullTools);
297
+ const driftToolDefs = new Map();
298
+ for (const tool of fullTools) {
299
+ driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
300
+ }
301
+ const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
302
+ if (driftResult.ok) {
303
+ results.push(okResult("Schema drift check — no drift detected"));
304
+ }
305
+ else {
306
+ for (const w of driftResult.warnings) {
307
+ results.push(warnResult(`Schema drift: ${w}`));
308
+ }
258
309
  }
259
310
  }
260
- }
261
- finally {
262
- if (previousProfile === undefined)
263
- delete process.env.PATCHWARDEN_TOOL_PROFILE;
264
- else
265
- process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
266
- }
267
- // 17. Release gate module loadable (v1.0.0) — module integrity only.
268
- // Does NOT execute local_ready (would recurse into doctor:ci).
269
- try {
270
- const releaseGateReady = typeof runReleaseGateCheck === "function";
271
- check("Release gate module loadable", releaseGateReady, releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid");
272
- }
273
- catch (error) {
274
- check("Release gate module loadable", false, error instanceof Error ? error.message : String(error));
275
- }
276
- // 9. HTTP port check
277
- const httpPort = config?.http?.port || 7331;
278
- try {
279
- const server = createServer();
280
- await new Promise((resolvePort, rejectPort) => {
281
- server.once("error", rejectPort);
282
- server.listen(httpPort, "127.0.0.1", () => {
283
- server.close();
284
- resolvePort();
311
+ finally {
312
+ if (previousProfile === undefined)
313
+ delete process.env.PATCHWARDEN_TOOL_PROFILE;
314
+ else
315
+ process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
316
+ }
317
+ return results;
318
+ },
319
+ };
320
+ // Release gate module loadable (v1.0.0) — module integrity only.
321
+ // Does NOT execute local_ready (would recurse into doctor:ci).
322
+ const checkReleaseGate = {
323
+ id: "release-gate",
324
+ description: "Release gate module loadable",
325
+ run() {
326
+ try {
327
+ const releaseGateReady = typeof runReleaseGateCheck === "function";
328
+ return [
329
+ checkResult("Release gate module loadable", releaseGateReady, releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid"),
330
+ ];
331
+ }
332
+ catch (error) {
333
+ return [checkResult("Release gate module loadable", false, error instanceof Error ? error.message : String(error))];
334
+ }
335
+ },
336
+ };
337
+ const checkHttpPort = {
338
+ id: "http-port",
339
+ description: "HTTP port check",
340
+ async run(context) {
341
+ const httpPort = context.config?.http?.port || context.config?.httpPort || 7331;
342
+ try {
343
+ const server = createServer();
344
+ await new Promise((resolvePort, rejectPort) => {
345
+ server.once("error", rejectPort);
346
+ server.listen(httpPort, "127.0.0.1", () => {
347
+ server.close();
348
+ resolvePort();
349
+ });
285
350
  });
286
- });
287
- results.push(`[OK] HTTP port ${httpPort} is free`);
288
- ok++;
289
- }
290
- catch {
291
- results.push(`[WARN] HTTP port ${httpPort} is in use — change http.port in config`);
292
- warn++;
293
- }
294
- // 10-12. dist file checks
295
- const distChecks = [
296
- { file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
297
- { file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
298
- { file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
299
- ];
300
- for (const { file, label, cmd: buildCmd } of distChecks) {
301
- check(`${label} exists`, existsSync(resolve(process.cwd(), file)), existsSync(resolve(process.cwd(), file)) ? file : `Missing — run: ${buildCmd}`);
302
- }
303
- // New tool registrations check
304
- const newTools = [
305
- "listTasks",
306
- "listAgents",
307
- "cancelTask",
308
- "killTask",
309
- "retryTask",
310
- "getTaskProgress",
311
- "getTaskSummary",
312
- "waitForTask",
313
- "getTaskStdoutTail",
314
- "healthCheck",
315
- "auditTask",
316
- ];
317
- for (const t of newTools) {
318
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
319
- check(`Tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
320
- }
321
- // Direct tool module checks
322
- const directToolModules = [
323
- "createDirectSession",
324
- "searchWorkspace",
325
- "applyPatch",
326
- "runVerification",
327
- "finalizeDirectSession",
328
- "auditSession",
329
- ];
330
- for (const t of directToolModules) {
331
- const compiled = resolve(process.cwd(), "dist/tools", `${t}.js`);
332
- check(`Direct tool module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
333
- }
334
- // Direct support module checks
335
- const directSupportModules = [
336
- "directSessionStore",
337
- "directGuards",
338
- "directPatch",
339
- "directVerification",
340
- "directAudit",
341
- ];
342
- for (const t of directSupportModules) {
343
- const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
344
- check(`Direct support module: ${t}`, existsSync(compiled), existsSync(compiled) ? "compiled" : "missing");
345
- }
346
- // Task directory writable
347
- if (config) {
348
- const tasksDir = resolve(config.workspaceRoot, config.tasksDir);
351
+ return [okResult(`HTTP port ${httpPort} is free`)];
352
+ }
353
+ catch {
354
+ return [warnResult(`HTTP port ${httpPort} is in use — change http.port in config`)];
355
+ }
356
+ },
357
+ };
358
+ const checkDistFiles = {
359
+ id: "dist-files",
360
+ description: "dist file checks",
361
+ run() {
362
+ const distChecks = [
363
+ { file: "dist/index.js", label: "stdio MCP entry", cmd: "npm run build" },
364
+ { file: "dist/httpServer.js", label: "HTTP MCP entry", cmd: "npm run build" },
365
+ { file: "dist/runner/watch.js", label: "watcher entry (npm run watch)", cmd: "npm run build" },
366
+ ];
367
+ const results = [];
368
+ for (const { file, label, cmd: buildCmd } of distChecks) {
369
+ const exists = existsSync(resolve(process.cwd(), file));
370
+ results.push(checkResult(`${label} exists`, exists, exists ? file : `Missing — run: ${buildCmd}`));
371
+ }
372
+ return results;
373
+ },
374
+ };
375
+ const checkToolModules = {
376
+ id: "tool-modules",
377
+ description: "Tool module checks",
378
+ run() {
379
+ const newTools = [
380
+ { name: "listTasks", subdir: "tasks" },
381
+ { name: "listAgents", subdir: "workspace" },
382
+ { name: "cancelTask", subdir: "tasks" },
383
+ { name: "killTask", subdir: "tasks" },
384
+ { name: "retryTask", subdir: "tasks" },
385
+ { name: "getTaskProgress", subdir: "tasks" },
386
+ { name: "getTaskSummary", subdir: "tasks" },
387
+ { name: "waitForTask", subdir: "tasks" },
388
+ { name: "getTaskStdoutTail", subdir: "tasks" },
389
+ { name: "healthCheck", subdir: "diagnostics" },
390
+ { name: "auditTask", subdir: "diagnostics" },
391
+ ];
392
+ const results = [];
393
+ for (const t of newTools) {
394
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
395
+ const exists = existsSync(compiled);
396
+ results.push(checkResult(`Tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
397
+ }
398
+ return results;
399
+ },
400
+ };
401
+ const checkDirectToolModules = {
402
+ id: "direct-tool-modules",
403
+ description: "Direct tool module checks",
404
+ run() {
405
+ const directToolModules = [
406
+ { name: "createDirectSession", subdir: "direct" },
407
+ { name: "searchWorkspace", subdir: "workspace" },
408
+ { name: "applyPatch", subdir: "workspace" },
409
+ { name: "runVerification", subdir: "tasks" },
410
+ { name: "finalizeDirectSession", subdir: "direct" },
411
+ { name: "auditSession", subdir: "diagnostics" },
412
+ ];
413
+ const results = [];
414
+ for (const t of directToolModules) {
415
+ const compiled = resolve(process.cwd(), "dist/tools", t.subdir, `${t.name}.js`);
416
+ const exists = existsSync(compiled);
417
+ results.push(checkResult(`Direct tool module: ${t.name}`, exists, exists ? "compiled" : "missing"));
418
+ }
419
+ return results;
420
+ },
421
+ };
422
+ const checkDirectSupportModules = {
423
+ id: "direct-support-modules",
424
+ description: "Direct support module checks",
425
+ run() {
426
+ const directSupportModules = [
427
+ "directSessionStore",
428
+ "directGuards",
429
+ "directPatch",
430
+ "directVerification",
431
+ "directAudit",
432
+ ];
433
+ const results = [];
434
+ for (const t of directSupportModules) {
435
+ const compiled = resolve(process.cwd(), "dist/direct", `${t}.js`);
436
+ const exists = existsSync(compiled);
437
+ results.push(checkResult(`Direct support module: ${t}`, exists, exists ? "compiled" : "missing"));
438
+ }
439
+ return results;
440
+ },
441
+ };
442
+ const checkTaskDirectoryWritable = {
443
+ id: "task-directory-writable",
444
+ description: "Task directory and workspaceRoot writable",
445
+ run(context) {
446
+ if (!context.config)
447
+ return [];
448
+ const results = [];
449
+ const tasksDir = resolve(context.config.workspaceRoot, context.config.tasksDir);
349
450
  try {
350
451
  mkdirSync(tasksDir, { recursive: true });
351
452
  const testFile = join(tasksDir, ".doctor-write-test");
352
453
  writeFileSync(testFile, "ok", "utf-8");
353
454
  rmSync(testFile);
354
- check("Task directory writable", true, tasksDir);
455
+ results.push(checkResult("Task directory writable", true, tasksDir));
355
456
  const sampleTaskDir = join(tasksDir, ".doctor-sample-task");
356
457
  mkdirSync(sampleTaskDir, { recursive: true });
357
458
  const sampleStatus = join(sampleTaskDir, "status.json");
358
459
  writeFileSync(sampleStatus, JSON.stringify({ status: "doctor" }), "utf-8");
359
460
  const sampleReadable = JSON.parse(readFileSync(sampleStatus, "utf-8")).status === "doctor";
360
461
  rmSync(sampleTaskDir, { recursive: true, force: true });
361
- check("Example task directory read/write", sampleReadable, sampleTaskDir);
462
+ results.push(checkResult("Example task directory read/write", sampleReadable, sampleTaskDir));
362
463
  }
363
464
  catch {
364
- warnCheck("Task directory writable", false, tasksDir);
465
+ results.push(warnCheckResult("Task directory writable", false, tasksDir));
365
466
  }
366
467
  // workspaceRoot writable
367
468
  try {
368
- const testFile = resolve(config.workspaceRoot, ".doctor-write-test");
469
+ const testFile = resolve(context.config.workspaceRoot, ".doctor-write-test");
369
470
  writeFileSync(testFile, "ok", "utf-8");
370
471
  rmSync(testFile);
371
- check("workspaceRoot writable", true, config.workspaceRoot);
472
+ results.push(checkResult("workspaceRoot writable", true, context.config.workspaceRoot));
372
473
  }
373
474
  catch {
374
- warnCheck("workspaceRoot writable", false, config.workspaceRoot);
475
+ results.push(warnCheckResult("workspaceRoot writable", false, context.config.workspaceRoot));
375
476
  }
376
- }
377
- if (config) {
378
- check("Watcher stale threshold is valid", config.watcherStaleSeconds >= 5 && config.watcherStaleSeconds <= 3600, `${config.watcherStaleSeconds}s`);
379
- }
380
- // allowedTestCommands has npm test
381
- if (config) {
382
- const hasNpmTest = config.allowedTestCommands.some((c) => c === "npm test" || c === "npm run test");
383
- warnCheck("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands");
384
- }
385
- if (config) {
386
- check("Task timeout defaults are valid", config.defaultTaskTimeoutSeconds > 0 && config.defaultTaskTimeoutSeconds <= config.maxTaskTimeoutSeconds, `default ${config.defaultTaskTimeoutSeconds}s, max ${config.maxTaskTimeoutSeconds}s`);
387
- }
388
- // 13. Agent command check
389
- if (config) {
390
- const agents = config.agents || {};
391
- for (const [name, agentCfg] of Object.entries(agents)) {
477
+ return results;
478
+ },
479
+ };
480
+ const checkWatcherStaleThreshold = {
481
+ id: "watcher-stale-threshold",
482
+ description: "Watcher stale threshold is valid",
483
+ run(context) {
484
+ if (!context.config)
485
+ return [];
486
+ return [
487
+ checkResult("Watcher stale threshold is valid", context.config.watcherStaleSeconds >= 5 && context.config.watcherStaleSeconds <= 3600, `${context.config.watcherStaleSeconds}s`),
488
+ ];
489
+ },
490
+ };
491
+ const checkAllowedTestCommandsIncludesNpmTest = {
492
+ id: "allowed-test-commands-npm-test",
493
+ description: "allowedTestCommands includes npm test",
494
+ run(context) {
495
+ if (!context.config)
496
+ return [];
497
+ const hasNpmTest = context.config.allowedTestCommands.some((c) => c === "npm test" || c === "npm run test");
498
+ return [warnCheckResult("allowedTestCommands includes npm test", hasNpmTest, hasNpmTest ? "present" : "npm test is missing — add it to allowedTestCommands")];
499
+ },
500
+ };
501
+ const checkTaskTimeoutDefaults = {
502
+ id: "task-timeout-defaults",
503
+ description: "Task timeout defaults are valid",
504
+ run(context) {
505
+ if (!context.config)
506
+ return [];
507
+ return [
508
+ checkResult("Task timeout defaults are valid", context.config.defaultTaskTimeoutSeconds > 0 && context.config.defaultTaskTimeoutSeconds <= context.config.maxTaskTimeoutSeconds, `default ${context.config.defaultTaskTimeoutSeconds}s, max ${context.config.maxTaskTimeoutSeconds}s`),
509
+ ];
510
+ },
511
+ };
512
+ const checkAgentCommands = {
513
+ id: "agent-commands",
514
+ description: "Agent command check",
515
+ run(context) {
516
+ if (!context.config)
517
+ return [];
518
+ const results = [];
519
+ for (const [name, agentCfg] of Object.entries(context.config.agents)) {
392
520
  const cmdName = agentCfg.command;
393
521
  const looksLikePath = isAbsolute(cmdName) || cmdName.includes("/") || cmdName.includes("\\");
394
522
  if (looksLikePath) {
395
523
  const agentExists = existsSync(cmdName);
396
- warnCheck(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`);
524
+ results.push(warnCheckResult(`Agent "${name}" command available`, agentExists, agentExists ? `Found: ${cmdName}` : `"${cmdName}" does not exist — agent tasks will fail`));
397
525
  continue;
398
526
  }
399
- // Platform-appropriate lookup: 'where' on Windows, 'command -v' on Unix
527
+ // Use an argumentized lookup utility; never interpolate config into a shell.
400
528
  const isWin = process.platform === "win32";
401
- const lookupCmd = isWin ? `where ${cmdName}` : `command -v ${cmdName}`;
402
- const fallbackCmd = isWin ? `command -v ${cmdName}` : `which ${cmdName}`;
403
- const found = cmd(lookupCmd) || cmd(fallbackCmd);
404
- warnCheck(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`);
529
+ let found = "";
530
+ if (isWin) {
531
+ try {
532
+ found = resolveAgentLaunch(name, cmdName, process.platform, process.env.PATH || "", existsSync, context.config.workspaceRoot, typeof agentCfg.adapter === "string" ? agentCfg.adapter : name).command;
533
+ }
534
+ catch {
535
+ found = "";
536
+ }
537
+ }
538
+ else {
539
+ found = cmd("which", [cmdName]);
540
+ }
541
+ results.push(warnCheckResult(`Agent "${name}" command available`, found !== "", found ? `Found: ${found.split("\n")[0]}` : `"${cmdName}" not in PATH — agent tasks will fail`));
405
542
  }
406
- }
407
- // 14. allowedTestCommands safety check
408
- if (config) {
409
- const testCmds = config.allowedTestCommands || [];
410
- check("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured");
543
+ return results;
544
+ },
545
+ };
546
+ const checkAllowedTestCommandsSafety = {
547
+ id: "allowed-test-commands-safety",
548
+ description: "allowedTestCommands safety check",
549
+ run(context) {
550
+ if (!context.config)
551
+ return [];
552
+ const results = [];
553
+ const testCmds = context.config.allowedTestCommands || [];
554
+ results.push(checkResult("allowedTestCommands is non-empty", testCmds.length > 0, testCmds.length > 0 ? `${testCmds.length} commands` : "No test commands configured"));
411
555
  const dangerous = ["rm -rf", "del /s", "format", "shutdown", "curl |", "wget |"];
412
556
  for (const cmdStr of testCmds) {
413
557
  for (const danger of dangerous) {
414
558
  if (cmdStr.toLowerCase().includes(danger)) {
415
- results.push(`[WARN] allowedTestCommands contains dangerous pattern: "${cmdStr}"`);
416
- warn++;
559
+ results.push(warnResult(`allowedTestCommands contains dangerous pattern: "${cmdStr}"`));
417
560
  }
418
561
  }
419
562
  }
420
- }
421
- // 14b. Direct profile config checks
422
- if (config) {
423
- const directCmds = config.directAllowedCommands || [];
424
- warnCheck("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured");
563
+ return results;
564
+ },
565
+ };
566
+ // Direct profile config checks
567
+ const checkDirectProfileConfig = {
568
+ id: "direct-profile-config",
569
+ description: "Direct profile config checks",
570
+ run(context) {
571
+ if (!context.config)
572
+ return [];
573
+ const results = [];
574
+ const directCmds = context.config.directAllowedCommands || [];
575
+ results.push(warnCheckResult("directAllowedCommands is non-empty", directCmds.length > 0, directCmds.length > 0 ? `${directCmds.length} commands` : "No Direct commands configured"));
425
576
  // npm run doctor should NOT be in default Direct whitelist
426
577
  const hasDoctor = directCmds.some((c) => c === "npm run doctor");
427
- check("directAllowedCommands does not include npm run doctor", !hasDoctor, hasDoctor ? "npm run doctor found in Direct whitelist — remove it for tighter security" : "not present");
428
- check("directSessionTtlSeconds is valid", config.directSessionTtlSeconds >= 60 && config.directSessionTtlSeconds <= 86400, `${config.directSessionTtlSeconds}s`);
429
- check("directMaxPatchBytes is positive", config.directMaxPatchBytes > 0, `${config.directMaxPatchBytes}`);
430
- check("directMaxFileBytes is positive", config.directMaxFileBytes > 0, `${config.directMaxFileBytes}`);
431
- }
432
- // 15. Tunnel example files check
433
- const tunnelFiles = [
434
- "examples/openai-tunnel/README.md",
435
- "examples/openai-tunnel/tunnel-client.example.yaml",
436
- "examples/openai-tunnel/chatgpt-test-prompt.md",
437
- "scripts/mcp/patchwarden-mcp-direct.cmd",
438
- "PatchWarden.cmd",
439
- "scripts/control/manage-patchwarden.ps1",
440
- "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
441
- ];
442
- for (const tf of tunnelFiles) {
443
- const full = resolve(process.cwd(), tf);
444
- const exists = existsSync(full);
445
- check(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing");
446
- // Check for leaked secrets in example files
447
- if (exists) {
448
- const content = readFileSync(full, "utf-8");
449
- // Only flag actual key-value assignments, not comments/mentions
450
- 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(
451
- // Strip comment lines first
452
- content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"));
453
- if (leaked) {
454
- results.push(`[WARN] ${tf} may contain secrets`);
455
- warn++;
456
- }
457
- else {
458
- results.push(`[OK] ${tf} no real secrets`);
459
- ok++;
578
+ 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"));
579
+ results.push(checkResult("directSessionTtlSeconds is valid", context.config.directSessionTtlSeconds >= 60 && context.config.directSessionTtlSeconds <= 86400, `${context.config.directSessionTtlSeconds}s`));
580
+ results.push(checkResult("directMaxPatchBytes is positive", context.config.directMaxPatchBytes > 0, `${context.config.directMaxPatchBytes}`));
581
+ results.push(checkResult("directMaxFileBytes is positive", context.config.directMaxFileBytes > 0, `${context.config.directMaxFileBytes}`));
582
+ return results;
583
+ },
584
+ };
585
+ // Tunnel example files check
586
+ const checkTunnelExamples = {
587
+ id: "tunnel-examples",
588
+ description: "Tunnel example files check",
589
+ run() {
590
+ const tunnelFiles = [
591
+ "examples/openai-tunnel/README.md",
592
+ "examples/openai-tunnel/tunnel-client.example.yaml",
593
+ "examples/openai-tunnel/chatgpt-test-prompt.md",
594
+ "scripts/mcp/patchwarden-mcp-direct.cmd",
595
+ "PatchWarden.cmd",
596
+ "scripts/control/manage-patchwarden.ps1",
597
+ "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
598
+ ];
599
+ const results = [];
600
+ for (const tf of tunnelFiles) {
601
+ const full = resolve(process.cwd(), tf);
602
+ const exists = existsSync(full);
603
+ results.push(checkResult(`Tunnel example: ${tf}`, exists, exists ? "present" : "missing"));
604
+ // Check for leaked secrets in example files
605
+ if (exists) {
606
+ const content = readFileSync(full, "utf-8");
607
+ // Only flag actual key-value assignments, not comments/mentions
608
+ 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(
609
+ // Strip comment lines first
610
+ content.split("\n").filter(l => !l.trim().startsWith("#") && !l.trim().startsWith("//")).join("\n"));
611
+ if (leaked) {
612
+ results.push(warnResult(`${tf} may contain secrets`));
613
+ }
614
+ else {
615
+ results.push(okResult(`${tf} — no real secrets`));
616
+ }
460
617
  }
461
618
  }
619
+ return results;
620
+ },
621
+ };
622
+ // ── Check registry ─────────────────────────────────────────────────
623
+ const checks = [
624
+ checkNodeVersion,
625
+ checkNpmAvailable,
626
+ checkConfirmPackage,
627
+ checkGitAvailable,
628
+ checkConfigFile,
629
+ checkPatchwardenConfigEnv,
630
+ checkConfigParseable,
631
+ checkWorkspaceRoot,
632
+ checkPathGuard,
633
+ checkSensitiveGuard,
634
+ checkSavePlanRules,
635
+ checkTaskArtifactAllowlist,
636
+ checkServerVersion,
637
+ checkToolProfiles,
638
+ checkReleaseGate,
639
+ checkHttpPort,
640
+ checkDistFiles,
641
+ checkToolModules,
642
+ checkDirectToolModules,
643
+ checkDirectSupportModules,
644
+ checkTaskDirectoryWritable,
645
+ checkWatcherStaleThreshold,
646
+ checkAllowedTestCommandsIncludesNpmTest,
647
+ checkTaskTimeoutDefaults,
648
+ checkAgentCommands,
649
+ checkAllowedTestCommandsSafety,
650
+ checkDirectProfileConfig,
651
+ checkTunnelExamples,
652
+ ];
653
+ // ── Context preparation ────────────────────────────────────────────
654
+ function prepareContext() {
655
+ const allowDefaultConfig = process.argv.includes("--allow-default-config");
656
+ const configPaths = [
657
+ resolve(process.cwd(), "patchwarden.config.json"),
658
+ process.env.PATCHWARDEN_CONFIG || "",
659
+ ].filter(Boolean);
660
+ let configPathUsed = "";
661
+ for (const p of configPaths) {
662
+ if (existsSync(p)) {
663
+ configPathUsed = p;
664
+ break;
665
+ }
666
+ }
667
+ // Load config (may fail)
668
+ let config = null;
669
+ let configError = null;
670
+ try {
671
+ config = getConfig();
672
+ }
673
+ catch (err) {
674
+ configError = err instanceof Error ? err.message : String(err);
675
+ }
676
+ return { config, configError, allowDefaultConfig, configPathUsed };
677
+ }
678
+ // ══════════════════════════════════════════════════════════════════
679
+ // Main
680
+ // ══════════════════════════════════════════════════════════════════
681
+ async function main() {
682
+ console.log("PatchWarden Doctor\n");
683
+ const context = prepareContext();
684
+ const allResults = [];
685
+ for (const check of checks) {
686
+ try {
687
+ const results = await check.run(context);
688
+ allResults.push(...results);
689
+ }
690
+ catch (err) {
691
+ allResults.push(failResult(check.description, err instanceof Error ? err.message : String(err)));
692
+ }
693
+ }
694
+ console.log(allResults.map(formatResult).join("\n"));
695
+ let ok = 0;
696
+ let warn = 0;
697
+ let fail = 0;
698
+ for (const r of allResults) {
699
+ if (r.level === "OK")
700
+ ok++;
701
+ else if (r.level === "WARN")
702
+ warn++;
703
+ else
704
+ fail++;
462
705
  }
463
- // ══════════════════════════════════════════════════════════════════
464
- // Summary
465
- // ══════════════════════════════════════════════════════════════════
466
- console.log(results.join("\n"));
467
706
  console.log(`\n${"=".repeat(50)}`);
468
707
  console.log(`OK: ${ok} WARN: ${warn} FAIL: ${fail}`);
469
708
  console.log(`${"=".repeat(50)}`);
@@ -480,8 +719,10 @@ async function main() {
480
719
  console.log("\n✅ All checks passed.");
481
720
  process.exit(0);
482
721
  }
483
- } // end async main
722
+ }
484
723
  main().catch((err) => {
485
- console.error("Doctor crashed:", err);
724
+ logger.fatal("Doctor crashed", {
725
+ error: err instanceof Error ? err.message : String(err),
726
+ });
486
727
  process.exit(1);
487
728
  });