@cleocode/skills 2.0.0

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 (171) hide show
  1. package/dispatch-config.json +404 -0
  2. package/index.d.ts +178 -0
  3. package/index.js +405 -0
  4. package/package.json +14 -0
  5. package/profiles/core.json +7 -0
  6. package/profiles/full.json +10 -0
  7. package/profiles/minimal.json +7 -0
  8. package/profiles/recommended.json +7 -0
  9. package/provider-skills-map.json +97 -0
  10. package/skills/_shared/cleo-style-guide.md +84 -0
  11. package/skills/_shared/manifest-operations.md +810 -0
  12. package/skills/_shared/placeholders.json +433 -0
  13. package/skills/_shared/skill-chaining-patterns.md +237 -0
  14. package/skills/_shared/subagent-protocol-base.md +223 -0
  15. package/skills/_shared/task-system-integration.md +232 -0
  16. package/skills/_shared/testing-framework-config.md +110 -0
  17. package/skills/ct-cleo/SKILL.md +490 -0
  18. package/skills/ct-cleo/references/anti-patterns.md +19 -0
  19. package/skills/ct-cleo/references/loom-lifecycle.md +136 -0
  20. package/skills/ct-cleo/references/orchestrator-constraints.md +55 -0
  21. package/skills/ct-cleo/references/session-protocol.md +162 -0
  22. package/skills/ct-codebase-mapper/SKILL.md +82 -0
  23. package/skills/ct-contribution/SKILL.md +521 -0
  24. package/skills/ct-contribution/templates/contribution-init.json +21 -0
  25. package/skills/ct-dev-workflow/SKILL.md +423 -0
  26. package/skills/ct-docs-lookup/SKILL.md +66 -0
  27. package/skills/ct-docs-review/SKILL.md +175 -0
  28. package/skills/ct-docs-write/SKILL.md +108 -0
  29. package/skills/ct-documentor/SKILL.md +231 -0
  30. package/skills/ct-epic-architect/SKILL.md +305 -0
  31. package/skills/ct-epic-architect/references/bug-epic-example.md +172 -0
  32. package/skills/ct-epic-architect/references/commands.md +201 -0
  33. package/skills/ct-epic-architect/references/feature-epic-example.md +210 -0
  34. package/skills/ct-epic-architect/references/migration-epic-example.md +244 -0
  35. package/skills/ct-epic-architect/references/output-format.md +92 -0
  36. package/skills/ct-epic-architect/references/patterns.md +284 -0
  37. package/skills/ct-epic-architect/references/refactor-epic-example.md +412 -0
  38. package/skills/ct-epic-architect/references/research-epic-example.md +226 -0
  39. package/skills/ct-epic-architect/references/shell-escaping.md +86 -0
  40. package/skills/ct-epic-architect/references/skill-aware-execution.md +195 -0
  41. package/skills/ct-grade/SKILL.md +230 -0
  42. package/skills/ct-grade/agents/analysis-reporter.md +203 -0
  43. package/skills/ct-grade/agents/blind-comparator.md +157 -0
  44. package/skills/ct-grade/agents/scenario-runner.md +134 -0
  45. package/skills/ct-grade/eval-viewer/__pycache__/generate_grade_review.cpython-314.pyc +0 -0
  46. package/skills/ct-grade/eval-viewer/generate_grade_review.py +1138 -0
  47. package/skills/ct-grade/eval-viewer/generate_grade_viewer.py +544 -0
  48. package/skills/ct-grade/eval-viewer/generate_review.py +283 -0
  49. package/skills/ct-grade/eval-viewer/grade-review.html +1574 -0
  50. package/skills/ct-grade/eval-viewer/viewer.html +219 -0
  51. package/skills/ct-grade/evals/evals.json +94 -0
  52. package/skills/ct-grade/references/ab-test-methodology.md +150 -0
  53. package/skills/ct-grade/references/domains.md +137 -0
  54. package/skills/ct-grade/references/grade-spec.md +236 -0
  55. package/skills/ct-grade/references/scenario-playbook.md +234 -0
  56. package/skills/ct-grade/references/token-tracking.md +120 -0
  57. package/skills/ct-grade/scripts/__pycache__/audit_analyzer.cpython-314.pyc +0 -0
  58. package/skills/ct-grade/scripts/__pycache__/run_ab_test.cpython-314.pyc +0 -0
  59. package/skills/ct-grade/scripts/__pycache__/run_all.cpython-314.pyc +0 -0
  60. package/skills/ct-grade/scripts/__pycache__/token_tracker.cpython-314.pyc +0 -0
  61. package/skills/ct-grade/scripts/audit_analyzer.py +279 -0
  62. package/skills/ct-grade/scripts/generate_report.py +283 -0
  63. package/skills/ct-grade/scripts/run_ab_test.py +504 -0
  64. package/skills/ct-grade/scripts/run_all.py +287 -0
  65. package/skills/ct-grade/scripts/setup_run.py +183 -0
  66. package/skills/ct-grade/scripts/token_tracker.py +630 -0
  67. package/skills/ct-grade-v2-1/SKILL.md +237 -0
  68. package/skills/ct-grade-v2-1/agents/analysis-reporter.md +203 -0
  69. package/skills/ct-grade-v2-1/agents/blind-comparator.md +157 -0
  70. package/skills/ct-grade-v2-1/agents/scenario-runner.md +179 -0
  71. package/skills/ct-grade-v2-1/evals/evals.json +74 -0
  72. package/skills/ct-grade-v2-1/grade-viewer/__pycache__/build_op_stats.cpython-314.pyc +0 -0
  73. package/skills/ct-grade-v2-1/grade-viewer/__pycache__/generate_grade_review.cpython-314.pyc +0 -0
  74. package/skills/ct-grade-v2-1/grade-viewer/build_op_stats.py +174 -0
  75. package/skills/ct-grade-v2-1/grade-viewer/eval-analysis.json +41 -0
  76. package/skills/ct-grade-v2-1/grade-viewer/eval-report.md +34 -0
  77. package/skills/ct-grade-v2-1/grade-viewer/generate_grade_review.py +1023 -0
  78. package/skills/ct-grade-v2-1/grade-viewer/generate_grade_viewer.py +548 -0
  79. package/skills/ct-grade-v2-1/grade-viewer/grade-review-eval.html +613 -0
  80. package/skills/ct-grade-v2-1/grade-viewer/grade-review.html +1532 -0
  81. package/skills/ct-grade-v2-1/grade-viewer/viewer.html +620 -0
  82. package/skills/ct-grade-v2-1/manifest-entry.json +31 -0
  83. package/skills/ct-grade-v2-1/references/ab-testing.md +233 -0
  84. package/skills/ct-grade-v2-1/references/domains-ssot.md +156 -0
  85. package/skills/ct-grade-v2-1/references/grade-spec-v2.md +167 -0
  86. package/skills/ct-grade-v2-1/references/playbook-v2.md +393 -0
  87. package/skills/ct-grade-v2-1/references/token-tracking.md +202 -0
  88. package/skills/ct-grade-v2-1/scripts/generate_report.py +419 -0
  89. package/skills/ct-grade-v2-1/scripts/run_ab_test.py +493 -0
  90. package/skills/ct-grade-v2-1/scripts/run_scenario.py +396 -0
  91. package/skills/ct-grade-v2-1/scripts/setup_run.py +207 -0
  92. package/skills/ct-grade-v2-1/scripts/token_tracker.py +175 -0
  93. package/skills/ct-memory/SKILL.md +84 -0
  94. package/skills/ct-orchestrator/INSTALL.md +61 -0
  95. package/skills/ct-orchestrator/README.md +69 -0
  96. package/skills/ct-orchestrator/SKILL.md +380 -0
  97. package/skills/ct-orchestrator/manifest-entry.json +19 -0
  98. package/skills/ct-orchestrator/orchestrator-prompt.txt +17 -0
  99. package/skills/ct-orchestrator/references/SUBAGENT-PROTOCOL-BLOCK.md +66 -0
  100. package/skills/ct-orchestrator/references/autonomous-operation.md +167 -0
  101. package/skills/ct-orchestrator/references/lifecycle-gates.md +98 -0
  102. package/skills/ct-orchestrator/references/orchestrator-compliance.md +271 -0
  103. package/skills/ct-orchestrator/references/orchestrator-handoffs.md +85 -0
  104. package/skills/ct-orchestrator/references/orchestrator-patterns.md +164 -0
  105. package/skills/ct-orchestrator/references/orchestrator-recovery.md +113 -0
  106. package/skills/ct-orchestrator/references/orchestrator-spawning.md +271 -0
  107. package/skills/ct-orchestrator/references/orchestrator-tokens.md +180 -0
  108. package/skills/ct-research-agent/SKILL.md +226 -0
  109. package/skills/ct-skill-creator/.cleo/.context-state.json +13 -0
  110. package/skills/ct-skill-creator/.cleo/logs/cleo.2026-03-07.1.log +24 -0
  111. package/skills/ct-skill-creator/.cleo/tasks.db +0 -0
  112. package/skills/ct-skill-creator/SKILL.md +356 -0
  113. package/skills/ct-skill-creator/agents/analyzer.md +276 -0
  114. package/skills/ct-skill-creator/agents/comparator.md +204 -0
  115. package/skills/ct-skill-creator/agents/grader.md +225 -0
  116. package/skills/ct-skill-creator/assets/eval_review.html +146 -0
  117. package/skills/ct-skill-creator/eval-viewer/__pycache__/generate_review.cpython-314.pyc +0 -0
  118. package/skills/ct-skill-creator/eval-viewer/generate_review.py +471 -0
  119. package/skills/ct-skill-creator/eval-viewer/viewer.html +1325 -0
  120. package/skills/ct-skill-creator/manifest-entry.json +17 -0
  121. package/skills/ct-skill-creator/references/dynamic-context.md +228 -0
  122. package/skills/ct-skill-creator/references/frontmatter.md +83 -0
  123. package/skills/ct-skill-creator/references/invocation-control.md +165 -0
  124. package/skills/ct-skill-creator/references/output-patterns.md +86 -0
  125. package/skills/ct-skill-creator/references/provider-deployment.md +175 -0
  126. package/skills/ct-skill-creator/references/schemas.md +430 -0
  127. package/skills/ct-skill-creator/references/workflows.md +28 -0
  128. package/skills/ct-skill-creator/scripts/__init__.py +1 -0
  129. package/skills/ct-skill-creator/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  130. package/skills/ct-skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-314.pyc +0 -0
  131. package/skills/ct-skill-creator/scripts/__pycache__/generate_report.cpython-314.pyc +0 -0
  132. package/skills/ct-skill-creator/scripts/__pycache__/improve_description.cpython-314.pyc +0 -0
  133. package/skills/ct-skill-creator/scripts/__pycache__/init_skill.cpython-314.pyc +0 -0
  134. package/skills/ct-skill-creator/scripts/__pycache__/quick_validate.cpython-314.pyc +0 -0
  135. package/skills/ct-skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
  136. package/skills/ct-skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
  137. package/skills/ct-skill-creator/scripts/__pycache__/utils.cpython-314.pyc +0 -0
  138. package/skills/ct-skill-creator/scripts/aggregate_benchmark.py +401 -0
  139. package/skills/ct-skill-creator/scripts/generate_report.py +326 -0
  140. package/skills/ct-skill-creator/scripts/improve_description.py +247 -0
  141. package/skills/ct-skill-creator/scripts/init_skill.py +306 -0
  142. package/skills/ct-skill-creator/scripts/package_skill.py +110 -0
  143. package/skills/ct-skill-creator/scripts/quick_validate.py +97 -0
  144. package/skills/ct-skill-creator/scripts/run_eval.py +310 -0
  145. package/skills/ct-skill-creator/scripts/run_loop.py +328 -0
  146. package/skills/ct-skill-creator/scripts/utils.py +47 -0
  147. package/skills/ct-skill-validator/SKILL.md +178 -0
  148. package/skills/ct-skill-validator/agents/ecosystem-checker.md +151 -0
  149. package/skills/ct-skill-validator/assets/valid-skill-example.md +13 -0
  150. package/skills/ct-skill-validator/evals/eval_set.json +14 -0
  151. package/skills/ct-skill-validator/evals/evals.json +52 -0
  152. package/skills/ct-skill-validator/manifest-entry.json +20 -0
  153. package/skills/ct-skill-validator/references/cleo-ecosystem-rules.md +163 -0
  154. package/skills/ct-skill-validator/references/validation-rules.md +168 -0
  155. package/skills/ct-skill-validator/scripts/__init__.py +0 -0
  156. package/skills/ct-skill-validator/scripts/__pycache__/audit_body.cpython-314.pyc +0 -0
  157. package/skills/ct-skill-validator/scripts/__pycache__/check_ecosystem.cpython-314.pyc +0 -0
  158. package/skills/ct-skill-validator/scripts/__pycache__/generate_validation_report.cpython-314.pyc +0 -0
  159. package/skills/ct-skill-validator/scripts/__pycache__/validate.cpython-314.pyc +0 -0
  160. package/skills/ct-skill-validator/scripts/audit_body.py +242 -0
  161. package/skills/ct-skill-validator/scripts/check_ecosystem.py +169 -0
  162. package/skills/ct-skill-validator/scripts/check_manifest.py +172 -0
  163. package/skills/ct-skill-validator/scripts/generate_validation_report.py +442 -0
  164. package/skills/ct-skill-validator/scripts/validate.py +422 -0
  165. package/skills/ct-spec-writer/SKILL.md +189 -0
  166. package/skills/ct-stickynote/README.md +14 -0
  167. package/skills/ct-stickynote/SKILL.md +46 -0
  168. package/skills/ct-task-executor/SKILL.md +296 -0
  169. package/skills/ct-validator/SKILL.md +216 -0
  170. package/skills/manifest.json +469 -0
  171. package/skills.json +281 -0
