Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 75 additions & 322 deletions README.md

Large diffs are not rendered by default.

84 changes: 38 additions & 46 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";

const root = new URL("../", import.meta.url);

function read(relativePath) {
return readFileSync(new URL(relativePath, root), "utf8");
}
const read = (relativePath) => readFileSync(new URL(relativePath, root), "utf8");

function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
if (!condition) throw new Error(message);
}

const skillNames = ["finn-build", "finn-review", "finn-spec"];
const skillDirectory = new URL("skills/", root);
const actualSkillNames = readdirSync(skillDirectory, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
const skillNames = ["hermes-build", "hermes-loop", "hermes-review", "hermes-spec"];
const actualSkillNames = readdirSync(new URL("skills/", root), { withFileTypes: true })
.filter(
(entry) =>
entry.isDirectory() &&
existsSync(new URL(`skills/${entry.name}/SKILL.md`, root)),
)
.map((entry) => entry.name)
.sort();

Expand All @@ -24,28 +22,17 @@ assert(
`Expected only ${skillNames.join(", ")}; found ${actualSkillNames.join(", ")}`,
);

const texts = {};
for (const skillName of skillNames) {
const relativePath = `skills/${skillName}/SKILL.md`;
const text = read(relativePath);
texts[skillName] = text;
const frontmatter = text.match(/^---\n([\s\S]*?)\n---\n/);

assert(frontmatter, `${relativePath} is missing YAML frontmatter`);

const fields = frontmatter[1]
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const name = fields.find((line) => line.startsWith("name: "))?.slice(6);
const description = fields
.find((line) => line.startsWith("description: "))
?.slice(13);

const fields = frontmatter[1].split("\n").map((line) => line.trim()).filter(Boolean);
assert(fields.length === 2, `${relativePath} must contain only name and description frontmatter`);
assert(name === skillName, `${relativePath} name must be ${skillName}`);
assert(description, `${relativePath} needs a description`);

const installed = text.replace(/\bTEAM\b/g, "ENG");
assert(!/\bTEAM\b/.test(installed), `${relativePath} left a TEAM placeholder after installation`);
assert(fields.includes(`name: ${skillName}`), `${relativePath} name must be ${skillName}`);
assert(fields.some((line) => line.startsWith("description: ")), `${relativePath} needs a description`);
}

const readme = read("README.md");
Expand All @@ -56,26 +43,31 @@ for (const match of readme.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
}
}

const build = read("skills/finn-build/SKILL.md");
const review = read("skills/finn-review/SKILL.md");
const requiredContracts = [
[build.includes("not labeled `blocked`"), "builder must exclude blocked issues"],
[build.includes("remove `loop-changes-requested`"), "builder escalation must leave the repair queue"],
[build.includes("defaultBranchRef"), "builder must detect the default branch"],
[build.includes("git status --porcelain"), "builder must protect dirty worktrees"],
[review.includes("gh pr checks NUMBER --required"), "reviewer must inspect required checks"],
[review.includes("Finn-loop review of COMMIT_SHA"), "reviewer must record the reviewed SHA"],
[readme.includes("/reload-skills"), "installer must reload newly copied skills"],
[readme.includes("linear.app/docs/github-integration"), "README must explain Linear's GitHub integration"],
const contracts = [
[texts["hermes-spec"].includes('multica issue create --title "<title>" --project "<project-id>" --description-file "<path>" --status backlog --output json'), "spec must document a complete project-bound create command"],
[texts["hermes-build"].includes("git status --porcelain"), "builder must protect dirty worktrees"],
[texts["hermes-build"].includes("Closes PRO-NNN"), "builder must link PRs to Multica"],
[texts["hermes-build"].includes("Hermes Repair round N/2"), "builder must record durable repair markers"],
[texts["hermes-review"].includes("Hermes Review of COMMIT_SHA"), "review must bind verdicts to a commit"],
[texts["hermes-review"].includes("gh pr checks NUMBER --required"), "review must inspect required checks"],
[texts["hermes-loop"].includes("fresh reviewer profile or isolated delegated context"), "loop must require independent review"],
[texts["hermes-loop"].includes("If no independent review context is available, require human review"), "loop must not self-review as fallback"],
[texts["hermes-loop"].includes("count of unique completed round numbers, never session memory"), "loop must recover repair count from durable state"],
[texts["hermes-loop"].includes("rounds 1 and 2 already exist"), "loop must stop before a third repair"],
[texts["hermes-loop"].includes("hermes-stuck") && texts["hermes-loop"].includes("needs-human-review"), "loop must label exhausted repair rounds"],
[texts["hermes-loop"].includes("remove `hermes-changes-requested` and `hermes-approved`"), "stuck escalation must leave automated queues"],
[texts["hermes-loop"].includes("Telegram merge-ready alert with the PR URL and reviewed SHA"), "loop must send actionable merge-ready alerts"],
[texts["hermes-loop"].includes("/goal outcome:"), "loop must use Hermes standing goals"],
[texts["hermes-loop"].includes("hermes send --to telegram"), "loop must alert through Telegram"],
[/humans merge/i.test(readme), "README must preserve human-only merge"],
[[texts["hermes-build"], texts["hermes-review"], texts["hermes-loop"]].every((text) => text.includes("Never merge")), "every agent-side skill must prohibit merge"],
];

for (const [condition, message] of requiredContracts) {
assert(condition, message);
}
for (const [condition, message] of contracts) assert(condition, message);

assert(!readme.includes("skills/spec/SKILL.md"), "README references the old spec path");
assert(!readme.includes("skills/build/SKILL.md"), "README references the old build path");
assert(!readme.includes("skills/review/SKILL.md"), "README references the old review path");
assert(!build.includes("origin/main"), "builder hardcodes origin/main");
const workflowText = [readme, ...Object.values(texts)].join("\n");
for (const stale of ["Linear", "Slack", "Claude Code", "agent-ready", "loop-approved"]) {
assert(!workflowText.includes(stale), `Stale workflow term remains: ${stale}`);
}

console.log(`Validated ${skillNames.length} skills, README links, and ${requiredContracts.length} safety contracts.`);
console.log(`Validated ${skillNames.length} Hermes skills, README links, and ${contracts.length} safety contracts.`);
127 changes: 0 additions & 127 deletions skills/finn-build/SKILL.md

This file was deleted.

118 changes: 0 additions & 118 deletions skills/finn-review/SKILL.md

This file was deleted.

Loading