@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.
335 lines • 13.1 kB
JavaScript
export { renderAuditSarif } from "./sarif.js";
// === Text renderer ===
const GREEN = "\x1b[32m";
const RED = "\x1b[31m";
const YELLOW = "\x1b[33m";
const CYAN = "\x1b[36m";
const DIM = "\x1b[2m";
const BOLD = "\x1b[1m";
const RESET = "\x1b[0m";
/** Render a colored status badge for terminal output. */
function statusBadge(status) {
if (status === "skipped")
return `${YELLOW}SKIP${RESET}`;
return status === "pass" ? `${GREEN}PASS${RESET}` : `${RED}FAIL${RESET}`;
}
const ENFORCEMENT_STATUS_LABELS = {
hard: "HARD",
limited: "LIMITED",
soft: "SOFT",
missing: "MISSING",
unknown: "UNKNOWN",
};
/** Render a non-gating enforcement status label. */
function enforcementStatus(status) {
if (status === "hard")
return `${GREEN}${ENFORCEMENT_STATUS_LABELS[status]}${RESET}`;
if (status === "missing") {
return `${RED}${ENFORCEMENT_STATUS_LABELS[status]}${RESET}`;
}
if (status === "limited" || status === "soft") {
return `${YELLOW}${ENFORCEMENT_STATUS_LABELS[status]}${RESET}`;
}
return `${DIM}${ENFORCEMENT_STATUS_LABELS[status]}${RESET}`;
}
/** Render the advisory enforcement matrix in terminal text format. */
function renderEnforcementMatrix(matrix) {
const lines = [];
lines.push(`${BOLD}Agent Enforcement Matrix:${RESET} ${DIM}advisory; does not affect audit status${RESET}`);
lines.push("");
for (const agent of matrix) {
lines.push(` ${CYAN}${agent.name}${RESET}`);
for (const capability of agent.capabilities) {
lines.push(` ${enforcementStatus(capability.status)} ${capability.label}: ${capability.summary}`);
}
lines.push("");
}
return lines.join("\n").trimEnd();
}
/** Render one audit scope in the terminal text format. */
function renderTextScope(name, scope) {
const lines = [];
lines.push(`${name}:${" ".repeat(Math.max(1, 24 - name.length))}${statusBadge(scope.status)}`);
for (const [key, value] of Object.entries(scope.summary)) {
const label = key.charAt(0).toUpperCase() + key.slice(1);
lines.push(` ${label}:${" ".repeat(Math.max(1, 22 - label.length))}${value}`);
}
for (const failure of scope.failures) {
lines.push(` ${RED}x ${failure.check}: ${failure.message}${RESET}`);
if (failure.howToFix) {
lines.push(` ${CYAN}-> ${failure.howToFix}${RESET}`);
}
}
return lines.join("\n");
}
const CONCERN_LABELS = {
context: "Context",
constraints: "Constraints",
verification: "Verification",
recovery: "Recovery",
feedback_loop: "Feedback Loop",
};
/**
* Append the stable harness concern summary used by terminal output.
*
* This branch structure is intentional because the no-harness tip, concern
* order, and recommendation/fix pairing are part of the public output contract;
* structured `details` stay out of prose to preserve JSON/SARIF-only semantics.
*/
function renderHarnessConcerns(report, lines) {
if (!report.concerns || !report.scopes.harness) {
lines.push(`${DIM}Tip: Run with --harness for AI harness completeness checks across 5 concerns.${RESET}`);
return;
}
lines.push("");
lines.push(`${BOLD}AI Harness Completeness:${RESET} ${statusBadge(report.scopes.harness.status)}`);
lines.push("");
for (const key of Object.keys(report.concerns)) {
const concern = report.concerns[key];
const label = CONCERN_LABELS[key];
const badge = statusBadge(concern.status);
lines.push(` ${CYAN}${label}${RESET} ${badge}`);
for (const finding of concern.findings) {
lines.push(` ${DIM}${finding}${RESET}`);
}
for (const limit of concern.limits) {
lines.push(` ${YELLOW}Limit: ${limit}${RESET}`);
}
for (let i = 0; i < concern.recommendations.length; i++) {
lines.push(` ${YELLOW}-> ${concern.recommendations[i]}${RESET}`);
if (concern.howToFix[i]) {
lines.push(` ${CYAN}Fix: ${concern.howToFix[i]}${RESET}`);
}
}
lines.push("");
}
}
/**
* Render the full audit report in the terminal text format.
*
* @param report - Audit report produced by `runAudit` or `runAuditBatch`.
* @returns Human-readable terminal output with ANSI status labels.
*/
export function renderAuditText(report) {
const lines = [];
lines.push(`${BOLD}GOAT Flow Audit: ${report.target}${RESET}`);
lines.push("");
lines.push(renderTextScope("GOAT Flow Setup", report.scopes.setup));
lines.push("");
lines.push(renderTextScope("Agent Setup", report.scopes.agent));
lines.push("");
lines.push(`Result: ${statusBadge(report.status)}`);
const enforcement = Array.isArray(report.enforcement)
? report.enforcement
: [];
if (enforcement.length > 0) {
lines.push("");
lines.push(renderEnforcementMatrix(enforcement));
}
renderHarnessConcerns(report, lines);
if (report.drift) {
lines.push("");
lines.push(`${BOLD}Skill Template Drift:${RESET} ${statusBadge(report.drift.status)} ${DIM}(${report.drift.checked} comparison(s))${RESET}`);
lines.push("");
renderTextDriftFindings(report.drift, lines);
}
if (report.content) {
lines.push("");
lines.push(`${BOLD}Cold-Path Content Lint:${RESET} ${statusBadge(report.content.status)} ${DIM}(${report.content.warnings} warning(s), ${report.content.infos} info, ${report.content.filesScanned} file(s) scanned)${RESET}`);
lines.push("");
renderTextContentFindings(report.content, lines);
}
return lines.join("\n");
}
/** Render content-check findings in the terminal text format. */
function renderTextContentFindings(content, lines) {
if (content.findings.length === 0) {
lines.push(` ${DIM}No content issues detected.${RESET}`);
return;
}
for (const finding of content.findings) {
const color = finding.severity === "warning" ? RED : YELLOW;
const loc = finding.line !== undefined
? `${finding.path}:${finding.line}`
: finding.path;
lines.push(` ${color}${finding.severity.toUpperCase()} [${finding.rule}] ${loc}${RESET}`);
lines.push(` ${DIM}${finding.message}${RESET}`);
if (finding.suggestion) {
lines.push(` ${CYAN}-> ${finding.suggestion}${RESET}`);
}
}
}
/** Map a skill-dir path prefix to a goat-flow install --agent target.
* Returns null when the path doesn't match a known satellite-agent dir. */
function pathToAgentLabel(path) {
// `.agents/skills/` is shared by codex and antigravity; codex is the
// default install target for repair suggestions.
if (path.startsWith(".agents/skills/"))
return "codex";
if (path.startsWith(".claude/skills/"))
return "claude";
if (path.startsWith(".github/skills/"))
return "copilot";
return null;
}
/**
* Render drift findings in the terminal text format.
*
* The tag vocabulary is a stable user-facing contract, and the second pass is
* intentional because deprecated skills need one combined repair hint per
* agent rather than one repeated hint per finding.
*/
function renderTextDriftFindings(drift, lines) {
if (drift.findings.length === 0) {
lines.push(` ${DIM}No drift detected.${RESET}`);
return;
}
for (const finding of drift.findings) {
const tag = finding.kind === "content"
? "drift"
: finding.kind === "missing"
? "missing"
: finding.kind === "deprecated"
? "deprecated"
: "orphan";
lines.push(` ${RED}x [${tag}] ${finding.path}${RESET}`);
lines.push(` ${DIM}${finding.message}${RESET}`);
}
const staleAgents = new Set();
for (const finding of drift.findings) {
if (finding.kind !== "deprecated")
continue;
const agent = pathToAgentLabel(finding.path);
if (agent !== null)
staleAgents.add(agent);
}
if (staleAgents.size > 0) {
const agentList = [...staleAgents].sort().join(" / ");
lines.push(` ${DIM}Multi-agent drift: run \`goat-flow install . --agent <agent>\` for each stale agent (${agentList}).${RESET}`);
}
}
// === JSON renderer ===
export function renderAuditJson(report) {
return JSON.stringify(report, null, 2);
}
// === Markdown renderer ===
function mdScopeStatus(status) {
return status === "pass" ? "PASS" : "FAIL";
}
/** Render one audit scope in markdown. */
function renderMdScope(name, scope) {
const lines = [];
lines.push(`### ${name}: ${mdScopeStatus(scope.status)}`);
for (const [key, value] of Object.entries(scope.summary)) {
lines.push(`- **${key}**: ${value}`);
}
for (const failure of scope.failures) {
lines.push(`- :x: **${failure.check}**: ${failure.message}`);
if (failure.howToFix) {
lines.push(` - *Fix:* ${failure.howToFix}`);
}
}
return lines.join("\n");
}
/**
* Render harness concerns in markdown.
*
* Markdown preserves the same stable concern order and summary contract as the
* terminal renderer because PR comments and terminal output need comparable
* failure/recommendation ordering.
*/
function renderMdHarnessConcerns(report, lines) {
if (!report.concerns || !report.scopes.harness) {
lines.push("> Tip: Run with --harness for AI harness completeness checks across 5 concerns.");
lines.push("");
return;
}
lines.push("");
lines.push(`## AI Harness Completeness: ${mdScopeStatus(report.scopes.harness.status)}`);
lines.push("");
for (const key of Object.keys(report.concerns)) {
const concern = report.concerns[key];
lines.push(`### ${CONCERN_LABELS[key]}: ${mdScopeStatus(concern.status)}`);
for (const finding of concern.findings) {
lines.push(`- ${finding}`);
}
for (const limit of concern.limits) {
lines.push(`- *Limit:* ${limit}`);
}
for (let i = 0; i < concern.recommendations.length; i++) {
lines.push(`- *Recommendation:* ${concern.recommendations[i]}`);
if (concern.howToFix[i]) {
lines.push(` - *Fix:* ${concern.howToFix[i]}`);
}
}
lines.push("");
}
}
/**
* Render drift findings in markdown using stable finding-kind labels.
*
* The `[kind] path - message` shape is the public markdown contract for drift
* reports, matching the data model without exposing terminal-only repair hints.
*/
function renderMdDrift(drift, lines) {
lines.push("");
lines.push(`## Skill Template Drift: ${mdScopeStatus(drift.status)} (${drift.checked} comparison(s))`);
if (drift.findings.length === 0) {
lines.push("");
lines.push("No drift detected.");
}
else {
for (const finding of drift.findings) {
lines.push(`- :x: **[${finding.kind}]** \`${finding.path}\` - ${finding.message}`);
}
}
lines.push("");
}
/**
* Render content-check findings in markdown using stable severity/rule labels.
*
* The location and suggestion shape is a public contract with content-quality
* consumers, so markdown keeps the same diagnostic fields as terminal output.
*/
function renderMdContent(content, lines) {
lines.push("");
lines.push(`## Cold-Path Content Lint: ${mdScopeStatus(content.status)} (${content.warnings} warning(s), ${content.infos} info, ${content.filesScanned} file(s) scanned)`);
if (content.findings.length === 0) {
lines.push("");
lines.push("No content issues detected.");
}
else {
for (const finding of content.findings) {
const loc = finding.line !== undefined
? `${finding.path}:${finding.line}`
: finding.path;
lines.push(`- :x: **${finding.severity.toUpperCase()} [${finding.rule}]** \`${loc}\` - ${finding.message}`);
if (finding.suggestion)
lines.push(` - *Fix:* ${finding.suggestion}`);
}
}
lines.push("");
}
/**
* Render the full audit report in markdown.
*
* @param report - Audit report produced by `runAudit` or `runAuditBatch`.
* @returns Markdown suitable for PR comments and copied audit summaries.
*/
export function renderAuditMarkdown(report) {
const lines = [];
lines.push(`# GOAT Flow Audit: ${report.target}`);
lines.push("");
lines.push(`**Result: ${mdScopeStatus(report.status)}**`);
lines.push("");
lines.push(renderMdScope("GOAT Flow Setup", report.scopes.setup));
lines.push("");
lines.push(renderMdScope("Agent Setup", report.scopes.agent));
renderMdHarnessConcerns(report, lines);
if (report.drift)
renderMdDrift(report.drift, lines);
if (report.content)
renderMdContent(report.content, lines);
return lines.join("\n");
}
//# sourceMappingURL=render.js.map