فهرست منبع

fix(opencode): use session.created event for bootstrap injection

Switch from chat.message hook to session.created event for injecting
the using-superpowers skill content. The new approach:

- Injects at session creation via session.prompt() with noReply: true
- Explicitly tells model the skill is already loaded to prevent
  redundant use_skill calls
- Consolidates bootstrap generation into getBootstrapContent() helper
- Removes fallback pattern in favor of single implementation

Tested with 10 consecutive test runs and manual skill trigger validation.
Jesse Vincent 10 ماه پیش
والد
کامیت
8c7826c34d
2فایلهای تغییر یافته به همراه85 افزوده شده و 63 حذف شده
  1. 73 63
      .opencode/plugin/superpowers.js
  2. 12 0
      RELEASE-NOTES.md

+ 73 - 63
.opencode/plugin/superpowers.js

@@ -21,6 +21,61 @@ export const SuperpowersPlugin = async ({ client, directory }) => {
   const superpowersSkillsDir = path.resolve(__dirname, '../../skills');
   const personalSkillsDir = path.join(homeDir, '.config/opencode/skills');
 
+  // Helper to generate bootstrap content
+  const getBootstrapContent = (compact = false) => {
+    const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
+    if (!usingSuperpowersPath) return null;
+
+    const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
+    const content = skillsCore.stripFrontmatter(fullContent);
+
+    const toolMapping = compact
+      ? `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
+
+**Skills naming (priority order):** project: > personal > superpowers:`
+      : `**Tool Mapping for OpenCode:**
+When skills reference tools you don't have, substitute OpenCode equivalents:
+- \`TodoWrite\` → \`update_plan\`
+- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
+- \`Skill\` tool → \`use_skill\` custom tool
+- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
+
+**Skills naming (priority order):**
+- Project skills: \`project:skill-name\` (in .opencode/skills/)
+- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
+- Superpowers skills: \`superpowers:skill-name\`
+- Project skills override personal, which override superpowers when names match`;
+
+    return `<EXTREMELY_IMPORTANT>
+You have superpowers.
+
+**IMPORTANT: The using-superpowers skill content is included below. It is ALREADY LOADED - you are currently following it. Do NOT use the use_skill tool to load "using-superpowers" - that would be redundant. Use use_skill only for OTHER skills.**
+
+${content}
+
+${toolMapping}
+</EXTREMELY_IMPORTANT>`;
+  };
+
+  // Helper to inject bootstrap via session.prompt
+  const injectBootstrap = async (sessionID, compact = false) => {
+    const bootstrapContent = getBootstrapContent(compact);
+    if (!bootstrapContent) return false;
+
+    try {
+      await client.session.prompt({
+        path: { id: sessionID },
+        body: {
+          noReply: true,
+          parts: [{ type: "text", text: bootstrapContent }]
+        }
+      });
+      return true;
+    } catch (err) {
+      return false;
+    }
+  };
+
   return {
     tool: {
       use_skill: tool({
@@ -132,72 +187,27 @@ export const SuperpowersPlugin = async ({ client, directory }) => {
         }
       })
     },
-    "chat.message": async (input, output) => {
-      // Only inject on first message of session (or every message if needed)
-      if (!output.message.system || output.message.system.length === 0) {
-        const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
-
-        if (usingSuperpowersPath) {
-          const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
-          const usingSuperpowersContent = skillsCore.stripFrontmatter(fullContent);
-
-          const toolMapping = `**Tool Mapping for OpenCode:**
-When skills reference tools you don't have, substitute OpenCode equivalents:
-- \`TodoWrite\` → \`update_plan\`
-- \`Task\` tool with subagents → Use OpenCode's subagent system (@mention)
-- \`Skill\` tool → \`use_skill\` custom tool
-- \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → Your native tools
-
-**Skills naming (priority order):**
-- Project skills: \`project:skill-name\` (in .opencode/skills/)
-- Personal skills: \`skill-name\` (in ~/.config/opencode/skills/)
-- Superpowers skills: \`superpowers:skill-name\`
-- Project skills override personal, which override superpowers when names match`;
-
-          output.message.system = `<EXTREMELY_IMPORTANT>
-You have superpowers.
-
-${usingSuperpowersContent}
-
-${toolMapping}
-</EXTREMELY_IMPORTANT>`;
+    event: async ({ event }) => {
+      // Extract sessionID from various event structures
+      const getSessionID = () => {
+        return event.properties?.info?.id ||
+               event.properties?.sessionID ||
+               event.session?.id;
+      };
+
+      // Inject bootstrap at session creation (before first user message)
+      if (event.type === 'session.created') {
+        const sessionID = getSessionID();
+        if (sessionID) {
+          await injectBootstrap(sessionID, false);
         }
       }
-    },
-    event: async ({ event }) => {
-      // Re-inject bootstrap after context compaction to maintain superpowers
-      if (event.type === 'session.compacted') {
-        const usingSuperpowersPath = skillsCore.resolveSkillPath('using-superpowers', superpowersSkillsDir, personalSkillsDir);
-
-        if (usingSuperpowersPath) {
-          const fullContent = fs.readFileSync(usingSuperpowersPath.skillFile, 'utf8');
-          const content = skillsCore.stripFrontmatter(fullContent);
-
-          const toolMapping = `**Tool Mapping:** TodoWrite->update_plan, Task->@mention, Skill->use_skill
-
-**Skills naming (priority order):** project: > personal > superpowers:`;
-
-          try {
-            await client.session.prompt({
-              path: { id: event.properties.sessionID },
-              body: {
-                noReply: true,
-                parts: [{
-                  type: "text",
-                  text: `<EXTREMELY_IMPORTANT>
-You have superpowers.
 
-${content}
-
-${toolMapping}
-</EXTREMELY_IMPORTANT>`
-                }]
-              }
-            });
-          } catch (err) {
-            // Silent failure - bootstrap will be missing but session continues
-            console.error('Failed to re-inject superpowers after compaction:', err.message);
-          }
+      // Re-inject bootstrap after context compaction (compact version to save tokens)
+      if (event.type === 'session.compacted') {
+        const sessionID = getSessionID();
+        if (sessionID) {
+          await injectBootstrap(sessionID, true);
         }
       }
     }

+ 12 - 0
RELEASE-NOTES.md

@@ -1,5 +1,17 @@
 # Superpowers Release Notes
 
+## v3.5.1 (2025-11-24)
+
+### Changed
+
+- **OpenCode Bootstrap Refactor**: Switched from `chat.message` hook to `session.created` event for bootstrap injection
+  - Bootstrap now injects at session creation via `session.prompt()` with `noReply: true`
+  - Explicitly tells the model that using-superpowers is already loaded to prevent redundant skill loading
+  - Consolidated bootstrap content generation into shared `getBootstrapContent()` helper
+  - Cleaner single-implementation approach (removed fallback pattern)
+
+---
+
 ## v3.5.0 (2025-11-23)
 
 ### Added