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
3 changes: 2 additions & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"defaultPrompt": [
"Use $crossframe-code to diagnose risky code and produce a minimal replacement plan.",
"Use $crossframe-code to review this diff for structural failure modes before implementation.",
"Use $crossframe-coder to implement a clear code change in small verified slices."
"Use $crossframe-coder to implement a clear code change in small verified slices.",
"For high-risk implementation, require the Approved High-Risk Plan Exception payload: exact files, behavior to preserve, non-goals, verification, first safe slice, resolved confirmations, environment marker, and decision_trace."
],
"brandColor": "#4F46E5"
}
Expand Down
2 changes: 2 additions & 0 deletions .cursor/rules/crossframe-code.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ Operational rules:
- Do not implement without a verification command or manual check.
- Use `crossframe-coder` only when the user explicitly asks to implement, build, add, fix, generate, modify, patch, or directly change code.
- Hand off from `crossframe-coder` to `crossframe-code` when mechanism, behavior preservation, auth, tenant, money, migration, concurrency, durable state, webhook, outbox, or security risk is unclear.
- Approved high-risk implementation may stay in `crossframe-coder` only when the approved-plan payload names exact files, behavior to preserve, non-goals, verification, first safe slice, resolved confirmations, high-risk categories, environment marker, and `decision_trace`; otherwise hand off and log missing fields.
- Decision trace shape: `decision_trace: environment=<agent/os>; risk=<categories>; schema=pass|fail; route=implement|handoff; files=<allowed>; verification=<command>; first_safe_slice=<slice>; reason=<reason>`.
10 changes: 9 additions & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ on:

