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
@@ -133,6 +133,8 @@ html.dark {
133
133
  background-color: currentColor;
134
134
  }
135
135
  </style>
136
+ <script src="/desktop-bootstrap.js"></script>
137
+ <link rel="stylesheet" href="/desktop.css">
136
138
  </head>
137
139
  <body class="flex h-screen overflow-hidden">
138
140
 
@@ -150,6 +152,10 @@ html.dark {
150
152
 
151
153
  <!-- Navigation -->
152
154
  <nav class="flex-1 flex flex-col gap-1 px-3 pt-4 overflow-y-auto">
155
+ <a href="/pages/getting-started.html" data-nav-key="getting-started" class="desktop-only-nav flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
156
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.87 12.87 0 0 1 22 2c0 2.72-.78 7.5-6.05 11a22.35 22.35 0 0 1-3.95 2z"/></svg>
157
+ <span class="truncate" data-i18n="nav.gettingStarted">开始使用</span>
158
+ </a>
153
159
 
154
160
  <a href="/pages/dashboard.html" data-nav-key="dashboard" data-active="true" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
155
161
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="shrink-0">
@@ -205,6 +211,10 @@ html.dark {
205
211
  <span class="truncate" data-zh="日志" data-en="Logs">日志</span>
206
212
  </a>
207
213
 
214
+ <a href="/pages/settings.html" data-nav-key="settings" class="desktop-only-nav flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
215
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="shrink-0"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3V2.8h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9A1.7 1.7 0 0 0 21 10h.2v4H21a1.7 1.7 0 0 0-1.6 1z"/></svg>
216
+ <span class="truncate" data-i18n="nav.settings">设置</span>
217
+ </a>
208
218
  </nav>
209
219
 
210
220
  <!-- Bottom Section -->
@@ -238,46 +248,67 @@ html.dark {
238
248
  <main class="flex-1 overflow-y-auto p-6" style="background-color: var(--pw-bg-elevated);">
239
249
 
240
250
  <!-- ===== TOP TOOLBAR ===== -->
241
- <div class="flex flex-wrap items-center gap-2 mb-4">
251
+ <div class="pw-dashboard-toolbar mb-4">
252
+ <div class="pw-toolbar-actions">
242
253
  <button data-action="start-all" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-state-success); color: var(--pw-text-inverse); font-family: var(--pw-font-sans);">
243
254
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>
244
- <span>Start All</span>
255
+ <span data-zh="全部启动" data-en="Start All">全部启动</span>
245
256
  </button>
246
257
  <button data-action="stop-all" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-state-error); color: #fff; font-family: var(--pw-font-sans);">
247
258
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="6" width="12" height="12" rx="1"/></svg>
248
- <span>Stop All</span>
259
+ <span data-zh="全部停止" data-en="Stop All">全部停止</span>
249
260
  </button>
250
261
  <button data-action="restart-all" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-state-warning); color: var(--pw-text-inverse); font-family: var(--pw-font-sans);">
251
262
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>
252
- <span>Restart All</span>
263
+ <span data-zh="全部重启" data-en="Restart All">全部重启</span>
253
264
  </button>
254
265
  <button data-action="refresh" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-bg-surface); color: var(--pw-text-primary); border: 1px solid var(--pw-border); font-family: var(--pw-font-sans);">
255
266
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>
256
- <span>Refresh</span>
267
+ <span data-zh="刷新" data-en="Refresh">刷新</span>
257
268
  </button>
258
- <div class="flex-1 min-w-0"></div>
269
+ </div>
270
+ <div class="pw-toolbar-repo flex items-center gap-2 px-3 py-1.5 rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); font-family: var(--pw-font-sans);">
271
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--pw-text-tertiary);"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2z"/></svg>
272
+ <select id="repo-selector" class="bg-transparent text-sm outline-none cursor-pointer" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans); min-width: 180px;">
273
+ <option value=".">工作区根目录</option>
274
+ </select>
275
+ </div>
276
+ <div class="pw-toolbar-links">
259
277
  <button data-action="open-core-tunnel" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: transparent; color: var(--pw-accent); border: 1px solid var(--pw-accent); font-family: var(--pw-font-sans);">
260
278
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
261
- <span>Open Core Tunnel UI</span>
279
+ <span data-zh="Core 隧道" data-en="Open Core Tunnel UI">Core 隧道</span>
262
280
  </button>
263
281
  <button data-action="open-direct-tunnel" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: transparent; color: var(--pw-accent); border: 1px solid var(--pw-accent); font-family: var(--pw-font-sans);">
264
282
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
265
- <span>Open Direct Tunnel UI</span>
283
+ <span data-zh="Direct 隧道" data-en="Open Direct Tunnel UI">Direct 隧道</span>
266
284
  </button>
267
285
  <button data-action="open-logs-folder" class="flex items-center gap-1.5 px-3.5 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-bg-surface); color: var(--pw-text-primary); border: 1px solid var(--pw-border); font-family: var(--pw-font-sans);">
268
286
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2z"/></svg>
269
- <span>Open Logs Folder</span>
287
+ <span data-zh="日志目录" data-en="Open Logs Folder">日志目录</span>
270
288
  </button>
289
+ </div>
271
290
  </div>
272
291
 
273
292
  <!-- ===== ERROR BANNER ===== -->
274
293
  <div id="error-banner" class="hidden mb-4 rounded-lg px-4 py-3 text-sm" style="background-color: var(--pw-state-error-bg); border: 1px solid var(--pw-state-error); color: var(--pw-state-error); font-family: var(--pw-font-sans);"></div>
275
294
 
295
+ <!-- ===== LIVE READINESS CARD ===== -->
296
+ <div class="mb-2"><h2 class="text-xs font-semibold uppercase tracking-wide" style="color:var(--pw-text-tertiary);" data-zh="运行状态" data-en="Live status">运行状态</h2></div>
297
+ <div id="health-score-card" class="rounded-lg mb-4" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
298
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
299
+ <h2 class="text-sm font-semibold truncate" data-zh="实时就绪状态" data-en="Live Readiness" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">实时就绪状态</h2>
300
+ <span id="health-score-badge" class="text-xs font-semibold px-2.5 py-1 rounded-full whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover);">—</span>
301
+ </div>
302
+ <div id="health-score-body" class="px-4 py-3 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
303
+ <div class="text-xs" style="color: var(--pw-text-tertiary);">正在加载…</div>
304
+ </div>
305
+ </div>
306
+
276
307
  <!-- ===== SETUP CHECKLIST ===== -->
277
308
  <div id="setup-checklist-card" class="rounded-lg mb-4" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
278
309
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
279
- <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">Setup checklist</h2>
280
- <span id="setup-checklist-summary" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">checking</span>
310
+ <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">启动准备</h2>
311
+ <span id="setup-checklist-summary" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">正在检查</span>
281
312
  </div>
282
313
  <div id="setup-checklist-body" class="grid grid-cols-1 md:grid-cols-3 gap-0"></div>
283
314
  </div>