@@ -0,0 +1,1532 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ct-grade v2.1 — Grade Review</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #0d1117;
13
+ --surface: #161b22;
14
+ --surface2: #1c2128;
15
+ --border: #30363d;
16
+ --text: #e6edf3;
17
+ --text-muted: #7d8590;
18
+ --accent: #f78166;
19
+ --radius: 6px;
20
+ --mono: 'JetBrains Mono', monospace;
21
+ --grade-a: #3fb950; --grade-a-bg: #0d2818;
22
+ --grade-b: #58a6ff; --grade-b-bg: #0d1f3c;
23
+ --grade-c: #e3b341; --grade-c-bg: #2d1d00;
24
+ --grade-d: #f0883e; --grade-d-bg: #2c1700;
25
+ --grade-f: #f85149; --grade-f-bg: #2d0f0d;
26
+ --mcp: #58a6ff; --cli: #a371f7; --tie: #7d8590;
27
+ --win: #3fb950; --loss: #f85149;
28
+ }
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+ body { font-family: 'Poppins', sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; font-size: 14px; }
31
+
32
+ /* Header */
33
+ .header { background: var(--surface); border-bottom: 1px solid var(--border); padding: .75rem 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; gap: 1rem; }
34
+ .header-left { display: flex; align-items: center; gap: .75rem; }
35
+ .header-logo { font-size: .65rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; background: var(--accent); color: #fff; padding: .2em .5em; border-radius: 3px; }
36
+ .header-title { font-size: 1rem; font-weight: 600; }
37
+ .header-subtitle { font-size: .75rem; color: var(--text-muted); }
38
+ .header-meta { font-size: .7rem; color: var(--text-muted); text-align: right; line-height: 1.6; }
39
+
40
+ /* Tabs */
41
+ .tabs-bar { background: var(--surface); border-bottom: 1px solid var(--border); display: flex; padding: 0 1.5rem; gap: .25rem; flex-shrink: 0; overflow-x: auto; }
42
+ .tab-btn { font-family: 'Poppins', sans-serif; font-size: .75rem; font-weight: 500; padding: .6rem .9rem; border: none; background: transparent; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; transition: color .15s, border-color .15s; letter-spacing: .02em; white-space: nowrap; }
43
+ .tab-btn:hover { color: var(--text); }
44
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
45
+ .tab-badge { display: inline-block; background: var(--surface2); border-radius: 9px; font-size: .65rem; padding: .1em .45em; margin-left: .3em; color: var(--text-muted); }
46
+ .tab-btn.active .tab-badge { background: rgba(247,129,102,.15); color: var(--accent); }
47
+
48
+ /* Layout */
49
+ .main { display: flex; flex: 1; overflow: hidden; }
50
+ .tab-panel { display: none; width: 100%; overflow: hidden; }
51
+ .tab-panel.active { display: flex; }
52
+ .sidebar { width: 260px; min-width: 200px; border-right: 1px solid var(--border); background: var(--surface); display: flex; flex-direction: column; flex-shrink: 0; overflow-y: auto; }
53
+ .sidebar-header { font-size: .65rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted); padding: .75rem 1rem .4rem; }
54
+ .sidebar-item { padding: .55rem 1rem; cursor: pointer; display: flex; align-items: center; gap: .5rem; border-left: 3px solid transparent; transition: background .1s; font-size: .8rem; }
55
+ .sidebar-item:hover { background: var(--surface2); }
56
+ .sidebar-item.active { background: var(--surface2); border-left-color: var(--accent); }
57
+ .item-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
+ .item-score { font-family: var(--mono); font-size: .7rem; font-weight: 500; }
59
+ .item-scenario { font-size: .6rem; color: var(--text-muted); background: var(--surface2); border: 1px solid var(--border); padding: .05em .35em; border-radius: 3px; margin-left: .25rem; }
60
+ .content { flex: 1; overflow-y: auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1.25rem; }
61
+ .scroll-panel { width: 100%; padding: 1.5rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1.25rem; }
62
+
63
+ /* Cards */
64
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; }
65
+ .card-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
66
+ .score-block { display: flex; align-items: center; gap: 1rem; }
67
+ .letter-badge { font-size: 2.5rem; font-weight: 700; width: 64px; height: 64px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
68
+ .letter-badge.sm { font-size: 1.1rem; width: 36px; height: 36px; border-radius: 5px; }
69
+ .letter-badge.A { background: var(--grade-a-bg); color: var(--grade-a); }
70
+ .letter-badge.B { background: var(--grade-b-bg); color: var(--grade-b); }
71
+ .letter-badge.C { background: var(--grade-c-bg); color: var(--grade-c); }
72
+ .letter-badge.D { background: var(--grade-d-bg); color: var(--grade-d); }
73
+ .letter-badge.F { background: var(--grade-f-bg); color: var(--grade-f); }
74
+ .score-details h2 { font-size: 1.5rem; font-weight: 700; line-height: 1; }
75
+ .score-details .pct { font-size: .85rem; color: var(--text-muted); }
76
+ .session-chip { font-family: var(--mono); font-size: .65rem; color: var(--text-muted); background: var(--surface2); padding: .2em .5em; border-radius: 3px; }
77
+ .section-title { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); margin-bottom: .75rem; }
78
+
79
+ /* Dimensions */
80
+ .dimensions { display: flex; flex-direction: column; gap: .65rem; }
81
+ .dim-row { display: flex; align-items: center; gap: .75rem; }
82
+ .dim-name { font-size: .7rem; font-weight: 500; width: 155px; flex-shrink: 0; }
83
+ .dim-bar-wrap { flex: 1; height: 8px; background: var(--surface2); border-radius: 4px; overflow: hidden; }
84
+ .dim-bar { height: 100%; border-radius: 4px; transition: width .4s ease; }
85
+ .dim-score { font-family: var(--mono); font-size: .7rem; width: 42px; text-align: right; flex-shrink: 0; }
86
+
87
+ /* Flags */
88
+ .flags-section h3 { font-size: .75rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: .07em; margin-bottom: .6rem; }
89
+ .flag-item { display: flex; align-items: flex-start; gap: .5rem; font-size: .78rem; padding: .4rem .6rem; background: var(--grade-f-bg); border-left: 3px solid var(--grade-f); border-radius: 0 var(--radius) var(--radius) 0; line-height: 1.4; margin-bottom: .35rem; }
90
+ .no-flags { font-size: .78rem; color: var(--win); background: var(--grade-a-bg); padding: .4rem .6rem; border-left: 3px solid var(--win); border-radius: 0 var(--radius) var(--radius) 0; }
91
+
92
+ /* Evidence collapsible */
93
+ .evidence-toggle { font-size: .72rem; color: var(--accent); cursor: pointer; background: none; border: none; padding: .25rem 0; }
94
+ .evidence-toggle:hover { text-decoration: underline; }
95
+ .evidence-list { display: none; padding-left: .75rem; }
96
+ .evidence-list.open { display: block; }
97
+ .ev-bullet { font-size: .74rem; padding: .15rem 0; color: var(--text-muted); }
98
+
99
+ /* Token meta */
100
+ .token-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: .6rem; }
101
+ .token-stat { background: var(--surface2); border-radius: var(--radius); padding: .6rem .75rem; }
102
+ .token-stat-label { font-size: .6rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); margin-bottom: .2rem; }
103
+ .token-stat-value { font-family: var(--mono); font-size: .95rem; font-weight: 500; }
104
+ .confidence-badge { display: inline-block; padding: .15em .5em; border-radius: 9px; font-size: .65rem; font-weight: 600; text-transform: uppercase; }
105
+ .confidence-badge.real { background: var(--grade-a-bg); color: var(--grade-a); }
106
+ .confidence-badge.high { background: var(--grade-b-bg); color: var(--grade-b); }
107
+ .confidence-badge.estimated { background: var(--grade-c-bg); color: var(--grade-c); }
108
+ .confidence-badge.coarse { background: var(--grade-d-bg); color: var(--grade-d); }
109
+
110
+ /* Tables */
111
+ table.data-table { width: 100%; border-collapse: collapse; font-size: .76rem; }
112
+ .data-table th { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); padding: .5rem .75rem; text-align: left; background: var(--surface); border-bottom: 1px solid var(--border); white-space: nowrap; position: sticky; top: 0; z-index: 1; }
113
+ .data-table td { padding: .45rem .75rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
114
+ .data-table tr:hover td { background: var(--surface2); }
115
+ .table-wrap { overflow-x: auto; }
116
+ .op-name { font-family: var(--mono); font-size: .72rem; }
117
+
118
+ /* Pills */
119
+ .pill { display: inline-block; padding: .15em .5em; border-radius: 9px; font-size: .65rem; font-weight: 600; text-transform: uppercase; white-space: nowrap; }
120
+ .pill.mcp { background: rgba(88,166,255,.15); color: var(--mcp); }
121
+ .pill.cli { background: rgba(163,113,247,.15); color: var(--cli); }
122
+ .pill.tie { background: var(--surface2); color: var(--text-muted); }
123
+ .pill.query { background: rgba(88,166,255,.15); color: var(--mcp); }
124
+ .pill.mutate { background: rgba(247,129,102,.15); color: var(--accent); }
125
+ .pill.active { background: var(--grade-a-bg); color: var(--grade-a); }
126
+ .pill.ended { background: var(--surface2); color: var(--text-muted); }
127
+ .pill.pass { background: var(--grade-a-bg); color: var(--grade-a); }
128
+ .pill.fail { background: var(--grade-f-bg); color: var(--grade-f); }
129
+ .pill.not-run { background: var(--surface2); color: var(--text-muted); }
130
+
131
+ /* Delta colors */
132
+ .delta-pos { color: var(--loss); font-family: var(--mono); font-size: .72rem; }
133
+ .delta-neg { color: var(--win); font-family: var(--mono); font-size: .72rem; }
134
+ .delta-zero { color: var(--text-muted); font-family: var(--mono); font-size: .72rem; }
135
+
136
+ /* Bar charts */
137
+ .bar-chart { display: flex; flex-direction: column; gap: .5rem; }
138
+ .bar-row { display: flex; align-items: center; gap: .75rem; }
139
+ .bar-label { font-family: var(--mono); font-size: .72rem; width: 130px; flex-shrink: 0; text-align: right; }
140
+ .bar-track { flex: 1; height: 16px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
141
+ .bar-fill { height: 100%; border-radius: 3px; transition: width .4s ease; }
142
+ .bar-val { font-family: var(--mono); font-size: .7rem; width: 80px; flex-shrink: 0; }
143
+ .chart-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; }
144
+ .chart-title { font-size: .8rem; font-weight: 600; margin-bottom: 1rem; }
145
+
146
+ /* Summary grid */
147
+ .stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: .75rem; }
148
+ .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .85rem 1rem; }
149
+ .stat-label { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); margin-bottom: .3rem; }
150
+ .stat-value { font-family: var(--mono); font-size: 1.3rem; font-weight: 500; }
151
+ .stat-value.win { color: var(--win); }
152
+ .stat-value.loss { color: var(--loss); }
153
+ .stat-value.neutral { color: var(--text-muted); }
154
+
155
+ /* Winner banner */
156
+ .winner-banner { display: flex; align-items: center; gap: .6rem; padding: .6rem 1rem; border-radius: var(--radius); font-size: .8rem; font-weight: 600; }
157
+ .winner-banner.mcp { background: rgba(88,166,255,.1); color: var(--mcp); }
158
+ .winner-banner.cli { background: rgba(163,113,247,.1); color: var(--cli); }
159
+ .winner-banner.tie { background: var(--surface2); color: var(--text-muted); }
160
+
161
+ /* Filter bar */
162
+ .filter-bar { display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; padding: .5rem 0; }
163
+ .filter-bar select, .filter-bar input { font-family: var(--mono); font-size: .72rem; background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); padding: .35rem .5rem; outline: none; }
164
+ .filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
165
+ .filter-bar label { font-size: .68rem; color: var(--text-muted); font-weight: 500; }
166
+
167
+ /* Session row expand */
168
+ .session-row { cursor: pointer; }
169
+ .session-expand { display: none; }
170
+ .session-expand.open { display: table-row; }
171
+ .session-expand td { padding: 1rem; background: var(--surface2); }
172
+ .pulse-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--grade-a); animation: pulse 1.5s ease-in-out infinite; margin-right: .35rem; vertical-align: middle; }
173
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
174
+
175
+ /* Session detail expand */
176
+ .session-detail { padding: .25rem 0; }
177
+ .session-detail-header { display: flex; align-items: center; gap: 1.5rem; padding: .5rem 0 .75rem; flex-wrap: wrap; }
178
+ .session-detail-stat { font-size: .72rem; color: var(--text-muted); }
179
+ .session-detail-stat strong { color: var(--text); font-weight: 600; }
180
+ .token-summary { display: flex; gap: 1.25rem; padding: .5rem .75rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: .75rem; flex-wrap: wrap; }
181
+ .token-stat { font-size: .72rem; color: var(--text-muted); }
182
+ .token-stat strong { font-family: var(--mono); color: var(--accent); }
183
+ .audit-table-wrap { overflow-x: auto; }
184
+ .audit-table { width: 100%; border-collapse: collapse; font-size: .72rem; }
185
+ .audit-table th { text-align: left; padding: .4rem .6rem; font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .07em; color: var(--text-muted); border-bottom: 1px solid var(--border); white-space: nowrap; }
186
+ .audit-table td { padding: .35rem .6rem; border-bottom: 1px solid rgba(48,54,61,.5); vertical-align: middle; white-space: nowrap; }
187
+ .audit-table tr:last-child td { border-bottom: none; }
188
+ .audit-table tr:hover td { background: rgba(255,255,255,.02); }
189
+ .audit-time { font-family: var(--mono); color: var(--text-muted); font-size: .68rem; }
190
+ .audit-domain { font-family: var(--mono); font-size: .72rem; }
191
+ .audit-op { font-family: var(--mono); font-size: .75rem; font-weight: 500; }
192
+ .audit-source { font-size: .68rem; color: var(--text-muted); }
193
+ .audit-ms { font-family: var(--mono); font-size: .7rem; text-align: right; color: var(--text-muted); }
194
+ .audit-ms.slow { color: var(--grade-c); }
195
+ .audit-ms.vslow { color: var(--grade-d); }
196
+ .audit-success { text-align: center; font-size: .85rem; }
197
+ .gw-pill { display: inline-block; padding: .12em .5em; border-radius: 3px; font-size: .65rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
198
+ .gw-pill.mutate { background: rgba(247,129,102,.15); color: #f78166; border: 1px solid rgba(247,129,102,.3); }
199
+ .gw-pill.query { background: rgba(88,166,255,.15); color: #58a6ff; border: 1px solid rgba(88,166,255,.3); }
200
+ .audit-section-title { font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); padding: .6rem 0 .4rem; }
201
+ .audit-loading { font-size: .75rem; color: var(--text-muted); padding: .5rem 0; font-style: italic; }
202
+ .audit-count-chip { font-family: var(--mono); font-size: .65rem; background: var(--surface); border: 1px solid var(--border); border-radius: 9px; padding: .1em .5em; margin-left: .5rem; color: var(--text-muted); }
203
+ .session-chain { display: flex; align-items: center; gap: .5rem; padding: .4rem 0 .6rem; font-size: .72rem; flex-wrap: wrap; }
204
+ .session-chain-label { color: var(--text-muted); font-size: .65rem; text-transform: uppercase; letter-spacing: .07em; margin-right: .25rem; }
205
+ .chain-sep { color: var(--text-muted); }
206
+ .chain-current { background: var(--accent); color: #fff; }
207
+ .session-scope { font-size: .72rem; color: var(--text-muted); padding: .25rem 0 .5rem; }
208
+ .session-scope strong { color: var(--text); }
209
+ .debrief-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .6rem .85rem; margin-bottom: .6rem; display: flex; flex-direction: column; gap: .35rem; }
210
+ .debrief-row { font-size: .72rem; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
211
+ .debrief-label { color: var(--text-muted); font-size: .68rem; text-transform: uppercase; letter-spacing: .06em; min-width: 90px; }
212
+ .debrief-note { font-size: .73rem; color: var(--text); font-style: italic; padding: .25rem 0; }
213
+ .git-branch { font-family: var(--mono); font-size: .7rem; color: var(--grade-b); }
214
+ .git-hash { font-family: var(--mono); font-size: .68rem; color: var(--text-muted); }
215
+ .uncommitted-badge { background: rgba(248,81,73,.15); color: var(--grade-f); border: 1px solid rgba(248,81,73,.3); padding: .1em .4em; border-radius: 3px; font-size: .63rem; font-weight: 600; }
216
+ .clean-badge { background: rgba(63,185,80,.15); color: var(--grade-a); border: 1px solid rgba(63,185,80,.3); padding: .1em .4em; border-radius: 3px; font-size: .63rem; }
217
+ .notes-toggle { font-size: .68rem; color: var(--accent); cursor: pointer; user-select: none; }
218
+ .notes-list { display: none; }
219
+ .notes-list.open { display: block; }
220
+ .note-entry { font-size: .7rem; color: var(--text-muted); padding: .2rem 0; border-bottom: 1px solid var(--border); display: flex; gap: .5rem; }
221
+ .note-ts { font-family: var(--mono); color: var(--text-muted); opacity: .6; flex-shrink: 0; font-size: .67rem; }
222
+ .note-text { color: var(--text); }
223
+
224
+ /* Eval cards */
225
+ .eval-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; }
226
+ .eval-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .75rem; }
227
+ .eval-card-title { font-weight: 600; font-size: .85rem; }
228
+ .pass-rate-bar { width: 120px; height: 6px; background: var(--surface2); border-radius: 3px; overflow: hidden; }
229
+ .pass-rate-fill { height: 100%; border-radius: 3px; background: var(--grade-a); }
230
+ .expectation-row { display: flex; align-items: flex-start; gap: .5rem; font-size: .76rem; padding: .3rem 0; border-bottom: 1px solid var(--border); }
231
+ .expectation-row:last-child { border-bottom: none; }
232
+ .expect-icon { flex-shrink: 0; font-size: .8rem; }
233
+ .expect-evidence { font-size: .7rem; color: var(--text-muted); margin-top: .15rem; }
234
+
235
+ /* Note block */
236
+ .note-block { font-size: .72rem; color: var(--text-muted); background: var(--grade-c-bg); border-left: 3px solid var(--grade-c); padding: .5rem .75rem; border-radius: 0 4px 4px 0; }
237
+
238
+ /* Empty state */
239
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .75rem; padding: 3rem 2rem; color: var(--text-muted); text-align: center; }
240
+ .empty-state h3 { font-size: .9rem; color: var(--text); }
241
+ .empty-state p { font-size: .78rem; max-width: 380px; line-height: 1.6; }
242
+ .empty-cmd { font-family: var(--mono); background: var(--surface2); padding: .25em .5em; border-radius: 3px; font-size: .74rem; display: inline-block; margin-top: .25rem; }
243
+
244
+ /* Scrollbar */
245
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
246
+ ::-webkit-scrollbar-track { background: transparent; }
247
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
248
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
249
+ </style>
250
+ </head>
251
+ <body>
252
+
253
+ <div class="header">
254
+ <div class="header-left">
255
+ <span class="header-logo">CLEO</span>
256
+ <div>
257
+ <div class="header-title" id="hdr-title">Grade Review</div>
258
+ <div class="header-subtitle" id="hdr-sub"></div>
259
+ </div>
260
+ </div>
261
+ <div class="header-meta" id="hdr-meta"></div>
262
+ </div>
263
+
264
+ <div class="tabs-bar">
265
+ <button class="tab-btn active" data-tab="grades">Grades <span class="tab-badge" id="tb-grades">0</span></button>
266
+ <button class="tab-btn" data-tab="sessions">Sessions <span class="tab-badge" id="tb-sessions">0</span></button>
267
+ <button class="tab-btn" data-tab="ab">A/B <span class="tab-badge" id="tb-ab">0</span></button>
268
+ <button class="tab-btn" data-tab="tokens">Token Economy</button>
269
+ <button class="tab-btn" data-tab="matrix">Operation Matrix <span class="tab-badge" id="tb-matrix">0</span></button>
270
+ <button class="tab-btn" data-tab="evals">Eval Report <span class="tab-badge" id="tb-evals">0</span></button>
271
+ </div>
272
+
273
+ <div class="main">
274
+ <div class="tab-panel active" id="panel-grades">
275
+ <div class="sidebar" id="grade-sidebar"></div>
276
+ <div class="content" id="grade-content"></div>
277
+ </div>
278
+ <div class="tab-panel" id="panel-sessions">
279
+ <div class="scroll-panel" id="sessions-content"></div>
280
+ </div>
281
+ <div class="tab-panel" id="panel-ab">
282
+ <div class="scroll-panel" id="ab-content"></div>
283
+ </div>
284
+ <div class="tab-panel" id="panel-tokens">
285
+ <div class="scroll-panel" id="tokens-content"></div>
286
+ </div>
287
+ <div class="tab-panel" id="panel-matrix">
288
+ <div class="scroll-panel" id="matrix-content"></div>
289
+ </div>
290
+ <div class="tab-panel" id="panel-evals">
291
+ <div class="scroll-panel" id="evals-content"></div>
292
+ </div>
293
+ </div>
294
+
295
+ <script id="grade-data" type="application/json">{{EMBEDDED_DATA}}</script>
296
+ <script>
297
+ // ── Data ──────────────────────────────────────────────────────────────────────
298
+ var DATA = {};
299
+ try { DATA = JSON.parse(document.getElementById('grade-data').textContent || '{}'); } catch(e) { DATA = {}; }
300
+
301
+ // ── Helpers ───────────────────────────────────────────────────────────────────
302
+ function scoreToLetter(score, max) {
303
+ if (score == null) return '?';
304
+ var pct = (score / (max || 100)) * 100;
305
+ if (pct >= 90) return 'A';
306
+ if (pct >= 80) return 'B';
307
+ if (pct >= 70) return 'C';
308
+ if (pct >= 60) return 'D';
309
+ return 'F';
310
+ }
311
+ function gradeColor(letter) {
312
+ var m = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
313
+ return m[letter] || 'var(--text-muted)';
314
+ }
315
+ var DIM_NAMES = ['S1 Session Discipline', 'S2 Task Efficiency', 'S3 Task Hygiene', 'S4 Protocol Adherence', 'S5 MCP Gateway'];
316
+ var DIM_KEYS = ['sessionDiscipline', 'taskEfficiency', 'taskHygiene', 'protocolAdherence', 'mcpGateway'];
317
+ var DIM_COLORS = ['#a371f7', '#58a6ff', '#3fb950', '#e3b341', '#f78166'];
318
+
319
+ function fmtNum(n, fallback) {
320
+ if (n == null) return fallback !== undefined ? fallback : '\u2014';
321
+ return typeof n === 'number' && !Number.isInteger(n) ? n.toFixed(1) : String(n);
322
+ }
323
+ function fmtDate(s) {
324
+ if (!s) return '';
325
+ try { return new Date(s).toLocaleString(undefined, { dateStyle: 'short', timeStyle: 'short' }); } catch(e) { return s; }
326
+ }
327
+ function fmtDuration(ms) {
328
+ if (!ms && ms !== 0) return '\u2014';
329
+ if (ms < 1000) return ms + 'ms';
330
+ var s = Math.round(ms / 1000);
331
+ if (s < 60) return s + 's';
332
+ var m = Math.floor(s / 60);
333
+ return m + 'm ' + (s % 60) + 's';
334
+ }
335
+
336
+ // Safe element builders
337
+ function mk(tag, cls, text) {
338
+ var e = document.createElement(tag);
339
+ if (cls) e.className = cls;
340
+ if (text != null) e.textContent = String(text);
341
+ return e;
342
+ }
343
+ function mks(tag, css, text) {
344
+ var e = document.createElement(tag);
345
+ if (css) e.style.cssText = css;
346
+ if (text != null) e.textContent = String(text);
347
+ return e;
348
+ }
349
+ function app(parent) {
350
+ for (var i = 1; i < arguments.length; i++) {
351
+ if (arguments[i] != null) parent.appendChild(arguments[i]);
352
+ }
353
+ return parent;
354
+ }
355
+ function emptyState(title, desc, cmd) {
356
+ var d = mk('div', 'empty-state');
357
+ app(d, mk('h3', null, title), mk('p', null, desc));
358
+ if (cmd) app(d, mk('span', 'empty-cmd', cmd));
359
+ return d;
360
+ }
361
+
362
+ // ── Tab switching ─────────────────────────────────────────────────────────────
363
+ document.querySelectorAll('.tab-btn').forEach(function(btn) {
364
+ btn.addEventListener('click', function() {
365
+ document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
366
+ document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
367
+ btn.classList.add('active');
368
+ document.getElementById('panel-' + btn.dataset.tab).classList.add('active');
369
+ });
370
+ });
371
+
372
+ // ── Tab 1: Grades ─────────────────────────────────────────────────────────────
373
+ function renderGradesTab(grades) {
374
+ var sidebar = document.getElementById('grade-sidebar');
375
+ var content = document.getElementById('grade-content');
376
+ sidebar.textContent = '';
377
+ content.textContent = '';
378
+ if (!grades || !grades.length) {
379
+ content.appendChild(emptyState('No grade results yet', 'Run a scenario to generate grades.', 'python scripts/run_scenario.py --scenario S1'));
380
+ return;
381
+ }
382
+ document.getElementById('tb-grades').textContent = grades.length;
383
+ app(sidebar, mk('div', 'sidebar-header', 'Sessions'));
384
+ grades.forEach(function(g, i) {
385
+ var score = g.totalScore != null ? g.totalScore : g.score;
386
+ var letter = scoreToLetter(score, g.maxScore);
387
+ var sid = (g.sessionId || '').slice(0, 22) || ('Run ' + (i + 1));
388
+ var item = mk('div', 'sidebar-item' + (i === 0 ? ' active' : ''));
389
+ var ls = mks('span', 'font-weight:700;font-size:.85rem;color:' + gradeColor(letter) + ';flex-shrink:0;width:18px;', letter);
390
+ var lab = mk('span', 'item-label', sid);
391
+ var sc = mk('span', 'item-score');
392
+ sc.textContent = score != null ? (score + '/100') : '\u2014';
393
+ sc.style.color = gradeColor(letter);
394
+ app(item, ls, lab);
395
+ if (g._scenarioId || g.scenario) {
396
+ app(item, mk('span', 'item-scenario', g._scenarioId || g.scenario));
397
+ }
398
+ app(item, sc);
399
+ item.addEventListener('click', (function(g2, item2) {
400
+ return function() {
401
+ sidebar.querySelectorAll('.sidebar-item').forEach(function(x) { x.classList.remove('active'); });
402
+ item2.classList.add('active');
403
+ renderGradeDetail(g2, content);
404
+ };
405
+ })(g, item));
406
+ sidebar.appendChild(item);
407
+ });
408
+ renderGradeDetail(grades[0], content);
409
+ }
410
+
411
+ function renderGradeDetail(g, container) {
412
+ container.textContent = '';
413
+ var score = g.totalScore != null ? g.totalScore : (g.score || 0);
414
+ var max = g.maxScore || 100;
415
+ var letter = scoreToLetter(score, max);
416
+ var pct = Math.round((score / max) * 100);
417
+ var dims = g.dimensions || {};
418
+ var flags = g.flags || [];
419
+ var tmeta = g._tokenMeta || null;
420
+ var scenario = g._scenarioId || g.scenario || '';
421
+
422
+ // Score card
423
+ var card = mk('div', 'card');
424
+ var hdr = mk('div', 'card-header');
425
+ var sb = mk('div', 'score-block');
426
+ var badge = mk('div', 'letter-badge ' + letter, letter);
427
+ var sd = mk('div', 'score-details');
428
+ var h2 = mk('h2'); h2.textContent = score + '/' + max; h2.style.color = gradeColor(letter);
429
+ var pctEl = mk('div', 'pct', pct + '% \u2014 Grade ' + letter + (scenario ? ' \u2014 ' + scenario : ''));
430
+ app(sd, h2, pctEl);
431
+ if (g.timestamp) app(sd, mks('div', 'font-size:.68rem;color:var(--text-muted);margin-top:.25rem;', fmtDate(g.timestamp)));
432
+ app(sb, badge, sd);
433
+ app(hdr, sb);
434
+ if (g.sessionId) app(hdr, mk('code', 'session-chip', g.sessionId));
435
+ card.appendChild(hdr);
436
+
437
+ // Dimension bars
438
+ var dimsWrap = mk('div', 'dimensions');
439
+ DIM_KEYS.forEach(function(key, i) {
440
+ var d = dims[key]; if (!d) return;
441
+ var ds = d.score || 0, dm = d.max || 20, dp = Math.round((ds / dm) * 100);
442
+ var row = mk('div', 'dim-row');
443
+ var bar = mk('div', 'dim-bar');
444
+ bar.style.cssText = 'width:' + dp + '%;background:' + DIM_COLORS[i] + ';';
445
+ var bw = mk('div', 'dim-bar-wrap'); bw.appendChild(bar);
446
+ var sl = mk('span', 'dim-score', ds + '/' + dm);
447
+ sl.style.color = DIM_COLORS[i];
448
+ app(row, mk('span', 'dim-name', DIM_NAMES[i]), bw, sl);
449
+ dimsWrap.appendChild(row);
450
+ });
451
+ card.appendChild(dimsWrap);
452
+ container.appendChild(card);
453
+
454
+ // Flags
455
+ var fs = mk('div', 'card flags-section');
456
+ var ft = mk('h3', null, 'Flags' + (flags.length ? ' (' + flags.length + ')' : ''));
457
+ fs.appendChild(ft);
458
+ if (!flags.length) {
459
+ fs.appendChild(mk('div', 'no-flags', '\u2713 Clean \u2014 all protocol checks passed'));
460
+ } else {
461
+ flags.forEach(function(f) {
462
+ var item = mk('div', 'flag-item');
463
+ app(item, mks('span', 'flex-shrink:0;', '\u26A0'), mk('span', null, f));
464
+ fs.appendChild(item);
465
+ });
466
+ }
467
+ container.appendChild(fs);
468
+
469
+ // Evidence (collapsed, expandable per dimension)
470
+ var hasEvidence = DIM_KEYS.some(function(k) { return dims[k] && dims[k].evidence && dims[k].evidence.length; });
471
+ if (hasEvidence) {
472
+ var ec = mk('div', 'card');
473
+ ec.appendChild(mk('div', 'section-title', 'Evidence'));
474
+ DIM_KEYS.forEach(function(key, i) {
475
+ var d = dims[key]; if (!d || !d.evidence || !d.evidence.length) return;
476
+ var dimLabel = mk('div', 'dim-name', DIM_NAMES[i]);
477
+ dimLabel.style.cssText = 'color:' + DIM_COLORS[i] + ';width:auto;margin-bottom:.25rem;';
478
+ ec.appendChild(dimLabel);
479
+ var toggle = mk('button', 'evidence-toggle', 'Show ' + d.evidence.length + ' evidence item(s)');
480
+ var list = mk('div', 'evidence-list');
481
+ d.evidence.forEach(function(ev) { list.appendChild(mk('div', 'ev-bullet', '\u2022 ' + ev)); });
482
+ toggle.addEventListener('click', function() {
483
+ var open = list.classList.toggle('open');
484
+ toggle.textContent = (open ? 'Hide' : 'Show ' + d.evidence.length) + ' evidence item(s)';
485
+ });
486
+ app(ec, toggle, list);
487
+ });
488
+ container.appendChild(ec);
489
+ }
490
+
491
+ // Token display
492
+ var tokCard = mk('div', 'card');
493
+ tokCard.appendChild(mk('div', 'section-title', 'Token Metadata'));
494
+ var grid = mk('div', 'token-grid');
495
+ function addStat(label, value) {
496
+ var s = mk('div', 'token-stat');
497
+ app(s, mk('div', 'token-stat-label', label), mk('div', 'token-stat-value', fmtNum(value, '\u2014')));
498
+ grid.appendChild(s);
499
+ }
500
+ if (tmeta && tmeta.confidence !== 'heuristic') {
501
+ var conf = (tmeta.confidence || tmeta.estimationMethod || 'estimated').toLowerCase();
502
+ var confBadge = mk('span', 'confidence-badge ' + conf, conf.toUpperCase());
503
+ var confWrap = mk('div', 'token-stat');
504
+ app(confWrap, mk('div', 'token-stat-label', 'Confidence'), confBadge);
505
+ grid.appendChild(confWrap);
506
+ if (tmeta.totalEstimatedTokens || tmeta.total_tokens) addStat('Total Tokens', tmeta.totalEstimatedTokens || tmeta.total_tokens);
507
+ if (tmeta.inputTokens) addStat('Input', tmeta.inputTokens);
508
+ if (tmeta.outputTokens) addStat('Output', tmeta.outputTokens);
509
+ if (tmeta.cacheReadTokens) addStat('Cache Read', tmeta.cacheReadTokens);
510
+ } else if (g.entryCount) {
511
+ var coarseBadge = mk('span', 'confidence-badge coarse', 'COARSE');
512
+ var cWrap = mk('div', 'token-stat');
513
+ app(cWrap, mk('div', 'token-stat-label', 'Confidence'), coarseBadge);
514
+ grid.appendChild(cWrap);
515
+ addStat('Estimate', '~' + (g.entryCount * 150) + 't');
516
+ addStat('Basis', g.entryCount + ' entries x ~150t');
517
+ } else {
518
+ addStat('Note', 'Enable OTEL for token data');
519
+ }
520
+ app(tokCard, grid);
521
+ container.appendChild(tokCard);
522
+ }
523
+
524
+ // ── Tab 2: Sessions ───────────────────────────────────────────────────────────
525
+ function renderSessionsTab(sessions) {
526
+ var container = document.getElementById('sessions-content');
527
+ container.textContent = '';
528
+ if (!sessions || !sessions.length) {
529
+ container.appendChild(emptyState('No sessions found', 'Session data will appear here after graded sessions run.'));
530
+ return;
531
+ }
532
+ document.getElementById('tb-sessions').textContent = sessions.length;
533
+
534
+ // Grade coverage summary
535
+ var summary = DATA.grade_summary || {};
536
+ if (summary.total > 0) {
537
+ var statsRow = mk('div', 'token-summary');
538
+ function gstat(label, val, color) {
539
+ var el = mk('div', 'token-stat');
540
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
541
+ var v = document.createElement('strong');
542
+ v.textContent = val != null ? String(val) : '\u2014';
543
+ if (color) v.style.color = color;
544
+ el.appendChild(lbl); el.appendChild(v);
545
+ return el;
546
+ }
547
+ var letter = summary.avgScore != null ? scoreToLetter(summary.avgScore) : null;
548
+ app(statsRow,
549
+ gstat('Total grades', summary.total),
550
+ gstat('Graded sessions', summary.graded),
551
+ gstat('Avg score', summary.avgScore != null ? summary.avgScore + '/100' : null, letter ? gradeColor(letter) : null),
552
+ gstat('Best', summary.maxScore),
553
+ gstat('Worst', summary.minScore)
554
+ );
555
+ // Distribution badges
556
+ if (summary.distribution) {
557
+ var distEl = mk('div', 'token-stat');
558
+ var distLbl = document.createElement('span'); distLbl.textContent = 'Distribution: ';
559
+ distEl.appendChild(distLbl);
560
+ ['A','B','C','D','F'].forEach(function(l) {
561
+ var n = summary.distribution[l];
562
+ if (n) {
563
+ var badge = mk('span', 'pill', l + ':' + n);
564
+ badge.style.cssText = 'background:' + gradeColorBg(l) + ';color:' + gradeColor(l) + ';margin-left:3px;padding:.1em .4em;border-radius:3px;font-size:.65rem;font-weight:700;';
565
+ distEl.appendChild(badge);
566
+ }
567
+ });
568
+ statsRow.appendChild(distEl);
569
+ }
570
+ container.appendChild(statsRow);
571
+ }
572
+
573
+ // Filter bar
574
+ var filterBar = mk('div', 'filter-bar');
575
+ var statusSel = document.createElement('select');
576
+ [['all', 'All'], ['active', 'Active'], ['ended', 'Ended']].forEach(function(opt) {
577
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; statusSel.appendChild(o);
578
+ });
579
+ var sortSel = document.createElement('select');
580
+ [['newest', 'Newest first'], ['oldest', 'Oldest first'], ['score', 'By score']].forEach(function(opt) {
581
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; sortSel.appendChild(o);
582
+ });
583
+ var searchInput = document.createElement('input');
584
+ searchInput.type = 'text'; searchInput.placeholder = 'Search sessions...';
585
+ app(filterBar, mk('label', null, 'Status:'), statusSel, mk('label', null, 'Sort:'), sortSel, searchInput);
586
+ container.appendChild(filterBar);
587
+
588
+ var tableWrap = mk('div', 'table-wrap');
589
+ container.appendChild(tableWrap);
590
+
591
+ function renderTable() {
592
+ tableWrap.textContent = '';
593
+ var filtered = sessions.slice();
594
+ var statusVal = statusSel.value;
595
+ if (statusVal !== 'all') {
596
+ filtered = filtered.filter(function(s) {
597
+ var st = (s.status || '').toLowerCase();
598
+ return statusVal === 'active' ? (st === 'active' || st === 'running') : (st === 'ended' || st === 'completed');
599
+ });
600
+ }
601
+ var q = searchInput.value.toLowerCase();
602
+ if (q) {
603
+ filtered = filtered.filter(function(s) {
604
+ return (s.name || '').toLowerCase().indexOf(q) >= 0 ||
605
+ (s.sessionId || '').toLowerCase().indexOf(q) >= 0 ||
606
+ (s.scope || '').toLowerCase().indexOf(q) >= 0;
607
+ });
608
+ }
609
+ var sortVal = sortSel.value;
610
+ filtered.sort(function(a, b) {
611
+ if (sortVal === 'score') return (b.gradeScore || 0) - (a.gradeScore || 0);
612
+ if (sortVal === 'oldest') return new Date(a.startedAt || 0) - new Date(b.startedAt || 0);
613
+ return new Date(b.startedAt || 0) - new Date(a.startedAt || 0);
614
+ });
615
+
616
+ if (!filtered.length) {
617
+ tableWrap.appendChild(emptyState('No matching sessions', 'Try adjusting your filters.'));
618
+ return;
619
+ }
620
+
621
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
622
+ var thead = document.createElement('thead');
623
+ var hrow = document.createElement('tr');
624
+ ['Status', 'Session Name', 'Session ID', 'Scope', 'Started', 'Duration', 'Grade Score', 'Grade Letter', 'Audit Entries', 'MCP Calls', 'CLI Calls'].forEach(function(h) {
625
+ hrow.appendChild(mk('th', null, h));
626
+ });
627
+ thead.appendChild(hrow); tbl.appendChild(thead);
628
+ var tbody = document.createElement('tbody');
629
+
630
+ filtered.forEach(function(s) {
631
+ var row = document.createElement('tr'); row.className = 'session-row';
632
+ var isActive = (s.status || '').toLowerCase() === 'active' || (s.status || '').toLowerCase() === 'running';
633
+ var letter = s.gradeLetter || (s.gradeScore != null ? scoreToLetter(s.gradeScore) : '\u2014');
634
+
635
+ // Status pill
636
+ var statusTd = document.createElement('td');
637
+ if (isActive) {
638
+ var dot = mk('span', 'pulse-dot');
639
+ statusTd.appendChild(dot);
640
+ }
641
+ statusTd.appendChild(mk('span', 'pill ' + (isActive ? 'active' : 'ended'), isActive ? 'Active' : 'Ended'));
642
+ row.appendChild(statusTd);
643
+
644
+ // Name with grade mode badge
645
+ var nameTd = document.createElement('td');
646
+ if (s.gradeMode) {
647
+ var gmBadge = mk('span', 'gw-pill query', '\u{1F4CA}');
648
+ gmBadge.style.cssText = 'font-size:.6rem;padding:.1em .3em;margin-right:.35rem;';
649
+ gmBadge.title = 'Graded session';
650
+ nameTd.appendChild(gmBadge);
651
+ }
652
+ var nameSpan = document.createElement('span');
653
+ nameSpan.textContent = s.name || '\u2014';
654
+ nameTd.appendChild(nameSpan);
655
+ row.appendChild(nameTd);
656
+ // Session ID chip
657
+ var idTd = document.createElement('td');
658
+ idTd.appendChild(mk('code', 'session-chip', (s.sessionId || '').slice(0, 18)));
659
+ row.appendChild(idTd);
660
+ // Scope
661
+ row.appendChild(mk('td', null, s.scope || '\u2014'));
662
+ // Started
663
+ row.appendChild(mk('td', null, fmtDate(s.startedAt)));
664
+ // Duration
665
+ row.appendChild(mk('td', null, s.durationMs ? fmtDuration(s.durationMs) : '\u2014'));
666
+ // Grade Score
667
+ var scoreTd = mk('td', null, s.gradeScore != null ? String(s.gradeScore) : '\u2014');
668
+ if (s.gradeScore != null) scoreTd.style.color = gradeColor(scoreToLetter(s.gradeScore));
669
+ row.appendChild(scoreTd);
670
+ // Grade Letter
671
+ var letterTd = mk('td');
672
+ if (letter !== '\u2014') {
673
+ letterTd.style.fontWeight = '700';
674
+ letterTd.style.color = gradeColor(letter);
675
+ }
676
+ letterTd.textContent = letter;
677
+ row.appendChild(letterTd);
678
+ // Audit entries
679
+ row.appendChild(mk('td', null, fmtNum(s.auditEntries, '0')));
680
+ // MCP Calls
681
+ row.appendChild(mk('td', null, fmtNum(s.mcpCalls, '0')));
682
+ // CLI Calls
683
+ row.appendChild(mk('td', null, fmtNum(s.cliCalls, '0')));
684
+
685
+ tbody.appendChild(row);
686
+
687
+ // Expandable detail row
688
+ var expandRow = document.createElement('tr'); expandRow.className = 'session-expand';
689
+ var expandTd = document.createElement('td'); expandTd.colSpan = 11;
690
+
691
+ row.addEventListener('click', function() {
692
+ var open = expandRow.classList.toggle('open');
693
+ if (open && !expandTd.hasChildNodes()) {
694
+ renderSessionExpand(s, expandTd);
695
+ }
696
+ });
697
+ expandRow.appendChild(expandTd);
698
+ tbody.appendChild(expandRow);
699
+ });
700
+ tbl.appendChild(tbody);
701
+ tableWrap.appendChild(tbl);
702
+ }
703
+
704
+ statusSel.addEventListener('change', renderTable);
705
+ sortSel.addEventListener('change', renderTable);
706
+ searchInput.addEventListener('input', renderTable);
707
+ renderTable();
708
+ }
709
+
710
+ function renderSessionExpand(s, td) {
711
+ var wrap = mk('div', 'session-detail');
712
+ td.appendChild(wrap);
713
+
714
+ // ── Header: session metadata ──────────────────────────────────────────────
715
+ var hdr = mk('div', 'session-detail-header');
716
+ function stat(label, val) {
717
+ var el = mk('div', 'session-detail-stat');
718
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
719
+ var v = document.createElement('strong'); v.textContent = val || '\u2014';
720
+ el.appendChild(lbl); el.appendChild(v); return el;
721
+ }
722
+ app(hdr,
723
+ stat('ID', s.sessionId || s.id),
724
+ stat('Scope', s.scope || ((s.scopeType || '') + (s.scopeRootTaskId ? ':' + s.scopeRootTaskId : ''))),
725
+ stat('Started', fmtDate(s.startedAt)),
726
+ stat('Duration', s.durationMs ? fmtDuration(s.durationMs) : (s.endedAt ? fmtDuration(new Date(s.endedAt) - new Date(s.startedAt)) : 'Active')),
727
+ stat('Resumes', s.resumeCount != null ? String(s.resumeCount) : '0'),
728
+ stat('Tasks Done', s.tasksCompleted != null ? String(s.tasksCompleted) : '\u2014')
729
+ );
730
+
731
+ // Grade badge inline
732
+ if (s.gradeScore != null) {
733
+ var letter = s.gradeLetter || scoreToLetter(s.gradeScore);
734
+ var gbadge = mk('div', 'letter-badge sm ' + letter, letter);
735
+ gbadge.style.cssText = 'width:24px;height:24px;font-size:.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:4px;font-weight:700;background:' + gradeColorBg(letter) + ';color:' + gradeColor(letter) + ';flex-shrink:0;';
736
+ var ginfo = mks('span', 'font-size:.75rem;color:var(--text-muted);margin-left:.4rem;', s.gradeScore + '/100 \u2014 Grade ' + letter);
737
+ var gline = mk('div', 'session-detail-stat');
738
+ gline.style.display = 'flex'; gline.style.alignItems = 'center';
739
+ app(gline, gbadge, ginfo);
740
+ hdr.appendChild(gline);
741
+ }
742
+ wrap.appendChild(hdr);
743
+
744
+ // ── Token summary (if available) ─────────────────────────────────────────
745
+ if (s.totalTokens || s.tokenRecords) {
746
+ var tsumm = mk('div', 'token-summary');
747
+ function tstat(label, val) {
748
+ var el = mk('div', 'token-stat');
749
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
750
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
751
+ el.appendChild(lbl); el.appendChild(v); return el;
752
+ }
753
+ app(tsumm,
754
+ tstat('Total tokens', s.totalTokens ? fmtNum(s.totalTokens) : '\u2014'),
755
+ tstat('Token records', s.tokenRecords || 0)
756
+ );
757
+ wrap.appendChild(tsumm);
758
+ }
759
+
760
+ // ── Grade dimension mini-chart (from session-grade join) ────────────────
761
+ var gradeDetails = s.gradeDetails || {};
762
+ var gradeFlags = s.gradeFlags || [];
763
+ if (gradeDetails && typeof gradeDetails === 'object' && Object.keys(gradeDetails).length) {
764
+ var dims = ['sessionDiscipline','taskEfficiency','taskHygiene','protocolAdherence','mcpGateway'];
765
+ var dimNames = ['S1 Discipline','S2 Efficiency','S3 Hygiene','S4 Protocol','S5 MCP Gateway'];
766
+ var dimColors = ['#a371f7','#58a6ff','#3fb950','#e3b341','#f78166'];
767
+ var dimCard = mk('div', 'token-summary');
768
+ dimCard.style.flexDirection = 'column';
769
+ dims.forEach(function(k, i) {
770
+ var dim = gradeDetails[k] || {};
771
+ var score = dim.score != null ? dim.score : (typeof dim === 'number' ? dim : null);
772
+ var maxS = dim.max != null ? dim.max : 20;
773
+ if (score === null) return;
774
+ var row = mk('div', '');
775
+ row.style.cssText = 'display:flex;align-items:center;gap:.5rem;margin:.2rem 0;';
776
+ var label = mk('span', '');
777
+ label.style.cssText = 'font-size:.68rem;color:var(--text-muted);width:100px;flex-shrink:0;';
778
+ label.textContent = dimNames[i];
779
+ var track = mk('div', '');
780
+ track.style.cssText = 'flex:1;height:6px;background:var(--surface2);border-radius:3px;overflow:hidden;';
781
+ var fill = mk('div', '');
782
+ var pct = maxS > 0 ? Math.min(100, (score / maxS) * 100) : 0;
783
+ fill.style.cssText = 'height:100%;border-radius:3px;background:' + dimColors[i] + ';width:' + pct + '%;';
784
+ track.appendChild(fill);
785
+ var val = mk('span', '');
786
+ val.style.cssText = 'font-family:var(--mono);font-size:.65rem;color:var(--text-muted);width:40px;flex-shrink:0;text-align:right;';
787
+ val.textContent = score + '/' + maxS;
788
+ app(row, label, track, val);
789
+ dimCard.appendChild(row);
790
+ });
791
+ if (gradeFlags.length) {
792
+ var flagRow = mk('div', '');
793
+ flagRow.style.cssText = 'margin-top:.4rem;display:flex;gap:.3rem;flex-wrap:wrap;';
794
+ gradeFlags.forEach(function(f) {
795
+ flagRow.appendChild(mk('span', 'pill', f.replace(/_/g,' ')));
796
+ });
797
+ dimCard.appendChild(flagRow);
798
+ }
799
+ wrap.appendChild(dimCard);
800
+ }
801
+
802
+ // ── Audit log section (fetched from /sessions-data) ───────────────────────
803
+ var auditTitle = mk('div', 'audit-section-title');
804
+ var countChip = mk('span', 'audit-count-chip', s.auditEntries ? fmtNum(s.auditEntries) + ' entries' : '…');
805
+ auditTitle.textContent = 'Audit Log';
806
+ auditTitle.appendChild(countChip);
807
+ wrap.appendChild(auditTitle);
808
+
809
+ var tableWrap = mk('div', 'audit-table-wrap');
810
+ wrap.appendChild(tableWrap);
811
+ var loadingMsg = mk('div', 'audit-loading', 'Loading audit entries…');
812
+ tableWrap.appendChild(loadingMsg);
813
+
814
+ // Fetch full audit data from the server
815
+ var sid = s.sessionId || s.id || '';
816
+ if (!sid) {
817
+ loadingMsg.textContent = 'No session ID available.';
818
+ return;
819
+ }
820
+
821
+ fetch('/sessions-data?sessionId=' + encodeURIComponent(sid))
822
+ .then(function(r) { return r.json(); })
823
+ .then(function(data) {
824
+ tableWrap.textContent = '';
825
+ var entries = (data.entries || []);
826
+ var tokens = data.tokens || {};
827
+
828
+ // Update token summary if richer data available
829
+ if (tokens.total_tokens && !s.totalTokens) {
830
+ var tsumm2 = mk('div', 'token-summary');
831
+ function tstat2(label, val) {
832
+ var el = mk('div', 'token-stat');
833
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
834
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
835
+ el.appendChild(lbl); el.appendChild(v); return el;
836
+ }
837
+ app(tsumm2,
838
+ tstat2('Total tokens', fmtNum(tokens.total_tokens)),
839
+ tstat2('Input', fmtNum(tokens.input_tokens || 0)),
840
+ tstat2('Output', fmtNum(tokens.output_tokens || 0)),
841
+ tstat2('Records', tokens.records || 0)
842
+ );
843
+ wrap.insertBefore(tsumm2, auditTitle);
844
+ }
845
+
846
+ // ── Rich session detail sections ────────────────────────────────────
847
+ var sess = data.session || {};
848
+
849
+ // 1. Stats card from session.stats
850
+ var sstats = sess.stats;
851
+ if (sstats) {
852
+ var statsCard = mk('div', 'token-summary');
853
+ function sstat(label, val) {
854
+ var el = mk('div', 'token-stat');
855
+ var lbl = document.createElement('span'); lbl.textContent = label + ': ';
856
+ var v = document.createElement('strong'); v.textContent = val != null ? String(val) : '\u2014';
857
+ el.appendChild(lbl); el.appendChild(v); return el;
858
+ }
859
+ app(statsCard,
860
+ sstat('Tasks Done', sstats.tasksCompleted),
861
+ sstat('Tasks Created', sstats.tasksCreated),
862
+ sstat('Focus Changes', sstats.focusChanges),
863
+ sstat('Active', sstats.totalActiveMinutes != null ? sstats.totalActiveMinutes + ' min' : '\u2014'),
864
+ sstat('Suspends', sstats.suspendCount),
865
+ sstat('Resumes', sess.resumeCount != null ? sess.resumeCount : 0)
866
+ );
867
+ wrap.insertBefore(statsCard, auditTitle);
868
+ }
869
+
870
+ // 2. Session chain (prev / current / next)
871
+ if (sess.previousSessionId || sess.nextSessionId) {
872
+ var chainDiv = mk('div', 'session-chain');
873
+ var chainLabel = mk('span', 'session-chain-label', 'Session Chain');
874
+ chainDiv.appendChild(chainLabel);
875
+
876
+ if (sess.previousSessionId) {
877
+ var prevChip = mk('code', 'session-chip', '\u2190 ' + sess.previousSessionId);
878
+ chainDiv.appendChild(prevChip);
879
+ } else {
880
+ var prevDim = mk('span', 'chain-sep', '\u2014');
881
+ chainDiv.appendChild(prevDim);
882
+ }
883
+
884
+ var sep1 = mk('span', 'chain-sep', '\u2022');
885
+ chainDiv.appendChild(sep1);
886
+
887
+ var curChip = mk('code', 'session-chip chain-current', sess.id || sid);
888
+ chainDiv.appendChild(curChip);
889
+
890
+ var sep2 = mk('span', 'chain-sep', '\u2022');
891
+ chainDiv.appendChild(sep2);
892
+
893
+ if (sess.nextSessionId) {
894
+ var nextChip = mk('code', 'session-chip', sess.nextSessionId + ' \u2192');
895
+ chainDiv.appendChild(nextChip);
896
+ } else {
897
+ var nextDim = mk('span', 'chain-sep', '\u2014');
898
+ chainDiv.appendChild(nextDim);
899
+ }
900
+
901
+ wrap.insertBefore(chainDiv, auditTitle);
902
+ }
903
+
904
+ // 3. Scope section
905
+ var scope = sess.scope;
906
+ if (scope && scope.computedTaskIds && scope.computedTaskIds.length) {
907
+ var scopeDiv = mk('div', 'session-scope');
908
+ var scopeLabel = document.createElement('span');
909
+ scopeLabel.textContent = 'Scope: ';
910
+ var scopeStrong = document.createElement('strong');
911
+ scopeStrong.textContent = (scope.type || 'epic') + ':' + (scope.rootTaskId || '\u2014');
912
+ var scopeCount = document.createElement('span');
913
+ scopeCount.textContent = ' \u2022 ' + scope.computedTaskIds.length + ' tasks in scope';
914
+ scopeDiv.appendChild(scopeLabel);
915
+ scopeDiv.appendChild(scopeStrong);
916
+ scopeDiv.appendChild(scopeCount);
917
+ wrap.insertBefore(scopeDiv, auditTitle);
918
+ }
919
+
920
+ // 4. Debrief section
921
+ var debrief = sess.debrief;
922
+ if (debrief) {
923
+ var ho = debrief.handoff || debrief;
924
+ var debriefCard = mk('div', 'debrief-card');
925
+
926
+ // Note
927
+ if (ho.note) {
928
+ var noteDiv = mk('div', 'debrief-note', ho.note);
929
+ debriefCard.appendChild(noteDiv);
930
+ }
931
+
932
+ // Git state
933
+ var git = ho.gitState || debrief.gitState;
934
+ if (git) {
935
+ var gitRow = mk('div', 'debrief-row');
936
+ var gitLabel = mk('span', 'debrief-label', 'Git');
937
+ gitRow.appendChild(gitLabel);
938
+ if (git.branch) {
939
+ var brSpan = mk('span', 'git-branch', git.branch);
940
+ gitRow.appendChild(brSpan);
941
+ }
942
+ if (git.commit) {
943
+ var hashSpan = mk('span', 'git-hash', String(git.commit).slice(0, 7));
944
+ gitRow.appendChild(hashSpan);
945
+ }
946
+ if (git.uncommittedChanges || git.dirty) {
947
+ var ucBadge = mk('span', 'uncommitted-badge', 'uncommitted changes');
948
+ gitRow.appendChild(ucBadge);
949
+ } else {
950
+ var cleanBadge = mk('span', 'clean-badge', 'clean');
951
+ gitRow.appendChild(cleanBadge);
952
+ }
953
+ debriefCard.appendChild(gitRow);
954
+ }
955
+
956
+ // Chain position
957
+ var chainPos = ho.chainPosition || debrief.chainPosition;
958
+ var chainLen = ho.chainLength || debrief.chainLength;
959
+ if (chainPos != null && chainLen != null) {
960
+ var posRow = mk('div', 'debrief-row');
961
+ var posLabel = mk('span', 'debrief-label', 'Chain');
962
+ posRow.appendChild(posLabel);
963
+ var posVal = document.createElement('span');
964
+ posVal.textContent = 'Position ' + chainPos + ' of ' + chainLen;
965
+ posRow.appendChild(posVal);
966
+ debriefCard.appendChild(posRow);
967
+ }
968
+
969
+ // Next suggested
970
+ var nextSugg = ho.nextSuggested || debrief.nextSuggested;
971
+ if (nextSugg && nextSugg.length) {
972
+ var nsRow = mk('div', 'debrief-row');
973
+ var nsLabel = mk('span', 'debrief-label', 'Next');
974
+ nsRow.appendChild(nsLabel);
975
+ nextSugg.forEach(function(tid) {
976
+ var chip = mk('code', 'session-chip', tid);
977
+ nsRow.appendChild(chip);
978
+ });
979
+ debriefCard.appendChild(nsRow);
980
+ }
981
+
982
+ // Open blockers
983
+ var blockers = ho.openBlockers || debrief.openBlockers;
984
+ if (blockers && blockers.length) {
985
+ var blRow = mk('div', 'debrief-row');
986
+ var blLabel = mk('span', 'debrief-label', 'Blockers');
987
+ blRow.appendChild(blLabel);
988
+ var blCount = document.createElement('span');
989
+ blCount.textContent = blockers.length;
990
+ blCount.style.color = 'var(--grade-d)';
991
+ blCount.style.fontWeight = '600';
992
+ blRow.appendChild(blCount);
993
+ debriefCard.appendChild(blRow);
994
+ }
995
+
996
+ wrap.insertBefore(debriefCard, auditTitle);
997
+ }
998
+
999
+ // 5. Session notes
1000
+ var notes = sess.notes;
1001
+ if (notes && notes.length) {
1002
+ var notesWrap = mk('div', '');
1003
+ var toggle = mk('span', 'notes-toggle', 'Session Notes (' + notes.length + ') \u25B6');
1004
+ var notesList = mk('div', 'notes-list');
1005
+
1006
+ var displayNotes = notes.slice(-5);
1007
+ displayNotes.forEach(function(n) {
1008
+ var entry = mk('div', 'note-entry');
1009
+ var ts = mk('span', 'note-ts', '');
1010
+ var noteText = typeof n === 'string' ? n : (n.text || n.note || '');
1011
+ var noteTs = typeof n === 'object' && n.timestamp ? fmtTime(n.timestamp) : '';
1012
+ ts.textContent = noteTs ? '[' + noteTs + ']' : '';
1013
+ var txt = mk('span', 'note-text', noteText);
1014
+ entry.appendChild(ts);
1015
+ entry.appendChild(txt);
1016
+ notesList.appendChild(entry);
1017
+ });
1018
+
1019
+ toggle.addEventListener('click', function() {
1020
+ var open = notesList.classList.toggle('open');
1021
+ toggle.textContent = 'Session Notes (' + notes.length + ') ' + (open ? '\u25BC' : '\u25B6');
1022
+ });
1023
+
1024
+ notesWrap.appendChild(toggle);
1025
+ notesWrap.appendChild(notesList);
1026
+ wrap.insertBefore(notesWrap, auditTitle);
1027
+ }
1028
+
1029
+ // Update count chip
1030
+ countChip.textContent = fmtNum(entries.length) + ' entries';
1031
+
1032
+ if (!entries.length) {
1033
+ tableWrap.appendChild(mks('div', 'color:var(--text-muted);font-size:.75rem;padding:.5rem 0;', 'No audit entries found for this session.'));
1034
+ return;
1035
+ }
1036
+
1037
+ // Build full audit table
1038
+ var tbl = document.createElement('table'); tbl.className = 'audit-table';
1039
+ var thead = document.createElement('thead');
1040
+ var hrow = document.createElement('tr');
1041
+ ['Time', 'Domain', 'Operation', 'Source', 'Gateway', 'Duration ms', 'Success'].forEach(function(h) {
1042
+ var th = document.createElement('th'); th.textContent = h; hrow.appendChild(th);
1043
+ });
1044
+ thead.appendChild(hrow); tbl.appendChild(thead);
1045
+
1046
+ var tbody = document.createElement('tbody');
1047
+ entries.forEach(function(e) {
1048
+ var tr = document.createElement('tr');
1049
+
1050
+ // Time
1051
+ var timeTd = document.createElement('td');
1052
+ timeTd.className = 'audit-time';
1053
+ timeTd.textContent = fmtTime(e.timestamp);
1054
+ tr.appendChild(timeTd);
1055
+
1056
+ // Domain
1057
+ var domTd = document.createElement('td');
1058
+ domTd.className = 'audit-domain';
1059
+ domTd.textContent = e.domain || '\u2014';
1060
+ tr.appendChild(domTd);
1061
+
1062
+ // Operation
1063
+ var opTd = document.createElement('td');
1064
+ opTd.className = 'audit-op';
1065
+ opTd.textContent = e.operation || '\u2014';
1066
+ tr.appendChild(opTd);
1067
+
1068
+ // Source
1069
+ var srcTd = document.createElement('td');
1070
+ srcTd.className = 'audit-source';
1071
+ srcTd.textContent = e.source || '\u2014';
1072
+ tr.appendChild(srcTd);
1073
+
1074
+ // Gateway pill
1075
+ var gwTd = document.createElement('td');
1076
+ var gw = (e.gateway || '').toUpperCase();
1077
+ var pill = mk('span', 'gw-pill ' + (e.gateway || '').toLowerCase(), gw || '\u2014');
1078
+ gwTd.appendChild(pill);
1079
+ tr.appendChild(gwTd);
1080
+
1081
+ // Duration ms
1082
+ var ms = e.duration_ms != null ? e.duration_ms : null;
1083
+ var msTd = document.createElement('td');
1084
+ msTd.className = 'audit-ms' + (ms > 2000 ? ' vslow' : ms > 500 ? ' slow' : '');
1085
+ msTd.textContent = ms != null ? ms : '\u2014';
1086
+ tr.appendChild(msTd);
1087
+
1088
+ // Success
1089
+ var sucTd = document.createElement('td');
1090
+ sucTd.className = 'audit-success';
1091
+ var sucSpan = document.createElement('span');
1092
+ sucSpan.textContent = e.success ? '\u2713' : '\u2717';
1093
+ sucSpan.style.color = e.success ? 'var(--grade-a)' : 'var(--grade-f)';
1094
+ sucSpan.style.fontWeight = '700';
1095
+ sucTd.appendChild(sucSpan);
1096
+ tr.appendChild(sucTd);
1097
+
1098
+ tbody.appendChild(tr);
1099
+ });
1100
+ tbl.appendChild(tbody);
1101
+ tableWrap.appendChild(tbl);
1102
+ })
1103
+ .catch(function(err) {
1104
+ tableWrap.textContent = '';
1105
+ tableWrap.appendChild(mks('div', 'color:var(--grade-f);font-size:.75rem;padding:.5rem 0;', 'Failed to load audit entries: ' + err.message));
1106
+ });
1107
+ }
1108
+
1109
+ function fmtTime(ts) {
1110
+ if (!ts) return '\u2014';
1111
+ try {
1112
+ var d = new Date(ts);
1113
+ return d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
1114
+ } catch(e) { return String(ts).slice(11, 19) || ts; }
1115
+ }
1116
+
1117
+ function gradeColorBg(l) {
1118
+ var m = {'A':'var(--grade-a-bg)','B':'var(--grade-b-bg)','C':'var(--grade-c-bg)','D':'var(--grade-d-bg)','F':'var(--grade-f-bg)'};
1119
+ return m[l] || 'var(--surface2)';
1120
+ }
1121
+
1122
+ // ── Tab 3: A/B Comparison ─────────────────────────────────────────────────────
1123
+ function renderABTab(abResults, abHistory) {
1124
+ var container = document.getElementById('ab-content');
1125
+ container.textContent = '';
1126
+ var history = abHistory || [];
1127
+ if (!history.length && (!abResults || !abResults.total_runs)) {
1128
+ container.appendChild(emptyState('No A/B results yet', 'Run a blind comparison to generate data.', 'python scripts/run_ab_test.py --domain tasks --runs 3'));
1129
+ return;
1130
+ }
1131
+
1132
+ // Run selector
1133
+ if (history.length) {
1134
+ var selWrap = mk('div', 'filter-bar');
1135
+ selWrap.appendChild(mk('label', null, 'Run:'));
1136
+ var runSel = document.createElement('select');
1137
+ history.sort(function(a, b) { return new Date(b.createdAt || 0) - new Date(a.createdAt || 0); });
1138
+ history.forEach(function(run, i) {
1139
+ var o = document.createElement('option'); o.value = i;
1140
+ o.textContent = (run.name || 'Run ' + (i + 1)) + ' \u2014 ' + fmtDate(run.createdAt);
1141
+ runSel.appendChild(o);
1142
+ });
1143
+ app(selWrap, runSel);
1144
+ container.appendChild(selWrap);
1145
+ }
1146
+
1147
+ renderABContent(abResults, container);
1148
+ document.getElementById('tb-ab').textContent = (abResults && abResults.total_runs) || history.length || 0;
1149
+ }
1150
+
1151
+ function renderABContent(ab, container) {
1152
+ if (!ab || !ab.total_runs) return;
1153
+ var wins = ab.global_wins || {}, wr = ab.global_win_rate || {};
1154
+ var delta = ab.avg_token_delta_mcp_minus_cli;
1155
+ var winner = ab.overall_winner || 'tie';
1156
+
1157
+ // Winner banner
1158
+ var banner = mk('div', 'winner-banner ' + winner);
1159
+ var wlabel = winner === 'mcp' ? 'MCP wins overall' : winner === 'cli' ? 'CLI wins overall' : 'Overall tie';
1160
+ banner.appendChild(mk('span', null, wlabel));
1161
+ if (delta != null) {
1162
+ var sign = delta > 0 ? '+' : '';
1163
+ banner.appendChild(mks('span', 'font-size:.7rem;font-weight:400;opacity:.8;margin-left:.5rem;',
1164
+ 'MCP uses ' + sign + delta.toFixed(1) + ' tokens/op vs CLI'));
1165
+ }
1166
+ container.appendChild(banner);
1167
+
1168
+ // Summary grid
1169
+ container.appendChild(mk('div', 'section-title', 'Global Stats'));
1170
+ var grid = mk('div', 'stat-grid');
1171
+ [
1172
+ ['Total Runs', ab.total_runs, ''],
1173
+ ['Arm A Wins', wins.mcp || 0, 'win'],
1174
+ ['Arm B Wins', wins.cli || 0, winner === 'cli' ? 'win' : ''],
1175
+ ['Ties', wins.tie || 0, 'neutral'],
1176
+ ].forEach(function(row) {
1177
+ var c = mk('div', 'stat-card');
1178
+ app(c, mk('div', 'stat-label', row[0]), mk('div', 'stat-value ' + row[2], String(row[1])));
1179
+ grid.appendChild(c);
1180
+ });
1181
+ container.appendChild(grid);
1182
+
1183
+ // Per-operation table
1184
+ var rawPerOp = ab.per_operation;
1185
+ var perOp = null;
1186
+ if (Array.isArray(rawPerOp)) {
1187
+ perOp = {};
1188
+ rawPerOp.forEach(function(s) { if (s && s.operation) perOp[s.operation] = s; });
1189
+ } else if (rawPerOp && typeof rawPerOp === 'object') {
1190
+ perOp = rawPerOp;
1191
+ }
1192
+ if (perOp && Object.keys(perOp).length) {
1193
+ container.appendChild(mk('div', 'section-title', 'Per-Operation Results'));
1194
+ var wrap = mk('div', 'table-wrap');
1195
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
1196
+ var thead = document.createElement('thead'), hrow = document.createElement('tr');
1197
+ ['Operation', 'Winner', 'MCP Calls', 'CLI Calls', 'Token \u0394', 'MCP ms', 'CLI ms', 'MCP Chars', 'CLI Chars'].forEach(function(h) {
1198
+ hrow.appendChild(mk('th', null, h));
1199
+ });
1200
+ thead.appendChild(hrow); tbl.appendChild(thead);
1201
+ var tbody = document.createElement('tbody');
1202
+ Object.keys(perOp).sort().forEach(function(opKey) {
1203
+ var d = perOp[opKey], row = document.createElement('tr');
1204
+ var dt = d.avg_token_delta != null ? d.avg_token_delta : 0;
1205
+ var dcls = dt > 10 ? 'delta-pos' : dt < -10 ? 'delta-neg' : 'delta-zero';
1206
+ var opW = d.winner || (d.mcp_wins > d.cli_wins ? 'mcp' : d.cli_wins > d.mcp_wins ? 'cli' : 'tie');
1207
+ var cells = [
1208
+ mk('span', 'op-name', opKey),
1209
+ mk('span', 'pill ' + opW, opW.toUpperCase()),
1210
+ document.createTextNode(String(d.mcp_wins || d.mcp_calls || 0)),
1211
+ document.createTextNode(String(d.cli_wins || d.cli_calls || 0)),
1212
+ mk('span', dcls, (dt > 0 ? '+' : '') + dt.toFixed(0) + 't'),
1213
+ document.createTextNode(fmtNum(d.avg_mcp_ms) + 'ms'),
1214
+ document.createTextNode(fmtNum(d.avg_cli_ms) + 'ms'),
1215
+ document.createTextNode(fmtNum(d.avg_mcp_chars)),
1216
+ document.createTextNode(fmtNum(d.avg_cli_chars)),
1217
+ ];
1218
+ cells.forEach(function(c) { var td = document.createElement('td'); td.appendChild(c); row.appendChild(td); });
1219
+ tbody.appendChild(row);
1220
+ });
1221
+ tbl.appendChild(tbody); wrap.appendChild(tbl); container.appendChild(wrap);
1222
+ }
1223
+ }
1224
+
1225
+ // ── Tab 4: Token Economy ──────────────────────────────────────────────────────
1226
+ function renderTokensTab(ta) {
1227
+ var container = document.getElementById('tokens-content');
1228
+ container.textContent = '';
1229
+ if (!ta || (!ta.by_transport && !ta.by_domain)) {
1230
+ container.appendChild(emptyState('No token data yet', 'Token economy data will appear after graded sessions with OTEL enabled.', 'source .cleo/setup-otel.sh'));
1231
+ return;
1232
+ }
1233
+
1234
+ // Confidence badge
1235
+ if (ta.confidence) {
1236
+ var conf = ta.confidence.toLowerCase();
1237
+ var confCard = mk('div', 'card');
1238
+ confCard.style.cssText = 'display:flex;align-items:center;gap:.75rem;';
1239
+ confCard.appendChild(mk('div', 'section-title', 'Data Confidence'));
1240
+ confCard.appendChild(mk('span', 'confidence-badge ' + conf, conf.toUpperCase()));
1241
+ container.appendChild(confCard);
1242
+ }
1243
+
1244
+ // By Transport
1245
+ if (ta.by_transport && Object.keys(ta.by_transport).length) {
1246
+ var entries = Object.entries(ta.by_transport).map(function(kv) {
1247
+ return [kv[0], kv[1].mean || kv[1].avg || kv[1].total || 0, kv[1].count || kv[1].samples || 0];
1248
+ });
1249
+ var maxT = Math.max.apply(null, entries.map(function(e) { return e[1]; }).concat([1]));
1250
+ var tColors = { mcp: 'var(--mcp)', cli: 'var(--cli)', agent: 'var(--grade-c)', api: 'var(--accent)' };
1251
+ var card = mk('div', 'chart-card');
1252
+ card.appendChild(mk('div', 'chart-title', 'By Transport'));
1253
+ var chart = mk('div', 'bar-chart');
1254
+ entries.forEach(function(row) {
1255
+ var pct = maxT > 0 ? Math.min((row[1] / maxT) * 90, 90) : 0;
1256
+ var r = mk('div', 'bar-row');
1257
+ var track = mk('div', 'bar-track');
1258
+ var fill = mk('div', 'bar-fill');
1259
+ fill.style.cssText = 'width:' + pct + '%;background:' + (tColors[row[0]] || 'var(--accent)') + ';';
1260
+ track.appendChild(fill);
1261
+ app(r, mk('span', 'bar-label', row[0]), track, mk('span', 'bar-val', fmtNum(row[1]) + 't (n=' + row[2] + ')'));
1262
+ chart.appendChild(r);
1263
+ });
1264
+ card.appendChild(chart);
1265
+ container.appendChild(card);
1266
+ }
1267
+
1268
+ // By Domain
1269
+ if (ta.by_domain && Object.keys(ta.by_domain).length) {
1270
+ var dEntries = Object.entries(ta.by_domain).map(function(kv) {
1271
+ return [kv[0], kv[1].mean || kv[1].avg || kv[1].total || 0];
1272
+ });
1273
+ dEntries.sort(function(a, b) { return b[1] - a[1]; });
1274
+ var maxD = Math.max.apply(null, dEntries.map(function(e) { return e[1]; }).concat([1]));
1275
+ var domColors = ['#a371f7', '#58a6ff', '#3fb950', '#e3b341', '#f78166', '#f0883e', '#7d8590', '#3fb950', '#58a6ff', '#a371f7'];
1276
+ var dCard = mk('div', 'chart-card');
1277
+ dCard.appendChild(mk('div', 'chart-title', 'By Domain'));
1278
+ var dChart = mk('div', 'bar-chart');
1279
+ dEntries.forEach(function(row, i) {
1280
+ var pct = maxD > 0 ? Math.min((row[1] / maxD) * 90, 90) : 0;
1281
+ var r = mk('div', 'bar-row');
1282
+ var track = mk('div', 'bar-track');
1283
+ var fill = mk('div', 'bar-fill');
1284
+ fill.style.cssText = 'width:' + pct + '%;background:' + domColors[i % domColors.length] + ';';
1285
+ track.appendChild(fill);
1286
+ app(r, mk('span', 'bar-label', row[0]), track, mk('span', 'bar-val', fmtNum(row[1]) + 't'));
1287
+ dChart.appendChild(r);
1288
+ });
1289
+ dCard.appendChild(dChart);
1290
+ container.appendChild(dCard);
1291
+ }
1292
+ }
1293
+
1294
+ // ── Tab 5: Operation Matrix ───────────────────────────────────────────────────
1295
+ function renderMatrixTab(matrix) {
1296
+ var container = document.getElementById('matrix-content');
1297
+ container.textContent = '';
1298
+ if (!matrix || !matrix.length) {
1299
+ container.appendChild(emptyState('No operation matrix data', 'Operation matrix data is generated from the 202-op registry.'));
1300
+ return;
1301
+ }
1302
+ document.getElementById('tb-matrix').textContent = matrix.length;
1303
+
1304
+ // Collect unique domains and gateways
1305
+ var domains = ['all'];
1306
+ var domainSet = {};
1307
+ matrix.forEach(function(op) {
1308
+ if (op.domain && !domainSet[op.domain]) { domainSet[op.domain] = true; domains.push(op.domain); }
1309
+ });
1310
+
1311
+ // Filter row
1312
+ var filterBar = mk('div', 'filter-bar');
1313
+ var domainSel = document.createElement('select');
1314
+ domains.forEach(function(d) {
1315
+ var o = document.createElement('option'); o.value = d; o.textContent = d === 'all' ? 'All Domains' : d; domainSel.appendChild(o);
1316
+ });
1317
+ var gatewaySel = document.createElement('select');
1318
+ [['all', 'All Gateways'], ['query', 'Query'], ['mutate', 'Mutate']].forEach(function(opt) {
1319
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; gatewaySel.appendChild(o);
1320
+ });
1321
+ var testedSel = document.createElement('select');
1322
+ [['all', 'All'], ['tested', 'Tested'], ['untested', 'Untested']].forEach(function(opt) {
1323
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; testedSel.appendChild(o);
1324
+ });
1325
+ app(filterBar, mk('label', null, 'Domain:'), domainSel, mk('label', null, 'Gateway:'), gatewaySel, mk('label', null, 'Tested:'), testedSel);
1326
+ container.appendChild(filterBar);
1327
+
1328
+ var tableWrap = mk('div', 'table-wrap');
1329
+ container.appendChild(tableWrap);
1330
+
1331
+ function renderMatrix() {
1332
+ tableWrap.textContent = '';
1333
+ var filtered = matrix.slice();
1334
+ if (domainSel.value !== 'all') filtered = filtered.filter(function(op) { return op.domain === domainSel.value; });
1335
+ if (gatewaySel.value !== 'all') filtered = filtered.filter(function(op) { return op.gateway === gatewaySel.value; });
1336
+ if (testedSel.value === 'tested') filtered = filtered.filter(function(op) { return op.tested; });
1337
+ if (testedSel.value === 'untested') filtered = filtered.filter(function(op) { return !op.tested; });
1338
+
1339
+ if (!filtered.length) {
1340
+ tableWrap.appendChild(emptyState('No operations match filters', 'Adjust the filters above.'));
1341
+ return;
1342
+ }
1343
+
1344
+ var tbl = document.createElement('table'); tbl.className = 'data-table';
1345
+ var thead = document.createElement('thead');
1346
+ var hrow = document.createElement('tr');
1347
+ ['Operation', 'Domain', 'Gateway', 'Tested', 'MCP Calls', 'CLI Calls', 'Avg MCP ms', 'Avg CLI ms'].forEach(function(h) {
1348
+ hrow.appendChild(mk('th', null, h));
1349
+ });
1350
+ thead.appendChild(hrow); tbl.appendChild(thead);
1351
+ var tbody = document.createElement('tbody');
1352
+
1353
+ filtered.forEach(function(op) {
1354
+ var row = document.createElement('tr');
1355
+ // Operation name
1356
+ row.appendChild((function() { var td = document.createElement('td'); td.appendChild(mk('span', 'op-name', op.operation || op.name || '')); return td; })());
1357
+ // Domain
1358
+ row.appendChild(mk('td', null, op.domain || '\u2014'));
1359
+ // Gateway pill
1360
+ var gwTd = document.createElement('td');
1361
+ gwTd.appendChild(mk('span', 'pill ' + (op.gateway || ''), (op.gateway || '').toUpperCase()));
1362
+ row.appendChild(gwTd);
1363
+ // Tested
1364
+ var testedTd = mk('td');
1365
+ testedTd.textContent = op.tested ? '\u2713' : '\u2014';
1366
+ testedTd.style.color = op.tested ? 'var(--win)' : 'var(--text-muted)';
1367
+ row.appendChild(testedTd);
1368
+ // MCP Calls
1369
+ row.appendChild(mk('td', null, fmtNum(op.mcpCalls || op.mcp_calls, '0')));
1370
+ // CLI Calls
1371
+ row.appendChild(mk('td', null, fmtNum(op.cliCalls || op.cli_calls, '0')));
1372
+ // Avg MCP ms
1373
+ row.appendChild(mk('td', null, op.avgMcpMs || op.avg_mcp_ms ? fmtNum(op.avgMcpMs || op.avg_mcp_ms) + 'ms' : '\u2014'));
1374
+ // Avg CLI ms
1375
+ row.appendChild(mk('td', null, op.avgCliMs || op.avg_cli_ms ? fmtNum(op.avgCliMs || op.avg_cli_ms) + 'ms' : '\u2014'));
1376
+ tbody.appendChild(row);
1377
+ });
1378
+ tbl.appendChild(tbody);
1379
+ tableWrap.appendChild(tbl);
1380
+ }
1381
+
1382
+ domainSel.addEventListener('change', renderMatrix);
1383
+ gatewaySel.addEventListener('change', renderMatrix);
1384
+ testedSel.addEventListener('change', renderMatrix);
1385
+ renderMatrix();
1386
+ }
1387
+
1388
+ // ── Tab 6: Eval Report ────────────────────────────────────────────────────────
1389
+ function renderEvalsTab(evalReport) {
1390
+ var container = document.getElementById('evals-content');
1391
+ container.textContent = '';
1392
+ if (!evalReport || (!evalReport.evals && !evalReport.results)) {
1393
+ container.appendChild(emptyState('No eval report data', 'Eval results will appear after running the eval suite.'));
1394
+ return;
1395
+ }
1396
+
1397
+ var evals = evalReport.evals || evalReport.results || [];
1398
+ if (!Array.isArray(evals)) {
1399
+ // Could be object keyed by eval ID
1400
+ evals = Object.entries(evals).map(function(kv) { var e = kv[1]; e.id = e.id || kv[0]; return e; });
1401
+ }
1402
+ document.getElementById('tb-evals').textContent = evals.length;
1403
+
1404
+ // Sort controls
1405
+ var sortBar = mk('div', 'filter-bar');
1406
+ var sortSel = document.createElement('select');
1407
+ [['id', 'By ID'], ['passRate', 'By Pass Rate'], ['name', 'By Name']].forEach(function(opt) {
1408
+ var o = document.createElement('option'); o.value = opt[0]; o.textContent = opt[1]; sortSel.appendChild(o);
1409
+ });
1410
+ app(sortBar, mk('label', null, 'Sort:'), sortSel);
1411
+ container.appendChild(sortBar);
1412
+
1413
+ var cardsWrap = mk('div', null);
1414
+ cardsWrap.style.cssText = 'display:flex;flex-direction:column;gap:1rem;';
1415
+ container.appendChild(cardsWrap);
1416
+
1417
+ function renderEvals() {
1418
+ cardsWrap.textContent = '';
1419
+ var sorted = evals.slice();
1420
+ var sortVal = sortSel.value;
1421
+ sorted.sort(function(a, b) {
1422
+ if (sortVal === 'passRate') {
1423
+ var aRate = getPassRate(a), bRate = getPassRate(b);
1424
+ return bRate - aRate;
1425
+ }
1426
+ if (sortVal === 'name') return (a.name || '').localeCompare(b.name || '');
1427
+ return (a.id || '').localeCompare(b.id || '');
1428
+ });
1429
+
1430
+ sorted.forEach(function(ev) {
1431
+ var card = mk('div', 'eval-card');
1432
+ var header = mk('div', 'eval-card-header');
1433
+
1434
+ // Title + badge
1435
+ var titleWrap = mk('div');
1436
+ app(titleWrap, mk('span', 'eval-card-title', (ev.id ? ev.id + ': ' : '') + (ev.name || ev.description || 'Eval')));
1437
+ header.appendChild(titleWrap);
1438
+
1439
+ // Status badge
1440
+ var status = getEvalStatus(ev);
1441
+ header.appendChild(mk('span', 'pill ' + status.cls, status.label));
1442
+ card.appendChild(header);
1443
+
1444
+ // Pass rate bar
1445
+ var expectations = ev.expectations || ev.results || [];
1446
+ if (expectations.length) {
1447
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1448
+ var total = expectations.length;
1449
+ var pct = Math.round((passed / total) * 100);
1450
+
1451
+ var rateRow = mk('div', null);
1452
+ rateRow.style.cssText = 'display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem;';
1453
+ rateRow.appendChild(mks('span', 'font-family:var(--mono);font-size:.72rem;', passed + '/' + total + ' (' + pct + '%)'));
1454
+ var prBar = mk('div', 'pass-rate-bar');
1455
+ var prFill = mk('div', 'pass-rate-fill');
1456
+ prFill.style.width = pct + '%';
1457
+ if (pct < 60) prFill.style.background = 'var(--grade-f)';
1458
+ else if (pct < 80) prFill.style.background = 'var(--grade-c)';
1459
+ prBar.appendChild(prFill);
1460
+ rateRow.appendChild(prBar);
1461
+ card.appendChild(rateRow);
1462
+
1463
+ // Expectation list
1464
+ expectations.forEach(function(exp) {
1465
+ var expRow = mk('div', 'expectation-row');
1466
+ var passed2 = exp.pass || exp.passed || exp.result === 'pass';
1467
+ var icon = mk('span', 'expect-icon');
1468
+ icon.textContent = passed2 ? '\u2713' : '\u2717';
1469
+ icon.style.color = passed2 ? 'var(--win)' : 'var(--loss)';
1470
+ expRow.appendChild(icon);
1471
+ var expContent = mk('div');
1472
+ expContent.appendChild(mk('div', null, exp.name || exp.description || exp.expectation || 'Expectation'));
1473
+ if (exp.evidence) {
1474
+ expContent.appendChild(mk('div', 'expect-evidence', exp.evidence));
1475
+ }
1476
+ expRow.appendChild(expContent);
1477
+ card.appendChild(expRow);
1478
+ });
1479
+ }
1480
+ cardsWrap.appendChild(card);
1481
+ });
1482
+ }
1483
+
1484
+ sortSel.addEventListener('change', renderEvals);
1485
+ renderEvals();
1486
+ }
1487
+
1488
+ function getPassRate(ev) {
1489
+ var expectations = ev.expectations || ev.results || [];
1490
+ if (!expectations.length) return -1;
1491
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1492
+ return passed / expectations.length;
1493
+ }
1494
+
1495
+ function getEvalStatus(ev) {
1496
+ var expectations = ev.expectations || ev.results || [];
1497
+ if (!expectations.length) return { cls: 'not-run', label: 'NOT RUN' };
1498
+ var passed = expectations.filter(function(e) { return e.pass || e.passed || e.result === 'pass'; }).length;
1499
+ if (passed === expectations.length) return { cls: 'pass', label: 'PASS' };
1500
+ return { cls: 'fail', label: 'FAIL' };
1501
+ }
1502
+
1503
+ // ── Init ──────────────────────────────────────────────────────────────────────
1504
+ (function() {
1505
+ var meta = DATA.metadata || {};
1506
+ document.getElementById('hdr-title').textContent = 'ct-grade v2.1 \u2014 Grade Review';
1507
+ if (meta.workspace) document.getElementById('hdr-sub').textContent = meta.workspace;
1508
+ var hdrMeta = document.getElementById('hdr-meta');
1509
+ if (meta.generated_at) {
1510
+ hdrMeta.appendChild(document.createTextNode('Generated ' + fmtDate(meta.generated_at)));
1511
+ hdrMeta.appendChild(document.createElement('br'));
1512
+ }
1513
+ var grades = DATA.grades || [];
1514
+ var sessions = DATA.sessions || [];
1515
+ hdrMeta.appendChild(document.createTextNode(
1516
+ grades.length + ' grade(s) \u00b7 ' + sessions.length + ' session(s)'
1517
+ ));
1518
+ if (meta.skill_version) {
1519
+ hdrMeta.appendChild(document.createElement('br'));
1520
+ hdrMeta.appendChild(document.createTextNode('v' + meta.skill_version));
1521
+ }
1522
+
1523
+ renderGradesTab(grades);
1524
+ renderSessionsTab(sessions);
1525
+ renderABTab(DATA.ab_results || {}, DATA.ab_history || []);
1526
+ renderTokensTab(DATA.token_analysis || {});
1527
+ renderMatrixTab(DATA.operation_matrix || []);
1528
+ renderEvalsTab(DATA.eval_report || {});
1529
+ })();
1530
+ </script>
1531
+ </body>
1532
+ </html>