From f1fec0e53934737d3b3b8388b0fd1651e8b62f4f Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Tue, 4 Aug 2026 21:42:25 -0400 Subject: [PATCH] feat: add retention feedback loop and honest support matrix (#2681) * feat: add retention feedback loop * test: retire obsolete README parity row guard * fix: harden public feedback guidance * fix: let feedback CLI output flush * test: keep feedback help coverage focused --- .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 40 ++++++++++ .github/ISSUE_TEMPLATE/install-problem.yml | 93 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/quick-feedback.yml | 56 +++++++++++++ COMMANDS-QUICK-REF.md | 14 ++++ README.md | 63 ++++++++------- package.json | 1 + scripts/ci/catalog.js | 51 ------------ scripts/doctor.js | 6 ++ scripts/ecc.js | 6 ++ scripts/feedback.js | 65 +++++++++++++++ scripts/lib/feedback-links.js | 39 +++++++++ scripts/repair.js | 5 ++ scripts/uninstall.js | 5 ++ tests/ci/validators.test.js | 10 +-- tests/plugin-manifest.test.js | 7 -- tests/scripts/doctor.test.js | 15 ++++ tests/scripts/ecc.test.js | 8 ++ tests/scripts/feedback.test.js | 83 +++++++++++++++++++ tests/scripts/npm-publish-surface.test.js | 2 + tests/scripts/uninstall.test.js | 2 + 21 files changed, 486 insertions(+), 93 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/install-problem.yml create mode 100644 .github/ISSUE_TEMPLATE/quick-feedback.yml create mode 100644 scripts/feedback.js create mode 100644 scripts/lib/feedback-links.js create mode 100644 tests/scripts/feedback.test.js diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..cf4c257b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: ECC questions and setup help + url: https://github.com/affaan-m/ECC/discussions/categories/q-a + about: Ask a public question or get help from the community. + - name: Private security report + url: https://github.com/affaan-m/ECC/security/advisories/new + about: Report vulnerabilities privately. Do not put secrets in a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..b8d2cf10a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,40 @@ +name: Feature idea +description: Describe the outcome you need and your current workaround. +title: "[Idea] " +labels: + - enhancement + - needs-triage +body: + - type: markdown + attributes: + value: | + This is a public GitHub issue. Do not include secrets, prompts, customer data, private repository details, or unredacted paths. + - type: textarea + id: outcome + attributes: + label: What outcome do you need? + description: Describe the job to be done, not an implementation if you do not have one in mind. + validations: + required: true + - type: textarea + id: workaround + attributes: + label: What do you do today? + description: Optional. A workaround helps us understand urgency and scope. + - type: dropdown + id: harness + attributes: + label: Which harness is affected? + options: + - All harnesses + - Claude Code + - Codex + - Cursor + - OpenCode + - GitHub Copilot + - Another harness + - type: textarea + id: success + attributes: + label: What would success look like? + description: Optional acceptance criteria or a small example. diff --git a/.github/ISSUE_TEMPLATE/install-problem.yml b/.github/ISSUE_TEMPLATE/install-problem.yml new file mode 100644 index 000000000..8807b0789 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/install-problem.yml @@ -0,0 +1,93 @@ +name: Install or runtime problem +description: Tell us what failed without writing a full diagnostic report. +title: "[Problem] " +labels: + - bug + - needs-triage + - area:install +body: + - type: markdown + attributes: + value: | + Thanks for reporting this. Keep it short: what happened and which setup you used are enough to start. + + This issue is public. Do not paste secrets, prompts, private repository names, or unredacted home/project paths. ECC never uploads diagnostics automatically. + - type: dropdown + id: impact + attributes: + label: What is the impact? + options: + - ECC will not install + - ECC installs, but nothing loads + - Some components are missing or silently ignored + - ECC is duplicated or conflicts with another install + - A hook or command interrupts normal work + - Doctor or repair does not recover the install + - Other runtime problem + validations: + required: true + - type: textarea + id: happened + attributes: + label: What happened? + description: Include the shortest error or symptom that explains the problem. + placeholder: I expected …, but … + validations: + required: true + - type: dropdown + id: harness + attributes: + label: Harness + options: + - Claude Code + - Codex app or CLI + - Cursor + - OpenCode + - GitHub Copilot + - Kimi Code + - Gemini CLI + - Zed + - Antigravity + - Qwen + - Hermes + - OpenClaw + - CodeBuddy or JoyCode + - Other + validations: + required: true + - type: dropdown + id: install_method + attributes: + label: Install method + options: + - Claude plugin marketplace + - ecc or ecc-install CLI + - Manual clone or copy + - Codex sync script + - Codex marketplace plugin + - Harness-specific installer target + - Unknown + - Other + - type: dropdown + id: operating_system + attributes: + label: Operating system + options: + - Windows (native) + - Windows (WSL) + - macOS + - Linux + - Other + validations: + required: true + - type: input + id: versions + attributes: + label: ECC and harness versions + description: If known. A tag, commit, or package version is enough. + placeholder: ECC 2.1.0; Claude Code 2.x + - type: textarea + id: diagnostics + attributes: + label: Optional redacted diagnostics + description: Paste only the relevant lines from `ecc doctor`. Remove paths, repository names, prompts, tokens, and secrets. diff --git a/.github/ISSUE_TEMPLATE/quick-feedback.yml b/.github/ISSUE_TEMPLATE/quick-feedback.yml new file mode 100644 index 000000000..dfc607ea8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/quick-feedback.yml @@ -0,0 +1,56 @@ +name: Quick product feedback +description: One required choice and an optional sentence. Leaving ECC is valid feedback. +title: "[Feedback] " +labels: + - feedback + - needs-triage +body: + - type: markdown + attributes: + value: | + Thank you for telling us what got in the way. This form is intentionally short. + + This is a public GitHub issue. Do not include secrets, prompts, customer data, or private repository details. + + Report a vulnerability through [GitHub's private security advisory form](https://github.com/affaan-m/ECC/security/advisories/new), not here. Non-vulnerability security or trust concerns are welcome in this form. + - type: dropdown + id: reason + attributes: + label: What best describes your feedback? + options: + - I could not install or activate ECC + - ECC made the agent slower or the output worse + - ECC used too much token or context budget + - Hooks or gates interrupted normal work + - ECC was too complicated or required too much configuration + - My harness or operating system was missing or unreliable + - I had a security or trust concern + - A feature I needed was missing + - Support was too slow + - I was only testing and no longer need it + - Something worked especially well + - Other + validations: + required: true + - type: dropdown + id: harness + attributes: + label: Where did you use ECC? + options: + - Claude Code + - Codex + - Cursor + - OpenCode + - GitHub Copilot + - Another harness + - I did not get far enough to use it + - type: textarea + id: change + attributes: + label: What is the one change that would matter most? + description: Optional. One sentence is plenty. + - type: textarea + id: keep + attributes: + label: What should ECC keep? + description: Optional. Tell us what was valuable even if the overall experience did not work. diff --git a/COMMANDS-QUICK-REF.md b/COMMANDS-QUICK-REF.md index 5fb87c0b4..6a9fa22e2 100644 --- a/COMMANDS-QUICK-REF.md +++ b/COMMANDS-QUICK-REF.md @@ -152,6 +152,20 @@ executable instructions or policy. --- +## Install Health & Feedback CLI + +These lifecycle commands are also available through the `ecc` CLI. + +| Command | What it does | +|---------|-------------| +| `ecc list-installed` | Show installs recorded in ECC's managed state | +| `ecc doctor` | Diagnose missing or drifted managed files and point failures to the short problem form | +| `ecc repair` | Restore missing or drifted managed files | +| `ecc uninstall` | Remove only install-state-managed files and optionally show the 20-second exit-feedback route | +| `ecc feedback` | Show the public problem, quick-feedback, and feature routes without reading files or uploading diagnostics | + +--- + ## Learning & Improvement | Command | What it does | diff --git a/README.md b/README.md index de7cc5591..d0766f671 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,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, with first-class Codex support and adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. +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. Access to 67 agents, 281 skills, and 94 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. @@ -141,6 +141,8 @@ You can use ECC with Claude Code, Codex, and other harnesses at the same time. C If you already layered multiple installs and things look duplicated, skip straight to [Reset / Uninstall ECC](#reset--uninstall-ecc). +**Install trouble?** Open the short [install or runtime problem form](https://github.com/affaan-m/ECC/issues/new?template=install-problem.yml), or run `ecc feedback`. ECC never uploads diagnostics automatically. + ### Claude Code Run these commands inside Claude Code: @@ -535,6 +537,8 @@ node scripts/uninstall.js --dry-run node scripts/uninstall.js ``` +If you are leaving, the uninstall command prints an optional [20-second feedback form](https://github.com/affaan-m/ECC/issues/new?template=quick-feedback.yml). It is a public GitHub issue, never blocks uninstall, and ECC does not upload diagnostics. You can also run `ecc feedback` at any time to see the problem, feedback, and feature routes. + Plugin users should remove the plugin from Claude Code, then delete only the rule folders they manually copied and no longer want. ECC only removes files recorded in its install-state. It does not claim unrelated files in your harness directories. If you stacked methods, clean up in this order: @@ -1305,7 +1309,16 @@ See [`rules/README.md`](rules/README.md) for installation and structure details. ## Cross-Platform Support -ECC fully supports **Windows, macOS, and Linux**, alongside tight integration across major IDEs (Cursor, Zed, OpenCode, Antigravity) and CLI harnesses. All hooks and scripts are written in Node.js for maximum compatibility. +ECC's core Node.js CLI and managed installers run on **Windows, macOS, and Linux**, but optional capabilities are not at full parity. Some continuous-learning, GAN, and orchestration paths still require Bash or Python; harnesses also expose different hook, agent, and skill APIs. + +| Platform | Status | Current limitation | +|---|---|---| +| Linux | Supported core | Optional features may require Bash, Python, or provider-specific tools. | +| macOS | Supported core | The standalone GAN shell path is not compatible with the system Bash 3.2 and currently has a score-parsing defect ([#2674](https://github.com/affaan-m/ECC/issues/2674)). | +| Windows + WSL | Supported core | WSL follows the Linux paths; Windows host integrations still vary by harness. | +| Windows native | Supported with limitations | Continuous-learning v2's observer daemon and memory-vault writes have open native-Windows defects ([#2489](https://github.com/affaan-m/ECC/issues/2489), [#2626](https://github.com/affaan-m/ECC/issues/2626)). Shell-backed optional features require Git Bash/WSL or are unavailable. | + +Treat `stable`, `beta`, `experimental`, and `instruction-only` below as capability statements, not marketing tiers.
Package manager detection @@ -1400,31 +1413,25 @@ See [affaan-m/ECC#2065](https://github.com/affaan-m/ECC/issues/2065). ## Platform Support -| Harness | ECC distribution | Main instruction surface | Automation | +| Harness | Status | Recommended distribution | Important limitation | |---|---|---|---| -| Claude Code | Plugin or selective installer | `CLAUDE.md`, rules, skills, agents | Native plugin hooks | -| Codex | Sync flow, repo config, experimental ECC marketplace | `AGENTS.md`, skills, `.codex/config.toml` | Git hooks and Codex-native configuration | -| Cursor | Project adapter | `.cursor/rules/`, scoped agents | Cursor hook adapter | -| OpenCode | Built plugin plus selective installer | `opencode.json`, instructions, commands | OpenCode plugin events | -| GitHub Copilot | Checked-in instruction layer | `copilot-instructions.md`, prompt files | No ECC hook runtime | +| 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 sync; marketplace experimental | Repo config or `sync-ecc-to-codex.sh` | No ECC hook runtime. The marketplace package can omit shared repository content from Codex's cache; use sync for the reliable path. | +| Cursor | Beta project adapter | Selective installer into `.cursor/` | Agent discovery varies by Cursor build, and ECC's installer paths do not yet expose identical hook sets ([#2419](https://github.com/affaan-m/ECC/issues/2419)). | +| OpenCode | Beta built plugin | Build plugin, then selective installer | ECC ships a subset of the catalog and the reference config pins Anthropic models; select models available to your provider ([#2617](https://github.com/affaan-m/ECC/issues/2617)). | +| GitHub Copilot | Instruction-only | Checked-in instructions and prompt files | No ECC hooks, runtime agents, delegation, or native skill discovery. | +| Gemini, Zed, Antigravity, Qwen, Hermes, OpenClaw, Kimi, CodeBuddy, JoyCode | Experimental/minimal adapters | Harness-specific selective target | File placement and instruction portability are tested; full Claude feature parity is not claimed. | -### Cross-Tool Feature Parity +### Cross-tool capability map -| Feature | Claude Code | Cursor IDE | Codex CLI | OpenCode | GitHub Copilot | -|---------|-----------------------|------------|-----------|----------|----------------| -| **Agents** | 67 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 | N/A | -| **Commands** | 94 | Shared | Instruction-based | 35 | 5 prompts | -| **Skills** | 281 | Shared | 10 (native format) | 37 | Via instructions | -| **Hook Events** | 8 types | 15 types | None yet | 11 types | None | -| **Hook Scripts** | 20+ scripts | 16 scripts (DRY adapter) | N/A | Plugin hooks | N/A | -| **Rules** | 34 (common + lang) | 34 (YAML frontmatter) | Instruction-based | 13 instructions | 1 always-on file | -| **Custom Tools** | Via hooks | Via hooks | N/A | 6 native tools | N/A | -| **MCP Servers** | 14 | Shared (mcp.json) | 7 (auto-merged via TOML parser) | Full | N/A | -| **Config Format** | settings.json | hooks.json + rules/ | config.toml | opencode.json | copilot-instructions.md + settings.json | -| **Context File** | CLAUDE.md + AGENTS.md | AGENTS.md | AGENTS.md | AGENTS.md | copilot-instructions.md | -| **Secret Detection** | Hook-based | beforeSubmitPrompt hook | Sandbox-based | Hook-based | Instruction-based | -| **Auto-Format** | PostToolUse hook | afterFileEdit hook | N/A | file.edited hook | N/A | -| **Version** | Plugin | Plugin | Reference config | 2.1.0 | Instruction layer | +| Capability | Claude Code | Codex | Cursor | OpenCode | GitHub Copilot | +|---|---|---|---|---|---| +| Instructions | Native | Native `AGENTS.md` | Project rules | Plugin instructions | Native instruction file | +| Skills | Native installed set | Native synced set | Build-dependent/project set | Built subset | Prompt/instruction references only | +| Agents/delegation | Native agents | Codex multi-agent roles | Build-dependent project agents | Plugin agents | Not supported | +| ECC hooks | Native plugin hooks | Not supported | Cursor hook adapter; install-path differences remain | Plugin events | Not supported | +| MCP configuration | Available, explicit activation | TOML merge through sync | Explicit project/user config | Provider/plugin config | Not supplied by ECC | +| Parity with Claude Code | Primary reference | Partial | Partial | Partial | Not a parity target | **Key architectural decisions:** - **AGENTS.md** at root is the universal cross-tool file (read by Claude Code, Cursor, Codex, and OpenCode; GitHub Copilot uses `.github/copilot-instructions.md` instead) @@ -1518,7 +1525,7 @@ alwaysApply: false
Codex macOS app + CLI support in depth -ECC provides **first-class Codex support** for both the macOS app and CLI, with a reference configuration, Codex-specific AGENTS.md supplement, and shared skills. For repo navigation, surface ownership, and PR diff packet guidance, start with [`docs/CODEX-NAVIGATION-GUIDE.md`](docs/CODEX-NAVIGATION-GUIDE.md). +ECC provides a supported Codex repo/sync path for the macOS app and CLI, with a reference configuration, Codex-specific AGENTS.md supplement, and shared skills. The ECC marketplace route remains experimental. For repo navigation, surface ownership, and PR diff packet guidance, start with [`docs/CODEX-NAVIGATION-GUIDE.md`](docs/CODEX-NAVIGATION-GUIDE.md). ```bash # Run Codex CLI in the repo: AGENTS.md and .codex/ are auto-detected @@ -1597,7 +1604,7 @@ The adapter writes ECC-managed files under `.zed/` and keeps BYOK/OpenRouter cre
OpenCode support in depth -ECC provides **full OpenCode support** including plugins and hooks. +ECC provides a beta OpenCode plugin integration with instructions, a catalog subset, commands, custom tools, and hook events. It does not provide feature parity with Claude Code, and the reference model IDs must exist in the user's configured provider. ```bash # Install OpenCode @@ -1921,8 +1928,8 @@ Each component is fully independent. Yes. ECC is cross-platform: - **Cursor**: Pre-translated configs in `.cursor/`. See [Platform Support](#platform-support). - **Gemini CLI**: Experimental project-local support via `.gemini/GEMINI.md` and shared installer plumbing. -- **OpenCode**: Full plugin support in `.opencode/`. -- **Codex**: First-class support for both macOS app and CLI, with adapter drift guards and SessionStart fallback. +- **OpenCode**: Beta plugin integration in `.opencode/`; provider model selection and catalog parity remain limited. +- **Codex**: Supported repo/sync path for macOS app and CLI; ECC's marketplace package remains experimental. - **GitHub Copilot (VS Code)**: Instruction and prompt layer via `.github/copilot-instructions.md`, `.vscode/settings.json`, and `.github/prompts/`. - **Antigravity**: Tightly integrated setup for workflows, skills, and flattened rules in `.agent/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md). - **JoyCode / CodeBuddy**: Project-local selective install adapters for commands, agents, skills, and flattened rules. See [JoyCode Adapter Guide](docs/JOYCODE-GUIDE.md). diff --git a/package.json b/package.json index 4cae4bd50..42c2ec23e 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "scripts/discussion-audit.js", "scripts/doctor.js", "scripts/ecc.js", + "scripts/feedback.js", "scripts/memory.js", "scripts/memory-mcp.mjs", "scripts/gemini-adapt-agents.js", diff --git a/scripts/ci/catalog.js b/scripts/ci/catalog.js index c9be440af..d538dad36 100644 --- a/scripts/ci/catalog.js +++ b/scripts/ci/catalog.js @@ -133,38 +133,6 @@ function parseReadmeExpectations(readmeContent) { }); } - const parityPatterns = [ - { - category: 'agents', - regex: /^\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|(?:\s*N\/A\s*\|)?$/im, - source: 'README.md parity table' - }, - { - category: 'commands', - regex: /^\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*\d+\s*\|(?:\s*\d+\s+prompts\s*\|)?$/im, - source: 'README.md parity table' - }, - { - category: 'skills', - regex: /^\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*(\d+)\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|(?:\s*Via instructions\s*\|)?$/im, - source: 'README.md parity table' - } - ]; - - for (const pattern of parityPatterns) { - const match = readmeContent.match(pattern.regex); - if (!match) { - throw new Error(`${pattern.source} is missing the ${pattern.category} row`); - } - - expectations.push({ - category: pattern.category, - mode: 'exact', - expected: Number(match[1]), - source: `${pattern.source} (${pattern.category})` - }); - } - return expectations; } @@ -439,25 +407,6 @@ function syncEnglishReadme(content, catalog) { (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, 'README.md comparison table (skills)' ); - nextContent = replaceOrThrow( - nextContent, - /^(\|\s*(?:\*\*)?Agents(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*Shared\s*\(AGENTS\.md\)\s*\|\s*12\s*\|(?:\s*N\/A\s*\|)?)$/im, - (_, prefix, __, suffix) => `${prefix}${catalog.agents.count}${suffix}`, - 'README.md parity table (agents)' - ); - nextContent = replaceOrThrow( - nextContent, - /^(\|\s*(?:\*\*)?Commands(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*Instruction-based\s*\|\s*\d+\s*\|(?:\s*\d+\s+prompts\s*\|)?)$/im, - (_, prefix, __, suffix) => `${prefix}${catalog.commands.count}${suffix}`, - 'README.md parity table (commands)' - ); - nextContent = replaceOrThrow( - nextContent, - /^(\|\s*(?:\*\*)?Skills(?:\*\*)?\s*\|\s*)(\d+)(\s*\|\s*Shared\s*\|\s*10\s*\(native format\)\s*\|\s*37\s*\|(?:\s*Via instructions\s*\|)?)$/im, - (_, prefix, __, suffix) => `${prefix}${catalog.skills.count}${suffix}`, - 'README.md parity table (skills)' - ); - return nextContent; } diff --git a/scripts/doctor.js b/scripts/doctor.js index 4341315df..80505d3f6 100644 --- a/scripts/doctor.js +++ b/scripts/doctor.js @@ -3,6 +3,7 @@ const os = require('os'); const { buildDoctorReport } = require('./lib/install-lifecycle'); const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests'); +const { problemReportLines } = require('./lib/feedback-links'); function showHelp(exitCode = 0) { console.log(` @@ -58,6 +59,7 @@ function statusLabel(status) { function printHuman(report) { if (report.results.length === 0) { console.log('No ECC install-state files found for the current home/project context.'); + console.log(`\n${problemReportLines().join('\n')}`); return; } @@ -78,6 +80,10 @@ function printHuman(report) { } console.log(`\nSummary: checked=${report.summary.checkedCount}, ok=${report.summary.okCount}, warnings=${report.summary.warningCount}, errors=${report.summary.errorCount}`); + + if (report.summary.errorCount > 0 || report.summary.warningCount > 0) { + console.log(`\n${problemReportLines().join('\n')}`); + } } function main() { diff --git a/scripts/ecc.js b/scripts/ecc.js index 2ed3d563b..c97b5289c 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -47,6 +47,10 @@ const COMMANDS = { script: 'doctor.js', description: 'Diagnose missing or drifted ECC-managed files', }, + feedback: { + script: 'feedback.js', + description: 'Open the shortest path to report a problem, feedback, or an idea', + }, repair: { script: 'repair.js', description: 'Restore drifted or missing ECC-managed files', @@ -99,6 +103,7 @@ const PRIMARY_COMMANDS = [ 'memory', 'list-installed', 'doctor', + 'feedback', 'repair', 'auto-update', 'status', @@ -152,6 +157,7 @@ Examples: ecc memory search "migration blockers" --target-harness hermes ecc list-installed --json ecc doctor --target cursor + ecc feedback ecc repair --dry-run ecc auto-update --dry-run ecc status --json diff --git a/scripts/feedback.js b/scripts/feedback.js new file mode 100644 index 000000000..e8fe8d836 --- /dev/null +++ b/scripts/feedback.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +const { + FEEDBACK_ROUTES, + getFeedbackPayload, +} = require('./lib/feedback-links'); + +function showHelp() { + process.stdout.write(` +Usage: ecc feedback [--json] [--help|-h] + +Print ECC's low-friction public feedback routes. This command never uploads +diagnostics or reads project files. +`); +} + +function parseArgs(argv) { + return argv.slice(2).reduce((parsed, arg) => { + if (arg === '--json') { + return { ...parsed, json: true }; + } + + if (arg === '--help' || arg === '-h') { + return { ...parsed, help: true }; + } + + throw new Error(`Unknown argument: ${arg}`); + }, { json: false, help: false }); +} + +function printHuman() { + process.stdout.write([ + 'ECC feedback', + '', + `Install or runtime problem:\n${FEEDBACK_ROUTES.problem}`, + '', + `Quick feedback (public GitHub issue):\n${FEEDBACK_ROUTES.feedback}`, + '', + `Feature idea:\n${FEEDBACK_ROUTES.feature}`, + '', + 'ECC does not upload diagnostics or read project files. Redact sensitive information before posting publicly.', + '', + ].join('\n')); +} + +function main() { + try { + const options = parseArgs(process.argv); + if (options.help) { + showHelp(); + return; + } + + if (options.json) { + process.stdout.write(`${JSON.stringify(getFeedbackPayload(), null, 2)}\n`); + } else { + printHuman(); + } + } catch (error) { + process.stderr.write(`Error: ${error.message}\n`); + process.exitCode = 1; + } +} + +main(); diff --git a/scripts/lib/feedback-links.js b/scripts/lib/feedback-links.js new file mode 100644 index 000000000..ba37b0fab --- /dev/null +++ b/scripts/lib/feedback-links.js @@ -0,0 +1,39 @@ +const REPOSITORY_ISSUES_URL = 'https://github.com/affaan-m/ECC/issues/new'; + +const FEEDBACK_ROUTES = Object.freeze({ + problem: `${REPOSITORY_ISSUES_URL}?template=install-problem.yml`, + feedback: `${REPOSITORY_ISSUES_URL}?template=quick-feedback.yml`, + feature: `${REPOSITORY_ISSUES_URL}?template=feature-request.yml`, +}); + +function getFeedbackPayload() { + return { + schemaVersion: 'ecc.feedback.v1', + privacy: 'public-github', + diagnosticsUploaded: false, + routes: { ...FEEDBACK_ROUTES }, + }; +} + +function problemReportLines() { + return [ + 'Report this problem (public GitHub issue):', + FEEDBACK_ROUTES.problem, + 'ECC does not upload diagnostics. Redact paths, repository names, prompts, and secrets before sharing output.', + ]; +} + +function exitFeedbackLines() { + return [ + 'Optional 20-second exit feedback (public GitHub issue):', + FEEDBACK_ROUTES.feedback, + 'ECC does not upload diagnostics or block uninstall.', + ]; +} + +module.exports = { + FEEDBACK_ROUTES, + exitFeedbackLines, + getFeedbackPayload, + problemReportLines, +}; diff --git a/scripts/repair.js b/scripts/repair.js index 74055b524..8386208ef 100644 --- a/scripts/repair.js +++ b/scripts/repair.js @@ -3,6 +3,7 @@ const os = require('os'); const { repairInstalledStates } = require('./lib/install-lifecycle'); const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests'); +const { problemReportLines } = require('./lib/feedback-links'); function showHelp(exitCode = 0) { console.log(` @@ -64,6 +65,10 @@ function printHuman(result) { } console.log(`\nSummary: checked=${result.summary.checkedCount}, ${result.dryRun ? 'planned' : 'repaired'}=${result.dryRun ? result.summary.plannedRepairCount : result.summary.repairedCount}, errors=${result.summary.errorCount}`); + + if (result.summary.errorCount > 0) { + console.log(`\n${problemReportLines().join('\n')}`); + } } function main() { diff --git a/scripts/uninstall.js b/scripts/uninstall.js index c9bdc8598..427ebfc94 100644 --- a/scripts/uninstall.js +++ b/scripts/uninstall.js @@ -3,6 +3,7 @@ const os = require('os'); const { uninstallInstalledStates } = require('./lib/install-lifecycle'); const { SUPPORTED_INSTALL_TARGETS } = require('./lib/install-manifests'); +const { exitFeedbackLines } = require('./lib/feedback-links'); function showHelp(exitCode = 0) { console.log(` @@ -64,6 +65,10 @@ function printHuman(result) { } console.log(`\nSummary: checked=${result.summary.checkedCount}, ${result.dryRun ? 'planned' : 'uninstalled'}=${result.dryRun ? result.summary.plannedRemovalCount : result.summary.uninstalledCount}, errors=${result.summary.errorCount}`); + + if (!result.dryRun) { + console.log(`\n${exitFeedbackLines().join('\n')}`); + } } function main() { diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index 8c34d0029..702ab4cd7 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -498,7 +498,7 @@ function runTests() { cleanupTestDir(testDir); })) passed++; else failed++; - if (test('fails when README parity table counts drift', () => { + if (test('does not require obsolete cross-harness parity counts in README', () => { const testDir = createTestDir(); const { readmePath, @@ -526,11 +526,7 @@ function runTests() { MARKETPLACE_JSON_PATH: marketplaceJsonPath, }); - assert.strictEqual(result.code, 1, 'Should fail when README parity table drifts'); - assert.ok( - (result.stdout + result.stderr).includes('README.md parity table'), - 'Should mention the README parity table mismatch' - ); + assert.strictEqual(result.code, 0, 'Catalog counts should be validated from inventory surfaces, not parity claims'); cleanupTestDir(testDir); })) passed++; else failed++; @@ -622,7 +618,7 @@ function runTests() { assert.ok(readme.includes('|-- agents/ # 1 specialized subagents for delegation'), 'Should sync README project tree agents count'); assert.ok(readme.includes('| Agents | PASS: 1 agents |'), 'Should sync README comparison table'); assert.ok(readme.includes('| Skills | 16 | .agents/skills/ |'), 'Should not rewrite unrelated README tables'); - assert.ok(readme.includes('| **Agents** | 1 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |'), 'Should sync README parity table'); + assert.ok(readme.includes('| **Agents** | 7 | Shared (AGENTS.md) | Shared (AGENTS.md) | 12 |'), 'Should leave obsolete parity prose untouched'); assert.ok(agentsDoc.includes('providing 1 specialized agents, 1 skills, 1 commands'), 'Should sync AGENTS summary'); assert.ok(agentsDoc.includes('skills/ — 1 workflow skills and domain knowledge'), 'Should sync AGENTS structure'); assert.ok(zhRootReadme.includes('你现在可以使用 1 个代理、1 个技能和 1 个命令'), 'Should sync README.zh-CN quick-start summary'); diff --git a/tests/plugin-manifest.test.js b/tests/plugin-manifest.test.js index 0121cfb73..c12ca7476 100644 --- a/tests/plugin-manifest.test.js +++ b/tests/plugin-manifest.test.js @@ -477,13 +477,6 @@ test('.opencode/package-lock.json root version matches package.json', () => { assert.strictEqual(opencodePackageLock.packages[''].version, expectedVersion); }); -test('README version row matches package.json', () => { - const readme = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8'); - const match = readme.match(new RegExp(`^\\| \\*\\*Version\\*\\* \\| Plugin \\| Plugin \\| Reference config \\| (${semverPattern}) \\|(?: Instruction layer \\|)?$`, 'm')); - assert.ok(match, 'Expected README version summary row'); - assert.strictEqual(match[1], expectedVersion); -}); - test('user-facing docs do not use overlong legacy marketplace install commands', () => { const markdownFiles = [ path.join(repoRoot, 'README.md'), diff --git a/tests/scripts/doctor.test.js b/tests/scripts/doctor.test.js index 7cb540da5..bc088b7bd 100644 --- a/tests/scripts/doctor.test.js +++ b/tests/scripts/doctor.test.js @@ -77,6 +77,21 @@ function runTests() { let passed = 0; let failed = 0; + if (test('points users without install state to the guided problem report', () => { + const homeDir = createTempDir('doctor-home-'); + const projectRoot = createTempDir('doctor-project-'); + + try { + const result = run([], { cwd: projectRoot, homeDir }); + assert.strictEqual(result.code, 0, result.stderr); + assert.ok(result.stdout.includes('install-problem.yml')); + assert.ok(result.stdout.includes('does not upload diagnostics')); + } finally { + cleanup(homeDir); + cleanup(projectRoot); + } + })) passed++; else failed++; + if (test('reports a healthy install with exit code 0', () => { const homeDir = createTempDir('doctor-home-'); const projectRoot = createTempDir('doctor-project-'); diff --git a/tests/scripts/ecc.test.js b/tests/scripts/ecc.test.js index b3c757d93..11028eae3 100644 --- a/tests/scripts/ecc.test.js +++ b/tests/scripts/ecc.test.js @@ -75,6 +75,7 @@ function main() { assert.match(result.stdout, /work-items/); assert.match(result.stdout, /platform-audit/); assert.match(result.stdout, /security-ioc-scan/); + assert.match(result.stdout, /feedback/); }], ['delegates explicit install command', () => { const result = runCli(['install', '--dry-run', '--json', 'typescript']); @@ -196,6 +197,13 @@ function main() { assert.strictEqual(result.status, 0, result.stderr); assert.match(result.stdout, /Usage: node scripts\/repair\.js/); }], + ['delegates feedback command', () => { + const result = runCli(['feedback', '--json']); + assert.strictEqual(result.status, 0, result.stderr); + const payload = parseJson(result.stdout); + assert.strictEqual(payload.schemaVersion, 'ecc.feedback.v1'); + assert.strictEqual(payload.diagnosticsUploaded, false); + }], ['supports help for the auto-update subcommand', () => { const result = runCli(['help', 'auto-update']); assert.strictEqual(result.status, 0, result.stderr); diff --git a/tests/scripts/feedback.test.js b/tests/scripts/feedback.test.js new file mode 100644 index 000000000..0944a05f7 --- /dev/null +++ b/tests/scripts/feedback.test.js @@ -0,0 +1,83 @@ +/** + * Tests for scripts/feedback.js + */ + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'feedback.js'); + +function run(args = []) { + return spawnSync('node', [SCRIPT, ...args], { + encoding: 'utf8', + maxBuffer: 1024 * 1024, + }); +} + +function test(name, fn) { + try { + fn(); + console.log(` ✓ ${name}`); + return true; + } catch (error) { + console.log(` ✗ ${name}`); + console.log(` Error: ${error.message}`); + return false; + } +} + +const TEST_CASES = [ + ['prints low-friction feedback routes without collecting diagnostics', () => { + const result = run(); + assert.strictEqual(result.status, 0, result.stderr); + assert.match(result.stdout, /Quick feedback/); + assert.match(result.stdout, /install-problem\.yml/); + assert.match(result.stdout, /quick-feedback\.yml/); + assert.match(result.stdout, /feature-request\.yml/); + assert.match(result.stdout, /public GitHub issue/); + assert.match(result.stdout, /does not upload diagnostics/i); + }], + ['emits machine-readable feedback routes', () => { + const result = run(['--json']); + assert.strictEqual(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.strictEqual(payload.schemaVersion, 'ecc.feedback.v1'); + assert.strictEqual(payload.privacy, 'public-github'); + assert.match(payload.routes.problem, /install-problem\.yml/); + assert.match(payload.routes.feedback, /quick-feedback\.yml/); + assert.match(payload.routes.feature, /feature-request\.yml/); + assert.strictEqual(payload.diagnosticsUploaded, false); + }], + ['documents both help flags and returns after printing help', () => { + for (const flag of ['--help', '-h']) { + const result = run([flag]); + assert.strictEqual(result.status, 0, result.stderr); + assert.match(result.stdout, /Usage: ecc feedback \[--json\] \[--help\|-h\]/); + assert.doesNotMatch(result.stdout, /^ECC feedback$/m); + } + }], + ['lets stdout and stderr flush through natural process exit', () => { + const source = fs.readFileSync(SCRIPT, 'utf8'); + assert.doesNotMatch(source, /process\.exit\(/); + assert.match(source, /process\.exitCode = 1/); + }], + ['rejects unknown arguments', () => { + const result = run(['--send-diagnostics']); + assert.strictEqual(result.status, 1); + assert.match(result.stderr, /Unknown argument/); + }], +]; + +function main() { + console.log('\n=== Testing feedback.js ===\n'); + + const passed = TEST_CASES.filter(([name, fn]) => test(name, fn)).length; + const failed = TEST_CASES.length - passed; + + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + process.exitCode = failed > 0 ? 1 : 0; +} + +main(); diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index 18fc779f8..7648c2b7c 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -42,6 +42,7 @@ function buildExpectedPublishPaths(repoRoot) { const extraPaths = [ "manifests", "scripts/ecc.js", + "scripts/feedback.js", "scripts/catalog.js", "scripts/ci/scan-supply-chain-iocs.js", "scripts/ci/supply-chain-advisory-sources.js", @@ -148,6 +149,7 @@ function main() { "scripts/ci/supply-chain-advisory-sources.js", "scripts/consult.js", "scripts/control-pane.js", + "scripts/feedback.js", "scripts/ito.js", "scripts/memory.js", "scripts/memory-mcp.mjs", diff --git a/tests/scripts/uninstall.test.js b/tests/scripts/uninstall.test.js index 29b794d2a..e31ae3dbe 100644 --- a/tests/scripts/uninstall.test.js +++ b/tests/scripts/uninstall.test.js @@ -109,6 +109,8 @@ function runTests() { }); assert.strictEqual(uninstallResult.code, 0, uninstallResult.stderr); assert.ok(uninstallResult.stdout.includes('Uninstall summary')); + assert.ok(uninstallResult.stdout.includes('quick-feedback.yml')); + assert.ok(uninstallResult.stdout.includes('public GitHub issue')); assert.ok(!fs.existsSync(managedPath)); assert.ok(!fs.existsSync(statePath)); assert.ok(fs.existsSync(unrelatedPath));