ma-agents 3.13.2 → 3.14.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 (70) hide show
  1. package/lib/agents.js +7 -0
  2. package/lib/bmad-cache/bmb/.claude-plugin/marketplace.json +3 -3
  3. package/lib/bmad-cache/bmb/_git_preserved/index +0 -0
  4. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.idx +0 -0
  5. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/{pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.pack → pack-b388526edf307cf56f8e65673466cff074105abf.pack} +0 -0
  6. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-b388526edf307cf56f8e65673466cff074105abf.rev +0 -0
  7. package/lib/bmad-cache/bmb/_git_preserved/packed-refs +1 -1
  8. package/lib/bmad-cache/bmb/_git_preserved/refs/heads/main +1 -1
  9. package/lib/bmad-cache/bmb/_git_preserved/shallow +1 -1
  10. package/lib/bmad-cache/bmb/package-lock.json +2 -2
  11. package/lib/bmad-cache/bmb/package.json +1 -1
  12. package/lib/bmad-cache/bmb/samples/bmad-agent-code-coach/scripts/wake.py +78 -0
  13. package/lib/bmad-cache/bmb/samples/bmad-agent-creative-muse/scripts/wake.py +78 -0
  14. package/lib/bmad-cache/bmb/samples/bmad-agent-sentinel/scripts/wake.py +78 -0
  15. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/customize-template.toml +3 -0
  16. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/init-sanctum-template.py +11 -5
  17. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/report-shell.html +1073 -0
  18. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/assets/wake-template.py +78 -0
  19. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/customize.toml +48 -0
  20. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/count_tokens.py +78 -0
  21. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass.py +258 -0
  22. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/process-template.py +20 -0
  23. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/render_report.py +387 -0
  24. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/adapter-claude-code.json +9 -0
  25. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/aggregate_benchmark.py +236 -0
  26. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/memlog.py +197 -0
  27. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_evals.py +513 -390
  28. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/run_triggers.py +347 -251
  29. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_env_isolation.py +79 -0
  30. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/tests/test_trigger_detection.py +123 -0
  31. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/tests/test-validate-module.py +82 -4
  32. package/lib/bmad-cache/bmb/skills/bmad-module-builder/scripts/validate-module.py +55 -11
  33. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/customize-template.toml +3 -5
  34. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/assets/report-shell.html +860 -0
  35. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/customize.toml +59 -0
  36. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/count_tokens.py +77 -0
  37. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/init_skill.py +132 -0
  38. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-prompt-metrics.py +186 -221
  39. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/quick_validate.py +117 -0
  40. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/render_report.py +387 -0
  41. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_canon_sync.py +46 -0
  42. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_count_tokens.py +180 -0
  43. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/tests/test_render_report.py +171 -0
  44. package/lib/bmad-cache/cache-manifest.json +4 -4
  45. package/lib/bmad-extension/.claude-plugin/marketplace.json.template +1 -1
  46. package/lib/bmad-extension-plugin/.claude-plugin/marketplace.json +2 -2
  47. package/lib/bmad.js +6 -1
  48. package/lib/templates/instruction-block-git.template.md +25 -25
  49. package/lib/templates/instruction-block-onprem.template.md +86 -86
  50. package/lib/templates/instruction-block-universal.template.md +29 -29
  51. package/package.json +3 -3
  52. package/lib/.bmad-extension-plugin.build-22612-1782454014750/.claude-plugin/marketplace.json +0 -109
  53. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module-help.csv +0 -62
  54. package/lib/.bmad-extension-plugin.build-22612-1782454014750/skills/module.yaml +0 -20
  55. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.idx +0 -0
  56. package/lib/bmad-cache/bmb/_git_preserved/objects/pack/pack-8f8b045fef5af6911495cf3b2a89f1ed75e120f7.rev +0 -0
  57. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/references/sample-init-sanctum.py +0 -274
  58. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/generate-html-report.py +0 -534
  59. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-execution-deps.py +0 -337
  60. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-prompt-metrics.py +0 -425
  61. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-sanctum-architecture.py +0 -385
  62. package/lib/bmad-cache/bmb/skills/bmad-agent-builder/scripts/prepass-structure-capabilities.py +0 -482
  63. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/assets/Dockerfile +0 -29
  64. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/docker_setup.py +0 -115
  65. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/generate_report.py +0 -184
  66. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/pty_runner.py +0 -171
  67. package/lib/bmad-cache/bmb/skills/bmad-eval-runner/scripts/utils.py +0 -260
  68. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/extract-report-json.py +0 -287
  69. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/generate-html-report.py +0 -588
  70. package/lib/bmad-cache/bmb/skills/bmad-workflow-builder/scripts/prepass-execution-deps.py +0 -288
