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
60 changes: 55 additions & 5 deletions packages/argent-installer/src/mcp-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,15 @@ function hasEnv(entry: McpServerEntry): entry is McpServerEntry & { env: Record<
return entry.env != null && Object.keys(entry.env).length > 0;
}

function removeDirIfEmpty(dirPath: string): void {
function removeDirIfEmpty(dirPath: string): boolean {
try {
if (!fs.existsSync(dirPath)) return;
if (!fs.statSync(dirPath).isDirectory()) return;
if (fs.readdirSync(dirPath).length > 0) return;
if (!fs.existsSync(dirPath)) return false;
if (!fs.statSync(dirPath).isDirectory()) return false;
if (fs.readdirSync(dirPath).length > 0) return false;
fs.rmdirSync(dirPath);
return true;
} catch {
// non-fatal
return false;
}
}

Expand Down Expand Up @@ -1215,6 +1216,51 @@ export function removeCodexRules(configPath: string): boolean {
return true;
}

// ── Stale rule pruning ───────────────────────────────────────────────────────

// Argent guidance moved from always-on editor rules to the on-demand argent skill.
// Prune legacy argent.md files (and Codex developer_instructions injection) on
// init, update, and uninstall so stale always-on rules do not linger.

/** @internal Exported for tests. */
export const DEPRECATED_ARGENT_RULE_FILES = ["argent.md"] as const;

export function pruneStaleArgentRules(
adapters: McpConfigAdapter[],
root: string,
scope: ManagedContentScope
): string[] {
const results: string[] = [];
const managedTargets = getManagedContentTargets(adapters, root, scope);

for (const target of managedTargets.ruleTargets) {
for (const fileName of DEPRECATED_ARGENT_RULE_FILES) {
const filePath = path.join(target.targetPath, fileName);
try {
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) continue;
fs.rmSync(filePath, { force: true });
const removedRoot = removeDirIfEmpty(target.targetPath);
const rootLabel = removedRoot ? " (removed empty rules directory)" : "";
results.push(`Removed stale ${fileName} from ${target.label}${rootLabel}`);
} catch (err) {
results.push(`Could not remove stale ${fileName} from ${target.label}: ${err}`);
}
}
}

for (const target of managedTargets.codexConfigTargets) {
try {
if (removeCodexRules(target.targetPath)) {
results.push(`Removed stale argent rules from ${target.label}`);
}
} catch (err) {
results.push(`Could not remove stale argent rules from ${target.label}: ${err}`);
}
}

return results;
}

// ── Copy orchestrator ────────────────────────────────────────────────────────
// MARK: Copy orchestrator

