patchwarden 1.5.0 → 1.5.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 (217) hide show
  1. package/README.en.md +43 -2
  2. package/README.md +43 -2
  3. package/dist/assessments/agentAssessor.d.ts +1 -1
  4. package/dist/assessments/agentAssessor.js +3 -3
  5. package/dist/assessments/assessmentStore.d.ts +1 -1
  6. package/dist/assessments/assessmentStore.js +2 -2
  7. package/dist/assessments/confirmCli.js +5 -4
  8. package/dist/config.d.ts +7 -0
  9. package/dist/config.js +34 -0
  10. package/dist/control/middleware/auth.d.ts +10 -0
  11. package/dist/control/middleware/auth.js +8 -0
  12. package/dist/control/middleware/static.d.ts +3 -0
  13. package/dist/control/middleware/static.js +65 -0
  14. package/dist/control/routes/audit.d.ts +15 -0
  15. package/dist/control/routes/audit.js +277 -0
  16. package/dist/control/routes/evidence.d.ts +4 -0
  17. package/dist/control/routes/evidence.js +96 -0
  18. package/dist/control/routes/lineage.d.ts +3 -0
  19. package/dist/control/routes/lineage.js +71 -0
  20. package/dist/control/routes/policy.d.ts +3 -0
  21. package/dist/control/routes/policy.js +81 -0
  22. package/dist/control/routes/process.d.ts +5 -0
  23. package/dist/control/routes/process.js +200 -0
  24. package/dist/control/routes/sessions.d.ts +22 -0
  25. package/dist/control/routes/sessions.js +224 -0
  26. package/dist/control/routes/status.d.ts +6 -0
  27. package/dist/control/routes/status.js +250 -0
  28. package/dist/control/routes/taskActions.d.ts +21 -0
  29. package/dist/control/routes/taskActions.js +233 -0
  30. package/dist/control/routes/tasks.d.ts +20 -0
  31. package/dist/control/routes/tasks.js +310 -0
  32. package/dist/control/routes/workspace.d.ts +15 -0
  33. package/dist/control/routes/workspace.js +193 -0
  34. package/dist/control/runtime.d.ts +91 -0
  35. package/dist/control/runtime.js +392 -0
  36. package/dist/control/server.d.ts +13 -0
  37. package/dist/control/server.js +479 -0
  38. package/dist/control/shared.d.ts +35 -0
  39. package/dist/control/shared.js +288 -0
  40. package/dist/controlCenter.d.ts +6 -0
  41. package/dist/controlCenter.js +7 -2197
  42. package/dist/direct/directGuards.js +30 -8
  43. package/dist/doctor.d.ts +18 -1
  44. package/dist/doctor.js +579 -348
  45. package/dist/goal/goalReport.d.ts +54 -0
  46. package/dist/goal/goalReport.js +204 -0
  47. package/dist/goal/goalStatus.d.ts +6 -0
  48. package/dist/goal/specKitImport.d.ts +63 -0
  49. package/dist/goal/specKitImport.js +220 -0
  50. package/dist/goal/subgoalSync.js +2 -1
  51. package/dist/httpServer.js +15 -12
  52. package/dist/index.js +7 -4
  53. package/dist/logging.d.ts +7 -1
  54. package/dist/logging.js +8 -0
  55. package/dist/runner/changeCapture.d.ts +3 -3
  56. package/dist/runner/changeCapture.js +63 -39
  57. package/dist/runner/cli.js +7 -6
  58. package/dist/runner/postTaskCleanup.js +26 -2
  59. package/dist/runner/runTask.js +245 -221
  60. package/dist/runner/simpleProcess.js +4 -4
  61. package/dist/runner/watch.js +17 -14
  62. package/dist/security/contentRedaction.d.ts +6 -0
  63. package/dist/security/contentRedaction.js +22 -0
  64. package/dist/smoke-test.js +257 -251
  65. package/dist/test/unit/apply-patch.test.d.ts +1 -0
  66. package/dist/test/unit/apply-patch.test.js +225 -0
  67. package/dist/test/unit/create-task.test.d.ts +1 -0
  68. package/dist/test/unit/create-task.test.js +197 -0
  69. package/dist/test/unit/direct-guards.test.js +124 -9
  70. package/dist/test/unit/evidence-pack.test.js +95 -1
  71. package/dist/test/unit/get-task-status.test.d.ts +1 -0
  72. package/dist/test/unit/get-task-status.test.js +174 -0
  73. package/dist/test/unit/get-task-summary.test.d.ts +1 -0
  74. package/dist/test/unit/get-task-summary.test.js +146 -0
  75. package/dist/test/unit/goal-report.test.d.ts +1 -0
  76. package/dist/test/unit/goal-report.test.js +159 -0
  77. package/dist/test/unit/goal-subgoal-task.test.js +6 -6
  78. package/dist/test/unit/goal-tools-registry.test.js +6 -4
  79. package/dist/test/unit/path-guard.test.js +24 -0
  80. package/dist/test/unit/spec-kit-import.test.d.ts +1 -0
  81. package/dist/test/unit/spec-kit-import.test.js +341 -0
  82. package/dist/test/unit/wait-for-task.test.d.ts +1 -0
  83. package/dist/test/unit/wait-for-task.test.js +144 -0
  84. package/dist/tools/auditTask.d.ts +8 -63
  85. package/dist/tools/auditTask.js +12 -8
  86. package/dist/tools/createDirectSession.d.ts +1 -1
  87. package/dist/tools/createDirectSession.js +2 -2
  88. package/dist/tools/createTask.d.ts +2 -2
  89. package/dist/tools/createTask.js +4 -4
  90. package/dist/tools/dispatch/coreDispatch.d.ts +9 -0
  91. package/dist/tools/dispatch/coreDispatch.js +282 -0
  92. package/dist/tools/dispatch/diagnosticDispatch.d.ts +14 -0
  93. package/dist/tools/dispatch/diagnosticDispatch.js +78 -0
  94. package/dist/tools/dispatch/directDispatch.d.ts +8 -0
  95. package/dist/tools/dispatch/directDispatch.js +115 -0
  96. package/dist/tools/dispatch/goalDispatch.d.ts +8 -0
  97. package/dist/tools/dispatch/goalDispatch.js +91 -0
  98. package/dist/tools/dispatch/releaseDispatch.d.ts +8 -0
  99. package/dist/tools/dispatch/releaseDispatch.js +45 -0
  100. package/dist/tools/dispatch/types.d.ts +23 -0
  101. package/dist/tools/dispatch/types.js +15 -0
  102. package/dist/tools/evidencePack.d.ts +6 -0
  103. package/dist/tools/evidencePack.js +213 -6
  104. package/dist/tools/finalizeDirectSession.d.ts +1 -1
  105. package/dist/tools/finalizeDirectSession.js +3 -3
  106. package/dist/tools/goalSubgoalTask.d.ts +1 -1
  107. package/dist/tools/goalSubgoalTask.js +2 -2
  108. package/dist/tools/healthCheck.js +3 -3
  109. package/dist/tools/registry.d.ts +3 -3
  110. package/dist/tools/registry.js +60 -503
  111. package/dist/tools/retryTask.d.ts +2 -2
  112. package/dist/tools/retryTask.js +2 -2
  113. package/dist/tools/runTaskLoop.js +4 -4
  114. package/dist/tools/safeViews.d.ts +2 -2
  115. package/dist/tools/safeViews.js +2 -2
  116. package/dist/tools/toolRegistry.js +22 -0
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/docs/CODE_WIKI.md +983 -0
  120. package/docs/agentseal-integration.md +150 -0
  121. package/docs/architecture.md +63 -0
  122. package/docs/assets/patchwarden-oss-demo.gif +0 -0
  123. package/docs/dashboard-overview.md +86 -0
  124. package/docs/demo.md +8 -0
  125. package/docs/direct-session-workflow.md +98 -0
  126. package/docs/evidence-pack-schema.md +215 -0
  127. package/docs/execution-plan-2026-07-09.md +315 -0
  128. package/docs/lineage-evidence-pack-workflow.md +127 -0
  129. package/docs/mcp-inspector-testing.md +200 -0
  130. package/docs/open-source-application.md +162 -0
  131. package/docs/opencode-worker.md +151 -0
  132. package/docs/openhands-worker.md +181 -0
  133. package/docs/release-evidence.md +72 -0
  134. package/docs/roadmap-execution-and-acceptance.md +365 -0
  135. package/docs/spec-kit-integration.md +131 -0
  136. package/docs/task-safe-review-workflow.md +98 -0
  137. package/docs/threat-model.md +79 -0
  138. package/docs/user-feedback.md +40 -0
  139. package/docs/why-patchwarden.md +110 -0
  140. package/package.json +2 -2
  141. package/scripts/checks/control-center-smoke.js +356 -0
  142. package/scripts/checks/lifecycle-smoke.js +29 -23
  143. package/scripts/checks/mcp-smoke.js +3 -1
  144. package/scripts/generate-demo-gif.py +320 -0
  145. package/src/assessments/agentAssessor.ts +3 -3
  146. package/src/assessments/assessmentStore.ts +2 -2
  147. package/src/assessments/confirmCli.ts +5 -4
  148. package/src/config.ts +37 -0
  149. package/src/control/middleware/auth.ts +17 -0
  150. package/src/control/middleware/static.ts +71 -0
  151. package/src/control/routes/audit.ts +321 -0
  152. package/src/control/routes/evidence.ts +107 -0
  153. package/src/control/routes/lineage.ts +92 -0
  154. package/src/control/routes/policy.ts +81 -0
  155. package/src/control/routes/process.ts +204 -0
  156. package/src/control/routes/sessions.ts +251 -0
  157. package/src/control/routes/status.ts +325 -0
  158. package/src/control/routes/taskActions.ts +248 -0
  159. package/src/control/routes/tasks.ts +323 -0
  160. package/src/control/routes/workspace.ts +203 -0
  161. package/src/control/runtime.ts +472 -0
  162. package/src/control/server.ts +471 -0
  163. package/src/control/shared.ts +294 -0
  164. package/src/controlCenter.ts +7 -2347
  165. package/src/direct/directGuards.ts +28 -7
  166. package/src/doctor.ts +741 -481
  167. package/src/goal/goalReport.ts +271 -0
  168. package/src/goal/goalStatus.ts +6 -0
  169. package/src/goal/specKitImport.ts +355 -0
  170. package/src/goal/subgoalSync.ts +4 -2
  171. package/src/httpServer.ts +17 -14
  172. package/src/index.ts +7 -4
  173. package/src/logging.ts +10 -1
  174. package/src/runner/changeCapture.ts +70 -42
  175. package/src/runner/cli.ts +7 -6
  176. package/src/runner/postTaskCleanup.ts +26 -2
  177. package/src/runner/runTask.ts +325 -223
  178. package/src/runner/simpleProcess.ts +4 -4
  179. package/src/runner/watch.ts +17 -14
  180. package/src/security/contentRedaction.ts +29 -0
  181. package/src/smoke-test.ts +252 -250
  182. package/src/test/unit/apply-patch.test.ts +293 -0
  183. package/src/test/unit/create-task.test.ts +255 -0
  184. package/src/test/unit/direct-guards.test.ts +178 -8
  185. package/src/test/unit/evidence-pack.test.ts +110 -1
  186. package/src/test/unit/get-task-status.test.ts +203 -0
  187. package/src/test/unit/get-task-summary.test.ts +173 -0
  188. package/src/test/unit/goal-report.test.ts +189 -0
  189. package/src/test/unit/goal-subgoal-task.test.ts +6 -6
  190. package/src/test/unit/goal-tools-registry.test.ts +7 -5
  191. package/src/test/unit/path-guard.test.ts +30 -0
  192. package/src/test/unit/spec-kit-import.test.ts +429 -0
  193. package/src/test/unit/wait-for-task.test.ts +176 -0
  194. package/src/tools/auditTask.ts +99 -59
  195. package/src/tools/createDirectSession.ts +3 -3
  196. package/src/tools/createTask.ts +7 -7
  197. package/src/tools/dispatch/coreDispatch.ts +374 -0
  198. package/src/tools/dispatch/diagnosticDispatch.ts +101 -0
  199. package/src/tools/dispatch/directDispatch.ts +167 -0
  200. package/src/tools/dispatch/goalDispatch.ts +127 -0
  201. package/src/tools/dispatch/releaseDispatch.ts +65 -0
  202. package/src/tools/dispatch/types.ts +24 -0
  203. package/src/tools/evidencePack.ts +291 -6
  204. package/src/tools/finalizeDirectSession.ts +4 -4
  205. package/src/tools/goalSubgoalTask.ts +2 -2
  206. package/src/tools/healthCheck.ts +3 -3
  207. package/src/tools/registry.ts +68 -628
  208. package/src/tools/retryTask.ts +2 -2
  209. package/src/tools/runTaskLoop.ts +4 -4
  210. package/src/tools/safeViews.ts +2 -2
  211. package/src/tools/toolRegistry.ts +22 -0
  212. package/src/version.ts +1 -1
  213. package/ui/pages/audit.html +192 -3
  214. package/ui/pages/dashboard.html +959 -44
  215. package/ui/pages/direct-sessions.html +505 -53
  216. package/ui/pages/task-detail.html +456 -438
  217. package/ui/pages/tasks.html +598 -61
