@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.
653 lines (637 loc) • 25.6 kB
HTML
<!-- ═══ Quality View ═══ -->
<div
x-show="activeView === 'quality'"
x-cloak
style="height: calc(100vh - 3.5rem); overflow-y: auto"
>
<div style="max-width: 1100px; margin: 0 auto; padding: 20px 24px 24px">
<div style="margin-bottom: 14px">
<div
class="gf-text-primary"
style="font-size: 20px; font-weight: 600; margin-bottom: 4px"
>
Quality
</div>
<div class="gf-text-muted" style="font-size: 13px">
Separate deterministic baseline state from historical quality trends,
then run a focused quality-assessment prompt by mode.
</div>
</div>
<!-- Agent selector (full width above columns) -->
<div
style="
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 12px;
"
>
<template x-for="agent in supportedAgents" :key="'quality-' + agent.id">
<button
@click="qualityAgent = agent.id"
class="gf-card"
:style="qualityAgent === agent.id ? { borderColor: 'var(--accent-border)', background: 'var(--accent-bg)' } : {}"
style="padding: 11px 12px; text-align: left"
>
<div
class="gf-text-primary"
style="font-size: 14px; font-weight: 500"
x-text="agent.name"
></div>
<div
style="
display: flex;
align-items: baseline;
gap: 6px;
margin-top: 4px;
"
>
<span
style="color: var(--accent); font-size: 18px; font-weight: 600"
x-text="(() => { const s = report?.agentScores?.find(a => a.id === agent.id); if (!s?.harness) return '-'; const scored = s.harness.checks.filter(c => c.status !== 'skipped'); const pct = scored.length ? Math.round((scored.filter(c => c.status === 'pass').length / scored.length) * 100) : 0; return pct >= 90 ? 'A' : pct >= 80 ? 'B' : pct >= 70 ? 'C' : 'F'; })()"
></span>
<span
class="gf-text-muted"
style="font-size: 13px"
x-text="(() => { const s = report?.agentScores?.find(a => a.id === agent.id); if (!s?.harness) return 'Not audited'; const scored = s.harness.checks.filter(c => c.status !== 'skipped'); const pct = scored.length ? Math.round((scored.filter(c => c.status === 'pass').length / scored.length) * 100) : 0; return pct + '%'; })()"
></span>
</div>
</button>
</template>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
margin-bottom: 12px;
"
>
<template x-for="mode in qualityModes" :key="'quality-mode-' + mode.id">
<button
@click="selectedQualityModeId = mode.id"
class="gf-card"
:style="selectedQualityModeId === mode.id ? { borderColor: 'var(--accent-border)', background: 'var(--accent-bg)' } : {}"
style="padding: 11px 12px; text-align: left"
>
<div
class="gf-text-primary"
style="font-size: 13px; font-weight: 600"
x-text="mode.label"
></div>
<div
class="gf-text-muted"
style="font-size: 11px; line-height: 1.35; margin-top: 4px"
x-text="mode.desc"
></div>
</button>
</template>
</div>
<!-- Two-column layout -->
<div
style="
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
align-items: start;
"
>
<!-- LEFT COLUMN: baseline + history -->
<div style="display: flex; flex-direction: column; gap: 12px">
<!-- Current deterministic baseline -->
<div
class="gf-card"
x-show="report"
x-data="{
concernMeta: {
context: { label: 'Context' },
constraints: { label: 'Constraints' },
verification: { label: 'Verification' },
recovery: { label: 'Recovery' },
feedback_loop: { label: 'Feedback Loop' }
},
concernKeys: ['context', 'constraints', 'verification', 'recovery', 'feedback_loop'],
scoreColor(score) { return score >= 80 ? 'var(--status-pass)' : score >= 70 ? 'var(--status-waiting)' : score >= 60 ? 'var(--orange-400)' : 'var(--status-danger)' },
agentScore(agent) { if (!agent?.harness) return null; const scored = agent.harness.checks.filter(c => c.status !== 'skipped'); return scored.length ? Math.round(scored.filter(c => c.status === 'pass').length / scored.length * 100) : null; },
toGrade(s) { if (s >= 90) return 'A'; if (s >= 80) return 'B'; if (s >= 70) return 'C'; if (s >= 60) return 'D'; return 'F'; },
get selectedAgent() {
return report?.agentScores?.find(a => a.id === qualityAgent) || null;
},
recommendationSummary(agent) {
if (!agent) return 'Not audited';
if (!agent.concerns) {
const passed = agent.agent.checks.filter(c => c.status === 'pass').length;
return `${passed}/${agent.agent.checks.length} installed`;
}
const recs = this.concernKeys.reduce((n, k) => n + (agent.concerns[k]?.recommendations?.length || 0), 0);
const warnings = (agent.harness?.checks || []).filter(c => c.status === 'fail' && c.impact === 'score-only').length;
if (recs === 0 && warnings === 0) return 'All checks passing';
if (warnings > 0) return `${warnings} score warning${warnings !== 1 ? 's' : ''}`;
return `${recs} recommendation${recs !== 1 ? 's' : ''}`;
}
}"
style="padding: 16px 20px"
>
<div
class="gf-text-secondary"
style="font-size: 13px; font-weight: 600; margin-bottom: 10px"
>
Current deterministic baseline
</div>
<div style="display: flex; flex-direction: column; gap: 12px">
<div class="gf-detect-row">
<span class="gf-detect-label">GOAT Flow setup</span>
<span
style="font-size: 12px; font-weight: 600"
:style="{ color: report?.scopes?.setup?.status === 'pass' ? 'var(--status-pass)' : 'var(--red-400)' }"
x-text="(() => { const sc = report?.scopes?.setup; if (!sc?.checks) return ''; const p = sc.checks.filter(c => c.status === 'pass').length; return p + '/' + sc.checks.length + ' checks passing'; })()"
></span>
</div>
<template x-if="selectedAgent">
<div class="gf-card" style="padding: 14px 16px">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2px;
"
>
<span
class="gf-text-primary"
style="font-size: 14px; font-weight: 600"
x-text="`${selectedAgent.name} harness completeness`"
></span>
<div
x-show="agentScore(selectedAgent) !== null"
style="display: flex; align-items: baseline; gap: 4px"
>
<span
style="font-size: 18px; font-weight: 700"
:style="{ color: scoreColor(agentScore(selectedAgent)) }"
x-text="toGrade(agentScore(selectedAgent))"
></span>
<span
style="font-size: 12px; font-weight: 600"
:style="{ color: scoreColor(agentScore(selectedAgent)) }"
x-text="agentScore(selectedAgent) + '%'"
></span>
</div>
</div>
<div
class="gf-text-muted"
style="font-size: 11px; margin-bottom: 8px"
x-text="recommendationSummary(selectedAgent)"
></div>
<template x-if="selectedAgent.concerns">
<div style="display: flex; flex-direction: column; gap: 4px">
<template
x-for="ck in concernKeys"
:key="selectedAgent.id + '-quality-bar-' + ck"
>
<div
style="
display: flex;
align-items: center;
gap: 8px;
padding: 2px 0;
"
>
<span
class="gf-text-muted"
style="
font-size: 10px;
width: 68px;
flex-shrink: 0;
text-align: right;
"
x-text="concernMeta[ck].label"
></span>
<div
style="
flex: 1;
height: 5px;
background: var(--surface-elevated, #27272a);
border-radius: 999px;
overflow: hidden;
"
>
<div
style="
height: 100%;
border-radius: 999px;
transition: width 0.3s;
"
:style="{ width: selectedAgent.concerns[ck].score + '%', background: scoreColor(selectedAgent.concerns[ck].score) }"
></div>
</div>
<span
style="
font-size: 10px;
font-weight: 600;
width: 32px;
flex-shrink: 0;
text-align: right;
"
:style="{ color: scoreColor(selectedAgent.concerns[ck].score) }"
x-text="selectedAgent.concerns[ck].score + '%'"
></span>
</div>
</template>
</div>
</template>
</div>
</template>
</div>
</div>
<!-- Quality history trends -->
<div class="gf-card" style="padding: 16px 20px">
<div
class="gf-text-secondary"
style="font-size: 13px; font-weight: 600; margin-bottom: 10px"
>
Quality history trends
</div>
<div
x-show="qualityHistoryWarnings.length > 0"
style="
margin-bottom: 10px;
border: 1px solid rgba(251, 191, 36, 0.35);
background: rgba(251, 191, 36, 0.08);
color: #facc15;
border-radius: 8px;
padding: 8px 10px;
font-size: 11px;
"
>
<div style="font-weight: 600; margin-bottom: 4px">
Some history files were skipped
</div>
<div style="display: flex; flex-direction: column; gap: 3px">
<template
x-for="warning in qualityHistoryWarnings"
:key="warning"
>
<div x-text="warning"></div>
</template>
</div>
</div>
<div
x-show="qualityHistoryLoading"
style="
display: flex;
align-items: center;
gap: 8px;
padding: 10px 0;
font-size: 12px;
"
>
<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">Loading quality history...</span>
</div>
<template x-if="!qualityHistoryLoading && qualityHistoryLatest">
<div
style="
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 10px;
"
>
<div class="gf-card" style="padding: 10px 12px">
<div
class="gf-text-muted"
style="font-size: 10px; margin-bottom: 2px"
>
Latest run
</div>
<div
class="gf-text-primary"
style="font-size: 12px; font-weight: 600"
>
<span x-text="qualityHistoryLatest.date"></span>
<span
class="gf-text-muted"
x-text="' ' + qualityHistoryLatest.time"
></span>
</div>
</div>
<div class="gf-card" style="padding: 10px 12px">
<div
class="gf-text-muted"
style="font-size: 10px; margin-bottom: 2px"
>
Setup trend
</div>
<div
class="gf-text-primary"
style="font-size: 12px; font-weight: 600"
>
<span x-text="qualityHistoryLatest.setupTotal + '%'"></span>
<span
class="gf-text-muted"
x-show="qualityHistoryRows[0] && qualityHistoryRows[0].setupDelta !== null"
x-text="qualityHistoryRows[0].setupDelta > 0 ? ' (+' + qualityHistoryRows[0].setupDelta + ')' : ' (' + qualityHistoryRows[0].setupDelta + ')'"
></span>
</div>
</div>
<div class="gf-card" style="padding: 10px 12px">
<div
class="gf-text-muted"
style="font-size: 10px; margin-bottom: 2px"
>
System score
</div>
<div
class="gf-text-primary"
style="font-size: 12px; font-weight: 600"
>
<span x-text="qualityHistoryLatest.systemTotal + '%'"></span>
<span
class="gf-text-muted"
x-show="qualityHistoryRows.length > 1"
x-text="(() => { const d = qualityHistoryRows[0].systemTotal - qualityHistoryRows[1].systemTotal; return d > 0 ? ' (+' + d + ')' : ' (' + d + ')'; })()"
></span>
</div>
</div>
<div class="gf-card" style="padding: 10px 12px">
<div
class="gf-text-muted"
style="font-size: 10px; margin-bottom: 2px"
>
Findings
</div>
<div
class="gf-text-primary"
style="font-size: 12px; font-weight: 600"
>
<span
x-text="'B:' + qualityHistoryLatest.blockerCount"
></span>
<span
class="gf-text-muted"
x-text="' M:' + qualityHistoryLatest.majorCount"
></span>
<span
class="gf-text-muted"
x-text="' m:' + qualityHistoryLatest.minorCount"
></span>
</div>
</div>
</div>
</template>
<div x-show="!qualityHistoryLoading && qualityHistoryRows.length > 0">
<div style="overflow-x: auto">
<table
style="width: 100%; border-collapse: collapse; font-size: 12px"
>
<thead>
<tr class="gf-text-muted" style="text-align: left">
<th style="padding: 8px 10px">Date</th>
<th style="padding: 8px 10px">Setup</th>
<th style="padding: 8px 10px">System</th>
<th style="padding: 8px 10px">Blocker</th>
<th style="padding: 8px 10px">Major</th>
<th style="padding: 8px 10px">Minor</th>
</tr>
</thead>
<tbody>
<template
x-for="row in qualityHistoryRows"
:key="'qh-' + row.id"
>
<tr style="border-top: 1px solid var(--border-subtle)">
<td
style="padding: 8px 10px"
class="gf-text-secondary"
x-text="row.date"
></td>
<td style="padding: 8px 10px">
<span
class="gf-text-primary"
x-text="row.setupTotal + '%'"
></span>
<span
class="gf-text-muted"
x-show="row.setupDelta !== null"
x-text="row.setupDelta > 0 ? ' (+' + row.setupDelta + ')' : ' (' + row.setupDelta + ')'"
></span>
</td>
<td
style="padding: 8px 10px"
class="gf-text-primary"
x-text="row.systemTotal + '%'"
></td>
<td
style="padding: 8px 10px; color: #f87171"
x-text="row.blockerCount"
></td>
<td
style="padding: 8px 10px; color: #fbbf24"
x-text="row.majorCount"
></td>
<td
style="padding: 8px 10px"
class="gf-text-secondary"
x-text="row.minorCount"
></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div
x-show="!qualityHistoryLoading && qualityHistoryRows.length === 0"
class="gf-empty-state"
style="padding: 24px 10px"
>
<span class="gf-empty-mark">QH</span>
<p>
No saved quality history yet. Run a quality assessment and save
findings in `.goat-flow/logs/quality` to start trend tracking.
</p>
</div>
</div>
</div>
<!-- RIGHT COLUMN: prompt generation -->
<div class="gf-prompt-card" style="position: sticky; top: 20px">
<div class="gf-prompt-hdr">
<div>
<span
class="gf-text-primary"
style="font-size: 16px; font-weight: 600"
>
Quality prompt
</span>
<div class="gf-text-muted" style="font-size: 12px; margin-top: 2px">
<span
x-text="selectedQualityModeMeta ? selectedQualityModeMeta.targetScope : 'Runner prompt for qualitative review, not deterministic checks.'"
></span>
</div>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<template x-if="!qualityLoading && qualityResult">
<div style="display: flex; gap: 8px">
<button
x-data="{ copied: false }"
@click="copyQuality(); copied = true; setTimeout(() => copied = false, 1500)"
class="gf-btn gf-btn-md gf-btn-secondary"
:style="copied ? { background: 'var(--accent-bg)', color: 'var(--accent)', borderColor: 'var(--accent-border)' } : {}"
>
<span x-text="copied ? 'Copied' : 'Copy'"></span>
</button>
<button
@click="generateQuality({ fresh: true })"
class="gf-btn gf-btn-md gf-btn-secondary"
>
Regenerate
</button>
</div>
</template>
</div>
</div>
<template x-if="!qualityLoading && qualityResult && terminalAvailable">
<div class="gf-terminal-cta" style="padding: 16px; border-radius: 0">
<div
style="
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 8px;
"
>
<button
@click="launchPreset(qualityResult.prompt, activeRunner, qualityLaunchLabel(), { cwdPath: selectedQualityModeId === 'agent-setup' ? projectPath : (window.__GOAT_FLOW_DEFAULT_PATH__ || '.') }); activeView = 'workspace'"
:disabled="launching || serverSessions.length >= 10"
:title="serverSessions.length >= 10 ? 'Session limit reached (10/10). End one to launch another.' : 'Launch in a new Runner session.'"
class="gf-btn-terminal-cta"
>
<svg
style="width: 16px; height: 16px"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 5l8 7-8 7"
/>
</svg>
<span>Run Quality Assessment in Runner</span>
</button>
</div>
<div
class="gf-text-muted"
style="font-size: 12px; line-height: 1.4"
>
<template x-if="!(terminalSessionId && !terminalEnded)">
<span>
Runs with
<span
x-text="agentName(activeRunner)"
class="gf-text-secondary"
style="font-weight: 500"
></span>
against the
<span
x-text="agentName(qualityAgent)"
class="gf-text-secondary"
style="font-weight: 500"
></span>
quality prompt for the selected mode.
</span>
</template>
<template x-if="terminalSessionId && !terminalEnded">
<span>
Pastes this prompt into the active
<span
x-text="lastRunAgent || activeRunner"
class="gf-text-secondary"
style="font-weight: 500"
></span>
Runner session and switches to Workspace.
</span>
</template>
</div>
</div>
</template>
<div class="gf-prompt-loading" :class="!qualityLoading && 'gf-hidden'">
<svg
class="animate-spin"
style="width: 16px; height: 16px; 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"
>Generating...</span
>
</div>
<template x-if="!qualityLoading && qualityResult">
<pre class="gf-prompt-body" x-text="qualityResult.prompt"></pre>
</template>
<p
x-show="!qualityLoading && !qualityResult"
class="gf-text-disabled"
style="
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
"
>
Select a Runner to auto-generate the quality prompt.
</p>
</div>
</div>
<div
class="gf-footer"
style="text-align: center; font-size: 11px; padding: 12px 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>