package evolution import ( "context" "crypto/sha1" "encoding/hex" "errors" "fmt" "os" "path/filepath" "sort" "strings" "sync" "time" "unicode/utf8" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/skills" ) var ErrApplyDraftFailed = errors.New("apply draft failed") type RuntimeOptions struct { Config config.EvolutionConfig Now func() time.Time Store *Store Organizer *Organizer SuccessJudge SuccessJudge SkillsRecaller *SkillsRecaller DraftGenerator DraftGenerator GeneratorFactory func(workspace string) DraftGenerator SuccessJudgeFactory func(workspace string) SuccessJudge Applier *Applier ApplierFactory func(workspace string) *Applier } type Runtime struct { cfg config.EvolutionConfig mu sync.Mutex now func() time.Time writer *CaseWriter store *Store organizer *Organizer successJudge SuccessJudge skillsRecaller *SkillsRecaller draftGenerator DraftGenerator generatorFactory func(workspace string) DraftGenerator successJudgeFactory func(workspace string) SuccessJudge applier *Applier applierFactory func(workspace string) *Applier } type TurnCaseInput struct { Workspace string WorkspaceID string TurnID string SessionKey string AgentID string Status string UserMessage string FinalContent string ToolKinds []string ToolExecutions []ToolExecutionRecord ActiveSkillNames []string AttemptedSkillNames []string FinalSuccessfulPath []string SkillContextSnapshots []SkillContextSnapshot } func NewRuntime(opts RuntimeOptions) (*Runtime, error) { now := opts.Now if now == nil { now = time.Now } organizer := opts.Organizer if organizer == nil { organizer = NewOrganizer(OrganizerOptions{ MinCaseCount: opts.Config.MinCaseCount, MinSuccessRate: opts.Config.MinSuccessRate, Now: now, }) } return &Runtime{ cfg: opts.Config, now: now, store: opts.Store, organizer: organizer, successJudge: opts.SuccessJudge, skillsRecaller: opts.SkillsRecaller, draftGenerator: opts.DraftGenerator, generatorFactory: opts.GeneratorFactory, successJudgeFactory: opts.SuccessJudgeFactory, applier: opts.Applier, applierFactory: opts.ApplierFactory, }, nil } func (rt *Runtime) FinalizeTurn(ctx context.Context, input TurnCaseInput) error { if rt == nil || !rt.cfg.Enabled || input.Workspace == "" || shouldSkipLearningRecord(input) { return nil } success := input.Status == "completed" skillUsage := buildSkillUsage(input) workspaceID := input.WorkspaceID if workspaceID == "" { workspaceID = input.Workspace } toolKinds := cloneToolKinds(input) record := LearningRecord{ ID: input.TurnID, Kind: RecordKindTask, WorkspaceID: workspaceID, CreatedAt: rt.now(), SessionKey: input.SessionKey, TaskHash: buildTaskHash(input, skillUsage, toolKinds), Summary: buildRecordSummary(input), UserGoal: summarizeText(input.UserMessage, 240), FinalOutput: summarizeText(input.FinalContent, 240), Source: map[string]any{"turn_id": input.TurnID, "session_key": input.SessionKey, "agent_id": input.AgentID}, Status: RecordStatus("new"), Success: &success, ToolKinds: toolKinds, ToolExecutions: cloneToolExecutions(input.ToolExecutions), InitialSkillNames: append([]string(nil), skillUsage.Initial...), AddedSkillNames: append([]string(nil), skillUsage.Added...), UsedSkillNames: append([]string(nil), skillUsage.Used...), AllLoadedSkillNames: append([]string(nil), skillUsage.All...), ActiveSkillNames: append([]string(nil), skillUsage.Initial...), AttemptTrail: buildAttemptTrail(skillUsage, success), Signals: buildLearningSignals(skillUsage, success), } paths := NewPaths(input.Workspace, rt.cfg.StateDir) rt.mu.Lock() if rt.writer == nil || rt.writer.paths.RootDir != paths.RootDir { rt.writer = NewCaseWriter(paths) } writer := rt.writer rt.mu.Unlock() if err := writer.AppendCase(ctx, record); err != nil { return err } if err := rt.recordSkillUsage(input, success); err != nil { return err } logger.InfoCF("evolution", "Recorded hot path learning record", map[string]any{ "workspace": input.Workspace, "turn_id": input.TurnID, "success": success, "tool_count": len(record.ToolExecutions), "used_skills": len(record.UsedSkillNames), "all_skills": len(record.AllLoadedSkillNames), }) return nil } type skillUsageRecord struct { Initial []string Added []string Used []string All []string Attempted []string Final []string } func buildAttemptTrail(usage skillUsageRecord, success bool) *AttemptTrail { if len(usage.All) == 0 { return nil } trail := &AttemptTrail{ AttemptedSkills: append([]string(nil), usage.Attempted...), } if success { finalPath := usage.Final if len(finalPath) == 0 { finalPath = usage.All } trail.FinalSuccessfulPath = append([]string(nil), finalPath...) } return trail } func buildTaskHash(input TurnCaseInput, usage skillUsageRecord, toolKinds []string) string { parts := make([]string, 0, len(usage.All)+len(toolKinds)+3) parts = append(parts, normalizedValues(usage.All)...) parts = append(parts, normalizedValues(toolKinds)...) if goal := summarizeText(input.UserMessage, 120); goal != "" { parts = append(parts, strings.ToLower(goal)) } if strings.TrimSpace(input.Status) != "" { parts = append(parts, strings.ToLower(strings.TrimSpace(input.Status))) } if len(parts) == 0 { parts = append(parts, "empty") } sum := sha1.Sum([]byte(strings.Join(parts, "\x00"))) return hex.EncodeToString(sum[:8]) } func buildLearningSignals(usage skillUsageRecord, success bool) []string { if !success || len(usage.Used) == 0 { return nil } return []string{"potentially_learnable"} } func normalizedValues(values []string) []string { out := make([]string, 0, len(values)) for _, value := range values { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { continue } out = append(out, value) } return out } func buildRecordSummary(input TurnCaseInput) string { if goal := summarizeText(input.UserMessage, 160); goal != "" { return goal } return fmt.Sprintf("turn %s finished with status=%s", input.TurnID, input.Status) } func summarizeText(text string, maxLen int) string { text = strings.TrimSpace(text) if text == "" || maxLen <= 0 { return text } if utf8.RuneCountInString(text) <= maxLen { return text } if maxLen <= 3 { runes := []rune(text) return string(runes[:maxLen]) } runes := []rune(text) return string(runes[:maxLen-3]) + "..." } func cloneToolKinds(input TurnCaseInput) []string { if len(input.ToolKinds) > 0 { return append([]string(nil), input.ToolKinds...) } if len(input.ToolExecutions) == 0 { return nil } out := make([]string, 0, len(input.ToolExecutions)) seen := make(map[string]struct{}, len(input.ToolExecutions)) for _, exec := range input.ToolExecutions { name := strings.TrimSpace(exec.Name) if name == "" { continue } if _, ok := seen[name]; ok { continue } seen[name] = struct{}{} out = append(out, name) } return out } func cloneToolExecutions(input []ToolExecutionRecord) []ToolExecutionRecord { if len(input) == 0 { return nil } out := make([]ToolExecutionRecord, 0, len(input)) for _, exec := range input { name := strings.TrimSpace(exec.Name) if name == "" { continue } out = append(out, ToolExecutionRecord{ Name: name, Success: exec.Success, ErrorSummary: strings.TrimSpace(exec.ErrorSummary), SkillNames: uniqueTrimmedNames(exec.SkillNames), }) } return out } func buildSkillUsage(input TurnCaseInput) skillUsageRecord { initial := uniqueTrimmedNames(input.ActiveSkillNames) if len(input.SkillContextSnapshots) > 0 { initial = uniqueTrimmedNames(input.SkillContextSnapshots[0].SkillNames) } all := append([]string(nil), initial...) added := make([]string, 0) seen := make(map[string]struct{}, len(all)) for _, skillName := range all { seen[strings.ToLower(skillName)] = struct{}{} } appendNew := func(skillNames []string) { for _, skillName := range uniqueTrimmedNames(skillNames) { key := strings.ToLower(skillName) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} added = append(added, skillName) all = append(all, skillName) } } for _, snapshot := range input.SkillContextSnapshots { appendNew(snapshot.SkillNames) } for _, exec := range input.ToolExecutions { appendNew(exec.SkillNames) } attempted := uniqueTrimmedNames(input.AttemptedSkillNames) if len(attempted) == 0 { attempted = append([]string(nil), all...) } else { appendNew(attempted) } final := uniqueTrimmedNames(input.FinalSuccessfulPath) if len(final) > 0 { appendNew(final) } if len(attempted) == 0 { attempted = append([]string(nil), all...) } if len(final) == 0 { final = append([]string(nil), added...) } return skillUsageRecord{ Initial: initial, Added: append([]string(nil), added...), Used: append([]string(nil), added...), All: all, Attempted: append([]string(nil), attempted...), Final: append([]string(nil), final...), } } func shouldSkipLearningRecord(input TurnCaseInput) bool { if strings.EqualFold(strings.TrimSpace(input.SessionKey), "heartbeat") { return true } return false } func uniqueTrimmedNames(values []string) []string { out := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } key := strings.ToLower(value) if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, value) } return out } func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error { if rt == nil || !rt.cfg.Enabled || workspace == "" { return nil } mode := rt.cfg.EffectiveMode() runID := fmt.Sprintf("%d", rt.now().UnixNano()) if mode == "" || mode == "observe" { logger.InfoCF("evolution", "Skipped cold path run", map[string]any{ "workspace": workspace, "mode": mode, "run_id": runID, }) return nil } logger.InfoCF("evolution", "Started cold path run", map[string]any{ "workspace": workspace, "mode": mode, "run_id": runID, }) store := rt.storeForWorkspace(workspace) records, err := store.LoadLearningRecords() if err != nil { return err } logger.InfoCF("evolution", "Loaded evolution records", map[string]any{ "workspace": workspace, "record_count": len(records), "run_id": runID, }) admittedCount := 0 newRuleCount := 0 if rt.organizer != nil { recordsForOrganizer, err := rt.recordsForColdPath(ctx, workspace, records) if err != nil { return err } admittedCount = countTaskLearningRecords(recordsForOrganizer) logger.InfoCF("evolution", "Admitted task records for cold path", map[string]any{ "workspace": workspace, "admitted_tasks": admittedCount, "organizer_input": len(recordsForOrganizer), "task_ids": joinRecordIDs(recordsForOrganizer), "run_id": runID, }) rules, err := rt.organizer.BuildRules(recordsForOrganizer) if err != nil { return err } newRules := filterNewRules(records, rules, workspace) newRuleCount = len(newRules) logger.InfoCF("evolution", "Built learning patterns", map[string]any{ "workspace": workspace, "pattern_count": len(rules), "new_patterns": len(newRules), "admitted_tasks": admittedCount, "patterns": summarizePatternRecords(rules), "run_id": runID, }) if len(newRules) > 0 { if err := store.AppendLearningRecords(newRules); err != nil { return err } records = append(records, newRules...) } } generator := rt.draftGeneratorForWorkspace(workspace) if generator == nil { logger.InfoCF("evolution", "Skipped drafting because no draft generator is available", map[string]any{ "workspace": workspace, "run_id": runID, }) return rt.runLifecycleMaintenance(workspace, store, runID) } recaller := rt.skillsRecallerForWorkspace(workspace) applier := rt.applierForWorkspace(workspace) readyRules := filterReadyRules(records, workspace) if len(readyRules) == 0 { logger.InfoCF("evolution", "Finished cold path run without ready patterns", map[string]any{ "workspace": workspace, "record_count": len(records), "new_patterns": newRuleCount, "admitted_tasks": admittedCount, "run_id": runID, }) return rt.runLifecycleMaintenance(workspace, store, runID) } existingDrafts, err := store.LoadDrafts() if err != nil { return err } existingBySource := existingDraftSourceSet(existingDrafts, workspace) logger.InfoCF("evolution", "Selected ready patterns for drafting", map[string]any{ "workspace": workspace, "ready_patterns": len(readyRules), "existing_draft_count": len(existingBySource), "ready_pattern_ids": joinRecordIDs(readyRules), "ready_patterns_info": summarizePatternRecords(readyRules), "run_id": runID, }) processedRules := 0 for _, rule := range readyRules { select { case <-ctx.Done(): return ctx.Err() default: } if _, exists := existingBySource[rule.ID]; exists { logger.InfoCF("evolution", "Skipped pattern because a non-quarantined draft already exists", map[string]any{ "workspace": workspace, "pattern_id": rule.ID, "pattern_info": summarizePatternRecord(rule), "run_id": runID, }) continue } matches, err := recaller.RecallSimilarSkills(rule) if err != nil { return err } logger.InfoCF("evolution", "Generating skill draft", map[string]any{ "workspace": workspace, "pattern_id": rule.ID, "matched_skill_count": len(matches), "pattern_info": summarizePatternRecord(rule), "run_id": runID, }) draft, err := generator.GenerateDraft(ctx, rule, matches) if err != nil { return err } draft = rt.finalizeDraft(workspace, rule, matches, draft) logger.InfoCF("evolution", "Finalized skill draft", map[string]any{ "workspace": workspace, "pattern_id": rule.ID, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "change_kind": string(draft.ChangeKind), "status": string(draft.Status), "run_id": runID, }) if mode == "apply" && applier != nil && draft.Status == DraftStatusCandidate { logger.InfoCF("evolution", "Applying skill draft", map[string]any{ "workspace": workspace, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "change_kind": string(draft.ChangeKind), "run_id": runID, }) rollbackApply, err := applier.applyDraftWithRollback(ctx, workspace, draft) if err != nil { logger.WarnCF("evolution", "Skill draft apply failed", map[string]any{ "workspace": workspace, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "error": err.Error(), "run_id": runID, }) draft.Status = DraftStatusQuarantined draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("apply failed: %v", err)) if auditErr := rt.recordRollbackAudit(store, draft, err); auditErr != nil { draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("rollback audit failed: %v", auditErr)) if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr, saveErr) } return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr) } if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), saveErr) } return fmt.Errorf("%w: %v", ErrApplyDraftFailed, err) } draft.Status = DraftStatusAccepted if err := rt.saveAppliedProfile(store, workspace, draft); err != nil { logger.WarnCF("evolution", "Skill profile save failed after apply", map[string]any{ "workspace": workspace, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "error": err.Error(), "run_id": runID, }) draft.Status = DraftStatusQuarantined draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("profile save failed: %v", err)) if rollbackErr := rollbackApply(); rollbackErr != nil { draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, fmt.Sprintf("apply rollback failed: %v", rollbackErr)) if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr, saveErr) } return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr) } if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), saveErr) } return fmt.Errorf("%w: %v", ErrApplyDraftFailed, err) } logger.InfoCF("evolution", "Applied skill draft successfully", map[string]any{ "workspace": workspace, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "run_id": runID, }) } if err := store.SaveDrafts([]SkillDraft{draft}); err != nil { return err } logger.InfoCF("evolution", "Saved skill draft", map[string]any{ "workspace": workspace, "draft_id": draft.ID, "target_skill": draft.TargetSkillName, "status": string(draft.Status), "run_id": runID, }) existingBySource[rule.ID] = struct{}{} processedRules++ } logger.InfoCF("evolution", "Finished cold path run", map[string]any{ "workspace": workspace, "ready_patterns": len(readyRules), "processed_patterns": processedRules, "new_patterns": newRuleCount, "run_id": runID, }) return rt.runLifecycleMaintenance(workspace, store, runID) } func (rt *Runtime) recordsForColdPath( ctx context.Context, workspace string, records []LearningRecord, ) ([]LearningRecord, error) { out := make([]LearningRecord, 0, len(records)) judge := rt.successJudgeForWorkspace(workspace) for _, record := range records { if !isTaskRecordKind(record.Kind) { out = append(out, record) continue } if record.WorkspaceID != workspace { continue } if !passesColdPathRuleFilter(record) { continue } if judge != nil { decision, err := judge.JudgeTaskRecord(ctx, record) if err != nil { return nil, err } if !decision.Success { continue } } out = append(out, record) } return out, nil } func passesColdPathRuleFilter(record LearningRecord) bool { if !isTaskRecordKind(record.Kind) { return false } if record.Success == nil || !*record.Success { return false } if strings.TrimSpace(record.UserGoal) == "" { return false } if strings.EqualFold(strings.TrimSpace(record.SessionKey), "heartbeat") { return false } if strings.EqualFold(strings.TrimSpace(record.FinalOutput), "HEARTBEAT_OK") { return false } if len(record.ToolExecutions) > 0 { allFailed := true for _, exec := range record.ToolExecutions { if exec.Success { allFailed = false break } } if allFailed { return false } } if len(record.ToolKinds) == 0 && len(record.UsedSkillNames) == 0 && strings.TrimSpace(record.FinalOutput) == "" { return false } return true } func (rt *Runtime) storeForWorkspace(workspace string) *Store { paths := NewPaths(workspace, rt.cfg.StateDir) rt.mu.Lock() defer rt.mu.Unlock() if rt.store == nil || rt.store.paths.RootDir != paths.RootDir { rt.store = NewStore(paths) } return rt.store } func (rt *Runtime) skillsRecallerForWorkspace(workspace string) *SkillsRecaller { rt.mu.Lock() defer rt.mu.Unlock() if rt.skillsRecaller == nil || rt.skillsRecaller.workspace != workspace { rt.skillsRecaller = NewSkillsRecaller(workspace) } return rt.skillsRecaller } func (rt *Runtime) draftGeneratorForWorkspace(workspace string) DraftGenerator { if rt.generatorFactory != nil { if generator := rt.generatorFactory(workspace); generator != nil { return generator } } if rt.draftGenerator != nil { return rt.draftGenerator } return NewDefaultDraftGenerator(workspace) } func (rt *Runtime) successJudgeForWorkspace(workspace string) SuccessJudge { if rt.successJudgeFactory != nil { if judge := rt.successJudgeFactory(workspace); judge != nil { return judge } } if rt.successJudge != nil { return rt.successJudge } return &HeuristicSuccessJudge{} } func (rt *Runtime) applierForWorkspace(workspace string) *Applier { if rt.applierFactory != nil { if applier := rt.applierFactory(workspace); applier != nil { return applier } } return rt.applier } func (rt *Runtime) finalizeDraft(workspace string, rule LearningRecord, matches []skills.SkillInfo, draft SkillDraft) SkillDraft { if draft.ID == "" { draft.ID = "draft-" + rule.ID } if draft.CreatedAt.IsZero() { draft.CreatedAt = rt.now() } draft.WorkspaceID = workspace draft.SourceRecordID = rule.ID if len(draft.MatchedSkillRefs) == 0 { draft.MatchedSkillRefs = collectSkillRefs(matches) } draft, normalizationNotes := rt.normalizeDraftForWorkspace(workspace, rule, draft) review := ReviewDraft(draft) draft.Status = review.Status draft.ReviewNotes = append([]string(nil), review.ReviewNotes...) draft.ReviewNotes = append(draft.ReviewNotes, normalizationNotes...) if len(review.Findings) == 0 { draft.ScanFindings = nil return draft } draft.ScanFindings = append([]string(nil), review.Findings...) return draft } func (rt *Runtime) normalizeDraftForWorkspace( workspace string, rule LearningRecord, draft SkillDraft, ) (SkillDraft, []string) { target := strings.TrimSpace(draft.TargetSkillName) if workspace == "" || target == "" { return draft, nil } notes := make([]string, 0, 4) if combinedTarget := inferCombinedSkillName(rule); combinedTarget != "" && combinedTarget != target { originalTarget := target draft.TargetSkillName = combinedTarget target = combinedTarget notes = append(notes, fmt.Sprintf( "retargeted draft from %q to combined shortcut skill %q because the winning path was a stable multi-skill chain", originalTarget, combinedTarget, )) } skillPath := filepath.Join(workspace, "skills", target, "SKILL.md") _, err := os.Stat(skillPath) hasExisting := err == nil if err != nil && !errors.Is(err, os.ErrNotExist) { return draft, notes } if combinedTarget := inferCombinedSkillName(rule); combinedTarget != "" && combinedTarget == target { draft.HumanSummary = buildCombinedSkillHumanSummary(target, rule, hasExisting) draft.PreferredEntryPath = []string{target} draft.AvoidPatterns = appendUniqueStrings( draft.AvoidPatterns, buildCombinedSkillAvoidPattern(target, rule), ) if hasExisting { draft.ChangeKind = ChangeKindAppend draft.BodyOrPatch = synthesizeCombinedSkillAppendBody(target, draft, rule) notes = append(notes, "normalized combined shortcut draft to append onto the existing combined skill") } else { draft.ChangeKind = ChangeKindCreate draft.BodyOrPatch = synthesizeCombinedSkillDocument(target, draft, rule) notes = append(notes, "normalized combined shortcut draft to create a new standalone shortcut skill") } return draft, notes } if !hasExisting { switch draft.ChangeKind { case ChangeKindAppend, ChangeKindMerge, ChangeKindReplace: draft.ChangeKind = ChangeKindCreate notes = append(notes, "normalized change_kind to create because target skill did not exist") if !looksLikeSkillDocument(draft.BodyOrPatch) { draft.BodyOrPatch = synthesizeSkillDocumentFromPartialDraft(target, draft, rule) notes = append(notes, "synthesized full skill document because draft body was partial") } } return draft, notes } if draft.ChangeKind == ChangeKindCreate && !looksLikeSkillDocument(draft.BodyOrPatch) { draft.ChangeKind = ChangeKindAppend notes = append(notes, "normalized change_kind to append because target skill already existed") } return draft, notes } func looksLikeSkillDocument(body string) bool { body = strings.TrimSpace(body) return strings.HasPrefix(body, "---\n") && strings.Contains(body, "\n# ") } func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, rule LearningRecord) string { description := strings.TrimSpace(draft.HumanSummary) if description == "" { description = fmt.Sprintf("Learned workflow for %s.", target) } bodyContent := strings.TrimSpace(draft.BodyOrPatch) if bodyContent == "" { bodyContent = "No learned content was generated." } if strings.HasPrefix(bodyContent, "# ") { return buildSkillDocument(target, description, bodyContent) } body := strings.Join([]string{ "# " + titleCaseSkillName(target), "", "## Start Here", synthesizedStartHereLine(rule, target), "", "## Learned Evolution", bodyContent, "", }, "\n") return buildSkillDocument(target, description, body) } func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule LearningRecord) string { description := strings.TrimSpace(draft.HumanSummary) if description == "" { description = buildCombinedSkillHumanSummary(target, rule, false) } body := strings.Join([]string{ "# " + titleCaseSkillName(target), "", "## Start Here", synthesizedCombinedStartHereLine(rule, target), "", "## When To Use", synthesizedCombinedWhenToUseLine(rule, target), "", "## Wrapped Path", synthesizedWrappedPathLine(rule), "", "## Learned Shortcut", synthesizedCombinedLearnedContent(draft.BodyOrPatch), "", }, "\n") return buildSkillDocument(target, description, body) } func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule LearningRecord) string { lines := []string{ "## Learned Shortcut Update", fmt.Sprintf("- Shortcut skill: `%s`", target), fmt.Sprintf("- Task summary: %s", fallbackEvolutionSummary(rule)), fmt.Sprintf("- Wrapped path: %s", synthesizedWrappedPathLine(rule)), "- Guidance: prefer this shortcut directly instead of replaying the whole path when the task matches.", "", synthesizedCombinedLearnedContent(draft.BodyOrPatch), "", } return strings.Join(lines, "\n") } func synthesizedStartHereLine(rule LearningRecord, target string) string { if len(rule.WinningPath) > 0 { return fmt.Sprintf("Start with `%s` for tasks like `%s`.", strings.Join(rule.WinningPath, " -> "), strings.TrimSpace(rule.Summary)) } if summary := strings.TrimSpace(rule.Summary); summary != "" { return fmt.Sprintf("Use `%s` when the task matches `%s`.", target, summary) } return fmt.Sprintf("Use `%s` for the learned task pattern.", target) } func synthesizedCombinedStartHereLine(rule LearningRecord, target string) string { return fmt.Sprintf("Use `%s` directly when the task matches `%s`.", target, fallbackEvolutionSummary(rule)) } func synthesizedCombinedWhenToUseLine(rule LearningRecord, target string) string { if len(rule.WinningPath) == 0 { return fmt.Sprintf("Use `%s` when the learned task pattern appears again.", target) } return fmt.Sprintf("Use `%s` as a direct shortcut instead of replaying `%s` step by step.", target, strings.Join(rule.WinningPath, " -> ")) } func synthesizedWrappedPathLine(rule LearningRecord) string { if len(rule.WinningPath) == 0 { return "No explicit wrapped path was recorded." } return strings.Join(rule.WinningPath, " -> ") } func synthesizedCombinedLearnedContent(body string) string { content := strings.TrimSpace(stripSkillFrontmatter(body)) if content == "" { return "Use this shortcut directly when the same task pattern appears again." } return demoteMarkdownHeadings(content) } func stripSkillFrontmatter(body string) string { trimmed := strings.TrimSpace(body) if !strings.HasPrefix(trimmed, "---\n") { return trimmed } rest := strings.TrimPrefix(trimmed, "---\n") end := strings.Index(rest, "\n---\n") if end < 0 { return trimmed } return strings.TrimSpace(rest[end+5:]) } func demoteMarkdownHeadings(content string) string { lines := strings.Split(content, "\n") for i, line := range lines { trimmed := strings.TrimLeft(line, " \t") if !strings.HasPrefix(trimmed, "#") { continue } prefixLen := len(line) - len(trimmed) lines[i] = line[:prefixLen] + "##" + trimmed } return strings.TrimSpace(strings.Join(lines, "\n")) } func fallbackEvolutionSummary(rule LearningRecord) string { if summary := strings.TrimSpace(rule.Summary); summary != "" { return summary } if len(rule.WinningPath) > 0 { return strings.Join(rule.WinningPath, " -> ") } return "the learned task pattern" } func buildCombinedSkillHumanSummary(target string, rule LearningRecord, hasExisting bool) string { action := "Create" if hasExisting { action = "Refresh" } return fmt.Sprintf("%s combined shortcut %s from learned pattern: %s", action, target, fallbackEvolutionSummary(rule)) } func buildCombinedSkillAvoidPattern(target string, rule LearningRecord) string { if len(rule.WinningPath) == 0 { return fmt.Sprintf("avoid bypassing `%s` when the same learned task pattern appears again", target) } return fmt.Sprintf("avoid replaying %s before trying `%s` directly", strings.Join(rule.WinningPath, " -> "), target) } func collectSkillRefs(matches []skills.SkillInfo) []string { if len(matches) == 0 { return nil } refs := make([]string, 0, len(matches)) for _, match := range matches { if strings := match.Path; strings != "" { refs = append(refs, strings) continue } refs = append(refs, match.Source+":"+match.Name) } return refs } func countTaskLearningRecords(records []LearningRecord) int { count := 0 for _, record := range records { if isTaskRecordKind(record.Kind) { count++ } } return count } func (rt *Runtime) runLifecycleMaintenance(workspace string, store *Store, runID string) error { if rt == nil || store == nil || workspace == "" { return nil } paths := NewPaths(workspace, rt.cfg.StateDir) logger.InfoCF("evolution", "Started lifecycle maintenance", map[string]any{ "workspace": workspace, "run_id": runID, }) summary, err := RunLifecycleOnce(store, paths, workspace, rt.now()) if err != nil { logger.WarnCF("evolution", "Lifecycle maintenance failed", map[string]any{ "workspace": workspace, "run_id": runID, "error": err.Error(), }) return err } logger.InfoCF("evolution", "Finished lifecycle maintenance", map[string]any{ "workspace": workspace, "run_id": runID, "evaluated_profiles": summary.EvaluatedProfiles, "transitioned_profiles": summary.TransitionedProfiles, "deleted_skills": summary.DeletedSkills, }) return nil } func joinRecordIDs(records []LearningRecord) string { if len(records) == 0 { return "" } ids := make([]string, 0, len(records)) for _, record := range records { if strings.TrimSpace(record.ID) == "" { continue } ids = append(ids, record.ID) } return strings.Join(ids, ",") } func summarizePatternRecords(records []LearningRecord) string { if len(records) == 0 { return "" } parts := make([]string, 0, len(records)) for _, record := range records { parts = append(parts, summarizePatternRecord(record)) } return strings.Join(parts, " | ") } func summarizePatternRecord(record LearningRecord) string { label := strings.TrimSpace(record.ID) if label == "" { label = "unknown-pattern" } path := strings.Join(record.WinningPath, " -> ") if path == "" { path = strings.TrimSpace(record.Summary) } if path == "" { path = "no-summary" } return fmt.Sprintf("%s[%s]", label, path) } func filterNewRules(records []LearningRecord, rules []LearningRecord, workspace string) []LearningRecord { existing := make(map[string]struct{}, len(records)) for _, record := range records { if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace { continue } existing[record.ID] = struct{}{} } out := make([]LearningRecord, 0, len(rules)) for _, rule := range rules { if rule.WorkspaceID != workspace { continue } if _, ok := existing[rule.ID]; ok { continue } existing[rule.ID] = struct{}{} out = append(out, rule) } return out } func filterReadyRules(records []LearningRecord, workspace string) []LearningRecord { seen := make(map[string]LearningRecord) for _, record := range records { if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace || record.Status != RecordStatus("ready") { continue } seen[record.ID] = record } out := make([]LearningRecord, 0, len(seen)) for _, record := range seen { out = append(out, record) } sort.Slice(out, func(i, j int) bool { if !out[i].CreatedAt.Equal(out[j].CreatedAt) { return out[i].CreatedAt.Before(out[j].CreatedAt) } return out[i].ID < out[j].ID }) return out } func existingDraftSourceSet(drafts []SkillDraft, workspace string) map[string]struct{} { out := make(map[string]struct{}, len(drafts)) for _, draft := range drafts { if draft.WorkspaceID != workspace || draft.SourceRecordID == "" { continue } if draft.Status == DraftStatusQuarantined { continue } out[draft.SourceRecordID] = struct{}{} } return out } func (rt *Runtime) saveAppliedProfile(store *Store, workspace string, draft SkillDraft) error { now := rt.now() return SaveAppliedProfile(store, workspace, draft, now) } func (rt *Runtime) recordRollbackAudit(store *Store, draft SkillDraft, applyErr error) error { profile, err := store.LoadProfile(draft.TargetSkillName) if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } now := rt.now() profile.VersionHistory = append(profile.VersionHistory, SkillVersionEntry{ Version: profile.CurrentVersion, Action: "rollback", Timestamp: now, DraftID: draft.ID, Summary: fmt.Sprintf("Rolled back failed draft apply: %s", draft.HumanSummary), Rollback: true, RollbackReason: applyErr.Error(), }) return store.SaveProfile(profile) } func profileOrigin(origin string) string { if origin == "manual" { return origin } return "evolved" } func appendUniqueStrings(existing []string, values ...string) []string { seen := make(map[string]struct{}, len(existing)) for _, value := range existing { seen[value] = struct{}{} } for _, value := range values { if strings.TrimSpace(value) == "" { continue } if _, ok := seen[value]; ok { continue } existing = append(existing, value) seen[value] = struct{}{} } return existing } func (rt *Runtime) recordSkillUsage(input TurnCaseInput, success bool) error { usage := buildSkillUsage(input) if len(usage.All) == 0 { return nil } store := rt.storeForWorkspace(input.Workspace) seen := make(map[string]struct{}, len(usage.All)) for _, skillName := range usage.All { skillName = strings.TrimSpace(skillName) if skillName == "" { continue } if _, ok := seen[skillName]; ok { continue } seen[skillName] = struct{}{} if err := rt.touchSkillProfile(store, input, skillName, success); err != nil { return err } } return nil } func (rt *Runtime) touchSkillProfile(store *Store, input TurnCaseInput, skillName string, success bool) error { now := rt.now() profile, err := store.LoadProfile(skillName) if err != nil && !errors.Is(err, os.ErrNotExist) { return err } if errors.Is(err, os.ErrNotExist) { profile = SkillProfile{ SkillName: skillName, WorkspaceID: input.Workspace, Status: SkillStatusActive, Origin: "manual", HumanSummary: skillName, RetentionScore: 0.2, } } profile.SkillName = skillName profile.WorkspaceID = input.Workspace if profile.Status == SkillStatusCold || profile.Status == SkillStatusArchived || profile.Status == "" { profile.Status = SkillStatusActive } if profile.Origin == "" { profile.Origin = "manual" } if strings.TrimSpace(profile.HumanSummary) == "" { profile.HumanSummary = skillName } profile.LastUsedAt = now profile.UseCount++ profile.RetentionScore = nextRetentionScore(profile.RetentionScore, success) return store.SaveProfile(profile) } func nextRetentionScore(current float64, success bool) float64 { increment := 0.05 if success { increment = 0.1 } current += increment if current > 1 { return 1 } return current }