@@ -372,176 +372,192 @@ html.dark {
372
372
  </div>
373
373
  </section>
374
374
 
375
- <!-- Two-Column Layout -->
376
- <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
377
-
378
- <!-- Left Column (~58%) -->
379
- <div class="lg:col-span-7 flex flex-col gap-6">
380
-
381
- <!-- Execution Status Card -->
382
- <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
383
- <h2 class="text-base font-semibold mb-4" data-zh="执行状态" data-en="Execution Status" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">执行状态</h2>
384
- <div class="grid grid-cols-1 sm:grid-cols-3 gap-x-6 gap-y-3 text-sm">
385
- <div class="flex flex-col gap-0.5 min-w-0">
386
- <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="运行阶段" data-en="Runtime Phase">运行阶段</span>
387
- <span id="rtPhase" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
388
- </div>
389
- <div class="flex flex-col gap-0.5 min-w-0">
390
- <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="最近心跳" data-en="Last Heartbeat">最近心跳</span>
391
- <span id="rtHeartbeat" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
392
- </div>
393
- <div class="flex flex-col gap-0.5 min-w-0">
394
- <span class="text-xs whitespace-nowrap" style="color: var(--pw-text-tertiary);" data-zh="当前命令" data-en="Current Command">当前命令</span>
395
- <span id="rtCurrentCmd" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
396
- </div>
397
- </div>
398
- </section>
375
+ <!-- ===== Safe Summary Section (DEFAULT visible, loaded from 4 safe endpoints) ===== -->
376
+ <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
377
+ <div class="flex items-center gap-2 mb-1">
378
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
379
+ <h2 class="text-base font-semibold" data-zh="安全摘要" data-en="Safe Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">安全摘要</h2>
380
+ <span class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-accent); background-color: var(--pw-accent-subtle); font-family: var(--pw-font-mono);">safe-first</span>
381
+ </div>
382
+ <p class="text-xs mb-5" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="默认仅展示脱敏后的安全摘要。完整结果、diff、测试日志请见下方「高级」区域。" data-en="Shows redacted safe summaries by default. Full result/diff/test log are in the Advanced section below.">默认仅展示脱敏后的安全摘要。完整结果、diff、测试日志请见下方「高级」区域。</p>
383
+
384
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
399
385
 
400
- <!-- Diff Preview Card -->
401
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
402
- <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
403
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
404
- <h2 class="text-base font-semibold" data-zh="变更预览" data-en="Diff Preview" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">变更预览</h2>
386
+ <!-- Safe Result Overview -->
387
+ <div class="rounded-lg p-4 flex flex-col gap-3" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">
388
+ <div class="flex items-center justify-between">
389
+ <h3 class="text-sm font-semibold" data-zh="结果概览" data-en="Safe Result" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">结果概览 (safe_result)</h3>
390
+ <span id="srStatusBadge" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
405
391
  </div>
406
- <div id="diffEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无 diff 内容" data-en="No diff available">暂无 diff 内容</div>
407
- <pre id="diffContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 480px; overflow-y: auto;"></pre>
408
- </section>
409
-
410
- <!-- Changed Files Card -->
411
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
412
- <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
413
- <div class="flex items-center gap-2">
414
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" 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>
415
- <h2 class="text-base font-semibold" data-zh="变更文件" data-en="Changed Files" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">变更文件</h2>
416
- </div>
417
- <span id="cfCountBadge" class="hidden text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
392
+ <dl class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs">
393
+ <div class="flex flex-col gap-0.5 min-w-0"><dt style="color: var(--pw-text-tertiary);">terminal</dt><dd id="srTerminal" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
394
+ <div class="flex flex-col gap-0.5 min-w-0"><dt style="color: var(--pw-text-tertiary);">acceptance_status</dt><dd id="srAcceptance" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
395
+ <div class="flex flex-col gap-0.5 min-w-0"><dt style="color: var(--pw-text-tertiary);">changed_files_total</dt><dd id="srChangedTotal" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
396
+ <div class="flex flex-col gap-0.5 min-w-0"><dt style="color: var(--pw-text-tertiary);">out_of_scope_changes_total</dt><dd id="srOosTotal" class="truncate" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
397
+ </dl>
398
+ <div class="flex flex-col gap-0.5 min-w-0">
399
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">verification.headline</span>
400
+ <span id="srVerifyHeadline" class="text-xs break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
418
401
  </div>