jobs:
validate:
runs-on: ubuntu-latest
name: validate (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ When working on this repo:
- Do not weaken the default no-edit contract, evidence-anchor requirements, or verification requirements.
- Keep platform adapters concise: they should point agents to `SKILL.md` and name only the triggering rules each platform needs.
- Keep the split clear: `crossframe-code` thinks through risk and plans; `crossframe-coder` implements clear approved changes in verified slices.
- Approved high-risk implementation requires the approved-plan payload fields from `skills/crossframe-coder/references/approved-plan-payload-schema.md`; otherwise hand off to `crossframe-code` and log missing fields.
- Platform adapters must preserve the `decision_trace` line for high-risk handoff or implementation decisions.
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Trigger this workflow when the user asks for `crossframe-code`, code diagnosis,

Use `crossframe-coder` when the user explicitly asks to implement, build, add, fix, generate, modify, patch, or directly change code. Hand off back to `crossframe-code` when mechanism, behavior preservation, auth, tenant, money, migration, concurrency, durable state, webhook, outbox, or security risk is unclear.

Approved high-risk implementation may stay in `crossframe-coder` only when the approved-plan payload names exact files, behavior to preserve, non-goals, verification, first safe slice, resolved confirmations, high-risk categories, environment marker, and `decision_trace`. Otherwise hand off to `crossframe-code` and log the missing payload fields.

Core behavior:

- Default output is read-only diagnosis or a patch plan; do not edit files unless the user explicitly asks for implementation.
Expand All @@ -16,3 +18,4 @@ Core behavior:
- Every P0/P1 must include an evidence anchor with file, line/function/symbol, observed behavior, and why this is risky.
- Do not rank files high risk by line count alone.
- Do not implement without a verification path.
- Handoff/implement decisions for approved high-risk plans must include `decision_trace: environment=<agent/os>; risk=<categories>; schema=pass|fail; route=implement|handoff; files=<allowed>; verification=<command>; first_safe_slice=<slice>; reason=<reason>`.
4 changes: 4 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ Non-negotiable rules:
- Every implementation plan must include verification.

Use `crossframe-coder` when the user explicitly asks to implement, build, add, fix, generate, modify, patch, or directly change code. Hand off to `crossframe-code` when mechanism, behavior preservation, auth, tenant, money, migration, concurrency, durable state, webhook, outbox, or security risk is unclear.

Approved high-risk implementation may stay in `crossframe-coder` only when the approved-plan payload names exact files, behavior to preserve, non-goals, verification, first safe slice, resolved confirmations, high-risk categories, environment marker, and `decision_trace`. Otherwise hand off to `crossframe-code` and log the missing payload fields.

Decision trace shape: `decision_trace: environment=<agent/os>; risk=<categories>; schema=pass|fail; route=implement|handoff; files=<allowed>; verification=<command>; first_safe_slice=<slice>; reason=<reason>`.
14 changes: 13 additions & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,17 @@ This copies `AGENTS.md` and both skill folders to `.agent-skills/` inside the ta
- 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.
- Non-identical skill directories are synchronized before use; stale destination files are pruned by the installer and exact post-sync verification reports missing, stale, or changed files if sync fails.
- File copy and stale-file removal retry transient Windows-style file locks (`EBUSY` / `EPERM`) before failing.
- Platform adapters are intentionally thin. They point agents to `skills/crossframe-code/SKILL.md` instead of duplicating the full skill instructions.

## Smoke Test

Run from the repository root:

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

The installer smoke test uses temporary Codex and Claude homes plus a temporary target project for Cursor, Gemini, and generic adapters. It verifies identical-directory skip, stale-file pruning, exact skill sync, `--all` preflight failure before writes, and multi-platform installs on the current OS. CI runs the same checks on Linux, Windows, and macOS.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Use `crossframe-code` when the user asks "what is risky?", "can this PR merge?",

Use `crossframe-coder` when the user explicitly asks to implement, build, add, fix, generate, modify, patch, or directly change code and the change is clear enough to verify.

Approved high-risk implementation is a narrow exception. It requires an approved-plan payload with exact files, behavior to preserve, non-goals, verification, first safe slice, resolved confirmations, high-risk categories, environment marker, and `decision_trace`; otherwise `crossframe-coder` hands the work back to `crossframe-code`.

## What It Does

- Diagnoses the exact code object under review: function, component, module, interface, test, runtime path, or architecture boundary.
Expand Down Expand Up @@ -157,12 +159,14 @@ skills/
crossframe-coder/
SKILL.md
agents/
schemas/
references/
templates/
examples/
evals/
scripts/
install-adapters.mjs
test-install-adapters.mjs
validate-skill.mjs
```

Expand Down Expand Up @@ -203,6 +207,16 @@ 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.

Quick smoke run before publishing:

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

The installer smoke test uses temporary Codex/Claude homes and a temporary target project to verify exact skill sync, stale-file pruning, identical-directory skip, `--all` preflight behavior, and multi-platform adapter installs.

## Influences and Attribution

This repository studies adjacent public skill repositories and adapts compatible ideas into the CrossFrame Code workflow. Borrowed ideas are rewritten as local instructions and validator-backed structures rather than copied wholesale.
Expand Down
95 changes: 75 additions & 20 deletions scripts/install-adapters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
// Copy the companion implementation skill before the active diagnosis skill so
// Codex self-updates cannot leave the suite half-installed if the host reloads.
const skillNames = ["crossframe-coder", "crossframe-code"];
const fileLockRetryCodes = new Set(["EBUSY", "EPERM"]);
const fileLockRetryLimit = 3;

const args = process.argv.slice(2);
const platforms = new Set();
Expand Down Expand Up @@ -120,7 +122,7 @@ function copyFile(source, destination) {
logWrite("file", source, destination);
if (dryRun) return;
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(source, destination);
withFileLockRetry(() => fs.copyFileSync(source, destination), `copy file ${destination}`);
}

function copyDir(source, destination) {
Expand All @@ -134,8 +136,9 @@ function copyDir(source, destination) {
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}`);
const comparison = compareDirectories(source, destination);
if (!comparison.matches) {
throw new Error(`Directory sync did not produce an exact match: ${destination}. ${formatDirectoryMismatch(comparison)}`);
}
}

