Skip to content

feat: automatic skill detection and extraction prompt injection#2

Open
bhodgens wants to merge 2 commits intodeiviuds:mainfrom
bhodgens:feat/auto-skill-detection
Open

feat: automatic skill detection and extraction prompt injection#2
bhodgens wants to merge 2 commits intodeiviuds:mainfrom
bhodgens:feat/auto-skill-detection

Conversation

@bhodgens
Copy link

Summary

Adds automatic skill detection to opencode-brain. At session end, the plugin analyzes observations for patterns that suggest extractable, reusable knowledge. Detected candidates are staged to disk and injected into the next session's system prompt, prompting the agent to evaluate and extract them as SKILL.md files.

This enables a continuous learning loop: work produces observations -> observations are analyzed for patterns -> candidates are surfaced to the agent -> agent creates reusable skills.

How it works

Session N (detection):

  1. Session ends (session.idle)
  2. Skill detector scans observations for 4 heuristic patterns:
    • Problem-Solution pairs — related by file or content overlap, boosted if non-obvious signals present
    • Error-Fix sequences — specific error messages followed by resolution
    • Deep investigations — 5+ observations on the same file with both problems and fixes
    • Non-obvious discoveries — observations containing signals like "turns out", "root cause", "the trick"
  3. Candidates are written to .claude/mind-skills-pending.json

Session N+1 (injection):

  1. On first experimental.chat.system.transform call, plugin checks for pending candidates
  2. If found, injects an <openception-skill-extraction> block into the system prompt
  3. Agent evaluates candidates and extracts skills (or dismisses them)
  4. At session end, staging file is cleared

New files

  • src/hooks/skill-detector.ts — 4 pattern detectors with confidence scoring (high/medium), dedup, capped at 5 candidates per session
  • src/utils/skill-staging.ts — Read/write/merge/clear pending candidates on disk, cross-session stacking with dedup, cap at 10 total

Modified files

  • src/hooks/session-end.ts — Calls skill detector after session summary generation
  • src/plugin.ts — Injects pending candidates into system prompt (once per session), clears staging at session end

Design decisions

  • Detection in plugin code, extraction by the agent: The plugin does cheap heuristic pattern matching. Actual skill writing requires LLM reasoning (quality evaluation, description writing, dedup checking), so that's delegated to the agent via prompt injection.
  • Cross-session staging: Candidates persist across sessions. If the user runs multiple sessions without the candidates being injected (e.g., using a different tool), they stack up to 10 and get deduped by title.
  • One injection per session: The skillCandidatesInjected flag ensures candidates are injected exactly once per plugin lifecycle, not on every message.

Depends on

@gemini-code-assist
Copy link

Summary of Changes

Hello @bhodgens, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the opencode-brain plugin by introducing an automated skill detection and extraction system. It establishes a continuous learning loop where the plugin identifies potential skills from an agent's session observations, stages them to disk, and then injects these candidates into subsequent system prompts. This process empowers the agent to autonomously evaluate and formalize these insights into reusable SKILL.md files, fostering a self-improving AI development environment.

Highlights

  • Automatic Skill Detection: A new mechanism was implemented to automatically detect potential skills from session observations using four heuristic patterns: Problem-Solution pairs, Error-Fix sequences, Deep Investigations, and Non-obvious Discoveries. These patterns are scored for confidence (high/medium).
  • Cross-Session Skill Staging: Detected skill candidates are now saved to disk in .claude/mind-skills-pending.json at the end of a session. This allows candidates to persist across multiple sessions, with deduplication and a cap of 10 total candidates.
  • System Prompt Injection: Pending skill candidates are injected into the agent's system prompt at the beginning of the next session. This prompts the agent to evaluate and extract them as SKILL.md files, enabling a continuous learning loop.
  • Agent-Driven Extraction: The plugin performs cheap heuristic pattern matching to identify candidates, but the actual skill writing, quality evaluation, description, and deduplication are delegated to the agent via prompt injection, leveraging its LLM reasoning capabilities.
  • Plugin Export Simplification: The main index.ts file was refactored to export only the default plugin, streamlining its integration and ensuring compatibility with the OpenCode plugin loader.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/hooks/session-end.ts
    • Imported detectSkillCandidates and savePendingCandidates for skill processing.
    • Added logic to detect and save skill candidates after the session summary has been generated.
  • src/hooks/skill-detector.ts
    • Added a new file to implement the core logic for detecting skill candidates.
    • Defined SkillCandidate interface and ObsWindow for observation classification.
    • Included signal arrays for identifying errors, fixes, and non-obvious discoveries within observations.
    • Developed helper functions such as classify, hasNonObviousSignals, extractTitle, relatedByFile, relatedByContent, and toEvidence.
    • Implemented four distinct skill detection patterns with associated confidence scoring (high/medium).
    • Incorporated logic for deduplicating candidates and capping the number of candidates per session.
  • src/index.ts
    • Refactored the module's exports to only include the default export from plugin.ts.
    • Removed direct exports of various types, core components, and utility functions.
  • src/plugin.ts
    • Imported loadPendingCandidates, markInjected, clearPendingCandidates, and the SkillCandidate type.
    • Added formatSkillCandidatesForSystem function to structure skill candidates for system prompt injection.
    • Introduced a skillCandidatesInjected state variable to control injection frequency.
    • Modified the session.idle event handler to clear pending candidates from staging if they were previously injected.
    • Implemented logic within experimental.chat.system.transform to load and inject pending skill candidates into the system prompt once per session, and to mark them as injected.
  • src/utils/skill-staging.ts
    • Added a new file to manage the persistence of skill candidates.
    • Defined STAGING_FILE constant and StagedCandidates interface for data structure.
    • Implemented functions for stagingPath, loadPendingCandidates, savePendingCandidates, markInjected, and clearPendingCandidates to handle file operations.
    • Developed mergeWithExisting function for combining new and existing skill candidates while ensuring deduplication and adherence to the maximum cap.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Summary by CodeRabbit

  • New Features
    • Automatically detects and scores learnable patterns from session observations, including problem-solution pairs, error fixes, and discovery sequences
    • Persists detected skills with evidence tracking for carryover across sessions
    • Integrates learned patterns into system prompts to enhance future AI interactions