419
- <div id="cfEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无变更文件记录" data-en="No changed-files record">暂无变更文件记录</div>
420
- <div id="cfBody" class="hidden px-4 py-3 flex flex-col gap-3">
421
- <!-- file_stats summary row -->
422
- <div id="cfStatsRow" class="hidden grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs"></div>
423
- <!-- changed files list (truncated) -->
424
- <div id="cfList" class="rounded-md p-2 text-xs leading-relaxed overflow-auto" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px;"></div>
402
+ <div id="srFailureBlock" class="hidden flex flex-col gap-1">
403
+ <div class="flex flex-col gap-0.5 min-w-0"><span class="text-xs" style="color: var(--pw-state-error);">failure_reason</span><span id="srFailureReason" class="text-xs break-words" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);">—</span></div>
404
+ <div class="flex flex-col gap-0.5 min-w-0"><span class="text-xs" style="color: var(--pw-state-error);">failed_command</span><span id="srFailedCommand" class="text-xs break-all" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);">—</span></div>
425
405
  </div>
426
- </section>
427
-
428
- <!-- Verify Log Card -->
429
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
430
- <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
431
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
432
- <h2 class="text-base font-semibold" data-zh="验证日志" data-en="Verify Log" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">验证日志</h2>
406
+ <div class="flex flex-col gap-0.5 min-w-0">
407
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">next_action</span>
408
+ <span id="srNextAction" class="text-xs break-words" style="color: var(--pw-accent); font-family: var(--pw-font-mono);">—</span>
409
+ </div>
410
+ <div id="srBlockedBlock" class="hidden flex flex-col gap-0.5 min-w-0">
411
+ <span class="text-xs" style="color: var(--pw-state-warning);">execution_blocked / pending_reason</span>
412
+ <span id="srPendingReason" class="text-xs break-words" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">—</span>
433
413
  </div>
434
- <div id="verifyLogEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无验证日志" data-en="No verify log">暂无验证日志</div>
435
- <pre id="verifyLogContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px; overflow-y: auto;"></pre>
436
- </section>
414
+ <div id="srWarningsBlock" class="hidden flex flex-col gap-1 min-w-0">
415
+ <span class="text-xs font-medium" style="color: var(--pw-state-warning);">warnings</span>
416
+ <pre id="srWarnings" class="rounded-md p-2 text-xs leading-relaxed whitespace-pre-wrap break-all" style="background-color: var(--pw-state-warning-bg); color: var(--pw-state-warning); font-family: var(--pw-font-mono); max-height: 160px; overflow-y: auto;"></pre>
417
+ </div>
418
+ </div>
437
419
 
438
- </div>
420
+ <!-- Safe Test Summary -->
421
+ <div class="rounded-lg p-4 flex flex-col gap-3" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">
422
+ <div class="flex items-center justify-between">
423
+ <h3 class="text-sm font-semibold" data-zh="测试摘要" data-en="Safe Test Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">测试摘要 (safe_test_summary)</h3>
424
+ <span id="stStatusBadge" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
425
+ </div>
426
+ <dl class="grid grid-cols-4 gap-x-3 gap-y-2 text-xs">
427
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">commands</dt><dd id="stCmdCount" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
428
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">passed</dt><dd id="stPassed" style="color: var(--pw-state-success); font-family: var(--pw-font-mono);">—</dd></div>
429
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">failed</dt><dd id="stFailed" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);">—</dd></div>
430
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">skipped</dt><dd id="stSkipped" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</dd></div>
431
+ </dl>
432
+ <div class="flex flex-col gap-0.5 min-w-0">
433
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">headline</span>
434
+ <span id="stHeadline" class="text-xs break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
435
+ </div>
436
+ <div class="flex flex-col gap-1 min-w-0">
437
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">commands</span>
438
+ <div id="stCommandsBody" class="rounded-md p-2 text-xs leading-relaxed overflow-auto" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 220px;"></div>
439
+ </div>
440
+ </div>
439
441
 
440
- <!-- Right Column (~42%) -->
441
- <div class="lg:col-span-5 flex flex-col gap-6">
442
-
443
- <!-- Result Summary Card -->
444
- <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
445
- <h2 class="text-base font-semibold mb-4" data-zh="结果摘要" data-en="Result Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">结果摘要</h2>
446
- <div class="flex flex-col gap-3">
447
- <div class="flex items-center justify-between text-sm">
448
- <span style="color: var(--pw-text-secondary);" data-zh="任务状态" data-en="Status">任务状态</span>
449
- <span id="rsStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
450
- </div>
451
- <div class="flex items-center justify-between text-sm">
452
- <span style="color: var(--pw-text-secondary);" data-zh="验证状态" data-en="Verify Status">验证状态</span>
453
- <span id="rsVerifyStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
454
- </div>
455
- <div id="rsSummaryBlock" class="hidden rounded-md p-3 text-xs leading-relaxed" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle); color: var(--pw-text-secondary); font-family: var(--pw-font-sans);">
456
- <span id="rsSummary" class="break-all whitespace-pre-wrap"></span>
457
- </div>
442
+ <!-- Safe Diff Summary -->
443
+ <div class="rounded-lg p-4 flex flex-col gap-3" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">
444
+ <div class="flex items-center justify-between">
445
+ <h3 class="text-sm font-semibold" data-zh="变更摘要" data-en="Safe Diff Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">变更摘要 (safe_diff_summary)</h3>
446
+ <span id="sdPatchMode" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
458
447
  </div>
459
- </section>
448
+ <dl class="grid grid-cols-4 gap-x-3 gap-y-2 text-xs">
449
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">files</dt><dd id="sdChangedTotal" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</dd></div>
450
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">additions</dt><dd id="sdAdditions" style="color: var(--pw-state-success); font-family: var(--pw-font-mono);">—</dd></div>
451
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">deletions</dt><dd id="sdDeletions" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);">—</dd></div>
452
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">diff_available</dt><dd id="sdDiffAvailable" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</dd></div>
453
+ </dl>
454
+ <div id="sdTruncatedBlock" class="hidden text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠ diff_truncated / large_diff_omitted</div>
455
+ <div class="flex flex-col gap-1 min-w-0">
456
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">files (path · change · kind)</span>
457
+ <div id="sdFilesBody" class="rounded-md p-2 text-xs leading-relaxed overflow-auto" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 220px;"></div>
458
+ </div>
459
+ <div id="sdHygieneBlock" class="hidden flex flex-col gap-0.5 min-w-0">
460
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">artifact_hygiene_counts</span>
461
+ <pre id="sdHygiene" class="text-xs whitespace-pre-wrap break-all" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);"></pre>
462
+ </div>
463
+ </div>
460
464
 
461
- <!-- Audit Results Card -->
462
- <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
463
- <div class="flex items-center justify-between mb-4">
464
- <h2 class="text-base font-semibold" data-zh="审计结果" data-en="Audit Result" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">审计结果</h2>
465
- <span id="auditVerdictBadge" class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap" style="background-color: var(--pw-bg-hover); color: var(--pw-text-tertiary); border: 1px solid var(--pw-border-subtle);">—</span>
465
+ <!-- Safe Audit -->
466
+ <div class="rounded-lg p-4 flex flex-col gap-3" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">
467
+ <div class="flex items-center justify-between">
468
+ <h3 class="text-sm font-semibold" data-zh="审计结论" data-en="Safe Audit" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">审计结论 (safe_audit)</h3>
469
+ <span id="saVerdictBadge" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
470
+ </div>
471
+ <dl class="grid grid-cols-3 gap-x-3 gap-y-2 text-xs">
472
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">pass</dt><dd id="saCountsPass" style="color: var(--pw-state-success); font-family: var(--pw-font-mono);">—</dd></div>
473
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">warn</dt><dd id="saCountsWarn" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">—</dd></div>
474
+ <div class="flex flex-col gap-0.5"><dt style="color: var(--pw-text-tertiary);">fail</dt><dd id="saCountsFail" style="color: var(--pw-state-error); font-family: var(--pw-font-mono);">—</dd></div>
475
+ </dl>
476
+ <div class="flex flex-col gap-0.5 min-w-0">
477
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">acceptance.verdict / status</span>
478
+ <span id="saAccVerdict" class="text-xs break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
479
+ </div>
480
+ <div class="flex flex-col gap-0.5 min-w-0">
481
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">acceptance.reason</span>
482
+ <span id="saAccReason" class="text-xs break-words" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
466
483
  </div>
