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.

127 lines 5.73 kB
/** * Classify a project's goat-flow adoption state by probing for config files, * skill directories, and AI instruction markers. Used by both the dashboard * `/api/projects/status` endpoint and the `goat-flow status` CLI command. */ import { AUDIT_VERSION, SKILL_NAMES, STALE_SKILL_NAMES } from "./constants.js"; import { getAgentProfiles } from "./agents/registry.js"; const CURRENT_VERSION_FAMILY = AUDIT_VERSION.split(".").slice(0, 2).join("."); const AGENT_PROFILES = getAgentProfiles(); const INSTRUCTION_FILES = AGENT_PROFILES.map((profile) => profile.instructionFile); const SKILL_ROOTS = [ ...new Set(AGENT_PROFILES.map((profile) => profile.skillsDir)), ]; /** Collect canonical skills found in any supported skill root. */ function collectInstalledSkills(fs) { return SKILL_NAMES.filter((skill) => SKILL_ROOTS.some((root) => fs.exists(`${root}/${skill}/SKILL.md`))); } /** Check whether any supported top-level instruction file exists. */ function hasAnyInstructionFile(fs) { return INSTRUCTION_FILES.some((file) => fs.exists(file)); } /** Collect deprecated skill directories still present in the project. */ function collectOldSkills(fs) { return STALE_SKILL_NAMES.filter((skill) => SKILL_ROOTS.some((root) => fs.exists(`${root}/${skill}/SKILL.md`))); } /** Build the detail message for a current-but-incomplete installation. */ function buildIncompleteDetails(installedSkills, hasInstructionFile, hasPreamble, hasConventions) { const missing = []; const missingSkills = SKILL_NAMES.filter((skill) => !installedSkills.includes(skill)); if (missingSkills.length > 0) { missing.push(`missing skills: ${missingSkills.join(", ")}`); } if (!hasInstructionFile) { missing.push("missing instruction file (CLAUDE.md / AGENTS.md / .github/copilot-instructions.md)"); } if (!hasPreamble) { missing.push("missing .goat-flow/skill-docs/skill-preamble.md"); } if (!hasConventions) { missing.push("missing .goat-flow/skill-docs/skill-conventions.md"); } return `Config says current goat-flow ${CURRENT_VERSION_FAMILY}.x but install is incomplete: ${missing.join("; ")}`; } /** Map from agentId to that agent's instruction file. */ const AGENT_INSTRUCTION_FILE = Object.fromEntries(AGENT_PROFILES.map((profile) => [profile.id, profile.instructionFile])); /** Classify a project's GOAT Flow adoption state. */ // eslint-disable-next-line complexity -- intentional branchy state machine; each branch maps one adoption state. export function classifyProjectState(fs, agentId) { const hasConfig = fs.exists(".goat-flow/config.yaml"); const installedSkills = collectInstalledSkills(fs); const currentSkillCount = installedSkills.length; const oldSkills = collectOldSkills(fs); const hasInstructionFile = agentId && AGENT_INSTRUCTION_FILE[agentId] ? fs.exists(AGENT_INSTRUCTION_FILE[agentId]) : hasAnyInstructionFile(fs); const hasPreamble = fs.exists(".goat-flow/skill-docs/skill-preamble.md"); const hasConventions = fs.exists(".goat-flow/skill-docs/skill-conventions.md"); const hasAIInstructions = fs.exists(".github/instructions") || hasInstructionFile; if (hasConfig) { const configContent = fs.readFile(".goat-flow/config.yaml"); const versionMatch = configContent?.match(/version:\s*["']?(\d+\.\d+\.\d+)/); const version = versionMatch?.[1]; if (!version) { return { state: "error", action: "setup", details: "Config exists but version could not be parsed from .goat-flow/config.yaml. Run setup to regenerate.", }; } if (version.startsWith(`${CURRENT_VERSION_FAMILY}.`)) { // Skill check is OR-union across roots - fast pre-check only. // A "healthy" classification here does not guarantee per-agent audit passes. // Run `goat-flow audit` for authoritative validation. const isHealthy = currentSkillCount === SKILL_NAMES.length && hasInstructionFile && hasPreamble && hasConventions; if (isHealthy) { return { state: "current", action: "audit", details: `Current version (${version}) - run \`goat-flow audit . --agent <agent>\` for per-agent validation`, version, }; } return { state: "current", action: "incomplete", details: buildIncompleteDetails(installedSkills, hasInstructionFile, hasPreamble, hasConventions), version, }; } return { state: "outdated", action: "upgrade", details: `Version ${version} - upgrade available`, version, }; } if (oldSkills.length > 0) { return { state: "v0.9", action: "migration", details: `Old skill names found (${oldSkills.join(", ")})`, }; } if (currentSkillCount > 0) { return { state: "partial", action: "setup", details: `${currentSkillCount}/${SKILL_NAMES.length} canonical skills found but no .goat-flow/ config - run setup to complete installation`, }; } if (hasAIInstructions) { return { state: "partial", action: "setup", details: "AI instructions exist but no goat-flow", }; } return { state: "bare", action: "setup", details: "No AI agent configuration found", }; } //# sourceMappingURL=classify-state.js.map