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,405 @@
1
+ /**
2
+ * Control Center routes — runtime status, events, diagnostics, tunnel URLs.
3
+ *
4
+ * `handleStatus` is the dashboard's primary poll endpoint: it fans out health
5
+ * probes, watcher/task snapshots, and suggestion generation, and records
6
+ * observed state-change events (core/direct/watcher/task transitions) into the
7
+ * activity timeline. The remaining endpoints expose the control-center status
8
+ * file, the event timeline, diagnostics (redacted), and tunnel UI URLs.
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { type ServerResponse } from "node:http";
12
+ import { type AgentAvailability } from "../../tools/workspace/listAgents.js";
13
+ import { type TaskEntry } from "../../tools/tasks/listTasks.js";
14
+ import { type WatcherStatusSnapshot } from "../../watcherStatus.js";
15
+ import { PATCHWARDEN_VERSION, TOOL_SCHEMA_EPOCH } from "../../version.js";
16
+ import { redactSensitiveValue } from "../../security/contentRedaction.js";
17
+ import {
18
+ type ControlCenterStatusFile,
19
+ type RuntimeHealth,
20
+ type StaleClassification,
21
+ type StatusTasks,
22
+ listAgentsSafe,
23
+ listTasksForStatus,
24
+ probeRuntimeHealth,
25
+ readEvents,
26
+ recordEvent,
27
+ readToolManifest,
28
+ readTunnelStatus,
29
+ readTunnelUrl,
30
+ readWatcherStatusSafe,
31
+ resolveWorkspaceRootSafe,
32
+ } from "../runtime.js";
33
+ import { findTunnelClientExecutable } from "./process.js";
34
+ import {
35
+ config,
36
+ configIdentitySha256,
37
+ controlCenterStatusPath,
38
+ CORE_BASE_URL,
39
+ DIRECT_BASE_URL,
40
+ errorMessage,
41
+ readJsonFileSafe,
42
+ sendJson,
43
+ } from "../shared.js";
44
+
45
+ // ── Health suggestions ────────────────────────────────────────────
46
+
47
+ interface Suggestion {
48
+ code: string;
49
+ severity: "info" | "warning" | "error";
50
+ message: string;
51
+ action?: string;
52
+ link?: string;
53
+ }
54
+
55
+ interface StatusSnapshotForSuggestions {
56
+ core: RuntimeHealth;
57
+ direct: RuntimeHealth;
58
+ watcher: WatcherStatusSnapshot;
59
+ tunnel: { core: Record<string, unknown>; direct: Record<string, unknown> };
60
+ agents: AgentAvailability[];
61
+ tasks: StatusTasks;
62
+ direct_profile_enabled: boolean;
63
+ }
64
+
65
+ export interface ExperienceStatus {
66
+ readiness: "ready" | "needs_action" | "blocked";
67
+ live_checks: Array<{
68
+ key: string;
69
+ label: string;
70
+ state: "ready" | "needs_action" | "blocked" | "optional";
71
+ detail: string;
72
+ }>;
73
+ historical_summary: {
74
+ stale_tasks: number;
75
+ failed_tasks: number;
76
+ note: string;
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Keep current readiness separate from historical evidence. A month-old failed
82
+ * task is useful for audit, but must not make a configured local bridge look
83
+ * unavailable on the first screen.
84
+ */
85
+ export function buildExperienceStatus(snapshot: StatusSnapshotForSuggestions): ExperienceStatus {
86
+ const coreReady = snapshot.core.available && snapshot.tunnel.core.ready === true;
87
+ const watcherReady = snapshot.watcher.status === "healthy";
88
+ const agentsReady = snapshot.agents.some((agent) => agent.available);
89
+ const directEnabled = snapshot.direct_profile_enabled;
90
+ const directReady = snapshot.direct.available && snapshot.tunnel.direct.ready === true;
91
+ const staleTasks = snapshot.tasks.stale;
92
+ const failedTasks = snapshot.tasks.tasks.filter((task) => String((task as TaskEntry).status || "").includes("failed")).length;
93
+
94
+ const live_checks: ExperienceStatus["live_checks"] = [
95
+ { key: "workspace_agents", label: "工作区与 Agent", state: agentsReady ? "ready" : "blocked", detail: agentsReady ? "工作区内至少有一个可用 Agent" : "需要配置工作区或可用 Agent" },
96
+ { key: "core", label: "Core 服务", state: coreReady ? "ready" : "needs_action", detail: coreReady ? "Core 与 Tunnel 已就绪" : "启动 Core 后会继续检查 Tunnel 与 Watcher" },
97
+ { key: "watcher", label: "任务监视器", state: watcherReady ? "ready" : "needs_action", detail: watcherReady ? "Watcher 心跳正常" : "Core 启动后 Watcher 将处理排队任务" },
98
+ { key: "direct", label: "Direct 编辑", state: directEnabled ? (directReady ? "ready" : "needs_action") : "optional", detail: directEnabled ? (directReady ? "Direct 已就绪" : "已启用但尚未就绪") : "可选高级能力,未启用不会影响 Core" },
99
+ ];
100
+
101
+ const readiness: ExperienceStatus["readiness"] = !agentsReady
102
+ ? "blocked"
103
+ : coreReady && watcherReady && (!directEnabled || directReady)
104
+ ? "ready"
105
+ : "needs_action";
106
+
107
+ return {
108
+ readiness,
109
+ live_checks,
110
+ historical_summary: {
111
+ stale_tasks: staleTasks,
112
+ failed_tasks: failedTasks,
113
+ note: "历史任务与审计记录不会影响当前实时就绪状态。",
114
+ },
115
+ };
116
+ }
117
+
118
+ export function reconcileTunnelStatus(
119
+ status: Record<string, unknown>,
120
+ health: RuntimeHealth,
121
+ ): Record<string, unknown> {
122
+ if (!health.available) return status;
123
+ return {
124
+ ...status,
125
+ observed: true,
126
+ status: "running",
127
+ ready: true,
128
+ reason_code: "health_endpoint_ready",
129
+ health_observed: true,
130
+ };
131
+ }
132
+
133
+ export function buildSuggestions(s: StatusSnapshotForSuggestions): Suggestion[] {
134
+ const out: Suggestion[] = [];
135
+
136
+ if (!s.core.available) {
137
+ out.push({
138
+ code: "core_stopped",
139
+ severity: "warning",
140
+ message: "Core 未运行,建议启动 Core profile",
141
+ action: "/api/core/start",
142
+ });
143
+ }
144
+ if (s.direct_profile_enabled && !s.direct.available) {
145
+ out.push({
146
+ code: "direct_stopped",
147
+ severity: "warning",
148
+ message: "Direct 未运行,建议启动 Direct profile",
149
+ action: "/api/direct/start",
150
+ });
151
+ }
152
+
153
+ if (s.watcher.status === "degraded" || s.watcher.status === "stale" || s.watcher.status === "unreadable") {
154
+ out.push({
155
+ code: "watcher_stale",
156
+ severity: "error",
157
+ message: "Watcher 处于 " + s.watcher.status + " 状态,建议重启 Core",
158
+ action: "/api/core/restart",
159
+ });
160
+ }
161
+
162
+ if (s.tasks.stale > 0) {
163
+ out.push({
164
+ code: "stale_task",
165
+ severity: "warning",
166
+ message: "存在 " + s.tasks.stale + " 个 stale 任务,建议查看并 reconcile",
167
+ link: "/pages/tasks.html?filter=stale",
168
+ });
169
+ }
170
+
171
+ const coreTunnelReady = !!(s.tunnel.core && s.tunnel.core.ready);
172
+ const directTunnelReady = !!(s.tunnel.direct && s.tunnel.direct.ready);
173
+ if (!coreTunnelReady || (s.direct_profile_enabled && !directTunnelReady)) {
174
+ out.push({
175
+ code: "tunnel_not_ready",
176
+ severity: "warning",
177
+ message: "Tunnel 未就绪,建议启动 profile 或检查代理",
178
+ action: "/api/start-all",
179
+ });
180
+ }
181
+
182
+ const missingAgents = s.agents.filter((a) => !a.available);
183
+ if (missingAgents.length > 0) {
184
+ out.push({
185
+ code: "agent_missing",
186
+ severity: "info",
187
+ message: "Agent 未就绪:" + missingAgents.map((a) => a.name).join(", ") + "(请检查对应 CLI 安装与路径)",
188
+ });
189
+ }
190
+
191
+ return out;
192
+ }
193
+
194
+ // ── Observed state-change detection (drives activity timeline) ────
195
+
196
+ interface StatusSnapshotDigest {
197
+ core_available: boolean;
198
+ direct_available: boolean;
199
+ watcher_status: string;
200
+ task_statuses: Record<string, string>;
201
+ }
202
+
203
+ let lastStatusDigest: StatusSnapshotDigest | null = null;
204
+
205
+ function buildStatusDigest(s: StatusSnapshotForSuggestions): StatusSnapshotDigest {
206
+ const task_statuses: Record<string, string> = {};
207
+ for (const t of s.tasks.tasks) {
208
+ const entry = t as TaskEntry & StaleClassification;
209
+ task_statuses[entry.task_id] = entry.status;
210
+ }
211
+ return {
212
+ core_available: s.core.available,
213
+ direct_available: s.direct.available,
214
+ watcher_status: s.watcher.status,
215
+ task_statuses,
216
+ };
217
+ }
218
+
219
+ function diffAndRecordEvents(prev: StatusSnapshotDigest, curr: StatusSnapshotDigest): void {
220
+ if (prev.core_available !== curr.core_available) {
221
+ recordEvent("core.status_changed", { from: prev.core_available, to: curr.core_available });
222
+ }
223
+ if (prev.direct_available !== curr.direct_available) {
224
+ recordEvent("direct.status_changed", { from: prev.direct_available, to: curr.direct_available });
225
+ }
226
+ if (prev.watcher_status !== curr.watcher_status) {
227
+ recordEvent("watcher.status_changed", { from: prev.watcher_status, to: curr.watcher_status });
228
+ }
229
+ for (const [taskId, newStatus] of Object.entries(curr.task_statuses)) {
230
+ const oldStatus = prev.task_statuses[taskId];
231
+ if (oldStatus && oldStatus !== newStatus) {
232
+ recordEvent("task.status_changed", { task_id: taskId, from: oldStatus, to: newStatus });
233
+ }
234
+ }
235
+ }
236
+
237
+ // ── Handlers ──────────────────────────────────────────────────────
238
+
239
+ export async function handleStatus(res: ServerResponse): Promise<void> {
240
+ try {
241
+ const [coreHealth, directHealth, watcher, tunnelCoreRaw, tunnelDirectRaw, toolsCore, toolsDirect, agents, workspaceRoot, tasks] = await Promise.all([
242
+ probeRuntimeHealth(CORE_BASE_URL).catch((err): RuntimeHealth => ({
243
+ available: false,
244
+ reason: errorMessage(err),
245
+ healthz: null,
246
+ readyz: null,
247
+ })),
248
+ probeRuntimeHealth(DIRECT_BASE_URL).catch((err): RuntimeHealth => ({
249
+ available: false,
250
+ reason: errorMessage(err),
251
+ healthz: null,
252
+ readyz: null,
253
+ })),
254
+ Promise.resolve(readWatcherStatusSafe()),
255
+ Promise.resolve(readTunnelStatus(false)),
256
+ Promise.resolve(readTunnelStatus(true)),
257
+ Promise.resolve(readToolManifest(false)),
258
+ Promise.resolve(readToolManifest(true)),
259
+ Promise.resolve(listAgentsSafe()),
260
+ Promise.resolve(resolveWorkspaceRootSafe()),
261
+ Promise.resolve(listTasksForStatus()),
262
+ ]);
263
+ const tunnelCore = reconcileTunnelStatus(tunnelCoreRaw, coreHealth);
264
+ const tunnelDirect = reconcileTunnelStatus(tunnelDirectRaw, directHealth);
265
+ const snapshotForSuggestions: StatusSnapshotForSuggestions = {
266
+ core: coreHealth,
267
+ direct: directHealth,
268
+ watcher,
269
+ tunnel: { core: tunnelCore, direct: tunnelDirect },
270
+ agents,
271
+ tasks,
272
+ direct_profile_enabled: config.enableDirectProfile === true,
273
+ };
274
+ const suggestions = buildSuggestions(snapshotForSuggestions);
275
+ const experience = buildExperienceStatus(snapshotForSuggestions);
276
+ const tunnelClientExe = findTunnelClientExecutable();
277
+
278
+ // Diff against the previous poll to record observed state-change events.
279
+ // This is the only place that observes Core/Direct/watcher/task transitions
280
+ // (the control center is otherwise stateless and pull-driven).
281
+ const digest = buildStatusDigest(snapshotForSuggestions);
282
+ if (lastStatusDigest) {
283
+ diffAndRecordEvents(lastStatusDigest, digest);
284
+ }
285
+ lastStatusDigest = digest;
286
+
287
+ sendJson(res, 200, {
288
+ core: coreHealth,
289
+ direct: directHealth,
290
+ watcher,
291
+ tunnel: { core: tunnelCore, direct: tunnelDirect },
292
+ tools: { core: toolsCore, direct: toolsDirect },
293
+ agents,
294
+ workspace_root: workspaceRoot,
295
+ tasks,
296
+ suggestions,
297
+ experience,
298
+ direct_profile_enabled: config.enableDirectProfile === true,
299
+ setup: {
300
+ tunnel_client: {
301
+ available: tunnelClientExe !== null,
302
+ path: tunnelClientExe,
303
+ source: config.tunnelClientPath && tunnelClientExe === config.tunnelClientPath ? "config" : "detected",
304
+ },
305
+ workspace_root: workspaceRoot,
306
+ watcher: {
307
+ status: watcher.status,
308
+ available: watcher.available,
309
+ reason: watcher.reason,
310
+ },
311
+ },
312
+ });
313
+ } catch (err) {
314
+ sendJson(res, 200, { error: errorMessage(err), partial: true });
315
+ }
316
+ }
317
+
318
+ export function handleControlCenterStatus(res: ServerResponse): void {
319
+ // Public read of the status file (used by tray/launcher to confirm identity).
320
+ if (!existsSync(controlCenterStatusPath)) {
321
+ sendJson(res, 200, { running: false });
322
+ return;
323
+ }
324
+ const data = readJsonFileSafe<ControlCenterStatusFile>(controlCenterStatusPath);
325
+ if (!data) {
326
+ sendJson(res, 200, { running: false });
327
+ return;
328
+ }
329
+ sendJson(res, 200, { running: true, ...data });
330
+ }
331
+
332
+ export function handleEvents(res: ServerResponse, limit: number): void {
333
+ const events = readEvents(limit);
334
+ sendJson(res, 200, {
335
+ events,
336
+ total: events.length,
337
+ limit,
338
+ });
339
+ }
340
+
341
+ export function handleTunnelUiUrl(res: ServerResponse): void {
342
+ sendJson(res, 200, {
343
+ core: readTunnelUrl(false),
344
+ direct: readTunnelUrl(true),
345
+ });
346
+ }
347
+
348
+ export async function handleDiagnostics(res: ServerResponse): Promise<void> {
349
+ try {
350
+ const watcher = readWatcherStatusSafe();
351
+ const [coreHealth, directHealth] = await Promise.all([
352
+ probeRuntimeHealth(CORE_BASE_URL),
353
+ probeRuntimeHealth(DIRECT_BASE_URL),
354
+ ]);
355
+ const tunnelCore = reconcileTunnelStatus(readTunnelStatus(false), coreHealth);
356
+ const tunnelDirect = reconcileTunnelStatus(readTunnelStatus(true), directHealth);
357
+ const toolsCore = readToolManifest(false);
358
+ const toolsDirect = readToolManifest(true);
359
+ const agents = listAgentsSafe();
360
+
361
+ let workspaceRoot: string | null = null;
362
+ try {
363
+ workspaceRoot = resolveWorkspaceRootSafe();
364
+ } catch {
365
+ workspaceRoot = null;
366
+ }
367
+
368
+ // Recent failures: last 5 failed tasks (status contains "failed")
369
+ let recentFailures: Array<{ task_id: string; status: string }> = [];
370
+ try {
371
+ const result = listTasksForStatus();
372
+ recentFailures = (result.tasks as Array<{ task_id: string; status: string }>)
373
+ .filter((t) => typeof t.status === "string" && t.status.includes("failed"))
374
+ .slice(0, 5)
375
+ .map((t) => ({ task_id: t.task_id, status: t.status }));
376
+ } catch {
377
+ recentFailures = [];
378
+ }
379
+
380
+ const coreReady = !!(tunnelCore && tunnelCore.ready);
381
+ const directReady = !!(tunnelDirect && tunnelDirect.ready);
382
+
383
+ const diagnostics = {
384
+ server_version: PATCHWARDEN_VERSION,
385
+ schema_epoch: TOOL_SCHEMA_EPOCH,
386
+ tool_manifest_sha256: toolsCore.tool_manifest_sha256,
387
+ watcher_status: watcher.status,
388
+ tunnel_core_ready: coreReady,
389
+ tunnel_direct_ready: directReady,
390
+ core_tool_count: toolsCore.tool_count,
391
+ direct_tool_count: toolsDirect.tool_count,
392
+ agent_status: agents.map((a) => ({ name: a.name, available: a.available })),
393
+ workspace_root: workspaceRoot,
394
+ recent_failures: recentFailures,
395
+ direct_profile_enabled: config.enableDirectProfile ?? false,
396
+ config_identity_sha256: configIdentitySha256,
397
+ };
398
+
399
+ // Redact any sensitive content that may have leaked into string fields.
400
+ const redacted = redactSensitiveValue(diagnostics);
401
+ sendJson(res, 200, redacted.value);
402
+ } catch (err) {
403
+ sendJson(res, 200, { error: errorMessage(err) });
404
+ }
405
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Control Center routes — mutating task actions (POST /api/tasks/:id/*).
3
+ *
4
+ * These handlers are invoked only after the server router has validated the
5
+ * control token. They cover reconcile, on-demand audit_task, opening the task
6
+ * folder in the host file explorer, and hiding a stale task from the
7
+ * dashboard. None of them delete task files; they annotate or launch external
8
+ * viewers.
9
+ */
10
+ import { existsSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { type ServerResponse } from "node:http";
13
+ import { auditTask } from "../../tools/diagnostics/auditTask.js";
14
+ import { getTasksDir } from "../../config.js";
15
+ import {
16
+ classifyStaleTask,
17
+ isValidTaskId,
18
+ readHiddenStaleIds,
19
+ reconstructTaskEntry,
20
+ readWatcherStatusSafe,
21
+ recordEvent,
22
+ TERMINAL_TASK_STATUSES,
23
+ writeHiddenStaleIds,
24
+ } from "../runtime.js";
25
+ import { launchFileManager } from "../fileManager.js";
26
+ import { config, errorMessage, guardControlPath, readJsonFileSafe, sendJson } from "../shared.js";
27
+ import { atomicWriteJsonFileSync } from "../../utils/atomicFile.js";
28
+ import { mutateTaskStatus } from "../../runner/taskStatusStore.js";
29
+
30
+ /**
31
+ * Reconcile a stale task. Does NOT delete the task. Reads the task files,
32
+ * decides whether it is safe to mark the task as stale/archived, writes a
33
+ * reconcile record, and (when safe) annotates status.json with reconcile
34
+ * metadata. The task status enum is never changed — only metadata is added.
35
+ */
36
+ export function handleReconcile(res: ServerResponse, taskId: string): void {
37
+ try {
38
+ if (!isValidTaskId(taskId)) {
39
+ sendJson(res, 400, { error: "Invalid task id" });
40
+ return;
41
+ }
42
+ const tasksDir = getTasksDir(config);
43
+ const taskDir = guardControlPath(join(tasksDir, taskId), config.tasksDir);
44
+ if (!taskDir || !existsSync(taskDir) || !statSync(taskDir).isDirectory()) {
45
+ sendJson(res, 404, { error: "Task not found" });
46
+ return;
47
+ }
48
+
49
+ const statusPath = join(taskDir, "status.json");
50
+ const runtimePath = join(taskDir, "runtime.json");
51
+ const statusData = readJsonFileSafe<Record<string, unknown>>(statusPath) ?? {};
52
+ const runtimeData = readJsonFileSafe<Record<string, unknown>>(runtimePath) ?? {};
53
+
54
+ const watcher = readWatcherStatusSafe();
55
+ const taskEntry = reconstructTaskEntry(taskId, statusData, runtimeData, watcher);
56
+
57
+ const cls = classifyStaleTask(taskEntry, watcher);
58
+ const isTerminal = TERMINAL_TASK_STATUSES.has(taskEntry.status);
59
+
60
+ // Safe to mark stale/archived when:
61
+ // - terminal status -> archive (already finished)
62
+ // - stale AND watcher is not actively driving it (no current_command OR watcher not healthy)
63
+ let decision: "marked_stale" | "marked_archived" | "no_action";
64
+ let safe = false;
65
+ if (isTerminal) {
66
+ decision = "marked_archived";
67
+ safe = true;
68
+ } else if (
69
+ cls.is_stale &&
70
+ (taskEntry.current_command === null || taskEntry.current_command === "" || watcher.status !== "healthy")
71
+ ) {
72
+ decision = "marked_stale";
73
+ safe = true;
74
+ } else {
75
+ decision = "no_action";
76
+ safe = false;
77
+ }
78
+
79
+ const reconciledAt = new Date().toISOString();
80
+ const reconcileRecord = {
81
+ task_id: taskId,
82
+ reconciled_at: reconciledAt,
83
+ decision,
84
+ safe,
85
+ previous_status: taskEntry.status,
86
+ previous_phase: taskEntry.phase,
87
+ is_stale: cls.is_stale,
88
+ stale_reasons: cls.stale_reasons,
89
+ watcher_status: watcher.status,
90
+ watcher_last_heartbeat_at: watcher.last_heartbeat_at,
91
+ task_last_heartbeat_at: taskEntry.last_heartbeat_at || null,
92
+ task_current_command: taskEntry.current_command,
93
+ notes:
94
+ decision === "no_action"
95
+ ? "Task does not currently qualify for safe reconcile (still actively running or watcher is healthy)."
96
+ : "Task annotated with reconcile metadata; original status preserved. No files were deleted.",
97
+ };
98
+
99
+ // Write the reconcile record artifact.
100
+ try {
101
+ atomicWriteJsonFileSync(join(taskDir, "reconcile.json"), reconcileRecord);
102
+ } catch (writeErr) {
103
+ sendJson(res, 500, { error: `Failed to write reconcile record: ${errorMessage(writeErr)}` });
104
+ return;
105
+ }
106
+
107
+ // Annotate status.json with reconcile metadata (do not mutate status enum).
108
+ if (safe) {
109
+ const annotation = {
110
+ reconcile_state: decision === "marked_archived" ? "archived" : "stale",
111
+ reconciled_at: reconciledAt,
112
+ };
113
+ try {
114
+ mutateTaskStatus(statusPath, (current) => {
115
+ if (current.status !== taskEntry.status) {
116
+ throw new Error("Task status changed while reconcile was being evaluated; retry.");
117
+ }
118
+ const next = { ...current, ...annotation, updated_at: new Date().toISOString() };
119
+ return { next, result: next };
120
+ });
121
+ } catch (writeErr) {
122
+ sendJson(res, 500, { error: `Failed to annotate status.json: ${errorMessage(writeErr)}` });
123
+ return;
124
+ }
125
+ }
126
+
127
+ recordEvent("task.reconciled", {
128
+ task_id: taskId,
129
+ decision,
130
+ safe,
131
+ previous_status: taskEntry.status,
132
+ is_stale: cls.is_stale,
133
+ });
134
+ sendJson(res, 200, reconcileRecord);
135
+ } catch (err) {
136
+ sendJson(res, 500, { error: errorMessage(err) });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Run audit_task for a task. Only safe to delegate when the task directory
142
+ * exists and the task is in a terminal state (auditing a running task mid-flight
143
+ * would race with the watcher writing artifacts).
144
+ */
145
+ export function handleTaskAudit(res: ServerResponse, taskId: string): void {
146
+ try {
147
+ if (!isValidTaskId(taskId)) {
148
+ sendJson(res, 400, { error: "Invalid task id" });
149
+ return;
150
+ }
151
+ const tasksDir = getTasksDir(config);
152
+ const taskDir = guardControlPath(join(tasksDir, taskId), config.tasksDir);
153
+ if (!taskDir || !existsSync(taskDir) || !statSync(taskDir).isDirectory()) {
154
+ sendJson(res, 404, { error: "Task not found" });
155
+ return;
156
+ }
157
+ const statusData = readJsonFileSafe<Record<string, unknown>>(join(taskDir, "status.json"));
158
+ const taskStatus = statusData ? String(statusData.status || "") : "";
159
+ if (!TERMINAL_TASK_STATUSES.has(taskStatus as string)) {
160
+ sendJson(res, 409, {
161
+ error: "Task is not in a terminal state; audit_task can only run safely after completion.",
162
+ status: taskStatus || "unknown",
163
+ });
164
+ return;
165
+ }
166
+ const output = auditTask(taskId);
167
+ recordEvent("task.audited", { task_id: taskId, previous_status: taskStatus });
168
+ sendJson(res, 200, { ok: true, audit: output });
169
+ } catch (err) {
170
+ sendJson(res, 500, { error: errorMessage(err) });
171
+ }
172
+ }
173
+
174
+ export function handleOpenTaskFolder(res: ServerResponse, taskId: string): void {
175
+ try {
176
+ if (!isValidTaskId(taskId)) {
177
+ sendJson(res, 400, { error: "Invalid task id" });
178
+ return;
179
+ }
180
+ const tasksDir = getTasksDir(config);
181
+ const taskDir = guardControlPath(join(tasksDir, taskId), config.tasksDir);
182
+ if (!taskDir || !existsSync(taskDir) || !statSync(taskDir).isDirectory()) {
183
+ sendJson(res, 404, { error: "Task not found" });
184
+ return;
185
+ }
186
+ launchFileManager(taskDir, tasksDir);
187
+ sendJson(res, 200, { ok: true, path: taskDir });
188
+ } catch (err) {
189
+ sendJson(res, 500, { error: errorMessage(err) });
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Hide a stale task from the dashboard's stale-task view. The task itself is
195
+ * NOT deleted or modified — only the control-center's local hidden-stale-ids
196
+ * state file is updated. Requires control token (enforced by the POST router).
197
+ */
198
+ export function handleHideStale(res: ServerResponse, taskId: string): void {
199
+ try {
200
+ if (!isValidTaskId(taskId)) {
201
+ sendJson(res, 400, { error: "Invalid task id" });
202
+ return;
203
+ }
204
+ const ids = readHiddenStaleIds();
205
+ if (!ids.includes(taskId)) {
206
+ ids.push(taskId);
207
+ writeHiddenStaleIds(ids);
208
+ }
209
+ recordEvent("task.hide_stale", { task_id: taskId });
210
+ sendJson(res, 200, { ok: true, hidden: taskId });
211
+ } catch (err) {
212
+ sendJson(res, 500, { error: errorMessage(err) });
213
+ }
214
+ }