467
- <div class="flex flex-col gap-3">
468
- <div class="flex items-center justify-between text-sm">
469
- <span style="color: var(--pw-text-secondary);" data-zh="范围变更" data-en="Scope Changes">范围变更</span>
470
- <span id="auditScope" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
471
- </div>
472
- <div class="flex items-center justify-between text-sm">
473
- <span style="color: var(--pw-text-secondary);" data-zh="检查时间" data-en="Checked At">检查时间</span>
474
- <span id="auditCheckedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
475
- </div>
476
- <div id="auditEmptyHint" class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无审计记录" data-en="No audit record yet">暂无审计记录</div>
484
+ <div class="flex flex-col gap-0.5 min-w-0">
485
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">next_suggested_task</span>
486
+ <span id="saAccNextTask" class="text-xs break-words" style="color: var(--pw-accent); font-family: var(--pw-font-mono);">—</span>
477
487
  </div>
478
- </section>
479
-
480
- <!-- Independent Review Card (audit_task artifact) -->
481
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
482
- <div class="flex items-center justify-between px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
483
- <div class="flex items-center gap-2">
484
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
485
- <h2 class="text-base font-semibold" data-zh="独立审计报告" data-en="Independent Review" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">独立审计报告</h2>
486
- </div>
487
- <span id="reviewVerdictBadge" class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-text-tertiary); background-color: var(--pw-bg-hover); font-family: var(--pw-font-mono);">—</span>
488
+ <div id="saFailChecksBlock" class="hidden flex flex-col gap-0.5 min-w-0">
489
+ <span class="text-xs" style="color: var(--pw-state-error);">fail_checks</span>
490
+ <pre id="saFailChecks" class="rounded-md p-2 text-xs whitespace-pre-wrap break-all" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); font-family: var(--pw-font-mono); max-height: 120px; overflow-y: auto;"></pre>
488
491
  </div>
489
- <div id="reviewEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="尚未运行 audit_task" data-en="audit_task not run yet">尚未运行 audit_task</div>
490
- <pre id="reviewContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 320px; overflow-y: auto;"></pre>
491
- </section>
492
-
493
- <!-- Verification Summary Card -->
494
- <section class="rounded-lg p-5" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
495
- <h2 class="text-base font-semibold mb-4" data-zh="验证摘要" data-en="Verification Summary" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">验证摘要</h2>
496
- <div class="flex flex-col gap-3">
497
- <div class="flex items-center justify-between text-sm">
498
- <span style="color: var(--pw-text-secondary);" data-zh="验证状态" data-en="Status">验证状态</span>
499
- <span id="vsStatus" class="font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">—</span>
500
- </div>
501
- <div class="flex items-center justify-between text-sm">
502
- <span style="color: var(--pw-text-secondary);" data-zh="检查时间" data-en="Checked At">检查时间</span>
503
- <span id="vsCheckedAt" class="truncate" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">—</span>
504
- </div>
505
- <div id="vsCommandsBlock" class="hidden rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle); color: var(--pw-text-secondary); font-family: var(--pw-font-mono);">
506
- <div class="text-xs mb-1" style="color: var(--pw-text-tertiary);" data-zh="验证命令" data-en="Commands">验证命令</div>
507
- <pre id="vsCommands" class="whitespace-pre-wrap break-all"></pre>
508
- </div>
509
- <div id="vsEmptyHint" class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无验证摘要" data-en="No verification summary">暂无验证摘要</div>
492
+ <div id="saManualBlock" class="hidden text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠ manual_verification_required</div>
493
+ <div id="saActionsBlock" class="hidden flex flex-col gap-0.5 min-w-0">
494
+ <span class="text-xs" style="color: var(--pw-text-tertiary);">recommended_next_actions</span>
495
+ <pre id="saActions" class="text-xs whitespace-pre-wrap break-all" style="color: var(--pw-text-secondary); font-family: var(--pw-font-mono);"></pre>
510
496
  </div>
511
- </section>
497
+ </div>
498
+
499
+ </div>
500
+ </section>
512
501
 
513
- <!-- Warnings / Errors Card -->
514
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
515
- <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
516
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
517
- <h2 class="text-base font-semibold" data-zh="警告 / 错误" data-en="Warnings / Errors" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">警告 / 错误</h2>
502
+ <!-- ===== Advanced Section (collapsed by default; full content loaded ON DEMAND) ===== -->
503
+ <details class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
504
+ <summary class="flex items-center gap-2 px-5 py-4 cursor-pointer select-none" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">
505
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
506
+ <span class="text-base font-semibold" data-zh="高级" data-en="Advanced">高级</span>
507
+ <span class="text-xs px-2 py-0.5 rounded-md whitespace-nowrap" style="color: var(--pw-state-warning); background-color: var(--pw-state-warning-bg); font-family: var(--pw-font-mono);">⚠️ may contain large output</span>
508
+ </summary>
509
+ <div class="flex flex-col gap-4 px-5 pb-5">
510
+ <p class="text-xs" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="以下内容仅在点击时按需加载,可能包含完整 stdout/stderr/diff 等较大输出。" data-en="The following content is loaded on demand only and may contain full stdout/stderr/diff.">以下内容仅在点击时按需加载,可能包含完整 stdout/stderr/diff 等较大输出。</p>
511
+
512
+ <!-- Advanced: full result -->
513
+ <div class="flex flex-col gap-2">
514
+ <div class="flex items-center gap-2 flex-wrap">
515
+ <button id="advBtnResult" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-primary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
516
+ <svg id="advSpinnerResult" class="hidden pw-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>
517
+ <span data-zh="加载完整结果 (get_result)" data-en="Load full result (get_result)">加载完整结果 (get_result)</span>
518
+ </button>
519
+ <span class="text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠️ Advanced view: may contain large output</span>
518
520
  </div>
519
- <div id="weEmpty" class="px-4 py-6 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="无警告或错误" data-en="No warnings or errors">无警告或错误</div>
520
- <div id="weBody" class="hidden px-4 py-3 flex flex-col gap-3">
521
- <div id="weWarningsBlock" class="hidden">
522
- <div class="text-xs mb-1.5 font-medium" style="color: var(--pw-state-warning);" data-zh="警告" data-en="Warnings">警告</div>
523
- <div id="weWarnings" class="rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-state-warning-bg); color: var(--pw-state-warning); font-family: var(--pw-font-mono);"></div>
524
- </div>
525
- <div id="weErrorsBlock" class="hidden">
526
- <div class="text-xs mb-1.5 font-medium" style="color: var(--pw-state-error);" data-zh="错误" data-en="Errors">错误</div>
527
- <div id="weErrors" class="rounded-md p-2 text-xs leading-relaxed" style="background-color: var(--pw-state-error-bg); color: var(--pw-state-error); font-family: var(--pw-font-mono);"></div>
528
- </div>
521
+ <pre id="advPreResult" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 520px; overflow-y: auto;"></pre>
522
+ </div>
523
+
524
+ <!-- Advanced: full test log -->
525
+ <div class="flex flex-col gap-2">
526
+ <div class="flex items-center gap-2 flex-wrap">
527
+ <button id="advBtnTestLog" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-primary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
528
+ <svg id="advSpinnerTestLog" class="hidden pw-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>
529
+ <span data-zh="加载完整测试日志 (get_test_log)" data-en="Load full test log (get_test_log)">加载完整测试日志 (get_test_log)</span>
530
+ </button>
531
+ <span class="text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠️ Advanced view: may contain large output</span>
529
532
  </div>
530
- </section>
533
+ <pre id="advPreTestLog" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 520px; overflow-y: auto;"></pre>
534
+ </div>
531
535
 
532
- <!-- Test Log Card -->
533
- <section class="rounded-lg overflow-hidden" style="background-color: var(--pw-bg-surface); border: 1px solid var(--pw-border);">
534
- <div class="flex items-center gap-2 px-4 py-3" style="border-bottom: 1px solid var(--pw-border);">
535
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--pw-text-secondary)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
536
- <h2 class="text-base font-semibold" data-zh="测试日志" data-en="Test Log" style="color: var(--pw-text-primary); font-family: var(--pw-font-sans);">测试日志</h2>
536
+ <!-- Advanced: full diff -->
537
+ <div class="flex flex-col gap-2">
538
+ <div class="flex items-center gap-2 flex-wrap">
539
+ <button id="advBtnDiff" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-bg-hover); color: var(--pw-text-primary); border: 1px solid var(--pw-border-subtle); font-family: var(--pw-font-sans);">
540
+ <svg id="advSpinnerDiff" class="hidden pw-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>
541
+ <span data-zh="加载完整 diff (get_diff)" data-en="Load full diff (get_diff)">加载完整 diff (get_diff)</span>
542
+ </button>
543
+ <span class="text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠️ Advanced view: may contain large output</span>
537
544
  </div>
538
- <div id="logEmpty" class="px-4 py-8 text-center text-sm" style="color: var(--pw-text-tertiary); font-family: var(--pw-font-sans);" data-zh="暂无测试日志" data-en="No test log available">暂无测试日志</div>
539
- <pre id="logContent" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 480px; overflow-y: auto;"></pre>
540
- </section>
545
+ <pre id="advPreDiff" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 520px; overflow-y: auto;"></pre>
546
+ </div>
541
547
 