@@ -1,534 +0,0 @@
1
- # /// script
2
- # requires-python = ">=3.9"
3
- # ///
4
-
5
- #!/usr/bin/env python3
6
- """
7
- Generate an interactive HTML quality analysis report for a BMad agent.
8
-
9
- Reads report-data.json produced by the report creator and renders a
10
- self-contained HTML report with:
11
- - BMad Method branding
12
- - Agent portrait (icon, name, title, personality description)
13
- - Capability dashboard with expandable per-capability findings
14
- - Opportunity themes with "Fix This Theme" prompt generation
15
- - Expandable strengths and detailed analysis
16
-
17
- Usage:
18
- python3 generate-html-report.py {quality-report-dir} [--open]
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import argparse
24
- import json
25
- import platform
26
- import subprocess
27
- import sys
28
- from pathlib import Path
29
-
30
-
31
- def load_report_data(report_dir: Path) -> dict:
32
- """Load report-data.json from the report directory."""
33
- data_file = report_dir / 'report-data.json'
34
- if not data_file.exists():
35
- print(f'Error: {data_file} not found', file=sys.stderr)
36
- sys.exit(2)
37
- return json.loads(data_file.read_text(encoding='utf-8'))
38
-
39
-
40
- HTML_TEMPLATE = r"""<!DOCTYPE html>
41
- <html lang="en">
42
- <head>
43
- <meta charset="utf-8">
44
- <meta name="viewport" content="width=device-width, initial-scale=1">
45
- <title>BMad Method · Quality Analysis: SKILL_NAME</title>
46
- <style>
47
- :root {
48
- --bg: #0d1117; --surface: #161b22; --surface2: #21262d; --border: #30363d;
49
- --text: #e6edf3; --text-muted: #8b949e; --text-dim: #6e7681;
50
- --critical: #f85149; --high: #f0883e; --medium: #d29922; --low: #58a6ff;
51
- --strength: #3fb950; --suggestion: #a371f7;
52
- --accent: #58a6ff; --accent-hover: #79c0ff;
53
- --brand: #a371f7;
54
- --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
55
- --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
56
- }
57
- @media (prefers-color-scheme: light) {
58
- :root {
59
- --bg: #ffffff; --surface: #f6f8fa; --surface2: #eaeef2; --border: #d0d7de;
60
- --text: #1f2328; --text-muted: #656d76; --text-dim: #8c959f;
61
- --critical: #cf222e; --high: #bc4c00; --medium: #9a6700; --low: #0969da;
62
- --strength: #1a7f37; --suggestion: #8250df;
63
- --accent: #0969da; --accent-hover: #0550ae;
64
- --brand: #8250df;
65
- }
66
- }
67
- * { margin: 0; padding: 0; box-sizing: border-box; }
68
- body { font-family: var(--font); background: var(--bg); color: var(--text); line-height: 1.5; padding: 2rem; max-width: 900px; margin: 0 auto; }
69
- .brand { color: var(--brand); font-size: 0.8rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; margin-bottom: 0.25rem; }
70
- h1 { font-size: 1.5rem; margin-bottom: 0.25rem; }
71
- .subtitle { color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
72
- .subtitle a { color: var(--accent); text-decoration: none; }
73
- .subtitle a:hover { text-decoration: underline; }
74
- .portrait { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1.25rem; margin-bottom: 1.5rem; }
75
- .portrait-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; }
76
- .portrait-icon { font-size: 2rem; }
77
- .portrait-name { font-size: 1.25rem; font-weight: 700; }
78
- .portrait-title { font-size: 0.9rem; color: var(--text-muted); }
79
- .portrait-desc { font-size: 0.95rem; color: var(--text-muted); line-height: 1.6; font-style: italic; }
80
- .grade { font-size: 2.5rem; font-weight: 700; margin: 0.5rem 0; }
81
- .grade-Excellent { color: var(--strength); }
82
- .grade-Good { color: var(--low); }
83
- .grade-Fair { color: var(--medium); }
84
- .grade-Poor { color: var(--critical); }
85
- .narrative { color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.5rem; line-height: 1.6; }
86
- .badge { display: inline-flex; align-items: center; padding: 0.15rem 0.5rem; border-radius: 2rem; font-size: 0.75rem; font-weight: 600; }
87
- .badge-critical { background: color-mix(in srgb, var(--critical) 20%, transparent); color: var(--critical); }
88
- .badge-high { background: color-mix(in srgb, var(--high) 20%, transparent); color: var(--high); }
89
- .badge-medium { background: color-mix(in srgb, var(--medium) 20%, transparent); color: var(--medium); }
90
- .badge-low { background: color-mix(in srgb, var(--low) 20%, transparent); color: var(--low); }
91
- .badge-strength { background: color-mix(in srgb, var(--strength) 20%, transparent); color: var(--strength); }
92
- .badge-good { background: color-mix(in srgb, var(--strength) 15%, transparent); color: var(--strength); }
93
- .badge-attention { background: color-mix(in srgb, var(--medium) 15%, transparent); color: var(--medium); }
94
- .section { border: 1px solid var(--border); border-radius: 0.5rem; margin: 0.75rem 0; overflow: hidden; }
95
- .section-header { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: var(--surface); cursor: pointer; user-select: none; }
96
- .section-header:hover { background: var(--surface2); }
97
- .section-header .arrow { font-size: 0.7rem; transition: transform 0.15s; color: var(--text-muted); width: 1rem; }
98
- .section-header.open .arrow { transform: rotate(90deg); }
99
- .section-header .label { font-weight: 600; flex: 1; }
100
- .section-header .actions { display: flex; gap: 0.5rem; }
101
- .section-body { display: none; }
102
- .section-body.open { display: block; }
103
- .cap-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; border-top: 1px solid var(--border); }
104
- .cap-row:hover { background: var(--surface); }
105
- .cap-name { font-weight: 600; font-size: 0.9rem; flex: 1; }
106
- .cap-file { font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); }
107
- .cap-findings { display: none; padding: 0.5rem 1rem 0.5rem 2rem; border-top: 1px solid var(--border); background: var(--bg); }
108
- .cap-findings.open { display: block; }
109
- .cap-finding { font-size: 0.85rem; padding: 0.25rem 0; color: var(--text-muted); }
110
- .item { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
111
- .item:hover { background: var(--surface); }
112
- .item-title { font-weight: 600; font-size: 0.9rem; }
113
- .item-file { font-family: var(--mono); font-size: 0.75rem; color: var(--text-muted); }
114
- .item-desc { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; }
115
- .item-action { font-size: 0.85rem; margin-top: 0.25rem; }
116
- .item-action strong { color: var(--strength); }
117
- .opp { padding: 1rem; border-top: 1px solid var(--border); }
118
- .opp-header { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
119
- .opp-name { font-weight: 600; font-size: 1rem; flex: 1; }
120
- .opp-count { font-size: 0.8rem; color: var(--text-muted); }
121
- .opp-desc { font-size: 0.9rem; color: var(--text-muted); margin: 0.5rem 0; }
122
- .opp-impact { font-size: 0.85rem; color: var(--text-dim); font-style: italic; }
123
- .opp-findings { margin-top: 0.75rem; padding-left: 1rem; border-left: 2px solid var(--border); display: none; }
124
- .opp-findings.open { display: block; }
125
- .opp-finding { font-size: 0.85rem; padding: 0.25rem 0; color: var(--text-muted); }
126
- .opp-finding .source { font-size: 0.75rem; color: var(--text-dim); }
127
- .btn { background: none; border: 1px solid var(--border); border-radius: 0.25rem; padding: 0.3rem 0.7rem; cursor: pointer; color: var(--text-muted); font-size: 0.8rem; transition: all 0.15s; }
128
- .btn:hover { border-color: var(--accent); color: var(--accent); }
129
- .btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
130
- .btn-primary:hover { background: var(--accent-hover); }
131
- .strength-item { padding: 0.5rem 1rem; border-top: 1px solid var(--border); }
132
- .strength-item .title { font-weight: 600; font-size: 0.9rem; color: var(--strength); }
133
- .strength-item .detail { font-size: 0.85rem; color: var(--text-muted); }
134
- .analysis-section { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
135
- .analysis-section h4 { font-size: 0.9rem; margin-bottom: 0.25rem; }
136
- .analysis-section p { font-size: 0.85rem; color: var(--text-muted); }
137
- .analysis-finding { font-size: 0.85rem; padding: 0.25rem 0 0.25rem 1rem; border-left: 2px solid var(--border); margin: 0.25rem 0; color: var(--text-muted); }
138
- .recs { padding: 0.75rem 1rem; border-top: 1px solid var(--border); }
139
- .rec { padding: 0.3rem 0; font-size: 0.9rem; }
140
- .rec-rank { font-weight: 700; color: var(--accent); margin-right: 0.5rem; }
141
- .rec-resolves { font-size: 0.8rem; color: var(--text-dim); }
142
- .modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 200; align-items: center; justify-content: center; }
143
- .modal-overlay.visible { display: flex; }
144
- .modal { background: var(--surface); border: 1px solid var(--border); border-radius: 0.5rem; padding: 1.5rem; width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; }
145
- .modal h3 { margin-bottom: 0.75rem; }
146
- .modal pre { background: var(--bg); border: 1px solid var(--border); border-radius: 0.375rem; padding: 1rem; font-family: var(--mono); font-size: 0.8rem; white-space: pre-wrap; word-wrap: break-word; max-height: 50vh; overflow-y: auto; }
147
- .modal-actions { display: flex; gap: 0.75rem; margin-top: 1rem; justify-content: flex-end; }
148
- </style>
149
- </head>
150
- <body>
151
-
152
- <div class="brand">BMad Method</div>
153
- <h1>Quality Analysis: <span id="skill-name"></span></h1>
154
- <div class="subtitle" id="subtitle"></div>
155
-
156
- <div id="portrait"></div>
157
- <div id="grade-area"></div>
158
- <div class="narrative" id="narrative"></div>
159
-
160
- <div id="capabilities-section"></div>
161
- <div id="broken-section"></div>
162
- <div id="opportunities-section"></div>
163
- <div id="strengths-section"></div>
164
- <div id="recommendations-section"></div>
165
- <div id="detailed-section"></div>
166
-
167
- <div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
168
- <div class="modal">
169
- <h3 id="modal-title">Generated Prompt</h3>
170
- <pre id="modal-content"></pre>
171
- <div class="modal-actions">
172
- <button class="btn" onclick="closeModal()">Close</button>
173
- <button class="btn btn-primary" onclick="copyModal()">Copy to Clipboard</button>
174
- </div>
175
- </div>
176
- </div>
177
-
178
- <script>
179
- const RAW = JSON.parse(document.getElementById('report-data').textContent);
180
- const DATA = normalize(RAW);
181
-
182
- function normalize(d) {
183
- if (d.meta) {
184
- d.meta.skill_name = d.meta.skill_name || d.meta.skill || d.meta.name || 'Unknown';
185
- d.meta.scanner_count = typeof d.meta.scanner_count === 'number' ? d.meta.scanner_count
186
- : Array.isArray(d.meta.scanners_run) ? d.meta.scanners_run.length
187
- : d.meta.scanner_count || 0;
188
- }
189
- d.strengths = (d.strengths || []).map(s =>
190
- typeof s === 'string' ? { title: s, detail: '' } : { title: s.title || '', detail: s.detail || '' }
191
- );
192
- (d.opportunities || []).forEach(o => {
193
- o.name = o.name || o.title || '';
194
- o.finding_count = o.finding_count || (o.findings || o.findings_resolved || []).length;
195
- if (!o.findings && o.findings_resolved) o.findings = [];
196
- o.action = o.action || o.fix || '';
197
- });
198
- (d.broken || []).forEach(b => {
199
- b.detail = b.detail || b.description || '';
200
- b.action = b.action || b.fix || '';
201
- });
202
- (d.recommendations || []).forEach((r, i) => {
203
- r.action = r.action || r.description || '';
204
- r.rank = r.rank || i + 1;
205
- });
206
- // Fix journeys
207
- if (d.detailed_analysis && d.detailed_analysis.experience) {
208
- d.detailed_analysis.experience.journeys = (d.detailed_analysis.experience.journeys || []).map(j => ({
209
- archetype: j.archetype || j.persona || j.name || 'Unknown',
210
- summary: j.summary || j.journey_summary || j.description || j.friction || '',
211
- friction_points: j.friction_points || (j.friction ? [j.friction] : []),
212
- bright_spots: j.bright_spots || (j.bright ? [j.bright] : [])
213
- }));
214
- }
215
- // Fix capabilities
216
- (d.capabilities || []).forEach(c => {
217
- c.finding_count = c.finding_count || (c.findings || []).length;
218
- c.status = c.status || (c.finding_count > 0 ? 'needs-attention' : 'good');
219
- });
220
- return d;
221
- }
222
-
223
- function esc(s) {
224
- if (!s) return '';
225
- const d = document.createElement('div');
226
- d.textContent = String(s);
227
- return d.innerHTML;
228
- }
229
-
230
- function init() {
231
- const m = DATA.meta;
232
- document.getElementById('skill-name').textContent = m.skill_name;
233
- document.getElementById('subtitle').innerHTML =
234
- `${esc(m.skill_path)} &bull; ${m.timestamp ? m.timestamp.split('T')[0] : ''} &bull; ${m.scanner_count || 0} scanners &bull; <a href="quality-report.md">Full Report &nearr;</a>`;
235
-
236
- renderPortrait();
237
- document.getElementById('grade-area').innerHTML = `<div class="grade grade-${DATA.grade}">${esc(DATA.grade)}</div>`;
238
- document.getElementById('narrative').textContent = DATA.narrative || '';
239
-
240
- renderCapabilities();
241
- renderBroken();
242
- renderOpportunities();
243
- renderStrengths();
244
- renderRecommendations();
245
- renderDetailed();
246
- }
247
-
248
- function renderPortrait() {
249
- const p = DATA.agent_profile;
250
- if (!p) return;
251
- let html = `<div class="portrait"><div class="portrait-header">`;
252
- if (p.icon) html += `<span class="portrait-icon">${esc(p.icon)}</span>`;
253
- html += `<div><div class="portrait-name">${esc(p.display_name)}</div>`;
254
- if (p.title) html += `<div class="portrait-title">${esc(p.title)}</div>`;
255
- html += `</div></div>`;
256
- if (p.portrait) html += `<div class="portrait-desc">${esc(p.portrait)}</div>`;
257
- html += `</div>`;
258
- document.getElementById('portrait').innerHTML = html;
259
- }
260
-
261
- function renderCapabilities() {
262
- const caps = DATA.capabilities || [];
263
- if (!caps.length) return;
264
- const good = caps.filter(c => c.status === 'good').length;
265
- const attn = caps.length - good;
266
- let summary = `${caps.length} capabilities`;
267
- if (attn > 0) summary += ` \u00b7 ${attn} need attention`;
268
-
269
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
270
- html += `<span class="arrow">&#9654;</span><span class="label">Capabilities (${summary})</span>`;
271
- html += `</div><div class="section-body open">`;
272
- caps.forEach((cap, idx) => {
273
- const statusBadge = cap.status === 'good'
274
- ? `<span class="badge badge-good">Good</span>`
275
- : `<span class="badge badge-attention">${cap.finding_count} observation${cap.finding_count !== 1 ? 's' : ''}</span>`;
276
- const hasFindings = cap.findings && cap.findings.length > 0;
277
- html += `<div class="cap-row" ${hasFindings ? `onclick="toggleCapFindings(${idx})" style="cursor:pointer"` : ''}>`;
278
- html += `${statusBadge} <span class="cap-name">${esc(cap.name)}</span>`;
279
- if (cap.file) html += `<span class="cap-file">${esc(cap.file)}</span>`;
280
- html += `</div>`;
281
- if (hasFindings) {
282
- html += `<div class="cap-findings" id="cap-findings-${idx}">`;
283
- cap.findings.forEach(f => {
284
- html += `<div class="cap-finding">`;
285
- if (f.severity) html += `<span class="badge badge-${f.severity}">${esc(f.severity)}</span> `;
286
- html += `${esc(f.title)}`;
287
- if (f.source) html += ` <span class="source" style="font-size:0.75rem;color:var(--text-dim)">[${esc(f.source)}]</span>`;
288
- html += `</div>`;
289
- });
290
- html += `</div>`;
291
- }
292
- });
293
- html += `</div></div>`;
294
- document.getElementById('capabilities-section').innerHTML = html;
295
- }
296
-
297
- function renderBroken() {
298
- const items = DATA.broken || [];
299
- if (!items.length) return;
300
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
301
- html += `<span class="arrow">&#9654;</span><span class="label">Broken / Critical (${items.length})</span>`;
302
- html += `<div class="actions"><button class="btn btn-primary" onclick="event.stopPropagation();showBrokenPrompt()">Fix These</button></div>`;
303
- html += `</div><div class="section-body open">`;
304
- items.forEach(item => {
305
- const loc = item.file ? `${item.file}${item.line ? ':'+item.line : ''}` : '';
306
- html += `<div class="item"><span class="badge badge-${item.severity || 'high'}">${esc(item.severity || 'high')}</span> `;
307
- if (loc) html += `<span class="item-file">${esc(loc)}</span>`;
308
- html += `<div class="item-title">${esc(item.title)}</div>`;
309
- if (item.detail) html += `<div class="item-desc">${esc(item.detail)}</div>`;
310
- if (item.action) html += `<div class="item-action"><strong>Fix:</strong> ${esc(item.action)}</div>`;
311
- html += `</div>`;
312
- });
313
- html += `</div></div>`;
314
- document.getElementById('broken-section').innerHTML = html;
315
- }
316
-
317
- function renderOpportunities() {
318
- const opps = DATA.opportunities || [];
319
- if (!opps.length) return;
320
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
321
- html += `<span class="arrow">&#9654;</span><span class="label">Opportunities (${opps.length})</span>`;
322
- html += `</div><div class="section-body open">`;
323
- opps.forEach((opp, idx) => {
324
- html += `<div class="opp"><div class="opp-header">`;
325
- html += `<span class="badge badge-${opp.severity || 'medium'}">${esc(opp.severity || 'medium')}</span>`;
326
- html += `<span class="opp-name">${idx+1}. ${esc(opp.name)}</span>`;
327
- html += `<span class="opp-count">${opp.finding_count || (opp.findings||[]).length} observations</span>`;
328
- html += `<button class="btn" onclick="toggleFindings(${idx})">Details</button>`;
329
- html += `<button class="btn btn-primary" onclick="showThemePrompt(${idx})">Fix This</button>`;
330
- html += `</div>`;
331
- html += `<div class="opp-desc">${esc(opp.description)}</div>`;
332
- if (opp.impact) html += `<div class="opp-impact">Impact: ${esc(opp.impact)}</div>`;
333
- html += `<div class="opp-findings" id="findings-${idx}">`;
334
- (opp.findings || []).forEach(f => {
335
- const loc = f.file ? `${f.file}${f.line ? ':'+f.line : ''}` : '';
336
- html += `<div class="opp-finding"><strong>${esc(f.title)}</strong>`;
337
- if (loc) html += ` <span class="item-file">${esc(loc)}</span>`;
338
- if (f.source) html += ` <span class="source">[${esc(f.source)}]</span>`;
339
- if (f.detail) html += `<br>${esc(f.detail)}`;
340
- html += `</div>`;
341
- });
342
- html += `</div></div>`;
343
- });
344
- html += `</div></div>`;
345
- document.getElementById('opportunities-section').innerHTML = html;
346
- }
347
-
348
- function renderStrengths() {
349
- const items = DATA.strengths || [];
350
- if (!items.length) return;
351
- let html = `<div class="section"><div class="section-header" onclick="toggleSection(this)">`;
352
- html += `<span class="arrow">&#9654;</span><span class="label">Strengths (${items.length})</span>`;
353
- html += `</div><div class="section-body">`;
354
- items.forEach(s => {
355
- html += `<div class="strength-item"><div class="title">${esc(s.title)}</div>`;
356
- if (s.detail) html += `<div class="detail">${esc(s.detail)}</div>`;
357
- html += `</div>`;
358
- });
359
- html += `</div></div>`;
360
- document.getElementById('strengths-section').innerHTML = html;
361
- }
362
-
363
- function renderRecommendations() {
364
- const recs = DATA.recommendations || [];
365
- if (!recs.length) return;
366
- let html = `<div class="section"><div class="section-header open" onclick="toggleSection(this)">`;
367
- html += `<span class="arrow">&#9654;</span><span class="label">Recommendations</span>`;
368
- html += `</div><div class="section-body open"><div class="recs">`;
369
- recs.forEach(r => {
370
- html += `<div class="rec"><span class="rec-rank">#${r.rank}</span>${esc(r.action)}`;
371
- if (r.resolves) html += ` <span class="rec-resolves">(resolves ${r.resolves} observations)</span>`;
372
- html += `</div>`;
373
- });
374
- html += `</div></div></div>`;
375
- document.getElementById('recommendations-section').innerHTML = html;
376
- }
377
-
378
- function renderDetailed() {
379
- const da = DATA.detailed_analysis;
380
- if (!da) return;
381
- const dims = [
382
- ['structure', 'Structure & Capabilities'],
383
- ['persona', 'Persona & Voice'],
384
- ['cohesion', 'Identity Cohesion'],
385
- ['efficiency', 'Execution Efficiency'],
386
- ['experience', 'Conversation Experience'],
387
- ['scripts', 'Script Opportunities']
388
- ];
389
- let html = `<div class="section"><div class="section-header" onclick="toggleSection(this)">`;
390
- html += `<span class="arrow">&#9654;</span><span class="label">Detailed Analysis</span>`;
391
- html += `</div><div class="section-body">`;
392
- dims.forEach(([key, label]) => {
393
- const dim = da[key];
394
- if (!dim) return;
395
- html += `<div class="analysis-section"><h4>${label}</h4>`;
396
- if (dim.assessment) html += `<p>${esc(dim.assessment)}</p>`;
397
- if (dim.dimensions) {
398
- html += `<table style="width:100%;font-size:0.85rem;margin:0.5rem 0;border-collapse:collapse;">`;
399
- html += `<tr><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Dimension</th><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Score</th><th style="text-align:left;padding:0.3rem;border-bottom:1px solid var(--border)">Notes</th></tr>`;
400
- Object.entries(dim.dimensions).forEach(([d, v]) => {
401
- if (v && typeof v === 'object') {
402
- html += `<tr><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(d.replace(/_/g,' '))}</td><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(v.score||'')}</td><td style="padding:0.3rem;border-bottom:1px solid var(--border)">${esc(v.notes||'')}</td></tr>`;
403
- }
404
- });
405
- html += `</table>`;
406
- }
407
- if (dim.journeys && dim.journeys.length) {
408
- dim.journeys.forEach(j => {
409
- html += `<div style="margin:0.5rem 0"><strong>${esc(j.archetype)}</strong>: ${esc(j.summary || j.journey_summary || '')}`;
410
- if (j.friction_points && j.friction_points.length) {
411
- html += `<ul style="color:var(--high);font-size:0.85rem;padding-left:1.25rem">`;
412
- j.friction_points.forEach(fp => { html += `<li>${esc(fp)}</li>`; });
413
- html += `</ul>`;
414
- }
415
- html += `</div>`;
416
- });
417
- }
418
- if (dim.autonomous) {
419
- const a = dim.autonomous;
420
- html += `<p><strong>Headless Potential:</strong> ${esc(a.potential||'')}`;
421
- if (a.notes) html += ` \u2014 ${esc(a.notes)}`;
422
- html += `</p>`;
423
- }
424
- (dim.findings || []).forEach(f => {
425
- const loc = f.file ? `${f.file}${f.line ? ':'+f.line : ''}` : '';
426
- html += `<div class="analysis-finding">`;
427
- if (f.severity) html += `<span class="badge badge-${f.severity}">${esc(f.severity)}</span> `;
428
- html += `${esc(f.title)}`;
429
- if (loc) html += ` <span class="item-file">${esc(loc)}</span>`;
430
- html += `</div>`;
431
- });
432
- html += `</div>`;
433
- });
434
- html += `</div></div>`;
435
- document.getElementById('detailed-section').innerHTML = html;
436
- }
437
-
438
- function toggleSection(el) { el.classList.toggle('open'); el.nextElementSibling.classList.toggle('open'); }
439
- function toggleFindings(idx) { document.getElementById('findings-'+idx).classList.toggle('open'); }
440
- function toggleCapFindings(idx) { document.getElementById('cap-findings-'+idx).classList.toggle('open'); }
441
-
442
- function showThemePrompt(idx) {
443
- const opp = DATA.opportunities[idx];
444
- if (!opp) return;
445
- let prompt = `## Task: ${opp.name}\nAgent path: ${DATA.meta.skill_path}\n\n### Problem\n${opp.description}\n\n### Fix\n${opp.action}\n\n`;
446
- if (opp.findings && opp.findings.length) {
447
- prompt += `### Specific observations to address:\n\n`;
448
- opp.findings.forEach((f, i) => {
449
- const loc = f.file ? (f.line ? `${f.file}:${f.line}` : f.file) : '';
450
- prompt += `${i+1}. **${f.title}**`;
451
- if (loc) prompt += ` (${loc})`;
452
- if (f.detail) prompt += `\n ${f.detail}`;
453
- prompt += `\n`;
454
- });
455
- }
456
- document.getElementById('modal-title').textContent = `Fix: ${opp.name}`;
457
- document.getElementById('modal-content').textContent = prompt.trim();
458
- document.getElementById('modal').classList.add('visible');
459
- }
460
-
461
- function showBrokenPrompt() {
462
- const items = DATA.broken || [];
463
- let prompt = `## Task: Fix Critical Issues\nAgent path: ${DATA.meta.skill_path}\n\n`;
464
- items.forEach((item, i) => {
465
- const loc = item.file ? (item.line ? `${item.file}:${item.line}` : item.file) : '';
466
- prompt += `${i+1}. **[${(item.severity||'high').toUpperCase()}] ${item.title}**\n`;
467
- if (loc) prompt += ` File: ${loc}\n`;
468
- if (item.detail) prompt += ` Context: ${item.detail}\n`;
469
- if (item.action) prompt += ` Fix: ${item.action}\n\n`;
470
- });
471
- document.getElementById('modal-title').textContent = 'Fix Critical Issues';
472
- document.getElementById('modal-content').textContent = prompt.trim();
473
- document.getElementById('modal').classList.add('visible');
474
- }
475
-
476
- function closeModal() { document.getElementById('modal').classList.remove('visible'); }
477
- function copyModal() {
478
- navigator.clipboard.writeText(document.getElementById('modal-content').textContent).then(() => {
479
- const btn = document.querySelector('.modal .btn-primary');
480
- btn.textContent = 'Copied!';
481
- setTimeout(() => { btn.textContent = 'Copy to Clipboard'; }, 1500);
482
- });
483
- }
484
-
485
- init();
486
- </script>
487
- </body>
488
- </html>"""
489
-
490
-
491
- def generate_html(report_data: dict) -> str:
492
- data_json = json.dumps(report_data, indent=None, ensure_ascii=False)
493
- data_tag = f'<script id="report-data" type="application/json">{data_json}</script>'
494
- html = HTML_TEMPLATE.replace('<script>\nconst RAW', f'{data_tag}\n<script>\nconst RAW')
495
- html = html.replace('SKILL_NAME', report_data.get('meta', {}).get('skill_name', 'Unknown'))
496
- return html
497
-
498
-
499
- def main() -> int:
500
- parser = argparse.ArgumentParser(description='Generate interactive HTML quality analysis report for a BMad agent')
501
- parser.add_argument('report_dir', type=Path, help='Directory containing report-data.json')
502
- parser.add_argument('--open', action='store_true', help='Open in default browser')
503
- parser.add_argument('--output', '-o', type=Path, help='Output HTML file path')
504
- args = parser.parse_args()
505
-
506
- if not args.report_dir.is_dir():
507
- print(f'Error: {args.report_dir} is not a directory', file=sys.stderr)
508
- return 2
509
-
510
- report_data = load_report_data(args.report_dir)
511
- html = generate_html(report_data)
512
- output_path = args.output or (args.report_dir / 'quality-report.html')
513
- output_path.write_text(html, encoding='utf-8')
514
-
515
- print(json.dumps({
516
- 'html_report': str(output_path),
517
- 'grade': report_data.get('grade', 'Unknown'),
518
- 'opportunities': len(report_data.get('opportunities', [])),
519
- 'broken': len(report_data.get('broken', [])),
520
- }))
521
-
522
- if args.open:
523
- system = platform.system()
524
- if system == 'Darwin':
525
- subprocess.run(['open', str(output_path)])
526
- elif system == 'Linux':
527
- subprocess.run(['xdg-open', str(output_path)])
528
- elif system == 'Windows':
529
- subprocess.run(['start', str(output_path)], shell=True)
530
- return 0
531
-
532
-
533
- if __name__ == '__main__':
534
- sys.exit(main())