Walkthrough

The changes implement a skill detection and staging system that identifies potential skills from session observations, stages them persistently, and injects them into system prompts. Simultaneously, the public API surface is simplified by removing most re-exports from the main index file.

Changes

Cohort / File(s) Summary
Skill Detection & Staging
src/hooks/skill-detector.ts, src/utils/skill-staging.ts
Introduces skill candidate detection with pattern-based classification (problem-solution pairs, error-fix pairs, deep investigations, non-obvious discoveries) and persistent staging with deduplication, merging, and capacity limits (max 10 candidates).
Session Integration
src/hooks/session-end.ts
Detects skill-worthy patterns at session end via detectSkillCandidates and stages candidates using savePendingCandidates for subsequent sessions.
System Prompt Injection
src/plugin.ts
Adds skill candidate injection workflow with per-session state tracking; loads candidates, formats them into system prompt, and marks as injected exactly once per session; clears pending candidates on session idle.
Public API Simplification
src/index.ts
Removes re-exports of types, utilities, and core functions (Observation, Mind, formatTimestamp, etc.), retaining only the default export from plugin.js.

Sequence Diagram

sequenceDiagram
    participant Session as Session Handler
    participant Detector as Skill Detector
    participant Staging as Staging Manager
    participant Plugin as Plugin/Prompt
    participant Next as Next Session

    Session->>Detector: detectSkillCandidates(observations, sessionId)
    Detector->>Detector: Classify observations into patterns<br/>(problems, solutions, errors, fixes, etc.)
    Detector->>Detector: Generate candidates via 4 patterns<br/>(Problem→Solution, Error→Fix, etc.)
    Detector->>Detector: Deduplicate & sort by confidence
    Detector-->>Session: Return SkillCandidate[]

    Session->>Staging: savePendingCandidates(directory, candidates)
    Staging->>Staging: Load existing staged data
    Staging->>Staging: Merge new with un-injected candidates
    Staging->>Staging: Deduplicate by title, cap at 10
    Staging-->>Session: Persist staging file

    Note over Plugin: On next session start
    Plugin->>Staging: loadPendingCandidates(directory)
    Staging-->>Plugin: Return StagedCandidates
    Plugin->>Plugin: Format candidates for system prompt
    Plugin->>Staging: markInjected(directory)
    Next->>Next: System prompt includes skill context

    Note over Staging: On session idle
    Session->>Staging: clearPendingCandidates(directory)
    Staging-->>Session: Remove staging file
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature added: automatic skill detection and extraction via prompt injection.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the detection mechanism, injection workflow, new files, modifications, and design decisions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin.ts (1)

96-98: ⚠️ Potential issue | 🔴 Critical

Bug: skillCandidatesInjected is not reset on session.created, so injection only works for the first session in the plugin's lifecycle.

sessionSummaryGenerated is correctly reset to false on session.created (line 97), but skillCandidatesInjected is not. If the plugin process survives across sessions, candidates staged by Session N's handleSessionEnd will never be injected into Session N+1's system prompt because the flag remains true from Session N.

