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.

442 lines (441 loc) 20.8 kB
"use strict"; /** * Skill-quality view helpers for the dashboard Alpine app. This file backs the "Skills" tab: it * turns a SkillQualityReport into the at-a-glance summary banner the UI shows above the metric * breakdown, deriving a single severity (fail > warn > pass) and a human sentence from the report's * per-metric severities and recommendation. The banner builder is a pure projection over the * report, so the view layer never has to compute severity precedence itself. */ function dashboardSkillSummaryBanner(ctx, report) { if (!report) return { title: "", desc: "", severity: "warn" }; const pct = ctx.skillReportPct(report); const warnCount = report.metrics.filter((metric) => metric.severity === "warn").length; const failCount = report.metrics.filter((metric) => metric.severity === "fail").length; const rec = report.recommendation; if (failCount > 0) { return { title: "Critical structural issues require attention", desc: `${failCount} failing metric${failCount > 1 ? "s" : ""}${warnCount ? ` and ${warnCount} warning${warnCount > 1 ? "s" : ""}` : ""}. Recommended: ${rec}.`, severity: "fail", }; } if (warnCount > 0) { const title = pct >= 0.85 ? "Strong skill identity with adequate structural quality" : "Acceptable skill with non-blocking issues"; return { title, desc: `${warnCount} non-blocking issue${warnCount > 1 ? "s" : ""}. Recommended: ${rec}, address warnings.`, severity: "warn", }; } return { title: "All structural metrics passing", desc: `Recommended: ${rec}.`, severity: "pass", }; } function dashboardSkillQualityReportFragment() { return { /** Re-run the inventory + prefetch from scratch - used by the page-level * "Re-audit all" button. */ async reauditAllSkills() { this.skillQualityReport = null; this.skillQualitySelectedId = null; await this.loadSkillQualityInventory(); }, /** Load or reuse one skill-quality report; reports errors and aborts prior fetches because users can switch quickly. */ async loadSkillQualityReport(artifactId) { this.skillQualitySelectedId = artifactId; const cached = this.skillQualityReports[artifactId]; if (cached) { this.skillQualityReport = cached; this.skillQualityLoading = false; return; } this.skillQualityAbortController?.abort(); const controller = new AbortController(); this.skillQualityAbortController = controller; const requestProjectPath = this.projectPath; const requestRunner = this.activeRunner; this.skillQualityReport = null; this.skillQualityLoading = true; try { const res = await dashboardFetch(`/api/skill-quality?path=${encodeURIComponent(requestProjectPath)}&agent=${encodeURIComponent(requestRunner)}&artifact=${encodeURIComponent(artifactId)}`, { signal: controller.signal }); const payload = readRecord(await res.json(), "Skill quality report"); const error = readErrorMessage(payload); if (error) { this.showToast(error, true); } else if (this.projectPath === requestProjectPath && this.activeRunner === requestRunner && this.skillQualitySelectedId === artifactId) { // Same server-owned /api/skill-quality payload; TS needs the unknown // hop here too because JsonRecord and SkillQualityReport don't overlap. const report = payload; this.skillQualityReport = report; this.skillQualityReports[artifactId] = report; } } catch (err) { if (controller.signal.aborted) return; const msg = err instanceof Error ? err.message : String(err); this.showToast(msg || "Skill quality scoring failed", true); } if (this.skillQualityAbortController === controller) { this.skillQualityLoading = false; this.skillQualityAbortController = null; } }, /** Map a 0..1 ratio to an A/B/C/D/F letter grade. Matches the convention * used on the Setup and Quality pages (≥0.9 A, ≥0.8 B, ≥0.7 C, ≥0.6 D). */ skillLetterGrade(pct) { if (pct >= 0.9) return "A"; if (pct >= 0.8) return "B"; if (pct >= 0.7) return "C"; if (pct >= 0.6) return "D"; return "F"; }, /** Convert a skill-quality report score to a 0..1 ratio. */ skillReportPct(report) { if (!report || !report.profileMax) return 0; return report.totalScore / report.profileMax; }, /** Aggregate count of skills whose stored report has at least one * warn/fail metric. Used for the scope-strip "N with warnings" line. */ skillsWithWarningsCount() { let count = 0; for (const id in this.skillQualityReports) { const report = this.skillQualityReports[id]; if (!report) continue; if (report.metrics.some((metric) => metric.severity === "warn" || metric.severity === "fail")) count++; } return count; }, /** Mean score across all prefetched reports as a 0..1 ratio. */ skillsAvgPct() { const reports = Object.values(this.skillQualityReports); if (reports.length === 0) return 0; let sum = 0; for (const report of reports) sum += Number(this.skillReportPct(report)); return sum / reports.length; }, /** * Build the skills detail headline from recommendation and warn/fail counts. * The branch order promotes blocking findings above percentage score so a high * score cannot hide a small number of load-bearing structural failures because * review must see the risk before the aggregate grade. */ skillSummaryBanner(report) { return dashboardSkillSummaryBanner(this, report); }, }; } function dashboardSkillEvaluatorResultFragment() { return { /** Verdict-banner copy for the Skill Evaluator result. * * The headline title softens its tone to match the recommendation: a * `needs-human-review` verdict says "needs review before keeping", not * "block ship" - "block ship" is reserved for verdicts that the engine * is genuinely confident about (retire / consider-revision). Mismatch * between pill and copy was confusing readers about how confident the * engine actually is. */ skillEvaluatorVerdict(report) { if (!report) return { title: "", desc: "" }; const cls = report.classification; const detected = cls.detectedSubtype; const detectedShape = report.detectedShape ?? detected; const shapeConfidence = report.shapeConfidence ?? cls.confidence; const shapeMismatch = report.shapeMismatch ?? detectedShape !== report.subtype; const failCount = report.metrics.filter((metric) => metric.severity === "fail").length; const warnCount = report.metrics.filter((metric) => metric.severity === "warn").length; const isHardVerdict = report.recommendation === "retire" || report.recommendation === "consider-revision"; let title = ""; if (shapeMismatch && shapeConfidence >= 0.7) { const packagedAs = report.artifact.kind === "skill" ? "skill" : "reference"; title = `Packaged as ${packagedAs}, reads like ${detectedShape}`; } else if (cls.confidence >= 0.85 && detected !== report.subtype) { title = `This reads as a ${detected}, not a ${report.subtype}`; } else if (failCount > 0) { const tail = isHardVerdict ? "block ship" : "- needs review before keeping"; title = `${failCount} failing metric${failCount > 1 ? "s" : ""} ${tail}`; } else if (warnCount > 0) { title = `${warnCount} non-blocking warning${warnCount > 1 ? "s" : ""}`; } else { title = "All structural metrics passing"; } const recHuman = report.recommendation === "needs-human-review" ? "Manual review required" : report.recommendation === "consider-reclassifying" ? "Consider reclassifying" : report.recommendation === "consider-revision" ? "Revise before shipping" : report.recommendation === "retire" ? "Retire or rewrite" : report.recommendation === "reference-playbook" ? "Ship as a reference" : "Keep as a skill"; const detail = shapeMismatch && shapeConfidence >= 0.7 ? `${Math.round(shapeConfidence * 100)}% shape confidence` : cls.confidence >= 0.85 && detected !== report.subtype ? `${Math.round(cls.confidence * 100)}% ${detected} classification` : `${failCount + warnCount} non-passing metric${failCount + warnCount === 1 ? "" : "s"}`; return { title, desc: `${detail}. ${recHuman} before deciding to keep, convert, or discard.`, }; }, /** Group improvement tips by their metric so the modal result can render * one collapsible cluster per metric (with the metric's score in the * header). Order follows the metrics array (ranking from skill-quality.ts). */ skillEvaluatorTipGroups(report) { if (!report || report.tips.length === 0) return []; const tipsByMetric = new Map(); for (const tip of report.tips) { const arr = tipsByMetric.get(tip.metric) ?? []; arr.push(tip); tipsByMetric.set(tip.metric, arr); } const groups = []; for (const metric of report.metrics) { const tips = tipsByMetric.get(metric.metric); if (!tips || tips.length === 0) continue; groups.push({ metric: metric.metric, label: metric.label, score: metric.score, maxScore: metric.maxScore, severity: metric.severity, tips, }); } return groups; }, toggleSkillEvaluatorTipGroup(metric) { this.skillEvaluatorTipCollapsed[metric] = !this.skillEvaluatorTipCollapsed[metric]; }, /** Pretty "audited just now / 3 minutes ago" formatter for the scope strip. */ skillAuditedRelative() { const ts = this.skillQualityAuditedAt; if (!ts) return "audited recently"; const ms = Date.now() - ts; if (ms < 60_000) return "audited just now"; const min = Math.floor(ms / 60_000); if (min < 60) return `audited ${min} min${min > 1 ? "s" : ""} ago`; const hr = Math.floor(min / 60); return `audited ${hr} hr${hr > 1 ? "s" : ""} ago`; }, /** Pill-style file-role label used in the composed-from list and evaluator * file chips. */ skillFileRole(name) { if (name === "skill-preamble.md") return "PREAMBLE"; if (name === "skill-conventions.md") return "CONVENTIONS"; if (name === "SKILL.md") return "SKILL"; if (name.startsWith("references/")) return "REFERENCE"; return "FILE"; }, /** Generate a stable slug for an evaluator result. Used in the result * footer as a copyable identifier so users can reference a specific * evaluation run later (e.g. when comparing two scoring sessions). */ skillEvaluatorSlug(report) { if (!report) return ""; const today = new Date().toISOString().slice(0, 10); const safe = (report.artifact.name || "skill") .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/^-+|-+$/g, ""); return `evaluation-${today}-${safe}`; }, }; } function dashboardSkillEvaluatorClipboardFragment() { return { /** Copy a markdown summary of the current evaluation result to the user's * clipboard. The format mirrors what the engine itself emits so the * result can be pasted into PR descriptions or session notes. */ async copySkillEvaluatorReport() { const result = this.skillEvaluatorResult; if (!result) return; const lines = []; const pct = Math.round(this.skillReportPct(result) * 100); const grade = this.skillLetterGrade(this.skillReportPct(result)); lines.push(`# ${result.artifact.name} - ${grade} ${pct}%`); lines.push(`Slug: \`${this.skillEvaluatorSlug(result)}\``); lines.push(`Subtype: ${result.subtype} (${Math.round(result.classification.confidence * 100)}% ${result.classification.detectedSubtype})`); if (result.shapeMismatch && result.detectedShape) { lines.push(`Detected shape: ${result.detectedShape} (${Math.round((result.shapeConfidence ?? 0) * 100)}%)`); } lines.push(`Verdict: \`${result.recommendation}\``); lines.push(`Score: ${result.totalScore} / ${result.profileMax}`); lines.push(""); lines.push("## Structural metrics"); for (const metric of result.metrics) { const score = metric.severity === "n/a" ? "n/a" : `${metric.score}/${metric.maxScore}`; lines.push(`- ${metric.label}: ${score} (${metric.severity})`); } if (result.tips.length > 0) { lines.push(""); lines.push("## Improvement tips"); for (const tip of result.tips) { lines.push(`- [${tip.metric}] ${tip.message}`); } } if (result.composedFrom.length > 0) { lines.push(""); lines.push("## Composed from"); for (const src of result.composedFrom) { lines.push(`- ${src}`); } } try { const ok = await this.copyTextToClipboard(lines.join("\n")); if (!ok) throw new Error("Clipboard write failed"); this.skillEvaluatorReportCopied = true; if (this._skillEvaluatorReportCopiedTimer) { clearTimeout(this._skillEvaluatorReportCopiedTimer); } this._skillEvaluatorReportCopiedTimer = setTimeout(() => { this.skillEvaluatorReportCopied = false; this._skillEvaluatorReportCopiedTimer = null; }, 4000); this.showToast("Report copied to clipboard"); } catch (err) { this.skillEvaluatorReportCopied = false; if (this._skillEvaluatorReportCopiedTimer) { clearTimeout(this._skillEvaluatorReportCopiedTimer); this._skillEvaluatorReportCopiedTimer = null; } const msg = err instanceof Error ? err.message : String(err); this.showToast(msg || "Copy failed", true); } }, // -- Skill evaluator page -- resetSkillEvaluator() { this.skillEvaluatorName = ""; this.skillEvaluatorContent = ""; this.skillEvaluatorFiles = []; this.skillEvaluatorDragActive = false; this.skillEvaluatorResult = null; this.skillEvaluatorError = null; this.skillEvaluatorLoading = false; this.skillEvaluatorReportCopied = false; if (this._skillEvaluatorReportCopiedTimer) { clearTimeout(this._skillEvaluatorReportCopiedTimer); this._skillEvaluatorReportCopiedTimer = null; } }, clearSkillEvaluatorResult() { this.skillEvaluatorResult = null; this.skillEvaluatorError = null; this.skillEvaluatorReportCopied = false; if (this._skillEvaluatorReportCopiedTimer) { clearTimeout(this._skillEvaluatorReportCopiedTimer); this._skillEvaluatorReportCopiedTimer = null; } }, /** Read multiple `.md` files via FileReader; populates the file list and * pre-fills the suggestedName from the first file. Skips non-markdown * inputs and surfaces a per-file error if any one fails. */ async _ingestSkillEvaluatorFiles(fileList) { const list = Array.from(fileList).filter((file) => file.name.endsWith(".md") || file.name.endsWith(".markdown") || file.type === "text/markdown" || file.type === "text/plain"); if (list.length === 0) { this.skillEvaluatorError = "Drop .md / .markdown files only (got 0 valid files)."; return; } const reads = list.map((file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve({ name: file.name, content: reader.result }); } else { reject(new Error(`Could not read ${file.name}`)); } }; reader.onerror = () => { reject(new Error(`Could not read ${file.name}`)); }; reader.readAsText(file); })); try { const loaded = await Promise.all(reads); const existing = new Set(this.skillEvaluatorFiles.map((file) => file.name)); for (const item of loaded) { if (existing.has(item.name)) continue; this.skillEvaluatorFiles.push(item); } if (!this.skillEvaluatorName && this.skillEvaluatorFiles[0]) { const first = this.skillEvaluatorFiles[0]; this.skillEvaluatorName = first.name.replace(/\.(md|markdown)$/i, ""); } this.skillEvaluatorError = null; } catch (err) { this.skillEvaluatorError = err instanceof Error ? err.message : String(err); } }, /** File input change handler (multi-select). */ loadSkillEvaluatorFile(event) { const input = event.target; if (!input.files || input.files.length === 0) return; void this._ingestSkillEvaluatorFiles(input.files); input.value = ""; }, /** dragover handler - keep the dropzone visually active. */ skillEvaluatorDragOver(event) { event.preventDefault(); this.skillEvaluatorDragActive = true; }, /** dragleave handler - only clear when leaving the evaluator panel itself. */ skillEvaluatorDragLeave(event) { const related = event.relatedTarget; const target = event.currentTarget; if (target && related && target.contains(related)) return; this.skillEvaluatorDragActive = false; }, }; }