@@ -237,10 +237,12 @@ func printUsage() {
237237
238238Commands:
239239 run Execute a task with the agent loop
240+ run --learn Execute with skill learning (detects patterns, suggests skills)
240241 run --session Execute and save conversation as a session
241242 continue Continue the most recent session (or by --id)
242243 repl Interactive REPL mode (multi-turn session)
243244 session Manage sessions: list, show, delete, trim, cleanup
245+ skill Manage skills: list, view, save, delete, import, curate
244246 init Create a config file (default: ./kode.json)
245247 version Print version and exit
246248
@@ -260,8 +262,17 @@ Run flags:
260262 --no-color Disable colored terminal output
261263 --no-agents Skip loading AGENTS.md from working directory
262264 --session Save conversation as a multi-turn session
265+ --learn Enable skill learning mode (detects patterns, saves skills)
263266 --system <prompt> System prompt override
264267
268+ Skill commands:
269+ kode skill list List all available skills
270+ kode skill view <name> View a skill's full content
271+ kode skill delete <name> Delete a skill
272+ kode skill import <uri> [flags] Import a skill from file:// or https://
273+ Flags: --basic (skip LLM), --yes (auto-approve)
274+ kode skill curate Analyze skills for quality, staleness, overlap
275+
265276Sandbox flags:
266277 --sandbox Run in isolated Docker container
267278 --sandbox-image <img> Docker image (default: alpine:latest)
@@ -606,6 +617,10 @@ func run(args []string) error {
606617
607618 rend .Start (f .Task )
608619
620+ // Shared agent run — capture messages for --learn mode
621+ var allMessages []llm.Message
622+ var runErr error
623+
609624 if f .Session != nil && * f .Session {
610625 // Multi-turn session mode: save conversation history
611626 messages := []llm.Message {
@@ -615,30 +630,43 @@ func run(args []string) error {
615630 messages = append ([]llm.Message {{Role : "system" , Content : systemMessage }}, messages ... )
616631 }
617632
618- result , allMessages , err := agent .RunWithMessages (ctx , messages )
619- if err != nil {
620- return err
621- }
633+ var result string
634+ result , allMessages , runErr = agent .RunWithMessages (ctx , messages )
622635 _ = result
623636
624- store , err := session .NewStore ()
625- if err != nil {
626- return fmt .Errorf ("session store: %w" , err )
627- }
628- sess , err := store .Create (allMessages , resolved .Model , f .Task )
629- if err != nil {
630- return fmt .Errorf ("save session: %w" , err )
637+ if runErr == nil {
638+ store , err := session .NewStore ()
639+ if err != nil {
640+ return fmt .Errorf ("session store: %w" , err )
641+ }
642+ sess , err := store .Create (allMessages , resolved .Model , f .Task )
643+ if err != nil {
644+ return fmt .Errorf ("save session: %w" , err )
645+ }
646+ sess .Sandbox = resolved .Sandbox
647+ store .Save (sess )
648+ fmt .Fprintf (os .Stderr , "kode: session %s saved — continue with: kode continue \" ...\" \n " , sess .ID )
631649 }
632- sess .Sandbox = resolved .Sandbox
633- store .Save (sess )
634- fmt .Fprintf (os .Stderr , "kode: session %s saved — continue with: kode continue \" ...\" \n " , sess .ID )
635650 } else {
636651 // Single-shot mode (default)
637- _ , err := agent .Run (ctx , f .Task )
638- if err != nil {
639- return err
652+ messages := []llm.Message {
653+ {Role : "user" , Content : f .Task },
654+ }
655+ if systemMessage != "" {
656+ messages = append ([]llm.Message {{Role : "system" , Content : systemMessage }}, messages ... )
640657 }
658+ _ , allMessages , runErr = agent .RunWithMessages (ctx , messages )
641659 }
660+
661+ if runErr != nil {
662+ return runErr
663+ }
664+
665+ // ── Learn loop: run self-improvement heuristics ──
666+ if resolved .Skills .Learn && sm != nil {
667+ runLearnLoop (allMessages , f .Task , sm )
668+ }
669+
642670 return nil
643671}
644672
@@ -803,6 +831,74 @@ func getVersion() string {
803831
804832// ── Skill Commands ─────────────────────────────────────────────────────
805833
834+ // ── Skill Commands ─────────────────────────────────────────────────────
835+
836+ // runLearnLoop runs self-improvement heuristics on agent output and
837+ // offers to save detected patterns as skills.
838+ func runLearnLoop (messages []llm.Message , task string , sm * skills.SkillManager ) {
839+ // Convert llm.Message to skills.llmMessage
840+ skillMsgs := make ([]skills.LlmMessage , 0 , len (messages ))
841+ for _ , m := range messages {
842+ msg := skills.LlmMessage {
843+ Role : m .Role ,
844+ Content : m .Content ,
845+ Name : m .Name ,
846+ ToolCallID : m .ToolCallID ,
847+ }
848+ for _ , tc := range m .ToolCalls {
849+ msg .ToolCalls = append (msg .ToolCalls , skills.LlmToolCall {
850+ ID : tc .ID ,
851+ })
852+ msg .ToolCalls [len (msg .ToolCalls )- 1 ].Function .Name = tc .Function .Name
853+ msg .ToolCalls [len (msg .ToolCalls )- 1 ].Function .Arguments = tc .Function .Arguments
854+ }
855+ skillMsgs = append (skillMsgs , msg )
856+ }
857+
858+ userMessages := extractUserMessages (messages )
859+ suggestions := skills .RunAllHeuristics (skillMsgs , userMessages )
860+ if len (suggestions ) == 0 {
861+ return
862+ }
863+
864+ fmt .Fprintf (os .Stderr , "\n 🔍 Learning: detected %d skill pattern(s)\n " , len (suggestions ))
865+ for _ , s := range suggestions {
866+ fmt .Fprint (os .Stderr , skills .FormatSuggestion (s ))
867+ fmt .Fprintf (os .Stderr , " Save as skill? [Y/n]: " )
868+
869+ var response string
870+ fmt .Scanf ("%s" , & response )
871+ response = strings .ToLower (strings .TrimSpace (response ))
872+
873+ if response == "" || response == "y" || response == "yes" {
874+ userDir := expandHome ("~/.kode/skills" )
875+ os .MkdirAll (userDir , 0755 )
876+ if err := skills .SaveSuggestion (userDir , s ); err != nil {
877+ fmt .Fprintf (os .Stderr , " ✗ Error saving skill: %v\n " , err )
878+ } else {
879+ fmt .Fprintf (os .Stderr , " ✓ Saved skill %q\n " , s .Name )
880+ // Reload the skill manager
881+ sm2 := skills .NewSkillManager (userDir , "./.kode/skills" )
882+ // Update the caller's manager reference if possible
883+ _ = sm2
884+ }
885+ } else {
886+ fmt .Fprintf (os .Stderr , " Skipped.\n " )
887+ }
888+ }
889+ }
890+
891+ // extractUserMessages extracts user message content from llm messages.
892+ func extractUserMessages (messages []llm.Message ) []string {
893+ var out []string
894+ for _ , m := range messages {
895+ if m .Role == "user" {
896+ out = append (out , m .Content )
897+ }
898+ }
899+ return out
900+ }
901+
806902// skillCmd handles `kode skill <list|view|save|delete|import|curate>`.
807903func skillCmd (args []string ) error {
808904 if len (args ) == 0 {
@@ -880,12 +976,18 @@ func skillCmd(args []string) error {
880976 }
881977
882978 llmCall := func (prompt string ) (string , error ) {
883- // TODO: use configured LLM for assessment
884979 if basicOnly {
885980 return "" , fmt .Errorf ("basic mode — no LLM call" )
886981 }
887- // For now, return elevated as a safe default
888- return `{"risk_class": "elevated", "reasons": ["no LLM configured for assessment"], "what_it_does": "imported skill", "recommended_triggers": [], "red_flags": []}` , nil
982+ // Load config and create LLM client for assessment
983+ cfg := config .LoadConfig (config.CLIFlags {})
984+ client := llm .New (cfg .BaseURL , cfg .APIKey , cfg .Model , "" , 30 )
985+ ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
986+ defer cancel ()
987+ return client .SimpleCall (ctx ,
988+ "You are a security assessment tool. Analyze skill files for risk." ,
989+ prompt ,
990+ )
889991 }
890992
891993 result , err := skills .ImportSkill (skills.ImportOptions {
0 commit comments