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
@@ -0,0 +1,508 @@
1
+ /**
2
+ * PatchWarden Control Center — runtime state readers and domain helpers.
3
+ *
4
+ * Provides health probing, runtime file readers (tunnel status, tool manifest,
5
+ * watcher status), stale-task classification, the control-center status/event
6
+ * timeline, hidden-id UI state, and small shared helpers (verdict parsing,
7
+ * file mtime, id validation). These are pure helpers consumed by the route
8
+ * modules; they hold no HTTP-layer concerns beyond reusing `sendJson`.
9
+ */
10
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { get as httpGet } from "node:http";
13
+ import { listTasks, type TaskEntry } from "../tools/tasks/listTasks.js";
14
+ import { listAgents, type AgentAvailability } from "../tools/workspace/listAgents.js";
15
+ import { readWatcherStatus, type WatcherStatusSnapshot } from "../watcherStatus.js";
16
+ import { resolveWorkspaceRoot } from "../config.js";
17
+ import { PATCHWARDEN_VERSION } from "../version.js";
18
+ import { isValidDirectSessionId as isValidStoredDirectSessionId } from "../direct/directSessionStore.js";
19
+ import { atomicWriteFileSync, atomicWriteJsonFileSync } from "../utils/atomicFile.js";
20
+ import { withFileLockSync } from "../utils/lockedJsonFile.js";
21
+ import { logger } from "../logging.js";
22
+ import {
23
+ config,
24
+ controlCenterEventsPath,
25
+ controlCenterStatusPath,
26
+ CORE_BASE_URL,
27
+ DIRECT_BASE_URL,
28
+ errorMessage,
29
+ getControlCenterLogDir,
30
+ getRuntimeRoot,
31
+ host,
32
+ MAX_EVENT_LINES,
33
+ port,
34
+ readJsonFileSafe,
35
+ } from "./shared.js";
36
+
37
+ // ── Health probing ────────────────────────────────────────────────
38
+
39
+ export interface HealthProbe {
40
+ available: boolean;
41
+ status: number | null;
42
+ reason: string | null;
43
+ }
44
+
45
+ export function probeHealthStatus(targetUrl: string): Promise<HealthProbe> {
46
+ return new Promise((resolve) => {
47
+ const controller = new AbortController();
48
+ let settled = false;
49
+ const finish = (result: HealthProbe) => {
50
+ if (settled) return;
51
+ settled = true;
52
+ clearTimeout(timer);
53
+ resolve(result);
54
+ };
55
+ const timer = setTimeout(() => {
56
+ try { controller.abort(); } catch { /* ignore */ }
57
+ finish({ available: false, status: null, reason: "timeout after 2000ms" });
58
+ }, 2000);
59
+ try {
60
+ const req = httpGet(targetUrl, { signal: controller.signal }, (resp) => {
61
+ resp.resume();
62
+ const status = resp.statusCode ?? 0;
63
+ // Only 2xx counts as available. A 404 from an unrelated service (or a
64
+ // 4xx/5xx from the real service) must NOT be treated as healthy.
65
+ if (status >= 200 && status < 300) {
66
+ finish({ available: true, status, reason: null });
67
+ } else {
68
+ finish({ available: false, status, reason: `unexpected status ${status}` });
69
+ }
70
+ });
71
+ req.on("error", (err) => {
72
+ finish({ available: false, status: null, reason: err.message });
73
+ });
74
+ } catch (err) {
75
+ finish({ available: false, status: null, reason: errorMessage(err) });
76
+ }
77
+ });
78
+ }
79
+
80
+ export interface RuntimeHealth {
81
+ available: boolean;
82
+ reason: string | null;
83
+ healthz: { status: number } | null;
84
+ readyz: { status: number } | null;
85
+ }
86
+
87
+ export async function probeRuntimeHealth(baseUrl: string): Promise<RuntimeHealth> {
88
+ const [h, r] = await Promise.all([
89
+ probeHealthStatus(`${baseUrl}/healthz`),
90
+ probeHealthStatus(`${baseUrl}/readyz`),
91
+ ]);
92
+ if (h.available && r.available && h.status !== null && r.status !== null) {
93
+ return { available: true, reason: null, healthz: { status: h.status }, readyz: { status: r.status } };
94
+ }
95
+ const failed = !h.available ? h : r;
96
+ return { available: false, reason: failed.reason ?? "unavailable", healthz: null, readyz: null };
97
+ }
98
+
99
+ // ── Runtime file readers ──────────────────────────────────────────
100
+
101
+ export function readTunnelStatus(direct: boolean): Record<string, unknown> {
102
+ const filePath = join(getRuntimeRoot(direct), "tunnel-status.json");
103
+ if (!existsSync(filePath)) return { observed: false };
104
+ try {
105
+ const data = readJsonFileSafe<Record<string, unknown>>(filePath);
106
+ if (data === null) return { observed: true, error: "invalid JSON" };
107
+ return { observed: true, ...data };
108
+ } catch (err) {
109
+ return { observed: true, error: errorMessage(err) };
110
+ }
111
+ }
112
+
113
+ export interface ToolManifestSummary {
114
+ tool_profile: string | null;
115
+ tool_count: number | null;
116
+ schema_epoch: string | null;
117
+ tool_manifest_sha256: string | null;
118
+ tool_names: string[] | null;
119
+ }
120
+
121
+ export function readToolManifest(direct: boolean): ToolManifestSummary {
122
+ const empty: ToolManifestSummary = {
123
+ tool_profile: null,
124
+ tool_count: null,
125
+ schema_epoch: null,
126
+ tool_manifest_sha256: null,
127
+ tool_names: null,
128
+ };
129
+ const filePath = join(getRuntimeRoot(direct), "tool-manifest.json");
130
+ if (!existsSync(filePath)) return empty;
131
+ const data = readJsonFileSafe<Record<string, unknown>>(filePath);
132
+ if (!data) return empty;
133
+ return {
134
+ tool_profile: typeof data.tool_profile === "string" ? data.tool_profile : null,
135
+ tool_count: typeof data.tool_count === "number" ? data.tool_count : null,
136
+ schema_epoch: typeof data.schema_epoch === "string" ? data.schema_epoch : null,
137
+ tool_manifest_sha256: typeof data.tool_manifest_sha256 === "string" ? data.tool_manifest_sha256 : null,
138
+ tool_names: Array.isArray(data.tool_names) ? (data.tool_names as string[]) : null,
139
+ };
140
+ }
141
+
142
+ export function readTunnelUrl(direct: boolean): { url: string | null; reason: string | null } {
143
+ const filePath = join(getRuntimeRoot(direct), "tunnel-health-url.txt");
144
+ if (!existsSync(filePath)) return { url: null, reason: "tunnel-health-url.txt not found" };
145
+ try {
146
+ const content = readFileSync(filePath, "utf-8").trim();
147
+ if (!content) return { url: null, reason: "tunnel-health-url.txt is empty" };
148
+ return { url: content, reason: null };
149
+ } catch (err) {
150
+ return { url: null, reason: errorMessage(err) };
151
+ }
152
+ }
153
+
154
+ // ── Safe wrappers around reusable modules ─────────────────────────
155
+
156
+ export function readWatcherStatusSafe(): WatcherStatusSnapshot {
157
+ try {
158
+ return readWatcherStatus(config);
159
+ } catch (err) {
160
+ return {
161
+ status: "unreadable",
162
+ available: false,
163
+ stale_after_seconds: config.watcherStaleSeconds,
164
+ last_heartbeat_at: null,
165
+ heartbeat_age_seconds: null,
166
+ heartbeat_pid: null,
167
+ instance_id: null,
168
+ launcher_pid: null,
169
+ reason: errorMessage(err),
170
+ activity: null,
171
+ };
172
+ }
173
+ }
174
+
175
+ export function listAgentsSafe(): AgentAvailability[] {
176
+ try {
177
+ return listAgents().agents;
178
+ } catch {
179
+ return [];
180
+ }
181
+ }
182
+
183
+ export function resolveWorkspaceRootSafe(): string | null {
184
+ try {
185
+ return resolveWorkspaceRoot(config);
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ // ── Stale task classification ─────────────────────────────────────
192
+
193
+ export interface StatusTasks {
194
+ tasks: unknown[];
195
+ total: number;
196
+ active: number;
197
+ stale: number;
198
+ stale_task_ids: string[];
199
+ reason: string | null;
200
+ }
201
+
202
+ export interface StaleClassification {
203
+ is_stale: boolean;
204
+ stale_reasons: string[];
205
+ }
206
+
207
+ export const TERMINAL_TASK_STATUSES = new Set([
208
+ "done",
209
+ "done_by_agent",
210
+ "failed",
211
+ "failed_verification",
212
+ "failed_scope_violation",
213
+ "failed_policy_violation",
214
+ "canceled",
215
+ "timeout",
216
+ ]);
217
+
218
+ /**
219
+ * Classify a task as stale based on Phase 2 rules:
220
+ * - status=running but last_heartbeat_at exceeds threshold
221
+ * - phase=collecting_artifacts exceeds threshold
222
+ * - current_command=null AND watcher currently healthy
223
+ * - task last_heartbeat_at significantly earlier than current watcher heartbeat
224
+ *
225
+ * Only pending/running tasks can be stale; terminal tasks are never stale.
226
+ */
227
+ export function classifyStaleTask(
228
+ task: TaskEntry,
229
+ watcher: WatcherStatusSnapshot,
230
+ nowMs = Date.now()
231
+ ): StaleClassification {
232
+ const reasons: string[] = [];
233
+ if (TERMINAL_TASK_STATUSES.has(task.status)) {
234
+ return { is_stale: false, stale_reasons: reasons };
235
+ }
236
+ // Only pending/running are candidates for staleness.
237
+ if (task.status !== "pending" && task.status !== "running") {
238
+ return { is_stale: false, stale_reasons: reasons };
239
+ }
240
+
241
+ const staleThresholdMs = config.watcherStaleSeconds * 1000;
242
+ const hbMs = Date.parse(task.last_heartbeat_at || "");
243
+ const heartbeatAgeMs = Number.isFinite(hbMs) ? Math.max(0, nowMs - hbMs) : null;
244
+
245
+ // Rule 1: running with stale heartbeat
246
+ if (task.status === "running" && heartbeatAgeMs !== null && heartbeatAgeMs > staleThresholdMs) {
247
+ reasons.push("heartbeat_stale");
248
+ }
249
+
250
+ // Rule 2: collecting_artifacts phase exceeds threshold
251
+ if (task.phase === "collecting_artifacts" && heartbeatAgeMs !== null && heartbeatAgeMs > staleThresholdMs) {
252
+ reasons.push("collecting_artifacts_stale");
253
+ }
254
+
255
+ // Rule 3: running with no current_command while watcher is healthy
256
+ if (
257
+ task.status === "running" &&
258
+ (task.current_command === null || task.current_command === "") &&
259
+ watcher.status === "healthy"
260
+ ) {
261
+ reasons.push("running_no_command_watcher_healthy");
262
+ }
263
+
264
+ // Rule 4: task heartbeat significantly earlier than watcher heartbeat
265
+ if (heartbeatAgeMs !== null && watcher.last_heartbeat_at) {
266
+ const watcherHbMs = Date.parse(watcher.last_heartbeat_at);
267
+ if (Number.isFinite(watcherHbMs)) {
268
+ const gapMs = watcherHbMs - hbMs;
269
+ // Task heartbeat is "significantly earlier" than watcher heartbeat when
270
+ // the task has not heartbeat for at least 2x the stale threshold while
271
+ // the watcher is alive.
272
+ if (gapMs > staleThresholdMs * 2 && watcher.status === "healthy") {
273
+ reasons.push("heartbeat_far_behind_watcher");
274
+ }
275
+ }
276
+ }
277
+
278
+ return { is_stale: reasons.length > 0, stale_reasons: reasons };
279
+ }
280
+
281
+ export function augmentTaskWithStale(task: TaskEntry, watcher: WatcherStatusSnapshot, nowMs = Date.now()): TaskEntry & StaleClassification {
282
+ const cls = classifyStaleTask(task, watcher, nowMs);
283
+ return { ...task, is_stale: cls.is_stale, stale_reasons: cls.stale_reasons };
284
+ }
285
+
286
+ export function listTasksForStatus(): StatusTasks {
287
+ try {
288
+ const result = listTasks({ limit: 100 });
289
+ const watcher = result.watcher;
290
+ const now = Date.now();
291
+ let active = 0;
292
+ let stale = 0;
293
+ const staleTaskIds: string[] = [];
294
+ const augmented = result.tasks.map((t) => {
295
+ const a = augmentTaskWithStale(t, watcher, now);
296
+ if (t.status === "pending" || t.status === "running") active++;
297
+ if (a.is_stale) {
298
+ stale++;
299
+ staleTaskIds.push(t.task_id);
300
+ }
301
+ return a;
302
+ });
303
+ return { tasks: augmented, total: result.total, active, stale, stale_task_ids: staleTaskIds, reason: null };
304
+ } catch (err) {
305
+ return { tasks: [], total: 0, active: 0, stale: 0, stale_task_ids: [], reason: errorMessage(err) };
306
+ }
307
+ }
308
+
309
+ // ── Control Center status file + activity timeline ────────────────
310
+
311
+ export interface ControlCenterStatusFile {
312
+ pid: number;
313
+ port: number;
314
+ started_at: string;
315
+ url: string;
316
+ version: string;
317
+ }
318
+
319
+ export function writeStatusFile(): void {
320
+ const status: ControlCenterStatusFile = {
321
+ pid: process.pid,
322
+ port,
323
+ started_at: new Date().toISOString(),
324
+ url: `http://${host}:${port}/`,
325
+ version: PATCHWARDEN_VERSION,
326
+ };
327
+ try {
328
+ mkdirSync(getControlCenterLogDir(), { recursive: true });
329
+ atomicWriteJsonFileSync(controlCenterStatusPath, status);
330
+ } catch (err) {
331
+ logger.error("[control-center] Failed to write status file", { error: errorMessage(err) });
332
+ }
333
+ }
334
+
335
+ export function removeStatusFile(): void {
336
+ try {
337
+ if (existsSync(controlCenterStatusPath)) {
338
+ unlinkSync(controlCenterStatusPath);
339
+ }
340
+ } catch (err) {
341
+ logger.error("[control-center] Failed to remove status file", { error: errorMessage(err) });
342
+ }
343
+ }
344
+
345
+ export interface ControlCenterEvent {
346
+ timestamp: string;
347
+ type: string;
348
+ payload?: Record<string, unknown>;
349
+ }
350
+
351
+ /**
352
+ * Append a single event line to the JSONL timeline. Best-effort: a write
353
+ * failure is logged but never crashes the server. Trims the file to
354
+ * MAX_EVENT_LINES lazily when it grows past 1.5x the cap, so we don't pay the
355
+ * trim cost on every event.
356
+ */
357
+ export function recordEvent(type: string, payload?: Record<string, unknown>): void {
358
+ const event: ControlCenterEvent = {
359
+ timestamp: new Date().toISOString(),
360
+ type,
361
+ payload,
362
+ };
363
+ try {
364
+ mkdirSync(getControlCenterLogDir(), { recursive: true });
365
+ withFileLockSync(controlCenterEventsPath, () => {
366
+ appendFileSync(controlCenterEventsPath, JSON.stringify(event) + "\n", "utf-8");
367
+ const stat = statSync(controlCenterEventsPath);
368
+ if (stat.size > 512 * 1024) trimEventsFileUnlocked();
369
+ });
370
+ } catch (err) {
371
+ logger.error("[control-center] Failed to write event", { error: errorMessage(err) });
372
+ }
373
+ }
374
+
375
+ function trimEventsFileUnlocked(): void {
376
+ try {
377
+ if (!existsSync(controlCenterEventsPath)) return;
378
+ const raw = readFileSync(controlCenterEventsPath, "utf-8");
379
+ const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
380
+ if (lines.length <= MAX_EVENT_LINES) return;
381
+ const trimmed = lines.slice(lines.length - MAX_EVENT_LINES);
382
+ atomicWriteFileSync(controlCenterEventsPath, trimmed.join("\n") + "\n");
383
+ } catch (err) {
384
+ logger.error("[control-center] Failed to trim events file", { error: errorMessage(err) });
385
+ }
386
+ }
387
+
388
+ export function readEvents(limit: number): ControlCenterEvent[] {
389
+ if (!existsSync(controlCenterEventsPath)) return [];
390
+ try {
391
+ const raw = readFileSync(controlCenterEventsPath, "utf-8");
392
+ const lines = raw.split(/\r?\n/).filter((l) => l.length > 0);
393
+ const sliced = lines.slice(Math.max(0, lines.length - limit));
394
+ const events: ControlCenterEvent[] = [];
395
+ for (const line of sliced) {
396
+ try {
397
+ events.push(JSON.parse(line) as ControlCenterEvent);
398
+ } catch {
399
+ /* skip malformed line */
400
+ }
401
+ }
402
+ return events;
403
+ } catch {
404
+ return [];
405
+ }
406
+ }
407
+
408
+ // ── Hidden stale task IDs (local UI state) ────────────────────────
409
+
410
+ export function readHiddenStaleIds(): string[] {
411
+ const p = join(getControlCenterLogDir(), "hidden-stale-ids.json");
412
+ if (!existsSync(p)) return [];
413
+ return readStoredStringArray(p);
414
+ }
415
+
416
+ export function reconstructTaskEntry(
417
+ taskId: string,
418
+ statusData: Record<string, unknown>,
419
+ runtimeData: Record<string, unknown> = {},
420
+ watcher: WatcherStatusSnapshot = readWatcherStatusSafe(),
421
+ ): TaskEntry {
422
+ const validAcceptance = new Set(["pending", "accepted", "rejected", "needs_fix", "blocked"]);
423
+ const taskStatus = String(statusData.status || "pending");
424
+ const acceptanceStatus = taskStatus === "done_by_agent"
425
+ ? (typeof statusData.acceptance_status === "string" && validAcceptance.has(statusData.acceptance_status)
426
+ ? statusData.acceptance_status
427
+ : "pending")
428
+ : null;
429
+ return {
430
+ task_id: taskId,
431
+ plan_id: String(statusData.plan_id || ""),
432
+ title: "",
433
+ agent: String(statusData.agent || ""),
434
+ status: taskStatus as TaskEntry["status"],
435
+ phase: String(runtimeData.phase || statusData.phase || "queued") as TaskEntry["phase"],
436
+ acceptance_status: acceptanceStatus as TaskEntry["acceptance_status"],
437
+ created_at: String(statusData.created_at || ""),
438
+ updated_at: String(statusData.updated_at || ""),
439
+ workspace_root: String(statusData.workspace_root || config.workspaceRoot),
440
+ repo_path: String(statusData.repo_path || "."),
441
+ resolved_repo_path: String(statusData.resolved_repo_path || statusData.repo_path || config.workspaceRoot),
442
+ test_command: String(statusData.test_command || ""),
443
+ verify_commands: Array.isArray(statusData.verify_commands)
444
+ ? statusData.verify_commands.filter((command): command is string => typeof command === "string")
445
+ : [],
446
+ error: statusData.error ? String(statusData.error) : null,
447
+ last_heartbeat_at: String(runtimeData.last_heartbeat_at || statusData.last_heartbeat_at || statusData.updated_at || ""),
448
+ current_command: runtimeData.current_command === undefined ? null : String(runtimeData.current_command || "") || null,
449
+ timeout_seconds: Number(statusData.timeout_seconds) || config.defaultTaskTimeoutSeconds,
450
+ pending_reason: null,
451
+ watcher_status: watcher.status,
452
+ };
453
+ }
454
+
455
+ export function writeHiddenStaleIds(ids: string[]): void {
456
+ const dir = getControlCenterLogDir();
457
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
458
+ atomicWriteJsonFileSync(join(dir, "hidden-stale-ids.json"), normalizeStoredIds(ids));
459
+ }
460
+
461
+ // ── Hidden direct session IDs (local UI state) ────────────────────
462
+
463
+ export function readHiddenDirectSessionIds(): string[] {
464
+ const p = join(getControlCenterLogDir(), "hidden-direct-session-ids.json");
465
+ if (!existsSync(p)) return [];
466
+ return readStoredStringArray(p);
467
+ }
468
+
469
+ export function writeHiddenDirectSessionIds(ids: string[]): void {
470
+ const dir = getControlCenterLogDir();
471
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
472
+ atomicWriteJsonFileSync(join(dir, "hidden-direct-session-ids.json"), normalizeStoredIds(ids));
473
+ }
474
+
475
+ export function isValidDirectSessionId(sessionId: string): boolean {
476
+ return isValidStoredDirectSessionId(sessionId);
477
+ }
478
+
479
+ export function isValidTaskId(taskId: string): boolean {
480
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,159}$/.test(taskId);
481
+ }
482
+
483
+ function readStoredStringArray(path: string): string[] {
484
+ const value = readJsonFileSafe<unknown>(path);
485
+ if (!Array.isArray(value)) return [];
486
+ return normalizeStoredIds(value);
487
+ }
488
+
489
+ function normalizeStoredIds(value: unknown[]): string[] {
490
+ return [...new Set(value.filter((entry): entry is string => typeof entry === "string" && entry.length > 0))];
491
+ }
492
+
493
+ // ── Small shared helpers ──────────────────────────────────────────
494
+
495
+ export function parseReviewVerdict(content: string): string | null {
496
+ // independent-review.md format: "**Verdict**: PASS" (case-insensitive)
497
+ const m = content.match(/\*\*Verdict\*\*\s*:\s*([A-Za-z]+)/);
498
+ return m ? m[1].toLowerCase() : null;
499
+ }
500
+
501
+ export function fileMtimeIso(filePath: string): string | null {
502
+ try {
503
+ const m = statSync(filePath).mtime;
504
+ return m ? m.toISOString() : null;
505
+ } catch {
506
+ return null;
507
+ }
508
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * PatchWarden Control Center — HTTP server creation, routing, and lifecycle.
3
+ *
4
+ * This module wires together the route handlers (routes/*.ts), middleware
5
+ * (auth/static), and the shared/runtime helpers. It owns the `handleRequest`
6
+ * router that dispatches every GET/POST route, the POST control-token gate,
7
+ * and the server bootstrap/shutdown lifecycle (status file, activity events,
8
+ * SIGINT/SIGTERM).
9
+ *
10
+ * Entry point: `controlCenter.ts` imports `startServer()` from here.
11
+ */
12
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
13
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../version.js";
14
+ import { logger } from "../logging.js";
15
+ import {
16
+ config,
17
+ controlToken,
18
+ errorMessage,
19
+ host,
20
+ PAGE_ALIASES,
21
+ port,
22
+ readBody,
23
+ sendJson,
24
+ } from "./shared.js";
25
+ import { recordEvent, removeStatusFile, writeStatusFile } from "./runtime.js";
26
+ import { checkControlToken, isTrustedControlHost } from "./middleware/auth.js";
27
+ import { serveFavicon, serveStatic } from "./middleware/static.js";
28
+ import { buildRoutes } from "./routeTable.js";
29
+
30
+ // ── Request router ────────────────────────────────────────────────
31
+
32
+ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
33
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
34
+ res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
35
+ res.setHeader("Referrer-Policy", "no-referrer");
36
+ res.setHeader("X-Content-Type-Options", "nosniff");
37
+ res.setHeader("X-Frame-Options", "DENY");
38
+
39
+ if (!isTrustedControlHost(req, port)) {
40
+ sendJson(res, 421, { error: "Untrusted Host header" });
41
+ return;
42
+ }
43
+
44
+ const parsedUrl = new URL(req.url || "/", `http://${host}:${port}`);
45
+ const pathname = parsedUrl.pathname;
46
+ const method = (req.method || "GET").toUpperCase();
47
+
48
+ // ── Static resource priority branches (not in routeTable.ts) ──
49
+ if (method === "GET" && pathname === "/") {
50
+ serveStatic(res, "pages/dashboard.html");
51
+ return;
52
+ }
53
+ const pageAlias = PAGE_ALIASES[pathname];
54
+ if (method === "GET" && pageAlias) {
55
+ serveStatic(res, pageAlias);
56
+ return;
57
+ }
58
+ if (method === "GET" && pathname === "/control-token.json") {
59
+ sendJson(res, 200, { token: controlToken });
60
+ return;
61
+ }
62
+ if (method === "GET" && pathname === "/favicon.ico") {
63
+ serveFavicon(res);
64
+ return;
65
+ }
66
+ if (
67
+ method === "GET" &&
68
+ (pathname === "/colors_and_type.css" ||
69
+ pathname === "/desktop.css" ||
70
+ pathname === "/desktop-bridge.js" ||
71
+ pathname === "/i18n.js" ||
72
+ pathname === "/getting-started.js" ||
73
+ pathname === "/settings.js" ||
74
+ pathname.startsWith("/pages/") ||
75
+ pathname.startsWith("/partials/") ||
76
+ pathname.startsWith("/vendor/"))
77
+ ) {
78
+ serveStatic(res, pathname);
79
+ return;
80
+ }
81
+
82
+ // ── POST routes: drain body + verify control token before dispatch ──
83
+ // All POST routes in the table declare `requiresToken: true`; the body is
84
+ // drained up-front so HTTP keep-alive connections stay clean even when the
85
+ // route is unknown (preserving the original handleRequest behavior).
86
+ if (method === "POST") {
87
+ await readBody(req);
88
+ if (!checkControlToken(req)) {
89
+ sendJson(res, 403, { error: "Missing or invalid control token" });
90
+ return;
91
+ }
92
+ }
93
+
94
+ // ── Declarative route table dispatch ──
95
+ // Filter by method → match pattern (in declaration order, so specific
96
+ // patterns win over generic `:id`) → call handler with capture-group slice.
97
+ const routes = buildRoutes(parsedUrl);
98
+ for (const route of routes) {
99
+ if (route.method !== method) continue;
100
+ const match = pathname.match(route.pattern);
101
+ if (!match) continue;
102
+ await route.handler(res, match.slice(1));
103
+ return;
104
+ }
105
+
106
+ sendJson(res, 404, { error: "Not found" });
107
+ }
108
+
109
+ // ── Server bootstrap ──────────────────────────────────────────────
110
+
111
+ let server: Server | null = null;
112
+
113
+ export function startServer(): Server {
114
+ server = createServer((req, res) => {
115
+ handleRequest(req, res).catch((err) => {
116
+ if (!res.headersSent) {
117
+ sendJson(res, 500, { error: errorMessage(err) });
118
+ } else {
119
+ try { res.end(); } catch { /* ignore */ }
120
+ }
121
+ });
122
+ });
123
+
124
+ server.on("error", (err) => {
125
+ logger.fatal("[control-center] Server error", { error: errorMessage(err) });
126
+ process.exit(1);
127
+ });
128
+
129
+ server.listen(port, host, () => {
130
+ const addr = server!.address();
131
+ const formatted = addr && typeof addr === "object" ? `http://${addr.address}:${addr.port}/` : `http://${host}:${port}/`;
132
+ logger.info(`[control-center] PatchWarden v${PATCHWARDEN_VERSION} (schema epoch ${TOOL_SCHEMA_EPOCH})`);
133
+ logger.info(`[control-center] Workspace: ${config.workspaceRoot}`);
134
+ logger.info(`[control-center] Listening: ${formatted}`);
135
+ logger.info(`[control-center] Bound to 127.0.0.1 only — not exposed to network`);
136
+ // Persist status file so the launcher can detect a running instance and
137
+ // open the browser without spawning a second server.
138
+ writeStatusFile();
139
+ recordEvent("control_center.started", {
140
+ pid: process.pid,
141
+ port,
142
+ url: formatted,
143
+ version: PATCHWARDEN_VERSION,
144
+ });
145
+ });
146
+
147
+ process.on("SIGINT", shutdown);
148
+ process.on("SIGTERM", shutdown);
149
+
150
+ return server;
151
+ }
152
+
153
+ function shutdown(): void {
154
+ logger.info("[control-center] Shutting down...");
155
+ recordEvent("control_center.stopped", { pid: process.pid });
156
+ removeStatusFile();
157
+ if (server) {
158
+ server.close(() => {
159
+ try { process.exit(0); } catch { /* ignore */ }
160
+ });
161
+ }
162
+ setTimeout(() => {
163
+ try { process.exit(0); } catch { /* ignore */ }
164
+ }, 3000).unref();
165
+ }