🐛 Proposed fix
       if (event.type === "session.created") {
         sessionSummaryGenerated = false
+        skillCandidatesInjected = false
         const memoryPath = resolve(directory, ".claude/mind.mv2")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin.ts` around lines 96 - 98, The flag skillCandidatesInjected must be
reset on new sessions like sessionSummaryGenerated is; in the event.type ===
"session.created" branch set skillCandidatesInjected = false so candidates
staged by handleSessionEnd can be injected into the next session's system
prompt—locate the session.created handling block in plugin.ts (where
sessionSummaryGenerated is reset) and add the reset of skillCandidatesInjected
there.
🧹 Nitpick comments (4)
src/utils/skill-staging.ts (1)

59-61: Unnecessary dynamic import of unlink — it can be statically imported at line 1.

readFile, writeFile, and mkdir are already statically imported from "node:fs/promises" at line 1. The dynamic import("node:fs/promises") for unlink is inconsistent and adds needless overhead.

♻️ Proposed fix
-import { readFile, writeFile, mkdir } from "node:fs/promises"
+import { readFile, writeFile, mkdir, unlink } from "node:fs/promises"

Then in clearPendingCandidates:

   try {
-    const { unlink } = await import("node:fs/promises")
     await unlink(path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/skill-staging.ts` around lines 59 - 61, In clearPendingCandidates,
remove the dynamic import("node:fs/promises") and instead add unlink to the
existing static imports (alongside readFile, writeFile, mkdir) at the top; then
call unlink(path) directly in the try block — this eliminates the unnecessary
runtime import and keeps imports consistent for unlink, readFile, writeFile, and
mkdir.
src/hooks/skill-detector.ts (2)

135-154: Timestamp guard uses <=, which excludes distinct problem→solution pairs that share the same timestamp.

Line 137 skips when solution.timestamp <= problem.timestamp. If two distinct observations occur in the same millisecond (or both lack timestamps and default to 0), a valid pair is missed. This is a conservative tradeoff; just noting it in case rapid-fire observations are common.

♻️ Alternative: use strict `<` and add identity check
-      if ((solution.timestamp ?? 0) <= (problem.timestamp ?? 0)) continue
+      if (solution === problem) continue
+      if ((solution.timestamp ?? 0) < (problem.timestamp ?? 0)) continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/skill-detector.ts` around lines 135 - 154, The timestamp guard in
the nested loops over win.problems and win.solutions (checking
(solution.timestamp ?? 0) <= (problem.timestamp ?? 0)) incorrectly excludes
distinct problem→solution pairs that share the exact timestamp; change the
comparison to a strict `<` and add an identity check so pairs with equal
timestamps are allowed when solution and problem are not the same observation
(e.g., compare unique identifiers or object identity of solution vs problem
before continuing). Update the condition in the loop where (solution.timestamp
?? 0) and (problem.timestamp ?? 0) are compared to use `<` plus a check like
solution !== problem or solution.id !== problem.id to preserve ordering while
not dropping same-timestamp distinct matches.

50-66: Observation double-classification: an observation can be both a problem and a solution.

An observation of type "solution" or "bugfix" whose content contains an ERROR_SIGNAL word (e.g., "Fixed the error in auth") will be pushed into win.problems, win.errors, win.solutions, and potentially win.fixes. This is handled downstream — Pattern 1's timestamp guard (<=) prevents self-pairing since solution.timestamp === problem.timestamp for the same object — but it's subtle and worth a brief inline comment for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/skill-detector.ts` around lines 50 - 66, The current classification
loop can push the same observation into both problem/error and solution/fix
buckets (e.g., a "solution" whose text contains an ERROR_SIGNAL); add a concise
inline comment near the logic in the loop (around the checks that use
observations, ERROR_SIGNALS, FIX_SIGNALS and the pushes to win.problems,
win.errors, win.solutions, win.fixes) explaining that double-classification is
intentional and safe because downstream pairing logic (Pattern 1) uses the
timestamp guard (<=) to avoid self-pairing for the same observation; this will
help future maintainers understand why we allow overlapping pushes rather than
trying to de-duplicate here.
src/plugin.ts (1)

156-166: Setting skillCandidatesInjected = true before the load attempt prevents retry on transient failure.

If loadPendingCandidates throws or returns null due to a transient I/O error, the flag is already true and no further attempts will be made for the rest of the session. Consider moving the flag assignment to after the successful injection.

♻️ Suggested reorder
         if (!skillCandidatesInjected) {
-          skillCandidatesInjected = true
           const staged = await loadPendingCandidates(directory)
           if (staged?.candidates?.length && !staged.injectedAt) {
             output.system.push(formatSkillCandidatesForSystem(staged.candidates))
             await markInjected(directory)
+            skillCandidatesInjected = true
             debug(`Injected ${staged.candidates.length} skill candidate(s) into system prompt`)
+          } else {
+            skillCandidatesInjected = true // no candidates to inject, stop checking
           }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin.ts` around lines 156 - 166, The code sets skillCandidatesInjected
= true before attempting loadPendingCandidates, which prevents retries on
transient failures; modify the flow in the block that references
skillCandidatesInjected, loadPendingCandidates, formatSkillCandidatesForSystem,
markInjected, output.system, and debug so the flag is only set after a
successful injection: call loadPendingCandidates(directory) first, check staged
is non-null and staged.candidates.length > 0 and !staged.injectedAt, push the
formatted candidates to output.system, await markInjected(directory), call
debug(...), and only then set skillCandidatesInjected = true; ensure the flag is
not set if loadPendingCandidates throws or returns invalid data so retries can
occur later in the session.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/plugin.ts`:
- Around line 96-98: The flag skillCandidatesInjected must be reset on new
sessions like sessionSummaryGenerated is; in the event.type ===
"session.created" branch set skillCandidatesInjected = false so candidates
staged by handleSessionEnd can be injected into the next session's system
prompt—locate the session.created handling block in plugin.ts (where
sessionSummaryGenerated is reset) and add the reset of skillCandidatesInjected
there.

---

Nitpick comments:
In `@src/hooks/skill-detector.ts`:
- Around line 135-154: The timestamp guard in the nested loops over win.problems
and win.solutions (checking (solution.timestamp ?? 0) <= (problem.timestamp ??
0)) incorrectly excludes distinct problem→solution pairs that share the exact
timestamp; change the comparison to a strict `<` and add an identity check so
pairs with equal timestamps are allowed when solution and problem are not the
same observation (e.g., compare unique identifiers or object identity of
solution vs problem before continuing). Update the condition in the loop where
(solution.timestamp ?? 0) and (problem.timestamp ?? 0) are compared to use `<`
plus a check like solution !== problem or solution.id !== problem.id to preserve
ordering while not dropping same-timestamp distinct matches.
- Around line 50-66: The current classification loop can push the same
observation into both problem/error and solution/fix buckets (e.g., a "solution"
whose text contains an ERROR_SIGNAL); add a concise inline comment near the
logic in the loop (around the checks that use observations, ERROR_SIGNALS,
FIX_SIGNALS and the pushes to win.problems, win.errors, win.solutions,
win.fixes) explaining that double-classification is intentional and safe because
downstream pairing logic (Pattern 1) uses the timestamp guard (<=) to avoid
self-pairing for the same observation; this will help future maintainers
understand why we allow overlapping pushes rather than trying to de-duplicate
here.

In `@src/plugin.ts`:
- Around line 156-166: The code sets skillCandidatesInjected = true before
attempting loadPendingCandidates, which prevents retries on transient failures;
modify the flow in the block that references skillCandidatesInjected,
loadPendingCandidates, formatSkillCandidatesForSystem, markInjected,
output.system, and debug so the flag is only set after a successful injection:
call loadPendingCandidates(directory) first, check staged is non-null and
staged.candidates.length > 0 and !staged.injectedAt, push the formatted
candidates to output.system, await markInjected(directory), call debug(...), and
only then set skillCandidatesInjected = true; ensure the flag is not set if
loadPendingCandidates throws or returns invalid data so retries can occur later
in the session.

In `@src/utils/skill-staging.ts`:
- Around line 59-61: In clearPendingCandidates, remove the dynamic
import("node:fs/promises") and instead add unlink to the existing static imports
(alongside readFile, writeFile, mkdir) at the top; then call unlink(path)
directly in the try block — this eliminates the unnecessary runtime import and
keeps imports consistent for unlink, readFile, writeFile, and mkdir.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an automatic skill detection and extraction feature, which is a significant enhancement to the opencode-brain plugin. The implementation is well-structured, separating concerns into skill-detector.ts for heuristic pattern matching, skill-staging.ts for cross-session persistence, and integrating these into session-end.ts and plugin.ts. The logic for detecting skill candidates, handling cross-session staging, and injecting them into the system prompt for agent evaluation is robust and thoughtfully designed. The changes to src/index.ts to export only the default plugin align with the plugin loader compatibility requirements. Overall, this is a valuable addition that enables a continuous learning loop for the agent.

if (!existsSync(path)) return

try {
const { unlink } = await import("node:fs/promises")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dynamic import for unlink on this line is generally less performant and can make code analysis harder compared to a static import. For a utility that is always needed when this function is called, it's better to use a static import at the top of the file for clarity and consistency.

import { readFile, writeFile, mkdir, unlink } from "node:fs/promises"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant