diff --git a/.eca-plugin/marketplace.json b/.eca-plugin/marketplace.json new file mode 100644 index 0000000000..a8fbd8f27b --- /dev/null +++ b/.eca-plugin/marketplace.json @@ -0,0 +1,14 @@ +{ + "plugins": [ + { + "name": "superpowers", + "description": "Agentic development methodology: spec-driven brainstorming, structured planning, subagent-driven development, TDD, and systematic debugging", + "source": ".eca-plugin/superpowers", + "category": "Workflow", + "tags": ["methodology", "skill", "planning", "tdd", "debugging", "subagent", "workflow"], + "author": "obra", + "icon": "⚡", + "featured": true + } + ] +} diff --git a/.eca-plugin/superpowers/eca.json b/.eca-plugin/superpowers/eca.json new file mode 100644 index 0000000000..941758dddb --- /dev/null +++ b/.eca-plugin/superpowers/eca.json @@ -0,0 +1,5 @@ +{ + "skills": [ + {"path": "skills"} + ] +} diff --git a/.eca-plugin/superpowers/hooks/hooks.json b/.eca-plugin/superpowers/hooks/hooks.json new file mode 100644 index 0000000000..48e1f63720 --- /dev/null +++ b/.eca-plugin/superpowers/hooks/hooks.json @@ -0,0 +1,22 @@ +{ + "superpowers-bootstrap": { + "type": "chatStart", + "visible": false, + "actions": [ + { + "type": "shell", + "file": "${plugin:root}/hooks/session-start" + } + ] + }, + "superpowers-bootstrap-compact": { + "type": "postCompact", + "visible": false, + "actions": [ + { + "type": "shell", + "file": "${plugin:root}/hooks/session-start" + } + ] + } +} diff --git a/.eca-plugin/superpowers/hooks/session-start b/.eca-plugin/superpowers/hooks/session-start new file mode 120000 index 0000000000..91afb49de0 --- /dev/null +++ b/.eca-plugin/superpowers/hooks/session-start @@ -0,0 +1 @@ +../../../hooks/session-start \ No newline at end of file diff --git a/.eca-plugin/superpowers/skills b/.eca-plugin/superpowers/skills new file mode 120000 index 0000000000..5dcab58e15 --- /dev/null +++ b/.eca-plugin/superpowers/skills @@ -0,0 +1 @@ +../../skills \ No newline at end of file diff --git a/.version-bump.json b/.version-bump.json index 7fc6cc9fed..89551d0b97 100644 --- a/.version-bump.json +++ b/.version-bump.json @@ -6,7 +6,8 @@ { "path": ".codex-plugin/plugin.json", "field": "version" }, { "path": ".kimi-plugin/plugin.json", "field": "version" }, { "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }, - { "path": "gemini-extension.json", "field": "version" } + { "path": "gemini-extension.json", "field": "version" }, + { "path": ".eca-plugin/marketplace.json", "field": "plugins.0.version" } ], "audit": { "exclude": [ diff --git a/README.md b/README.md index bb398c6b68..492f484c10 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ If this sounds like someone you know, definitely send them our way. ## Quickstart -Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi). +Give your agent Superpowers: [Claude Code](#claude-code), [Antigravity](#antigravity), [Codex App](#codex-app), [Codex CLI](#codex-cli), [Cursor](#cursor), [Factory Droid](#factory-droid), [Gemini CLI](#gemini-cli), [GitHub Copilot CLI](#github-copilot-cli), [Kimi Code](#kimi-code), [OpenCode](#opencode), [Pi](#pi), [ECA](#eca). ## How it works @@ -199,6 +199,23 @@ pi -e /path/to/superpowers The Pi package loads the Superpowers skills and a small extension that injects the `using-superpowers` bootstrap at session startup and again after compaction. Pi has native skills, so no compatibility `Skill` tool is required. Subagent and task-list tools remain optional Pi companion packages. +### ECA + +Add Superpowers as a plugin source in `~/.config/eca/config.json` (or `.eca/config.json` for project-local): + +```json +{ + "plugins": { + "superpowers-source": { + "source": "https://github.com/obra/superpowers.git" + }, + "install": ["superpowers"] + } +} +``` + +Restart ECA. Detailed docs: [docs/README.eca.md](docs/README.eca.md) + ## The Basic Workflow 1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document. diff --git a/docs/README.eca.md b/docs/README.eca.md new file mode 100644 index 0000000000..434854b41f --- /dev/null +++ b/docs/README.eca.md @@ -0,0 +1,105 @@ +# Superpowers for ECA (Editor Code Assistant) + +Complete guide for using Superpowers with [ECA](https://eca.dev). + +## Installation + +Add Superpowers as a plugin source in your ECA config: + +**Global config** (`~/.config/eca/config.json`): + +```json +{ + "plugins": { + "superpowers-source": { + "source": "https://github.com/obra/superpowers.git" + }, + "install": ["superpowers"] + } +} +``` + +**Project-local config** (`.eca/config.json`): + +```json +{ + "plugins": { + "superpowers-source": { + "source": "https://github.com/obra/superpowers.git" + }, + "install": ["superpowers"] + } +} +``` + +ECA's runtime plugin loader matches install entries against marketplace entries by exact name. The plugin marketplace entry is named `superpowers`, so the install list must say `"superpowers"` — not `"superpowers@source-name"`. The `@source` suffix is only parsed by the `/plugin-install` command, not by the loader that resolves plugins on session start. + +Restart ECA for the plugin to take effect. + +Verify by sending: + +> Tell me about your superpowers + +### Updating + +ECA re-resolves plugin sources on startup and periodically (approximately hourly). To force an update, restart ECA or clear the plugin cache at `~/.eca/cache/plugins/`. + +To pin a specific version, use a branch or tag in the source URL: + +```json +{ + "plugins": { + "superpowers-source": { + "source": "https://github.com/obra/superpowers.git#v5.1.0" + }, + "install": ["superpowers"] + } +} +``` + +## How It Works + +The plugin provides three things: + +1. **Skills** — All Superpowers skills are registered under the `superpowers:` namespace. ECA discovers them via the plugin's `skills/` directory (symlinked to the project's `skills/` root). +2. **Bootstrap injection** — A `chatStart` hook runs the `session-start` script, which injects the `using-superpowers` skill content as additional context at the start of every chat. +3. **Tool mapping** — The `using-superpowers` skill references `references/eca-tools.md` for ECA-specific tool name equivalents. + +### Skill Namespacing + +ECA plugins expose skills under a `:` namespace. Superpowers skills are accessed as: + +- `superpowers:brainstorming` +- `superpowers:test-driven-development` +- `superpowers:systematic-debugging` +- `superpowers:using-superpowers` +- etc. + +When the plugin name matches the skill name (as with `using-superpowers`), the qualified form `superpowers:using-superpowers` and the bare form `using-superpowers` both resolve. + +To manually invoke a skill: + +``` +Load the superpowers:brainstorming skill +``` + +## Troubleshooting + +### Plugin not loading + +1. Check that your `config.json` has the correct `source` URL. +2. Verify the git repo is accessible: `git ls-remote https://github.com/obra/superpowers.git` +3. Check ECA logs for plugin resolution errors. + +### Skills not found + +1. Use `/plugins` to verify `superpowers` appears in the plugin list. +2. Confirm the `skills` symlink inside `.eca-plugin/superpowers/` resolves to the repo's `skills/` directory. + +### Bootstrap not appearing + +The `chatStart` hook runs `hooks/session-start`, which reads `skills/using-superpowers/SKILL.md` and injects it as `additionalContext`. If the bootstrap is missing: + +1. Check that the `session-start` symlink resolves to the repo-root `hooks/session-start` script. +2. Confirm the hook is registered in `.eca-plugin/superpowers/hooks/hooks.json` with `"type": "chatStart"`. +3. Start a new session — hooks fire on chat start, not mid-session. diff --git a/docs/porting-to-a-new-harness.md b/docs/porting-to-a-new-harness.md index 4ae9603def..754cc1bc88 100644 --- a/docs/porting-to-a-new-harness.md +++ b/docs/porting-to-a-new-harness.md @@ -792,6 +792,7 @@ Use this as the live index; when in doubt, read the files, not this table. | Kimi Code | `.kimi-plugin/plugin.json` | manifest `sessionStart.skill` loads `using-superpowers` | inline `skillInstructions` in manifest | `tests/kimi/` | marketplace or `/plugins install` GitHub URL | | OpenCode | `.opencode/plugins/superpowers.js` (declared via root `package.json` `main`) | in-process: `config` hook registers skills dir; `experimental.chat.messages.transform` injects user message | inline in `superpowers.js` | `tests/opencode/` | `opencode.json` plugin git URL | | pi | `.pi/extensions/superpowers.ts` | in-process: `resources_discover` registers skills; `context` event injects user message; lifecycle-flag + compaction-aware | `piToolMapping()` inline **and** `references/pi-tools.md` | `tests/pi/` | repo-root `package.json` fields | +| ECA | `.eca-plugin/marketplace.json` + `.eca-plugin/superpowers/eca.json` | shell hook → `hooks/session-start` (`additionalContext`); `chatStart` + `postCompact` hooks | `references/eca-tools.md` | `tests/hooks/`, `tests/eca/` | plugin source (git URL or local path) | ## Appendix B — Gotchas that have bitten porters diff --git a/hooks/session-start b/hooks/session-start index 93a6bc2c6b..51798b55c4 100755 --- a/hooks/session-start +++ b/hooks/session-start @@ -42,7 +42,8 @@ elif [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -z "${COPILOT_CLI:-}" ]; then # Claude Code sets CLAUDE_PLUGIN_ROOT without COPILOT_CLI printf '{\n "hookSpecificOutput": {\n "hookEventName": "SessionStart",\n "additionalContext": "%s"\n }\n}\n' "$session_context" | cat else - # Copilot CLI (sets COPILOT_CLI=1) or unknown platform — SDK standard format + # Copilot CLI (sets COPILOT_CLI=1), ECA (no harness env vars), or unknown + # platform — SDK standard format printf '{\n "additionalContext": "%s"\n}\n' "$session_context" | cat fi diff --git a/scripts/sync-to-codex-plugin.sh b/scripts/sync-to-codex-plugin.sh index ef8e0839f0..7bef6e740d 100755 --- a/scripts/sync-to-codex-plugin.sh +++ b/scripts/sync-to-codex-plugin.sh @@ -48,6 +48,7 @@ EXCLUDES=( "/.claude-plugin/" "/.codex/" "/.cursor-plugin/" + "/.eca-plugin/" "/.git/" "/.gitattributes" "/.github/" diff --git a/skills/using-superpowers/SKILL.md b/skills/using-superpowers/SKILL.md index 8a08873ba0..e4c529865b 100644 --- a/skills/using-superpowers/SKILL.md +++ b/skills/using-superpowers/SKILL.md @@ -56,6 +56,7 @@ If your harness appears here, read its reference file for special instructions: - Codex: `references/codex-tools.md` - Pi: `references/pi-tools.md` - Antigravity: `references/antigravity-tools.md` +- ECA: `references/eca-tools.md` ## User Instructions diff --git a/skills/using-superpowers/references/eca-tools.md b/skills/using-superpowers/references/eca-tools.md new file mode 100644 index 0000000000..ba93a4af1c --- /dev/null +++ b/skills/using-superpowers/references/eca-tools.md @@ -0,0 +1,44 @@ +# ECA Tool Mapping + +Skills speak in actions ("dispatch a subagent", "create a todo", "read a file"). On ECA (Editor Code Assistant) these resolve to the tools below. + +| Action skills request | ECA equivalent | +|----------------------|----------------| +| Read a file | `eca__read_file` | +| Create / overwrite a file | `eca__write_file` | +| Edit a file (targeted replace) | `eca__edit_file` | +| Move / rename a file | `eca__move_file` | +| Run a shell command | `eca__shell_command` | +| Search file contents | `eca__grep` | +| List directory tree | `eca__directory_tree` | +| Task tracking ("create a todo", "mark complete") | `eca__task` | +| Invoke a skill | `eca__skill` | +| Check editor diagnostics | `eca__editor_diagnostics` | +| Git operations | `eca__git` | +| Fetch a URL / web search | `eca__shell_command` (use `curl`/`wget`) | + +## Subagent support + +ECA supports subagents via the `eca__spawn_agent` tool. Available agent types: + +- `general` — General-purpose agent for complex multi-step tasks and research. +- `explorer` — Codebase search specialist for finding and reading file contents. + +When a skill says to dispatch a named agent type, use `eca__spawn_agent` with the appropriate agent and a detailed task prompt: + +| Skill instruction | ECA equivalent | +|-------------------|----------------| +| `Task tool (superpowers:implementer)` | `eca__spawn_agent(agent: "general", task: "")` | +| `Task tool (superpowers:spec-reviewer)` | `eca__spawn_agent(agent: "general", task: "")` | +| `Task tool (superpowers:code-reviewer)` | `eca__spawn_agent(agent: "general", task: "")` | +| `Task tool (superpowers:code-quality-reviewer)` | `eca__spawn_agent(agent: "general", task: "")` | +| `Task tool (general-purpose)` with inline prompt | `eca__spawn_agent(agent: "general", task: "")` | +| `Task tool (explorer)` | `eca__spawn_agent(agent: "explorer", task: "")` | + +### Prompt filling + +Skills provide prompt templates with placeholders like `{WHAT_WAS_IMPLEMENTED}` or `[FULL TEXT of task]`. Fill all placeholders and pass the complete prompt as the `task` argument to `eca__spawn_agent`. + +### Parallel dispatch + +ECA supports parallel subagent dispatch. When a skill asks you to dispatch multiple independent subagent tasks in parallel, issue the `eca__spawn_agent` calls together. Keep dependent tasks sequential. diff --git a/tests/eca/run-tests.sh b/tests/eca/run-tests.sh new file mode 100755 index 0000000000..885ee7e4dd --- /dev/null +++ b/tests/eca/run-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "Running ECA plugin structure tests..." +bash "$SCRIPT_DIR/test-plugin-structure.sh" diff --git a/tests/eca/test-plugin-structure.sh b/tests/eca/test-plugin-structure.sh new file mode 100755 index 0000000000..baee5c8300 --- /dev/null +++ b/tests/eca/test-plugin-structure.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +FAILURES=0 + +pass() { + echo " [PASS] $1" +} + +fail() { + echo " [FAIL] $1" + FAILURES=$((FAILURES + 1)) +} + +echo "ECA plugin structure tests" + +MARKETPLACE="$REPO_ROOT/.eca-plugin/marketplace.json" +PLUGIN_JSON="$REPO_ROOT/.eca-plugin/superpowers/eca.json" +HOOKS_JSON="$REPO_ROOT/.eca-plugin/superpowers/hooks/hooks.json" +SESSION_START_LINK="$REPO_ROOT/.eca-plugin/superpowers/hooks/session-start" +SKILLS_LINK="$REPO_ROOT/.eca-plugin/superpowers/skills" +TOOLS_REF="$REPO_ROOT/skills/using-superpowers/references/eca-tools.md" + +if [ ! -f "$MARKETPLACE" ]; then + fail "marketplace.json missing at $MARKETPLACE" +else + pass "marketplace.json exists" +fi + +if ! python3 - "$MARKETPLACE" <<'PY' +import json, sys +from pathlib import Path + +mp = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +plugins = mp.get("plugins", []) +if not isinstance(plugins, list) or len(plugins) != 1: + raise AssertionError("marketplace.json must have exactly one plugin entry") +entry = plugins[0] +if entry.get("name") != "superpowers": + raise AssertionError(f"plugin name: expected 'superpowers', got {entry.get('name')!r}") +if entry.get("source") != ".eca-plugin/superpowers": + raise AssertionError(f"plugin source: expected '.eca-plugin/superpowers', got {entry.get('source')!r}") +PY +then + fail "marketplace.json content invalid" +else + pass "marketplace.json content valid" +fi + +if [ ! -f "$PLUGIN_JSON" ]; then + fail "eca.json missing at $PLUGIN_JSON" +else + pass "eca.json exists" +fi + +if ! python3 - "$PLUGIN_JSON" <<'PY' +import json, sys +from pathlib import Path + +cfg = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +skills = cfg.get("skills") +if not isinstance(skills, list) or len(skills) != 1: + raise AssertionError("eca.json skills must be a list with one entry") +if skills[0].get("path") != "skills": + raise AssertionError(f"skills path: expected 'skills', got {skills[0].get('path')!r}") +PY +then + fail "eca.json content invalid" +else + pass "eca.json content valid" +fi + +if [ ! -f "$HOOKS_JSON" ]; then + fail "hooks.json missing at $HOOKS_JSON" +else + pass "hooks.json exists" +fi + +if ! python3 - "$HOOKS_JSON" <<'PY' +import json, sys +from pathlib import Path + +hooks = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) + +expected_hooks = { + "superpowers-bootstrap": "chatStart", + "superpowers-bootstrap-compact": "postCompact", +} + +for name, hook_type in expected_hooks.items(): + hook = hooks.get(name) + if not isinstance(hook, dict): + raise AssertionError(f"hooks.json missing '{name}' object") + if hook.get("type") != hook_type: + raise AssertionError( + f"{name} type: expected {hook_type!r}, got {hook.get('type')!r}" + ) + actions = hook.get("actions") + if not isinstance(actions, list) or len(actions) != 1: + raise AssertionError(f"{name} actions must be a list with one entry") + action = actions[0] + if action.get("type") != "shell": + raise AssertionError( + f"{name} action type: expected 'shell', got {action.get('type')!r}" + ) + if "${plugin:root}/hooks/session-start" not in action.get("file", ""): + raise AssertionError( + f"{name} action file should reference session-start, got {action.get('file')!r}" + ) +PY +then + fail "hooks.json content invalid" +else + pass "hooks.json content valid" +fi + +if [ ! -L "$SESSION_START_LINK" ]; then + fail "session-start symlink missing at $SESSION_START_LINK" +elif [ "$(readlink "$SESSION_START_LINK")" != "../../../hooks/session-start" ]; then + fail "session-start symlink target is '$(readlink "$SESSION_START_LINK")', expected '../../../hooks/session-start'" +else + pass "session-start symlink correct" +fi + +if [ ! -L "$SKILLS_LINK" ]; then + fail "skills symlink missing at $SKILLS_LINK" +elif [ "$(readlink "$SKILLS_LINK")" != "../../skills" ]; then + fail "skills symlink target is '$(readlink "$SKILLS_LINK")', expected '../../skills'" +else + pass "skills symlink correct" +fi + +if [ ! -f "$TOOLS_REF" ]; then + fail "eca-tools.md reference missing at $TOOLS_REF" +else + pass "eca-tools.md reference exists" +fi + +if [[ "$FAILURES" -gt 0 ]]; then + echo "STATUS: FAILED ($FAILURES failure(s))" + exit 1 +fi + +echo "STATUS: PASSED" diff --git a/tests/hooks/test-session-start.sh b/tests/hooks/test-session-start.sh index b027f3c650..f39854ca46 100755 --- a/tests/hooks/test-session-start.sh +++ b/tests/hooks/test-session-start.sh @@ -196,6 +196,15 @@ assert_command_output \ CLAUDE_PLUGIN_ROOT="$REPO_ROOT" \ bash "$HOOK_UNDER_TEST" +eca_home="$(make_home eca)" +assert_command_output \ + "ECA (no harness env vars) emits top-level additionalContext" \ + "sdk" \ + "" \ + "" \ + "$eca_home" \ + bash "$HOOK_UNDER_TEST" + if [[ "$FAILURES" -gt 0 ]]; then echo "STATUS: FAILED ($FAILURES failure(s))" exit 1