Expand All @@ -1228,6 +1274,10 @@ export function copyRulesAndAgents(
const results: string[] = [];
const managedTargets = getManagedContentTargets(adapters, root, scope);

for (const message of pruneStaleArgentRules(adapters, root, scope)) {
results.push(` ${message}`);
}

for (const target of managedTargets.ruleTargets) {
try {
if (fs.existsSync(rulesDir)) {
Expand Down
23 changes: 8 additions & 15 deletions packages/argent-installer/src/uninstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { FAILURE_CODES, type FailureSignal } from "@argent/registry";
import {
ALL_ADAPTERS,
getManagedContentTargets,
removeCodexRules,
pruneStaleArgentRules,
type ManagedContentTarget,
} from "./mcp-configs.js";
import {
Expand Down Expand Up @@ -470,6 +470,13 @@ export async function uninstall(args: string[]): Promise<void> {
},
];

for (const message of [
...pruneStaleArgentRules(ALL_ADAPTERS, projectRoot, "local"),
...pruneStaleArgentRules(ALL_ADAPTERS, projectRoot, "global"),
]) {
pruneResults.push(`${pc.green("+")} ${message}`);
}

for (const { sourceDir, targets, contentLabel } of bundledTargets) {
try {
pruneResults.push(...cleanupBundledTargets(sourceDir, targets, contentLabel));
Expand All @@ -478,20 +485,6 @@ export async function uninstall(args: string[]): Promise<void> {
}
}

// Codex: remove argent rules from developer_instructions in config.toml
for (const { targetPath, label } of [
...localTargets.codexConfigTargets,
...globalTargets.codexConfigTargets,
]) {
try {
if (removeCodexRules(targetPath)) {
pruneResults.push(`${pc.green("+")} Removed argent rules from ${label}`);
}
} catch (err) {
pruneResults.push(`${pc.red("x")} Could not clean ${label}: ${err}`);
}
}

if (pruneResults.length > 0) {
p.note(pruneResults.join("\n"), "Pruned Argent Content");
} else {
Expand Down
83 changes: 83 additions & 0 deletions packages/argent-installer/test/mcp-configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
findConfiguredAdapterScopes,
getManagedContentTargets,
injectCodexRules,
pruneStaleArgentRules,
removeCodexRules,
} from "../src/mcp-configs.js";

Expand Down Expand Up @@ -1338,6 +1339,88 @@ describe("copyRulesAndAgents", () => {
expect(content).toContain("argent rules");
expect(content).toContain("developer_instructions");
});

it("prunes stale argent.md when the bundled rules directory is empty", () => {
const claudeAdapter = ALL_ADAPTERS.find((a) => a.name === "Claude Code")!;
const emptyRulesDir = path.join(tmpDir, "empty-rules");
fs.mkdirSync(emptyRulesDir, { recursive: true });
const rulesTarget = path.join(tmpDir, ".claude", "rules");
fs.mkdirSync(rulesTarget, { recursive: true });
fs.writeFileSync(path.join(rulesTarget, "argent.md"), "# stale rule");
fs.writeFileSync(path.join(rulesTarget, "custom.md"), "# keep me");

const results = copyRulesAndAgents(
[claudeAdapter],
tmpDir,
"local",
emptyRulesDir,
agentsDir
);

expect(results.some((r) => r.includes("Removed stale argent.md"))).toBe(true);
expect(fs.existsSync(path.join(rulesTarget, "argent.md"))).toBe(false);
expect(fs.existsSync(path.join(rulesTarget, "custom.md"))).toBe(true);
});
});

// ── pruneStaleArgentRules ─────────────────────────────────────────────────────

describe("pruneStaleArgentRules", () => {
let agentsDir: string;

beforeEach(() => {
agentsDir = path.join(tmpDir, "src-agents");
fs.mkdirSync(agentsDir, { recursive: true });
});

it("removes stale argent.md from editor rule targets but keeps other rules", () => {
const cursorAdapter = ALL_ADAPTERS.find((a) => a.name === "Cursor")!;
const rulesTarget = path.join(tmpDir, ".cursor", "rules");
fs.mkdirSync(rulesTarget, { recursive: true });
fs.writeFileSync(path.join(rulesTarget, "argent.md"), "# stale");
fs.writeFileSync(path.join(rulesTarget, "team-style.md"), "# keep");

const results = pruneStaleArgentRules([cursorAdapter], tmpDir, "local");

expect(results).toEqual(["Removed stale argent.md from .cursor/rules"]);
expect(fs.existsSync(path.join(rulesTarget, "argent.md"))).toBe(false);
expect(fs.existsSync(path.join(rulesTarget, "team-style.md"))).toBe(true);
});

it("removes an empty rules directory after deleting argent.md", () => {
const claudeAdapter = ALL_ADAPTERS.find((a) => a.name === "Claude Code")!;
const rulesTarget = path.join(tmpDir, ".claude", "rules");
fs.mkdirSync(rulesTarget, { recursive: true });
fs.writeFileSync(path.join(rulesTarget, "argent.md"), "# stale");

const results = pruneStaleArgentRules([claudeAdapter], tmpDir, "local");

expect(results[0]).toContain("removed empty rules directory");
expect(fs.existsSync(rulesTarget)).toBe(false);
});

it("removes injected Codex developer_instructions", () => {
const codexAdapter = ALL_ADAPTERS.find((a) => a.name === "Codex")!;
const rulesDir = path.join(tmpDir, "src-rules");
fs.mkdirSync(rulesDir, { recursive: true });
fs.writeFileSync(path.join(rulesDir, "argent.md"), "Use argent tools.");
const configPath = path.join(tmpDir, ".codex", "config.toml");
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, 'developer_instructions = "Keep me."\n');
injectCodexRules(configPath, rulesDir);

const results = pruneStaleArgentRules([codexAdapter], tmpDir, "local");

expect(results).toEqual(["Removed stale argent rules from .codex/config.toml"]);
const content = fs.readFileSync(configPath, "utf8");
expect(content).toContain("Keep me.");
expect(content).not.toContain("argent rules");
});

it("returns no results when nothing stale is present", () => {
const claudeAdapter = ALL_ADAPTERS.find((a) => a.name === "Claude Code")!;
expect(pruneStaleArgentRules([claudeAdapter], tmpDir, "local")).toEqual([]);
});
});

// ── Codex developer_instructions injection ──────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion packages/argent-mcp/src/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export async function startMcpServer(options: StartMcpServerOptions): Promise<vo
"Argent — iOS Simulator, Android Emulator, and Chromium app control for interacting, testing, profiling and debugging mobile and Chromium applications. " +
"Always use discovery tools (describe / debugger-component-tree / screenshot) before tapping — never guess coordinates. " +
"On session end: call stop-all-simulator-servers and perform any necessary cleanup. " +
"Full guidance is in the argent rule loaded from .claude/rules/argent.md.",
"Load the argent skill for full workflow guidance and sub-skill routing.",
}
);

Expand Down
Loading