feat: add native Qoder plugin support

This commit is contained in:
calm
2026-10-10 20:11:00 +08:00
parent 4eb71d92a3
commit 2d9e6977dc
11 changed files with 278 additions and 5 deletions
+40
View File
@@ -0,0 +1,40 @@
# ECC for Qoder
This directory contains ECC's native Qoder plugin manifest. The repository root
is the plugin bundle: Qoder loads the canonical `skills/`, `agents/`, and
`commands/` directories rather than copies maintained under `.qoder-plugin/`.
## Install and verify
ECC's hook runtime requires Node.js 18 or newer on Qoder's non-interactive
`PATH`. From a reviewed checkout:
```bash
qoder plugins validate . --strict
qoder plugins install .
qoder plugins list
```
For Qoder IDE, create a ZIP whose root contains `.qoder-plugin/`, `skills/`,
`agents/`, `commands/`, `hooks/`, `scripts/`, and `.mcp.json`, then use
**Upload Plugin**. Do not upload only `.qoder-plugin/` or an individual
`SKILL.md`; both would omit referenced resources.
## Capability boundary
- Qoder discovers all compatible skills and agents from their canonical root
directories. `commands/` remains a legacy compatibility surface; workflows
should prefer their corresponding skills.
- The bundled `chrome-devtools` MCP server uses the same pinned `npx` launch
contract as Codex. It is downloaded on first use and therefore requires Node,
npm/npx, and network access.
- Qoder receives only ECC's reviewed `SessionStart` bootstrap. The much broader
Claude hook profile is not copied: blocking tool hooks, asynchronous stop
jobs, and Claude-specific matchers have not been asserted to be Qoder-safe.
- The Qoder hook uses `command` plus `args`, `QODER_PLUGIN_ROOT`, and Node's own
executable for child processes. It does not rely on Bash, `python3`, or shell
variable expansion, so the launcher contract is portable across native
Windows, macOS, and Linux when Node 18+ is available.
Qoder plugin installation is separate from ECC's managed installer. Do not
combine it with a manually copied Qoder skill installation.
+27
View File
@@ -0,0 +1,27 @@
{
"name": "ecc",
"displayName": "ECC",
"version": "2.2.3",
"description": "Harness-native engineering workflows for Qoder: 302 skills, 71 agents, legacy command shims, a reviewed SessionStart hook, and optional MCP tooling.",
"author": {
"name": "Affaan Mustafa",
"url": "https://x.com/affaanmustafa"
},
"homepage": "https://ecc.tools",
"repository": "https://github.com/affaan-m/ECC",
"license": "MIT",
"keywords": [
"qoder",
"agents",
"skills",
"tdd",
"code-review",
"security",
"workflow",
"automation"
],
"skills": "./skills/",
"commands": "./commands/",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/qoder-hooks.json"
}
+27 -1
View File
@@ -138,7 +138,7 @@ Instead of rebuilding that process in every prompt, you install it once and make
> Optimize the context window. Persist everything else.
ECC is MIT-licensed open source. It works best with Claude Code today, has a supported Codex sync path, and provides capability-limited adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. See the [support status matrix](#platform-support) before assuming feature parity.
ECC is MIT-licensed open source. It works best with Claude Code today, has native Codex and Qoder plugin paths, and provides capability-limited adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. See the [support status matrix](#platform-support) before assuming feature parity.
Access to 71 agents, 302 skills, and 95 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work.
@@ -383,6 +383,31 @@ You can also open the ECC repository directly in Codex for a project-local setup
For repo navigation, surface ownership, and PR diff packet guidance, read the [Codex ECC Navigation Map](docs/CODEX-NAVIGATION-GUIDE.md). See the [.codex plugin notes](.codex-plugin/README.md) for native lifecycle details.
### Qoder CLI and IDE
ECC ships a native Qoder plugin that reuses the canonical 302 skills, 71 agents,
95 legacy command shims, and the pinned `chrome-devtools` MCP definition. From a
reviewed checkout, validate and install it with Qoder CLI:
```bash
qoder plugins validate . --strict
qoder plugins install .
qoder plugins list
```
For Qoder IDE, upload a ZIP of the repository root. The ZIP root must contain
`.qoder-plugin/` together with `skills/`, `agents/`, `commands/`, `hooks/`,
`scripts/`, and `.mcp.json`; uploading only the manifest directory or one
`SKILL.md` omits required resources.
The universal Node.js 18+ requirement applies to Qoder's non-interactive hook
environment. The MCP server also requires `npx` and network access on first use.
Qoder receives one reviewed, cross-platform `SessionStart` bootstrap using
`QODER_PLUGIN_ROOT` and exec-form arguments. ECC does not copy the full Claude
hook profile into Qoder: blocking tool hooks, async stop jobs, and
Claude-specific matchers remain outside this initial native integration. See
the [.qoder-plugin notes](.qoder-plugin/README.md) for the exact boundary.
### Other agents and editors
<details>
@@ -1292,6 +1317,7 @@ Treat `stable`, `beta`, `experimental`, and `instruction-only` below as capabili
|---|---|---|---|
| Claude Code | Stable primary | Plugin or selective installer | The plugin advertises the installed catalog to the model; use a selective/manual profile when context footprint matters. Optional shell-backed skills are not portable to every OS. |
| Codex | Supported native plugin | Codex marketplace plugin or repo config | Native hooks require an explicit trust decision and do not use Claude's hook profiles. The legacy sync is compatibility-only. |
| Qoder | Supported native plugin | Local plugin directory in Qoder CLI or ZIP upload in Qoder IDE | Loads the canonical skills, agents, commands, and MCP config; only the reviewed SessionStart hook is enabled, and Node.js 18+ must be on the non-interactive PATH. |
| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build; hook events use profile-gated dispatchers, but full Claude feature parity is not claimed. |
| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog; connect a provider and select a model in OpenCode ([#2617](https://github.com/affaan-m/ECC/issues/2617)). |
| GitHub Copilot Chat | Instruction-only | Checked-in instructions and prompt files | ECC hooks are not adapted. |
+1 -1
View File
@@ -3,7 +3,7 @@ name: harness-optimizer
description: Improve local agent-harness configuration reliability and cost using eval-driven grading (pass@k/pass^k) derived from the eval-harness skill.
tools: Read, Grep, Glob, Bash, Edit
model: sonnet
color: teal
color: cyan
---
## Prompt Defense Baseline
+22
View File
@@ -0,0 +1,22 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact|new",
"hooks": [
{
"type": "command",
"command": "node",
"args": [
"${QODER_PLUGIN_ROOT}/scripts/hooks/plugin-hook-bootstrap.js",
"node",
"scripts/hooks/session-start-bootstrap.js"
],
"timeout": 30,
"statusMessage": "Loading ECC session context"
}
]
}
]
}
}
+1
View File
@@ -56,6 +56,7 @@
".claude-plugin/",
".codex/",
".codex-plugin/",
".qoder-plugin/",
".cursor/",
".gemini/",
".github/PULL_REQUEST_TEMPLATE.md",
+1 -1
View File
@@ -262,7 +262,7 @@ async function main() {
contextScanner.push(contextDecoder.end());
const hookContext = contextScanner.context;
const rootDir = normalizePluginRootForPlatform(
process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT
process.env.QODER_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT || process.env.ECC_PLUGIN_ROOT
);
if (!mode || !relPath || !rootDir) {
+4 -2
View File
@@ -16,6 +16,7 @@ PLUGIN_JSON=".claude-plugin/plugin.json"
MARKETPLACE_JSON=".claude-plugin/marketplace.json"
CODEX_MARKETPLACE_JSON=".agents/plugins/marketplace.json"
CODEX_PLUGIN_JSON=".codex-plugin/plugin.json"
QODER_PLUGIN_JSON=".qoder-plugin/plugin.json"
CODEX_MARKETPLACE_PLUGIN_JSON="plugins/ecc/.codex-plugin/plugin.json"
OPENCODE_PACKAGE_JSON=".opencode/package.json"
OPENCODE_PACKAGE_LOCK_JSON=".opencode/package-lock.json"
@@ -60,7 +61,7 @@ if [[ -n "$(git status --porcelain --untracked-files=all)" ]]; then
fi
# Verify versioned manifests exist
for FILE in "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ROOT_ZH_CN_README_FILE" "$TR_README_FILE" "$PT_BR_README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"; do
for FILE in "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$QODER_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ROOT_ZH_CN_README_FILE" "$TR_README_FILE" "$PT_BR_README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"; do
if [[ ! -f "$FILE" ]]; then
echo "Error: $FILE not found"
exit 1
@@ -306,6 +307,7 @@ update_version "$PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSIO
update_marketplace_plugin_version "$MARKETPLACE_JSON"
update_codex_marketplace_version
update_version "$CODEX_PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|"
update_version "$QODER_PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|"
update_version "$CODEX_MARKETPLACE_PLUGIN_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|"
update_version "$OPENCODE_PACKAGE_JSON" "s|\"version\": *\"[^\"]*\"|\"version\": \"$VERSION\"|"
update_package_lock_version "$OPENCODE_PACKAGE_LOCK_JSON"
@@ -330,7 +332,7 @@ node tests/plugin-manifest.test.js
# Stage, commit, explicitly sign an annotated tag, and push. Signing failure
# stops here under set -e; no personal tag.gpgSign default is assumed.
git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$CODEX_MARKETPLACE_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ROOT_ZH_CN_README_FILE" "$TR_README_FILE" "$PT_BR_README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"
git add "$ROOT_PACKAGE_JSON" "$PACKAGE_LOCK_JSON" "$ROOT_AGENTS_MD" "$TR_AGENTS_MD" "$ZH_CN_AGENTS_MD" "$AGENT_YAML" "$VERSION_FILE" "$PLUGIN_JSON" "$MARKETPLACE_JSON" "$CODEX_MARKETPLACE_JSON" "$CODEX_PLUGIN_JSON" "$QODER_PLUGIN_JSON" "$CODEX_MARKETPLACE_PLUGIN_JSON" "$OPENCODE_PACKAGE_JSON" "$OPENCODE_PACKAGE_LOCK_JSON" "$OPENCODE_ECC_HOOKS_PLUGIN" "$README_FILE" "$ROOT_ZH_CN_README_FILE" "$TR_README_FILE" "$PT_BR_README_FILE" "$ZH_CN_README_FILE" "$SELECTIVE_INSTALL_ARCHITECTURE_DOC"
git commit -m "chore: bump plugin version to $VERSION"
git tag -s "v$VERSION" -m "Release v$VERSION"
git push origin main "v$VERSION"
+27
View File
@@ -461,6 +461,33 @@ test('codex plugin presentation assets exist and ship in npm package', () => {
}
});
// ── Qoder plugin manifest ─────────────────────────────────────────────────────
console.log('\n=== .qoder-plugin/plugin.json ===\n');
const qoderPluginPath = path.join(repoRoot, '.qoder-plugin', 'plugin.json');
const qoderPlugin = loadJsonObject(qoderPluginPath, '.qoder-plugin/plugin.json');
test('qoder plugin.json version matches package.json', () => {
assert.strictEqual(qoderPlugin.name, 'ecc');
assert.strictEqual(qoderPlugin.version, expectedVersion);
});
test('qoder plugin references canonical resources inside the bundle', () => {
for (const field of ['skills', 'commands', 'mcpServers', 'hooks']) {
assert.strictEqual(typeof qoderPlugin[field], 'string', `Expected Qoder manifest ${field} path`);
const target = path.resolve(repoRoot, qoderPlugin[field]);
assert.ok(target === repoRoot || target.startsWith(repoRoot + path.sep), `${field} escapes the Qoder bundle`);
assert.ok(fs.existsSync(target), `${field} target is missing: ${qoderPlugin[field]}`);
}
});
test('qoder native surface ships in npm package', () => {
const packageFiles = new Set(rootPackage.files);
assert.ok(packageFiles.has('.qoder-plugin/'));
assert.ok(packageFiles.has('hooks/'));
assert.ok(packageFiles.has('scripts/hooks/'));
});
// ── .mcp.json at plugin root ──────────────────────────────────────────────────
// Per official docs: keep .mcp.json at plugin root, NOT inside .codex-plugin/
console.log('\n=== .mcp.json (plugin root) ===\n');
+126
View File
@@ -0,0 +1,126 @@
/**
* Qoder native plugin contract tests.
*
* These tests intentionally execute the declared hook entry point instead of
* only inspecting JSON. Qoder launches hooks without a shell when `command`
* and `args` are separate, which keeps the same contract on Windows, macOS,
* and Linux.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const repoRoot = path.resolve(__dirname, '..');
const manifestPath = path.join(repoRoot, '.qoder-plugin', 'plugin.json');
const hooksPath = path.join(repoRoot, 'hooks', 'qoder-hooks.json');
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
const releaseScript = fs.readFileSync(path.join(repoRoot, 'scripts', 'release.sh'), 'utf8');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` ✓ ${name}`);
passed += 1;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
console.log('\n=== Qoder native plugin ===\n');
test('manifest and hook projection exist', () => {
assert.ok(fs.existsSync(manifestPath), 'Expected .qoder-plugin/plugin.json');
assert.ok(fs.existsSync(hooksPath), 'Expected hooks/qoder-hooks.json');
});
if (fs.existsSync(manifestPath) && fs.existsSync(hooksPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf8'));
test('manifest stays versioned with the npm package', () => {
assert.strictEqual(manifest.name, 'ecc');
assert.strictEqual(manifest.version, packageJson.version);
});
test('release tooling updates and stages the Qoder manifest', () => {
assert.ok(releaseScript.includes('QODER_PLUGIN_JSON=".qoder-plugin/plugin.json"'));
assert.ok(releaseScript.includes('update_version "$QODER_PLUGIN_JSON"'));
assert.ok(releaseScript.includes('git add ') && releaseScript.includes('"$QODER_PLUGIN_JSON"'));
});
test('manifest reuses canonical plugin resources', () => {
assert.strictEqual(manifest.skills, './skills/');
assert.strictEqual(manifest.commands, './commands/');
assert.strictEqual(manifest.mcpServers, './.mcp.json');
assert.strictEqual(manifest.hooks, './hooks/qoder-hooks.json');
});
test('agent colors are accepted by Qoder', () => {
const allowedColors = new Set([
'red', 'blue', 'green', 'yellow', 'purple', 'orange', 'pink', 'cyan',
]);
for (const fileName of fs.readdirSync(path.join(repoRoot, 'agents'))) {
if (!fileName.endsWith('.md')) continue;
const source = fs.readFileSync(path.join(repoRoot, 'agents', fileName), 'utf8');
const match = source.match(/^color:\s*(\S+)$/m);
if (match) {
assert.ok(allowedColors.has(match[1]), `${fileName} uses unsupported Qoder color ${match[1]}`);
}
}
});
test('Qoder hook is a minimal SessionStart projection using exec form', () => {
assert.deepStrictEqual(Object.keys(hooks.hooks), ['SessionStart']);
const registrations = hooks.hooks.SessionStart;
assert.strictEqual(registrations.length, 1);
assert.strictEqual(registrations[0].matcher, 'startup|resume|clear|compact|new');
assert.strictEqual(registrations[0].hooks.length, 1);
const hook = registrations[0].hooks[0];
assert.strictEqual(hook.type, 'command');
assert.strictEqual(hook.command, 'node');
assert.ok(Array.isArray(hook.args));
assert.strictEqual(hook.args[0], '${QODER_PLUGIN_ROOT}/scripts/hooks/plugin-hook-bootstrap.js');
assert.deepStrictEqual(hook.args.slice(1), [
'node',
'scripts/hooks/session-start-bootstrap.js',
]);
});
test('declared Qoder SessionStart launcher executes from QODER_PLUGIN_ROOT', () => {
const hook = hooks.hooks.SessionStart[0].hooks[0];
const args = hook.args.map(value => value.replace('${QODER_PLUGIN_ROOT}', repoRoot));
const result = spawnSync(hook.command, args, {
cwd: repoRoot,
env: {
...process.env,
QODER_PLUGIN_ROOT: repoRoot,
ECC_HOOKS_ENABLED: 'false',
},
input: JSON.stringify({
hook_event_name: 'SessionStart',
source: 'startup',
cwd: repoRoot,
}),
encoding: 'utf8',
timeout: 10000,
windowsHide: true,
});
assert.ifError(result.error);
assert.strictEqual(result.signal, null);
assert.strictEqual(result.status, 0, result.stderr);
});
}
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
@@ -113,6 +113,7 @@ function buildExpectedPublishPaths(repoRoot) {
"scripts/codex/merge-codex-config.js",
"scripts/codex/merge-mcp-config.js",
".codex-plugin",
".qoder-plugin",
"plugins/ecc",
".mcp.json",
"install.sh",
@@ -280,6 +281,7 @@ function main() {
".claude-plugin/plugin.json",
".github/PULL_REQUEST_TEMPLATE.md",
".codex-plugin/plugin.json",
".qoder-plugin/plugin.json",
".agents/skills/unified-memory/SKILL.md",
".agents/skills/unified-memory/agents/openai.yaml",
".cursor/skills/unified-memory/SKILL.md",