Bladeren bron

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 9 maanden geleden
bovenliggende
commit
8c7826c34d
2 gewijzigde bestanden met toevoegingen van 85 en 63 verwijderingen
  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