feat(skill): rename skill to reverse-engineer-anything

This commit is contained in:
morluto 2026-07-17 00:35:19 +00:00
parent 34252f55e9
commit 9356634ccb
15 changed files with 199 additions and 56 deletions

View File

@ -28,7 +28,7 @@
<li><span><span class="tsd-kind-parameter">providerEnvironment</span>: <a href="../types/application_Setup.SetupProviderEnvironment.html" class="tsd-signature-type tsd-kind-type-alias">SetupProviderEnvironment</a></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-keyword">readonly</span> <a href="../interfaces/application_Setup.SetupAction.html" class="tsd-signature-type tsd-kind-interface">SetupAction</a><span class="tsd-signature-symbol">[]</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L8">src/application/SetupPlan.ts:8</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L9">src/application/SetupPlan.ts:9</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -24,7 +24,7 @@
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">boolean</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L26">src/application/SetupSkill.ts:26</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L34">src/application/SetupSkill.ts:34</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -24,7 +24,7 @@
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol">&lt;</span><span class="tsd-signature-type">&quot;failed&quot;</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">&quot;unchanged&quot;</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">&quot;installed&quot;</span><span class="tsd-signature-symbol">&gt;</span></h4><aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L38">src/application/SetupSkill.ts:38</a></li></ul></aside></div></li></ul></section></div>
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L50">src/application/SetupSkill.ts:50</a></li></ul></aside></div></li></ul></section></div>
<div class="col-sidebar">
<div class="page-menu">
<div class="tsd-navigation settings">

View File

