From 27d58e589a4ebac8482da9a08855239eeff0f2d8 Mon Sep 17 00:00:00 2001 From: serhattayaz-ai <322430760+serhattayaz-ai@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:10:35 +0300 Subject: [PATCH] fix(install): strip ecc: plugin namespace from manual Claude installs The ecc@ecc plugin namespaces agents, commands and skills as `ecc:`. A manual install (install-apply --target claude|claude-project) places the same files at bare names under ~/.claude, so plugin-prefixed references in copied markdown point at agents and commands that do not exist in that layout (e.g. subagent_type "ecc:planner", frontmatter agent: ecc:...). - Add plugin-namespace.js with stripPluginNamespace(), which removes the `ecc:` prefix before an identifier and leaves `ecc@ecc`, URLs and words ending in "ecc" alone. - Apply it to markdown copy operations when the target is claude or claude-project. Non-markdown files and other targets are unchanged. - Reword the agents rule so it describes both install modes instead of stating that agents only ship with the plugin. - Cover the transform with tests/lib/install-plugin-namespace.test.js. --- rules/common/agents.md | 4 +- scripts/lib/install/apply.js | 12 ++- scripts/lib/install/plugin-namespace.js | 19 +++++ tests/lib/install-plugin-namespace.test.js | 86 ++++++++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 scripts/lib/install/plugin-namespace.js create mode 100644 tests/lib/install-plugin-namespace.test.js diff --git a/rules/common/agents.md b/rules/common/agents.md index 14d9b9005..98b53e79f 100644 --- a/rules/common/agents.md +++ b/rules/common/agents.md @@ -2,8 +2,8 @@ ## Available Agents -ECC agents ship with the `ecc@ecc` plugin, not in `~/.claude/agents/`. -They are invoked through the Agent tool with a plugin-scoped `subagent_type`: +ECC agents come either from the `ecc@ecc` plugin (plugin-scoped names, `ecc:`) or from a manual install into `~/.claude/agents/` (bare names, ``). Use the name that matches the install you have; the manual installer strips the `ecc:` prefix from copied files. +They are invoked through the Agent tool with that `subagent_type`: ```text Agent(subagent_type: "ecc:planner", prompt: "...") diff --git a/scripts/lib/install/apply.js b/scripts/lib/install/apply.js index 209e53784..2f378fc7b 100644 --- a/scripts/lib/install/apply.js +++ b/scripts/lib/install/apply.js @@ -51,6 +51,11 @@ const { prepareExcludedPathsReconciliation, } = require('./excluded-paths-reconciliation'); const { buildInstallIndex, rewriteRelativeLinks } = require('./link-rewrite'); +const { stripPluginNamespace } = require('./plugin-namespace'); + +// Manual installs place agents, commands and skills at bare names; the +// ecc@ecc plugin namespaces them as `ecc:`. +const MANUAL_CLAUDE_TARGETS = new Set(['claude', 'claude-project']); const { adaptAntigravityAgent } = require('./antigravity-agent'); function isMarkdownPath(filePath) { @@ -874,13 +879,16 @@ function applyInstallPlanLocked(plan, dependencies = {}, settingsLockHeld = fals && operation.sourceRelativePath && isMarkdownPath(operation.destinationPath) ); - if (operation.kind === 'copy-file' && (operation.contentTransform || needsLinkRewrite)) { + const stripNamespace = MANUAL_CLAUDE_TARGETS.has(plan.adapter?.target) + && isMarkdownPath(operation.destinationPath); + if (operation.kind === 'copy-file' && (operation.contentTransform || needsLinkRewrite || stripNamespace)) { const transformed = transformInstallContent( operation, fs.readFileSync(operation.sourcePath, 'utf8') ); + const namespaced = stripNamespace ? stripPluginNamespace(transformed) : transformed; const installedContent = needsLinkRewrite - ? rewriteRelativeLinks(transformed, { + ? rewriteRelativeLinks(namespaced, { sourceRel: operation.sourceRelativePath, index: linkIndex, }) diff --git a/scripts/lib/install/plugin-namespace.js b/scripts/lib/install/plugin-namespace.js new file mode 100644 index 000000000..8a9022d6d --- /dev/null +++ b/scripts/lib/install/plugin-namespace.js @@ -0,0 +1,19 @@ +'use strict'; + +// The ecc@ecc plugin exposes agents, commands and skills under an `ecc:` +// namespace (`ecc:planner`, `/ecc:plan`). A manual install +// (install-apply --target claude|claude-project) places the same files at bare +// names, so plugin-prefixed references in copied markdown must drop the prefix +// or they point at agents and commands that do not exist in that layout. +// +// The leading guard keeps `ecc@ecc` and words that merely end in "ecc" (e.g. +// "decc:") untouched; the lookahead requires a real identifier after the colon. +const PLUGIN_NAMESPACE_PREFIX = /(^|[^A-Za-z0-9_@.-])ecc:(?=[a-z][a-z0-9-]*)/gm; + +function stripPluginNamespace(content) { + return String(content).replace(PLUGIN_NAMESPACE_PREFIX, '$1'); +} + +module.exports = { + stripPluginNamespace, +}; diff --git a/tests/lib/install-plugin-namespace.test.js b/tests/lib/install-plugin-namespace.test.js new file mode 100644 index 000000000..7a0d2d15e --- /dev/null +++ b/tests/lib/install-plugin-namespace.test.js @@ -0,0 +1,86 @@ +/** + * Tests for scripts/lib/install/plugin-namespace.js — manual Claude installs + * place agents, commands and skills at bare names, so plugin-prefixed + * `ecc:` references in copied markdown must lose the prefix. + */ + +'use strict'; + +const assert = require('assert'); + +const { stripPluginNamespace } = require('../../scripts/lib/install/plugin-namespace'); + +function test(name, fn) { + try { + fn(); + console.log(` PASS ${name}`); + return true; + } catch (error) { + console.log(` FAIL ${name}`); + console.log(` Error: ${error.message}`); + console.error(error.stack || error); + return false; + } +} + +function runTests() { + console.log('Running install plugin-namespace tests...\n'); + let passed = 0; + let failed = 0; + const record = ok => (ok ? passed++ : failed++); + + record(test('strips the prefix from agent names in code spans, bold and tables', () => { + const input = [ + '| ecc:planner | Planning |', + '1. Use **ecc:code-reviewer** agent', + 'Use `ecc:tdd-guide` here', + ].join('\n'); + assert.strictEqual(stripPluginNamespace(input), [ + '| planner | Planning |', + '1. Use **code-reviewer** agent', + 'Use `tdd-guide` here', + ].join('\n')); + })); + + record(test('strips the prefix from agent frontmatter and tool-call arguments', () => { + assert.strictEqual( + stripPluginNamespace('agent: ecc:security-reviewer'), + 'agent: security-reviewer' + ); + assert.strictEqual( + stripPluginNamespace('Agent(subagent_type: "ecc:planner", prompt: "...")'), + 'Agent(subagent_type: "planner", prompt: "...")' + ); + })); + + record(test('strips the prefix from slash commands', () => { + assert.strictEqual(stripPluginNamespace('see `/ecc:ecc-guide`'), 'see `/ecc-guide`'); + assert.strictEqual(stripPluginNamespace('/ecc:plan "Add auth"'), '/plan "Add auth"'); + })); + + record(test('leaves the plugin identifier ecc@ecc untouched', () => { + const input = '`/plugin install ecc@ecc` and `ecc@ecc`'; + assert.strictEqual(stripPluginNamespace(input), input); + })); + + record(test('leaves words that only end in "ecc" untouched', () => { + const input = 'the decc:thing and tecc:value'; + assert.strictEqual(stripPluginNamespace(input), input); + })); + + record(test('does not touch a colon with no identifier after it', () => { + const input = 'the `ecc:` prefix in plugin mode'; + assert.strictEqual(stripPluginNamespace(input), input); + })); + + record(test('returns content with no prefix unchanged', () => { + const input = '# Plain markdown\n\nNo namespace here.\n'; + assert.strictEqual(stripPluginNamespace(input), input); + })); + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + return failed === 0; +} + +const ok = runTests(); +process.exit(ok ? 0 : 1);