@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
JavaScript
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