@@ -942,7 +942,7 @@ func run(args []string) error {
942942 if resolved .Skills .Learn && sm != nil {
943943 // Create LLM client for skill enhancement
944944 skillsLLM := llm .New (resolved .BaseURL , resolved .APIKey , resolved .Model , "" , 30 * time .Second )
945- runLearnLoop (allMessages , f .Task , sm , skillsLLM , resolved .Skills . LLMLearn )
945+ runLearnLoop (allMessages , f .Task , sm , skillsLLM , resolved .Skills )
946946 }
947947
948948 // ── Session end — extract episode if enough turns ──
@@ -1263,7 +1263,9 @@ func getVersion() string {
12631263// enhancement, fires "suggested" events via the SkillManager's notifier,
12641264// and returns the enhanced suggestions for interactive handling by callers.
12651265// This is the non-interactive core shared by CLI, WebUI, and Telegram.
1266- func learnAndSuggest (messages []llm.Message , sm * skills.SkillManager , llmClient skills.LLMClient , llmLearn bool ) []skills.SkillSuggestion {
1266+ // When suppressSuggested is true, "suggested" notifier events are skipped
1267+ // (caller handles presentation, e.g. when auto-save is enabled).
1268+ func learnAndSuggest (messages []llm.Message , sm * skills.SkillManager , llmClient skills.LLMClient , llmLearn , suppressSuggested bool ) []skills.SkillSuggestion {
12671269 // Convert llm.Message to skills.LlmMessage
12681270 skillMsgs := make ([]skills.LlmMessage , 0 , len (messages ))
12691271 for _ , m := range messages {
@@ -1298,50 +1300,115 @@ func learnAndSuggest(messages []llm.Message, sm *skills.SkillManager, llmClient
12981300 }
12991301 }
13001302
1301- // Fire suggested events via notifier
1302- for _ , s := range suggestions {
1303- sm .Notifier .Notify (skills.SkillEvent {
1304- Type : "suggested" ,
1305- SkillName : s .Name ,
1306- Heuristic : s .Heuristic ,
1307- Timestamp : time .Now ().UTC (),
1308- })
1303+ // Fire suggested events via notifier (unless suppressed)
1304+ if ! suppressSuggested {
1305+ for _ , s := range suggestions {
1306+ sm .Notifier .Notify (skills.SkillEvent {
1307+ Type : "suggested" ,
1308+ SkillName : s .Name ,
1309+ Heuristic : s .Heuristic ,
1310+ Body : s .Body ,
1311+ Timestamp : time .Now ().UTC (),
1312+ })
1313+ }
13091314 }
13101315
13111316 return suggestions
13121317}
13131318
1314- func runLearnLoop (messages []llm.Message , task string , sm * skills.SkillManager , llmClient skills.LLMClient , llmLearn bool ) {
1315- suggestions := learnAndSuggest (messages , sm , llmClient , llmLearn )
1319+ func runLearnLoop (messages []llm.Message , task string , sm * skills.SkillManager , llmClient skills.LLMClient , skillsCfg skills. SkillsConfig ) {
1320+ suggestions := learnAndSuggest (messages , sm , llmClient , skillsCfg . LLMLearn , true )
13161321 if len (suggestions ) == 0 {
13171322 return
13181323 }
13191324
1320- fmt .Fprintf (os .Stderr , "\n 🔍 Learning: detected %d skill pattern(s)\n " , len (suggestions ))
1321- for _ , s := range suggestions {
1322- fmt .Fprint (os .Stderr , skills .FormatSuggestion (s ))
1323- fmt .Fprintf (os .Stderr , " Save as skill? [Y/n]: " )
1325+ userDir := expandHome ("~/.odek/skills" )
1326+ os .MkdirAll (userDir , 0755 )
1327+
1328+ // Filter out previously-skipped suggestions
1329+ filtered , skipped := skills .FilterSkipped (suggestions , userDir ,
1330+ skillsCfg .Curation .SkipThreshold , skillsCfg .Curation .SkipResetDays )
1331+ if skipped > 0 {
1332+ fmt .Fprintf (os .Stderr , " (%d suggestion(s) previously skipped, suppressed)\n " , skipped )
1333+ }
1334+ if len (filtered ) == 0 {
1335+ return
1336+ }
1337+
1338+ // Auto-save if enabled
1339+ if skillsCfg .AutoSave .Enabled {
1340+ if ! skillsCfg .AutoSave .RequireLLM || skillsCfg .LLMLearn {
1341+ result := skills .AutoSaveSuggestions (filtered , userDir , skillsCfg )
1342+ for _ , name := range result .Saved {
1343+ heuristic := result .Heuristics [name ]
1344+ if heuristic != "" {
1345+ fmt .Fprintf (os .Stderr , " ✓ Auto-saved skill %q (%s)\n " , name , heuristic )
1346+ } else {
1347+ fmt .Fprintf (os .Stderr , " ✓ Auto-saved skill %q\n " , name )
1348+ }
1349+ sm .Notifier .Notify (skills.SkillEvent {
1350+ Type : "saved" , SkillName : name , Timestamp : time .Now ().UTC (),
1351+ })
1352+ }
1353+ if result .Skipped > 0 {
1354+ fmt .Fprintf (os .Stderr , " (%d previously skipped, suppressed)\n " , result .Skipped )
1355+ }
1356+ for _ , name := range result .Failed {
1357+ fmt .Fprintf (os .Stderr , " ⚠ Quality gate failed for %q (use --no-auto-save to review manually)\n " , name )
1358+ }
1359+ if len (result .Saved ) > 0 {
1360+ sm .Reload ()
1361+ // Run micro-curation after auto-save
1362+ runMicroCuration (userDir , sm , skillsCfg )
1363+ }
1364+ return
1365+ }
1366+ }
1367+
1368+ // Interactive fallback: show preview and prompt
1369+ fmt .Fprintf (os .Stderr , "\n 🔍 Learning: detected %d skill pattern(s)\n " , len (filtered ))
1370+ for _ , s := range filtered {
1371+ fmt .Fprint (os .Stderr , skills .FormatSuggestionWithPreview (s , true , 400 ))
1372+ fmt .Fprintf (os .Stderr , " Save as skill? [Y/n/s=skip always]: " )
13241373
13251374 var response string
13261375 fmt .Scanf ("%s" , & response )
13271376 response = strings .ToLower (strings .TrimSpace (response ))
13281377
13291378 if response == "" || response == "y" || response == "yes" {
1330- userDir := expandHome ("~/.odek/skills" )
1331- os .MkdirAll (userDir , 0755 )
13321379 if err := skills .SaveSuggestion (userDir , s ); err != nil {
13331380 fmt .Fprintf (os .Stderr , " ✗ Error saving skill: %v\n " , err )
13341381 } else {
13351382 fmt .Fprintf (os .Stderr , " ✓ Saved skill %q\n " , s .Name )
1336- // Reload the skill manager to pick up the new skill
13371383 sm .Reload ()
13381384 }
1385+ } else if response == "s" || response == "skip" {
1386+ sl := skills .LoadSkipList (userDir )
1387+ sl .RecordSkip (userDir , s .Name , s .Heuristic )
1388+ fmt .Fprintf (os .Stderr , " Skipped permanently. Use `odek skill reset-skips` to re-enable.\n " )
13391389 } else {
1390+ sl := skills .LoadSkipList (userDir )
1391+ sl .RecordSkip (userDir , s .Name , s .Heuristic )
13401392 fmt .Fprintf (os .Stderr , " Skipped.\n " )
13411393 }
13421394 }
13431395}
13441396
1397+ // runMicroCuration triggers micro-curation after auto-save.
1398+ func runMicroCuration (userDir string , sm * skills.SkillManager , cfg skills.SkillsConfig ) {
1399+ allSkills := sm .AllSkills ()
1400+ var newSkills []skills.Skill
1401+ for _ , s := range allSkills {
1402+ if s .Quality == skills .QualityDraft {
1403+ newSkills = append (newSkills , s )
1404+ }
1405+ }
1406+ result := skills .MicroCuration (userDir , newSkills , allSkills , cfg .Curation )
1407+ if msg := skills .FormatMicroCurationResult (result ); msg != "" {
1408+ fmt .Fprint (os .Stderr , msg )
1409+ }
1410+ }
1411+
13451412// extractUserMessages extracts user message content from llm messages.
13461413func extractUserMessages (messages []llm.Message ) []string {
13471414 var out []string
@@ -1353,10 +1420,10 @@ func extractUserMessages(messages []llm.Message) []string {
13531420 return out
13541421}
13551422
1356- // skillCmd handles `odek skill <list|view|save|delete|import|curate>`.
1423+ // skillCmd handles `odek skill <list|view|save|delete|import|curate|reset-skips >`.
13571424func skillCmd (args []string ) error {
13581425 if len (args ) == 0 {
1359- fmt .Fprintf (os .Stderr , "Usage: odek skill <list|view|save|delete|import|curate> [args]\n " )
1426+ fmt .Fprintf (os .Stderr , "Usage: odek skill <list|view|save|delete|import|curate|reset-skips > [args]\n " )
13601427 return nil
13611428 }
13621429
@@ -1501,8 +1568,24 @@ func skillCmd(args []string) error {
15011568 fmt .Print (skills .FormatCurationReport (report ))
15021569 return nil
15031570
1571+ case "reset-skips" :
1572+ sl := skills .LoadSkipList (userDir )
1573+ if len (subArgs ) == 0 {
1574+ if err := sl .ClearAllSkips (userDir ); err != nil {
1575+ return fmt .Errorf ("reset all skips: %w" , err )
1576+ }
1577+ fmt .Println ("✓ Cleared all skipped suggestions." )
1578+ } else {
1579+ name := subArgs [0 ]
1580+ if err := sl .ClearSkip (userDir , name ); err != nil {
1581+ return fmt .Errorf ("reset skip %q: %w" , name , err )
1582+ }
1583+ fmt .Printf ("✓ Cleared skip for %q.\n " , name )
1584+ }
1585+ return nil
1586+
15041587 default :
1505- return fmt .Errorf ("unknown skill command %q (use list, view, delete, import, curate)" , sub )
1588+ return fmt .Errorf ("unknown skill command %q (use list, view, delete, import, curate, reset-skips )" , sub )
15061589 }
15071590}
15081591
0 commit comments