Expand All @@ -149,7 +152,7 @@ function copyDirectoryContents(source, destination) {
copyDirectoryContents(sourcePath, destinationPath);
} else if (entry.isFile()) {
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
fs.copyFileSync(sourcePath, destinationPath);
withFileLockRetry(() => fs.copyFileSync(sourcePath, destinationPath), `copy file ${destinationPath}`);
}
}
}
Expand All @@ -160,7 +163,7 @@ function pruneStaleDestination(source, destination) {
for (const relativePath of destinationFiles) {
if (!sourceFiles.has(relativePath)) {
const stalePath = path.join(destination, relativePath);
fs.rmSync(stalePath, { force: true });
withFileLockRetry(() => fs.rmSync(stalePath, { force: true }), `remove stale file ${stalePath}`);
logSkip("file", path.join(source, relativePath), stalePath, "removed stale destination file");
}
}
Expand All @@ -177,31 +180,62 @@ function pruneEmptyDirs(root, relativeRoot = "") {
}
}
if (relativeRoot && fs.readdirSync(absoluteRoot).length === 0) {
fs.rmdirSync(absoluteRoot);
withFileLockRetry(() => fs.rmdirSync(absoluteRoot), `remove empty directory ${absoluteRoot}`);
}
}

function directoriesMatch(source, destination) {
return compareDirectories(source, destination).matches;
}