@@ -296,6 +327,7 @@ html.dark {
296
327
  </div>
297
328
 
298
329
  <!-- ===== HEALTH SUGGESTIONS CARD ===== -->
330
+ <div class="mb-2"><h2 class="text-xs font-semibold uppercase tracking-wide" style="color:var(--pw-text-tertiary);" data-zh="待处理项" data-en="Needs attention">待处理项</h2></div>
299
331
  <div id="suggestions-card" class="hidden rounded-lg mb-6" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
300
332
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
301
333
  <h2 class="text-sm font-semibold truncate" data-zh="健康建议" data-en="Health Suggestions" style="color: var(--pw-text-primary);">健康建议</h2>
@@ -305,35 +337,45 @@ html.dark {
305
337
  </div>
306
338
 
307
339
  <!-- ===== V1.5 RELEASE / POLICY / LINEAGE / EVIDENCE OVERVIEW ===== -->
340
+ <div class="mb-2 flex items-center justify-between gap-3"><h2 class="text-xs font-semibold uppercase tracking-wide" style="color:var(--pw-text-tertiary);" data-zh="历史概览" data-en="History overview">历史概览</h2><span class="text-xs" style="color:var(--pw-text-tertiary);" data-zh="历史任务、审计和发布检查不影响实时健康" data-en="History, audits, and release checks do not affect live health">历史任务、审计和发布检查不影响实时健康</span></div>
308
341
  <div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
309
342
  <div class="rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
310
343
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
311
- <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">Lineage</h2>
344
+ <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">任务链路</h2>
312
345
  <span id="lineage-count" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">...</span>
313
346
  </div>
314
- <div id="lineage-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">Loading</div>
347
+ <div id="lineage-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">正在加载…</div>
315
348
  </div>
316
349
  <div class="rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
317
350
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
318
- <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">Project Policy</h2>
351
+ <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">项目策略</h2>
319
352
  <span id="policy-status" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">...</span>
320
353
  </div>
321
- <div id="policy-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">Loading</div>
354
+ <div id="policy-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">正在加载…</div>
322
355
  </div>
323
356
  <div class="rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
324
357
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
325
- <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">Release</h2>
358
+ <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">发布检查</h2>
326
359
  <span id="release-status" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">...</span>
327
360
  </div>
328
- <div id="release-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">Loading</div>
361
+ <div id="release-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">正在加载…</div>
329
362
  </div>
330
363
  <div id="evidence-pack-card" class="rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
331
364
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
332
- <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">Evidence Pack</h2>
365
+ <h2 class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">证据包</h2>
333
366
  <span id="evidence-pack-status" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">...</span>
334
367
  </div>
335
- <div id="evidence-pack-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">Loading</div>
368
+ <div id="evidence-pack-overview-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">正在加载…</div>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- ===== STALE TASKS CARD ===== -->
373
+ <div id="stale-tasks-card" class="hidden rounded-lg mb-6" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); border-left: 3px solid var(--pw-state-warning);">
374
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
375
+ <h2 class="text-sm font-semibold truncate" data-zh="过期任务" data-en="Stale Tasks" style="color: var(--pw-text-primary);">过期任务</h2>
376
+ <span id="stale-tasks-count" class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">—</span>
336
377
  </div>
378
+ <div id="stale-tasks-body" class="px-4 py-3 text-sm" style="color: var(--pw-text-tertiary);">正在加载…</div>
337
379
  </div>
338
380
 
339
381
  <!-- ===== TWO-COLUMN LAYOUT ===== -->
@@ -352,8 +394,12 @@ html.dark {
352
394
 
353
395
  <!-- RIGHT COLUMN: 系统状态 (2/5 width) -->
354
396
  <div class="lg:col-span-2 rounded-lg" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
355
- <div class="px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
397
+ <div class="flex items-center justify-between gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
356
398
  <h2 class="text-sm font-semibold truncate" data-zh="系统状态" data-en="System Status" style="color: var(--pw-text-primary);">系统状态</h2>
399
+ <button id="btn-copy-diagnostics" class="flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium transition-colors whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);" type="button" title="Copy diagnostics to clipboard">
400
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
401
+ <span data-zh="复制诊断" data-en="Copy diagnostics">复制诊断</span>
402
+ </button>
357
403
  </div>
358
404
  <div id="system-status-body" class="px-4 py-2">
359
405
  <div class="py-3 text-center text-sm" style="color: var(--pw-text-tertiary);">加载中…</div>
@@ -366,7 +412,7 @@ html.dark {
366
412
  <div class="rounded-lg mb-6" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
367
413
  <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
368
414
  <h2 class="text-sm font-semibold truncate" data-zh="活动日志" data-en="Activity Log" style="color: var(--pw-text-primary);">活动日志</h2>
369
- <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">Core + Direct stdout 尾部</span>
415
+ <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">Core + Direct 输出摘要</span>
370
416
  </div>
371
417
  <div id="activity-log-body">
372
418
  <div class="px-4 py-3 text-xs" style="color: var(--pw-text-tertiary);">加载日志中…</div>
@@ -384,6 +430,17 @@ html.dark {
384
430
  </div>
385
431
  </div>
386
432
 
433
+ <!-- ===== SAFE MODAL ===== -->
434
+ <div id="safe-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: rgba(0,0,0,0.6);">
435
+ <div class="rounded-lg w-full max-w-2xl max-h-[80vh] flex flex-col" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border); box-shadow: var(--pw-shadow-modal);">
436
+ <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
437
+ <h3 id="safe-modal-title" class="text-sm font-semibold truncate" style="color: var(--pw-text-primary);">—</h3>
438
+ <button id="safe-modal-close" class="text-lg leading-none" style="color: var(--pw-text-tertiary);" type="button">×</button>
439
+ </div>
440
+ <div id="safe-modal-body" class="flex-1 overflow-auto px-4 py-3 text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono); white-space: pre-wrap; word-break: break-all;"></div>
441
+ </div>
442
+ </div>
443
+
387
444
  </main>
388
445
 
389
446
  </div>
@@ -477,6 +534,44 @@ html.dark {
477
534
  @keyframes pw-toast-out {
478
535
  to { opacity: 0; transform: translateX(20px); }
479
536
  }
537
+ /* Quick action icon buttons */
538
+ .pw-quick-btn {
539
+ display: inline-flex;
540
+ align-items: center;
541
+ justify-content: center;
542
+ width: 24px;
543
+ height: 24px;
544
+ border-radius: 4px;
545
+ border: 1px solid var(--pw-border-subtle);
546
+ background-color: var(--pw-bg-hover);
547
+ color: var(--pw-text-secondary);
548
+ cursor: pointer;
549
+ transition: background-color var(--pw-transition-fast), color var(--pw-transition-fast);
550
+ text-decoration: none;
551
+ }
552
+ .pw-quick-btn:hover {
553
+ background-color: var(--pw-bg-active);
554
+ color: var(--pw-text-primary);
555
+ opacity: 1;
556
+ }
557
+ /* Acceptance status badge - more prominent than status */
558
+ .pw-acceptance-badge {
559
+ font-size: var(--pw-text-sm);
560
+ font-weight: var(--pw-weight-bold);
561
+ padding: 2px 8px;
562
+ border-width: 1px;
563
+ border-style: solid;
564
+ }
565
+ /* Stale task explanation - prominent */
566
+ .pw-stale-explanation {
567
+ font-size: var(--pw-text-sm);
568
+ font-weight: var(--pw-weight-medium);
569
+ color: var(--pw-text-primary);
570
+ padding: 6px 10px;
571
+ border-radius: var(--pw-radius-sm);
572
+ background-color: var(--pw-state-warning-bg);
573
+ border-left: 2px solid var(--pw-state-warning);
574
+ }
480
575
  </style>
481
576
 
482
577
  <script>
@@ -485,6 +580,7 @@ html.dark {
485
580
 
486
581
  let controlToken = null;
487
582
  let statusTimer = null;
583
+ let currentRepoPath = ".";
488
584
 
489
585
  // ---------- Utilities ----------
490
586
  function $(id) { return document.getElementById(id); }
@@ -656,26 +752,26 @@ html.dark {
656
752
  {
657
753
  label: 'tunnel-client.exe',
658
754
  ok: !!tunnel.available,
659
- value: tunnel.available ? (tunnel.path || 'available') : 'missing',
660
- hint: tunnel.available ? 'Ready for Web UI and tray start/restart.' : 'Expected at TUNNEL_CLIENT_EXE, PATCHWARDEN_TUNNEL_CLIENT_EXE, PATH, or the saved D:\\ai_agent tunnel-client folder.'
755
+ value: tunnel.available ? (tunnel.path || '已找到') : '未找到',
756
+ hint: tunnel.available ? '已可供桌面启动和重启使用。' : '请在设置中自动检测或选择 tunnel-client.exe。'
661
757
  },
662
758
  {
663
- label: 'workspaceRoot',
759
+ label: '工作区',
664
760
  ok: !!workspaceRoot,
665
- value: workspaceRoot || 'not loaded',
666
- hint: 'This is the workspace currently managed by PatchWarden tasks.'
761
+ value: workspaceRoot || '未加载',
762
+ hint: 'PatchWarden 任务仅在此工作区内运行。'
667
763
  },
668
764
  {
669
- label: 'Watcher',
765
+ label: '任务监视器',
670
766
  ok: watcher.status === 'healthy',
671
- value: watcher.status || 'missing',
672
- hint: watcher.status === 'healthy' ? 'Task execution watcher is healthy.' : (watcher.reason || 'Start Core/Watcher when you want queued tasks to execute.')
767
+ value: watcher.status || '未运行',
768
+ hint: watcher.status === 'healthy' ? '任务监视器运行正常。' : (watcher.reason || '启动 Core 后,任务监视器会处理排队任务。')
673
769
  }
674
770
  ];
675
771
 
676
772
  const missingCount = items.filter(function (item) { return !item.ok; }).length;
677
773
  if (summary) {
678
- summary.textContent = missingCount === 0 ? 'ready' : (missingCount + ' needs attention');
774
+ summary.textContent = missingCount === 0 ? '已就绪' : (missingCount + ' 项需要处理');
679
775
  summary.style.color = missingCount === 0 ? 'var(--pw-state-success)' : 'var(--pw-state-warning)';
680
776
  }
681
777
 
@@ -688,7 +784,7 @@ html.dark {
688
784
  + '<div class="flex items-center gap-2 mb-1">'
689
785
  + dotHtml(kind)
690
786
  + '<span class="text-xs font-semibold whitespace-nowrap" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(item.label) + '</span>'
691
- + '<span class="ml-auto text-xs px-2 py-0.5 rounded-full" style="color: ' + c.color + '; background-color: ' + c.bg + ';">' + (item.ok ? 'ok' : 'check') + '</span>'
787
+ + '<span class="ml-auto text-xs px-2 py-0.5 rounded-full" style="color: ' + c.color + '; background-color: ' + c.bg + ';">' + (item.ok ? '正常' : '处理') + '</span>'
692
788
  + '</div>'
693
789
  + '<div class="text-xs truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);" title="' + escapeHtml(item.value) + '">' + escapeHtml(item.value) + '</div>'
694
790
  + '<div class="text-xs mt-1" style="color: var(--pw-text-tertiary);">' + escapeHtml(item.hint) + '</div>'
@@ -866,9 +962,11 @@ html.dark {
866
962
  let data = null;
867
963
  try { data = await res.json(); } catch (_) { /* ignore */ }
868
964
  if (!res.ok) {
869
- const msg = (data && (data.error || data.reason || data.message)) || ('HTTP ' + res.status);
965
+ var msg = (data && (data.error || data.reason || data.message)) || ('HTTP ' + res.status);
966
+ if (data && Array.isArray(data.next_steps) && data.next_steps.length) msg += ' 下一步:' + data.next_steps.join(';');
870
967
  throw new Error(msg);
871
968
  }
969
+ if (data && data.ok === false) throw new Error(data.error || data.reason || '操作未完成');
872
970
  return data;
873
971
  } finally {
874
972
  if (btnEl) {
@@ -878,6 +976,155 @@ html.dark {
878
976
  }
879
977
  }
880
978
 
979
+ // ---------- Clipboard + Safe Modal ----------
980
+ function copyToClipboard(text, label) {
981
+ if (!text) return;
982
+ if (navigator.clipboard && navigator.clipboard.writeText) {
983
+ navigator.clipboard.writeText(text).then(function () {
984
+ showToast('success', '已复制', (label || '内容') + '已复制到剪贴板');
985
+ }).catch(function () {
986
+ showToast('error', '复制失败', '请手动复制');
987
+ });
988
+ } else {
989
+ showToast('warning', '复制不支持', '浏览器不支持剪贴板 API');
990
+ }
991
+ }
992
+
993
+ // Format diagnostics response as plain text for clipboard.
994
+ function formatDiagnosticsText(d) {
995
+ if (!d || typeof d !== 'object') return '';
996
+ const lines = [];
997
+ lines.push('PatchWarden Diagnostics');
998
+ lines.push('======================');
999
+ lines.push('server_version: ' + (d.server_version != null ? d.server_version : '—'));
1000
+ lines.push('schema_epoch: ' + (d.schema_epoch != null ? d.schema_epoch : '—'));
1001
+ lines.push('tool_manifest_sha256: ' + (d.tool_manifest_sha256 != null ? d.tool_manifest_sha256 : '—'));
1002
+ lines.push('watcher_status: ' + (d.watcher_status != null ? d.watcher_status : '—'));
1003
+ lines.push('tunnel_core_ready: ' + (d.tunnel_core_ready === true ? 'true' : 'false'));
1004
+ lines.push('tunnel_direct_ready: ' + (d.tunnel_direct_ready === true ? 'true' : 'false'));
1005
+ lines.push('core_tool_count: ' + (d.core_tool_count != null ? d.core_tool_count : '—'));
1006
+ lines.push('direct_tool_count: ' + (d.direct_tool_count != null ? d.direct_tool_count : '—'));
1007
+ lines.push('direct_profile_enabled: ' + (d.direct_profile_enabled === true ? 'true' : 'false'));
1008
+ lines.push('workspace_root: ' + (d.workspace_root != null ? d.workspace_root : '—'));
1009
+ lines.push('');
1010
+ const agents = Array.isArray(d.agent_status) ? d.agent_status : [];
1011
+ lines.push('Agents:');
1012
+ if (agents.length === 0) {
1013
+ lines.push(' (none)');
1014
+ } else {
1015
+ agents.forEach(function (a) {
1016
+ lines.push(' - ' + (a.name || '?') + ': ' + (a.available ? 'available' : 'unavailable'));
1017
+ });
1018
+ }
1019
+ lines.push('');
1020
+ const failures = Array.isArray(d.recent_failures) ? d.recent_failures : [];
1021
+ lines.push('Recent failures:');
1022
+ if (failures.length === 0) {
1023
+ lines.push(' (none)');
1024
+ } else {
1025
+ failures.forEach(function (f) {
1026
+ lines.push(' - task_id: ' + (f.task_id || '?') + ', status: ' + (f.status || '?'));
1027
+ });
1028
+ }
1029
+ return lines.join('\n');
1030
+ }
1031
+
1032
+ async function copyDiagnostics(btnEl) {
1033
+ const orig = btnEl ? btnEl.innerHTML : null;
1034
+ if (btnEl) {
1035
+ btnEl.disabled = true;
1036
+ btnEl.innerHTML =
1037
+ '<span class="inline-block w-3 h-3 align-middle border-2 border-current border-t-transparent rounded-full pw-spin"></span>'
1038
+ + '<span class="ml-1.5">处理中…</span>';
1039
+ }
1040
+ try {
1041
+ const data = await fetchJson('/api/diagnostics');
1042
+ const text = formatDiagnosticsText(data);
1043
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1044
+ await navigator.clipboard.writeText(text);
1045
+ showToast('success', '诊断信息已复制到剪贴板', 'PatchWarden Diagnostics 已复制到剪贴板');
1046
+ } else {
1047
+ showToast('warning', '复制不支持', '浏览器不支持剪贴板 API');
1048
+ }
1049
+ } catch (err) {
1050
+ showToast('error', '诊断复制失败', err.message);
1051
+ } finally {
1052
+ if (btnEl) {
1053
+ btnEl.disabled = false;
1054
+ btnEl.innerHTML = orig;
1055
+ }
1056
+ }
1057
+ }
1058
+
1059
+ function showSafeModal(title, data) {
1060
+ const modal = $('safe-modal');
1061
+ const titleEl = $('safe-modal-title');
1062
+ const bodyEl = $('safe-modal-body');
1063
+ if (!modal || !titleEl || !bodyEl) return;
1064
+ titleEl.textContent = title || '—';
1065
+ let content;
1066
+ if (data && typeof data === 'object') {
1067
+ content = JSON.stringify(data, null, 2);
1068
+ } else {
1069
+ content = String(data || '');
1070
+ }
1071
+ bodyEl.textContent = content;
1072
+ modal.classList.remove('hidden');
1073
+ }
1074
+
1075
+ function closeSafeModal() {
1076
+ const modal = $('safe-modal');
1077
+ if (modal) modal.classList.add('hidden');
1078
+ }
1079
+
1080
+ async function fetchSafeSummary(taskId, kind, title) {
1081
+ if (!taskId) return;
1082
+ try {
1083
+ const data = await fetchJson('/api/tasks/' + encodeURIComponent(taskId) + '/' + kind);
1084
+ showSafeModal(title, data);
1085
+ } catch (err) {
1086
+ showError('加载 ' + title + ' 失败: ' + err.message);
1087
+ }
1088
+ }
1089
+
1090
+ // ---------- Task status priority ----------
1091
+ function primaryStatus(task) {
1092
+ if (!task) return { key: '', label: '—', kind: 'neutral' };
1093
+ const priority = [
1094
+ 'failed_policy_violation',
1095
+ 'failed_scope_violation',
1096
+ 'failed_verification',
1097
+ 'needs_fix',
1098
+ 'ready_for_review',
1099
+ 'done_by_agent',
1100
+ 'running',
1101
+ 'pending'
1102
+ ];
1103
+ const status = task.status || '';
1104
+ const acceptance = task.acceptance_status || '';
1105
+ const statusIdx = priority.indexOf(status);
1106
+ const acceptanceIdx = priority.indexOf(acceptance);
1107
+ let chosen = '';
1108
+ if (statusIdx >= 0 && acceptanceIdx >= 0) {
1109
+ chosen = statusIdx <= acceptanceIdx ? status : acceptance;
1110
+ } else if (statusIdx >= 0) {
1111
+ chosen = status;
1112
+ } else if (acceptanceIdx >= 0) {
1113
+ chosen = acceptance;
1114
+ } else {
1115
+ chosen = acceptance || status || '';
1116
+ }
1117
+ return { key: chosen, label: chosen || '—', kind: statusKind(chosen || status) };
1118
+ }
1119
+
1120
+ function verificationHeadline(t) {
1121
+ if (!t) return '—';
1122
+ if (t.verification && t.verification.headline) return t.verification.headline;
1123
+ if (t.safe_result && t.safe_result.verification && t.safe_result.verification.headline) return t.safe_result.verification.headline;
1124
+ if (t.verification_headline) return t.verification_headline;
1125
+ return '—';
1126
+ }
1127
+
881
1128
  // ---------- Render: Service Card ----------
882
1129
  function renderServiceCard(prefix, label, status) {
883
1130
  const cardEl = $('card-' + prefix);
@@ -938,11 +1185,12 @@ html.dark {
938
1185
  const tunnel = (status && status.tunnel) || {};
939
1186
  const tools = (status && status.tools && status.tools.core) || {};
940
1187
  const agents = (status && status.agents) || [];
1188
+ const directProfileEnabled = !!(status && status.direct_profile_enabled);
941
1189
 
942
1190
  const watcherKind = statusKind(watcher.status);
943
1191
  const coreReady = !!(tunnel.core && tunnel.core.ready);
944
1192
  const directReady = !!(tunnel.direct && tunnel.direct.ready);
945
- const tunnelReady = coreReady && directReady;
1193
+ const tunnelReady = coreReady && (!directProfileEnabled || directReady);
946
1194
  const tunnelKind = tunnelReady ? 'success' : (coreReady || directReady ? 'warning' : 'error');
947
1195
 
948
1196
  let agentsHtml = '';
@@ -972,7 +1220,7 @@ html.dark {
972
1220
  row('Watcher',
973
1221
  '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + watcherText + '</span>',
974
1222
  watcherKind)
975
- + row('Tunnel ready',
1223
+ + row('隧道就绪',
976
1224
  '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">core ' + (coreReady ? '✓' : '✗') + ' / direct ' + (directReady ? '✓' : '✗') + '</span>',
977
1225
  tunnelKind)
978
1226
  + row('schema_epoch',
@@ -1000,21 +1248,77 @@ html.dark {
1000
1248
  bodyEl.innerHTML = '<div class="px-4 py-6 text-center text-sm" style="color: var(--pw-text-tertiary);">暂无任务</div>';
1001
1249
  return;
1002
1250
  }
1251
+ const titleMap = {
1252
+ 'safe-result': 'Safe Result',
1253
+ 'safe-audit': 'Safe Audit',
1254
+ 'safe-test-summary': 'Safe Test Summary',
1255
+ 'safe-diff-summary': 'Safe Diff Summary'
1256
+ };
1003
1257
  let html = '';
1004
1258
  top.forEach(function (t) {
1005
- const kind = statusKind(t.status);
1006
- const c = statusColors(kind);
1007
- const href = '/pages/task-detail.html?id=' + encodeURIComponent(t.task_id || '');
1259
+ const taskId = t.task_id || '';
1260
+ const href = '/pages/task-detail.html?id=' + encodeURIComponent(taskId);
1261
+ const sKind = statusKind(t.status);
1262
+ const sColor = statusColors(sKind);
1263
+ const aKind = statusKind(t.acceptance_status);
1264
+ const aColor = statusColors(aKind);
1265
+ // Skip status badge when redundant with acceptance (done + done_by_agent)
1266
+ const skipStatusBadge = (t.status === 'done' && t.acceptance_status === 'done_by_agent');
1267
+ const headline = verificationHeadline(t);
1268
+ const updated = t.updated_at || t.created_at || '';
1269
+
1008
1270
  html +=
1009
- '<a href="' + href + '" data-task-row class="flex items-center gap-3 px-4 py-3 transition-colors cursor-pointer" style="border-bottom: 1px solid var(--pw-border-subtle);">'
1010
- + '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap" style="color: var(--pw-text-secondary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);" title="' + escapeHtml(t.task_id || '') + '">' + escapeHtml(shortId(t.task_id)) + '</span>'
1011
- + '<span class="flex-1 min-w-0 text-sm truncate" style="color: var(--pw-text-primary);" title="' + escapeHtml(t.title || '') + '">' + escapeHtml(t.title || '—') + '</span>'
1012
- + '<span class="shrink-0 text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);" title="phase">' + escapeHtml(t.phase || '') + '</span>'
1013
- + '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap" style="color: ' + c.color + '; background-color: ' + c.bg + ';">' + escapeHtml(t.status || '—') + '</span>'
1014
- + '<span class="shrink-0 text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">' + escapeHtml(fmtTime(t.created_at)) + '</span>'
1015
- + '</a>';
1271
+ '<div data-task-row class="px-4 py-3 transition-colors" style="border-bottom: 1px solid var(--pw-border-subtle);">'
1272
+ + '<div class="flex items-center gap-2 flex-wrap">'
1273
+ // short task_id + copy button
1274
+ + '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap" style="color: var(--pw-text-secondary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);" title="' + escapeHtml(taskId) + '">' + escapeHtml(shortId(taskId)) + '</span>'
1275
+ + '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(taskId) + '" title="复制 task_id" type="button">'
1276
+ + '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>'
1277
+ + '</button>'
1278
+ // title (truncated, link to detail)
1279
+ + '<a href="' + href + '" class="flex-1 min-w-0 text-sm truncate hover:underline" style="color: var(--pw-text-primary);" title="' + escapeHtml(t.title || '') + '">' + escapeHtml(t.title || '—') + '</a>'
1280
+ // agent badge
1281
+ + (t.agent ? badgeHtml(t.agent, 'info') : '')
1282
+ // status badge (skip if redundant)
1283
+ + (skipStatusBadge ? '' : '<span class="shrink-0 text-xs font-medium px-2 py-0.5 rounded-full whitespace-nowrap" style="color: ' + sColor.color + '; background-color: ' + sColor.bg + ';">' + escapeHtml(t.status || '—') + '</span>')
1284
+ // acceptance badge (more prominent)
1285
+ + (t.acceptance_status ? '<span class="shrink-0 pw-acceptance-badge rounded-full whitespace-nowrap" style="color: ' + aColor.color + '; background-color: ' + aColor.bg + '; border-color: ' + aColor.color + ';">' + escapeHtml(t.acceptance_status) + '</span>' : '')
1286
+ + '</div>'
1287
+ // second line: repo_path, verification headline, updated_at, next_action
1288
+ + '<div class="flex items-center gap-3 mt-1.5 flex-wrap">'
1289
+ + '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono); max-width: 200px;" title="' + escapeHtml(t.repo_path || '') + '">' + escapeHtml(t.repo_path || '—') + '</span>'
1290
+ + '<span class="text-xs truncate" style="color: var(--pw-text-secondary); max-width: 280px;" title="' + escapeHtml(headline) + '">verify: ' + escapeHtml(headline) + '</span>'
1291
+ + '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);">' + escapeHtml(fmtTime(updated)) + '</span>'
1292
+ + (t.next_action ? '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); max-width: 200px;" title="' + escapeHtml(t.next_action) + '">next: ' + escapeHtml(t.next_action) + '</span>' : '')
1293
+ + '</div>'
1294
+ // quick action buttons
1295
+ + '<div class="flex items-center gap-1 mt-1.5">'
1296
+ + '<button class="pw-quick-btn" data-safe-action="safe-result" data-task-id="' + escapeHtml(taskId) + '" title="safe result" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></button>'
1297
+ + '<button class="pw-quick-btn" data-safe-action="safe-audit" data-task-id="' + escapeHtml(taskId) + '" title="safe audit" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg></button>'
1298
+ + '<button class="pw-quick-btn" data-safe-action="safe-test-summary" data-task-id="' + escapeHtml(taskId) + '" title="safe test summary" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></button>'
1299
+ + '<button class="pw-quick-btn" data-safe-action="safe-diff-summary" data-task-id="' + escapeHtml(taskId) + '" title="safe diff summary" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg></button>'
1300
+ + '<a href="' + href + '" class="pw-quick-btn" title="打开详情"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>'
1301
+ + '</div>'
1302
+ + '</div>';
1016
1303
  });
1017
1304
  bodyEl.innerHTML = html;
1305
+ // Wire quick action buttons
1306
+ bodyEl.querySelectorAll('[data-safe-action]').forEach(function (btn) {
1307
+ btn.addEventListener('click', function (e) {
1308
+ e.preventDefault();
1309
+ e.stopPropagation();
1310
+ const action = btn.getAttribute('data-safe-action');
1311
+ const tid = btn.getAttribute('data-task-id');
1312
+ fetchSafeSummary(tid, action, titleMap[action] || action);
1313
+ });
1314
+ });
1315
+ bodyEl.querySelectorAll('[data-copy-id]').forEach(function (btn) {
1316
+ btn.addEventListener('click', function (e) {
1317
+ e.preventDefault();
1318
+ e.stopPropagation();
1319
+ copyToClipboard(btn.getAttribute('data-copy-id'), 'task_id');
1320
+ });
1321
+ });
1018
1322
  }
1019
1323
 
1020
1324
  // ---------- Render: Activity Log ----------
@@ -1054,7 +1358,7 @@ html.dark {
1054
1358
 
1055
1359
  el.innerHTML =
1056
1360
  '<details class="px-4 py-3">'
1057
- + '<summary class="cursor-pointer text-xs font-medium" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">Show Core / Direct log tails</summary>'
1361
+ + '<summary class="cursor-pointer text-xs font-medium" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">查看 Core / Direct 本次日志尾部</summary>'
1058
1362
  + '<pre class="mt-3 text-xs overflow-auto m-0" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono); white-space: pre-wrap; word-break: break-all; max-height: 320px;">' + escapeHtml(merged) + '</pre>'
1059
1363
  + '</details>';
1060
1364
  }
@@ -1069,49 +1373,262 @@ html.dark {
1069
1373
  const lineages = Array.isArray(data.lineages) ? data.lineages : [];
1070
1374
  if (count) count.textContent = String(data.total || lineages.length);
1071
1375
  if (!lineages.length) {
1072
- body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">No loop lineage yet</div>';
1376
+ body.innerHTML =
1377
+ '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">暂无任务链路</div>'
1378
+ + '<div class="flex items-center gap-2">'
1379
+ + '<a href="/tasks.html" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">创建受控任务链</a>'
1380
+ + '<a href="/tasks.html" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">查看最近运行</a>'
1381
+ + '</div>';
1073
1382
  return;
1074
1383
  }
1075
1384
  const top = lineages[0];
1076
- const directSessions = top.tasks && Array.isArray(top.tasks.direct_sessions) ? top.tasks.direct_sessions : [];
1077
- const direct = directSessions.length ? directSessions[0] : null;
1078
- const worktree = top.worktree || {};
1079
- const directStatus = direct
1080
- ? ((direct.status || 'unknown') + ' / audit ' + (direct.audit_decision || 'not_run'))
1385
+ const dv = top.direct_verification || null;
1386
+ const directStatus = dv
1387
+ ? (escapeHtml(dv.status || 'unknown') + ' / audit ' + escapeHtml(dv.audit_decision || 'not_run'))
1081
1388
  : 'not run';
1389
+ const mainCount = top.main_task_count != null ? top.main_task_count : (top.tasks && top.tasks.main ? 1 : 0);
1390
+ const fixCount = top.fix_task_count != null ? top.fix_task_count : (top.tasks && Array.isArray(top.tasks.fix) ? top.tasks.fix.length : 0);
1391
+ const cleanupCount = top.cleanup_task_count != null ? top.cleanup_task_count : (top.tasks && Array.isArray(top.tasks.cleanup) ? top.tasks.cleanup.length : 0);
1392
+ const iterCount = top.iterations != null ? top.iterations : (Array.isArray(top.rounds) ? top.rounds.length : 0);
1393
+ const warnCount = top.warnings_count != null ? top.warnings_count : (Array.isArray(top.warnings) ? top.warnings.length : 0);
1082
1394
  body.innerHTML =
1083
1395
  '<div class="flex items-center justify-between gap-2 mb-2">'
1084
1396
  + '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(top.lineage_id || '') + '">' + escapeHtml(shortId(top.lineage_id)) + '</span>'
1085
- + badgeHtml(top.stop_reason || 'unknown', statusKind(top.final_status))
1397
+ + badgeHtml(top.final_status || 'unknown', statusKind(top.final_status))
1086
1398
  + '</div>'
1087
1399
  + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-secondary);" title="' + escapeHtml(top.goal || '') + '">' + escapeHtml(top.goal || 'No goal') + '</div>'
1088
- + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">Isolation: ' + escapeHtml(worktree.isolation_mode || 'current_repo') + '</div>'
1089
- + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(direct && direct.session_id ? direct.session_id : '') + '">Direct: ' + escapeHtml(directStatus) + '</div>'
1090
- + '<div class="text-xs" style="color: var(--pw-text-tertiary);">Next: ' + escapeHtml(top.next_action || 'none') + '</div>';
1400
+ + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">stop_reason: ' + escapeHtml(top.stop_reason || 'unknown') + '</div>'
1401
+ + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);">iterations: ' + escapeHtml(String(iterCount)) + ' · main/fix/cleanup: ' + escapeHtml(String(mainCount)) + '/' + escapeHtml(String(fixCount)) + '/' + escapeHtml(String(cleanupCount)) + '</div>'
1402
+ + '<div class="text-xs truncate mb-1" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(dv && dv.session_id ? dv.session_id : '') + '">direct: ' + directStatus + '</div>'
1403
+ + '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">warnings: ' + escapeHtml(String(warnCount)) + '</div>'
1404
+ + '<div class="flex items-center gap-2">'
1405
+ + '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-lineage-detail="' + escapeHtml(top.lineage_id || '') + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">View detail</button>'
1406
+ + '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(top.lineage_id || '') + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
1407
+ + '</div>';
1408
+ // Wire lineage detail button
1409
+ const detailBtn = body.querySelector('[data-lineage-detail]');
1410
+ if (detailBtn) {
1411
+ detailBtn.addEventListener('click', function () {
1412
+ const lid = detailBtn.getAttribute('data-lineage-detail');
1413
+ if (lid) showLineageDetail(lid);
1414
+ });
1415
+ }
1416
+ // Wire copy button
1417
+ const copyBtn = body.querySelector('[data-copy-id]');
1418
+ if (copyBtn) {
1419
+ copyBtn.addEventListener('click', function (e) {
1420
+ e.preventDefault();
1421
+ e.stopPropagation();
1422
+ copyToClipboard(copyBtn.getAttribute('data-copy-id'), 'lineage_id');
1423
+ });
1424
+ }
1091
1425
  } catch (err) {
1092
1426
  if (count) count.textContent = 'error';
1093
1427
  body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
1094
1428
  }
1095
1429
  }
1096
1430
 
1431
+ async function showLineageDetail(lineageId) {
1432
+ if (!lineageId) return;
1433
+ const modal = $('safe-modal');
1434
+ const titleEl = $('safe-modal-title');
1435
+ const bodyEl = $('safe-modal-body');
1436
+ if (!modal || !titleEl || !bodyEl) return;
1437
+ titleEl.textContent = 'Lineage Detail · ' + shortId(lineageId);
1438
+ bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Loading…</div>';
1439
+ modal.classList.remove('hidden');
1440
+ try {
1441
+ const data = await fetchJson('/api/lineages/' + encodeURIComponent(lineageId));
1442
+ bodyEl.innerHTML = renderLineageDetail(data);
1443
+ } catch (err) {
1444
+ bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
1445
+ }
1446
+ }
1447
+
1448
+ function renderLineageDetail(d) {
1449
+ if (!d) return '<div class="text-xs" style="color: var(--pw-text-tertiary);">No data</div>';
1450
+ const rounds = Array.isArray(d.rounds) ? d.rounds : [];
1451
+ const mainRounds = rounds.filter(function (r) { return r.role === 'main'; });
1452
+ const fixRounds = rounds.filter(function (r) { return r.role === 'fix_tests'; });
1453
+ const cleanupRounds = rounds.filter(function (r) { return r.role === 'cleanup'; });
1454
+ const directSessions = (d.tasks && Array.isArray(d.tasks.direct_sessions)) ? d.tasks.direct_sessions : [];
1455
+
1456
+ function roundRow(r) {
1457
+ return '<div class="py-1.5" style="border-bottom: 1px solid var(--pw-border-subtle);">'
1458
+ + '<div class="flex items-center gap-2 mb-1 flex-wrap">'
1459
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(r.task_id || '') + '">' + escapeHtml(shortId(r.task_id)) + '</span>'
1460
+ + badgeHtml(r.status || '—', statusKind(r.status))
1461
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">iter ' + escapeHtml(String(r.iteration || 0)) + '</span>'
1462
+ + '</div>'
1463
+ + '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">verification: ' + escapeHtml(r.verification_status || '—') + '</div>'
1464
+ + '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">audit: ' + escapeHtml(r.audit_verdict || '—') + '</div>'
1465
+ + (r.next_action ? '<div class="text-xs" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(r.next_action) + '</div>' : '')
1466
+ + '</div>';
1467
+ }
1468
+
1469
+ function directRow(s) {
1470
+ return '<div class="py-1.5" style="border-bottom: 1px solid var(--pw-border-subtle);">'
1471
+ + '<div class="flex items-center gap-2 mb-1 flex-wrap">'
1472
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(s.session_id || '') + '">' + escapeHtml(shortId(s.session_id)) + '</span>'
1473
+ + badgeHtml(s.status || 'unknown', statusKind(s.status))
1474
+ + '</div>'
1475
+ + '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">audit: ' + escapeHtml(s.audit_decision || 'not_run') + '</div>'
1476
+ + '<div class="text-xs mb-0.5" style="color: var(--pw-text-secondary);">commands: ' + escapeHtml(String(s.command_count || 0)) + ' (passed ' + escapeHtml(String(s.passed_commands || 0)) + ' / failed ' + escapeHtml(String(s.failed_commands || 0)) + ')</div>'
1477
+ + (s.next_action ? '<div class="text-xs" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(s.next_action) + '</div>' : '')
1478
+ + '</div>';
1479
+ }
1480
+
1481
+ function section(title, items, renderFn) {
1482
+ if (!items || items.length === 0) return '';
1483
+ return '<div class="mt-3">'
1484
+ + '<div class="text-xs font-semibold mb-1" style="color: var(--pw-text-primary);">' + escapeHtml(title) + ' (' + items.length + ')</div>'
1485
+ + items.map(renderFn).join('')
1486
+ + '</div>';
1487
+ }
1488
+
1489
+ const header =
1490
+ '<div class="mb-2">'
1491
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">goal:</span> ' + escapeHtml(d.goal || '—') + '</div>'
1492
+ + '<div class="flex items-center gap-2 mb-1 flex-wrap">'
1493
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">final_status:</span>' + badgeHtml(d.final_status || 'unknown', statusKind(d.final_status))
1494
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">stop_reason:</span><span class="text-xs" style="color: var(--pw-text-secondary);">' + escapeHtml(d.stop_reason || '—') + '</span>'
1495
+ + '</div>'
1496
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">iterations: ' + escapeHtml(String(d.iterations != null ? d.iterations : rounds.length)) + ' · warnings: ' + escapeHtml(String(d.warnings_count != null ? d.warnings_count : (Array.isArray(d.warnings) ? d.warnings.length : 0))) + '</div>'
1497
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">main: ' + escapeHtml(String(d.main_task_count != null ? d.main_task_count : 0)) + ' · fix: ' + escapeHtml(String(d.fix_task_count != null ? d.fix_task_count : 0)) + ' · cleanup: ' + escapeHtml(String(d.cleanup_task_count != null ? d.cleanup_task_count : 0)) + '</div>'
1498
+ + '<div class="text-xs" style="color: var(--pw-text-tertiary);">final_recommended_next_action: ' + escapeHtml(d.next_action || '—') + '</div>'
1499
+ + '</div>';
1500
+
1501
+ return header
1502
+ + section('Main Tasks', mainRounds, roundRow)
1503
+ + section('Fix Tasks', fixRounds, roundRow)
1504
+ + section('Cleanup Tasks', cleanupRounds, roundRow)
1505
+ + section('Direct Verification', directSessions, directRow);
1506
+ }
1507
+
1508
+ // ---------- Live readiness ----------
1509
+ // The status route owns this classification. Historic task records remain
1510
+ // visible elsewhere and never change the live readiness result.
1511
+ function calculateHealthScore(status) {
1512
+ const experience = status && status.experience;
1513
+ if (!experience || !Array.isArray(experience.live_checks)) {
1514
+ return { level: 'blocked', dimensions: [] };
1515
+ }
1516
+ return {
1517
+ level: experience.readiness === 'ready' ? 'healthy' : experience.readiness,
1518
+ dimensions: experience.live_checks.map(function (check) {
1519
+ return { key: check.key, label: check.label, ok: check.state === 'ready' || check.state === 'optional', optional: check.state === 'optional', detail: check.detail };
1520
+ }),
1521
+ history: experience.historical_summary || null
1522
+ };
1523
+ }
1524
+
1525
+ function healthScoreColor(level) {
1526
+ if (level === 'healthy') return { color: 'var(--pw-state-success)', bg: 'var(--pw-state-success-bg)' };
1527
+ if (level === 'needs_action') return { color: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)' };
1528
+ if (level === 'warning') return { color: 'var(--pw-state-warning)', bg: 'var(--pw-state-warning-bg)' };
1529
+ if (level === 'degraded') return { color: '#f97316', bg: 'rgba(249, 115, 22, 0.1)' };
1530
+ if (level === 'blocked') return { color: 'var(--pw-state-error)', bg: 'var(--pw-state-error-bg)' };
1531
+ return { color: 'var(--pw-text-secondary)', bg: 'var(--pw-bg-hover)' };
1532
+ }
1533
+
1534
+ function renderHealthScore(score) {
1535
+ const card = $('health-score-card');
1536
+ const badge = $('health-score-badge');
1537
+ const body = $('health-score-body');
1538
+ if (!card || !badge || !body || !score) return;
1539
+
1540
+ const c = healthScoreColor(score.level);
1541
+ badge.textContent = score.level;
1542
+ badge.style.color = c.color;
1543
+ badge.style.backgroundColor = c.bg;
1544
+
1545
+ let html = '';
1546
+ score.dimensions.forEach(function (d) {
1547
+ const okColor = d.optional ? 'var(--pw-text-tertiary)' : (d.ok ? 'var(--pw-state-success)' : 'var(--pw-state-error)');
1548
+ const icon = d.optional ? '○' : (d.ok ? '✓' : '✗');
1549
+ html +=
1550
+ '<div class="flex items-center gap-2 px-2 py-1.5 rounded" style="background-color: var(--pw-bg-hover); border: 1px solid var(--pw-border-subtle);">'
1551
+ + '<span class="shrink-0 text-xs font-semibold" style="color: ' + okColor + '; font-family: var(--pw-font-mono);">' + icon + '</span>'
1552
+ + '<div class="min-w-0 flex-1">'
1553
+ + '<div class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">' + escapeHtml(d.label) + '</div>'
1554
+ + '<div class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono);" title="' + escapeHtml(d.detail) + '">' + escapeHtml(d.detail) + '</div>'
1555
+ + '</div>'
1556
+ + '</div>';
1557
+ });
1558
+ if (score.history) {
1559
+ html += '<div class="md:col-span-2 lg:col-span-4 text-xs px-2 py-1" style="color: var(--pw-text-tertiary);">历史摘要:' + escapeHtml(String(score.history.stale_tasks || 0)) + ' 个过期任务,' + escapeHtml(String(score.history.failed_tasks || 0)) + ' 个失败任务</div>';
1560
+ }
1561
+ body.innerHTML = html;
1562
+ }
1563
+
1564
+ async function refreshHealthScore() {
1565
+ const card = $('health-score-card');
1566
+ if (!card) return;
1567
+ try {
1568
+ const status = await fetchJson('/api/status');
1569
+ const score = calculateHealthScore(status);
1570
+ renderHealthScore(score);
1571
+ } catch (err) {
1572
+ const body = $('health-score-body');
1573
+ const badge = $('health-score-badge');
1574
+ if (body) body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">健康评分加载失败: ' + escapeHtml(err.message) + '</div>';
1575
+ if (badge) { badge.textContent = 'error'; badge.style.color = 'var(--pw-state-error)'; badge.style.backgroundColor = 'var(--pw-state-error-bg)'; }
1576
+ }
1577
+ }
1578
+
1097
1579
  async function refreshProjectPolicy() {
1098
1580
  const body = $('policy-overview-body');
1099
1581
  const status = $('policy-status');
1100
1582
  if (!body) return;
1101
1583
  try {
1102
- const data = await fetchJson('/api/project-policy?repo_path=.');
1584
+ const data = await fetchJson('/api/project-policy?repo_path=' + encodeURIComponent(currentRepoPath));
1103
1585
  const valid = data && data.valid !== false;
1104
- if (status) status.textContent = valid ? 'valid' : 'issues';
1586
+ if (status) {
1587
+ status.textContent = valid ? 'valid' : 'issues';
1588
+ status.style.color = valid ? 'var(--pw-state-success)' : 'var(--pw-state-error)';
1589
+ }
1105
1590
  const issueCount = Array.isArray(data.issues) ? data.issues.length : 0;
1106
1591
  const policy = data.effective_policy || {};
1107
1592
  const release = (policy.release_mode || {});
1593
+
1594
+ // auto_cleanup: boolean or object with enabled field
1595
+ const autoCleanupRaw = policy.auto_cleanup;
1596
+ let autoCleanupEnabled = false;
1597
+ if (autoCleanupRaw === true) autoCleanupEnabled = true;
1598
+ else if (autoCleanupRaw && autoCleanupRaw.enabled === true) autoCleanupEnabled = true;
1599
+
1600
+ // protected_paths / high_risk_commands counts
1601
+ const protectedPathsCount = Array.isArray(policy.protected_paths) ? policy.protected_paths.length : 0;
1602
+ const highRiskCmdCount = Array.isArray(policy.high_risk_commands) ? policy.high_risk_commands.length : 0;
1603
+
1604
+ // release_mode summary
1605
+ const versionSource = release.version_source || 'package.json';
1606
+ const readyState = release.ready_state || '';
1607
+
1608
+ function row(label, valueHtml) {
1609
+ return '<div class="flex items-center justify-between gap-2 py-1" style="border-bottom: 1px solid var(--pw-border-subtle);">'
1610
+ + '<span class="text-xs whitespace-nowrap" style="color: var(--pw-text-secondary);">' + label + '</span>'
1611
+ + '<span class="text-xs text-right min-w-0 truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + valueHtml + '</span>'
1612
+ + '</div>';
1613
+ }
1614
+
1108
1615
  body.innerHTML =
1109
- '<div class="flex items-center justify-between gap-2 mb-2">'
1616
+ '<div class="flex items-center justify-between gap-2 mb-1">'
1110
1617
  + '<span class="text-xs" style="color: var(--pw-text-secondary);">policy file</span>'
1111
1618
  + badgeHtml(data.exists ? 'present' : 'defaults', data.exists ? 'success' : 'warning')
1112
1619
  + '</div>'
1113
- + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">issues: ' + issueCount + '</div>'
1114
- + '<div class="text-xs truncate" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(release.version_source || '') + '">version: ' + escapeHtml(release.version_source || 'package.json') + '</div>';
1620
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1621
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">validity</span>'
1622
+ + badgeHtml(valid ? 'valid' : 'invalid', valid ? 'success' : 'error')
1623
+ + '</div>'
1624
+ + row('issues', String(issueCount))
1625
+ + row('auto_cleanup', autoCleanupEnabled
1626
+ ? '<span style="color: var(--pw-state-success);">enabled</span>'
1627
+ : '<span style="color: var(--pw-text-tertiary);">disabled</span>')
1628
+ + row('protected_paths', String(protectedPathsCount))
1629
+ + row('high_risk_commands', String(highRiskCmdCount))
1630
+ + row('version_source', escapeHtml(versionSource))
1631
+ + (readyState ? row('ready_state', escapeHtml(readyState)) : '');
1115
1632
  } catch (err) {
1116
1633
  if (status) status.textContent = 'error';
1117
1634
  body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
@@ -1123,19 +1640,89 @@ html.dark {
1123
1640
  const status = $('release-status');
1124
1641
  if (!body) return;
1125
1642
  try {
1126
- const data = await fetchJson('/api/release/status?repo_path=.');
1127
- const ready = data && data.policy_valid && data.release_readiness && data.release_readiness.version_consistent !== false;
1128
- if (status) status.textContent = ready ? 'ready' : 'blocked';
1129
- const readiness = data.release_readiness || {};
1130
- const commands = Array.isArray(readiness.required_commands) ? readiness.required_commands : [];
1131
- const blocked = commands.filter(function (entry) { return !entry.allowed; }).length;
1643
+ const data = await fetchJson('/api/release/status?repo_path=' + encodeURIComponent(currentRepoPath));
1644
+ const vs = data.version_source || '';
1645
+ const ver = data.version || '';
1646
+ const consistent = data.version_consistent;
1647
+ const commands = Array.isArray(data.required_commands) ? data.required_commands : [];
1648
+ const blockedCount = data.commands_blocked_count != null ? data.commands_blocked_count : commands.filter(function (e) { return !e.allowed; }).length;
1649
+ const readyState = data.ready_state || 'unknown';
1650
+ const nextAction = data.next_action || '';
1651
+
1652
+ // Version display logic
1653
+ let versionDisplay;
1654
+ if (vs === 'workspace_root' || !ver) {
1655
+ versionDisplay = '选择具体 repo 以读取 version';
1656
+ } else {
1657
+ versionDisplay = ver;
1658
+ }
1659
+
1660
+ // ready_state badge
1661
+ let readyKind = 'warning';
1662
+ if (readyState === 'ready') readyKind = 'success';
1663
+ else if (readyState === 'blocked') readyKind = 'error';
1664
+
1665
+ // version_consistent badge
1666
+ const consistentKind = consistent === true ? 'success' : (consistent === false ? 'error' : 'neutral');
1667
+ const consistentText = consistent === true ? '✓' : (consistent === false ? '✗' : '—');
1668
+
1669
+ // commands list
1670
+ let cmdHtml = '';
1671
+ if (commands.length > 0) {
1672
+ cmdHtml = '<div class="mt-2 space-y-1">';
1673
+ commands.forEach(function (cmd) {
1674
+ const allowedIcon = cmd.allowed ? '✓' : '✗';
1675
+ const cmdKind = cmd.allowed ? 'success' : 'error';
1676
+ const c = statusColors(cmdKind);
1677
+ cmdHtml += '<div class="flex items-center gap-2 text-xs">'
1678
+ + '<span style="color: ' + c.color + ';">' + allowedIcon + '</span>'
1679
+ + '<span style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(cmd.command) + '</span>'
1680
+ + (cmd.blocked_reason ? '<span style="color: var(--pw-state-warning); font-family: var(--pw-font-sans);">(' + escapeHtml(cmd.blocked_reason) + ')</span>' : '')
1681
+ + '</div>';
1682
+ });
1683
+ cmdHtml += '</div>';
1684
+ }
1685
+
1686
+ // blocked reason text
1687
+ let blockedReasonHtml = '';
1688
+ if (readyState === 'blocked' && data.blocked_reason) {
1689
+ blockedReasonHtml = '<div class="text-xs mt-2" style="color: var(--pw-state-error);">' + escapeHtml(data.blocked_reason) + '</div>';
1690
+ }
1691
+
1692
+ if (status) {
1693
+ status.textContent = readyState;
1694
+ const sc = statusColors(readyKind);
1695
+ status.style.color = sc.color;
1696
+ }
1697
+
1132
1698
  body.innerHTML =
1133
- '<div class="flex items-center justify-between gap-2 mb-2">'
1699
+ '<div class="flex items-center justify-between gap-2 mb-1">'
1700
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">package</span>'
1701
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(data.package_name || '—') + '</span>'
1702
+ + '</div>'
1703
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1704
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">version_source</span>'
1705
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(vs || 'unknown') + '</span>'
1706
+ + '</div>'
1707
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1134
1708
  + '<span class="text-xs" style="color: var(--pw-text-secondary);">version</span>'
1135
- + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(readiness.version || 'unknown') + '</span>'
1709
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(versionDisplay) + '</span>'
1710
+ + '</div>'
1711
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1712
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">consistent</span>'
1713
+ + '<span class="text-xs" style="color: ' + statusColors(consistentKind).color + ';">' + consistentText + '</span>'
1714
+ + '</div>'
1715
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1716
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">commands</span>'
1717
+ + '<span class="text-xs" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + blockedCount + ' / ' + commands.length + ' blocked</span>'
1136
1718
  + '</div>'
1137
- + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">commands blocked: ' + blocked + ' / ' + commands.length + '</div>'
1138
- + '<div class="text-xs" style="color: var(--pw-text-tertiary);">' + escapeHtml(data.next_action || '') + '</div>';
1719
+ + '<div class="flex items-center justify-between gap-2 mb-1">'
1720
+ + '<span class="text-xs" style="color: var(--pw-text-secondary);">ready</span>'
1721
+ + badgeHtml(readyState, readyKind)
1722
+ + '</div>'
1723
+ + cmdHtml
1724
+ + blockedReasonHtml
1725
+ + (nextAction ? '<div class="text-xs mt-1" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(nextAction) + '</div>' : '');
1139
1726
  } catch (err) {
1140
1727
  if (status) status.textContent = 'error';
1141
1728
  body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
@@ -1150,24 +1737,261 @@ html.dark {
1150
1737
  const data = await fetchJson('/api/evidence-packs');
1151
1738
  const packs = Array.isArray(data.evidence_packs) ? data.evidence_packs : [];
1152
1739
  if (status) status.textContent = String(data.total || packs.length);
1740
+ const hasLineages = data.has_lineages === true;
1741
+ const pendingIds = Array.isArray(data.pending_lineage_ids) ? data.pending_lineage_ids : [];
1742
+
1153
1743
  if (!packs.length) {
1154
- body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">No exported evidence pack yet</div>';
1744
+ if (!hasLineages) {
1745
+ body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Evidence pack is available after run_task_loop.</div>';
1746
+ return;
1747
+ }
1748
+ // Lineage exists but no evidence pack yet — offer export for the most recent pending lineage.
1749
+ const exportId = pendingIds.length > 0 ? pendingIds[0] : null;
1750
+ if (exportId) {
1751
+ body.innerHTML =
1752
+ '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">Lineage exists but no evidence pack yet.</div>'
1753
+ + '<div class="flex items-center gap-2">'
1754
+ + '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-export-pack="' + escapeHtml(exportId) + '" type="button" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">Export evidence pack</button>'
1755
+ + '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(exportId) + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
1756
+ + '</div>';
1757
+ const exportBtn = body.querySelector('[data-export-pack]');
1758
+ if (exportBtn) {
1759
+ exportBtn.addEventListener('click', function () {
1760
+ const lid = exportBtn.getAttribute('data-export-pack');
1761
+ if (!lid) return;
1762
+ postAction('/api/evidence-packs/' + encodeURIComponent(lid) + '/export', exportBtn).then(function () {
1763
+ showToast('success', '导出成功', 'Evidence pack 已导出');
1764
+ refreshEvidencePacks();
1765
+ }).catch(function (err) {
1766
+ showToast('error', '导出失败', err.message);
1767
+ });
1768
+ });
1769
+ }
1770
+ const copyBtn = body.querySelector('[data-copy-id]');
1771
+ if (copyBtn) {
1772
+ copyBtn.addEventListener('click', function (e) {
1773
+ e.preventDefault();
1774
+ e.stopPropagation();
1775
+ copyToClipboard(copyBtn.getAttribute('data-copy-id'), 'lineage_id');
1776
+ });
1777
+ }
1778
+ } else {
1779
+ body.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">No exported evidence pack yet</div>';
1780
+ }
1155
1781
  return;
1156
1782
  }
1783
+
1157
1784
  const top = packs[0];
1785
+ const exportStatus = top.export_status || 'exported';
1786
+ const jsonExists = top.evidence_json_exists === true;
1787
+ const mdExists = top.evidence_md_exists === true;
1788
+ const exportedAt = top.exported_at || (top.generated_at || '—');
1789
+ const lineageId = top.lineage_id || '';
1158
1790
  body.innerHTML =
1159
1791
  '<div class="flex items-center justify-between gap-2 mb-2">'
1160
- + '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(top.lineage_id || '') + '">' + escapeHtml(shortId(top.lineage_id)) + '</span>'
1161
- + badgeHtml(top.lineage && top.lineage.stop_reason ? top.lineage.stop_reason : 'unknown', statusKind(top.lineage && top.lineage.final_status))
1792
+ + '<span class="text-xs truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);" title="' + escapeHtml(lineageId) + '">' + escapeHtml(shortId(lineageId)) + '</span>'
1793
+ + badgeHtml(exportStatus, exportStatus === 'exported' ? 'success' : 'warning')
1162
1794
  + '</div>'
1163
- + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">policy issues: ' + escapeHtml(String(top.policy ? top.policy.issue_count : 0)) + '</div>'
1164
- + '<div class="text-xs truncate" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(top.next_action || '') + '">Next: ' + escapeHtml(top.next_action || 'review') + '</div>';
1795
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">evidence.json: ' + (jsonExists ? '✓' : '✗') + ' · EVIDENCE.md: ' + (mdExists ? '✓' : '✗') + '</div>'
1796
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);" title="' + escapeHtml(exportedAt) + '">exported_at: ' + escapeHtml(fmtTime(exportedAt)) + '</div>'
1797
+ + '<div class="text-xs mb-2" style="color: var(--pw-text-tertiary);">policy issues: ' + escapeHtml(String(top.policy ? top.policy.issue_count : 0)) + '</div>'
1798
+ + '<div class="flex items-center gap-2 flex-wrap">'
1799
+ + (exportStatus !== 'exported' ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-export-pack="' + escapeHtml(lineageId) + '" type="button" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid var(--pw-accent);">Export evidence pack</button>' : '')
1800
+ + '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-view-evidence="' + escapeHtml(lineageId) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">View detail</button>'
1801
+ + '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(lineageId) + '" title="复制 lineage_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
1802
+ + '</div>';
1803
+ // Wire export button (if present)
1804
+ const exportBtn2 = body.querySelector('[data-export-pack]');
1805
+ if (exportBtn2) {
1806
+ exportBtn2.addEventListener('click', function () {
1807
+ const lid = exportBtn2.getAttribute('data-export-pack');
1808
+ if (!lid) return;
1809
+ postAction('/api/evidence-packs/' + encodeURIComponent(lid) + '/export', exportBtn2).then(function () {
1810
+ showToast('success', '导出成功', 'Evidence pack 已导出');
1811
+ refreshEvidencePacks();
1812
+ }).catch(function (err) {
1813
+ showToast('error', '导出失败', err.message);
1814
+ });
1815
+ });
1816
+ }
1817
+ // Wire view detail button
1818
+ const viewBtn = body.querySelector('[data-view-evidence]');
1819
+ if (viewBtn) {
1820
+ viewBtn.addEventListener('click', function () {
1821
+ const lid = viewBtn.getAttribute('data-view-evidence');
1822
+ if (lid) showEvidencePackDetail(lid);
1823
+ });
1824
+ }
1825
+ // Wire copy button
1826
+ const copyBtn2 = body.querySelector('[data-copy-id]');
1827
+ if (copyBtn2) {
1828
+ copyBtn2.addEventListener('click', function (e) {
1829
+ e.preventDefault();
1830
+ e.stopPropagation();
1831
+ copyToClipboard(copyBtn2.getAttribute('data-copy-id'), 'lineage_id');
1832
+ });
1833
+ }
1165
1834
  } catch (err) {
1166
1835
  if (status) status.textContent = 'error';
1167
1836
  body.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
1168
1837
  }
1169
1838
  }
1170
1839
 
1840
+ async function showEvidencePackDetail(lineageId) {
1841
+ if (!lineageId) return;
1842
+ const modal = $('safe-modal');
1843
+ const titleEl = $('safe-modal-title');
1844
+ const bodyEl = $('safe-modal-body');
1845
+ if (!modal || !titleEl || !bodyEl) return;
1846
+ titleEl.textContent = 'Evidence Pack · ' + shortId(lineageId);
1847
+ bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-text-tertiary);">Loading…</div>';
1848
+ modal.classList.remove('hidden');
1849
+ try {
1850
+ const data = await fetchJson('/api/evidence-packs/' + encodeURIComponent(lineageId));
1851
+ const jsonPath = data.files && data.files.json ? data.files.json : '';
1852
+ const mdPath = data.files && data.files.markdown ? data.files.markdown : '';
1853
+ const lineage = data.lineage || {};
1854
+ bodyEl.innerHTML =
1855
+ '<div class="mb-3">'
1856
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">lineage_id:</span> ' + escapeHtml(data.lineage_id || lineageId) + '</div>'
1857
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-secondary);"><span style="color: var(--pw-text-tertiary);">generated_at:</span> ' + escapeHtml(fmtTime(data.generated_at || '—')) + '</div>'
1858
+ + '<div class="flex items-center gap-2 mb-1 flex-wrap">'
1859
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">final_status:</span>' + badgeHtml(lineage.final_status || 'unknown', statusKind(lineage.final_status))
1860
+ + '<span class="text-xs" style="color: var(--pw-text-tertiary);">stop_reason:</span><span class="text-xs" style="color: var(--pw-text-secondary);">' + escapeHtml(lineage.stop_reason || '—') + '</span>'
1861
+ + '</div>'
1862
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">policy valid: ' + (data.policy && data.policy.valid ? '✓' : '✗') + ' · issues: ' + escapeHtml(String(data.policy ? data.policy.issue_count : 0)) + '</div>'
1863
+ + (data.next_action ? '<div class="text-xs mb-1" style="color: var(--pw-text-tertiary);">next: ' + escapeHtml(data.next_action) + '</div>' : '')
1864
+ + '</div>'
1865
+ + '<div class="mt-2">'
1866
+ + '<div class="text-xs font-semibold mb-1" style="color: var(--pw-text-primary);">Files</div>'
1867
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono); word-break: break-all;" title="' + escapeHtml(jsonPath) + '">evidence.json: ' + (jsonPath ? escapeHtml(jsonPath) : '—') + '</div>'
1868
+ + '<div class="text-xs mb-1" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono); word-break: break-all;" title="' + escapeHtml(mdPath) + '">EVIDENCE.md: ' + (mdPath ? escapeHtml(mdPath) : '—') + '</div>'
1869
+ + '<div class="flex items-center gap-2 mt-2">'
1870
+ + (jsonPath ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-copy-path="' + escapeHtml(jsonPath) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">Copy evidence.json path</button>' : '')
1871
+ + (mdPath ? '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-copy-path="' + escapeHtml(mdPath) + '" type="button" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">Copy EVIDENCE.md path</button>' : '')
1872
+ + '</div>'
1873
+ + '</div>';
1874
+ // Wire copy path buttons
1875
+ bodyEl.querySelectorAll('[data-copy-path]').forEach(function (btn) {
1876
+ btn.addEventListener('click', function () {
1877
+ copyToClipboard(btn.getAttribute('data-copy-path'), 'file path');
1878
+ });
1879
+ });
1880
+ } catch (err) {
1881
+ bodyEl.innerHTML = '<div class="text-xs" style="color: var(--pw-state-error);">' + escapeHtml(err.message) + '</div>';
1882
+ }
1883
+ }
1884
+
1885
+ // ---------- Repo selector ----------
1886
+ async function loadRepos() {
1887
+ const select = $('repo-selector');
1888
+ if (!select) return;
1889
+ try {
1890
+ const data = await fetchJson('/api/workspace/repos');
1891
+ const repos = Array.isArray(data.repos) ? data.repos : [];
1892
+ let html = '<option value=".">工作区根目录</option>';
1893
+ repos.forEach(function (r) {
1894
+ const label = r.name + (r.has_package_json && r.version ? ' (v' + r.version + ')' : '');
1895
+ const val = r.path || r.name;
1896
+ html += '<option value="' + escapeHtml(val) + '">' + escapeHtml(label) + '</option>';
1897
+ });
1898
+ select.innerHTML = html;
1899
+ } catch (err) {
1900
+ // keep default option
1901
+ console.error('loadRepos failed:', err);
1902
+ }
1903
+ }
1904
+
1905
+ function onRepoChange(selectedRepoPath) {
1906
+ currentRepoPath = selectedRepoPath || '.';
1907
+ Promise.all([
1908
+ refreshProjectPolicy(),
1909
+ refreshReleaseStatus(),
1910
+ refreshTasks(),
1911
+ refreshLineages(),
1912
+ refreshEvidencePacks(),
1913
+ refreshStaleTasks(),
1914
+ refreshHealthScore()
1915
+ ]).catch(function (err) {
1916
+ showError('切换 repo 刷新失败: ' + err.message);
1917
+ });
1918
+ }
1919
+
1920
+ // ---------- Stale tasks ----------
1921
+ async function refreshStaleTasks() {
1922
+ const body = $('stale-tasks-body');
1923
+ const countEl = $('stale-tasks-count');
1924
+ const card = $('stale-tasks-card');
1925
+ if (!body) return;
1926
+ try {
1927
+ const data = await fetchJson('/api/tasks/stale');
1928
+ const staleTasks = Array.isArray(data.stale_tasks) ? data.stale_tasks : [];
1929
+ const total = (typeof data.total === 'number') ? data.total : staleTasks.length;
1930
+ if (countEl) countEl.textContent = String(total);
1931
+ if (!staleTasks.length) {
1932
+ if (card) card.classList.add('hidden');
1933
+ return;
1934
+ }
1935
+ if (card) card.classList.remove('hidden');
1936
+ let html = '';
1937
+ staleTasks.forEach(function (st) {
1938
+ const taskId = st.task_id || '';
1939
+ const href = '/pages/task-detail.html?id=' + encodeURIComponent(taskId);
1940
+ const statusBadge = badgeHtml(st.status || '—', statusKind(st.status));
1941
+ const errorText = st.error ? escapeHtml(st.error.length > 100 ? st.error.slice(0, 100) + '…' : st.error) : '';
1942
+ const explanation = st.explanation || '';
1943
+ const nextAction = st.next_action || '';
1944
+ html +=
1945
+ '<div class="py-3" style="border-bottom: 1px solid var(--pw-border-subtle);" data-stale-task="' + escapeHtml(taskId) + '">'
1946
+ + '<div class="flex items-center gap-2 mb-1.5 flex-wrap">'
1947
+ + '<span class="text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap" style="color: var(--pw-text-secondary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);" title="' + escapeHtml(taskId) + '">' + escapeHtml(shortId(taskId)) + '</span>'
1948
+ + statusBadge
1949
+ + '<span class="text-xs truncate" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-mono); max-width: 200px;" title="' + escapeHtml(st.repo_path || '') + '">' + escapeHtml(st.repo_path || '—') + '</span>'
1950
+ + '<div class="flex-1"></div>'
1951
+ + '<button class="pw-quick-btn" data-copy-id="' + escapeHtml(taskId) + '" title="复制 task_id" type="button"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>'
1952
+ + '</div>'
1953
+ + (explanation ? '<div class="pw-stale-explanation mb-1.5">' + escapeHtml(explanation) + '</div>' : '')
1954
+ + (errorText ? '<div class="text-xs mb-1" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);" title="' + escapeHtml(st.error || '') + '">' + errorText + '</div>' : '')
1955
+ + (nextAction ? '<div class="text-xs mb-1.5" style="color: var(--pw-text-secondary);">next: ' + escapeHtml(nextAction) + '</div>' : '')
1956
+ + '<div class="flex items-center gap-2">'
1957
+ + '<a href="' + href + '" class="text-xs px-2.5 py-1 rounded whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-secondary); border: 1px solid var(--pw-border-subtle);">查看详情</a>'
1958
+ + '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" data-hide-stale="' + escapeHtml(taskId) + '" type="button" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); border: 1px solid var(--pw-state-error);">隐藏</button>'
1959
+ + '<button class="text-xs px-2.5 py-1 rounded whitespace-nowrap" disabled style="background-color: var(--pw-bg-hover); color: var(--pw-text-tertiary); border: 1px solid var(--pw-border-subtle);">重建任务</button>'
1960
+ + '</div>'
1961
+ + '</div>';
1962
+ });
1963
+ body.innerHTML = html;
1964
+ // Wire copy buttons
1965
+ body.querySelectorAll('[data-copy-id]').forEach(function (btn) {
1966
+ btn.addEventListener('click', function (e) {
1967
+ e.preventDefault();
1968
+ e.stopPropagation();
1969
+ copyToClipboard(btn.getAttribute('data-copy-id'), 'task_id');
1970
+ });
1971
+ });
1972
+ // Wire hide-stale buttons
1973
+ body.querySelectorAll('[data-hide-stale]').forEach(function (btn) {
1974
+ btn.addEventListener('click', function (e) {
1975
+ e.preventDefault();
1976
+ e.stopPropagation();
1977
+ const tid = btn.getAttribute('data-hide-stale');
1978
+ postAction('/api/tasks/' + encodeURIComponent(tid) + '/hide-stale', btn).then(function () {
1979
+ showToast('success', '已隐藏', 'Stale 任务已隐藏');
1980
+ const row = btn.closest('[data-stale-task]');
1981
+ if (row && row.parentNode) row.parentNode.removeChild(row);
1982
+ const remaining = body.querySelectorAll('[data-stale-task]').length;
1983
+ if (countEl) countEl.textContent = String(remaining);
1984
+ if (remaining === 0 && card) card.classList.add('hidden');
1985
+ }).catch(function (err) {
1986
+ showToast('error', '隐藏失败', err.message);
1987
+ });
1988
+ });
1989
+ });
1990
+ } catch (err) {
1991
+ if (card) card.classList.add('hidden');
1992
+ }
1993
+ }
1994
+
1171
1995
  // ---------- Refresh orchestrators ----------
1172
1996
  async function refreshStatus() {
1173
1997
  try {
@@ -1197,7 +2021,7 @@ html.dark {
1197
2021
 
1198
2022
  async function refreshTasks() {
1199
2023
  try {
1200
- const data = await fetchJson('/api/tasks');
2024
+ const data = await fetchJson('/api/tasks?repo_path=' + encodeURIComponent(currentRepoPath));
1201
2025
  renderTasks(data);
1202
2026
  } catch (err) {
1203
2027
  const bodyEl = $('recent-tasks-body');
@@ -1208,14 +2032,14 @@ html.dark {
1208
2032
  }
1209
2033
 
1210
2034
  async function refreshAll() {
1211
- await Promise.all([refreshStatus(), refreshTasks(), refreshLogs(), refreshEvents(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks()]);
2035
+ await Promise.all([refreshStatus(), refreshTasks(), refreshLogs(), refreshEvents(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks(), refreshStaleTasks(), refreshHealthScore()]);
1212
2036
  }
1213
2037
 
1214
2038
  // ---------- Actions ----------
1215
2039
  async function handleAction(action, btnEl) {
1216
2040
  try {
1217
2041
  if (action === 'refresh') {
1218
- await Promise.all([refreshStatus(), refreshTasks(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks()]);
2042
+ await Promise.all([refreshStatus(), refreshTasks(), refreshLineages(), refreshProjectPolicy(), refreshReleaseStatus(), refreshEvidencePacks(), refreshStaleTasks(), refreshHealthScore()]);
1219
2043
  return;
1220
2044
  }
1221
2045
 
@@ -1251,7 +2075,8 @@ html.dark {
1251
2075
  };
1252
2076
  const url = map[action];
1253
2077
  if (!url) return;
1254
- await postAction(url, btnEl);
2078
+ const result = await postAction(url, btnEl);
2079
+ if (action !== 'open-logs-folder') showToast('success', '操作完成', result && result.verified ? '服务已通过就绪检查。' : '命令已完成。');
1255
2080
  // Refresh state after state-changing POSTs (open-logs-folder has no state change)
1256
2081
  if (action !== 'open-logs-folder') {
1257
2082
  await Promise.all([refreshStatus(), refreshTasks()]);
@@ -1269,6 +2094,35 @@ html.dark {
1269
2094
  });
1270
2095
  });
1271
2096
 
2097
+ // Copy diagnostics button (system status card header)
2098
+ const copyDiagBtn = $('btn-copy-diagnostics');
2099
+ if (copyDiagBtn) {
2100
+ copyDiagBtn.addEventListener('click', function () {
2101
+ copyDiagnostics(copyDiagBtn);
2102
+ });
2103
+ }
2104
+
2105
+ // Repo selector change handler
2106
+ const repoSelector = $('repo-selector');
2107
+ if (repoSelector) {
2108
+ repoSelector.addEventListener('change', function () {
2109
+ onRepoChange(repoSelector.value);
2110
+ });
2111
+ }
2112
+
2113
+ // Safe modal close handlers
2114
+ const modalClose = $('safe-modal-close');
2115
+ if (modalClose) {
2116
+ modalClose.addEventListener('click', function () { closeSafeModal(); });
2117
+ }
2118
+ const safeModal = $('safe-modal');
2119
+ if (safeModal) {
2120
+ safeModal.addEventListener('click', function (e) {
2121
+ if (e.target === safeModal) closeSafeModal();
2122
+ });
2123
+ }
2124
+
2125
+ loadRepos();
1272
2126
  loadToken().then(function () {
1273
2127
  auditSeeded = false;
1274
2128
  seenAuditKeys = new Set();
@@ -1282,6 +2136,8 @@ html.dark {
1282
2136
 
1283
2137
  // Auto-refresh /api/status every 30s (no logs to avoid performance issues)
1284
2138
  statusTimer = setInterval(function () { refreshStatus(); }, 30000);
2139
+ // Health score refresh (lightweight, reuses some endpoints) every 60s
2140
+ setInterval(function () { refreshHealthScore(); }, 60000);
1285
2141
  // Refresh activity timeline every 60s
1286
2142
  setInterval(function () { refreshEvents(); }, 60000);
1287
2143
  // Audit toast polling: every 90s, best-effort
@@ -1291,5 +2147,7 @@ html.dark {
1291
2147
  </script>
1292
2148
 
1293
2149
  <script>lucide.createIcons();</script>
2150
+ <script src="/i18n.js"></script>
2151
+ <script src="/desktop-bridge.js"></script>
1294
2152
  </body>
1295
2153
  </html>