548
+ <!-- Advanced: full audit_task review -->
549
+ <div class="flex flex-col gap-2">
550
+ <div class="flex items-center gap-2 flex-wrap">
551
+ <button id="advBtnAudit" class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors" style="background-color: var(--pw-accent-muted); color: var(--pw-accent); border: 1px solid rgba(45, 212, 168, 0.3); font-family: var(--pw-font-sans);">
552
+ <svg id="advSpinnerAudit" class="hidden pw-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>
553
+ <span data-zh="运行 audit_task 完整审计" data-en="Run audit_task full review">运行 audit_task 完整审计</span>
554
+ </button>
555
+ <span class="text-xs" style="color: var(--pw-state-warning); font-family: var(--pw-font-mono);">⚠️ Advanced view: may contain large output</span>
556
+ </div>
557
+ <pre id="advPreAudit" class="hidden px-4 py-3 text-xs leading-relaxed overflow-x-auto whitespace-pre-wrap break-all" style="background-color: #0d1117; color: var(--pw-text-secondary); font-family: var(--pw-font-mono); max-height: 520px; overflow-y: auto;"></pre>
558
+ </div>
542
559
  </div>
543
-
544
- </div>
560
+ </details>
545
561
 
546
562
  </div>
547
563
 
@@ -705,311 +721,171 @@ html.dark {
705
721
  show($('detailContent'));
706
722
  }
707
723
 
