@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.
436 lines • 16.7 kB
JavaScript
import { DEFAULT_QUALITY_CONFIG } from "./quality-config.js";
const MIN_DRAFT_LINES_FOR_SKILL = 30;
/** Extract deterministic structure signals from a draft before artifact routing. */
function inspectDraft(content, suggestedName) {
const lower = content.toLowerCase();
const lines = content.split("\n");
const name = (suggestedName ?? "").toLowerCase();
return {
hasStep0: /^##\s+step 0/im.test(content),
hasVerification: /^##\s+verification/im.test(content),
hasRouteMap: /^##\s+route map/im.test(content),
hasQuickScan: /^##\s+quick scan path/im.test(content),
hasAuditMode: /^##\s+audit mode/im.test(content),
hasAvailabilityCheck: /availability check/i.test(content),
hasFileWriteMode: /file-write/i.test(content),
hasIndexHints: /which file to load/i.test(content) ||
/sibling.*directory/i.test(content) ||
/routing table/i.test(lower),
hasRuleVocabulary: /\bMUST\b|\bMUST NOT\b|\balways\b|\bnever\b/i.test(content),
lineCount: lines.length,
startsWithIncident: /^(incident|postmortem|lesson)-/i.test(name) ||
/^#\s+(incident|postmortem|lesson)/im.test(content),
startsWithFootgun: /^footgun-/i.test(name) || /^#\s+footgun/im.test(content),
startsWithADR: /^adr-\d+/i.test(name),
hasADRStructure: /^##\s+(decision|context|consequences)/im.test(content) &&
/^##\s+context/im.test(content),
};
}
// eslint-disable-next-line complexity -- intentional because artifact-type signals must stay in priority order
function analyzeDraft(content, suggestedName) {
const signals = inspectDraft(content, suggestedName);
const reasoning = [];
// Strong skill signals
if (signals.hasStep0 && signals.hasVerification) {
reasoning.push("has both ## Step 0 and ## Verification headings");
return {
recommendedArtifact: { type: "skill", subtype: "workflow" },
confidence: 0.9,
reasoning,
nextSteps: [
{
action: "Place under .claude/skills/<name>/SKILL.md",
template: "workflow",
},
{ action: "Run skill-quality scoring after drafting" },
],
};
}
if (signals.hasRouteMap && !signals.hasStep0) {
reasoning.push("has ## Route Map without ## Step 0");
return {
recommendedArtifact: { type: "skill", subtype: "dispatcher" },
confidence: 0.85,
reasoning,
nextSteps: [
{
action: "Place under .claude/skills/<name>/SKILL.md",
template: "dispatcher",
},
],
};
}
if ((signals.hasQuickScan || signals.hasAuditMode) &&
!signals.hasFileWriteMode) {
reasoning.push("has Quick Scan / Audit Mode markers and no File-Write mode");
return {
recommendedArtifact: { type: "skill", subtype: "report" },
confidence: 0.85,
reasoning,
nextSteps: [
{
action: "Place under .claude/skills/<name>/SKILL.md",
template: "report",
},
],
};
}
// Reference signals
if (signals.hasAvailabilityCheck) {
reasoning.push("has Availability Check section");
return {
recommendedArtifact: { type: "reference", subtype: "playbook" },
confidence: 0.85,
reasoning,
nextSteps: [
{
action: "Place under .goat-flow/skill-docs/playbooks/<name>.md",
template: "playbook",
},
],
};
}
if (signals.hasIndexHints) {
reasoning.push("looks like an index/router for sibling references");
return {
recommendedArtifact: { type: "reference", subtype: "index" },
confidence: 0.7,
reasoning,
nextSteps: [
{
action: "Place under .goat-flow/skill-docs/playbooks/<name>.md",
template: "index",
},
],
};
}
// Learning-loop signals (name-based)
if (signals.startsWithIncident) {
reasoning.push("name or H1 matches incident/postmortem/lesson pattern");
return {
recommendedArtifact: { type: "learning-loop", subtype: "lesson" },
confidence: 0.85,
reasoning,
nextSteps: [
{
action: "Place under .goat-flow/learning-loop/lessons/<category>.md",
},
],
};
}
if (signals.startsWithFootgun) {
reasoning.push("name or H1 matches footgun pattern");
return {
recommendedArtifact: { type: "learning-loop", subtype: "footgun" },
confidence: 0.85,
reasoning,
nextSteps: [
{
action: "Place under .goat-flow/learning-loop/footguns/<category>.md",
},
],
};
}
if (signals.startsWithADR && signals.hasADRStructure) {
reasoning.push("ADR-NNN name with Decision/Context/Consequences structure");
return {
recommendedArtifact: { type: "learning-loop", subtype: "decision" },
confidence: 0.9,
reasoning,
nextSteps: [
{
action: "Place under .goat-flow/learning-loop/decisions/ADR-NNN-<title>.md",
},
],
};
}
// Short rule-shaped content
if (signals.lineCount < MIN_DRAFT_LINES_FOR_SKILL &&
signals.hasRuleVocabulary) {
reasoning.push(`${signals.lineCount} lines with MUST/always/never vocabulary`);
return {
recommendedArtifact: {
type: "instruction-file",
reason: "rule-shaped",
},
confidence: 0.75,
reasoning,
nextSteps: [
{
action: "Add to CLAUDE.md or AGENTS.md as a rule line",
},
],
};
}
if (signals.lineCount < 5) {
reasoning.push(`only ${signals.lineCount} lines of content`);
return {
recommendedArtifact: {
type: "do-not-create",
reason: "no-clear-intent",
},
confidence: 0.6,
reasoning,
nextSteps: [
{
action: "Provide more detail before deciding artifact type",
},
],
};
}
// Low-confidence fallback: needs human review
reasoning.push("no decisive structural signal found");
return {
recommendedArtifact: {
type: "do-not-create",
reason: "no-clear-intent",
},
confidence: 0.4,
reasoning,
nextSteps: [
{
action: "Add canonical headings (Step 0, Route Map, Availability Check, etc.) to clarify intent",
},
],
};
}
/** Preserve both original and lowercase description text for intent matchers. */
function tokenize(text) {
return { text, lower: text.toLowerCase() };
}
/** Detect whether description terms imply a reusable skill workflow. */
function matchSkillIntent(tokens) {
const { lower } = tokens;
const wantsGoatMode = /\bgoat-[a-z0-9-]+\b.*\bmode\b/.test(lower) ||
/\bmode\b.*\bgoat-[a-z0-9-]+\b/.test(lower);
const wantsWorkflow = /\b(workflow|protocol|process)\b/.test(lower) ||
/\b(?:plan|implement|execute|run)\b/.test(lower);
const wantsAudit = /\baudit|review|assess|check|inspect|scan|verify\b/.test(lower);
const wantsDispatch = /\bdispatch|route|orchestrate|coordinate|delegate\b/.test(lower);
const writesFiles = /\b(write|create|edit|modify|generate|produce|update)\b/.test(lower);
if (wantsGoatMode) {
return {
recommendedArtifact: { type: "skill", subtype: "workflow" },
confidence: 0.8,
reasoning: ["description changes a goat-* skill mode"],
nextSteps: [
{
action: "Edit the existing goat-* skill template and installed mirrors; keep Step 0, gates, and verification aligned",
},
],
};
}
if (wantsDispatch && !wantsWorkflow) {
return {
recommendedArtifact: { type: "skill", subtype: "dispatcher" },
confidence: 0.7,
reasoning: [
'description mentions dispatch/route vocabulary without "workflow"',
],
nextSteps: [
{ action: "Scaffold a dispatcher SKILL.md with a Route Map section" },
],
};
}
if (wantsAudit && !writesFiles) {
return {
recommendedArtifact: { type: "skill", subtype: "report" },
confidence: 0.75,
reasoning: ["description mentions audit/review without write actions"],
nextSteps: [
{
action: "Scaffold a report-style SKILL.md with Quick Scan Path and no File-Write mode",
},
],
};
}
if (wantsWorkflow) {
return {
recommendedArtifact: { type: "skill", subtype: "workflow" },
confidence: 0.7,
reasoning: ["description mentions workflow / protocol / execute"],
nextSteps: [
{
action: "Scaffold a workflow SKILL.md with Step 0, phases, and Verification gates",
},
],
};
}
return null;
}
function matchReferenceIntent(tokens) {
const { lower } = tokens;
const wantsReferenceArtifact = /\b(playbook|runbook|standards?|guidance|reference|how-?to)\b/.test(lower) ||
/\b(document|describe|explain|write)\b.*\b(standards?|guidance|runbook|playbook)\b/.test(lower);
const isSkillModeChange = /\bgoat-[a-z0-9-]+\b.*\bmode\b/.test(lower);
if (wantsReferenceArtifact && !isSkillModeChange) {
return {
recommendedArtifact: { type: "reference", subtype: "playbook" },
confidence: 0.75,
reasoning: [
"description asks for reusable standards/guidance rather than an invocation workflow",
],
nextSteps: [
{
action: "Place under .goat-flow/skill-docs/playbooks/<name>.md with Availability Check, boundary, workflow, fallback, and verification gate sections",
},
],
};
}
if (/\b(document|describe|explain|reference|playbook)\s+(how|the way)/.test(lower) ||
/\bhow to use\b/.test(lower)) {
return {
recommendedArtifact: { type: "reference", subtype: "playbook" },
confidence: 0.7,
reasoning: ["description asks to document how to use a tool/capability"],
nextSteps: [
{
action: "Place under .goat-flow/skill-docs/playbooks/<name>.md with Availability Check, boundary, workflow, fallback, and verification gate sections",
},
],
};
}
return null;
}
function matchInstructionRuleIntent(tokens) {
const { lower } = tokens;
if (/\b(rule|policy|constraint|always|never|must)\b/.test(lower) &&
!/\bworkflow|process|protocol|step\b/.test(lower)) {
return {
recommendedArtifact: {
type: "instruction-file",
reason: "rule-shaped",
},
confidence: 0.65,
reasoning: [
"description sounds like a rule/policy/constraint without procedure",
],
nextSteps: [{ action: "Add the rule to CLAUDE.md or AGENTS.md" }],
};
}
return null;
}
function matchLearningLoopIntent(tokens) {
const { lower } = tokens;
if (/\blearn(?:ed|t)\b.*\b(failure mode|mistake|incident|lesson)\b/.test(lower) ||
/\bremember|capture|record\b.*\b(mistake|incident|past|lesson)\b/.test(lower) ||
/\bpost.?mortem\b/.test(lower)) {
return {
recommendedArtifact: { type: "learning-loop", subtype: "lesson" },
confidence: 0.7,
reasoning: [
"description references past mistake / incident / postmortem",
],
nextSteps: [
{ action: "Add to .goat-flow/learning-loop/lessons/<category>.md" },
],
};
}
if (/\b(footgun|trap|gotcha|landmine)\b/.test(lower) ||
/\b(easy to|tempting to)\b.*\bbut\b/.test(lower)) {
return {
recommendedArtifact: { type: "learning-loop", subtype: "footgun" },
confidence: 0.7,
reasoning: ["description sounds like a footgun / trap warning"],
nextSteps: [
{ action: "Add to .goat-flow/learning-loop/footguns/<category>.md" },
],
};
}
if (/\b(decision record|adr|architecture decision)\b/.test(lower) ||
/\b(decided|chose)\b.*\b(because|so that)\b/.test(lower)) {
return {
recommendedArtifact: { type: "learning-loop", subtype: "decision" },
confidence: 0.75,
reasoning: ["description references a design/architecture decision"],
nextSteps: [
{
action: "Add an ADR under .goat-flow/learning-loop/decisions/ADR-NNN-<title>.md",
},
],
};
}
return null;
}
function matchCliCommandIntent(tokens) {
const { lower } = tokens;
if (/\bgenerate\b.*\b(index|indexes|indices)\b.*\b(markdown|md)\b/.test(lower) ||
(/\b(one.?(?:shot|time)|deterministic|same way every time)\b/.test(lower) &&
!/\b(decision|gate|approve)\b/.test(lower))) {
return {
recommendedArtifact: { type: "cli-command" },
confidence: 0.65,
reasoning: [
"description mentions one-shot / deterministic / no decisions",
],
nextSteps: [
{ action: "Add as a CLI subcommand or audit check, not a skill" },
],
};
}
return null;
}
/** Route a free-form artifact description to the recommended goat-flow artifact type. */
function analyzeDescription(text) {
const trimmed = text.trim();
if (trimmed.length === 0) {
return {
recommendedArtifact: {
type: "do-not-create",
reason: "no-clear-intent",
},
confidence: 0.95,
reasoning: ["empty description"],
nextSteps: [
{ action: "Describe what the artifact should do, then re-run" },
],
};
}
const tokens = tokenize(trimmed);
const matchers = [
matchLearningLoopIntent,
matchCliCommandIntent,
matchInstructionRuleIntent,
matchReferenceIntent,
matchSkillIntent,
];
for (const matcher of matchers) {
const result = matcher(tokens);
if (result)
return result;
}
return {
recommendedArtifact: {
type: "do-not-create",
reason: "no-clear-intent",
},
confidence: 0.4,
reasoning: [
"description does not match any recognized artifact intent (workflow, audit, reference, rule, lesson, footgun, decision, cli-command)",
],
nextSteps: [
{
action: "Rephrase with one of: 'I want a workflow that...', 'I want to document how to...', 'I want to remember a mistake...', 'I want a rule that...'",
},
],
};
}
/**
* Run the candidacy check against either a markdown draft or a free-text
* description. Returns a recommended artifact type and the reasoning behind
* the recommendation.
*
* The optional `config` parameter is reserved for future per-project
* heuristic overrides (currently the v1 heuristics are project-independent).
*
* @param input - draft markdown or free-text description to classify
* @param _config - reserved quality config for future project-specific heuristics
* @returns candidacy recommendation, confidence, reasoning, and next steps
*/
export function runCandidacyCheck(input, _config = DEFAULT_QUALITY_CONFIG) {
if (input.kind === "draft") {
return analyzeDraft(input.content, input.suggestedName);
}
return analyzeDescription(input.text);
}
//# sourceMappingURL=candidacy.js.map