@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.
198 lines • 7.99 kB
JavaScript
/**
* Dispatch layer for the `goat-flow quality` command and its subcommands (history, diff,
* candidacy, validate, and the default prompt builder). Each subcommand is a focused async
* handler; the public entry point only routes by `options.qualitySubcommand`.
*
* Heavy modules (history, candidacy, audit, prompt composition) are dynamically imported inside
* each handler so the CLI startup path stays lean and only loads what a given invocation needs.
* All filesystem and process behaviour is injected through QualityCommandDeps so the handlers
* stay testable; this module performs no I/O of its own beyond reading files the user pointed at.
*/
import { basename } from "node:path";
async function handleQualityHistorySubcommand(options, deps) {
const { buildQualityHistoryRows, loadQualityHistory, renderQualityHistoryText, selectQualityHistoryEntries, } = await import("./history.js");
const history = loadQualityHistory(options.projectPath);
for (const warning of history.warnings) {
console.error(warning);
}
const selectedEntries = selectQualityHistoryEntries(history.entries, {
agent: options.agent,
limit: options.includeAll ? null : 20,
qualityMode: options.qualityMode,
});
const rows = buildQualityHistoryRows(history.entries, {
agent: options.agent,
limit: options.includeAll ? null : 20,
qualityMode: options.qualityMode,
});
if (options.format === "json") {
deps.writeOutput(options, JSON.stringify({
reports: selectedEntries.map((entry) => ({
id: entry.id,
path: entry.path,
report: entry.report,
})),
deltas: rows.map((row) => ({
id: row.id,
setup_delta: row.setupDelta,
})),
}, null, 2));
return;
}
deps.writeOutput(options, renderQualityHistoryText(rows, {
agent: options.agent,
qualityMode: options.qualityMode,
includeAll: options.includeAll,
}));
}
async function handleQualityDiffSubcommand(options, deps) {
const { buildQualityDiff, loadQualityHistory, renderQualityDiffText } = await import("./history.js");
const history = loadQualityHistory(options.projectPath);
for (const warning of history.warnings) {
console.error(warning);
}
const diff = buildQualityDiff(history.entries, {
agent: options.agent,
pair: options.qualityDiffPair,
qualityMode: options.qualityMode,
});
if (!diff.ok)
throw new deps.CLIError(diff.error, 2);
if (options.format === "json") {
deps.writeOutput(options, JSON.stringify(diff.diff, null, 2));
return;
}
deps.writeOutput(options, renderQualityDiffText(diff.diff));
}
async function handleQualityCandidacySubcommand(options, deps) {
if (!options.candidacyInput) {
throw new deps.CLIError("quality candidacy: pass --draft <path> or a description string.", 2);
}
const { runCandidacyCheck } = await import("./candidacy.js");
const { readFileSync, existsSync } = await import("node:fs");
let result;
if (options.candidacyInput.mode === "draft") {
const path = options.candidacyInput.value;
if (!existsSync(path)) {
throw new deps.CLIError(`quality candidacy: file not found: ${path}`, 2);
}
result = runCandidacyCheck({
kind: "draft",
content: readFileSync(path, "utf-8"),
suggestedName: basename(path).replace(/\.md$/, ""),
});
}
else {
result = runCandidacyCheck({
kind: "description",
text: options.candidacyInput.value,
});
}
if (options.format === "json") {
deps.writeOutput(options, JSON.stringify(result, null, 2));
return;
}
const lines = [];
lines.push(`Recommended artifact: ${deps.formatCandidacyArtifact(result.recommendedArtifact)}`);
lines.push(`Confidence: ${Math.round(result.confidence * 100)}%`);
if (result.reasoning.length > 0) {
lines.push("");
lines.push("Reasoning:");
for (const reason of result.reasoning)
lines.push(` - ${reason}`);
}
if (result.nextSteps.length > 0) {
lines.push("");
lines.push("Next steps:");
for (const step of result.nextSteps) {
lines.push(` - ${step.action}${step.template ? ` (template: ${step.template})` : ""}`);
}
}
deps.writeOutput(options, lines.join("\n"));
}
async function handleQualityValidateSubcommand(options, deps) {
if (!options.qualityValidatePath) {
throw new deps.CLIError("quality validate requires a path to the report file.", 2);
}
const { readFileSync, existsSync } = await import("node:fs");
const { parseQualityReport } = await import("./schema.js");
const path = options.qualityValidatePath;
if (!existsSync(path)) {
throw new deps.CLIError(`quality validate: file not found: ${path}`, 2);
}
let raw;
try {
raw = JSON.parse(readFileSync(path, "utf-8"));
}
catch (error) {
throw new deps.CLIError(`quality validate: invalid JSON in ${path}: ${error instanceof Error ? error.message : String(error)}`, 2);
}
const parsed = parseQualityReport(raw);
if (!parsed.ok) {
throw new deps.CLIError(`quality validate: schema error in ${path}: ${parsed.error}`, 2);
}
deps.writeOutput(options, `OK ${path}`);
}
async function handleQualityPromptSubcommand(options, deps) {
if (!options.agent) {
throw new deps.CLIError(`quality requires --agent. Usage: goat-flow quality . --agent ${deps.validAgents()[0] ?? "claude"}`, 2);
}
const { createFS } = await import("../facts/fs.js");
const { runAudit } = await import("../audit/audit.js");
const { composeQuality } = await import("../prompt/compose-quality.js");
const { findLatestQualityReport } = await import("./history.js");
const { loadConfig } = await import("../config/reader.js");
const { extractSharedFacts } = await import("../facts/shared/index.js");
const fs = createFS(options.projectPath);
let auditReport = null;
try {
auditReport = runAudit(fs, options.projectPath, {
agentFilter: options.agent,
harness: true,
});
}
catch {
// Quality prompts still render with degraded audit context.
}
const qualityMode = options.qualityMode ?? "agent-setup";
const { entry: priorReport, warnings: historyWarnings } = findLatestQualityReport(options.projectPath, options.agent, qualityMode);
for (const warning of historyWarnings) {
console.error(warning);
}
const sharedFacts = extractSharedFacts(fs, loadConfig(options.projectPath, fs));
const result = composeQuality({
agent: options.agent,
projectPath: options.projectPath,
auditReport,
priorReport,
qualityMode,
sharedFacts,
});
if (options.format === "json") {
deps.writeOutput(options, JSON.stringify(result, null, 2));
}
else {
deps.writeOutput(options, result.prompt);
}
}
/** Dispatch quality subcommands through focused branch handlers. */
export async function handleQualityCommand(options, deps) {
if (options.qualitySubcommand === "history") {
await handleQualityHistorySubcommand(options, deps);
return;
}
if (options.qualitySubcommand === "diff") {
await handleQualityDiffSubcommand(options, deps);
return;
}
if (options.qualitySubcommand === "candidacy") {
await handleQualityCandidacySubcommand(options, deps);
return;
}
if (options.qualitySubcommand === "validate") {
await handleQualityValidateSubcommand(options, deps);
return;
}
await handleQualityPromptSubcommand(options, deps);
}
//# sourceMappingURL=quality-command.js.map