UNPKG

@blundergoat/goat-flow

Version:

AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.

971 lines (939 loc) 39.3 kB
<!-- ═══ Skills View ═══ --> <div x-show="activeView === 'skills'" x-cloak style="height: calc(100vh - 3.5rem); overflow-y: auto" > <div style="max-width: 1100px; margin: 0 auto; padding: 24px 32px 24px"> <!-- Page head: evaluator-first title + installed-skill support action. --> <div style=" display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 16px; " > <div style="flex: 1"> <div class="gf-text-primary" style="font-size: 22px; font-weight: 600; margin-bottom: 4px" > Skill Evaluator </div> <div class="gf-text-muted" style="font-size: 13px; max-width: 820px; line-height: 1.5" > Evaluate coding-agent skill drafts and bundles from pasted markdown or local files. Installed goat-flow skill reports stay available below as supporting evidence. </div> </div> <div style="display: flex; gap: 8px; flex-shrink: 0; margin-top: 4px"> <button @click="reauditAllSkills()" :disabled="skillQualityPrefetching" class="gf-btn gf-btn-md gf-btn-secondary" > <span x-show="!skillQualityPrefetching">Re-audit all</span> <span x-show="skillQualityPrefetching">Auditing…</span> </button> </div> </div> <div class="gf-skill-evaluator-panel" @dragover="skillEvaluatorDragOver($event)" @dragleave="skillEvaluatorDragLeave($event)" @drop="skillEvaluatorDrop($event)" :class="{ 'gf-modal-card-dragover': skillEvaluatorDragActive }" > <div class="gf-modal-header"> <div class="gf-modal-title"> <h2>Evaluate skill</h2> <span class="gf-modal-scope-tag" x-text="skillEvaluatorResult ? 'evaluated just now' : 'draft · not saved'" ></span> </div> <div class="gf-modal-actions"> <button x-show="skillEvaluatorResult" @click="skillEvaluatorReportCopied = true; copySkillEvaluatorReport()" class="gf-btn gf-btn-md gf-btn-secondary gf-copy-report-btn" :class="{ 'is-copied': skillEvaluatorReportCopied }" aria-live="polite" > <span x-text="skillEvaluatorReportCopied ? 'Copied' : 'Copy report'" ></span> </button> <button x-show="skillEvaluatorResult" @click="resetSkillEvaluator()" class="gf-btn gf-btn-md gf-btn-ghost" > Evaluate new </button> </div> </div> <div class="gf-modal-body"> <template x-if="!skillEvaluatorResult"> <div> <template x-if="skillEvaluatorFiles.length === 0"> <div> <label class="gf-dropzone" style="cursor: pointer"> <input type="file" accept=".md,.markdown,text/markdown,text/plain" multiple @change="loadSkillEvaluatorFile($event)" style="display: none" /> <div class="gf-dropzone-icon" aria-hidden="true"> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" > <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /> <polyline points="17 8 12 3 7 8" /> <line x1="12" y1="3" x2="12" y2="15" /> </svg> </div> <div class="gf-dropzone-primary"> Drop SKILL.md and companion markdown files </div> <div class="gf-dropzone-secondary"> or <span class="gf-dropzone-browse">browse</span> · accepts <code>.md</code> · bundles are composed before scoring </div> </label> <div class="gf-divider-or">or paste</div> <div class="gf-paste-block"> <textarea x-model="skillEvaluatorContent" placeholder="Paste SKILL.md content..." rows="7" ></textarea> </div> </div> </template> <template x-if="skillEvaluatorFiles.length > 0"> <div class="gf-dropzone has-files"> <div class="gf-files-header"> <span class="gf-fh-label" x-text="'Files · ' + skillEvaluatorFiles.length + ' attached'" ></span> <label class="gf-fh-add"> <input type="file" accept=".md,.markdown,text/markdown,text/plain" multiple @change="loadSkillEvaluatorFile($event)" style="display: none" /> + Add more </label> </div> <div class="gf-file-chip-list"> <template x-for="file in skillEvaluatorFiles" :key="file.name" > <span class="gf-file-chip"> <span class="gf-pill-file" :class="{ 'is-skill': file.name === 'SKILL.md' }" style="min-width: 50px" x-text="skillFileRole(file.name)" ></span> <span x-text="file.name"></span> <button @click="removeSkillEvaluatorFile(file.name)" class="gf-file-chip-x" aria-label="Remove file" title="Remove from bundle" > × </button> </span> </template> </div> </div> </template> <div x-show="skillEvaluatorDragActive" x-cloak class="gf-text-primary" style=" position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--accent-bg); border: 2px dashed var(--accent); border-radius: 8px; z-index: 10; font-weight: 600; font-size: 14px; pointer-events: none; " > Drop .md files to add to the bundle </div> <div class="gf-input-footer"> <span class="gf-if-hint" x-text="skillEvaluatorFiles.length > 0 ? 'Engine will compose ' + skillEvaluatorFiles.length + ' file' + (skillEvaluatorFiles.length === 1 ? '' : 's') + ' into one scored surface.' : 'Evaluation runs on this local dashboard server.'" ></span> <div class="gf-if-actions"> <button @click="resetSkillEvaluator()" x-show="skillEvaluatorFiles.length > 0 || skillEvaluatorContent.trim()" class="gf-btn gf-btn-md gf-btn-ghost" > Clear </button> <button @click="runSkillEvaluator()" :disabled="skillEvaluatorLoading || (skillEvaluatorFiles.length === 0 && !skillEvaluatorContent.trim())" class="gf-btn gf-btn-md gf-btn-primary" > <span x-show="!skillEvaluatorLoading">Evaluate</span> <span x-show="skillEvaluatorLoading">Scoring…</span> </button> </div> </div> <template x-if="skillEvaluatorError"> <div class="gf-modal-error" x-text="skillEvaluatorError"></div> </template> </div> </template> <template x-if="skillEvaluatorResult"> <div> <div class="gf-verdict-banner" :class="{ 'is-keep': skillEvaluatorResult.recommendation === 'keep-skill', 'is-reference': skillEvaluatorResult.recommendation === 'reference-playbook', 'is-retire': skillEvaluatorResult.recommendation === 'retire' || skillEvaluatorResult.recommendation === 'consider-revision', }" > <span class="gf-vb-pill" x-text="skillEvaluatorResult.recommendation" ></span> <div class="gf-vb-text"> <div class="gf-vb-title" x-text="skillEvaluatorVerdict(skillEvaluatorResult).title" ></div> <div class="gf-vb-desc" x-text="skillEvaluatorVerdict(skillEvaluatorResult).desc" ></div> </div> </div> <div class="gf-result-grid"> <div> <div class="gf-skill-id" style="margin-bottom: 10px"> <h2 style="font-size: 16px" x-text="skillEvaluatorResult.artifact.name" ></h2> <span class="gf-pill-type" :class="'is-' + skillEvaluatorResult.subtype" x-text="skillEvaluatorResult.subtype" ></span> <span x-show="skillEvaluatorResult.shapeMismatch" class="gf-pill-type" :class="'is-' + (skillEvaluatorResult.detectedShape || 'playbook')" x-text="'shape: ' + skillEvaluatorResult.detectedShape" ></span> </div> <div class="gf-score-display"> <div class="gf-sd-row"> <span class="gf-sd-grade" :class="{ 'is-a': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'A', 'is-b': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'B', 'is-c': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'C', 'is-d': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'D', 'is-f': skillLetterGrade(skillReportPct(skillEvaluatorResult)) === 'F', }" x-text="skillLetterGrade(skillReportPct(skillEvaluatorResult))" ></span> <span class="gf-sd-pct" x-text="Math.round(skillReportPct(skillEvaluatorResult) * 100) + '%'" ></span> </div> <div class="gf-sd-raw" x-text="skillEvaluatorResult.totalScore + ' / ' + skillEvaluatorResult.profileMax" ></div> </div> <div class="gf-meta-strip"> <span class="gf-ms-label">Classification</span> <span> <span class="gf-ms-pct" :class="{ 'is-low': skillEvaluatorResult.classification.confidence < 0.7 }" x-text="Math.round(skillEvaluatorResult.classification.confidence * 100) + '%'" ></span> <span x-text="skillEvaluatorResult.classification.detectedSubtype" ></span> <span x-show="skillEvaluatorResult.shapeMismatch" class="gf-cs-alt" x-text="'Shape: ' + skillEvaluatorResult.detectedShape + ' (' + Math.round((skillEvaluatorResult.shapeConfidence || 0) * 100) + '%)'" ></span> </span> </div> <div class="gf-section-label">Structural metrics</div> <div class="gf-metrics"> <template x-for="m in skillEvaluatorResult.metrics" :key="'eval-metric-' + m.metric" > <div class="gf-metric-row"> <div class="gf-metric-label" :class="{ 'is-na': m.severity === 'n/a' }" x-text="m.label" ></div> <div class="gf-bar-track" :class="{ 'is-na': m.severity === 'n/a' }" > <template x-if="m.severity !== 'n/a'"> <div class="gf-bar-fill" :class="{ 'is-ok': m.severity === 'ok', 'is-warn': m.severity === 'warn', 'is-fail': m.severity === 'fail', }" :style="{ width: (m.maxScore > 0 ? (m.score / m.maxScore) * 100 : 0) + '%' }" ></div> </template> </div> <div class="gf-metric-score" :class="{ 'is-ok': m.severity === 'ok', 'is-warn': m.severity === 'warn', 'is-fail': m.severity === 'fail', 'is-na': m.severity === 'n/a', }" x-text="m.severity === 'n/a' ? 'n/a' : (m.score + '/' + m.maxScore)" ></div> </div> </template> </div> <template x-if="skillEvaluatorResult.composedFrom.length > 0"> <div style="margin-top: 16px"> <div class="gf-section-label" x-text="'Composed from · ' + skillEvaluatorResult.composedFrom.length + ' file' + (skillEvaluatorResult.composedFrom.length === 1 ? '' : 's')" ></div> <div class="gf-composed-from"> <template x-for="src in skillEvaluatorResult.composedFrom" :key="src" > <div class="gf-composed-row"> <span class="gf-pill-file" :class="{ 'is-skill': src === 'SKILL.md' }" x-text="skillFileRole(src)" ></span> <span class="gf-composed-name" x-text="src"></span> </div> </template> </div> </div> </template> </div> <div> <div class="gf-tips-header"> <h3>Improvement tips</h3> <span class="gf-tips-count" x-text="skillEvaluatorResult.tips.length" ></span> </div> <template x-if="skillEvaluatorResult.tips.length === 0"> <div class="gf-text-muted" style="font-size: 12px; padding: 12px 0" > No improvement tips - all metrics passing. </div> </template> <template x-for="g in skillEvaluatorTipGroups(skillEvaluatorResult)" :key="'eval-tip-' + g.metric" > <div class="gf-tip-group"> <button @click="toggleSkillEvaluatorTipGroup(g.metric)" class="gf-tip-group-header" :class="{ 'is-collapsed': skillEvaluatorTipCollapsed[g.metric] }" > <div class="gf-tgh-left"> <span class="gf-tgh-chev"></span> <span class="gf-tgh-name" x-text="g.label"></span> <span class="gf-tgh-count" x-text="'· ' + g.tips.length + ' tip' + (g.tips.length === 1 ? '' : 's')" ></span> </div> <span class="gf-tgh-score" :class="{ 'is-warn': g.severity === 'warn', 'is-fail': g.severity === 'fail' }" x-text="g.score + '/' + g.maxScore" ></span> </button> <div class="gf-tip-group-body" x-show="!skillEvaluatorTipCollapsed[g.metric]" > <template x-for="(tip, idx) in g.tips" :key="'eval-tip-row-' + g.metric + '-' + idx" > <div class="gf-tip-row" :class="{ 'is-fail': tip.severity === 'fail' }" > <span class="gf-tr-marker"></span> <span class="gf-tr-body" x-text="tip.message"></span> </div> </template> </div> </div> </template> </div> </div> <div class="gf-modal-result-footer"> <div class="gf-mrf-meta"> Slug: <span class="gf-mrf-slug" x-text="skillEvaluatorSlug(skillEvaluatorResult)" ></span> </div> </div> </div> </template> </div> </div> <!-- Scope strip - counts of audited skills + warnings + scope tag + avg grade. Mirrors the Setup page's "14 of 14 installed · audited just now" pattern so the headline finding is visible without drilling in. --> <template x-if="skillQualityArtifacts.length > 0"> <div class="gf-skill-scope-strip"> <div class="gf-scope-meta"> <span class="gf-scope-pass"> <span class="gf-scope-check"></span> <strong x-text="skillQualityArtifacts.length + ' skill' + (skillQualityArtifacts.length === 1 ? '' : 's') + ' audited'" ></strong> </span> <span class="gf-scope-sep">·</span> <template x-if="skillsWithWarningsCount() > 0"> <span class="gf-scope-warn"><span x-text="skillsWithWarningsCount() + ' with warning' + (skillsWithWarningsCount() === 1 ? '' : 's')" ></span> </span> </template> <template x-if="skillsWithWarningsCount() === 0 && Object.keys(skillQualityReports).length > 0" > <span class="gf-scope-dim">no warnings</span> </template> <span class="gf-scope-sep">·</span> <span class="gf-scope-dim" x-text="skillAuditedRelative()"></span> <span class="gf-scope-sep">·</span> <span class="gf-scope-dim" x-text="'scope: ' + activeRunner"></span> </div> <div class="gf-scope-avg" x-show="Object.keys(skillQualityReports).length > 0" > avg <span class="gf-scope-avg-val"> <span x-text="skillLetterGrade(skillsAvgPct())"></span> <span x-text="Math.round(skillsAvgPct() * 100) + '%'"></span> </span> </div> </div> </template> <!-- Supporting installed-skill audit context. --> <div class="gf-card gf-installed-skills-grid" style=" display: grid; grid-template-columns: 240px 1fr; padding: 0; overflow: hidden; " > <!-- Sidebar - one row per discovered skill, with letter grade and percentage on the right (populated from the prefetch cache). --> <div class="gf-installed-skills-sidebar" style=" border-right: 1px solid var(--border-subtle); padding: 14px 0; max-height: calc(100vh - 16rem); overflow-y: auto; " > <div style="padding: 0 16px 8px"> <div style=" font-size: 11px; font-weight: 600; color: var(--text-muted); letter-spacing: 0.08em; text-transform: uppercase; " > Installed skills <span style="opacity: 0.5" x-text="'· ' + skillQualityArtifacts.length" ></span> </div> </div> <template x-if="skillQualityArtifacts.length === 0"> <div class="gf-text-muted" style="padding: 8px 16px; font-size: 12px; text-align: center" > No installed skills discovered. </div> </template> <div class="gf-skill-sidebar-list"> <template x-for="art in skillQualityArtifacts" :key="'sq-side-' + art.id" > <button @click="loadSkillQualityReport(art.id)" class="gf-skill-sidebar-item" :class="{ 'is-active': skillQualitySelectedId === art.id }" :aria-label="'View audit report for ' + art.name" title="View audit report" > <span x-text="art.name"></span> <template x-if="skillQualityReports[art.id]"> <span class="gf-skill-sidebar-score" :class="{ 'is-a': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'A', 'is-b': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'B', 'is-c': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'C', 'is-d': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'D', 'is-f': skillLetterGrade(skillReportPct(skillQualityReports[art.id])) === 'F', }" > <span x-text="skillLetterGrade(skillReportPct(skillQualityReports[art.id]))" ></span> <span x-text="Math.round(skillReportPct(skillQualityReports[art.id]) * 100) + '%'" ></span> </span> </template> <template x-if="!skillQualityReports[art.id]"> <span class="gf-skill-sidebar-score is-pending">-</span> </template> </button> </template> </div> </div> <!-- Detail panel: per-selected-skill report breakdown. --> <div style="padding: 22px 26px; min-width: 0"> <!-- Loading --> <template x-if="skillQualityLoading"> <div style=" padding: 20px 0; display: flex; align-items: center; gap: 8px; " > <svg class="animate-spin" style="width: 14px; height: 14px; color: var(--accent)" fill="none" viewBox="0 0 24 24" > <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" /> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /> </svg> <span class="gf-text-muted" style="font-size: 12px" >Scoring...</span > </div> </template> <!-- Empty state --> <template x-if="!skillQualityLoading && !skillQualityReport"> <div class="gf-text-muted" style="padding: 32px 0; font-size: 12px; text-align: center" > <span x-show="skillQualityArtifacts.length === 0" >Re-audit all to discover installed goat-flow skills.</span > <span x-show="skillQualityArtifacts.length > 0" >Select an installed skill to view its quality metrics.</span > </div> </template> <!-- Report --> <template x-if="!skillQualityLoading && skillQualityReport"> <div> <!-- Detail toolbar (top, right-aligned) --> <div class="gf-skill-detail-toolbar"> <template x-if="skillQualityReport.prompt && terminalAvailable"> <button @click="launchPreset(skillQualityReport.prompt, activeRunner, 'Skill Quality: ' + skillQualityReport.artifact.name, { cwdPath: projectPath }); activeView = 'workspace'" :disabled="launching || serverSessions.length >= 10" class="gf-btn gf-btn-md gf-btn-primary" > Assess in Runner <svg width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" > <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" /> </svg> </button> </template> <button @click="(skillQualityReports[skillQualityReport.artifact.id] = undefined); loadSkillQualityReport(skillQualityReport.artifact.id)" class="gf-btn gf-btn-md gf-btn-secondary" > Re-audit skill </button> </div> <!-- Detail head: skill identity + file path on the left, big grade block on the right. --> <div style=" display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 16px; " > <div style="flex: 1; min-width: 0"> <div class="gf-skill-id"> <h2 x-text="skillQualityReport.artifact.name"></h2> <span class="gf-pill-type" :class="'is-' + skillQualityReport.subtype" x-text="skillQualityReport.subtype" ></span> <span x-show="skillQualityReport.shapeMismatch" class="gf-pill-type" :class="'is-' + (skillQualityReport.detectedShape || 'playbook')" x-text="'shape: ' + skillQualityReport.detectedShape" ></span> <span class="gf-pill-verdict" :class="{ 'is-keep': skillQualityReport.recommendation === 'keep-skill', 'is-reference': skillQualityReport.recommendation === 'reference-playbook', 'is-review': skillQualityReport.recommendation === 'needs-human-review' || skillQualityReport.recommendation === 'consider-reclassifying', 'is-retire': skillQualityReport.recommendation === 'retire' || skillQualityReport.recommendation === 'consider-revision', }" x-text="skillQualityReport.recommendation" ></span> </div> <div class="gf-skill-file-path"> <span x-text="skillQualityReport.artifact.path"></span> <button @click="navigator.clipboard.writeText(skillQualityReport.artifact.path); showToast('Path copied')" class="gf-path-icon" title="Copy path" aria-label="Copy path" ></button> </div> </div> <div class="gf-grade-block"> <div class="gf-grade-row"> <span class="gf-grade-letter" :class="{ 'is-a': skillLetterGrade(skillReportPct(skillQualityReport)) === 'A', 'is-b': skillLetterGrade(skillReportPct(skillQualityReport)) === 'B', 'is-c': skillLetterGrade(skillReportPct(skillQualityReport)) === 'C', 'is-d': skillLetterGrade(skillReportPct(skillQualityReport)) === 'D', 'is-f': skillLetterGrade(skillReportPct(skillQualityReport)) === 'F', }" x-text="skillLetterGrade(skillReportPct(skillQualityReport))" ></span> <span class="gf-grade-pct" x-text="Math.round(skillReportPct(skillQualityReport) * 100) + '%'" ></span> </div> <div class="gf-grade-raw" x-text="skillQualityReport.totalScore + ' / ' + skillQualityReport.profileMax" ></div> </div> </div> <!-- Summary banner - promotes the headline conclusion (e.g. "Strong skill identity with adequate structural quality") instead of leaving it buried in fit notes. --> <div class="gf-summary-banner" :class="{ 'is-pass': skillSummaryBanner(skillQualityReport).severity === 'pass', 'is-danger': skillSummaryBanner(skillQualityReport).severity === 'fail', }" > <div class="gf-summary-title" x-text="skillSummaryBanner(skillQualityReport).title" ></div> <div class="gf-summary-desc" x-text="skillSummaryBanner(skillQualityReport).desc" ></div> </div> <!-- Classification strip --> <div class="gf-classification-strip"> <span class="gf-cs-label">Classification</span> <span> <span class="gf-cs-pct" :class="{ 'is-low': skillQualityReport.classification.confidence < 0.7 }" x-text="Math.round(skillQualityReport.classification.confidence * 100) + '%'" ></span> <span x-text="skillQualityReport.classification.detectedSubtype" ></span> </span> <span x-show="skillQualityReport.shapeMismatch" class="gf-cs-alt" x-text="'Shape: ' + skillQualityReport.detectedShape + ' (' + Math.round((skillQualityReport.shapeConfidence || 0) * 100) + '%)'" ></span> <template x-if="skillQualityReport.classification.alternatives.length > 0" > <span class="gf-cs-alt"> (also: <span x-text="skillQualityReport.classification.alternatives.map(a => a.subtype + '=' + a.score).join(', ')" ></span >) </span> </template> </div> <!-- Reclassification hint (kept from prior implementation). --> <template x-if="skillQualityReport.recommendation === 'consider-reclassifying'" > <div style=" padding: 8px 12px; background: rgba(251, 191, 36, 0.08); border: 1px solid rgba(251, 191, 36, 0.25); border-left: 3px solid var(--status-waiting); border-radius: 6px; margin-bottom: 14px; font-size: 12px; color: var(--text-secondary); " > <strong>Consider reclassifying:</strong> structure quality is high but subtype confidence is below <code>70%</code>. Review whether <code x-text="skillQualityReport.classification.detectedSubtype" ></code> is the correct shape, or <code x-text="skillQualityReport.classification.alternatives[0]?.subtype" ></code> fits better. </div> </template> <!-- Structural metrics --> <div class="gf-section-label">Structural metrics</div> <div class="gf-metrics"> <template x-for="m in skillQualityReport.metrics" :key="'sqm-' + m.metric" > <div class="gf-metric-row"> <div class="gf-metric-label" :class="{ 'is-na': m.severity === 'n/a' }" x-text="m.label" ></div> <div class="gf-bar-track" :class="{ 'is-na': m.severity === 'n/a' }" > <template x-if="m.severity !== 'n/a'"> <div class="gf-bar-fill" :class="{ 'is-ok': m.severity === 'ok', 'is-warn': m.severity === 'warn', 'is-fail': m.severity === 'fail', }" :style="{ width: (m.maxScore > 0 ? (m.score / m.maxScore) * 100 : 0) + '%' }" ></div> </template> </div> <div class="gf-metric-score" :class="{ 'is-ok': m.severity === 'ok', 'is-warn': m.severity === 'warn', 'is-fail': m.severity === 'fail', 'is-na': m.severity === 'n/a', }" x-text="m.severity === 'n/a' ? 'n/a' : (m.score + '/' + m.maxScore)" ></div> </div> </template> </div> <!-- Findings - only renders for warn/fail metrics. Each finding is its own bordered banner with severity colour, score meta, and the metric's detail string. --> <template x-if="skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail').length > 0" > <div> <div class="gf-section-label" style="margin-top: 22px; margin-bottom: 8px" x-text="'Findings · ' + skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail').length" ></div> <div class="gf-findings"> <template x-for="m in skillQualityReport.metrics.filter(m => m.severity === 'warn' || m.severity === 'fail')" :key="'sqf-' + m.metric" > <div class="gf-finding" :class="{ 'is-fail': m.severity === 'fail' }" > <span class="gf-finding-icon" x-text="m.severity === 'fail' ? '✕' : '⚠'" ></span> <div class="gf-finding-body"> <div class="gf-finding-title"> <span x-text="m.label"></span> <span class="gf-finding-meta" x-text="'· ' + m.score + '/' + m.maxScore" ></span> </div> <div class="gf-finding-desc" x-text="m.detail"></div> </div> </div> </template> </div> </div> </template> <!-- Composed-from list - every file the engine pulled into the scored surface. Lives at the bottom so the score and metric breakdown stay above the fold. The engine's fit-notes prose was duplicating the summary banner above, so it's been removed (the headline finding lives in the banner now). --> <template x-if="skillQualityReport.composedFrom.length > 0"> <div style="margin-top: 22px"> <div class="gf-section-label" x-text="'Composed from · ' + skillQualityReport.composedFrom.length + ' file' + (skillQualityReport.composedFrom.length === 1 ? '' : 's')" ></div> <div class="gf-composed-from"> <template x-for="src in skillQualityReport.composedFrom" :key="src" > <div class="gf-composed-row"> <span class="gf-pill-file" :class="{ 'is-skill': src === 'SKILL.md' }" x-text="skillFileRole(src)" ></span> <span class="gf-composed-name" x-text="src"></span> </div> </template> </div> </div> </template> </div> </template> </div> </div> <div class="gf-footer" style="text-align: center; font-size: 11px; padding: 14px 0" > Built by <a href="https://www.blundergoat.com" target="_blank" class="gf-footer-link" >BlunderGOAT</a > · <span x-text="dashboardVersion ? `v${dashboardVersion}` : ''"></span> </div> </div> </div>