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.

186 lines 7.47 kB
import { loadQualityConfig, } from "../quality/quality-config.js"; import { discoverArtifacts, evaluateContent, evaluateUploadedBundle, findArtifact, scoreArtifact, } from "../quality/skill-quality.js"; import { composeArtifactQualityPrompt } from "../prompt/compose-quality.js"; import { AGENT_PROFILE_MAP, KNOWN_AGENT_LIST, QUALITY_EVALUATE_MAX_BODY_BYTES, VALID_AGENTS, } from "./dashboard-route-types.js"; import { decodeEvaluateBody } from "./decoders.js"; function parseRequiredAgentParam(ctx, param, routeName, res) { if (!param || !VALID_AGENTS.has(param)) { ctx.jsonResponse(res, 400, { error: `${routeName} requires agent. Valid: ${KNOWN_AGENT_LIST}`, }); return null; } return param; } /** Map mirrored skill directories to the source label shown in quality reports. */ function skillSourceForDir(dir) { if (dir === ".agents/skills") return "agent-mirror"; if (dir === ".github/skills") return "github-mirror"; return "installed"; } /** Narrow skill-quality discovery to the selected runner's installed skill tree. */ function runnerSkillQualityConfig(projectPath, agent) { const base = loadQualityConfig(projectPath); const skillsDir = AGENT_PROFILE_MAP[agent].skillsDir; return { ...base, walkRoots: { skills: [{ dir: skillsDir, source: skillSourceForDir(skillsDir) }], references: base.walkRoots.references, }, }; } /** Return the skill/reference artifact inventory for a project. */ function handleSkillQualityInventoryRequest(ctx, url, res) { if (url.pathname !== "/api/skill-quality/inventory") return false; const agent = parseRequiredAgentParam(ctx, url.searchParams.get("agent"), "skill-quality inventory", res); if (!agent) return true; try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const artifacts = discoverArtifacts(projectPath, runnerSkillQualityConfig(projectPath, agent)); ctx.jsonResponse(res, 200, { artifacts }); } catch (err) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), { error: err instanceof Error ? err.message : String(err), }); } return true; } /** * Score one skill/reference artifact because the dashboard needs artifact-level * feedback without running the full project inventory again. * * Reports missing artifacts and validation failures as JSON. */ function handleSkillQualityRequest(ctx, url, res) { if (url.pathname !== "/api/skill-quality") return false; const agent = parseRequiredAgentParam(ctx, url.searchParams.get("agent"), "skill-quality", res); if (!agent) return true; const artifactId = url.searchParams.get("artifact"); if (!artifactId) { ctx.jsonResponse(res, 400, { error: "skill-quality requires ?artifact=<id>", }); return true; } try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const config = runnerSkillQualityConfig(projectPath, agent); const artifact = findArtifact(projectPath, artifactId, config); if (!artifact) { ctx.jsonResponse(res, 404, { error: `artifact not found: ${artifactId}`, }); return true; } const report = scoreArtifact(projectPath, artifact, config); const prompt = composeArtifactQualityPrompt(report); ctx.jsonResponse(res, 200, { ...report, prompt }); } catch (err) { ctx.jsonResponse(res, ctx.responseStatusForError(err, 500), { error: err instanceof Error ? err.message : String(err), }); } return true; } /** Writes deprecation headers on every response served via the `/analyse` alias. */ function markEvaluateAliasDeprecation(res) { res.setHeader("Deprecation", "true"); res.setHeader("Link", '</api/quality/evaluate>; rel="successor-version"'); } function sendEvaluateError(ctx, res, isAlias, status, payload) { if (isAlias) markEvaluateAliasDeprecation(res); ctx.jsonResponse(res, status, payload); } async function readEvaluateRequestBody(ctx, req, res, isAlias) { try { return await ctx.readBody(req, { maxBytes: QUALITY_EVALUATE_MAX_BODY_BYTES, tooLargeMessage: "Evaluate body too large", }); } catch (err) { sendEvaluateError(ctx, res, isAlias, 413, { error: err instanceof Error ? err.message : String(err), }); return null; } } /** * Score a decoded evaluate request against the project, routing a multi-file upload to the bundle * scorer and a single payload to the content scorer. Treats a missing `content` field as an empty * string so the content path always has a value to score. */ function evaluateRequestBody(projectPath, value) { if (value.files) { return evaluateUploadedBundle(projectPath, { files: value.files, suggestedName: value.suggestedName, kind: value.kind, }); } return evaluateContent(projectPath, { content: value.content ?? "", suggestedName: value.suggestedName, kind: value.kind, }); } /** POST /api/quality/evaluate - score uploaded markdown and return tips. */ async function handleQualityEvaluateRequest(ctx, req, url, res) { const isAlias = url.pathname === "/api/quality/analyse"; if (url.pathname !== "/api/quality/evaluate" && !isAlias) return false; if (req.method !== "POST") { sendEvaluateError(ctx, res, isAlias, 405, { error: "Method not allowed" }); return true; } const body = await readEvaluateRequestBody(ctx, req, res, isAlias); if (body === null) return true; const decoded = decodeEvaluateBody(body); if (!decoded.ok) { sendEvaluateError(ctx, res, isAlias, 400, { error: decoded.error, path: decoded.path, }); return true; } try { const projectPath = ctx.validatedPath(url.searchParams.get("path"), "project-read"); const result = evaluateRequestBody(projectPath, decoded.value); if (isAlias) markEvaluateAliasDeprecation(res); ctx.jsonResponse(res, 200, result); } catch (err) { sendEvaluateError(ctx, res, isAlias, ctx.responseStatusForError(err, 500), { error: err instanceof Error ? err.message : String(err), }); } return true; } /** * Bind the skill-quality handlers to one server's request context so each closure shares the path * validator, JSON responder, and body reader. * * @param ctx - per-server dashboard route context with path validation, the body reader, and IO hooks * @returns the skill-quality, inventory, and evaluate handlers; each resolves true once it has * answered a matching request, or false to let another handler claim the URL */ export function createSkillQualityRouteHandlers(ctx) { return { handleSkillQualityRequest: (url, res) => handleSkillQualityRequest(ctx, url, res), handleSkillQualityInventoryRequest: (url, res) => handleSkillQualityInventoryRequest(ctx, url, res), handleQualityEvaluateRequest: (req, url, res) => handleQualityEvaluateRequest(ctx, req, url, res), }; } //# sourceMappingURL=dashboard-skill-quality-routes.js.map