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,37 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { relative, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { cleanGeneratedOutput } from "../lib/clean-generated-output.js";
8
+ import { collectMatchingFiles } from "../lib/file-discovery.js";
9
+
10
+ const root = resolve(fileURLToPath(new URL("../..", import.meta.url)));
11
+ const sourceRoot = resolve(root, "src", "test", "unit");
12
+ const compiledRoot = resolve(root, "dist", "test", "unit");
13
+
14
+ const sourceTests = collectMatchingFiles(sourceRoot, (name) => name.endsWith(".test.ts"))
15
+ .map((path) => relative(sourceRoot, path).replace(/\\/g, "/").replace(/\.ts$/, ".js"));
16
+ const compiledTests = collectMatchingFiles(compiledRoot, (name) => name.endsWith(".test.js"))
17
+ .map((path) => relative(compiledRoot, path).replace(/\\/g, "/"));
18
+
19
+ assert.deepEqual(compiledTests, sourceTests, "compiled unit tests must exactly match source test paths");
20
+ assert.ok(compiledTests.some((path) => path.includes("/")), "unit discovery must include nested tests");
21
+
22
+ const fixtureRoot = mkdtempSync(resolve(tmpdir(), "patchwarden-clean-build-"));
23
+ try {
24
+ const fixtureDist = resolve(fixtureRoot, "dist");
25
+ const outside = resolve(fixtureRoot, "keep.txt");
26
+ mkdirSync(fixtureDist);
27
+ writeFileSync(resolve(fixtureDist, "stale.js"), "stale\n", "utf8");
28
+ writeFileSync(outside, "keep\n", "utf8");
29
+ cleanGeneratedOutput(fixtureRoot, fixtureDist);
30
+ assert.equal(existsSync(fixtureDist), false, "clean build must remove stale dist contents");
31
+ assert.equal(existsSync(outside), true, "clean build must preserve files outside dist");
32
+ assert.throws(() => cleanGeneratedOutput(fixtureRoot, resolve(fixtureRoot, "release")), /Refusing/);
33
+ } finally {
34
+ rmSync(fixtureRoot, { recursive: true, force: true });
35
+ }
36
+
37
+ console.log(`[build-output-check] OK: ${sourceTests.length} source tests match ${compiledTests.length} compiled tests.`);
@@ -121,9 +121,9 @@ process.on("SIGTERM", () => {
121
121
  process.exit(143);
122
122
  });
123
123
 
124
- function httpGet(url) {
124
+ function httpGet(url, headers = {}) {
125
125
  return new Promise((resolve, reject) => {
126
- const req = http.get(url, (res) => {
126
+ const req = http.get(url, { headers }, (res) => {
127
127
  let data = "";
128
128
  res.on("data", (chunk) => (data += chunk));
129
129
  res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
@@ -133,6 +133,34 @@ function httpGet(url) {
133
133
  });
134
134
  }
135
135
 
136
+ async function testLoopbackHostBoundary() {
137
+ const name = "Control Center rejects DNS-rebinding Host headers and cannot be framed";
138
+ try {
139
+ const hostileToken = await httpGet(`${BASE_URL}/control-token.json`, {
140
+ Host: "patchwarden.attacker.invalid",
141
+ });
142
+ const hostilePage = await httpGet(`${BASE_URL}/`, {
143
+ Host: "127.0.0.1.attacker.invalid",
144
+ });
145
+ const localPage = await httpGet(`${BASE_URL}/`);
146
+ const problems = [];
147
+ if (hostileToken.status !== 421 || hostileToken.body.includes("token")) {
148
+ problems.push(`hostile token request returned ${hostileToken.status}: ${hostileToken.body.slice(0, 120)}`);
149
+ }
150
+ if (hostilePage.status !== 421) problems.push(`hostile page request returned ${hostilePage.status}`);
151
+ if (localPage.status !== 200) problems.push(`loopback page returned ${localPage.status}`);
152
+ if (String(localPage.headers["x-frame-options"] || "").toUpperCase() !== "DENY") {
153
+ problems.push("X-Frame-Options is not DENY");
154
+ }
155
+ if (!String(localPage.headers["content-security-policy"] || "").includes("frame-ancestors 'none'")) {
156
+ problems.push("CSP does not deny frame ancestors");
157
+ }
158
+ record(name, problems.length === 0, problems.join("; "));
159
+ } catch (err) {
160
+ record(name, false, `error: ${err.message}`);
161
+ }
162
+ }
163
+
136
164
  function httpPost(url, headers = {}) {
137
165
  return new Promise((resolve, reject) => {
138
166
  const parsed = new URL(url);
@@ -221,6 +249,12 @@ async function testStaticFiles() {
221
249
  const root = await httpGet(`${BASE_URL}/`);
222
250
  const vendor = await httpGet(`${BASE_URL}/vendor/tailwindcss-browser.js`);
223
251
  const css = await httpGet(`${BASE_URL}/colors_and_type.css`);
252
+ const desktopCss = await httpGet(`${BASE_URL}/desktop.css`);
253
+ const desktopBridge = await httpGet(`${BASE_URL}/desktop-bridge.js`);
254
+ const i18n = await httpGet(`${BASE_URL}/i18n.js`);
255
+ const gettingStarted = await httpGet(`${BASE_URL}/pages/getting-started.html`);
256
+ const gettingStartedJs = await httpGet(`${BASE_URL}/getting-started.js`);
257
+ const settings = await httpGet(`${BASE_URL}/pages/settings.html`);
224
258
  const favicon = await httpGet(`${BASE_URL}/favicon.ico`);
225
259
 
226
260
  const checks = [];
@@ -228,7 +262,8 @@ async function testStaticFiles() {
228
262
  const rootCt = root.headers["content-type"] || "";
229
263
  if (!rootCt.includes("text/html")) checks.push(`GET / -> Content-Type ${rootCt} (expected text/html)`);
230
264
  if (!root.body.includes("setup-checklist-card")) checks.push("dashboard missing setup checklist card");
231
- if (!root.body.includes("Show Core / Direct log tails")) checks.push("dashboard activity log is not collapsed behind a summary");
265
+ if (!root.body.includes("查看 Core / Direct 本次日志尾部")) checks.push("dashboard activity log is not collapsed behind a summary");
266
+ if (!root.body.includes("pw-dashboard-toolbar")) checks.push("dashboard missing stable desktop toolbar layout");
232
267
  if (!root.body.includes("evidence-pack-card")) checks.push("dashboard missing v1.5 evidence pack card");
233
268
 
234
269
  if (vendor.status !== 200) checks.push(`GET /vendor/tailwindcss-browser.js -> status ${vendor.status}`);
@@ -239,6 +274,31 @@ async function testStaticFiles() {
239
274
  const cssCt = css.headers["content-type"] || "";
240
275
  if (!cssCt.includes("text/css")) checks.push(`GET /colors_and_type.css -> Content-Type ${cssCt}`);
241
276
 
277
+ if (desktopCss.status !== 200 || !(desktopCss.headers["content-type"] || "").includes("text/css")) {
278
+ checks.push("desktop refinement stylesheet is not served as CSS");
279
+ }
280
+ if (desktopBridge.status !== 200 || !(desktopBridge.headers["content-type"] || "").includes("javascript")) {
281
+ checks.push("desktop bridge is not served as JavaScript");
282
+ }
283
+ if (i18n.status !== 200 || !i18n.body.includes("applyTranslations") || !i18n.body.includes('"zh-CN"') || !i18n.body.includes("en:")) {
284
+ checks.push("shared Chinese/English translation dictionary is unavailable");
285
+ }
286
+ if (gettingStarted.status !== 200 || (gettingStarted.body.match(/class="readiness-item"/g) || []).length !== 4 || !gettingStarted.body.includes("home.manual")) {
287
+ checks.push("desktop getting-started page is missing its four readiness checks or manual ChatGPT state");
288
+ }
289
+ if (gettingStartedJs.status !== 200 || !gettingStartedJs.body.includes('fetchJson("/api/status")') || !gettingStartedJs.body.includes("health_check")) {
290
+ checks.push("getting-started controller is missing bounded Tunnel status or health_check guidance");
291
+ }
292
+ if (settings.status !== 200 || !settings.body.includes("/settings.js")) {
293
+ checks.push("desktop settings page is unavailable or missing its bounded bridge");
294
+ }
295
+ for (const marker of ["detectTunnel", "chooseTunnel", "enableDirectProfile", "proxyScope", "saveRuntime", "tunnelId", "runtimeKey", "provisionTunnel", "forgetCredential", "language"]) {
296
+ if (!settings.body.includes(marker)) checks.push(`desktop settings missing runtime control: ${marker}`);
297
+ }
298
+ if (!desktopBridge.body.includes("patchwardenApplyTheme") || !desktopBridge.body.includes("请求超过 10 秒")) {
299
+ checks.push("desktop bridge is missing renderer theme propagation or bounded loading timeout");
300
+ }
301
+
242
302
  if (favicon.status !== 200) checks.push(`GET /favicon.ico -> status ${favicon.status}`);
243
303
  const faviconCt = favicon.headers["content-type"] || "";
244
304
  if (!faviconCt.includes("image/svg+xml")) checks.push(`GET /favicon.ico -> Content-Type ${faviconCt}`);
@@ -268,6 +328,9 @@ async function testPageNavigationRoutes() {
268
328
  "/pages/workspace.html",
269
329
  "/pages/audit.html",
270
330
  "/pages/task-detail.html?id=smoke",
331
+ "/settings.html",
332
+ "/pages/settings.html",
333
+ "/pages/getting-started.html",
271
334
  ];
272
335
 
273
336
  for (const route of routes) {
@@ -411,6 +474,13 @@ async function testTasksJson() {
411
474
  if (!Array.isArray(json.tasks)) problems.push("'tasks' is not an array");
412
475
  if (typeof json.total !== "number") problems.push("'total' is not a number");
413
476
  if (typeof json.returned !== "number") problems.push("'returned' is not a number");
477
+ if (!(json.nextCursor === null || typeof json.nextCursor === "string")) problems.push("'nextCursor' is neither null nor string");
478
+ if (!isObject(json.filters) || !isObject(json.filters.applied) || !isObject(json.filters.options)) problems.push("'filters' contract is missing");
479
+
480
+ const traversal = await httpGet(`${BASE_URL}/api/tasks/..%2Foutside`);
481
+ if (traversal.status !== 400) {
482
+ problems.push(`encoded task traversal returned ${traversal.status} (expected 400)`);
483
+ }
414
484
 
415
485
  if (problems.length === 0) {
416
486
  record(name, true);
@@ -823,7 +893,7 @@ async function testStaleTasksApi() {
823
893
 
824
894
  // ── Test 12: GET /api/workspace/<traversal>/status -> 400 ───────
825
895
  async function testWorkspaceRepoPathTraversal() {
826
- const name = "Test 12: /api/workspace/<traversal>/status rejected with 400";
896
+ const name = "Test 12: workspace status rejects traversal without rejecting dotted names";
827
897
  // Use percent-encoded dots to bypass URL-parser segment normalization;
828
898
  // after decode the repoParam is "../secret" which contains ".." and is
829
899
  // rejected by the server's traversal guard.
@@ -833,7 +903,12 @@ async function testWorkspaceRepoPathTraversal() {
833
903
  if (res.status === 400) {
834
904
  const json = tryJson(res.body);
835
905
  if (json && typeof json.error === "string" && /traversal/i.test(json.error)) {
836
- record(name, true);
906
+ const dotted = await httpGet(`${BASE_URL}/api/workspace/release..candidate/status`);
907
+ if (dotted.status === 404) {
908
+ record(name, true);
909
+ } else {
910
+ record(name, false, `valid dotted repo name returned ${dotted.status} (expected 404 for missing directory)`);
911
+ }
837
912
  } else {
838
913
  record(name, false, `status 400 but error field missing/invalid: ${res.body.slice(0, 200)}`);
839
914
  }
@@ -882,10 +957,17 @@ async function testDirectSessionsEmptyList() {
882
957
  const problems = [];
883
958
  if (!Array.isArray(json.sessions)) problems.push("'sessions' is not an array");
884
959
  if (typeof json.total !== "number") problems.push("'total' is not a number");
960
+ if (!(json.nextCursor === null || typeof json.nextCursor === "string")) problems.push("'nextCursor' is neither null nor string");
961
+ if (!isObject(json.filters) || !isObject(json.filters.options)) problems.push("'filters' contract is missing");
962
+ if (typeof json.direct_profile_enabled !== "boolean") problems.push("'direct_profile_enabled' is not a boolean");
885
963
  if (json.reason !== null && typeof json.reason !== "string") problems.push("'reason' is neither null nor string");
886
964
  if (Array.isArray(json.sessions) && json.sessions.length !== json.total) {
887
965
  problems.push(`sessions.length (${json.sessions.length}) != total (${json.total})`);
888
966
  }
967
+ const invalidDetail = await httpGet(`${BASE_URL}/api/direct-sessions/invalid.id`);
968
+ const invalidSummary = await httpGet(`${BASE_URL}/api/direct-sessions/invalid.id/summary`);
969
+ if (invalidDetail.status !== 400) problems.push(`invalid detail id returned ${invalidDetail.status} (expected 400)`);
970
+ if (invalidSummary.status !== 400) problems.push(`invalid summary id returned ${invalidSummary.status} (expected 400)`);
889
971
  if (problems.length === 0) {
890
972
  record(name, true);
891
973
  } else {
@@ -915,6 +997,8 @@ async function testLogsTailParam() {
915
997
  if (typeof json.stderr !== "string") problems.push("'stderr' is not a string");
916
998
  if (json.tail !== 300) problems.push(`'tail' is ${json.tail} (expected 300)`);
917
999
  if (json.category !== "core") problems.push(`'category' is ${json.category} (expected "core")`);
1000
+ if (json.historical_snapshot !== true) problems.push("'historical_snapshot' is not true");
1001
+ if (json.redacted !== true) problems.push("'redacted' is not true");
918
1002
  if (problems.length === 0) {
919
1003
  record(name, true);
920
1004
  } else {
@@ -1088,6 +1172,351 @@ async function testControlCenterStatusApi() {
1088
1172
  }
1089
1173
  }
1090
1174
 
1175
+ // ── Test 21: GET /api/workspace/repos returns valid JSON with repos array ──
1176
+ async function testWorkspaceReposApi() {
1177
+ const name = "Test 21: /api/workspace/repos returns valid JSON with repos array";
1178
+ try {
1179
+ const res = await httpGet(`${BASE_URL}/api/workspace/repos`);
1180
+ if (res.status !== 200) {
1181
+ record(name, false, `status ${res.status} (expected 200)`);
1182
+ return;
1183
+ }
1184
+ const json = tryJson(res.body);
1185
+ if (!json || !isObject(json)) {
1186
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1187
+ return;
1188
+ }
1189
+ const problems = [];
1190
+ if (!Array.isArray(json.repos)) problems.push("'repos' is not an array");
1191
+ if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
1192
+ problems.push("'workspace_root' is neither string nor null");
1193
+ }
1194
+ if (Array.isArray(json.repos)) {
1195
+ for (let i = 0; i < json.repos.length; i++) {
1196
+ const repo = json.repos[i];
1197
+ if (!isObject(repo)) {
1198
+ problems.push(`repos[${i}] is not an object`);
1199
+ continue;
1200
+ }
1201
+ if (typeof repo.name !== "string") problems.push(`repos[${i}].name not string`);
1202
+ if (typeof repo.path !== "string") problems.push(`repos[${i}].path not string`);
1203
+ if (typeof repo.has_package_json !== "boolean") problems.push(`repos[${i}].has_package_json not boolean`);
1204
+ }
1205
+ }
1206
+ if (problems.length === 0) {
1207
+ record(name, true);
1208
+ } else {
1209
+ record(name, false, problems.join("; "));
1210
+ }
1211
+ } catch (err) {
1212
+ record(name, false, `error: ${err.message}`);
1213
+ }
1214
+ }
1215
+
1216
+ // ── Test 22: /api/release/status no longer mentions release_prepare and has ready_state ──
1217
+ async function testReleaseStatusFields() {
1218
+ const name = "Test 22: /api/release/status has ready_state and no release_prepare text";
1219
+ try {
1220
+ const res = await httpGet(`${BASE_URL}/api/release/status?repo_path=.`);
1221
+ if (res.status !== 200) {
1222
+ record(name, false, `status ${res.status} (expected 200)`);
1223
+ return;
1224
+ }
1225
+ const json = tryJson(res.body);
1226
+ if (!json || !isObject(json)) {
1227
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1228
+ return;
1229
+ }
1230
+ const problems = [];
1231
+ if (!("ready_state" in json)) problems.push("missing 'ready_state' field");
1232
+ if (typeof json.ready_state !== "string") problems.push("'ready_state' is not a string");
1233
+ if (!("package_name" in json)) problems.push("missing 'package_name' field");
1234
+ if (!("version_source" in json)) problems.push("missing 'version_source' field");
1235
+ if (!("version_consistent" in json)) problems.push("missing 'version_consistent' field");
1236
+ if (!Array.isArray(json.required_commands)) problems.push("'required_commands' is not an array");
1237
+ if (typeof json.commands_blocked_count !== "number") problems.push("'commands_blocked_count' is not a number");
1238
+ // The old misleading "release_prepare" text must no longer appear.
1239
+ if (res.body.includes("release_prepare")) {
1240
+ problems.push("response still contains 'release_prepare' text");
1241
+ }
1242
+ if (Array.isArray(json.required_commands)) {
1243
+ for (let i = 0; i < json.required_commands.length; i++) {
1244
+ const cmd = json.required_commands[i];
1245
+ if (!isObject(cmd)) {
1246
+ problems.push(`required_commands[${i}] is not an object`);
1247
+ continue;
1248
+ }
1249
+ if (typeof cmd.command !== "string") problems.push(`required_commands[${i}].command not string`);
1250
+ if (typeof cmd.allowed !== "boolean") problems.push(`required_commands[${i}].allowed not boolean`);
1251
+ if (!("blocked_reason" in cmd)) problems.push(`required_commands[${i}] missing 'blocked_reason'`);
1252
+ }
1253
+ }
1254
+ if (problems.length === 0) {
1255
+ record(name, true);
1256
+ } else {
1257
+ record(name, false, problems.join("; "));
1258
+ }
1259
+ } catch (err) {
1260
+ record(name, false, `error: ${err.message}`);
1261
+ }
1262
+ }
1263
+
1264
+ // ── Test 23: Safe task endpoints return valid JSON without full content ──
1265
+ async function testSafeTaskEndpoints() {
1266
+ const name = "Test 23: safe task endpoints return valid JSON without stdout/stderr/diff";
1267
+ const endpoints = [
1268
+ "safe-result",
1269
+ "safe-audit",
1270
+ "safe-test-summary",
1271
+ "safe-diff-summary",
1272
+ ];
1273
+ const problems = [];
1274
+ for (const ep of endpoints) {
1275
+ try {
1276
+ const res = await httpGet(`${BASE_URL}/api/tasks/nonexistent-smoke-task/${ep}`);
1277
+ if (res.status !== 200) {
1278
+ problems.push(`/api/tasks/:id/${ep} -> status ${res.status} (expected 200, not 500)`);
1279
+ continue;
1280
+ }
1281
+ const json = tryJson(res.body);
1282
+ if (!json || !isObject(json)) {
1283
+ problems.push(`/api/tasks/:id/${ep} body is not a JSON object: ${res.body.slice(0, 120)}`);
1284
+ continue;
1285
+ }
1286
+ // Safe endpoints must NOT expose full stdout/stderr/diff content.
1287
+ const text = res.body.toLowerCase();
1288
+ if (text.includes('"stdout"') && text.includes('"stderr"')) {
1289
+ // Some safe endpoints may include a "stdout" key as a bounded field; the
1290
+ // invariant we care about is that they don't return the full raw logs.
1291
+ // We only flag if both stdout AND stderr appear together (log-shaped).
1292
+ problems.push(`/api/tasks/:id/${ep} may leak stdout+stderr shaped fields`);
1293
+ }
1294
+ if (text.includes('"diff_patch"')) {
1295
+ problems.push(`/api/tasks/:id/${ep} leaks 'diff_patch' field`);
1296
+ }
1297
+ } catch (err) {
1298
+ problems.push(`/api/tasks/:id/${ep} error: ${err.message}`);
1299
+ }
1300
+ }
1301
+ if (problems.length === 0) {
1302
+ record(name, true);
1303
+ } else {
1304
+ record(name, false, problems.join("; "));
1305
+ }
1306
+ }
1307
+
1308
+ // ── Test 24: /api/tasks?status=running returns valid filtered JSON ──
1309
+ async function testTasksFilter() {
1310
+ const name = "Test 24: /api/tasks?status=running returns valid filtered JSON";
1311
+ try {
1312
+ const res = await httpGet(`${BASE_URL}/api/tasks?status=running`);
1313
+ if (res.status !== 200) {
1314
+ record(name, false, `status ${res.status} (expected 200)`);
1315
+ return;
1316
+ }
1317
+ const json = tryJson(res.body);
1318
+ if (!json || !isObject(json)) {
1319
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1320
+ return;
1321
+ }
1322
+ const problems = [];
1323
+ if (!Array.isArray(json.tasks)) problems.push("'tasks' is not an array");
1324
+ if (typeof json.total !== "number") problems.push("'total' is not a number");
1325
+ if (typeof json.returned !== "number") problems.push("'returned' is not a number");
1326
+ // Every returned task must match the filter (if any tasks exist).
1327
+ if (Array.isArray(json.tasks)) {
1328
+ for (let i = 0; i < json.tasks.length; i++) {
1329
+ if (json.tasks[i].status !== "running") {
1330
+ problems.push(`tasks[${i}].status is '${json.tasks[i].status}' (expected 'running')`);
1331
+ }
1332
+ }
1333
+ if (json.tasks.length !== json.total) {
1334
+ problems.push(`tasks.length (${json.tasks.length}) != total (${json.total})`);
1335
+ }
1336
+ }
1337
+ if (problems.length === 0) {
1338
+ record(name, true);
1339
+ } else {
1340
+ record(name, false, problems.join("; "));
1341
+ }
1342
+ } catch (err) {
1343
+ record(name, false, `error: ${err.message}`);
1344
+ }
1345
+ }
1346
+
1347
+ // ── Test 25: /api/tasks/stale has explanation and next_action on stale tasks ──
1348
+ async function testStaleTasksExplanation() {
1349
+ const name = "Test 25: /api/tasks/stale stale tasks have explanation and next_action";
1350
+ try {
1351
+ const res = await httpGet(`${BASE_URL}/api/tasks/stale`);
1352
+ if (res.status !== 200) {
1353
+ record(name, false, `status ${res.status} (expected 200)`);
1354
+ return;
1355
+ }
1356
+ const json = tryJson(res.body);
1357
+ if (!json || !isObject(json)) {
1358
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1359
+ return;
1360
+ }
1361
+ const problems = [];
1362
+ if (!Array.isArray(json.stale_tasks)) problems.push("'stale_tasks' is not an array");
1363
+ if (Array.isArray(json.stale_tasks) && json.stale_tasks.length > 0) {
1364
+ for (let i = 0; i < json.stale_tasks.length; i++) {
1365
+ const t = json.stale_tasks[i];
1366
+ if (!isObject(t)) {
1367
+ problems.push(`stale_tasks[${i}] is not an object`);
1368
+ continue;
1369
+ }
1370
+ if (typeof t.explanation !== "string" || t.explanation.length === 0) {
1371
+ problems.push(`stale_tasks[${i}] missing 'explanation' string`);
1372
+ }
1373
+ if (typeof t.next_action !== "string" || t.next_action.length === 0) {
1374
+ problems.push(`stale_tasks[${i}] missing 'next_action' string`);
1375
+ }
1376
+ if (typeof t.reason_code !== "string") {
1377
+ problems.push(`stale_tasks[${i}] missing 'reason_code' string`);
1378
+ }
1379
+ }
1380
+ }
1381
+ if (problems.length === 0) {
1382
+ record(name, true);
1383
+ } else {
1384
+ record(name, false, problems.join("; "));
1385
+ }
1386
+ } catch (err) {
1387
+ record(name, false, `error: ${err.message}`);
1388
+ }
1389
+ }
1390
+
1391
+ // ── Test 26: POST /api/tasks/:taskId/hide-stale token gate + valid response ──
1392
+ async function testHideStaleNoToken() {
1393
+ const name = "Test 26: POST /api/tasks/:taskId/hide-stale without token -> 403";
1394
+ try {
1395
+ const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {});
1396
+ if (res.status !== 403) {
1397
+ record(name, false, `status ${res.status} (expected 403 — token gate must fire before hide-stale runs)`);
1398
+ return;
1399
+ }
1400
+ const json = tryJson(res.body);
1401
+ if (!json || typeof json.error !== "string") {
1402
+ record(name, false, `response body missing 'error' field: ${res.body.slice(0, 200)}`);
1403
+ return;
1404
+ }
1405
+ record(name, true);
1406
+ } catch (err) {
1407
+ record(name, false, `error: ${err.message}`);
1408
+ }
1409
+ }
1410
+
1411
+ async function testHideStaleWithToken(token) {
1412
+ const name = "Test 27: POST /api/tasks/:taskId/hide-stale with token returns valid JSON";
1413
+ try {
1414
+ const res = await httpPost(`${BASE_URL}/api/tasks/nonexistent/hide-stale`, {
1415
+ "X-PatchWarden-Control-Token": token,
1416
+ });
1417
+ if (res.status !== 200) {
1418
+ record(name, false, `status ${res.status} (expected 200)`);
1419
+ return;
1420
+ }
1421
+ const json = tryJson(res.body);
1422
+ if (!json || !isObject(json)) {
1423
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1424
+ return;
1425
+ }
1426
+ if (json.ok !== true) {
1427
+ record(name, false, `expected ok=true, got: ${res.body.slice(0, 200)}`);
1428
+ return;
1429
+ }
1430
+ if (json.hidden !== "nonexistent") {
1431
+ record(name, false, `expected hidden='nonexistent', got: ${json.hidden}`);
1432
+ return;
1433
+ }
1434
+ record(name, true);
1435
+ } catch (err) {
1436
+ record(name, false, `error: ${err.message}`);
1437
+ }
1438
+ }
1439
+
1440
+ // ── Test 28: GET /api/warnings returns valid JSON ───────────────
1441
+ async function testWarningsApi() {
1442
+ const name = "Test 28: /api/warnings returns valid JSON with warnings array";
1443
+ try {
1444
+ const res = await httpGet(`${BASE_URL}/api/warnings`);
1445
+ if (res.status !== 200) {
1446
+ record(name, false, `status ${res.status} (expected 200)`);
1447
+ return;
1448
+ }
1449
+ const json = tryJson(res.body);
1450
+ if (!json || !isObject(json)) {
1451
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1452
+ return;
1453
+ }
1454
+ const problems = [];
1455
+ if (!Array.isArray(json.warnings)) problems.push("'warnings' is not an array");
1456
+ if (typeof json.total !== "number") problems.push("'total' is not a number");
1457
+ if (Array.isArray(json.warnings)) {
1458
+ for (let i = 0; i < json.warnings.length; i++) {
1459
+ const w = json.warnings[i];
1460
+ if (!isObject(w)) {
1461
+ problems.push(`warnings[${i}] is not an object`);
1462
+ continue;
1463
+ }
1464
+ if (typeof w.type !== "string") problems.push(`warnings[${i}].type is not a string`);
1465
+ if (typeof w.severity !== "string") problems.push(`warnings[${i}].severity is not a string`);
1466
+ if (typeof w.affected_tasks_count !== "number") problems.push(`warnings[${i}].affected_tasks_count is not a number`);
1467
+ if (typeof w.recommended_action !== "string") problems.push(`warnings[${i}].recommended_action is not a string`);
1468
+ }
1469
+ }
1470
+ if (problems.length === 0) {
1471
+ record(name, true);
1472
+ } else {
1473
+ record(name, false, problems.join("; "));
1474
+ }
1475
+ } catch (err) {
1476
+ record(name, false, `error: ${err.message}`);
1477
+ }
1478
+ }
1479
+
1480
+ // ── Test 29: GET /api/diagnostics returns valid JSON with redaction ─
1481
+ async function testDiagnosticsApi() {
1482
+ const name = "Test 29: /api/diagnostics returns valid JSON with required fields and redaction";
1483
+ try {
1484
+ const res = await httpGet(`${BASE_URL}/api/diagnostics`);
1485
+ if (res.status !== 200) {
1486
+ record(name, false, `status ${res.status} (expected 200)`);
1487
+ return;
1488
+ }
1489
+ const json = tryJson(res.body);
1490
+ if (!json || !isObject(json)) {
1491
+ record(name, false, `body is not a JSON object: ${res.body.slice(0, 200)}`);
1492
+ return;
1493
+ }
1494
+ const problems = [];
1495
+ if (typeof json.server_version !== "string") problems.push("'server_version' is not a string");
1496
+ if (typeof json.schema_epoch !== "string") problems.push("'schema_epoch' is not a string");
1497
+ if (typeof json.watcher_status !== "string") problems.push("'watcher_status' is not a string");
1498
+ if (typeof json.workspace_root !== "string" && json.workspace_root !== null) {
1499
+ problems.push("'workspace_root' is neither string nor null");
1500
+ }
1501
+ if (typeof json.direct_profile_enabled !== "boolean") problems.push("'direct_profile_enabled' is not a boolean");
1502
+ // Redaction: response body must not contain sensitive keywords
1503
+ const lower = res.body.toLowerCase();
1504
+ const forbidden = ["token", "password", "secret", "credential", "api_key"];
1505
+ for (const word of forbidden) {
1506
+ if (lower.includes(word)) {
1507
+ problems.push(`response body contains forbidden word '${word}' (redaction failure)`);
1508
+ }
1509
+ }
1510
+ if (problems.length === 0) {
1511
+ record(name, true);
1512
+ } else {
1513
+ record(name, false, problems.join("; "));
1514
+ }
1515
+ } catch (err) {
1516
+ record(name, false, `error: ${err.message}`);
1517
+ }
1518
+ }
1519
+
1091
1520
  async function main() {
1092
1521
  // Sanity: ensure the compiled server exists before spawning.
1093
1522
  if (!existsSync(serverPath) || !statSync(serverPath).isFile()) {
@@ -1095,7 +1524,7 @@ async function main() {
1095
1524
  process.exit(1);
1096
1525
  }
1097
1526
 
1098
- child = spawn("node", [serverPath], {
1527
+ child = spawn(process.execPath, [serverPath], {
1099
1528
  cwd: projectRoot,
1100
1529
  env: {
1101
1530
  ...process.env,
@@ -1135,6 +1564,7 @@ async function main() {
1135
1564
  console.log(`[control-center-smoke] server ready at ${BASE_URL}`);
1136
1565
 
1137
1566
  await testStaticFiles();
1567
+ await testLoopbackHostBoundary();
1138
1568
  await testPageNavigationRoutes();
1139
1569
  await testStatusJson();
1140
1570
  await testTasksJson();
@@ -1159,6 +1589,17 @@ async function main() {
1159
1589
  await testControlCenterLogApi();
1160
1590
  await testRestartAllNoToken();
1161
1591
  await testControlCenterStatusApi();
1592
+ // v1.5.1 Dashboard UI optimization coverage
1593
+ await testWorkspaceReposApi();
1594
+ await testReleaseStatusFields();
1595
+ await testSafeTaskEndpoints();
1596
+ await testTasksFilter();
1597
+ await testStaleTasksExplanation();
1598
+ await testHideStaleNoToken();
1599
+ if (token) await testHideStaleWithToken(token);
1600
+ // P1 Task 10/11 coverage
1601
+ await testWarningsApi();
1602
+ await testDiagnosticsApi();
1162
1603
 
1163
1604
  // Use token to silence unused-var linters and confirm we actually fetched it.
1164
1605
  if (token) {
@@ -139,12 +139,13 @@ try {
139
139
 
140
140
  const expectedFiles = [
141
141
  "PatchWarden.cmd",
142
- "PatchWarden-Control.cmd",
143
- "PatchWarden-Control-Tray.cmd",
144
- "PatchWarden-Desktop.cmd",
145
- "Restart-PatchWarden-Control.cmd",
146
- "Stop-PatchWarden.cmd",
142
+ "scripts/launchers/PatchWarden-Control.cmd",
143
+ "scripts/launchers/PatchWarden-Control-Tray.cmd",
144
+ "scripts/launchers/PatchWarden-Desktop.cmd",
145
+ "scripts/launchers/Restart-PatchWarden-Control.cmd",
146
+ "scripts/launchers/Stop-PatchWarden.cmd",
147
147
  "scripts/control/manage-patchwarden.ps1",
148
+ "scripts/control/run-background-supervisor.ps1",
148
149
  "scripts/control/stop-patchwarden.ps1",
149
150
  "scripts/launchers/Start-PatchWarden-Tunnel.cmd",
150
151
  "scripts/launchers/Start-PatchWarden-Direct-Tunnel.cmd",
@@ -158,14 +159,24 @@ try {
158
159
  if (!rootEntry.includes("manage-patchwarden.ps1")) {
159
160
  throw new Error("PatchWarden.cmd does not invoke the consolidated manager");
160
161
  }
161
- const stopEntry = readFileSync(join(root, "Stop-PatchWarden.cmd"), "utf8");
162
- if (!stopEntry.includes("scripts\\control\\stop-patchwarden.ps1")) {
162
+ const stopEntry = readFileSync(join(root, "scripts", "launchers", "Stop-PatchWarden.cmd"), "utf8");
163
+ if (!stopEntry.includes("control\\stop-patchwarden.ps1")) {
163
164
  throw new Error("Stop-PatchWarden.cmd does not invoke the one-click shutdown script");
164
165
  }
165
- const desktopEntry = readFileSync(join(root, "PatchWarden-Desktop.cmd"), "utf8");
166
+ const desktopEntry = readFileSync(join(root, "scripts", "launchers", "PatchWarden-Desktop.cmd"), "utf8");
166
167
  if (!desktopEntry.includes("control-center-tray.ps1") || !desktopEntry.includes("WindowStyle Hidden")) {
167
168
  throw new Error("PatchWarden-Desktop.cmd must launch the tray hidden as the daily desktop entry");
168
169
  }
170
+ const managerSource = readFileSync(manager, "utf8");
171
+ const wrapperSource = readFileSync(join(root, "scripts", "control", "run-background-supervisor.ps1"), "utf8");
172
+ for (const expected of ["supervisor-status.json", "supervisor.stdout.log", "supervisor.stderr.log", "run-background-supervisor.ps1", "Write-BackgroundStartingState"]) {
173
+ if (!managerSource.includes(expected)) {
174
+ throw new Error(`background manager is missing supervisor observability: ${expected}`);
175
+ }
176
+ }
177
+ if (!wrapperSource.includes("1> $stdout 2> $stderr") || wrapperSource.includes("CONTROL_PLANE_API_KEY")) {
178
+ throw new Error("background wrapper must redirect supervisor logs without credential arguments");
179
+ }
169
180
  console.log("ok - control handles orphan cleanup, scoped kill, port conflicts, health fallback, and Core/Direct lifecycle actions");
170
181
  } finally {
171
182
  if (fakeTunnel?.exitCode === null) fakeTunnel.kill();