708
- // ---- Render task detail ----
709
- function renderDetail(data) {
710
- var taskId = data.task_id || '—';
724
+ // ---- Render Safe Summary (from 4 safe endpoints) ----
725
+ // renderHeader populates the Task Header Card from safe_result
726
+ function renderHeader(taskId, safeResult) {
711
727
  text($('breadcrumbId'), taskId);
712
728
  text($('hdrTaskId'), taskId);
713
729
 
714
- // Status
715
- var status = (data.status && data.status.status) || data.status_status || null;
730
+ var status = safeResult.status || null;
716
731
  var statusMeta = getStatusMeta(status);
717
732
  text($('hdrStatusText'), statusMeta.text);
718
733
  applyBadgeColors($('hdrStatusBadge'), statusMeta.color);
719
734
 
720
- // Phase
721
- var phase = (data.status && data.status.phase) || null;
722
- text($('hdrPhase'), getPhaseLabel(phase));
735
+ text($('hdrPhase'), getPhaseLabel(safeResult.phase));
723
736
 
724
- // Agent
725
- var agent = (data.status && data.status.agent) || null;
726
- text($('hdrAgent'), agent || '—');
737
+ // repo_path from safe_result
738
+ text($('hdrRepoPath'), safeResult.repo_path || '—');
727
739
 
728
- // Times
729
- var createdAt = (data.status && data.status.created_at) || null;
730
- var updatedAt = (data.status && data.status.updated_at) || null;
731
- text($('hdrCreatedAt'), formatTime(createdAt));
732
- text($('hdrUpdatedAt'), formatTime(updatedAt));
740
+ // created_at/updated_at not available from safe endpoints; keep placeholder
741
+ text($('hdrCreatedAt'), '—');
742
+ text($('hdrUpdatedAt'), '—');
733
743
 
734
- // Test command + timeout
735
- var testCommand = (data.status && data.status.test_command) || null;
736
- text($('hdrTestCommand'), testCommand || '—');
737
- var timeoutSeconds = (data.status && data.status.timeout_seconds);
738
- text($('hdrTimeout'), (timeoutSeconds === null || timeoutSeconds === undefined) ? '—' : String(timeoutSeconds));
744
+ text($('hdrTestCommand'), '—');
745
+ text($('hdrTimeout'), '—');
746
+ text($('hdrAgent'), '—');
747
+ text($('hdrVerifyCommands'), '—');
748
+ text($('hdrPlanId'), '—');
739
749
 
740
- // Code metadata
741
- var repoPath = (data.status && data.status.repo_path) || null;
742
- text($('hdrRepoPath'), repoPath || '—');
743
-
744
- var verifyCommands = (data.status && data.status.verify_commands) || null;
745
- text($('hdrVerifyCommands'), renderListOrJoin(verifyCommands));
746
-
747
- var planId = (data.status && data.status.plan_id) || null;
748
- text($('hdrPlanId'), planId || '—');
749
-
750
- // Error block
751
- var statusError = (data.status && data.status.error) || null;
752
- if (statusError) {
753
- text($('hdrErrorText'), escapeText(statusError));
750
+ if (safeResult.errors && safeResult.errors.length > 0) {
751
+ text($('hdrErrorText'), safeResult.errors.join('\n'));
752
+ show($('hdrErrorBlock'));
753
+ } else if (safeResult.failure_reason) {
754
+ text($('hdrErrorText'), safeResult.failure_reason);
754
755
  show($('hdrErrorBlock'));
755
756
  } else {
756
757
  hide($('hdrErrorBlock'));
757
758
  }
759
+ }
758
760
 
759
- // ---- Execution status (runtime) ----
760
- var runtime = data.runtime || null;
761
- var rtPhase = runtime ? (runtime.phase || null) : null;
762
- text($('rtPhase'), getPhaseLabel(rtPhase));
763
- var rtHeartbeat = runtime ? (runtime.last_heartbeat_at || null) : null;
764
- text($('rtHeartbeat'), formatTime(rtHeartbeat));
765
- var rtCmd = runtime ? (runtime.current_command || null) : null;
766
- text($('rtCurrentCmd'), rtCmd || '—');
767
-
768
- // ---- Result summary ----
769
- var result = data.result || null;
770
- var rsStatus = result ? (result.status || null) : null;
771
- text($('rsStatus'), rsStatus || '—');
772
- var rsVerify = result ? (result.verify_status || null) : null;
773
- text($('rsVerifyStatus'), rsVerify || '—');
774
- var rsSummary = result ? (result.summary || null) : null;
775
- if (rsSummary) {
776
- text($('rsSummary'), escapeText(rsSummary));
777
- show($('rsSummaryBlock'));
778
- } else {
779
- hide($('rsSummaryBlock'));
780
- }
761
+ // Render safe_result card
762
+ function renderSafeResult(sr) {
763
+ text($('srStatusBadge'), sr.status || '—');
764
+ applyBadgeColors($('srStatusBadge'), getStatusMeta(sr.status).color);
781
765
 
782
- // ---- Audit ----
783
- var audit = data.audit || null;
784
- var verdict = audit ? (audit.verdict || audit.decision || null) : null;
785
- applyVerdictBadge($('auditVerdictBadge'), verdict);
786
- var scopeChanges = audit ? (audit.scope_changes || null) : null;
787
- text($('auditScope'), scopeChanges || '—');
788
- var checkedAt = audit ? (audit.checked_at || null) : null;
789
- text($('auditCheckedAt'), formatTime(checkedAt));
790
- if (audit) {
791
- hide($('auditEmptyHint'));
792
- } else {
793
- show($('auditEmptyHint'));
794
- }
766
+ text($('srTerminal'), sr.terminal ? 'true' : 'false');
767
+ text($('srAcceptance'), sr.acceptance_status || '—');
768
+ text($('srChangedTotal'), sr.changed_files_total != null ? String(sr.changed_files_total) : '—');
769
+ text($('srOosTotal'), sr.out_of_scope_changes_total != null ? String(sr.out_of_scope_changes_total) : '—');
795
770
 
796
- // ---- Diff preview ----
797
- // API returns diff_patch (snake_case); also accept diff/testLog/test_log defensively.
798
- var diff = data.diff_patch || data.diff || data.diffPatch || null;
799
- var diffEl = $('diffContent');
800
- var diffEmpty = $('diffEmpty');
801
- if (diff) {
802
- text(diffEl, escapeText(diff));
803
- show(diffEl);
804
- hide(diffEmpty);
805
- } else {
806
- hide(diffEl);
807
- show(diffEmpty);
808
- }
771
+ var verifyHeadline = (sr.verification && sr.verification.headline) || null;
772
+ text($('srVerifyHeadline'), verifyHeadline || '—');
809
773
 
810
- // ---- Test log ----
811
- var testLog = data.test_log || data.testLog || null;
812
- var logEl = $('logContent');
813
- var logEmpty = $('logEmpty');
814
- if (testLog) {
815
- text(logEl, escapeText(testLog));
816
- show(logEl);
817
- hide(logEmpty);
774
+ if (sr.failure_reason || sr.failed_command) {
775
+ text($('srFailureReason'), sr.failure_reason || '—');
776
+ text($('srFailedCommand'), sr.failed_command || '');
777
+ show($('srFailureBlock'));
818
778
  } else {
819
- hide(logEl);
820
- show(logEmpty);
779
+ hide($('srFailureBlock'));
821
780
  }
822
781
 
823
- // ---- Changed files ----
824
- renderChangedFiles(data.changed_files, data.file_stats);
825
-
826
- // ---- Verify log ----
827
- var verifyLog = data.verify_log || null;
828
- var vlEl = $('verifyLogContent');
829
- var vlEmpty = $('verifyLogEmpty');
830
- if (verifyLog) {
831
- text(vlEl, escapeText(verifyLog));
832
- show(vlEl);
833
- hide(vlEmpty);
782
+ text($('srNextAction'), sr.next_action || '—');
783
+
784
+ if (sr.execution_blocked || sr.pending_reason) {
785
+ text($('srPendingReason'), sr.pending_reason || (sr.execution_blocked ? 'blocked' : '—'));
786
+ show($('srBlockedBlock'));
834
787
  } else {
835
- hide(vlEl);
836
- show(vlEmpty);
788
+ hide($('srBlockedBlock'));
837
789
  }
838
790
 
839
- // ---- Independent review (audit_task artifact) ----
840
- var review = data.independent_review || null;
841
- var rvContent = review ? (review.content || null) : null;
842
- var rvVerdict = review ? (review.verdict || null) : null;
843
- var rvEl = $('reviewContent');
844
- var rvEmpty = $('reviewEmpty');
845
- var rvBadge = $('reviewVerdictBadge');
846
- if (rvContent) {
847
- text(rvEl, escapeText(rvContent));
848
- show(rvEl);
849
- hide(rvEmpty);
791
+ if (Array.isArray(sr.warnings) && sr.warnings.length > 0) {
792
+ $('srWarnings').textContent = sr.warnings.join('\n');
793
+ show($('srWarningsBlock'));
850
794
  } else {
851
- hide(rvEl);
852
- show(rvEmpty);
795
+ hide($('srWarningsBlock'));
853
796
  }
854
- applyVerdictBadge(rvBadge, rvVerdict);
797
+ }
855
798
 
856
- // ---- Verification summary ----
857
- renderVerificationSummary(data.verification_summary);
799
+ // Render safe_test_summary card
800
+ function renderSafeTestSummary(sts) {
801
+ text($('stStatusBadge'), sts.status || '—');
802
+ applyBadgeColors($('stStatusBadge'), getStatusMeta(sts.status).color);
858
803
 
859
- // ---- Warnings / Errors ----
860
- renderWarningsErrors(data.warnings, data.errors);
804
+ text($('stCmdCount'), String(sts.command_count || 0));
805
+ text($('stPassed'), String(sts.passed_commands || 0));
806
+ text($('stFailed'), String(sts.failed_commands || 0));
807
+ text($('stSkipped'), String(sts.skipped_commands || 0));
861
808
 
862
- // ---- Stale / Reconcile banner ----
863
- renderStaleBanner(data.stale, data.reconcile);
809
+ text($('stHeadline'), sts.headline || '—');
864
810
 
865
- showDetail();
811
+ var cmds = Array.isArray(sts.commands) ? sts.commands : [];
812
+ var cmdsHtml = '';
813
+ for (var i = 0; i < cmds.length; i++) {
814
+ var cmd = cmds[i];
815
+ var statusColor = cmd.status === 'passed' ? 'var(--pw-state-success)' : (cmd.status === 'failed' || cmd.status === 'timed_out') ? 'var(--pw-state-error)' : 'var(--pw-text-secondary)';
816
+ cmdsHtml += '<div style="margin-bottom:4px;"><span style="color:' + statusColor + ';">' + escapeHtml(cmd.status || 'unknown') + '</span> <span style="color:var(--pw-text-tertiary);">exit=' + (cmd.exit_code !== null ? cmd.exit_code : '—') + '</span> <span style="color:var(--pw-text-primary);">' + escapeHtml(cmd.command || '') + '</span></div>';
817
+ }
818
+ if (!cmdsHtml) cmdsHtml = '<div style="color:var(--pw-text-tertiary);">—</div>';
819
+ $('stCommandsBody').innerHTML = cmdsHtml;
866
820
  }
867
821
 
868
- // ---- Render: changed files ----
869
- function renderChangedFiles(changedFiles, fileStats) {
870
- var emptyEl = $('cfEmpty');
871
- var bodyEl = $('cfBody');
872
- var listEl = $('cfList');
873
- var statsEl = $('cfStatsRow');
874
- var badgeEl = $('cfCountBadge');
875
-
876
- if (!changedFiles) {
877
- hide(bodyEl);
878
- hide(badgeEl);
879
- show(emptyEl);
880
- return;
881
- }
822
+ // Render safe_diff_summary card
823
+ function renderSafeDiffSummary(sds) {
824
+ text($('sdPatchMode'), sds.patch_mode || '');
825
+ text($('sdChangedTotal'), String(sds.changed_files_total || 0));
826
+ text($('sdAdditions'), String(sds.additions || 0));
827
+ text($('sdDeletions'), String(sds.deletions || 0));
828
+ text($('sdDiffAvailable'), sds.diff_available ? 'true' : 'false');
882
829
 
883
- // Build a list of paths to display.
884
- var paths = [];
885
- if (Array.isArray(changedFiles.files)) {
886
- for (var i = 0; i < changedFiles.files.length; i++) {
887
- var f = changedFiles.files[i];
888
- if (typeof f === 'string') paths.push(f);
889
- else if (f && typeof f === 'object' && typeof f.path === 'string') paths.push(f.path);
890
- }
891
- }
892
- if (paths.length === 0 && Array.isArray(changedFiles.changed_files)) {
893
- for (var j = 0; j < changedFiles.changed_files.length; j++) {
894
- var cf = changedFiles.changed_files[j];
895
- if (typeof cf === 'string') paths.push(cf);
896
- else if (cf && typeof cf === 'object' && typeof cf.path === 'string') paths.push(cf.path);
897
- }
898
- }
899
-
900
- // file_stats summary
901
- var statsHtml = '';
902
- if (fileStats && typeof fileStats === 'object') {
903
- var counts = fileStats.counts || fileStats;
904
- var statItems = [
905
- { label: 'modified', value: counts.modified != null ? counts.modified : null },
906
- { label: 'added', value: counts.added != null ? counts.added : null },
907
- { label: 'deleted', value: counts.deleted != null ? counts.deleted : null },
908
- { label: 'untracked', value: counts.untracked != null ? counts.untracked : null }
909
- ];
910
- var hasAny = false;
911
- statsHtml = statItems.map(function (it) {
912
- if (it.value === null || it.value === undefined) return '';
913
- hasAny = true;
914
- return '<div class="rounded-md px-2 py-1.5" style="background-color: var(--pw-bg); border: 1px solid var(--pw-border-subtle);">'
915
- + '<div class="text-xs" style="color: var(--pw-text-tertiary);">' + escapeHtml(it.label) + '</div>'
916
- + '<div class="text-sm font-semibold" style="color: var(--pw-text-primary); font-family: var(--pw-font-mono);">' + escapeHtml(String(it.value)) + '</div>'
917
- + '</div>';
918
- }).join('');
919
- if (hasAny) {
920
- show(statsEl);
921
- statsEl.innerHTML = statsHtml;
922
- } else {
923
- hide(statsEl);
924
- }
830
+ if (sds.diff_truncated || sds.large_diff_omitted) {
831
+ show($('sdTruncatedBlock'));
925
832
  } else {
926
- hide(statsEl);
833
+ hide($('sdTruncatedBlock'));
927
834
  }
928
835
 
929
- if (paths.length === 0) {
930
- hide(bodyEl);
931
- hide(badgeEl);
932
- show(emptyEl);
933
- return;
934
- }
935
-
936
- // Render path list (cap at 500 to avoid rendering huge DOM).
937
- var displayCount = Math.min(paths.length, 500);
938
- var listHtml = '';
939
- for (var k = 0; k < displayCount; k++) {
940
- listHtml += escapeHtml(paths[k]) + '\n';
941
- }
942
- if (paths.length > displayCount) {
943
- listHtml += '\n... (' + (paths.length - displayCount) + ' more truncated)';
836
+ var files = Array.isArray(sds.files) ? sds.files : [];
837
+ var filesHtml = '';
838
+ for (var j = 0; j < files.length; j++) {
839
+ var f = files[j];
840
+ var changeColor = (f.change === 'added' || f.change === 'A') ? 'var(--pw-state-success)' : (f.change === 'deleted' || f.change === 'D') ? 'var(--pw-state-error)' : 'var(--pw-accent)';
841
+ filesHtml += '<div style="margin-bottom:2px;"><span style="color:' + changeColor + ';">' + escapeHtml(f.change || '?') + '</span> <span style="color:var(--pw-text-tertiary);">' + escapeHtml(f.kind || 'file') + '</span> <span style="color:var(--pw-text-primary);">' + escapeHtml(f.path || '') + '</span></div>';
944
842
  }
945
- listEl.textContent = listHtml;
843
+ if (!filesHtml) filesHtml = '<div style="color:var(--pw-text-tertiary);">—</div>';
844
+ $('sdFilesBody').innerHTML = filesHtml;
946
845
 
947
- // Badge
948
- text(badgeEl, paths.length + ' 个文件');
949
- show(badgeEl);
950
-
951
- hide(emptyEl);
952
- show(bodyEl);
953
- }
954
-
955
- // ---- Render: verification summary ----
956
- function renderVerificationSummary(vs) {
957
- var statusEl = $('vsStatus');
958
- var checkedEl = $('vsCheckedAt');
959
- var cmdsBlock = $('vsCommandsBlock');
960
- var cmdsEl = $('vsCommands');
961
- var emptyHint = $('vsEmptyHint');
962
-
963
- if (!vs) {
964
- text(statusEl, '—');
965
- text(checkedEl, '—');
966
- hide(cmdsBlock);
967
- show(emptyHint);
968
- return;
969
- }
970
- hide(emptyHint);
971
- text(statusEl, vs.status || '—');
972
- text(checkedEl, formatTime(vs.checked_at));
973
- if (Array.isArray(vs.commands) && vs.commands.length > 0) {
974
- text(cmdsEl, vs.commands.map(function (c) { return JSON.stringify(c); }).join('\n'));
975
- show(cmdsBlock);
846
+ if (sds.artifact_hygiene_counts && Object.keys(sds.artifact_hygiene_counts).length > 0) {
847
+ $('sdHygiene').textContent = JSON.stringify(sds.artifact_hygiene_counts, null, 2);
848
+ show($('sdHygieneBlock'));
976
849
  } else {
977
- hide(cmdsBlock);
850
+ hide($('sdHygieneBlock'));
978
851
  }
979
852
  }
980
853
 
981
- // ---- Render: warnings / errors ----
982
- function renderWarningsErrors(warnings, errors) {
983
- var emptyEl = $('weEmpty');
984
- var bodyEl = $('weBody');
985
- var warnBlock = $('weWarningsBlock');
986
- var errBlock = $('weErrorsBlock');
987
- var warnEl = $('weWarnings');
988
- var errEl = $('weErrors');
989
-
990
- var wList = Array.isArray(warnings) ? warnings : [];
991
- var eList = Array.isArray(errors) ? errors : [];
992
-
993
- if (wList.length === 0 && eList.length === 0) {
994
- show(emptyEl);
995
- hide(bodyEl);
996
- return;
854
+ // Render safe_audit card
855
+ function renderSafeAudit(sa) {
856
+ text($('saVerdictBadge'), sa.verdict || '');
857
+ applyVerdictBadge($('saVerdictBadge'), sa.verdict);
858
+
859
+ var cc = sa.check_counts || {};
860
+ text($('saCountsPass'), String(cc.pass || 0));
861
+ text($('saCountsWarn'), String(cc.warn || 0));
862
+ text($('saCountsFail'), String(cc.fail || 0));
863
+
864
+ var acc = sa.acceptance || {};
865
+ text($('saAccVerdict'), (acc.verdict || '—') + ' / ' + (acc.status || '—'));
866
+ text($('saAccReason'), acc.reason || '—');
867
+ text($('saAccNextTask'), acc.next_suggested_task || '—');
868
+
869
+ var failChecks = Array.isArray(sa.fail_checks) ? sa.fail_checks : [];
870
+ if (failChecks.length > 0) {
871
+ $('saFailChecks').textContent = failChecks.map(function (c) { return c.name || String(c); }).join('\n');
872
+ show($('saFailChecksBlock'));
873
+ } else {
874
+ hide($('saFailChecksBlock'));
997
875
  }
998
- hide(emptyEl);
999
- show(bodyEl);
1000
876
 
1001
- if (wList.length > 0) {
1002
- warnEl.textContent = wList.map(function (w) { return String(w); }).join('\n---\n');
1003
- show(warnBlock);
877
+ if (sa.manual_verification_required) {
878
+ show($('saManualBlock'));
1004
879
  } else {
1005
- hide(warnBlock);
880
+ hide($('saManualBlock'));
1006
881
  }
1007
882
 
1008
- if (eList.length > 0) {
1009
- errEl.textContent = eList.map(function (e) { return String(e); }).join('\n---\n');
1010
- show(errBlock);
883
+ var actions = Array.isArray(sa.recommended_next_actions) ? sa.recommended_next_actions : [];
884
+ if (actions.length > 0) {
885
+ $('saActions').textContent = actions.join('\n');
886
+ show($('saActionsBlock'));
1011
887
  } else {
1012
- hide(errBlock);
888
+ hide($('saActionsBlock'));
1013
889
  }
1014
890
  }
1015
891
 
@@ -1055,7 +931,7 @@ html.dark {
1055
931
  alert('运行审计失败: ' + msg);
1056
932
  } else {
1057
933
  alert('审计完成。verdict=' + ((r.data && r.data.audit && r.data.audit.verdict) || '—'));
1058
- loadTask(taskId);
934
+ loadSafeSummary(taskId);
1059
935
  }
1060
936
  })
1061
937
  .catch(function (err) {
@@ -1089,7 +965,7 @@ html.dark {
1089
965
  } else {
1090
966
  var decision = (r.data && r.data.decision) || '—';
1091
967
  alert('Reconcile 完成: ' + decision);
1092
- loadTask(taskId);
968
+ loadSafeSummary(taskId);
1093
969
  }
1094
970
  })
1095
971
  .catch(function (err) {
@@ -1146,45 +1022,167 @@ html.dark {
1146
1022
  } catch (e) { /* ignore */ }
1147
1023
  }
1148
1024
 
1149
- // ---- API fetch ----
1150
- function loadTask(taskId) {
1025
+ // ---- Safe summary fetch (4 safe endpoints, parallel; default on init) ----
1026
+ function safeGet(url) {
1027
+ return fetch(url, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
1028
+ .then(function (res) {
1029
+ return res.json().then(function (data) {
1030
+ return { ok: res.ok, status: res.status, data: data };
1031
+ });
1032
+ });
1033
+ }
1034
+
1035
+ function loadSafeSummary(taskId) {
1151
1036
  showLoading();
1152
- var url = '/api/tasks/' + encodeURIComponent(taskId);
1153
- fetch(url, {
1154
- method: 'GET',
1155
- headers: { 'Accept': 'application/json' },
1156
- cache: 'no-store'
1157
- })
1037
+ var base = '/api/tasks/' + encodeURIComponent(taskId);
1038
+ Promise.all([
1039
+ safeGet(base + '/safe-result'),
1040
+ safeGet(base + '/safe-test-summary'),
1041
+ safeGet(base + '/safe-diff-summary'),
1042
+ safeGet(base + '/safe-audit')
1043
+ ]).then(function (results) {
1044
+ var safeResult = results[0].data || {};
1045
+ var safeTest = results[1].data || {};
1046
+ var safeDiff = results[2].data || {};
1047
+ var safeAudit = results[3].data || {};
1048
+
1049
+ // If safe-result returned an explicit error and no status, treat as not found.
1050
+ if (safeResult.error && safeResult.status === undefined) {
1051
+ showError('任务不存在', safeResult.error || ('未找到任务 ID: ' + taskId));
1052
+ return;
1053
+ }
1054
+
1055
+ renderHeader(taskId, safeResult);
1056
+ renderSafeResult(safeResult);
1057
+ renderSafeTestSummary(safeTest);
1058
+ renderSafeDiffSummary(safeDiff);
1059
+ renderSafeAudit(safeAudit);
1060
+ showDetail();
1061
+ }).catch(function (err) {
1062
+ var msg = (err && err.message) ? err.message : '未知错误';
1063
+ if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
1064
+ showError('无法连接服务', '请确认 PatchWarden Control Center 正在运行 (127.0.0.1:8090)');
1065
+ } else {
1066
+ showError('加载失败', msg);
1067
+ }
1068
+ });
1069
+ }
1070
+
1071
+ // ---- Advanced on-demand loaders (full content; only fetched when clicked) ----
1072
+ // HTML id pattern: advBtn<Suffix>, advSpinner<Suffix>, advPre<Suffix>
1073
+ function setAdvLoading(suffix, loading) {
1074
+ var spinner = $('advSpinner' + suffix);
1075
+ var btn = $('advBtn' + suffix);
1076
+ if (spinner) { if (loading) show(spinner); else hide(spinner); }
1077
+ if (btn) { btn.disabled = loading; btn.style.opacity = loading ? '0.6' : ''; }
1078
+ }
1079
+
1080
+ function showAdvPre(suffix, content) {
1081
+ var pre = $('advPre' + suffix);
1082
+ if (!pre) return;
1083
+ pre.textContent = content;
1084
+ show(pre);
1085
+ }
1086
+
1087
+ // Load full result (GET /api/tasks/:taskId) — NOT auto-loaded
1088
+ function loadFullResult(taskId) {
1089
+ setAdvLoading('Result', true);
1090
+ fetch('/api/tasks/' + encodeURIComponent(taskId), { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
1158
1091
  .then(function (res) {
1159
- if (res.status === 404) {
1160
- // Task not found
1161
- return res.text().then(function () {
1162
- throw new Error('NOT_FOUND');
1163
- });
1092
+ if (res.status === 404) throw new Error('NOT_FOUND');
1093
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1094
+ return res.json();
1095
+ })
1096
+ .then(function (data) {
1097
+ // Refresh header metadata that is only available from the full endpoint.
1098
+ if (data && data.status) {
1099
+ var s = data.status;
1100
+ text($('hdrAgent'), s.agent || '—');
1101
+ text($('hdrCreatedAt'), formatTime(s.created_at));
1102
+ text($('hdrUpdatedAt'), formatTime(s.updated_at));
1103
+ text($('hdrTestCommand'), s.test_command || '—');
1104
+ text($('hdrTimeout'), (s.timeout_seconds === null || s.timeout_seconds === undefined) ? '—' : String(s.timeout_seconds));
1105
+ text($('hdrVerifyCommands'), renderListOrJoin(s.verify_commands));
1106
+ text($('hdrPlanId'), s.plan_id || '—');
1107
+ if (s.error) {
1108
+ text($('hdrErrorText'), escapeText(s.error));
1109
+ show($('hdrErrorBlock'));
1110
+ }
1164
1111
  }
1165
- if (!res.ok) {
1166
- return res.text().then(function (body) {
1167
- throw new Error('HTTP ' + res.status + (body ? (': ' + body.slice(0, 200)) : ''));
1168
- });
1112
+ if (data && (data.stale || data.reconcile)) {
1113
+ renderStaleBanner(data.stale, data.reconcile);
1169
1114
  }
1115
+ showAdvPre('Result', JSON.stringify(data, null, 2));
1116
+ })
1117
+ .catch(function (err) {
1118
+ var msg = (err && err.message) === 'NOT_FOUND' ? '任务不存在' : ((err && err.message) || '未知错误');
1119
+ showAdvPre('Result', '⚠ ' + msg);
1120
+ })
1121
+ .finally(function () { setAdvLoading('Result', false); });
1122
+ }
1123
+
1124
+ // Load full test log portion (GET /api/tasks/:taskId, extract test_log)
1125
+ function loadFullTestLog(taskId) {
1126
+ setAdvLoading('TestLog', true);
1127
+ fetch('/api/tasks/' + encodeURIComponent(taskId), { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
1128
+ .then(function (res) {
1129
+ if (res.status === 404) throw new Error('NOT_FOUND');
1130
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1170
1131
  return res.json();
1171
1132
  })
1172
1133
  .then(function (data) {
1173
- if (!data || data.error) {
1174
- throw new Error((data && data.error) ? data.error : '响应缺少任务数据');
1175
- }
1176
- renderDetail(data);
1134
+ var testLog = (data && (data.test_log || data.testLog)) || null;
1135
+ showAdvPre('TestLog', testLog ? escapeText(testLog) : '⚠ No test_log available for this task.');
1136
+ })
1137
+ .catch(function (err) {
1138
+ var msg = (err && err.message) === 'NOT_FOUND' ? '任务不存在' : ((err && err.message) || '未知错误');
1139
+ showAdvPre('TestLog', '⚠ ' + msg);
1140
+ })
1141
+ .finally(function () { setAdvLoading('TestLog', false); });
1142
+ }
1143
+
1144
+ // Load full diff portion (GET /api/tasks/:taskId, extract diff_patch/diff)
1145
+ function loadFullDiff(taskId) {
1146
+ setAdvLoading('Diff', true);
1147
+ fetch('/api/tasks/' + encodeURIComponent(taskId), { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' })
1148
+ .then(function (res) {
1149
+ if (res.status === 404) throw new Error('NOT_FOUND');
1150
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1151
+ return res.json();
1152
+ })
1153
+ .then(function (data) {
1154
+ var diff = (data && (data.diff_patch || data.diff || data.diffPatch)) || null;
1155
+ showAdvPre('Diff', diff ? escapeText(diff) : '⚠ No diff available for this task.');
1177
1156
  })
1178
1157
  .catch(function (err) {
1179
- var msg = (err && err.message) ? err.message : '未知错误';
1180
- if (msg === 'NOT_FOUND') {
1181
- showError('任务不存在', '未找到任务 ID: ' + taskId);
1182
- } else if (err && err.name === 'TypeError' && /fetch/i.test(msg)) {
1183
- showError('无法连接服务', '请确认 PatchWarden Control Center 正在运行 (127.0.0.1:8090)');
1158
+ var msg = (err && err.message) === 'NOT_FOUND' ? '任务不存在' : ((err && err.message) || '未知错误');
1159
+ showAdvPre('Diff', '⚠ ' + msg);
1160
+ })
1161
+ .finally(function () { setAdvLoading('Diff', false); });
1162
+ }
1163
+
1164
+ // Run audit_task full review (POST /api/tasks/:taskId/audit with control token)
1165
+ function runFullAuditReview(taskId) {
1166
+ setAdvLoading('Audit', true);
1167
+ postAction('/api/tasks/' + encodeURIComponent(taskId) + '/audit')
1168
+ .then(function (res) {
1169
+ return res.json().then(function (data) { return { ok: res.ok, status: res.status, data: data }; });
1170
+ })
1171
+ .then(function (r) {
1172
+ if (!r.ok) {
1173
+ var msg = (r.data && r.data.error) || ('HTTP ' + r.status);
1174
+ showAdvPre('Audit', '⚠ ' + msg);
1184
1175
  } else {
1185
- showError('加载失败', msg);
1176
+ showAdvPre('Audit', JSON.stringify(r.data, null, 2));
1177
+ // Refresh the safe-audit card so conclusions stay in sync.
1178
+ safeGet('/api/tasks/' + encodeURIComponent(taskId) + '/safe-audit')
1179
+ .then(function (res2) { if (res2.data && !res2.data.error) renderSafeAudit(res2.data); });
1186
1180
  }
1187
- });
1181
+ })
1182
+ .catch(function (err) {
1183
+ showAdvPre('Audit', '⚠ ' + ((err && err.message) || '未知错误'));
1184
+ })
1185
+ .finally(function () { setAdvLoading('Audit', false); });
1188
1186
  }
1189
1187
 
1190
1188
  // ---- Entry point ----
@@ -1193,13 +1191,13 @@ html.dark {
1193
1191
  if (!taskId) {
1194
1192
  showError('缺少 task id', '请在 URL 中提供 ?id=<task_id>');
1195
1193
  } else {
1196
- loadTask(taskId);
1194
+ loadSafeSummary(taskId);
1197
1195
  }
1198
1196
 
1199
1197
  // ---- Action button wiring ----
1200
1198
  var btnRefresh = $('btnRefresh');
1201
1199
  if (btnRefresh) btnRefresh.addEventListener('click', function () {
1202
- if (taskId) loadTask(taskId);
1200
+ if (taskId) loadSafeSummary(taskId);
1203
1201
  });
1204
1202
  var btnCopyId = $('btnCopyId');
1205
1203
  if (btnCopyId) btnCopyId.addEventListener('click', function () {
@@ -1221,6 +1219,26 @@ html.dark {
1221
1219
  if (!confirm('确认 reconcile 该任务?将写入 reconcile 记录(不会删除任务)。')) return;
1222
1220
  runReconcile(taskId);
1223
1221
  });
1222
+
1223
+ // ---- Advanced section on-demand button wiring ----
1224
+ var advBtnResult = $('advBtnResult');
1225
+ if (advBtnResult) advBtnResult.addEventListener('click', function () {
1226
+ if (taskId) loadFullResult(taskId);
1227
+ });
1228
+ var advBtnTestLog = $('advBtnTestLog');
1229
+ if (advBtnTestLog) advBtnTestLog.addEventListener('click', function () {
1230
+ if (taskId) loadFullTestLog(taskId);
1231
+ });
1232
+ var advBtnDiff = $('advBtnDiff');
1233
+ if (advBtnDiff) advBtnDiff.addEventListener('click', function () {
1234
+ if (taskId) loadFullDiff(taskId);
1235
+ });
1236
+ var advBtnAudit = $('advBtnAudit');
1237
+ if (advBtnAudit) advBtnAudit.addEventListener('click', function () {
1238
+ if (!taskId) return;
1239
+ if (!confirm('确认运行 audit_task 完整审计?')) return;
1240
+ runFullAuditReview(taskId);
1241
+ });
1224
1242
  })();
1225
1243
  </script>
1226
1244