@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
JavaScript
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