From 6c99b786ee66733b24ecd372ad841e9503916b9a Mon Sep 17 00:00:00 2001 From: Tommy Tyrnov-Tuchin Date: Tue, 26 May 2026 01:05:33 +0300 Subject: [PATCH] fix(commands): resolve active plugin root in /instinct-status (#2037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `/instinct-status` slash command template expanded `${CLAUDE_PLUGIN_ROOT}` directly and documented a manual-install fallback to `~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py`. When users had both an active plugin install (under `~/.claude/plugins/cache////`) and a legacy `~/.claude/skills/continuous-learning-v2/` directory left over from a previous manual install, an empty `CLAUDE_PLUGIN_ROOT` (which Claude Code does not always populate in slash-command shell contexts) silently made the command read the stale legacy install while the active plugin hooks and observer wrote to the new XDG path. The user saw "No instincts found" while the system was actively learning — exactly the divergence the bug reporter spent hours diagnosing. Replace the brittle two-block template with the same inline resolver pattern that `hooks/hooks.json` and `/sessions` / `/skill-health` already use: env var → standard install → known plugin roots → plugin cache walk → fallback. The resolver is the canonical `INLINE_RESOLVE` constant from `scripts/lib/resolve-ecc-root.js`, so no new code is introduced — just consistent adoption of the existing pattern. Apply the same fix to all five copies of the command: - commands/instinct-status.md (canonical) - .opencode/commands/instinct-status.md - docs/zh-CN/commands/instinct-status.md - docs/ja-JP/commands/instinct-status.md - docs/tr/commands/instinct-status.md Extend tests/lib/command-plugin-root.test.js with an assertion that the canonical instinct-status.md uses the inline resolver and no longer hard-codes the legacy `~/.claude/skills/...` fallback (regression guard). zh-CN copy: polish the Chinese phrasing per LanguageTool feedback (`使用与 ... 相同的解析器` → `以与 ... 相同的解析器`) so the verb is introduced by an explicit preposition instead of reading as an awkward verb-object construction. --- .opencode/commands/instinct-status.md | 15 +++++++-------- commands/instinct-status.md | 16 ++++++++-------- docs/ja-JP/commands/instinct-status.md | 16 ++++++++-------- docs/tr/commands/instinct-status.md | 16 ++++++++-------- docs/zh-CN/commands/instinct-status.md | 14 ++++++-------- tests/lib/command-plugin-root.test.js | 14 ++++++++++++++ 6 files changed, 51 insertions(+), 40 deletions(-) diff --git a/.opencode/commands/instinct-status.md b/.opencode/commands/instinct-status.md index fef3513117..38f9157b5c 100644 --- a/.opencode/commands/instinct-status.md +++ b/.opencode/commands/instinct-status.md @@ -9,16 +9,15 @@ Show instinct status from continuous-learning-v2: $ARGUMENTS ## Your Task -Run: +Resolve the active ECC plugin root with the same walker `hooks/hooks.json` +uses (env var → standard install → known plugin roots → plugin cache → +fallback), then run the instinct CLI. This avoids reading a stale legacy +`~/.claude/skills/continuous-learning-v2/` install when the plugin is +active under `~/.claude/plugins/cache/...` (#2037). ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status -``` - -If `CLAUDE_PLUGIN_ROOT` is unavailable, use: - -```bash -python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status +ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplaces','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplaces','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}" +python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status ``` ## Behavior Notes diff --git a/commands/instinct-status.md b/commands/instinct-status.md index c54f80222c..33682ea582 100644 --- a/commands/instinct-status.md +++ b/commands/instinct-status.md @@ -10,16 +10,16 @@ Shows learned instincts for the current project plus global instincts, grouped b ## Implementation -Run the instinct CLI using the plugin root path: +Run the instinct CLI, resolving the active ECC plugin root the same way +`hooks/hooks.json` and the other slash commands (`/sessions`, `/skill-health`) +do — env var → standard install → known plugin roots → plugin cache → fallback. +This avoids the divergence that happens when `CLAUDE_PLUGIN_ROOT` is unset +while a legacy `~/.claude/skills/continuous-learning-v2/` directory still +exists (#2037). ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status -``` - -Or if `CLAUDE_PLUGIN_ROOT` is not set (manual installation), use: - -```bash -python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status +ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplaces','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplaces','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}" +python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status ``` ## Usage diff --git a/docs/ja-JP/commands/instinct-status.md b/docs/ja-JP/commands/instinct-status.md index af87a1deaa..a140a621ab 100644 --- a/docs/ja-JP/commands/instinct-status.md +++ b/docs/ja-JP/commands/instinct-status.md @@ -10,16 +10,16 @@ command: true ## 実装 -プラグインルートパスを使用してインスティンクトCLIを実行します: +`hooks/hooks.json` および他のスラッシュコマンド(`/sessions`、`/skill-health`) +と同じリゾルバ(環境変数 → 標準インストール → 既知のプラグインルート → +プラグインキャッシュ → フォールバック)でインスティンクトCLIを実行します。 +これにより、`CLAUDE_PLUGIN_ROOT` が未設定で +レガシーの `~/.claude/skills/continuous-learning-v2/` ディレクトリが +残っているときに発生するパスの分岐を回避します (#2037)。 ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status -``` - -または、`CLAUDE_PLUGIN_ROOT` が設定されていない場合(手動インストール)の場合は: - -```bash -python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status +ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplaces','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplaces','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}" +python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status ``` ## 使用方法 diff --git a/docs/tr/commands/instinct-status.md b/docs/tr/commands/instinct-status.md index be334acbda..a35a4af7b5 100644 --- a/docs/tr/commands/instinct-status.md +++ b/docs/tr/commands/instinct-status.md @@ -10,16 +10,16 @@ Mevcut proje için öğrenilen içgüdüleri ve global içgüdüleri, domain'e g ## Uygulama -Plugin root path kullanarak instinct CLI'ı çalıştır: +`hooks/hooks.json` ve diğer slash komutlarının (`/sessions`, `/skill-health`) +kullandığı çözümleyiciyle (env var → standart kurulum → bilinen plugin +kökleri → plugin önbelleği → fallback) instinct CLI'ı çalıştır. +Bu, `CLAUDE_PLUGIN_ROOT` ayarlanmamışken eski bir +`~/.claude/skills/continuous-learning-v2/` dizini hâlâ varsa oluşan +yol sapmasını önler (#2037). ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status -``` - -Veya `CLAUDE_PLUGIN_ROOT` ayarlanmamışsa (manuel kurulum): - -```bash -python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status +ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplaces','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplaces','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}" +python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status ``` ## Kullanım diff --git a/docs/zh-CN/commands/instinct-status.md b/docs/zh-CN/commands/instinct-status.md index 86da92e3e4..110cedba7d 100644 --- a/docs/zh-CN/commands/instinct-status.md +++ b/docs/zh-CN/commands/instinct-status.md @@ -10,16 +10,14 @@ command: true ## 实现 -使用插件根路径运行本能 CLI: +以与 `hooks/hooks.json` 和其他斜杠命令(`/sessions`、`/skill-health`) +相同的解析器运行本能 CLI——环境变量 → 标准安装 → 已知插件根 → 插件缓存 → 回退。 +这样可以避免当 `CLAUDE_PLUGIN_ROOT` 未设置而旧的 +`~/.claude/skills/continuous-learning-v2/` 目录仍然存在时发生的路径分歧 (#2037)。 ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/continuous-learning-v2/scripts/instinct-cli.py" status -``` - -或者,如果未设置 `CLAUDE_PLUGIN_ROOT`(手动安装),则使用: - -```bash -python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py status +ECC_ROOT="${CLAUDE_PLUGIN_ROOT:-$(node -e "var r=(()=>{var e=process.env.CLAUDE_PLUGIN_ROOT;if(e&&e.trim())return e.trim();var p=require('path'),f=require('fs'),h=require('os').homedir(),d=p.join(h,'.claude'),q=p.join('scripts','lib','utils.js');if(f.existsSync(p.join(d,q)))return d;for(var s of [['ecc'],['ecc@ecc'],['marketplaces','ecc'],['everything-claude-code'],['everything-claude-code@everything-claude-code'],['marketplaces','everything-claude-code']]){var l=p.join(d,'plugins',...s);if(f.existsSync(p.join(l,q)))return l}try{for(var g of ['ecc','everything-claude-code']){var b=p.join(d,'plugins','cache',g);for(var o of f.readdirSync(b,{withFileTypes:true})){if(!o.isDirectory())continue;for(var v of f.readdirSync(p.join(b,o.name),{withFileTypes:true})){if(!v.isDirectory())continue;var c=p.join(b,o.name,v.name);if(f.existsSync(p.join(c,q)))return c}}}}catch(x){}return d})();console.log(r)")}" +python3 "$ECC_ROOT/skills/continuous-learning-v2/scripts/instinct-cli.py" status ``` ## 用法 diff --git a/tests/lib/command-plugin-root.test.js b/tests/lib/command-plugin-root.test.js index 78362fef38..5960be9d4c 100644 --- a/tests/lib/command-plugin-root.test.js +++ b/tests/lib/command-plugin-root.test.js @@ -22,6 +22,7 @@ function test(name, fn) { const sessionsDoc = fs.readFileSync(path.join(__dirname, '..', '..', 'commands', 'sessions.md'), 'utf8'); const skillHealthDoc = fs.readFileSync(path.join(__dirname, '..', '..', 'commands', 'skill-health.md'), 'utf8'); +const instinctStatusDoc = fs.readFileSync(path.join(__dirname, '..', '..', 'commands', 'instinct-status.md'), 'utf8'); test('sessions command uses shared inline resolver in all node scripts', () => { assert.strictEqual((sessionsDoc.match(/const _r = /g) || []).length, 6); @@ -37,6 +38,19 @@ test('skill-health command uses shared inline resolver in all shell snippets', ( assert.strictEqual((skillHealthDoc.match(/\['ecc','everything-claude-code'\]/g) || []).length, 3); }); +test('instinct-status command uses shared inline resolver (no stale legacy fallback) (#2037)', () => { + assert.strictEqual((instinctStatusDoc.match(/var r=/g) || []).length, 1); + assert.strictEqual((instinctStatusDoc.match(/\['marketplaces','ecc'\]/g) || []).length, 1); + assert.strictEqual((instinctStatusDoc.match(/\['marketplaces','everything-claude-code'\]/g) || []).length, 1); + assert.strictEqual((instinctStatusDoc.match(/\['ecc','everything-claude-code'\]/g) || []).length, 1); + // The pre-fix template hard-coded the legacy path as a fallback when + // CLAUDE_PLUGIN_ROOT was unset. Asserting its absence prevents regression. + assert.ok( + !instinctStatusDoc.includes('python3 ~/.claude/skills/continuous-learning-v2/scripts/instinct-cli.py'), + 'instinct-status should not hard-code the legacy ~/.claude install path as a fallback' + ); +}); + test('inline resolver covers current and legacy marketplace plugin roots', () => { assert.ok(INLINE_RESOLVE.includes("'marketplaces','ecc'")); assert.ok(INLINE_RESOLVE.includes("'marketplaces','everything-claude-code'"));