From e1d6d853f7730e8c0c3332ef4f9ea76aa6ff876e Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 30 Apr 2026 02:12:24 -0400 Subject: [PATCH] fix: namespace cursor agent installs --- README.md | 12 ++++-- scripts/lib/cursor-agent-names.js | 26 +++++++++++++ scripts/lib/install-executor.js | 10 ++++- scripts/lib/install-targets/cursor-project.js | 12 ++++++ scripts/lib/install-targets/helpers.js | 7 +++- tests/lib/install-executor.test.js | 5 ++- tests/lib/install-targets.test.js | 38 +++++++++++++++++++ tests/scripts/install-apply.test.js | 3 +- tests/scripts/install-readme-clarity.test.js | 15 ++++++++ 9 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 scripts/lib/cursor-agent-names.js diff --git a/README.md b/README.md index 0d4f6df1..4cf3fc97 100644 --- a/README.md +++ b/README.md @@ -1133,7 +1133,7 @@ These are not bundled with ECC and are not audited by this repo, but they are wo ## Cursor IDE Support -ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, commands, and MCP configs adapted for Cursor's native format. +ECC provides Cursor IDE support with hooks, rules, agents, skills, commands, and MCP configs adapted for Cursor's project layout. ### Quick Start (Cursor) @@ -1156,11 +1156,17 @@ ECC provides **full Cursor IDE support** with hooks, rules, agents, skills, comm | Hook Events | 15 | sessionStart, beforeShellExecution, afterFileEdit, beforeMCPExecution, beforeSubmitPrompt, and 10 more | | Hook Scripts | 16 | Thin Node.js scripts delegating to `scripts/hooks/` via shared adapter | | Rules | 34 | 9 common (alwaysApply) + 25 language-specific (TypeScript, Python, Go, Swift, PHP) | -| Agents | Shared | Via AGENTS.md at root (read by Cursor natively) | -| Skills | Shared + Bundled | Via AGENTS.md at root and `.cursor/skills/` for translated additions | +| Agents | 48 | `.cursor/agents/ecc-*.md` when installed; prefixed to avoid collisions with user or marketplace agents | +| Skills | Shared + Bundled | `.cursor/skills/` for translated additions | | Commands | Shared | `.cursor/commands/` if installed | | MCP Config | Shared | `.cursor/mcp.json` if installed | +### Cursor Loading Notes + +ECC does not install root `AGENTS.md` into `.cursor/`. Cursor treats nested `AGENTS.md` files as directory context, so copying ECC's repo identity into a host project would pollute that project. + +Cursor-native loading behavior can vary by Cursor build. ECC installs agents as `.cursor/agents/ecc-*.md`; if your Cursor build does not expose project agents, those files still work as explicit reference definitions instead of hidden global prompt context. + ### Hook Architecture (DRY Adapter Pattern) Cursor has **more hook events than Claude Code** (20 vs 8). The `.cursor/hooks/adapter.js` module transforms Cursor's stdin JSON to Claude Code's format, allowing existing `scripts/hooks/*.js` to be reused without duplication. diff --git a/scripts/lib/cursor-agent-names.js b/scripts/lib/cursor-agent-names.js new file mode 100644 index 00000000..945771e0 --- /dev/null +++ b/scripts/lib/cursor-agent-names.js @@ -0,0 +1,26 @@ +'use strict'; + +const path = require('path'); + +function toCursorAgentFileName(fileName) { + if (!fileName || fileName.startsWith('ecc-')) { + return fileName; + } + + return `ecc-${fileName}`; +} + +function toCursorAgentRelativePath(relativePath) { + const segments = String(relativePath || '').split(/[\\/]+/).filter(Boolean); + if (segments.length === 0) { + return relativePath; + } + + const fileName = segments.pop(); + return path.join(...segments, toCursorAgentFileName(fileName)); +} + +module.exports = { + toCursorAgentFileName, + toCursorAgentRelativePath, +}; diff --git a/scripts/lib/install-executor.js b/scripts/lib/install-executor.js index d6676c56..cee65085 100644 --- a/scripts/lib/install-executor.js +++ b/scripts/lib/install-executor.js @@ -3,6 +3,7 @@ const os = require('os'); const path = require('path'); const { execFileSync } = require('child_process'); +const { toCursorAgentRelativePath } = require('./cursor-agent-names'); const { LEGACY_INSTALL_TARGETS, parseInstallArgs } = require('./install/request'); const { SUPPORTED_INSTALL_TARGETS, @@ -154,7 +155,13 @@ function addRecursiveCopyOperations(operations, options) { for (const relativeFile of relativeFiles) { const sourceRelativePath = path.join(options.sourceRelativeDir, relativeFile); const sourcePath = path.join(options.sourceRoot, sourceRelativePath); - const destinationPath = path.join(options.destinationDir, relativeFile); + const destinationRelativePath = typeof options.destinationRelativePathTransform === 'function' + ? options.destinationRelativePathTransform(relativeFile, sourceRelativePath) + : relativeFile; + if (!destinationRelativePath) { + continue; + } + const destinationPath = path.join(options.destinationDir, destinationRelativePath); operations.push(buildCopyFileOperation({ moduleId: options.moduleId, sourcePath, @@ -351,6 +358,7 @@ function planCursorLegacyInstall(context) { sourceRoot: context.sourceRoot, sourceRelativeDir: path.join('.cursor', 'agents'), destinationDir: path.join(targetRoot, 'agents'), + destinationRelativePathTransform: toCursorAgentRelativePath, }); addRecursiveCopyOperations(operations, { moduleId: 'legacy-cursor-install', diff --git a/scripts/lib/install-targets/cursor-project.js b/scripts/lib/install-targets/cursor-project.js index 617bbcc0..81e71aef 100644 --- a/scripts/lib/install-targets/cursor-project.js +++ b/scripts/lib/install-targets/cursor-project.js @@ -1,7 +1,9 @@ const fs = require('fs'); const path = require('path'); +const { toCursorAgentFileName } = require('../cursor-agent-names'); const { + createFlatFileOperations, createFlatRuleOperations, createInstallTargetAdapter, createManagedOperation, @@ -149,6 +151,16 @@ module.exports = createInstallTargetAdapter({ })); } + if (sourceRelativePath === 'agents') { + return takeUniqueOperations(createFlatFileOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'agents'), + destinationNameTransform: toCursorAgentFileName, + })); + } + if (sourceRelativePath === '.cursor') { const cursorRoot = path.join(repoRoot, '.cursor'); if (!fs.existsSync(cursorRoot) || !fs.statSync(cursorRoot).isDirectory()) { diff --git a/scripts/lib/install-targets/helpers.js b/scripts/lib/install-targets/helpers.js index a39506e1..bb9a0648 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -181,7 +181,7 @@ function createNamespacedFlatRuleOperations(adapter, moduleId, sourceRelativePat return operations; } -function createFlatRuleOperations({ +function createFlatFileOperations({ moduleId, repoRoot, sourceRelativePath, @@ -242,6 +242,10 @@ function createFlatRuleOperations({ return operations; } +function createFlatRuleOperations(options) { + return createFlatFileOperations(options); +} + function createInstallTargetAdapter(config) { const adapter = { id: config.id, @@ -342,6 +346,7 @@ function createInstallTargetAdapter(config) { module.exports = { buildValidationIssue, + createFlatFileOperations, createFlatRuleOperations, createInstallTargetAdapter, createManagedOperation, diff --git a/tests/lib/install-executor.test.js b/tests/lib/install-executor.test.js index d5c97baf..3bc9e698 100644 --- a/tests/lib/install-executor.test.js +++ b/tests/lib/install-executor.test.js @@ -213,7 +213,10 @@ function runTests() { assert.strictEqual(plan.installRoot, targetRoot); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'common-style.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'rules', 'typescript-style.md'))); - assert.ok(operationFor(plan, path.join('.cursor', 'agents', 'planner.md'))); + assert.ok(operationFor(plan, path.join('.cursor', 'agents', 'ecc-planner.md'))); + assert.ok(!plan.operations.some(operation => ( + operation.destinationPath.endsWith(path.join('.cursor', 'agents', 'planner.md')) + ))); assert.ok(operationFor(plan, path.join('.cursor', 'skills', 'demo', 'SKILL.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'commands', 'plan.md'))); assert.ok(operationFor(plan, path.join('.cursor', 'hooks', 'hook.js'))); diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index 98c021dc..60476e33 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -202,6 +202,44 @@ function runTests() { ); })) passed++; else failed++; + if (test('plans cursor agents with ecc-prefixed filenames to avoid agent collisions', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const projectRoot = '/workspace/app'; + + const plan = planInstallTargetScaffold({ + target: 'cursor', + repoRoot, + projectRoot, + modules: [ + { + id: 'agents-core', + paths: ['agents'], + }, + ], + }); + + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'agents/architect.md' + && operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'ecc-architect.md') + )), + 'Should prefix Cursor agent files with ecc-' + ); + assert.ok( + !plan.operations.some(operation => ( + operation.destinationPath === path.join(projectRoot, '.cursor', 'agents', 'architect.md') + )), + 'Should not write bare Cursor agent filenames' + ); + assert.ok( + !plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'agents' + && operation.destinationPath === path.join(projectRoot, '.cursor', 'agents') + )), + 'Should not plan a whole-directory Cursor agent copy' + ); + })) passed++; else failed++; + if (test('plans cursor platform rule files as .mdc and excludes rule README docs', () => { const repoRoot = path.join(__dirname, '..', '..'); const projectRoot = '/workspace/app'; diff --git a/tests/scripts/install-apply.test.js b/tests/scripts/install-apply.test.js index 63fde637..8fcf0c70 100644 --- a/tests/scripts/install-apply.test.js +++ b/tests/scripts/install-apply.test.js @@ -136,7 +136,8 @@ function runTests() { assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.mdc'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'common-agents.md'))); assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'rules', 'README.mdc'))); - assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md'))); + assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'ecc-architect.md'))); + assert.ok(!fs.existsSync(path.join(projectDir, '.cursor', 'agents', 'architect.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'commands', 'plan.md'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'hooks.json'))); assert.ok(fs.existsSync(path.join(projectDir, '.cursor', 'mcp.json'))); diff --git a/tests/scripts/install-readme-clarity.test.js b/tests/scripts/install-readme-clarity.test.js index a1946ef0..7affd11f 100644 --- a/tests/scripts/install-readme-clarity.test.js +++ b/tests/scripts/install-readme-clarity.test.js @@ -93,6 +93,21 @@ function runTests() { ); })) passed++; else failed++; + if (test('README documents Cursor agent namespace and loading caveat', () => { + assert.ok( + readme.includes('`.cursor/agents/ecc-*.md`'), + 'README should document the Cursor agent namespace' + ); + assert.ok( + readme.includes('Cursor-native loading behavior can vary by Cursor build.'), + 'README should avoid overclaiming Cursor agent loading semantics' + ); + assert.ok( + readme.includes('ECC does not install root `AGENTS.md` into `.cursor/`.'), + 'README should explain why root AGENTS.md is not copied into Cursor context' + ); + })) passed++; else failed++; + if (test('README explains plugin-path cleanup and rules scoping', () => { assert.ok( readme.includes('remove the plugin from Claude Code'),