@ -8,7 +8,7 @@
"client": "2.0.0-beta.4",
"core": "2.0.0-beta.4"
},
"skill_version": "20"
"skill_version": "21"
},
"tools": {
"total": 95,

View File

@ -18,12 +18,12 @@ const lock = JSON.parse(
await readFile(join(root, "package-lock.json"), "utf8"),
);
const skill = await readFile(
join(root, "skills/rea-analysis/SKILL.md"),
join(root, "skills/reverse-engineer-anything/SKILL.md"),
"utf8",
);
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
if (skillVersion === undefined)
throw new Error("Missing rea-analysis skill metadata version");
throw new Error("Missing reverse-engineer-anything skill metadata version");
const versionOf = (name) => {
const value = lock.packages?.[`node_modules/${name}`]?.version;
if (typeof value !== "string")

View File

@ -10,7 +10,7 @@ for (const argument of arguments_)
throw new Error(`Unknown skill metadata option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const path = join(root, "skills/rea-analysis/SKILL.md");
const path = join(root, "skills/reverse-engineer-anything/SKILL.md");
const current = await readFile(path, "utf8");
const { CATALOG_IDENTITY } = await import(
`${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}`

View File

@ -756,14 +756,18 @@ try {
!mcpRegistration.includes("add-mcp npx -y rea-agents@latest mcp --name rea")
)
throw new Error("Incur mcp add did not register the latest npx command");
const skillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
const skillPath = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const legacySkillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
const siblingSkillPath = join(home, ".agents/skills/unrelated/SKILL.md");
if (supportedSetupHost) {
await mkdir(join(home, ".agents/skills/rea-analysis"), {
recursive: true,
});
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
await writeFile(skillPath, "stale managed skill\n");
await writeFile(legacySkillPath, "stale managed skill\n");
await writeFile(siblingSkillPath, "unrelated skill\n");
}
const plannedExecution = await runWithStatus(
@ -776,7 +780,7 @@ try {
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
const plannedCodexConfig = await readFile(codexTarget, "utf8");
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
const plannedSkill = await readFile(skillPath, "utf8");
const plannedSkill = await readFile(legacySkillPath, "utf8");
if (
planned.status !== "needs_confirmation" ||
plannedExecution.status !== 1 ||
@ -856,17 +860,18 @@ try {
throw new Error("packaged setup did not preserve config symlinks");
const skill = await readFile(skillPath, "utf8");
const canonicalSkill = await readFile(
join(root, "skills/rea-analysis/SKILL.md"),
join(root, "skills/reverse-engineer-anything/SKILL.md"),
"utf8",
);
if (skill !== canonicalSkill)
throw new Error("packaged skill did not match its canonical source");
if (
(await readFile(`${skillPath}.rea.backup`, "utf8")) !==
(await pathExists(legacySkillPath)) ||
(await readFile(`${legacySkillPath}.rea.backup`, "utf8")) !==
"stale managed skill\n" ||
(await readFile(siblingSkillPath, "utf8")) !== "unrelated skill\n"
)
throw new Error("packaged stale-skill upgrade was not isolated");
throw new Error("packaged skill-name migration was not isolated");
const alignedDoctorExecution = await runWithStatus(
cli,
["doctor", "--json"],

View File

@ -1,8 +1,8 @@
---
name: rea-analysis
name: reverse-engineer-anything
description: Reverse engineer native, Electron/JavaScript, and web applications with REA. Connect static artifacts to passive runtime evidence, explain how features work, and build a version tailored to the user's project.
metadata:
version: "20"
version: "21"
tool_count: 95
catalog_digest: "b3db06bedadc1ad21a6cfe76f58a9c6cebbf9bce4f9663733b6bab07a57203c3"
---
@ -101,9 +101,9 @@ Notes is only a documentation example. Never select an app unless the user names
## Ensure REA is ready
1. Run `npx -y rea-agents doctor`.
1. Run `npx -y rea-agents@latest doctor`.
2. If setup is needed, tell the user REA needs to install its local binary-analysis tools. Do not lead with implementation details or assume the user knows reverse-engineering products.
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents setup --yes`.
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents@latest setup --yes`.
4. If macOS or an installer requests human input, tell the user exactly what needs attention. After they finish, rerun setup and doctor.
5. If setup registers a new MCP server, tell the user to restart their agent to load all REA tools. Direct CLI commands remain available before restart.

View File

@ -3,6 +3,7 @@ import type {
SetupClient,
SetupProviderEnvironment,
} from "./Setup.js";
import { PRODUCT_IDENTITY } from "../identity.js";
/** Build the complete setup mutation plan before approval. */
export const setupPlan = (
@ -42,8 +43,9 @@ export const setupPlan = (
? [
{
kind: "install_skill" as const,
target: "~/.agents/skills/rea-analysis/SKILL.md",
detail: "Install or update the bundled REA analysis skill.",
target: `~/.agents/skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
detail:
"Install or update the bundled REA reverse-engineering skill.",
external: false,
},
]

View File

@ -1,18 +1,26 @@
import { mkdir, readFile, rm } from "node:fs/promises";
import { lstat, mkdir, readFile, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import writeFileAtomic from "write-file-atomic";
import { PRODUCT_IDENTITY } from "../identity.js";
const skillDestination = (home: string, name: string): string =>
join(home, ".agents/skills", name, "SKILL.md");
const readOptionalText = async (path: string): Promise<string | undefined> => {
try {
return await readFile(path, "utf8");
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
return undefined;
throw cause;
}
};
const canonicalSkillFiles = async (
home: string,
): Promise<{ readonly content: string; readonly destination: string }> => ({
destination: join(
home,
".agents/skills",
PRODUCT_IDENTITY.skillName,
"SKILL.md",
),
destination: skillDestination(home, PRODUCT_IDENTITY.skillName),
content: await readFile(
new URL(
`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
@ -28,7 +36,11 @@ export const canonicalSkillNeedsInstall = async (
): Promise<boolean> => {
try {
const { content, destination } = await canonicalSkillFiles(home);
return (await readFile(destination, "utf8")) !== content;
if ((await readOptionalText(destination)) !== content) return true;
for (const name of PRODUCT_IDENTITY.legacySkillNames)
if ((await readOptionalText(skillDestination(home, name))) !== undefined)
return true;
return false;
} catch {
return true;
}
@ -38,45 +50,80 @@ export const canonicalSkillNeedsInstall = async (
export const installCanonicalSkill = async (
home: string,
): Promise<"installed" | "unchanged" | "failed"> => {
let destination = join(
home,
".agents/skills",
PRODUCT_IDENTITY.skillName,
"SKILL.md",
);
let destination = skillDestination(home, PRODUCT_IDENTITY.skillName);
let backup = `${destination}.rea.backup`;
let original: string | undefined;
let canonicalChanged = false;
const removedLegacy: Array<{
readonly content: string;
readonly destination: string;
}> = [];
try {
const canonical = await canonicalSkillFiles(home);
destination = canonical.destination;
backup = `${destination}.rea.backup`;
const { content } = canonical;
original = await readFile(destination, "utf8").catch(() => undefined);
if (original === content) return "unchanged";
original = await readOptionalText(destination);
const legacy: Array<{
readonly content: string;
readonly destination: string;
}> = [];
for (const name of PRODUCT_IDENTITY.legacySkillNames) {
const legacyDestination = skillDestination(home, name);
const legacyContent = await readOptionalText(legacyDestination);
if (legacyContent !== undefined) {
if ((await lstat(dirname(legacyDestination))).isSymbolicLink())
throw new Error("legacy skill directory is a symbolic link");
legacy.push({ content: legacyContent, destination: legacyDestination });
}
}
if (original === content && legacy.length === 0) return "unchanged";
await mkdir(dirname(destination), { recursive: true });
if (original !== undefined)
if (original !== undefined && original !== content)
await writeFileAtomic(backup, original, {
encoding: "utf8",
mode: 0o600,
});
await writeFileAtomic(destination, content, {
encoding: "utf8",
mode: 0o600,
});
canonicalChanged = original !== content;
if (canonicalChanged)
await writeFileAtomic(destination, content, {
encoding: "utf8",
mode: 0o600,
});
if ((await readFile(destination, "utf8")) !== content)
throw new Error("skill readback mismatch");
for (const legacySkill of legacy) {
await writeFileAtomic(
`${legacySkill.destination}.rea.backup`,
legacySkill.content,
{ encoding: "utf8", mode: 0o600 },
);
await rm(legacySkill.destination, { force: true });
removedLegacy.push(legacySkill);
}
return "installed";
} catch {
try {
if (original === undefined) await rm(destination, { force: true });
else
if (canonicalChanged && original === undefined)
await rm(destination, { force: true });
else if (canonicalChanged && original !== undefined)
await writeFileAtomic(destination, original, {
encoding: "utf8",
mode: 0o600,
});
} catch {
// The backup remains beside the skill for explicit operator recovery.
// The canonical backup remains beside the skill for operator recovery.
}
for (const legacySkill of removedLegacy)
try {
await mkdir(dirname(legacySkill.destination), { recursive: true });
await writeFileAtomic(legacySkill.destination, legacySkill.content, {
encoding: "utf8",
mode: 0o600,
});
} catch {
// The legacy backup remains beside the skill for operator recovery.
}
return "failed";
}
};

View File

@ -87,12 +87,7 @@ export const systemUninstallHost = (
): UninstallHost => ({
clients: () => Promise.resolve(supportedClients(home)),
removeClient: (client) => removeClient(client, fileSystem),
removeSkill: () =>
removeManagedPath(
join(home, ".agents/skills", PRODUCT_IDENTITY.skillName),
"skill",
fileSystem,
),
removeSkill: () => removeManagedSkills(home, fileSystem),
purgeData: async () => [
await removeManagedPath(join(home, ".rea/cache"), "cache", fileSystem),
await removeManagedPath(join(home, ".rea/state"), "state", fileSystem),
@ -262,6 +257,36 @@ const removeManagedPath = async (
}
};
const removeManagedSkills = async (
home: string,
fileSystem: UninstallFileSystem,
): Promise<UninstallItem> => {
const results: UninstallItem[] = [];
for (const name of [
PRODUCT_IDENTITY.skillName,
...PRODUCT_IDENTITY.legacySkillNames,
])
results.push(
await removeManagedPath(
join(home, ".agents/skills", name),
"skill",
fileSystem,
),
);
const failed = results.find(({ status }) => status === "failed");
if (failed !== undefined) return failed;
const retained = results.find(({ status }) => status === "retained");
if (retained !== undefined) return retained;
const removed = results.filter(({ status }) => status === "removed");
if (removed.length > 0)
return item(
"skill",
"removed",
removed.map(({ detail }) => detail).join(" "),
);
return item("skill", "skipped", "No managed REA skill installation exists.");
};
const item = (
name: string,
status: UninstallItem["status"],

View File

@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
serverSdkVersion: "2.0.0-beta.4",
clientSdkVersion: "2.0.0-beta.4",
coreSdkVersion: "2.0.0-beta.4",
skillVersion: "20",
skillVersion: "21",
} as const;

View File

@ -11,7 +11,8 @@ export const PRODUCT_IDENTITY = {
cliBinary: "rea",
mcpCommand: `npx -y ${PACKAGE_METADATA.name}@latest mcp`,
mcpServerKey: "rea",
skillName: "rea-analysis",
skillName: "reverse-engineer-anything",
legacySkillNames: ["rea-analysis"],
skillVersion: PACKAGE_METADATA.skillVersion,
configFileName: "rea.json",
} as const;

View File

@ -146,9 +146,14 @@ describe("agent lifecycle", () => {
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-"));
roots.push(home);
const cursor = join(home, ".cursor/mcp.json");
const skill = join(home, ".agents/skills/rea-analysis/SKILL.md");
const skill = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const legacySkill = join(home, ".agents/skills/rea-analysis/SKILL.md");
await mkdir(dirname(cursor), { recursive: true });
await mkdir(dirname(skill), { recursive: true });
await mkdir(dirname(legacySkill), { recursive: true });
await writeFile(
cursor,
JSON.stringify({
@ -159,6 +164,7 @@ describe("agent lifecycle", () => {
}),
);
await writeFile(skill, "managed");
await writeFile(legacySkill, "legacy managed");
await mkdir(join(home, ".rea"), { recursive: true });
await import("node:fs/promises").then(({ symlink }) =>
symlink(home, join(home, ".rea/cache")),
@ -168,6 +174,8 @@ describe("agent lifecycle", () => {
expect(JSON.parse(await readFile(cursor, "utf8"))).toEqual({
mcpServers: { other: { command: "other" } },
});
await expect(readFile(skill, "utf8")).rejects.toThrow();
await expect(readFile(legacySkill, "utf8")).rejects.toThrow();
expect(first.items).toContainEqual(
expect.objectContaining({
name: "cache",
@ -300,7 +308,7 @@ describe("agent lifecycle", () => {
it("reports a managed-path removal failure", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-remove-"));
roots.push(home);
const skillRoot = join(home, ".agents/skills/rea-analysis");
const skillRoot = join(home, ".agents/skills/reverse-engineer-anything");
await mkdir(skillRoot, { recursive: true });
const result = await runUninstall(
false,

View File

@ -1,4 +1,11 @@
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import {
mkdtemp,
mkdir,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
@ -19,7 +26,10 @@ describe("canonical skill transaction", () => {
it("backs up and upgrades a stale managed skill without touching siblings", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const destination = join(home, ".agents/skills/rea-analysis/SKILL.md");
const destination = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
const sibling = join(home, ".agents/skills/unrelated/SKILL.md");
await mkdir(dirname(destination), { recursive: true });
await mkdir(dirname(sibling), { recursive: true });
@ -31,7 +41,7 @@ describe("canonical skill transaction", () => {
expect(await readFile(`${destination}.rea.backup`, "utf8")).toBe(
"stale managed skill\n",
);
expect(await readFile(destination, "utf8")).toContain('version: "20"');
expect(await readFile(destination, "utf8")).toContain('version: "21"');
expect(await readFile(destination, "utf8")).toContain(
`tool_count: ${String(TOOL_CONTRACTS.length)}`,
);
@ -39,4 +49,49 @@ describe("canonical skill transaction", () => {
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
expect(await installCanonicalSkill(home)).toBe("unchanged");
});
it("backs up and retires the legacy skill name", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const legacy = join(home, ".agents/skills/rea-analysis/SKILL.md");
const destination = join(
home,
".agents/skills/reverse-engineer-anything/SKILL.md",
);
await mkdir(dirname(legacy), { recursive: true });
await writeFile(legacy, "legacy managed skill\n");
expect(await canonicalSkillNeedsInstall(home)).toBe(true);
expect(await installCanonicalSkill(home)).toBe("installed");
expect(await readFile(destination, "utf8")).toContain(
"name: reverse-engineer-anything",
);
expect(await readFile(`${legacy}.rea.backup`, "utf8")).toBe(
"legacy managed skill\n",
);
await expect(readFile(legacy, "utf8")).rejects.toThrow();
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
});
it("does not migrate a legacy skill through a directory symlink", async () => {
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
roots.push(home);
const skillsRoot = join(home, ".agents/skills");
const external = join(home, "external-skill");
const legacyRoot = join(skillsRoot, "rea-analysis");
const legacy = join(legacyRoot, "SKILL.md");
const destination = join(skillsRoot, "reverse-engineer-anything/SKILL.md");
await mkdir(skillsRoot, { recursive: true });
await mkdir(external, { recursive: true });
await writeFile(join(external, "SKILL.md"), "external skill\n");
await symlink(
external,
legacyRoot,
process.platform === "win32" ? "junction" : "dir",
);
expect(await installCanonicalSkill(home)).toBe("failed");
expect(await readFile(legacy, "utf8")).toBe("external skill\n");
await expect(readFile(destination, "utf8")).rejects.toThrow();
});
});