@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.
97 lines • 4.49 kB
JavaScript
/**
* Raw manifest-JSON layer: reads `workflow/manifest.json`, validates its
* optional skill-docs shape, and resolves the required-instruction-section
* list that harness checks compare agent instruction files against.
*
* Split out of `manifest.ts` to break an import cycle. `manifest.ts` imports
* `HARNESS_CHECKS` to count them as a derived fact, while harness check
* `check-context` needs the section list - so importing the section helper from
* `manifest.ts` formed check-context -> manifest -> harness/index -> check-context.
* This module imports neither the harness checks nor `manifest.ts`, so it is a
* true leaf both can depend on. `required_sections` is raw JSON passthrough
* (not a drift-validated fact), so reading it here returns the same value
* without re-entering the cycle. (search: "design.circular-import")
*/
import { readFileSync } from "node:fs";
import { getTemplatePath } from "../paths.js";
import { ManifestValidationError } from "./types.js";
/** Validate optional `skills.references` shape before any consumer reads it. */
function validateOneSkillReference(canonical, skillName, files) {
const findings = [];
if (!canonical.has(skillName)) {
findings.push(`skills.references.${skillName} must reference a canonical skill name.`);
}
if (!Array.isArray(files)) {
findings.push(`skills.references.${skillName} must be a string array.`);
return findings;
}
if (files.some((file) => typeof file !== "string")) {
findings.push(`skills.references.${skillName} must contain only strings.`);
}
return findings;
}
/**
* Validate optional skill-docs metadata before consumers read it.
*
* Throws `ManifestValidationError` on malformed references because stale or
* misspelled reference lists change what the installer copies.
*
* @param json - Parsed manifest JSON to validate.
*/
export function validateSkillReferenceSchema(json) {
const references = json.skills.references;
if (references === undefined)
return;
if (typeof references !== "object" ||
references === null ||
Array.isArray(references)) {
throw new ManifestValidationError("workflow/manifest.json has an invalid `skills.references` value.", ["skills.references must be an object keyed by canonical skill name."]);
}
const findings = [];
const canonical = new Set(json.skills.canonical);
for (const [skillName, files] of Object.entries(references)) {
findings.push(...validateOneSkillReference(canonical, skillName, files));
}
if (findings.length > 0) {
throw new ManifestValidationError(`workflow/manifest.json has invalid skill reference metadata (${findings.length} finding${findings.length === 1 ? "" : "s"}).`, findings);
}
}
/**
* Read and skill-docs-validate the on-disk `workflow/manifest.json`.
*
* @returns the parsed manifest JSON - throws on a missing or malformed file, or
* when `skills.references` is structurally invalid (`ManifestValidationError`).
*/
export function readManifestJson() {
const path = getTemplatePath("workflow/manifest.json");
const raw = readFileSync(path, "utf-8");
const json = JSON.parse(raw);
validateSkillReferenceSchema(json);
return json;
}
/** Regex for a markdown heading whose text equals `label` (case-insensitive).
* Used by harness checks to find required instruction-file sections. */
function instructionSectionRegex(label) {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return new RegExp(`^#+\\s+${escaped}`, "im");
}
/**
* Resolved (label, pattern) pairs built from the manifest's required_sections.
* Harness checks import this instead of hand-rolling their own section list.
*
* Reads the raw manifest JSON rather than the validated/cached `loadManifest`
* result: `required_sections` is a straight passthrough field, so the value is
* identical, and reading it here keeps this module free of the harness-check
* import that would re-form the cycle described in the file header.
*
* @returns One entry per required section - its manifest label and the
* case-insensitive heading regex used to detect it in instruction files.
*/
export function getRequiredInstructionSections() {
const sections = readManifestJson().instruction_file.required_sections;
return sections.map((label) => ({
label,
pattern: instructionSectionRegex(label),
}));
}
//# sourceMappingURL=manifest-json.js.map