Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Validate CrossFrame Skills

on:
pull_request:
push:
branches:
- main

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- name: Validate skill structure
run: node scripts/validate-skill.mjs
- name: Test installer behavior
run: node scripts/test-install-adapters.mjs
3 changes: 3 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@ This copies `AGENTS.md` and both skill folders to `.agent-skills/` inside the ta

- Use `--dry-run` to preview writes.
- Existing files and directories are not overwritten unless `--force` is provided.
- For Codex self-installs, the installer copies `crossframe-coder` before `crossframe-code` so the suite is less likely to be left half-installed if the host reloads the active diagnosis skill.
- Identical skill directories are reported as `SKIP ... (identical)` even with `--force`; this avoids touching active skill directories unnecessarily.
- Non-identical skill directories are synchronized before use; stale destination files are pruned by the installer.
- Platform adapters are intentionally thin. They point agents to `skills/crossframe-code/SKILL.md` instead of duplicating the full skill instructions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ Run the static validation script from the repository root:
node scripts/validate-skill.mjs
```

CI should run the same validator and installer smoke test used locally:

```bash
node scripts/validate-skill.mjs
node scripts/test-install-adapters.mjs
```

The validator checks plugin metadata, skill frontmatter, referenced files, architecture lenses, templates, eval coverage, Golden Master rules, local-project risk scan rules, review scope and stack-convention rules, settlement-consistency constraints, and installation isolation.

## Influences and Attribution
Expand Down
62 changes: 61 additions & 1 deletion scripts/install-adapters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,23 @@ if (platforms.size === 0) {
process.exit(1);
}

validateInstallPlan();

for (const platform of platforms) {
install(platform);
}

function validateInstallPlan() {
for (const platform of platforms) {
if (!["codex", "claude", "cursor", "gemini", "generic"].includes(platform)) {
throw new Error(`Unsupported platform: ${platform}`);
}
if (["cursor", "gemini", "generic"].includes(platform) && !target) {
throw new Error(`--target is required for ${platform}`);
}
}
}

function install(platform) {
if (platform === "codex") {
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
Expand Down Expand Up @@ -118,7 +131,54 @@ function copyDir(source, destination) {
}
logWrite("dir", source, destination);
if (dryRun) return;
fs.cpSync(source, destination, { recursive: true, force });
fs.mkdirSync(destination, { recursive: true });
copyDirectoryContents(source, destination);
pruneStaleDestination(source, destination);
if (!directoriesMatch(source, destination)) {
throw new Error(`Directory sync did not produce an exact match: ${destination}`);
}
}

function copyDirectoryContents(source, destination) {
const entries = fs.readdirSync(source, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(source, entry.name);
const destinationPath = path.join(destination, entry.name);
if (entry.isDirectory()) {
fs.mkdirSync(destinationPath, { recursive: true });
copyDirectoryContents(sourcePath, destinationPath);
} else if (entry.isFile()) {
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFileSync(sourcePath, destinationPath);
}
}
}

function pruneStaleDestination(source, destination) {
const sourceFiles = new Set(collectFiles(source));
const destinationFiles = collectFiles(destination);
for (const relativePath of destinationFiles) {
if (!sourceFiles.has(relativePath)) {
const stalePath = path.join(destination, relativePath);
fs.rmSync(stalePath, { force: true });
logSkip("file", path.join(source, relativePath), stalePath, "removed stale destination file");
}
}
pruneEmptyDirs(destination);
}

function pruneEmptyDirs(root, relativeRoot = "") {
const absoluteRoot = path.join(root, relativeRoot);
if (!fs.existsSync(absoluteRoot)) return;
const entries = fs.readdirSync(absoluteRoot, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
pruneEmptyDirs(root, path.join(relativeRoot, entry.name));
}
}
if (relativeRoot && fs.readdirSync(absoluteRoot).length === 0) {
fs.rmdirSync(absoluteRoot);
}
}

function directoriesMatch(source, destination) {
Expand Down
59 changes: 59 additions & 0 deletions scripts/test-install-adapters.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import childProcess from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

function runInstall(home, args = ["--platform", "codex", "--force"]) {
return childProcess.spawnSync(process.execPath, ["scripts/install-adapters.mjs", ...args], {
cwd: repoRoot,
env: {
...process.env,
CODEX_HOME: home,
CLAUDE_HOME: path.join(home, ".claude"),
},
encoding: "utf8",
});
}

function assertSuccess(result) {
assert.equal(result.status, 0, result.stderr || result.stdout);
}

const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "crossframe-install-"));

try {
let result = runInstall(tempHome);
assertSuccess(result);

assert.ok(fs.existsSync(path.join(tempHome, "skills", "crossframe-coder", "SKILL.md")));
assert.ok(fs.existsSync(path.join(tempHome, "skills", "crossframe-code", "SKILL.md")));

const stale = path.join(tempHome, "skills", "crossframe-code", "obsolete-reference.md");
fs.writeFileSync(stale, "old file");

result = runInstall(tempHome);
assertSuccess(result);
assert.equal(fs.existsSync(stale), false, "stale file should be pruned");
assert.match(result.stdout, /removed stale destination file/);

result = runInstall(tempHome);
assertSuccess(result);
assert.match(result.stdout, /SKIP dir: .*crossframe-coder.*identical/);
assert.match(result.stdout, /SKIP dir: .*crossframe-code.*identical/);

const allHome = fs.mkdtempSync(path.join(os.tmpdir(), "crossframe-install-all-"));
try {
result = runInstall(allHome, ["--all", "--force"]);
assert.notEqual(result.status, 0, "--all without --target should fail before writes");
assert.match(result.stderr, /--target is required/);
assert.equal(fs.existsSync(path.join(allHome, "skills")), false, "--all preflight should not install Codex skills first");
} finally {
fs.rmSync(allHome, { recursive: true, force: true });
}
} finally {
fs.rmSync(tempHome, { recursive: true, force: true });
}
59 changes: 56 additions & 3 deletions scripts/validate-skill.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ function check(condition, name, detail = "") {
else fail(name, detail);
}

function occurrenceCount(text, phrase) {
return text.split(phrase).length - 1;
}

let plugin;
try {
plugin = JSON.parse(read(path.join(".codex-plugin", "plugin.json")));
Expand Down Expand Up @@ -268,6 +272,7 @@ check(skillText.includes("templates/debug-observation-output.md"), "SKILL.md ref
check(skillText.includes("templates/source-driven-output.md"), "SKILL.md references source-driven output");
check(skillText.includes("In dual-core use, prefer handing clear implementation requests to `crossframe-coder`"), "SKILL.md prefers coder handoff for clear implementation");
check(skillText.includes("Keep direct implementation in `crossframe-code` only when no coder skill is available"), "SKILL.md keeps crossframe-code implementation as fallback");
check(skillText.includes("evals/dual-core-routing-conflict-tests.md"), "crossframe-code Trial Materials lists dual-core routing conflict eval");
check(skillText.includes("Fixing suspicious legacy output"), "SKILL.md rejects premature suspicious-output fixes");

const problemRouter = exists(path.join("references", "problem-router.md"), skillRoot)
Expand Down Expand Up @@ -738,15 +743,25 @@ const dualCoreConflictEval = exists(path.join("evals", "dual-core-routing-confli
check(Boolean(dualCoreConflictEval), "dual-core routing conflict eval exists");
for (const phrase of [
"Vague Bugfix Without Failure Evidence",
"Approved Deep Risk Plan Implementation",
"Missing Approved Deep Risk Plan",
"Present Approved Deep Risk Plan",
"Review And Fix In One Prompt",
"Security Shortcut Request",
"Clear Local Implementation",
]) {
check(dualCoreConflictEval.includes(phrase), `dual-core routing conflict eval includes ${phrase}`);
}
check(dualCoreConflictEval.includes("review first"), "dual-core routing conflict eval requires review before fix");
check(dualCoreConflictEval.includes("crossframe-coder may implement only named files"), "dual-core routing conflict eval constrains approved plan implementation");
check(dualCoreConflictEval.includes("Review first"), "dual-core routing conflict eval requires review before fix");
check(dualCoreConflictEval.includes("No new files unless the plan names them"), "dual-core routing conflict eval forbids unplanned new files");
check(dualCoreConflictEval.includes("Run exactly the listed verification first"), "dual-core routing conflict eval constrains approved plan verification");
check(
occurrenceCount(dualCoreConflictEval, "crossframe-coder may implement only named files and planned verification.") === 0,
"dual-core routing conflict eval removes duplicate approved-plan line"
);
check(
occurrenceCount(dualCoreConflictEval, "review first, pin review scope, and separate blocking findings from suggestions.") === 0,
"dual-core routing conflict eval removes duplicate review-first line"
);

const ledgerLiteEvalPath = path.join("evals", "ledgerlite-project-risk-scan-smoke-tests.md");
const ledgerLiteEval = exists(ledgerLiteEvalPath, skillRoot) ? read(ledgerLiteEvalPath, skillRoot) : "";
Expand Down Expand Up @@ -775,6 +790,10 @@ check(coderText.includes("Edit code only when the user clearly asks"), "crossfra
check(coderText.includes("hand off to `crossframe-code`"), "crossframe-coder hands off unclear or high-risk work");
check(coderText.includes("Fresh verification is required"), "crossframe-coder requires fresh verification");
check(coderText.includes("verification blocked"), "crossframe-coder uses blocked verification status");
check(coderText.includes("Approved High-Risk Plan Exception"), "crossframe-coder defines approved high-risk plan exception");
check(coderText.includes("no unresolved required confirmations"), "crossframe-coder approved plan exception requires confirmations to be resolved");
check(coderText.includes("first safe slice"), "crossframe-coder approved plan exception limits implementation to first safe slice");
check(coderText.includes("evals/golden-implementation-reports.md"), "crossframe-coder Trial Materials lists golden implementation reports");

const coderReferenced = new Set(
Array.from(coderText.matchAll(/`([^`]+\.(?:md|yaml|json))`/g), (match) => match[1]).filter((relativePath) =>
Expand Down Expand Up @@ -812,6 +831,15 @@ const coderHandoff = exists(path.join("references", "handoff-to-crossframe-code.
: "";
check(coderHandoff.includes("auth") && coderHandoff.includes("tenant") && coderHandoff.includes("billing"), "crossframe-coder handoff covers high-risk boundaries");
check(coderHandoff.includes("Verification cannot be defined"), "crossframe-coder handoff covers missing verification");
check(coderHandoff.includes("unless the Approved High-Risk Plan Exception is satisfied"), "handoff reference allows approved high-risk plan exception");
check(coderHandoff.includes("Approved Plan Exception Check"), "handoff reference includes approved plan exception checklist");

const coderSourceDriven = exists(path.join("references", "source-driven-api-check.md"), coderRoot)
? read(path.join("references", "source-driven-api-check.md"), coderRoot)
: "";
check(coderSourceDriven.includes("Implementation Decision Table"), "crossframe-coder source-driven check includes implementation decision table");
check(coderSourceDriven.includes("Prefer local installed version"), "crossframe-coder source-driven check prefers local installed version");
check(coderSourceDriven.includes("approved plan exception"), "crossframe-coder source-driven check routes high-risk SDK work through approved plan exception");

const coderVerification = exists(path.join("references", "verification-matrix.md"), coderRoot)
? read(path.join("references", "verification-matrix.md"), coderRoot)
Expand Down Expand Up @@ -896,6 +924,7 @@ check(readme.includes("not automatically installed unless the user runs `scripts
check(readme.includes("Platform Adapters"), "README includes platform adapters section");
check(readme.includes("Claude Code") && readme.includes("Cursor") && readme.includes("Gemini CLI"), "README names supported non-Codex platforms");
check(readme.includes("install-adapters.mjs"), "README documents install adapter script");
check(readme.includes("node scripts/test-install-adapters.mjs"), "README documents installer smoke test command");
check(readme.includes("Influences and Attribution"), "README includes influences and attribution section");
check(readme.includes("felipereisdev/code-review-skill"), "README attributes felipereisdev code-review-skill");
check(readme.includes("review scope selection") && readme.includes("stack detection") && readme.includes("project convention-first"), "README names borrowed review-scope stack-convention ideas");
Expand All @@ -905,6 +934,9 @@ const installDoc = exists("INSTALL.md", repoRoot) ? read("INSTALL.md", repoRoot)
check(installDoc.includes("Installing CrossFrame Code"), "INSTALL.md exists and has title");
check(installDoc.includes("skills/crossframe-coder/SKILL.md"), "INSTALL.md documents crossframe-coder entrypoint");
check(installDoc.includes("both skills"), "INSTALL.md says installer copies both skills");
check(installDoc.includes("copies `crossframe-coder` before `crossframe-code`"), "INSTALL.md documents Codex self-install order");
check(installDoc.includes("SKIP") && installDoc.includes("identical"), "INSTALL.md documents identical skip behavior");
check(installDoc.includes("stale destination files"), "INSTALL.md documents stale file pruning");
for (const phrase of ["Codex", "Claude Code", "Cursor", "Gemini CLI", "Generic Agents", "--dry-run", "--force"]) {
check(installDoc.includes(phrase), `INSTALL.md documents ${phrase}`);
}
Expand Down Expand Up @@ -946,6 +978,27 @@ check(installScript.includes("codex") && installScript.includes("claude") && ins
check(installScript.includes("crossframe-code") && installScript.includes("crossframe-coder"), "install script installs both skills");
check(installScript.includes('["crossframe-coder", "crossframe-code"]'), "install script copies coder before code for Codex self-updates");
check(installScript.includes("directoriesMatch") && installScript.includes("SKIP"), "install script skips identical skill directories");
check(installScript.includes("validateInstallPlan"), "install script validates full install plan before writing");
check(installScript.includes("copyDirectoryContents") && installScript.includes("copyFileSync"), "install script copies directories without fs.cpSync");
check(installScript.includes("pruneStaleDestination"), "install script prunes stale destination files");
check(installScript.includes("Directory sync did not produce an exact match"), "install script verifies post-copy exact sync");
check(installScript.includes("removed stale destination file"), "install script logs stale destination pruning");

const installTest = exists(path.join("scripts", "test-install-adapters.mjs"), repoRoot)
? read(path.join("scripts", "test-install-adapters.mjs"), repoRoot)
: "";
check(Boolean(installTest), "install adapter executable smoke test exists");
check(installTest.includes("CODEX_HOME"), "install smoke test uses isolated CODEX_HOME");
check(installTest.includes("obsolete-reference.md"), "install smoke test covers stale file pruning");
check(installTest.includes("SKIP dir"), "install smoke test covers identical skip");
check(installTest.includes("--all") && installTest.includes("--target is required"), "install smoke test covers --all target preflight");

const validateWorkflow = exists(path.join(".github", "workflows", "validate.yml"), repoRoot)
? read(path.join(".github", "workflows", "validate.yml"), repoRoot)
: "";
check(Boolean(validateWorkflow), "GitHub Actions validation workflow exists");
check(validateWorkflow.includes("node scripts/validate-skill.mjs"), "workflow runs validator");
check(validateWorkflow.includes("node scripts/test-install-adapters.mjs"), "workflow runs installer smoke test");

const installedPath = path.join(os.homedir(), ".codex", "skills", "crossframe-code");
info("user .codex skill install status", fs.existsSync(installedPath) ? `installed at ${installedPath}` : `not installed at ${installedPath}`);
Expand Down
1 change: 1 addition & 0 deletions skills/crossframe-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,5 @@ Treat these as blockers:
- `evals/review-scope-and-stack-smoke-tests.md`: smoke prompts for diff scope, stack detection, project convention, and review verdict behavior.
- `evals/local-project-risk-scan-smoke-tests.md`: smoke prompts for risky modules, legacy hotspots, AI patch regression surfaces, and safe refactoring candidates.
- `evals/problem-router-smoke-tests.md`: smoke prompts for request routing across debugging, implementation, review, source-driven, high-risk, and toolchain cases.
- `evals/dual-core-routing-conflict-tests.md`: conflict checks for code/coder routing, approved-plan implementation, review-before-fix, and security shortcut handling.
- `evals/golden-patch-plans.md`: sample passing outputs for local, architecture, and post-implementation modes.
Loading
Loading