@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.
344 lines • 17.4 kB
JavaScript
import { loadManifest } from "../manifest/manifest.js";
import { PROFILES } from "../detect/agents.js";
import { getTemplatePath, getCliCommand } from "../paths.js";
/**
* Forward-slash version of a packaged template path, suitable for embedding
* in user-visible setup prompts. `getTemplatePath` returns OS-native paths
* (backslashes on Windows); they render as ugly backslash strings to the
* agent and break test assertions that compare against POSIX shapes.
*/
function displayTemplatePath(relative) {
return getTemplatePath(relative).replace(/\\/g, "/");
}
import { classifyProjectState } from "../classify-state.js";
import { createFS } from "../facts/fs.js";
import { resolve } from "node:path";
/** Return `.` when projectRoot is the cwd, otherwise the absolute path. */
function targetArg(projectRoot) {
return resolve(projectRoot) === resolve(process.cwd())
? "."
: resolve(projectRoot);
}
/** Public one-command installer users can run from any project. */
function installCommand(projectRoot, agentId) {
return `npx @blundergoat/goat-flow@latest install ${targetArg(projectRoot)} --agent ${agentId}`;
}
// ----------------------------------------------------------------
// Setup-step references
// ----------------------------------------------------------------
/** Maps audit check IDs to the setup step that fixes them. */
const CHECK_TO_STEP = {
lessons: "Step 05 (customise to project)",
footguns: "Step 05 (customise to project)",
architecture: "Step 04 (architecture and code map)",
"code-map": "Step 04 (architecture and code map)",
glossary: "Step 05 (customise to project)",
patterns: "Step 05 (customise to project)",
decisions: "Step 04 (architecture and code map)",
"session-logs": "Step 04 (architecture and code map)",
tasks: "Step 04 (architecture and code map)",
"other-files": "Step 02 (instruction file) or Step 04 (architecture)",
"config-parses": "Step 02 or Step 05 (config.yaml)",
"config-version": "Step 05 (config version field)",
"agent-instruction": "Step 02 (instruction file for agent)",
"agent-skills": "Step 03 (install skills)",
"agent-settings": "Step 05 (customise - settings file)",
"agent-guardrails": "Step 05 (customise - deny mechanism)",
};
/** Lookup from agent ID to its agent-specific setup guide. */
const SETUP_FILES = {
claude: "workflow/setup/agents/claude.md",
codex: "workflow/setup/agents/codex.md",
antigravity: "workflow/setup/agents/antigravity.md",
copilot: "workflow/setup/agents/copilot.md",
};
function usesLimitedDenyEvidence(evidenceLevel) {
return evidenceLevel === "static" || evidenceLevel === "present-only";
}
function auditPassHeadline(evidenceLevel) {
return usesLimitedDenyEvidence(evidenceLevel)
? "Dashboard setup checks pass."
: "All audit checks pass.";
}
function auditPassInstallLine(evidenceLevel) {
if (!usesLimitedDenyEvidence(evidenceLevel)) {
return "- Audit: all build checks passing";
}
const label = evidenceLevel === "present-only" ? "presence-only" : "static";
return `- Audit: ${label} setup checks passing; runtime deny-hook probes not run`;
}
// ----------------------------------------------------------------
// Mode: Audit pass (current version, all build checks passing)
// ----------------------------------------------------------------
function renderAuditPass(facts, agentId, evidenceLevel) {
const profile = PROFILES[agentId];
const agentFacts = facts.agents.find((af) => af.agent.id === agentId);
const lines = [];
lines.push(`# GOAT Flow Setup - ${profile.name}`);
lines.push("");
lines.push(auditPassHeadline(evidenceLevel));
lines.push("");
if (agentFacts) {
const skillCount = agentFacts.skills.found.length;
const totalSkills = loadManifest().facts.skills.total;
const hookScripts = [];
if (agentFacts.hooks.denyExists)
hookScripts.push("deny");
if (agentFacts.hooks.postTurnExists)
hookScripts.push("post-turn");
const hooksDir = profile.hooksDir ?? "hooks";
lines.push("**Installed:**");
lines.push(`- ${skillCount}/${totalSkills} skills installed (in ${profile.skillsDir}/)`);
if (hookScripts.length > 0) {
lines.push(`- ${hookScripts.length} hook scripts (${hookScripts.join(", ")}) in ${hooksDir}/`);
}
lines.push(auditPassInstallLine(evidenceLevel));
lines.push("");
}
lines.push("**Run now:**");
lines.push(`Run \`goat-flow audit ${targetArg(facts.root)} --harness --agent ${agentId}\` and report the per-concern scores. This is the harness verification gate - do not skip it.`);
lines.push("");
lines.push("**Maintenance:**");
lines.push("- After upgrading goat-flow, re-run `goat-flow audit` to check for new checks");
lines.push("- Run `goat-flow audit` in CI to catch drift");
lines.push("- Review `.goat-flow/learning-loop/footguns/` and `.goat-flow/learning-loop/lessons/` after incidents");
return lines.join("\n");
}
function renderHarnessCardPass(facts, agentId, evidenceLevel) {
const profile = PROFILES[agentId];
const lines = [];
lines.push(`# GOAT Flow Setup - ${profile.name}`);
lines.push("");
lines.push(auditPassHeadline(evidenceLevel));
lines.push("");
lines.push("The harness-scored Setup card is passing for this target agent.");
lines.push("");
lines.push("**Run now:**");
lines.push(`Run \`${rerunAuditCommand(facts, agentId, true)}\` and report the per-concern scores. This is the harness verification gate - do not skip it.`);
lines.push("");
lines.push("**Maintenance:**");
lines.push("- After upgrading goat-flow, re-run the dashboard Re-audit action to refresh card-scoped setup prompts");
return lines.join("\n");
}
function auditStatusForPrompt(auditReport, promptScope) {
if (promptScope === "harness-card") {
return auditReport.scopes.harness?.status ?? auditReport.status;
}
return auditReport.status;
}
function failedChecksForPrompt(auditReport, promptScope) {
const isPromptFailure = (c) => c.status === "fail" &&
c.failure !== undefined &&
!c.acknowledged &&
c.type !== "metric";
if (promptScope === "harness-card") {
return auditReport.scopes.harness?.checks.filter(isPromptFailure) ?? [];
}
return [
...auditReport.scopes.setup.checks.filter(isPromptFailure),
...auditReport.scopes.agent.checks.filter(isPromptFailure),
...(auditReport.scopes.harness?.checks.filter(isPromptFailure) ?? []),
];
}
function rerunAuditCommand(facts, agentId, includeHarness) {
const scopeFlag = includeHarness ? " --harness" : "";
return `${getCliCommand()} audit ${targetArg(facts.root)}${scopeFlag} --agent ${agentId}`;
}
/** Build the audit command shown in generated setup prompts for the selected agent. */
function auditCommand(facts, agentId) {
return `${getCliCommand()} audit ${targetArg(facts.root)} --agent ${agentId}`;
}
/** Build the harness audit command variant used by setup follow-up instructions. */
function harnessAuditCommand(facts, agentId) {
return `${getCliCommand()} audit ${targetArg(facts.root)} --agent ${agentId} --harness`;
}
function pushFinalSetupGate(lines, facts, agentId) {
lines.push("**Audit:** Run both required setup gates:");
lines.push(`- \`${auditCommand(facts, agentId)}\``);
lines.push(`- \`${harnessAuditCommand(facts, agentId)}\``);
lines.push("");
lines.push("**Target: both audits pass with zero failures.**");
lines.push(`If either audit fails, run \`${getCliCommand()} setup ${targetArg(facts.root)} --agent ${agentId}\` for remaining fix instructions, then re-run both audit gates. Repeat until both pass (max 3 cycles).`);
}
function promptIncludesHarness(auditReport, promptScope) {
return (promptScope === "harness-card" ||
auditReport.harness ||
auditReport.scopes.harness !== null);
}
function renderAuditFail(auditReport, facts, agentId, promptScope) {
const profile = PROFILES[agentId];
const lines = [];
const failedChecks = failedChecksForPrompt(auditReport, promptScope);
const includeHarness = promptIncludesHarness(auditReport, promptScope);
lines.push(`# GOAT Flow Setup - ${profile.name}`);
lines.push("");
lines.push(`${failedChecks.length} audit ${failedChecks.length === 1 ? "check" : "checks"} failed:`);
lines.push("");
let num = 1;
for (const check of failedChecks) {
const failure = check.failure;
if (!failure)
continue;
const step = CHECK_TO_STEP[check.id] ?? "relevant setup step";
lines.push(`${num++}. **${failure.check}** - FAIL`);
lines.push(` ${failure.message}`);
if (failure.evidence)
lines.push(` Evidence: ${failure.evidence}`);
if (failure.howToFix) {
lines.push(` Fix: ${failure.howToFix} (see ${step})`);
}
else {
lines.push(` See ${step}`);
}
lines.push("");
}
lines.push(`**Target: audit passes with zero failures.**`);
lines.push(`Re-run: \`${rerunAuditCommand(facts, agentId, includeHarness)}\``);
lines.push(`If audit fails, run \`${getCliCommand()} setup ${targetArg(facts.root)} --agent ${agentId}\` for fix instructions. Repeat until audit passes (max 3 cycles).`);
return lines.join("\n");
}
/** Select only setup failures because upgrade prompts should not include passing or warning checks. */
function failedInstallChecks(auditReport) {
return [
...auditReport.scopes.setup.checks.filter((c) => c.status === "fail"),
...auditReport.scopes.agent.checks.filter((c) => c.status === "fail"),
];
}
function pushDetectedInstallIssues(lines, auditReport) {
const failures = failedInstallChecks(auditReport).filter((check) => check.failure !== undefined);
if (failures.length === 0)
return;
lines.push("## Detected install issues");
lines.push("");
for (const check of failures) {
const failure = check.failure;
if (!failure)
continue;
lines.push(`- **${failure.check}:** ${failure.message}`);
if (failure.evidence)
lines.push(` Evidence: ${failure.evidence}`);
if (failure.howToFix)
lines.push(` Fix: ${failure.howToFix}`);
}
lines.push("");
}
function renderUpgradeRedirect(auditReport, facts, agentId, state, detectedVersion) {
const profile = PROFILES[agentId];
const lines = [];
if (state === "outdated") {
lines.push(`# GOAT Flow Upgrade - ${profile.name}`);
lines.push("");
lines.push(detectedVersion
? `This project has goat-flow ${detectedVersion}.`
: "This project has an older goat-flow version.");
lines.push("");
pushDetectedInstallIssues(lines, auditReport);
lines.push("## Step 1 - Install files");
lines.push("");
lines.push(`Run: \`${installCommand(facts.root, agentId)}\``);
lines.push("");
lines.push("This refreshes skills, hooks, settings, and reference files to the current version.");
lines.push("");
lines.push("## Step 2 - Rebuild project-specific content");
lines.push("");
lines.push(`Continue with \`${displayTemplatePath("workflow/setup/02-instruction-file.md")}\` and then the remaining numbered setup docs to refresh the instruction file and local goat-flow content in place.`);
}
else {
lines.push(`# GOAT Flow Migration - ${profile.name}`);
lines.push("");
lines.push("This project has old goat-flow skills (v0.9 era).");
lines.push("");
pushDetectedInstallIssues(lines, auditReport);
lines.push("## Step 1 - Install current files");
lines.push("");
lines.push(`Run: \`${installCommand(facts.root, agentId)}\``);
lines.push("");
lines.push(`This installs the ${loadManifest().facts.skills.total} canonical skills, hooks, settings, and reference files.`);
lines.push("");
lines.push("## Step 2 - Remove legacy surfaces");
lines.push("");
lines.push(`If the install step above did not already run with \`--clean-deprecated\`, run \`${installCommand(facts.root, agentId)} --clean-deprecated\` to remove deprecated skill directories. Preserve any useful content in \`.goat-flow/logs/sessions/\`, then remove any remaining flat learning-loop docs and legacy task-state files.`);
lines.push("");
lines.push("## Step 3 - Rebuild project-specific content");
lines.push("");
lines.push(`Continue with \`${displayTemplatePath("workflow/setup/02-instruction-file.md")}\` and then the remaining numbered setup docs to rebuild the project-specific goat-flow surfaces on the current layout.`);
}
lines.push("");
lines.push(`## ${state === "outdated" ? "Step 3" : "Step 4"} - Verify`);
lines.push("");
pushFinalSetupGate(lines, facts, agentId);
return lines.join("\n");
}
/** Render the full setup prompt for bare/partial projects because no targeted repair path exists yet. */
function renderFullSetup(facts, agentId) {
const profile = PROFILES[agentId];
const setupFile = displayTemplatePath(SETUP_FILES[agentId]);
const lines = [];
const agentFacts = facts.agents.find((af) => af.agent.id === agentId);
lines.push(`# GOAT Flow Setup - ${profile.name}`);
lines.push("");
if (agentFacts) {
lines.push(`This project has setup issues - it needs a full setup pass. Run \`${getCliCommand()} audit ${targetArg(facts.root)}\` after fixing to verify.`);
}
else {
lines.push(`No ${profile.name} configuration detected - this project needs a full setup.`);
}
lines.push("");
lines.push('Do NOT copy customization templates (architecture, footguns, code-map) verbatim. If a template says "[describe X]", describe X for THIS project. Skill SKILL.md files ARE installed verbatim - this rule applies to Step 04-05 artifacts only.');
lines.push("");
lines.push("## Step 1 - Install files");
lines.push("");
lines.push(`Run: \`${installCommand(facts.root, agentId)}\``);
lines.push("");
lines.push("This deterministically copies skills, hooks, settings, and reference files. It does not require an agent. Verify it completes with zero errors.");
lines.push("");
lines.push("## Step 2 - Create project-specific content");
lines.push("");
lines.push(`Read \`${setupFile}\` for agent-specific paths, then follow the setup steps in \`${displayTemplatePath("workflow/setup/")}\` one at a time:`);
lines.push("");
lines.push("- **01-system-overview.md** - Design intent, state check, session-log setup");
lines.push("- **02-instruction-file.md** - Create or update the instruction file");
lines.push("- **04-architecture-code-map.md** - Create architecture and code map docs");
lines.push("- **05-customise-to-project.md** - Deep codebase read, real footguns/lessons, auto-seeded git signals, and project-specific instruction refinement");
lines.push("- **06-final-verification.md** - Audit passes, stale-ref check, file manifest, command smoke test");
lines.push("");
lines.push("Each step is self-contained with a verification gate. Complete one step before moving to the next.");
lines.push("");
lines.push("## Step 3 - Verify");
lines.push("");
pushFinalSetupGate(lines, facts, agentId);
return lines.join("\n");
}
// ----------------------------------------------------------------
// Main entry point
// ----------------------------------------------------------------
const FULL_SETUP_STATES = new Set(["bare", "partial", "error"]);
const UPGRADE_STATES = new Set(["v0.9", "outdated"]);
/**
* Compose the setup prompt that matches the project's current install state.
*
* @param auditReport - current audit result used to select failure/upgrade/full setup copy
* @param facts - project facts used to derive installed state and prompt paths
* @param agentId - agent whose setup instructions should be rendered
* @param options - optional output scope and deny-mechanism evidence hint
* @returns setup prompt text, or null when no setup action applies
*/
export function composeSetup(auditReport, facts, agentId, options = {}) {
const projectFS = createFS(facts.root);
const projectState = classifyProjectState(projectFS, agentId);
const promptScope = options.promptScope ?? "full";
if (FULL_SETUP_STATES.has(projectState.state) ||
projectState.action === "incomplete") {
return renderFullSetup(facts, agentId);
}
if (UPGRADE_STATES.has(projectState.state)) {
return renderUpgradeRedirect(auditReport, facts, agentId, projectState.state, projectState.version);
}
if (auditStatusForPrompt(auditReport, promptScope) === "pass") {
return promptScope === "harness-card"
? renderHarnessCardPass(facts, agentId, options.denyMechanismEvidenceLevel)
: renderAuditPass(facts, agentId, options.denyMechanismEvidenceLevel);
}
return renderAuditFail(auditReport, facts, agentId, promptScope);
}
//# sourceMappingURL=compose-setup.js.map