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.

214 lines 8.69 kB
import { runAudit } from "../audit/audit.js"; import { loadConfig } from "../config/reader.js"; import { redactEvidenceText } from "../evidence/redaction.js"; import { createFS } from "../facts/fs.js"; import { extractSharedFacts } from "../facts/shared/index.js"; import { findLatestQualityReport } from "../quality/history.js"; import { QUALITY_MODES } from "../quality/schema.js"; import { composeQuality } from "../prompt/compose-quality.js"; import { KNOWN_AGENT_LIST, VALID_AGENTS, VALID_QUALITY_MODES, } from "./dashboard-route-types.js"; import { buildLatestQualitySummary, buildQualityAuditCacheKey, } from "./dashboard-reporting.js"; /** Parse the quality history limit. Invalid input (non-numeric, zero, negative) * falls back to the default so callers can't bypass the cap with ?limit=0. */ function parseQualityHistoryLimit(param) { if (param === null) return 20; const parsed = Number.parseInt(param, 10); if (!Number.isFinite(parsed) || parsed <= 0) return 20; return Math.min(parsed, 100); } /** Parse a dashboard quality-mode filter. */ function parseQualityModeParam(param) { if (param === null) return null; return VALID_QUALITY_MODES.has(param) ? param : null; } function qualityHistoryEntryMatchesFilters(entry, agent, qualityMode) { if (agent !== null && entry.agent !== agent) return false; if (qualityMode === null) return true; return (entry.report.quality_mode ?? "agent-setup") === qualityMode; } function readQualityHistoryFilters(ctx, url, res) { const agentParam = url.searchParams.get("agent"); const agent = agentParam && VALID_AGENTS.has(agentParam) ? agentParam : null; if (agentParam && !agent) { ctx.jsonResponse(res, 400, { error: `quality history agent must be one of: ${KNOWN_AGENT_LIST}`, }); return null; } const modeParam = url.searchParams.get("mode"); const qualityMode = parseQualityModeParam(modeParam); if (modeParam && !qualityMode) { ctx.jsonResponse(res, 400, { error: `quality history mode must be one of: ${QUALITY_MODES.join(", ")}`, }); return null; } return { agent, limit: parseQualityHistoryLimit(url.searchParams.get("limit")), qualityMode, }; } function parseQualityRequestParams(ctx, url, res) { const agentParam = url.searchParams.get("agent"); if (!agentParam || !VALID_AGENTS.has(agentParam)) { ctx.jsonResponse(res, 400, { error: `quality requires --agent. Valid: ${KNOWN_AGENT_LIST}`, }); return null; } const modeParam = url.searchParams.get("mode"); if (modeParam && !VALID_QUALITY_MODES.has(modeParam)) { ctx.jsonResponse(res, 400, { error: `quality mode must be one of: ${QUALITY_MODES.join(", ")}`, }); return null; } return { agent: agentParam, qualityMode: parseQualityModeParam(modeParam) ?? "agent-setup", includeFresh: url.searchParams.get("fresh") === "true", shouldUseFastCache: url.searchParams.get("fast") === "true", }; } function readQualityAuditCache(ctx, projectPath, agent, fresh) { if (fresh) return null; const cached = ctx.qualityAuditCache.get(buildQualityAuditCacheKey(projectPath, agent)); if (!cached) return null; if (Date.now() - cached.cachedAt >= 10_000) { ctx.qualityAuditCache.delete(buildQualityAuditCacheKey(projectPath, agent)); return null; } return cached.report; } function writeQualityAuditCache(ctx, projectPath, agent, report) { ctx.qualityAuditCache.set(buildQualityAuditCacheKey(projectPath, agent), { report, cachedAt: Date.now(), }); } function getOrRunQualityAudit(ctx, projectPath, agent, { cacheOnly = false, fresh = false, } = {}) { const cached = readQualityAuditCache(ctx, projectPath, agent, fresh); if (cached !== null) { return { report: cached, cacheStatus: "hit" }; } const cacheStatus = fresh ? "bypass" : "miss"; if (cacheOnly) return { report: null, cacheStatus }; try { const fs = createFS(projectPath); const report = runAudit(fs, projectPath, { agentFilter: agent, harness: true, }); writeQualityAuditCache(ctx, projectPath, agent, report); return { report, cacheStatus }; } catch { return { report: null, cacheStatus }; } } /** Generate a quality prompt and reports path/audit failures as JSON. */ function handleQualityRequest(ctx, url, res) { if (url.pathname !== "/api/quality") return false; const params = parseQualityRequestParams(ctx, url, res); if (params === null) return true; try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const selectedProjectPath = ctx.validatedPath(url.searchParams.get("target"), "project-read"); const fs = createFS(projectPath); const sharedFacts = extractSharedFacts(fs, loadConfig(projectPath, fs)); const audit = getOrRunQualityAudit(ctx, projectPath, params.agent, { cacheOnly: params.shouldUseFastCache, fresh: params.includeFresh, }); const auditReport = audit.report; const { entry: priorReport } = findLatestQualityReport(projectPath, params.agent, params.qualityMode); const result = composeQuality({ agent: params.agent, projectPath, auditReport, auditUnavailableReason: audit.report === null && params.shouldUseFastCache ? "fast-cache-only" : undefined, priorReport, qualityMode: params.qualityMode, selectedProjectPath, sharedFacts, }); ctx.recordDashboardEvent(projectPath, "quality.prompt", { agent: params.agent, quality_mode: params.qualityMode, audit_status: auditReport?.status ?? "unavailable", prompt: redactEvidenceText("quality prompt", result.prompt), }); ctx.jsonResponse(res, 200, { ...result, auditCacheStatus: audit.cacheStatus, }); } catch (err) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), { error: err instanceof Error ? err.message : String(err), }); } return true; } /** Return persisted quality-history rows and latest trend summary for dashboard UI rendering. */ async function handleQualityHistoryRequest(ctx, url, res) { if (url.pathname !== "/api/quality/history") return false; const filters = readQualityHistoryFilters(ctx, url, res); if (filters === null) return true; try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const { buildQualityHistoryRows, loadQualityHistoryWindow } = await import("../quality/history.js"); const history = loadQualityHistoryWindow(projectPath, { agent: filters.agent, limit: filters.limit, qualityMode: filters.qualityMode, }); const rows = buildQualityHistoryRows(history.entries, { agent: filters.agent, limit: filters.limit, qualityMode: filters.qualityMode, }); const latestEntry = history.entries.find((entry) => qualityHistoryEntryMatchesFilters(entry, filters.agent, filters.qualityMode)) ?? null; ctx.jsonResponse(res, 200, { rows, latest: buildLatestQualitySummary(latestEntry), warnings: history.warnings, }); } catch (err) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), { error: err instanceof Error ? err.message : String(err), }); } return true; } /** * Bind the quality handlers to one server's request context so each closure shares the path validator, * the per-server audit cache, and the evidence recorder. * * @param ctx - per-server dashboard route context with path validation, the quality audit cache, and IO hooks * @returns the quality-prompt and quality-history handlers; each resolves true once it has answered a * matching request, or false to let another handler claim the URL */ export function createQualityRouteHandlers(ctx) { return { handleQualityRequest: (url, res) => handleQualityRequest(ctx, url, res), handleQualityHistoryRequest: (url, res) => handleQualityHistoryRequest(ctx, url, res), }; } //# sourceMappingURL=dashboard-quality-routes.js.map