function compareDirectories(source, destination) {
try {
const sourceFiles = collectFiles(source);
const destinationFiles = collectFiles(destination);
if (sourceFiles.length !== destinationFiles.length) return false;
for (let index = 0; index < sourceFiles.length; index += 1) {
const relativePath = sourceFiles[index];
if (relativePath !== destinationFiles[index]) return false;
const sourceFile = path.join(source, relativePath);
const destinationFile = path.join(destination, relativePath);
const sourceStat = fs.statSync(sourceFile);
const destinationStat = fs.statSync(destinationFile);
if (sourceStat.size !== destinationStat.size) return false;
if (!fs.readFileSync(sourceFile).equals(fs.readFileSync(destinationFile))) return false;
const destinationFiles = fs.existsSync(destination) ? collectFiles(destination) : [];
const sourceSet = new Set(sourceFiles);
const destinationSet = new Set(destinationFiles);
const missingFiles = sourceFiles.filter((relativePath) => !destinationSet.has(relativePath));
const staleFiles = destinationFiles.filter((relativePath) => !sourceSet.has(relativePath));
const changedFiles = [];
for (const relativePath of sourceFiles) {
if (destinationSet.has(relativePath) && !filesEqual(path.join(source, relativePath), path.join(destination, relativePath))) {
changedFiles.push(relativePath);
}
}
return true;
} catch {
return false;
return {
matches: missingFiles.length === 0 && staleFiles.length === 0 && changedFiles.length === 0,
missingFiles,
staleFiles,
changedFiles,
error: "",
};
} catch (error) {
return {
matches: false,
missingFiles: [],
staleFiles: [],
changedFiles: [],
error: error.message,
};
}
}

function filesEqual(sourceFile, destinationFile) {
const sourceStat = fs.statSync(sourceFile);
const destinationStat = fs.statSync(destinationFile);
if (sourceStat.size !== destinationStat.size) return false;
return fs.readFileSync(sourceFile).equals(fs.readFileSync(destinationFile));
}

function formatDirectoryMismatch(comparison) {
const parts = [];
if (comparison.missingFiles.length > 0) parts.push(`missing=${comparison.missingFiles.join(",")}`);
if (comparison.staleFiles.length > 0) parts.push(`stale=${comparison.staleFiles.join(",")}`);
if (comparison.changedFiles.length > 0) parts.push(`changed=${comparison.changedFiles.join(",")}`);
if (comparison.error) parts.push(`error=${comparison.error}`);
return parts.length > 0 ? parts.join("; ") : "unknown mismatch";
}

function collectFiles(root, relativeRoot = "") {
const entries = fs.readdirSync(path.join(root, relativeRoot), { withFileTypes: true });
const files = [];
Expand Down Expand Up @@ -239,6 +273,27 @@ function logSkip(kind, source, destination, reason) {
console.log(`SKIP ${kind}: ${source} -> ${destination} (${reason})`);
}

function withFileLockRetry(operation, label) {
let lastError;
for (let attempt = 1; attempt <= fileLockRetryLimit; attempt += 1) {
try {
return operation();
} catch (error) {
lastError = error;
if (!fileLockRetryCodes.has(error.code) || attempt === fileLockRetryLimit) {
throw error;
}
console.warn(`RETRY file lock: ${label} (${error.code}, attempt ${attempt + 1}/${fileLockRetryLimit})`);
sleep(50 * attempt);
}
}
throw lastError;
}

function sleep(milliseconds) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
}

function printHelp() {
console.log(`Usage:
node scripts/install-adapters.mjs --platform codex
Expand Down
62 changes: 62 additions & 0 deletions scripts/test-install-adapters.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ function assertSuccess(result) {
assert.equal(result.status, 0, result.stderr || result.stdout);
}

function collectFiles(root, relativeRoot = "") {
const entries = fs.readdirSync(path.join(root, relativeRoot), { withFileTypes: true });
const files = [];
for (const entry of entries) {
const relativePath = path.join(relativeRoot, entry.name);
if (entry.isDirectory()) {
files.push(...collectFiles(root, relativePath));
} else if (entry.isFile()) {
files.push(relativePath);
}
}
return files.sort();
}

function assertDirectoryExact(source, destination) {
const sourceFiles = collectFiles(source);
const destinationFiles = collectFiles(destination);
assert.deepEqual(destinationFiles, sourceFiles, `${destination} should have exact file list`);
for (const relativePath of sourceFiles) {
const sourceFile = path.join(source, relativePath);
const destinationFile = path.join(destination, relativePath);
assert.equal(fs.statSync(destinationFile).size, fs.statSync(sourceFile).size, `${relativePath} size should match`);
assert.equal(fs.readFileSync(destinationFile).equals(fs.readFileSync(sourceFile)), true, `${relativePath} content should match`);
}
}

function assertSkillInstallExact(destinationRoot) {
for (const skillName of ["crossframe-coder", "crossframe-code"]) {
assertDirectoryExact(path.join(repoRoot, "skills", skillName), path.join(destinationRoot, skillName));
}
}

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

try {
Expand All @@ -31,6 +63,7 @@ try {

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

const stale = path.join(tempHome, "skills", "crossframe-code", "obsolete-reference.md");
fs.writeFileSync(stale, "old file");
Expand All @@ -39,6 +72,7 @@ try {
assertSuccess(result);
assert.equal(fs.existsSync(stale), false, "stale file should be pruned");
assert.match(result.stdout, /removed stale destination file/);
assertSkillInstallExact(path.join(tempHome, "skills"));

result = runInstall(tempHome);
assertSuccess(result);
Expand All @@ -54,6 +88,34 @@ try {
} finally {
fs.rmSync(allHome, { recursive: true, force: true });
}

const platformHome = fs.mkdtempSync(path.join(os.tmpdir(), "crossframe-install-platform-home-"));
const platformTarget = fs.mkdtempSync(path.join(os.tmpdir(), "crossframe-install-platform-target-"));
try {
result = runInstall(platformHome, ["--all", "--target", platformTarget, "--force"]);
assertSuccess(result);

assertSkillInstallExact(path.join(platformHome, "skills"));
assertSkillInstallExact(path.join(platformHome, ".claude", "skills"));
assert.ok(fs.existsSync(path.join(platformHome, ".claude", "CLAUDE.md")));

assertSkillInstallExact(path.join(platformTarget, ".cursor", "skills"));
assert.ok(fs.existsSync(path.join(platformTarget, ".cursor", "rules", "crossframe-code.mdc")));

assertSkillInstallExact(path.join(platformTarget, ".gemini", "skills"));
assert.ok(fs.existsSync(path.join(platformTarget, "GEMINI.md")));

assertSkillInstallExact(path.join(platformTarget, ".agent-skills"));
assert.ok(fs.existsSync(path.join(platformTarget, "AGENTS.md")));

result = runInstall(platformHome, ["--all", "--target", platformTarget, "--force"]);
assertSuccess(result);
assert.match(result.stdout, /SKIP dir: .*crossframe-coder.*identical/);
assert.match(result.stdout, /SKIP dir: .*crossframe-code.*identical/);
} finally {
fs.rmSync(platformHome, { recursive: true, force: true });
fs.rmSync(platformTarget, { recursive: true, force: true });
}
} finally {
fs.rmSync(tempHome, { recursive: true, force: true });
}
Loading
Loading