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.

499 lines 21.2 kB
import { getRequiredInstructionSections } from "../../manifest/manifest-json.js"; import { pass, fail, extractBacktickPaths } from "./helpers.js"; /** The execution-loop section label the manifest declares. Used by * `executionLoopPresent` to find the heading regex. Change here only if the * label itself changes; the regex is derived from the manifest, not literal. */ const EXECUTION_LOOP_LABEL = "Execution Loop"; const VERIFIED_ON = "2026-04-18"; const CONTROLLING_WORKSPACE_PATTERNS = [ /\bcontrolling\s+(?:goat-flow\s+)?workspace\b/i, /\bframework\s+workspace\b/i, /\bgoat-flow\s+controlling\s+workspace\b/i, ]; const TARGET_WORKSPACE_PATTERNS = [ /\bselected\s+target\b/i, /\btarget\s+project\b/i, /\bproject-specific\s+(?:harness\s+)?content\b/i, ]; const BOUNDARY_HEADING_PATTERN = /\bworkspace\s+boundary\b/i; const CORE_DOC_FILES = [ "CONTRIBUTING.md", ".goat-flow/code-map.md", ".goat-flow/glossary.md", "docs/cli.md", "docs/audit-and-quality.md", ]; /** Escape text for dynamic regex construction. */ function escapeRegex(text) { return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } /** * Return the markdown section body under a heading label. * * The helper reads only the provided string; RegExp cursors are local so section * detection cannot mutate audit context or project files. */ function markdownSectionByLabel(content, label) { const headingPattern = new RegExp(`^(?<marks>#{1,6})\\s+${escapeRegex(label)}\\b.*$`, "im"); const heading = headingPattern.exec(content); if (!heading?.groups?.marks) return null; const level = heading.groups.marks.length; const start = heading.index + heading[0].length; const nextHeadingPattern = /^#{1,6}\s+.*$/gm; nextHeadingPattern.lastIndex = start; let end = content.length; for (const nextHeading of content.matchAll(nextHeadingPattern)) { const marks = /^#+/.exec(nextHeading[0])?.[0] ?? ""; if (marks.length <= level) { end = nextHeading.index; break; } } return content.slice(start, end).trim(); } /** Return the context provenance. */ function contextProvenance(type, paths, sourceType = "spec") { return { source_type: sourceType, source_urls: [], verified_on: VERIFIED_ON, normative_level: type === "integrity" ? "MUST" : type === "advisory" ? "SHOULD" : "BEST_PRACTICE", evidence_paths: paths, }; } const instructionLineCount = { id: "instruction-line-count", name: "Instruction file size", concern: "context", type: "advisory", evidenceKind: "structural", provenance: contextProvenance("advisory", [ "docs/harness-audit.md", "CLAUDE.md", "AGENTS.md", ".github/copilot-instructions.md", ]), /** Run the Instruction file size check. */ run: (ctx) => { const findings = []; const recs = []; const fixes = []; const lineCounts = []; let hasLineCountFailure = false; const limit = ctx.config.config.lineLimits.limit; const target = ctx.config.config.lineLimits.target; for (const agentFacts of ctx.agents) { if (!agentFacts.instruction.exists) { findings.push(`${agentFacts.agent.id}: no instruction file`); recs.push(`Create ${agentFacts.agent.instructionFile}`); fixes.push(`Create ${agentFacts.agent.instructionFile} by running \`goat-flow setup\`.`); lineCounts.push({ agent: agentFacts.agent.id, actual: 0, target, hardLimit: limit, }); hasLineCountFailure = true; continue; } const lines = agentFacts.instruction.lineCount; lineCounts.push({ agent: agentFacts.agent.id, actual: lines, target, hardLimit: limit, }); if (lines > limit) { findings.push(`${agentFacts.agent.id}: ${lines} lines (exceeds hard limit ${limit})`); recs.push(`Reduce ${agentFacts.agent.instructionFile} below ${limit} lines`); fixes.push(`Reduce ${agentFacts.agent.instructionFile} to under ${limit} lines by moving verbose sections to .goat-flow/ docs.`); hasLineCountFailure = true; } else { findings.push(`${agentFacts.agent.id}: ${lines} lines (within limit ${limit})`); } } if (hasLineCountFailure) return fail(findings, recs, fixes, { lineCounts }); return pass(findings, { lineCounts }); }, }; const executionLoopPresent = { id: "execution-loop-present", name: "Execution loop structural smoke", concern: "context", type: "advisory", evidenceKind: "structural", provenance: contextProvenance("advisory", [ "docs/harness-audit.md", "CLAUDE.md", "AGENTS.md", ".github/copilot-instructions.md", ]), /** Run the Execution loop present check. */ run: (ctx) => { const headingEntry = getRequiredInstructionSections().find((s) => s.label === EXECUTION_LOOP_LABEL); if (!headingEntry) { // Manifest doesn't require Execution Loop - nothing to enforce here. return pass([ `manifest declares no "${EXECUTION_LOOP_LABEL}" section; check skipped`, ]); } const stepWords = ["read", "scope", "act", "verify"]; const findings = []; const recs = []; const executionLoop = []; let hasExecutionLoopFailure = false; for (const agentFacts of ctx.agents) { if (!agentFacts.instruction.exists || !agentFacts.instruction.content) { findings.push(`${agentFacts.agent.id}: no instruction file to check`); executionLoop.push({ agent: agentFacts.agent.id, found: false, sectionLabel: EXECUTION_LOOP_LABEL, missingSteps: stepWords, }); hasExecutionLoopFailure = true; continue; } const content = agentFacts.instruction.content; const executionLoopSection = markdownSectionByLabel(content, EXECUTION_LOOP_LABEL); if (!headingEntry.pattern.test(content) || executionLoopSection === null) { findings.push(`${agentFacts.agent.id}: no "${EXECUTION_LOOP_LABEL}" heading detected`); recs.push(`Add a "${EXECUTION_LOOP_LABEL}" heading with READ → SCOPE → ACT → VERIFY steps to ${agentFacts.agent.instructionFile}`); executionLoop.push({ agent: agentFacts.agent.id, found: false, sectionLabel: EXECUTION_LOOP_LABEL, missingSteps: stepWords, }); hasExecutionLoopFailure = true; continue; } // Heading present - verify the four step words actually appear under it. const lower = executionLoopSection.toLowerCase(); const missingSteps = stepWords.filter((s) => !lower.includes(s)); if (missingSteps.length === 0) { findings.push(`${agentFacts.agent.id}: execution loop has all 4 steps`); executionLoop.push({ agent: agentFacts.agent.id, found: true, sectionLabel: EXECUTION_LOOP_LABEL, missingSteps: [], }); } else { findings.push(`${agentFacts.agent.id}: execution loop heading present but missing step words inside the section (${missingSteps.join(", ")})`); recs.push(`Add READ, SCOPE, ACT, VERIFY steps under the "${EXECUTION_LOOP_LABEL}" heading in ${agentFacts.agent.instructionFile}`); executionLoop.push({ agent: agentFacts.agent.id, found: true, sectionLabel: EXECUTION_LOOP_LABEL, missingSteps, }); hasExecutionLoopFailure = true; } } if (hasExecutionLoopFailure) return fail(findings, recs, [ `Add an "${EXECUTION_LOOP_LABEL}" heading with READ, SCOPE, ACT, VERIFY steps to the instruction file.`, ], { executionLoop }); return pass(findings, { executionLoop }); }, }; /** Local-state paths are intentionally gitignored and may be absent on clean checkouts. */ function isGitignoredLocalStatePath(path) { return (path === ".goat-flow/dashboard-state.json" || path === ".goat-flow/project-id" || path.startsWith(".goat-flow/plans/") || path.startsWith(".goat-flow/scratchpad/") || path.startsWith(".goat-flow/logs/")); } /** * Resolve one backtick path reference from a documentation file. * * Line-number tokens report as unresolved even when the base file exists * because semantic anchors are the durable contract for learning-loop docs. */ function resolveDocPath(ctx, source, path) { const lineRef = /^(?<file>.+\.[a-z0-9]+):\d+$/i.exec(path); if (lineRef?.groups?.file && ctx.fs.exists(lineRef.groups.file)) { return { resolved: false, ref: path, finding: `${source}: line-number path \`${path}\` is brittle; use \`${lineRef.groups.file}\` plus a semantic anchor`, }; } if (isGitignoredLocalStatePath(path) || ctx.fs.exists(path)) { return { resolved: true }; } return { resolved: false, ref: path, finding: `${source}: unresolved \`${path}\``, }; } /** * Count resolved paths from one source file while preserving all diagnostics. * * @param ctx - audit context whose filesystem resolves the target project * @param source - file that contained the backtick path references * @param paths - path references extracted from that source * @returns per-source resolution counts, findings, and unresolved references */ function countResolvedPaths(ctx, source, paths) { const findings = []; const unresolved = []; let resolved = 0; for (const path of paths) { const result = resolveDocPath(ctx, source, path); if (result.resolved) { resolved++; } else { findings.push(result.finding); unresolved.push({ ref: result.ref, source }); } } return { resolved, findings, unresolved }; } /** Mutate the doc-path accumulator with one source file's resolution result. */ function addDocPathResolution(accumulator, pathCount, resolution) { accumulator.totalPaths += pathCount; accumulator.resolvedCount += resolution.resolved; accumulator.findings.push(...resolution.findings); accumulator.unresolved.push(...resolution.unresolved); } /** Mutate the accumulator with router-table paths already collected from agent facts. */ function collectRouterDocPaths(ctx, accumulator) { for (const agentFacts of ctx.agents) { accumulator.totalPaths += agentFacts.router.paths.length; accumulator.resolvedCount += agentFacts.router.resolved; if (agentFacts.router.unresolved.length === 0) continue; accumulator.findings.push(`${agentFacts.agent.id}: ${agentFacts.router.unresolved.length} dead router paths`); for (const ref of agentFacts.router.unresolved) { accumulator.unresolved.push({ ref, source: agentFacts.agent.instructionFile, }); } } } /** Mutate the accumulator with architecture.md path checks and its pass diagnostic. */ function collectArchitectureDocPaths(ctx, accumulator) { if (!ctx.facts.shared.architecture.exists) { accumulator.findings.push("architecture.md does not exist"); accumulator.hasHardFailure = true; return; } const content = ctx.fs.readFile(".goat-flow/architecture.md"); if (!content) return; const paths = extractBacktickPaths(content); const resolution = countResolvedPaths(ctx, ".goat-flow/architecture.md", paths); addDocPathResolution(accumulator, paths.length, resolution); if (resolution.findings.length === 0) { accumulator.findings.push(`All ${paths.length} architecture.md path references resolve`); } } /** Mutate the accumulator with the curated core docs path checks. */ function collectCoreDocPaths(ctx, accumulator) { for (const file of CORE_DOC_FILES) { const content = ctx.fs.readFile(file); if (!content) continue; const paths = extractBacktickPaths(content); const resolution = countResolvedPaths(ctx, file, paths); addDocPathResolution(accumulator, paths.length, resolution); } } /** * Consolidate router, architecture, and core-doc path validation. * * This reads only the audited project's filesystem and reports diagnostics * instead of throwing because context integrity should return every stale path * in one pass. The shape is branch-heavy to preserve the old check boundaries: * router-table paths, architecture presence, architecture refs, and curated * docs all feed one dashboard detail payload. */ function checkAllDocPaths(ctx) { const accumulator = { totalPaths: 0, resolvedCount: 0, findings: [], unresolved: [], hasHardFailure: false, }; collectRouterDocPaths(ctx, accumulator); collectArchitectureDocPaths(ctx, accumulator); collectCoreDocPaths(ctx, accumulator); return accumulator; } const docPathsResolve = { id: "doc-paths-resolve", name: "Documentation paths resolve", concern: "context", type: "integrity", evidenceKind: "structural", provenance: contextProvenance("integrity", [ "docs/harness-audit.md", ".goat-flow/learning-loop/footguns/docs-and-crossrefs.md", ".goat-flow/learning-loop/lessons/verification.md", ], "incident"), /** Run the Documentation paths resolve check. */ run: (ctx) => { const { totalPaths, resolvedCount, findings, unresolved, hasHardFailure } = checkAllDocPaths(ctx); const details = { docPaths: { totalPaths, resolvedCount, unresolved } }; // A missing canonical doc fails outright - resolved-path counts from other // sources must not balance the finding away. if (hasHardFailure) { return fail(findings, [ "Fix missing docs and add backtick-quoted file paths for drift detection", ], undefined, details); } if (totalPaths === 0) { return pass(["No file path references found in docs to validate"], details); } if (resolvedCount === totalPaths) { return pass([`All ${totalPaths} doc file paths resolve`], details); } return fail(findings, ["Update stale paths in docs to match current file locations"], [ "Update or remove dead paths in router table, architecture.md, and doc files.", ], details); }, }; const instructionSectionsPresent = { id: "instruction-sections-present", name: "Instruction sections structural smoke", concern: "context", type: "advisory", evidenceKind: "structural", provenance: contextProvenance("advisory", [ "docs/harness-audit.md", "src/cli/prompt/compose-quality.ts", "CLAUDE.md", "AGENTS.md", ".github/copilot-instructions.md", ]), /** Run the Instruction file required sections check. */ run: (ctx) => { const findings = []; const recs = []; const fixes = []; const sections = []; let hasInstructionSectionFailure = false; const requiredSections = getRequiredInstructionSections(); const required = requiredSections.map((s) => s.label); for (const agentFacts of ctx.agents) { if (!agentFacts.instruction.exists || !agentFacts.instruction.content) { findings.push(`${agentFacts.agent.id}: no instruction file to check`); sections.push({ agent: agentFacts.agent.id, required, present: [], missing: required, }); hasInstructionSectionFailure = true; continue; } const content = agentFacts.instruction.content; const missing = requiredSections .filter(({ pattern }) => !pattern.test(content)) .map(({ label }) => label); const present = required.filter((label) => !missing.includes(label)); sections.push({ agent: agentFacts.agent.id, required, present, missing }); if (missing.length === 0) { findings.push(`${agentFacts.agent.id}: all ${requiredSections.length} required sections present`); } else { findings.push(`${agentFacts.agent.id}: missing sections - ${missing.join(", ")}`); recs.push(`Add the missing hot-path sections to ${agentFacts.agent.instructionFile}: ${missing.join(", ")}`); fixes.push(`Add level-2 (or deeper) headings for ${missing.join(", ")} to ${agentFacts.agent.instructionFile}. Skeleton overlays are not sufficient for hot-path contract.`); hasInstructionSectionFailure = true; } } if (hasInstructionSectionFailure) return fail(findings, recs, fixes, { sections }); return pass(findings, { sections }); }, }; /** Return whether instruction text distinguishes controlling workspace from selected target. */ function boundaryGuidancePresent(content) { if (BOUNDARY_HEADING_PATTERN.test(content)) return true; return (CONTROLLING_WORKSPACE_PATTERNS.some((pattern) => pattern.test(content)) && TARGET_WORKSPACE_PATTERNS.some((pattern) => pattern.test(content))); } const boundaryGuidancePresentCheck = { id: "boundary-guidance-present", name: "Workspace boundary guidance structural smoke", concern: "context", type: "advisory", evidenceKind: "structural", provenance: contextProvenance("advisory", [ "docs/harness-audit.md", ".goat-flow/learning-loop/decisions/ADR-026-keep-workspace-boundary-path-agnostic.md", "CLAUDE.md", "AGENTS.md", ".github/copilot-instructions.md", ]), /** Run the Workspace boundary guidance present check. */ run: (ctx) => { const findings = []; const missing = []; const boundary = []; for (const agentFacts of ctx.agents) { if (!agentFacts.instruction.exists || !agentFacts.instruction.content) { findings.push(`${agentFacts.agent.id}: no instruction file to check`); missing.push(`${agentFacts.agent.id} (${agentFacts.agent.instructionFile})`); boundary.push({ agent: agentFacts.agent.id, controllingWorkspace: false, targetWorkspace: false, boundaryHeading: false, }); continue; } const content = agentFacts.instruction.content; const controllingWorkspace = CONTROLLING_WORKSPACE_PATTERNS.some((p) => p.test(content)); const targetWorkspace = TARGET_WORKSPACE_PATTERNS.some((p) => p.test(content)); const boundaryHeading = BOUNDARY_HEADING_PATTERN.test(content); boundary.push({ agent: agentFacts.agent.id, controllingWorkspace, targetWorkspace, boundaryHeading, }); if (boundaryGuidancePresent(content)) { findings.push(`${agentFacts.agent.id}: instruction file distinguishes controlling workspace from selected target`); } else { findings.push(`${agentFacts.agent.id}: instruction file has no workspace boundary guidance`); missing.push(`${agentFacts.agent.id} (${agentFacts.agent.instructionFile})`); } } if (missing.length === 0) return pass(findings, { boundary }); const missingText = missing.join(", "); return fail(findings, [ `Add path-agnostic workspace boundary guidance to every audited agent instruction file missing it: ${missingText}`, ], [ `Add wording that distinguishes the controlling goat-flow workspace from the selected target project in ${missingText}; do not hardcode machine-specific absolute paths.`, ], { boundary }); }, }; export const CONTEXT_CHECKS = [ instructionLineCount, executionLoopPresent, docPathsResolve, instructionSectionsPresent, boundaryGuidancePresentCheck, ]; //# sourceMappingURL=check-context.js.map