From 07be3462df8467d47a8a03ebcf496e85e672aaa4 Mon Sep 17 00:00:00 2001 From: lxowalle Date: Mon, 11 May 2026 11:42:03 +0800 Subject: [PATCH] fix ci --- pkg/agent/evolution_bridge.go | 6 +- pkg/agent/evolution_bridge_test.go | 112 +++++++++-- pkg/agent/legacy_events.go | 11 +- pkg/agent/turn_coord.go | 12 +- pkg/agent/turn_coord_test.go | 3 +- pkg/config/config.go | 9 - pkg/config/config_test.go | 15 +- pkg/evolution/apply.go | 37 ++-- pkg/evolution/apply_test.go | 34 ++-- pkg/evolution/cold_path_runner_test.go | 6 +- pkg/evolution/drafts.go | 64 +++++-- pkg/evolution/drafts_test.go | 23 ++- pkg/evolution/llm_draft_generator.go | 19 +- pkg/evolution/llm_draft_generator_test.go | 23 ++- pkg/evolution/pattern_clusterer.go | 141 ++++++++++++-- pkg/evolution/pattern_clusterer_test.go | 84 +++------ pkg/evolution/preview.go | 20 +- pkg/evolution/profile_sync.go | 8 +- pkg/evolution/runtime.go | 214 +++++++++++++--------- pkg/evolution/runtime_apply_test.go | 137 ++++++++------ pkg/evolution/runtime_cold_path_test.go | 76 ++++---- pkg/evolution/runtime_test.go | 170 +++++++---------- pkg/evolution/skills_recall_test.go | 18 +- pkg/evolution/store.go | 20 +- pkg/evolution/store_test.go | 48 ++--- 25 files changed, 815 insertions(+), 495 deletions(-) diff --git a/pkg/agent/evolution_bridge.go b/pkg/agent/evolution_bridge.go index da02d0024..f2d471bfb 100644 --- a/pkg/agent/evolution_bridge.go +++ b/pkg/agent/evolution_bridge.go @@ -34,7 +34,11 @@ type evolutionBridge struct { const evolutionDirectDeliveryAttr = "evolution_direct_delivery" -func newEvolutionBridge(registry *AgentRegistry, cfg *config.Config, provider providers.LLMProvider) (*evolutionBridge, error) { +func newEvolutionBridge( + registry *AgentRegistry, + cfg *config.Config, + provider providers.LLMProvider, +) (*evolutionBridge, error) { if cfg == nil { return nil, nil } diff --git a/pkg/agent/evolution_bridge_test.go b/pkg/agent/evolution_bridge_test.go index 68ebaa991..dbfd65bfb 100644 --- a/pkg/agent/evolution_bridge_test.go +++ b/pkg/agent/evolution_bridge_test.go @@ -355,7 +355,13 @@ func TestEvolutionBridge_ObserveTurnEndPayloadIncludesResolvedAttemptTrail(t *te sub := al.SubscribeEvents(16) defer al.UnsubscribeEvents(sub.ID) - resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-observe-attempt-trail", "cli", "direct") + resp, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-observe-attempt-trail", + "cli", + "direct", + ) if err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -412,7 +418,13 @@ func TestEvolutionBridge_ObserveTurnEndUsesLatestSkillSnapshotAfterRetry(t *test sub := al.SubscribeEvents(16) defer al.UnsubscribeEvents(sub.ID) - resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-observe-retry-snapshot", "cli", "direct") + resp, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-observe-retry-snapshot", + "cli", + "direct", + ) if err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -437,12 +449,21 @@ func TestEvolutionBridge_ObserveTurnEndUsesLatestSkillSnapshotAfterRetry(t *test t.Fatalf("len(SkillContextSnapshots) = %d, want 2", len(got)) } if turnEndPayload.SkillContextSnapshots[0].Trigger != skillContextTriggerInitialBuild { - t.Fatalf("SkillContextSnapshots[0].Trigger = %q, want %q", turnEndPayload.SkillContextSnapshots[0].Trigger, skillContextTriggerInitialBuild) + t.Fatalf( + "SkillContextSnapshots[0].Trigger = %q, want %q", + turnEndPayload.SkillContextSnapshots[0].Trigger, + skillContextTriggerInitialBuild, + ) } if turnEndPayload.SkillContextSnapshots[1].Trigger != skillContextTriggerContextRetryRebuild { - t.Fatalf("SkillContextSnapshots[1].Trigger = %q, want %q", turnEndPayload.SkillContextSnapshots[1].Trigger, skillContextTriggerContextRetryRebuild) + t.Fatalf( + "SkillContextSnapshots[1].Trigger = %q, want %q", + turnEndPayload.SkillContextSnapshots[1].Trigger, + skillContextTriggerContextRetryRebuild, + ) } - if got := turnEndPayload.SkillContextSnapshots[1].SkillNames; len(got) != 2 || got[0] != "base-skill" || got[1] != "late-skill" { + if got := turnEndPayload.SkillContextSnapshots[1].SkillNames; len(got) != 2 || got[0] != "base-skill" || + got[1] != "late-skill" { t.Fatalf("SkillContextSnapshots[1].SkillNames = %v, want [base-skill late-skill]", got) } } @@ -500,7 +521,13 @@ func TestEvolutionBridge_ScheduledModeDoesNotRunColdPathAfterTurn(t *testing.T) }, &simpleMockProvider{response: "ok"}) defer al.Close() - resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-scheduled-cold-path", "cli", "direct") + resp, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-scheduled-cold-path", + "cli", + "direct", + ) if err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -525,7 +552,13 @@ func TestEvolutionBridge_DraftModeUsesProviderBackedDraftGenerator(t *testing.T) }) defer al.Close() - resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-llm", "cli", "direct") + resp, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-auto-cold-path-llm", + "cli", + "direct", + ) if err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -567,7 +600,13 @@ func TestEvolutionBridge_DraftModeUsesProviderDefaultModel(t *testing.T) { al := NewAgentLoop(cfg, bus.NewMessageBus(), provider) defer al.Close() - if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-model", "cli", "direct"); err != nil { + if _, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-auto-cold-path-model", + "cli", + "direct", + ); err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -606,7 +645,13 @@ func TestEvolutionBridge_DraftModePrefersConfigDefaultModelName(t *testing.T) { al := NewAgentLoop(cfg, bus.NewMessageBus(), provider) defer al.Close() - if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-auto-cold-path-model-config", "cli", "direct"); err != nil { + if _, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-auto-cold-path-model-config", + "cli", + "direct", + ); err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -629,7 +674,13 @@ func TestEvolutionBridge_DraftModeKeepsCandidateDraft(t *testing.T) { }) defer al.Close() - if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-apply-no-auto-apply", "cli", "direct"); err != nil { + if _, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-apply-no-auto-apply", + "cli", + "direct", + ); err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -665,7 +716,13 @@ func TestEvolutionBridge_ApplyModeAutomaticallyRunsColdPathAndAppliesMergeDraft( }) defer al.Close() - if _, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-apply-merge", "cli", "direct"); err != nil { + if _, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-apply-merge", + "cli", + "direct", + ); err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -705,7 +762,13 @@ func TestEvolutionBridge_ObserveModeDoesNotRunColdPathOrCreateDraftFile(t *testi }, &simpleMockProvider{response: "ok"}) defer al.Close() - resp, err := al.ProcessDirectWithChannel(context.Background(), "hello", "session-no-auto-cold-path", "cli", "direct") + resp, err := al.ProcessDirectWithChannel( + context.Background(), + "hello", + "session-no-auto-cold-path", + "cli", + "direct", + ) if err != nil { t.Fatalf("ProcessDirectWithChannel failed: %v", err) } @@ -784,7 +847,11 @@ func TestEvolutionBridge_TurnEndUsesExplicitAttemptTrail(t *testing.T) { FinalSuccessfulPath: []string{"geocode", "weather"}, SkillContextSnapshots: []SkillContextSnapshot{ {Sequence: 1, Trigger: skillContextTriggerInitialBuild, SkillNames: []string{"weather"}}, - {Sequence: 2, Trigger: skillContextTriggerContextRetryRebuild, SkillNames: []string{"geocode", "weather"}}, + { + Sequence: 2, + Trigger: skillContextTriggerContextRetryRebuild, + SkillNames: []string{"geocode", "weather"}, + }, }, ToolKinds: []string{"echo_text"}, }, @@ -846,8 +913,8 @@ func TestEvolutionBridge_CloseRejectsLateTurnEndEvents(t *testing.T) { t.Fatalf("newEvolutionBridge: %v", err) } - if err := bridge.Close(); err != nil { - t.Fatalf("Close() error = %v", err) + if closeErr := bridge.Close(); closeErr != nil { + t.Fatalf("Close() error = %v", closeErr) } err = bridge.OnEvent(context.Background(), Event{ @@ -1035,7 +1102,12 @@ func seedReadyRule(t *testing.T, workspace string) { } } -func newEvolutionTestLoop(t *testing.T, workspace string, evo config.EvolutionConfig, provider providers.LLMProvider) *AgentLoop { +func newEvolutionTestLoop( + t *testing.T, + workspace string, + evo config.EvolutionConfig, + provider providers.LLMProvider, +) *AgentLoop { t.Helper() cfg := &config.Config{ @@ -1163,15 +1235,15 @@ func assertProfileNotExists(t *testing.T, workspace, skillName string) { t.Helper() store := evolution.NewStore(evolution.NewPaths(workspace, "")) - if _, err := store.LoadProfile(skillName); !os.IsNotExist(err) { - t.Fatalf("profile %q should not exist, got err = %v", skillName, err) + if _, loadErr := store.LoadProfile(skillName); !os.IsNotExist(loadErr) { + t.Fatalf("profile %q should not exist, got err = %v", skillName, loadErr) } } func assertNotExists(t *testing.T, path string) { t.Helper() - if _, err := os.Stat(path); !os.IsNotExist(err) { - t.Fatalf("%s should not exist, stat err = %v", path, err) + if _, statErr := os.Stat(path); !os.IsNotExist(statErr) { + t.Fatalf("%s should not exist, stat err = %v", path, statErr) } } diff --git a/pkg/agent/legacy_events.go b/pkg/agent/legacy_events.go index 781aee613..30761e8e6 100644 --- a/pkg/agent/legacy_events.go +++ b/pkg/agent/legacy_events.go @@ -42,10 +42,13 @@ func (al *AgentLoop) SubscribeEvents(buffer int) EventSubscription { } ctx, cancel := context.WithCancel(context.Background()) - sub, in, err := al.runtimeEvents.Channel().Source("agent").OfKind(legacyAgentEventKinds()...).SubscribeChan(ctx, runtimeevents.SubscribeOptions{ - Name: "legacy-agent-events", - Buffer: buffer, - }) + sub, in, err := al.runtimeEvents.Channel(). + Source("agent"). + OfKind(legacyAgentEventKinds()...). + SubscribeChan(ctx, runtimeevents.SubscribeOptions{ + Name: "legacy-agent-events", + Buffer: buffer, + }) if err != nil { cancel() close(out) diff --git a/pkg/agent/turn_coord.go b/pkg/agent/turn_coord.go index fb0c12207..060346339 100644 --- a/pkg/agent/turn_coord.go +++ b/pkg/agent/turn_coord.go @@ -219,11 +219,11 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel if finalContent == "" { finalContent = ts.opts.DefaultResponse } - result, err := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent) - if err != nil { + result, finalizeErr := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent) + if finalizeErr != nil { turnStatus = TurnEndStatusError } - return result, err + return result, finalizeErr case ControlToolLoop: // Execute tools via Pipeline toolCtrl := pipeline.ExecuteTools(ctx, turnCtx, ts, exec, iteration) @@ -250,11 +250,11 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel if exec.allResponsesHandled { finalContent = "" } - result, err := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent) - if err != nil { + result, finalizeErr := pipeline.Finalize(ctx, turnCtx, ts, exec, turnStatus, finalContent) + if finalizeErr != nil { turnStatus = TurnEndStatusError } - return result, err + return result, finalizeErr } } } diff --git a/pkg/agent/turn_coord_test.go b/pkg/agent/turn_coord_test.go index 30307bfcd..c7cdd8a32 100644 --- a/pkg/agent/turn_coord_test.go +++ b/pkg/agent/turn_coord_test.go @@ -835,7 +835,8 @@ func TestTurnState_SkillContextSnapshotsTrackLatestSuccessfulPath(t *testing.T) ts.recordSkillContextSnapshot(skillContextTriggerInitialBuild, []string{"skill-a"}) ts.recordSkillContextSnapshot(skillContextTriggerContextRetryRebuild, []string{"skill-b", "skill-c"}) - if got := ts.attemptedSkillsSnapshot(); len(got) != 3 || got[0] != "skill-a" || got[1] != "skill-b" || got[2] != "skill-c" { + if got := ts.attemptedSkillsSnapshot(); len(got) != 3 || got[0] != "skill-a" || got[1] != "skill-b" || + got[2] != "skill-c" { t.Fatalf("attemptedSkillsSnapshot = %v, want [skill-a skill-b skill-c]", got) } diff --git a/pkg/config/config.go b/pkg/config/config.go index ebb38e49e..11d179911 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -170,15 +170,6 @@ func (c EvolutionConfig) EffectiveColdPathTimes() []string { return out } -func (c EvolutionConfig) legacyRunsColdPathAutomatically() bool { - switch c.EffectiveMode() { - case "draft", "apply": - return true - default: - return false - } -} - func (c EvolutionConfig) AutoAppliesDrafts() bool { return c.EffectiveMode() == "apply" } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 873bddcaa..71e5666b3 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -333,7 +333,12 @@ func TestEvolutionConfig_ColdPathTriggerMode(t *testing.T) { assert.True(t, (EvolutionConfig{Enabled: true, Mode: "draft"}).RunsColdPathAfterTurn()) assert.False(t, (EvolutionConfig{Enabled: true, Mode: "draft"}).RunsColdPathScheduled()) - scheduled := EvolutionConfig{Enabled: true, Mode: "apply", ColdPathTrigger: "scheduled", ColdPathTimes: []string{"03:00"}} + scheduled := EvolutionConfig{ + Enabled: true, + Mode: "apply", + ColdPathTrigger: "scheduled", + ColdPathTimes: []string{"03:00"}, + } assert.Equal(t, "scheduled", scheduled.ColdPathTriggerMode()) assert.False(t, scheduled.RunsColdPathAfterTurn()) assert.True(t, scheduled.RunsColdPathScheduled()) @@ -448,8 +453,8 @@ func TestSaveConfig_DisabledEvolutionOmitsApplyMode(t *testing.T) { } var raw map[string]any - if err := json.Unmarshal(data, &raw); err != nil { - t.Fatalf("Unmarshal saved config: %v", err) + if unmarshalErr := json.Unmarshal(data, &raw); unmarshalErr != nil { + t.Fatalf("Unmarshal saved config: %v", unmarshalErr) } evolutionRaw, ok := raw["evolution"].(map[string]any) if !ok { @@ -464,8 +469,8 @@ func TestSaveConfig_DisabledEvolutionOmitsApplyMode(t *testing.T) { if err != nil { t.Fatalf("Marshal edited config: %v", err) } - if err := os.WriteFile(configPath, edited, 0o600); err != nil { - t.Fatalf("WriteFile(configPath): %v", err) + if writeErr := os.WriteFile(configPath, edited, 0o600); writeErr != nil { + t.Fatalf("WriteFile(configPath): %v", writeErr) } loaded, err := LoadConfig(configPath) diff --git a/pkg/evolution/apply.go b/pkg/evolution/apply.go index d6a5e4c63..6a0db20d5 100644 --- a/pkg/evolution/apply.go +++ b/pkg/evolution/apply.go @@ -8,9 +8,10 @@ import ( "strings" "time" + "gopkg.in/yaml.v3" + "github.com/sipeed/picoclaw/pkg/fileutil" "github.com/sipeed/picoclaw/pkg/skills" - "gopkg.in/yaml.v3" ) type Applier struct { @@ -37,14 +38,18 @@ func (a *Applier) ApplyDraft(ctx context.Context, workspace string, draft SkillD return nil } -func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string, draft SkillDraft) (func() error, error) { +func (a *Applier) applyDraftWithRollback( + ctx context.Context, + workspace string, + draft SkillDraft, +) (func() error, error) { select { case <-ctx.Done(): return nil, ctx.Err() default: } - if err := skills.ValidateSkillName(draft.TargetSkillName); err != nil { - return nil, err + if validateErr := skills.ValidateSkillName(draft.TargetSkillName); validateErr != nil { + return nil, validateErr } existingBody, backupPath, hadOriginal, err := a.backupCurrentSkill(workspace, draft.TargetSkillName) @@ -53,8 +58,8 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string, } skillDir := filepath.Join(workspace, "skills", draft.TargetSkillName) - if err := os.MkdirAll(skillDir, 0o755); err != nil { - return nil, err + if mkdirErr := os.MkdirAll(skillDir, 0o755); mkdirErr != nil { + return nil, mkdirErr } renderedBody, err := renderAppliedBody(draft, existingBody, hadOriginal) @@ -67,7 +72,11 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string, return nil, err } - if err := validateAppliedSkillBody(renderedBody, draft.TargetSkillName, allowsExistingFrontmatterFields(draft.ChangeKind, hadOriginal)); err != nil { + if err := validateAppliedSkillBody( + renderedBody, + draft.TargetSkillName, + allowsExistingFrontmatterFields(draft.ChangeKind, hadOriginal), + ); err != nil { if rollbackErr := a.rollbackSkill(skillPath, backupPath, hadOriginal); rollbackErr != nil { return nil, errorsJoin(err, rollbackErr) } @@ -79,9 +88,11 @@ func (a *Applier) applyDraftWithRollback(ctx context.Context, workspace string, }, nil } -func (a *Applier) backupCurrentSkill(workspace, skillName string) (currentBody, backupPath string, hadOriginal bool, err error) { - if err := skills.ValidateSkillName(skillName); err != nil { - return "", "", false, err +func (a *Applier) backupCurrentSkill( + workspace, skillName string, +) (currentBody, backupPath string, hadOriginal bool, err error) { + if validateErr := skills.ValidateSkillName(skillName); validateErr != nil { + return "", "", false, validateErr } skillPath := filepath.Join(workspace, "skills", skillName, "SKILL.md") @@ -217,7 +228,11 @@ func renderDeployablePatchBody(body, targetSkillName string) (string, error) { return "", err } if name := strings.TrimSpace(fields["name"]); name != "" && name != targetSkillName { - return "", fmt.Errorf("skill patch frontmatter name %q does not match target skill %q", name, targetSkillName) + return "", fmt.Errorf( + "skill patch frontmatter name %q does not match target skill %q", + name, + targetSkillName, + ) } } return strings.TrimSpace(stripLeadingH1(markdownBody)), nil diff --git a/pkg/evolution/apply_test.go b/pkg/evolution/apply_test.go index 06e32d25a..36e4e21e5 100644 --- a/pkg/evolution/apply_test.go +++ b/pkg/evolution/apply_test.go @@ -101,7 +101,10 @@ func TestApplier_CreateDraftRendersDeployableSkillWithoutLearningTrace(t *testin if !strings.Contains(content, "Use native-name query first.") { t.Fatalf("deployed skill lost procedure:\n%s", content) } - if !strings.Contains(content, "description: Perform mathematical calculations by applying specific theorems and their associated rules.") { + if !strings.Contains( + content, + "description: Perform mathematical calculations by applying specific theorems and their associated rules.", + ) { t.Fatalf("deployed skill did not clean description:\n%s", content) } } @@ -682,20 +685,23 @@ func TestApplier_BackupsAreScopedByWorkspace(t *testing.T) { } var backupBodies []string - if err := filepath.WalkDir(filepath.Join(sharedState, "backups"), func(path string, entry os.DirEntry, err error) error { - if err != nil { - return err - } - if entry.IsDir() || entry.Name() != "SKILL.md" { + if err := filepath.WalkDir( + filepath.Join(sharedState, "backups"), + func(path string, entry os.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() || entry.Name() != "SKILL.md" { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + backupBodies = append(backupBodies, string(data)) return nil - } - data, err := os.ReadFile(path) - if err != nil { - return err - } - backupBodies = append(backupBodies, string(data)) - return nil - }); err != nil { + }, + ); err != nil { t.Fatalf("WalkDir(backups): %v", err) } diff --git a/pkg/evolution/cold_path_runner_test.go b/pkg/evolution/cold_path_runner_test.go index 3582fa6eb..2a0b28309 100644 --- a/pkg/evolution/cold_path_runner_test.go +++ b/pkg/evolution/cold_path_runner_test.go @@ -8,10 +8,10 @@ import ( ) type blockingColdPathRuntime struct { - runCount atomic.Int32 + runCount atomic.Int32 cancelCount atomic.Int32 - started chan string - release chan struct{} + started chan string + release chan struct{} } func (r *blockingColdPathRuntime) RunColdPathOnce(ctx context.Context, workspace string) error { diff --git a/pkg/evolution/drafts.go b/pkg/evolution/drafts.go index 05cb480d9..0d48d6605 100644 --- a/pkg/evolution/drafts.go +++ b/pkg/evolution/drafts.go @@ -16,7 +16,12 @@ type DraftGenerator interface { } type EvidenceAwareDraftGenerator interface { - GenerateDraftWithEvidence(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error) + GenerateDraftWithEvidence( + ctx context.Context, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, + ) (SkillDraft, error) } type DraftEvidence struct { @@ -72,11 +77,20 @@ func NewDefaultDraftGenerator(workspace string) *DefaultDraftGenerator { } } -func (g *DefaultDraftGenerator) GenerateDraft(_ context.Context, rule LearningRecord, matches []skills.SkillInfo) (SkillDraft, error) { +func (g *DefaultDraftGenerator) GenerateDraft( + _ context.Context, + rule LearningRecord, + matches []skills.SkillInfo, +) (SkillDraft, error) { return g.GenerateDraftWithEvidence(context.Background(), rule, matches, DraftEvidence{}) } -func (g *DefaultDraftGenerator) GenerateDraftWithEvidence(_ context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error) { +func (g *DefaultDraftGenerator) GenerateDraftWithEvidence( + _ context.Context, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, +) (SkillDraft, error) { rule = enrichRuleWithDraftEvidence(rule, evidence) target := inferTargetSkillName(rule, matches) if target == "" { @@ -185,7 +199,9 @@ func inferCombinedSkillName(rule LearningRecord) string { tokens := tokenizeForEvolution(rule.Summary) suffix := commonWinningPathSuffix(path) if len(tokens) == 1 && isNumericToken(tokens[0]) && suffix != "" { - if candidate := validSkillNameOrEmpty("calculate-" + tokens[0] + "-via-" + pluralizeSuffix(suffix)); candidate != "" { + if candidate := validSkillNameOrEmpty( + "calculate-" + tokens[0] + "-via-" + pluralizeSuffix(suffix), + ); candidate != "" { return candidate } } @@ -334,8 +350,16 @@ func (g *DefaultDraftGenerator) buildHumanSummary(target string, rule LearningRe return fmt.Sprintf("Create %s from learned pattern: %s", target, rule.Summary) } -func (g *DefaultDraftGenerator) buildNewSkillBody(target string, rule LearningRecord, evidence DraftEvidence, matches []skills.SkillInfo) string { - description := fmt.Sprintf("Use this skill to %s when the task matches this workflow.", sentenceFragment(fallbackString(rule.Summary, target))) +func (g *DefaultDraftGenerator) buildNewSkillBody( + target string, + rule LearningRecord, + evidence DraftEvidence, + matches []skills.SkillInfo, +) string { + description := fmt.Sprintf( + "Use this skill to %s when the task matches this workflow.", + sentenceFragment(fallbackString(rule.Summary, target)), + ) body := strings.Join([]string{ "# " + titleCaseSkillName(target), "", @@ -363,7 +387,11 @@ func (g *DefaultDraftGenerator) buildNewSkillBody(target string, rule LearningRe return buildSkillDocument(target, description, body) } -func (g *DefaultDraftGenerator) buildAppendBody(rule LearningRecord, evidence DraftEvidence, matches []skills.SkillInfo) string { +func (g *DefaultDraftGenerator) buildAppendBody( + rule LearningRecord, + evidence DraftEvidence, + matches []skills.SkillInfo, +) string { return strings.Join([]string{ "## Learned Evolution", fmt.Sprintf("- Summary: %s", strings.TrimSpace(rule.Summary)), @@ -420,26 +448,28 @@ func (g *DefaultDraftGenerator) learnedPatternLine(rule LearningRecord) string { ) } if len(rule.WinningPath) > 0 { - return fmt.Sprintf("Prefer `%s` because it was the most reliable recent path.", strings.Join(rule.WinningPath, " -> ")) + return fmt.Sprintf( + "Prefer `%s` because it was the most reliable recent path.", + strings.Join(rule.WinningPath, " -> "), + ) } return fmt.Sprintf("Prefer the pattern summarized as `%s`.", strings.TrimSpace(rule.Summary)) } -func (g *DefaultDraftGenerator) winningPathLine(rule LearningRecord) string { - if len(rule.WinningPath) == 0 { - return "No explicit winning path was recorded." - } - return strings.Join(rule.WinningPath, " -> ") -} - func (g *DefaultDraftGenerator) procedureLine(rule LearningRecord, evidence DraftEvidence) string { if len(rule.WinningPath) > 0 { - return fmt.Sprintf("Follow `%s`, applying the concrete operation from each source skill, then return the final result directly.", strings.Join(rule.WinningPath, " -> ")) + return fmt.Sprintf( + "Follow `%s`, applying the concrete operation from each source skill, then return the final result directly.", + strings.Join(rule.WinningPath, " -> "), + ) } if excerpt := firstFinalOutputExcerpt(evidence, 260); excerpt != "" { return "Use the same operation demonstrated by the source task result: " + excerpt } - return fmt.Sprintf("Solve tasks matching `%s` using the learned successful workflow, then return the final result directly.", strings.TrimSpace(rule.Summary)) + return fmt.Sprintf( + "Solve tasks matching `%s` using the learned successful workflow, then return the final result directly.", + strings.TrimSpace(rule.Summary), + ) } func (g *DefaultDraftGenerator) expectedResultLine(evidence DraftEvidence) string { diff --git a/pkg/evolution/drafts_test.go b/pkg/evolution/drafts_test.go index 21cbbb04b..c45c3b69f 100644 --- a/pkg/evolution/drafts_test.go +++ b/pkg/evolution/drafts_test.go @@ -45,9 +45,21 @@ func TestDefaultDraftGenerator_PrefersCombinedSkillForStableMultiSkillPath(t *te EventCount: 3, SuccessRate: 1, }, []skills.SkillInfo{ - {Name: "three-one-theorem", Path: filepath.Join(workspace, "skills", "three-one-theorem", "SKILL.md"), Source: "workspace"}, - {Name: "four-two-theorem", Path: filepath.Join(workspace, "skills", "four-two-theorem", "SKILL.md"), Source: "workspace"}, - {Name: "five-three-theorem", Path: filepath.Join(workspace, "skills", "five-three-theorem", "SKILL.md"), Source: "workspace"}, + { + Name: "three-one-theorem", + Path: filepath.Join(workspace, "skills", "three-one-theorem", "SKILL.md"), + Source: "workspace", + }, + { + Name: "four-two-theorem", + Path: filepath.Join(workspace, "skills", "four-two-theorem", "SKILL.md"), + Source: "workspace", + }, + { + Name: "five-three-theorem", + Path: filepath.Join(workspace, "skills", "five-three-theorem", "SKILL.md"), + Source: "workspace", + }, }) if err != nil { t.Fatalf("GenerateDraft: %v", err) @@ -85,7 +97,10 @@ func TestDefaultDraftGenerator_CombinedSkillIncludesEvidenceAndSourceOperations( if err := os.WriteFile(skillPath, []byte(content), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } - matches = append(matches, skills.SkillInfo{Name: source.name, Path: skillPath, Source: "workspace", Description: "theorem helper"}) + matches = append( + matches, + skills.SkillInfo{Name: source.name, Path: skillPath, Source: "workspace", Description: "theorem helper"}, + ) } draft, err := generator.GenerateDraftWithEvidence(context.Background(), evolution.LearningRecord{ diff --git a/pkg/evolution/llm_draft_generator.go b/pkg/evolution/llm_draft_generator.go index 4881163cd..2db27004c 100644 --- a/pkg/evolution/llm_draft_generator.go +++ b/pkg/evolution/llm_draft_generator.go @@ -35,11 +35,20 @@ func NewLLMDraftGenerator(provider providers.LLMProvider, model string, fallback } } -func (g *LLMDraftGenerator) GenerateDraft(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo) (SkillDraft, error) { +func (g *LLMDraftGenerator) GenerateDraft( + ctx context.Context, + rule LearningRecord, + matches []skills.SkillInfo, +) (SkillDraft, error) { return g.GenerateDraftWithEvidence(ctx, rule, matches, DraftEvidence{}) } -func (g *LLMDraftGenerator) GenerateDraftWithEvidence(ctx context.Context, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) (SkillDraft, error) { +func (g *LLMDraftGenerator) GenerateDraftWithEvidence( + ctx context.Context, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, +) (SkillDraft, error) { rule = enrichRuleWithDraftEvidence(rule, evidence) if g == nil || g.provider == nil { return g.generateFallback(ctx, rule, matches, evidence) @@ -97,7 +106,11 @@ func (g *LLMDraftGenerator) generateFallback( return g.fallback.GenerateDraft(ctx, rule, matches) } -func (g *LLMDraftGenerator) buildPrompt(rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string { +func (g *LLMDraftGenerator) buildPrompt( + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, +) string { return strings.Join([]string{ "Generate a skill draft JSON object with these required string fields:", `target_skill_name, draft_type, change_kind, human_summary, body_or_patch.`, diff --git a/pkg/evolution/llm_draft_generator_test.go b/pkg/evolution/llm_draft_generator_test.go index 269f34c81..ffba62b84 100644 --- a/pkg/evolution/llm_draft_generator_test.go +++ b/pkg/evolution/llm_draft_generator_test.go @@ -125,7 +125,13 @@ func TestLLMDraftGenerator_BuildPromptIncludesMatchedSkillContent(t *testing.T) if err := os.MkdirAll(filepath.Dir(skillPath), 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } - if err := os.WriteFile(skillPath, []byte("---\nname: three-one-theorem\ndescription: Add 31 then delegate\n---\n# Three One\nAdd 31 to the input, then continue with the next theorem.\n"), 0o644); err != nil { + if err := os.WriteFile( + skillPath, + []byte( + "---\nname: three-one-theorem\ndescription: Add 31 then delegate\n---\n# Three One\nAdd 31 to the input, then continue with the next theorem.\n", + ), + 0o644, + ); err != nil { t.Fatalf("WriteFile: %v", err) } @@ -171,13 +177,22 @@ func TestLLMDraftGenerator_BuildPromptIncludesMatchedSkillContent(t *testing.T) if !strings.Contains(prompt, "The YAML frontmatter must contain only name and description fields") { t.Fatalf("prompt missing frontmatter instruction:\n%s", prompt) } - if !strings.Contains(prompt, "The description field must and only describe what this skill can do and when to use it") { + if !strings.Contains( + prompt, + "The description field must and only describe what this skill can do and when to use it", + ) { t.Fatalf("prompt missing description field instruction:\n%s", prompt) } - if !strings.Contains(prompt, "The deployable Markdown body should only contain what the skill is useful for and how to use it") { + if !strings.Contains( + prompt, + "The deployable Markdown body should only contain what the skill is useful for and how to use it", + ) { t.Fatalf("prompt missing deployable body scope instruction:\n%s", prompt) } - if !strings.Contains(prompt, "provide detailed step-by-step instructions for the exact operation or execution process") { + if !strings.Contains( + prompt, + "provide detailed step-by-step instructions for the exact operation or execution process", + ) { t.Fatalf("prompt missing step-by-step instruction:\n%s", prompt) } if !strings.Contains(prompt, "body_or_patch is an internal draft and review artifact") { diff --git a/pkg/evolution/pattern_clusterer.go b/pkg/evolution/pattern_clusterer.go index ca0655bb4..b167c7432 100644 --- a/pkg/evolution/pattern_clusterer.go +++ b/pkg/evolution/pattern_clusterer.go @@ -15,11 +15,23 @@ import ( ) type PatternClusterer interface { - BuildPatterns(ctx context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error) + BuildPatterns( + ctx context.Context, + workspace string, + tasks []LearningRecord, + existing []LearningRecord, + ) ([]LearningRecord, []string, error) } type evidencePatternClusterer interface { - BuildPatternsWithEvidence(ctx context.Context, workspace string, successfulTasks []LearningRecord, evidenceTasks []LearningRecord, existing []LearningRecord, minSuccessRatio float64) ([]LearningRecord, []string, error) + BuildPatternsWithEvidence( + ctx context.Context, + workspace string, + successfulTasks []LearningRecord, + evidenceTasks []LearningRecord, + existing []LearningRecord, + minSuccessRatio float64, + ) ([]LearningRecord, []string, error) } type HeuristicPatternClusterer struct { @@ -37,7 +49,12 @@ func NewHeuristicPatternClusterer(minCaseCount int, now func() time.Time) *Heuri return &HeuristicPatternClusterer{minCaseCount: minCaseCount, now: now} } -func (c *HeuristicPatternClusterer) BuildPatterns(_ context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error) { +func (c *HeuristicPatternClusterer) BuildPatterns( + _ context.Context, + workspace string, + tasks []LearningRecord, + existing []LearningRecord, +) ([]LearningRecord, []string, error) { groups := make(map[string][]LearningRecord) keys := make([]string, 0) for _, task := range tasks { @@ -68,7 +85,15 @@ func (c *HeuristicPatternClusterer) BuildPatterns(_ context.Context, workspace s if !hasExisting && len(cluster) < c.minCaseCount { continue } - pattern := buildPatternFromCluster(workspace, label, heuristicClusterSummary(label, cluster), "heuristic cluster by normalized task summary", cluster, existingPattern, c.now()) + pattern := buildPatternFromCluster( + workspace, + label, + heuristicClusterSummary(label, cluster), + "heuristic cluster by normalized task summary", + cluster, + existingPattern, + c.now(), + ) patterns = append(patterns, pattern) clusteredIDs = append(clusteredIDs, collectRecordIDs(cluster)...) } @@ -94,7 +119,13 @@ type llmCluster struct { Reason string `json:"cluster_reason"` } -func NewLLMPatternClusterer(provider providers.LLMProvider, model string, fallback PatternClusterer, minCount int, now func() time.Time) *LLMPatternClusterer { +func NewLLMPatternClusterer( + provider providers.LLMProvider, + model string, + fallback PatternClusterer, + minCount int, + now func() time.Time, +) *LLMPatternClusterer { if fallback == nil { fallback = NewHeuristicPatternClusterer(minCount, now) } @@ -113,7 +144,12 @@ func NewLLMPatternClusterer(provider providers.LLMProvider, model string, fallba } } -func (c *LLMPatternClusterer) BuildPatterns(ctx context.Context, workspace string, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string, error) { +func (c *LLMPatternClusterer) BuildPatterns( + ctx context.Context, + workspace string, + tasks []LearningRecord, + existing []LearningRecord, +) ([]LearningRecord, []string, error) { if c == nil { return NewHeuristicPatternClusterer(0, nil).BuildPatterns(ctx, workspace, tasks, existing) } @@ -175,14 +211,30 @@ func (c *LLMPatternClusterer) BuildPatternsWithEvidence( fallback = NewHeuristicPatternClusterer(c.minCount, c.now) } if c.provider == nil { - return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio) + return buildFallbackPatternsWithEvidence( + ctx, + fallback, + workspace, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) } model := strings.TrimSpace(c.model) if model == "" { model = strings.TrimSpace(c.provider.GetDefaultModel()) } if model == "" { - return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio) + return buildFallbackPatternsWithEvidence( + ctx, + fallback, + workspace, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) } if len(evidenceTasks) == 0 { evidenceTasks = successfulTasks @@ -201,17 +253,48 @@ func (c *LLMPatternClusterer) BuildPatternsWithEvidence( }, }, nil, model, map[string]any{"temperature": 0}) if err != nil || resp == nil || strings.TrimSpace(resp.Content) == "" { - return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio) + return buildFallbackPatternsWithEvidence( + ctx, + fallback, + workspace, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) } payload, ok := parseLLMClusterResponse(resp.Content) if !ok { - return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio) + return buildFallbackPatternsWithEvidence( + ctx, + fallback, + workspace, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) } if len(payload.Clusters) == 0 { - return buildFallbackPatternsWithEvidence(ctx, fallback, workspace, successfulTasks, evidenceTasks, existing, minSuccessRatio) + return buildFallbackPatternsWithEvidence( + ctx, + fallback, + workspace, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) } - patterns, clusteredIDs := c.validateAndBuildPatternsWithEvidence(workspace, payload.Clusters, successfulTasks, evidenceTasks, existing, minSuccessRatio) + patterns, clusteredIDs := c.validateAndBuildPatternsWithEvidence( + workspace, + payload.Clusters, + successfulTasks, + evidenceTasks, + existing, + minSuccessRatio, + ) return patterns, clusteredIDs, nil } @@ -316,7 +399,12 @@ func buildFallbackPatternsWithEvidence( return filteredPatterns, appendUniqueStrings(nil, clusteredIDs...), nil } -func (c *LLMPatternClusterer) validateAndBuildPatterns(workspace string, clusters []llmCluster, tasks []LearningRecord, existing []LearningRecord) ([]LearningRecord, []string) { +func (c *LLMPatternClusterer) validateAndBuildPatterns( + workspace string, + clusters []llmCluster, + tasks []LearningRecord, + existing []LearningRecord, +) ([]LearningRecord, []string) { taskByID := make(map[string]LearningRecord, len(tasks)) for _, task := range tasks { taskByID[task.ID] = task @@ -354,7 +442,15 @@ func (c *LLMPatternClusterer) validateAndBuildPatterns(workspace string, cluster if len(clusterTasks) == 0 { continue } - pattern := buildPatternFromCluster(workspace, label, cluster.Summary, cluster.Reason, clusterTasks, existingPattern, c.now()) + pattern := buildPatternFromCluster( + workspace, + label, + cluster.Summary, + cluster.Reason, + clusterTasks, + existingPattern, + c.now(), + ) patterns = append(patterns, pattern) clusteredIDs = append(clusteredIDs, collectRecordIDs(clusterTasks)...) } @@ -420,7 +516,15 @@ func (c *LLMPatternClusterer) validateAndBuildPatternsWithEvidence( if !hasExisting && len(clusterSuccesses) < c.minCount { continue } - pattern := buildPatternFromCluster(workspace, label, cluster.Summary, cluster.Reason, clusterSuccesses, existingPattern, c.now()) + pattern := buildPatternFromCluster( + workspace, + label, + cluster.Summary, + cluster.Reason, + clusterSuccesses, + existingPattern, + c.now(), + ) patterns = append(patterns, pattern) clusteredIDs = append(clusteredIDs, collectRecordIDs(clusterEvidence)...) } @@ -488,7 +592,12 @@ func buildPatternClusterPrompt(workspace string, tasks []LearningRecord, existin return string(data) } -func buildPatternFromCluster(workspace, label, summary, reason string, tasks []LearningRecord, existing LearningRecord, now time.Time) LearningRecord { +func buildPatternFromCluster( + workspace, label, summary, reason string, + tasks []LearningRecord, + existing LearningRecord, + now time.Time, +) LearningRecord { taskIDs := append([]string(nil), existing.TaskRecordIDs...) taskIDs = appendUniqueStrings(taskIDs, collectRecordIDs(tasks)...) if summary = strings.TrimSpace(summary); summary == "" { diff --git a/pkg/evolution/pattern_clusterer_test.go b/pkg/evolution/pattern_clusterer_test.go index 2da9c1872..0e0c91128 100644 --- a/pkg/evolution/pattern_clusterer_test.go +++ b/pkg/evolution/pattern_clusterer_test.go @@ -312,59 +312,14 @@ func TestLLMPatternClusterer_MarksAllAcceptedEvidenceClusteredButStoresSuccessfu content: `{"clusters":[{"label":"weather-lookup","summary":"lookup weather","task_record_ids":["task-success","task-failed"],"cluster_reason":"same weather lookup goal"}]}`, defaultModel: "test-model", } - clusterer := evolution.NewLLMPatternClusterer( + assertClustererMarksAllAcceptedEvidenceClustered( + t, provider, - "test-model", - evolution.NewHeuristicPatternClusterer(1, nil), - 1, - func() time.Time { return time.Unix(1700000000, 0).UTC() }, + "weather lookup shanghai", + "forecast for shanghai", + "could not complete", + "1", ) - success := true - failed := false - successfulTasks := []evolution.LearningRecord{ - { - ID: "task-success", - Kind: evolution.RecordKindTask, - WorkspaceID: "workspace-a", - Summary: "weather lookup shanghai", - FinalOutput: "sunny", - Status: evolution.RecordStatus("new"), - Success: &success, - }, - } - evidenceTasks := []evolution.LearningRecord{ - successfulTasks[0], - { - ID: "task-failed", - Kind: evolution.RecordKindTask, - WorkspaceID: "workspace-a", - Summary: "forecast for shanghai", - FinalOutput: "could not complete", - Status: evolution.RecordStatus("new"), - Success: &failed, - }, - } - - patterns, clusteredIDs, err := clusterer.BuildPatternsWithEvidence( - context.Background(), - "workspace-a", - successfulTasks, - evidenceTasks, - nil, - 0.5, - ) - if err != nil { - t.Fatalf("BuildPatternsWithEvidence: %v", err) - } - if len(patterns) != 1 { - t.Fatalf("len(patterns) = %d, want 1: %#v", len(patterns), patterns) - } - if got := strings.Join(patterns[0].TaskRecordIDs, ","); got != "task-success" { - t.Fatalf("pattern TaskRecordIDs = %v, want only successful task", patterns[0].TaskRecordIDs) - } - if got := strings.Join(clusteredIDs, ","); got != "task-success,task-failed" { - t.Fatalf("clusteredIDs = %v, want all accepted evidence IDs", clusteredIDs) - } } func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testing.T) { @@ -372,6 +327,25 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin content: `not-json`, defaultModel: "test-model", } + assertClustererMarksAllAcceptedEvidenceClustered( + t, + provider, + "weather lookup 100", + "weather lookup 200", + "partial result", + "fallback pattern", + ) +} + +func assertClustererMarksAllAcceptedEvidenceClustered( + t *testing.T, + provider *llmClusterTestProvider, + successSummary string, + failedSummary string, + failedOutput string, + wantPatternDescription string, +) { + t.Helper() clusterer := evolution.NewLLMPatternClusterer( provider, "test-model", @@ -386,7 +360,7 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin ID: "task-success", Kind: evolution.RecordKindTask, WorkspaceID: "workspace-a", - Summary: "weather lookup 100", + Summary: successSummary, FinalOutput: "sunny", Status: evolution.RecordStatus("new"), Success: &success, @@ -398,8 +372,8 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin ID: "task-failed", Kind: evolution.RecordKindTask, WorkspaceID: "workspace-a", - Summary: "weather lookup 200", - FinalOutput: "partial result", + Summary: failedSummary, + FinalOutput: failedOutput, Status: evolution.RecordStatus("new"), Success: &failed, }, @@ -417,7 +391,7 @@ func TestLLMPatternClusterer_FallbackMarksAllAcceptedEvidenceClustered(t *testin t.Fatalf("BuildPatternsWithEvidence: %v", err) } if len(patterns) != 1 { - t.Fatalf("len(patterns) = %d, want fallback pattern: %#v", len(patterns), patterns) + t.Fatalf("len(patterns) = %d, want %s: %#v", len(patterns), wantPatternDescription, patterns) } if got := strings.Join(patterns[0].TaskRecordIDs, ","); got != "task-success" { t.Fatalf("pattern TaskRecordIDs = %v, want only successful task", patterns[0].TaskRecordIDs) diff --git a/pkg/evolution/preview.go b/pkg/evolution/preview.go index db14202ab..ee2774136 100644 --- a/pkg/evolution/preview.go +++ b/pkg/evolution/preview.go @@ -75,11 +75,17 @@ func buildLineDiffPreview(currentBody, renderedBody string) string { } lines := make([]string, 0, (hunkBeforeEnd-hunkBeforeStart)+(hunkAfterEnd-hunkAfterStart)) - header := []string{ + header := make([]string, 0, 3+len(lines)) + header = append(header, "--- current", "+++ rendered", - formatUnifiedHunkHeader(hunkBeforeStart, hunkBeforeEnd-hunkBeforeStart, hunkAfterStart, hunkAfterEnd-hunkAfterStart), - } + formatUnifiedHunkHeader( + hunkBeforeStart, + hunkBeforeEnd-hunkBeforeStart, + hunkAfterStart, + hunkAfterEnd-hunkAfterStart, + ), + ) for _, line := range before[hunkBeforeStart:beforeChangeStart] { lines = append(lines, " "+line) } @@ -96,7 +102,13 @@ func buildLineDiffPreview(currentBody, renderedBody string) string { } func formatUnifiedHunkHeader(beforeStart, beforeCount, afterStart, afterCount int) string { - return "@@ -" + formatUnifiedRange(beforeStart+1, beforeCount) + " +" + formatUnifiedRange(afterStart+1, afterCount) + " @@" + return "@@ -" + formatUnifiedRange( + beforeStart+1, + beforeCount, + ) + " +" + formatUnifiedRange( + afterStart+1, + afterCount, + ) + " @@" } func formatUnifiedRange(start, count int) string { diff --git a/pkg/evolution/profile_sync.go b/pkg/evolution/profile_sync.go index 41f1fc46e..1499a6d44 100644 --- a/pkg/evolution/profile_sync.go +++ b/pkg/evolution/profile_sync.go @@ -64,6 +64,12 @@ func inferAvoidPatterns(rule LearningRecord) []string { return nil } return []string{ - "avoid starting with " + strings.Join(prefix, " -> ") + " before using " + strings.Join(rule.LateAddedSkills, " -> "), + "avoid starting with " + strings.Join( + prefix, + " -> ", + ) + " before using " + strings.Join( + rule.LateAddedSkills, + " -> ", + ), } } diff --git a/pkg/evolution/runtime.go b/pkg/evolution/runtime.go index 8aa06ea55..cb7d26128 100644 --- a/pkg/evolution/runtime.go +++ b/pkg/evolution/runtime.go @@ -278,11 +278,19 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error admittedCount := 0 newRuleCount := 0 if rt.patternClusterer != nil { - recordsForOrganizer, evidenceRecordsForOrganizer, err := rt.recordsForColdPathInputs(ctx, workspace, taskRecords) - if err != nil { - return err + recordsForOrganizer, evidenceRecordsForOrganizer, inputErr := rt.recordsForColdPathInputs( + ctx, + workspace, + taskRecords, + ) + if inputErr != nil { + return inputErr } - recordsForOrganizer = rt.filterRecordsByMinSuccessRatio(workspace, evidenceRecordsForOrganizer, recordsForOrganizer) + recordsForOrganizer = rt.filterRecordsByMinSuccessRatio( + workspace, + evidenceRecordsForOrganizer, + recordsForOrganizer, + ) admittedCount = countTaskLearningRecords(recordsForOrganizer) logger.DebugCF("evolution", "Admitted task records for cold path", map[string]any{ "workspace": workspace, @@ -303,7 +311,12 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error rt.cfg.EffectiveMinSuccessRatio(), ) } else { - rules, clusteredTaskIDs, err = rt.patternClusterer.BuildPatterns(ctx, workspace, recordsForOrganizer, patternRecords) + rules, clusteredTaskIDs, err = rt.patternClusterer.BuildPatterns( + ctx, + workspace, + recordsForOrganizer, + patternRecords, + ) } if err != nil { return err @@ -319,14 +332,14 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error }) if len(rules) > 0 { merged := mergePatternRecords(patternRecords, rules, workspace) - if err := store.MergePatternRecords(rules); err != nil { - return err + if mergeErr := store.MergePatternRecords(rules); mergeErr != nil { + return mergeErr } patternRecords = merged } if len(clusteredTaskIDs) > 0 { - if err := markTaskRecordsClustered(store, clusteredTaskIDs); err != nil { - return err + if markErr := markTaskRecordsClustered(store, clusteredTaskIDs); markErr != nil { + return markErr } } } @@ -371,17 +384,21 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error } rule, ok := readyRuleByID[draft.SourceRecordID] if !ok { - logger.DebugCF("evolution", "Skipped existing candidate draft because its source pattern is not ready", map[string]any{ - "workspace": workspace, - "draft_id": draft.ID, - "source_record_id": draft.SourceRecordID, - "run_id": runID, - }) + logger.DebugCF( + "evolution", + "Skipped existing candidate draft because its source pattern is not ready", + map[string]any{ + "workspace": workspace, + "draft_id": draft.ID, + "source_record_id": draft.SourceRecordID, + "run_id": runID, + }, + ) continue } - matches, err := recaller.RecallSimilarSkills(rule) - if err != nil { - return err + matches, recallErr := recaller.RecallSimilarSkills(rule) + if recallErr != nil { + return recallErr } draft.MatchedSkillRefs = collectSkillRefs(matches) var normalizationNotes []string @@ -393,17 +410,14 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error draft.ScanFindings = appendUniqueStrings(draft.ScanFindings, review.Findings...) changedExistingDrafts = true if draft.Status != DraftStatusCandidate || mode != "apply" || applier == nil { - if err := store.SaveDrafts([]SkillDraft{draft}); err != nil { - return err + if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { + return saveErr } continue } - if mode != "apply" || applier == nil { - continue - } - updatedDraft, err := rt.applyCandidateDraft(ctx, workspace, store, applier, draft, runID) - if err != nil { - return err + updatedDraft, applyErr := rt.applyCandidateDraft(ctx, workspace, store, applier, draft, runID) + if applyErr != nil { + return applyErr } if updatedDraft.Status == DraftStatusAccepted { appliedExistingDrafts++ @@ -436,12 +450,16 @@ func (rt *Runtime) RunColdPathOnce(ctx context.Context, workspace string) error } if _, exists := existingBySource[rule.ID]; exists { - logger.DebugCF("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, - }) + logger.DebugCF( + "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 } @@ -639,35 +657,6 @@ func coldPathSuccessRatioKey(workspace string, record LearningRecord) (string, b return key, true } -func passesColdPathRuleFilter(record LearningRecord) bool { - return coldPathRuleRejectReason(record) == "" -} - -func coldPathRuleRejectReason(record LearningRecord) string { - if !isTaskRecordKind(record.Kind) { - return "not a task record" - } - if record.Success == nil || !*record.Success { - return "task not completed" - } - if record.Status != "" && record.Status != RecordStatus("new") { - return "task already processed" - } - if strings.EqualFold(strings.TrimSpace(record.SessionKey), "heartbeat") { - return "heartbeat session" - } - if strings.EqualFold(strings.TrimSpace(record.FinalOutput), "HEARTBEAT_OK") { - return "heartbeat output" - } - if strings.TrimSpace(record.Summary) == "" { - return "missing summary" - } - if strings.TrimSpace(record.FinalOutput) == "" { - return "missing final output" - } - return "" -} - func coldPathEvidenceRejectReason(record LearningRecord) string { if !isTaskRecordKind(record.Kind) { return "not a task record" @@ -744,7 +733,13 @@ func (rt *Runtime) applierForWorkspace(workspace string) *Applier { return rt.applier } -func (rt *Runtime) finalizeDraft(workspace string, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence, draft SkillDraft) SkillDraft { +func (rt *Runtime) finalizeDraft( + workspace string, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, + draft SkillDraft, +) SkillDraft { if draft.ID == "" { draft.ID = "draft-" + rule.ID } @@ -843,7 +838,12 @@ func looksLikeSkillDocument(body string) bool { return strings.HasPrefix(body, "---\n") && strings.Contains(body, "\n# ") } -func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, rule LearningRecord, evidence DraftEvidence) string { +func synthesizeSkillDocumentFromPartialDraft( + target string, + draft SkillDraft, + rule LearningRecord, + evidence DraftEvidence, +) string { description := strings.TrimSpace(draft.HumanSummary) if description == "" { description = fmt.Sprintf("Learned workflow for %s.", target) @@ -876,7 +876,13 @@ func synthesizeSkillDocumentFromPartialDraft(target string, draft SkillDraft, ru return buildSkillDocument(target, description, body) } -func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string { +func synthesizeCombinedSkillDocument( + target string, + draft SkillDraft, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, +) string { description := strings.TrimSpace(draft.HumanSummary) if description == "" { description = buildCombinedSkillHumanSummary(target, rule, false) @@ -908,7 +914,13 @@ func synthesizeCombinedSkillDocument(target string, draft SkillDraft, rule Learn return buildSkillDocument(target, description, body) } -func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule LearningRecord, matches []skills.SkillInfo, evidence DraftEvidence) string { +func synthesizeCombinedSkillAppendBody( + target string, + draft SkillDraft, + rule LearningRecord, + matches []skills.SkillInfo, + evidence DraftEvidence, +) string { lines := []string{ "## Learned Shortcut Update", fmt.Sprintf("- Shortcut skill: `%s`", target), @@ -929,7 +941,11 @@ func synthesizeCombinedSkillAppendBody(target string, draft SkillDraft, rule Lea 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)) + 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) @@ -945,7 +961,11 @@ 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, " -> ")) + return fmt.Sprintf( + "Use `%s` as a direct shortcut instead of replaying `%s` step by step.", + target, + strings.Join(rule.WinningPath, " -> "), + ) } func synthesizedCombinedProcedure(matches []skills.SkillInfo, rule LearningRecord) string { @@ -954,7 +974,10 @@ func synthesizedCombinedProcedure(matches []skills.SkillInfo, rule LearningRecor if len(rule.WinningPath) == 0 { return "Use the learned shortcut directly and keep the response focused on the requested result." } - return fmt.Sprintf("Apply the recorded path `%s`, then return the final result with only the necessary explanation.", strings.Join(rule.WinningPath, " -> ")) + return fmt.Sprintf( + "Apply the recorded path `%s`, then return the final result with only the necessary explanation.", + strings.Join(rule.WinningPath, " -> "), + ) } return "Follow the source skill guidance below as one compact procedure, then return the final result without replaying unnecessary discovery steps." } @@ -994,12 +1017,18 @@ func synthesizedWrappedPathLine(rule LearningRecord) string { func synthesizedCombinedLearnedContent(body string, rule LearningRecord) string { content := strings.TrimSpace(stripSkillFrontmatter(body)) if content == "" { - return fmt.Sprintf("Learned from `%s`; use this shortcut directly when the same task pattern appears again.", fallbackEvolutionSummary(rule)) + return fmt.Sprintf( + "Learned from `%s`; use this shortcut directly when the same task pattern appears again.", + fallbackEvolutionSummary(rule), + ) } content = removeVerboseCombinedSections(content) content = strings.Join(strings.Fields(content), " ") if content == "" { - return fmt.Sprintf("Learned from `%s`; use this shortcut directly when the same task pattern appears again.", fallbackEvolutionSummary(rule)) + return fmt.Sprintf( + "Learned from `%s`; use this shortcut directly when the same task pattern appears again.", + fallbackEvolutionSummary(rule), + ) } content = trimAtReadableBoundary(content, 1200) return "- Learned task: " + fallbackEvolutionSummary(rule) + "\n- Reusable guidance: " + content @@ -1275,7 +1304,8 @@ func markTaskRecordsClustered(store *Store, ids []string) error { 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") { + if !isPatternRecordKind(record.Kind) || record.WorkspaceID != workspace || + record.Status != RecordStatus("ready") { continue } seen[record.ID] = record @@ -1341,7 +1371,10 @@ func (rt *Runtime) applyCandidateDraft( 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)) + draft.ScanFindings = appendUniqueStrings( + draft.ScanFindings, + fmt.Sprintf("rollback audit failed: %v", auditErr), + ) if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return draft, errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), auditErr, saveErr) } @@ -1379,7 +1412,10 @@ func (rt *Runtime) applyCandidateDraft( 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)) + draft.ScanFindings = appendUniqueStrings( + draft.ScanFindings, + fmt.Sprintf("apply rollback failed: %v", rollbackErr), + ) if saveErr := store.SaveDrafts([]SkillDraft{draft}); saveErr != nil { return draft, errorsJoin(fmt.Errorf("%w: %v", ErrApplyDraftFailed, err), rollbackErr, saveErr) } @@ -1401,21 +1437,25 @@ func (rt *Runtime) applyCandidateDraft( func (rt *Runtime) recordRollbackAudit(store *Store, draft SkillDraft, applyErr error) error { now := rt.now() - return store.UpdateProfile(draft.WorkspaceID, draft.TargetSkillName, func(profile *SkillProfile, exists bool) error { - if !exists { + return store.UpdateProfile( + draft.WorkspaceID, + draft.TargetSkillName, + func(profile *SkillProfile, exists bool) error { + if !exists { + return nil + } + 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 nil - } - 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 nil - }) + }, + ) } func profileOrigin(origin string) string { diff --git a/pkg/evolution/runtime_apply_test.go b/pkg/evolution/runtime_apply_test.go index ba5d85711..5f5a53185 100644 --- a/pkg/evolution/runtime_apply_test.go +++ b/pkg/evolution/runtime_apply_test.go @@ -62,13 +62,13 @@ func TestRuntime_RunColdPathOnce_ApplyModeWritesSkillAndProfile(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } skillPath := filepath.Join(root, "skills", "weather", "SKILL.md") - if _, err := os.Stat(skillPath); err != nil { - t.Fatalf("expected skill file: %v", err) + if _, statErr := os.Stat(skillPath); statErr != nil { + t.Fatalf("expected skill file: %v", statErr) } profile, err := store.LoadProfile("weather") @@ -90,7 +90,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeWritesSkillAndProfile(t *testing.T) { if len(profile.PreferredEntryPath) != 1 || profile.PreferredEntryPath[0] != "weather" { t.Fatalf("PreferredEntryPath = %v, want [weather]", profile.PreferredEntryPath) } - if len(profile.AvoidPatterns) != 1 || profile.AvoidPatterns[0] != "avoid translating city names before querying weather" { + if len(profile.AvoidPatterns) != 1 || + profile.AvoidPatterns[0] != "avoid translating city names before querying weather" { t.Fatalf("AvoidPatterns = %v, want populated metadata", profile.AvoidPatterns) } @@ -149,15 +150,15 @@ func TestRuntime_RunColdPathOnce_DraftModeKeepsCandidateDraft(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } - if _, err := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); !os.IsNotExist(err) { - t.Fatalf("expected no applied skill file, got err=%v", err) + if _, statErr := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); !os.IsNotExist(statErr) { + t.Fatalf("expected no applied skill file, got err=%v", statErr) } - if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) { - t.Fatalf("expected no profile, got err=%v", err) + if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) { + t.Fatalf("expected no profile, got err=%v", loadErr) } drafts, err := store.LoadDrafts() @@ -218,17 +219,19 @@ func TestRuntime_RunColdPathOnce_DraftModeRefreshesExistingCandidateWithEvidence }}); err != nil { t.Fatalf("SavePatternRecords: %v", err) } - if err := store.SaveDrafts([]evolution.SkillDraft{{ - ID: "draft-pattern-1", - WorkspaceID: root, - SourceRecordID: "pattern-1", - TargetSkillName: "learned-skill", - DraftType: evolution.DraftTypeShortcut, - ChangeKind: evolution.ChangeKindCreate, - HumanSummary: "old generic draft", - BodyOrPatch: "---\nname: learned-skill\ndescription: old\n---\n# Learned Skill\n\nNo explicit winning path was recorded.\n", - Status: evolution.DraftStatusCandidate, - }}); err != nil { + if err := store.SaveDrafts([]evolution.SkillDraft{ + { + ID: "draft-pattern-1", + WorkspaceID: root, + SourceRecordID: "pattern-1", + TargetSkillName: "learned-skill", + DraftType: evolution.DraftTypeShortcut, + ChangeKind: evolution.ChangeKindCreate, + HumanSummary: "old generic draft", + BodyOrPatch: "---\nname: learned-skill\ndescription: old\n---\n# Learned Skill\n\nNo explicit winning path was recorded.\n", + Status: evolution.DraftStatusCandidate, + }, + }); err != nil { t.Fatalf("SaveDrafts: %v", err) } @@ -241,8 +244,8 @@ func TestRuntime_RunColdPathOnce_DraftModeRefreshesExistingCandidateWithEvidence t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -326,15 +329,15 @@ func TestRuntime_RunColdPathOnce_ApplyModeAppliesExistingCandidateDraft(t *testi t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } - if _, err := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); err != nil { - t.Fatalf("expected existing candidate to be applied: %v", err) + if _, statErr := os.Stat(filepath.Join(root, "skills", "weather", "SKILL.md")); statErr != nil { + t.Fatalf("expected existing candidate to be applied: %v", statErr) } - if _, err := os.Stat(filepath.Join(root, "skills", "unused-weather", "SKILL.md")); !os.IsNotExist(err) { - t.Fatalf("expected source rule to stay skipped after applying existing draft, got err=%v", err) + if _, statErr := os.Stat(filepath.Join(root, "skills", "unused-weather", "SKILL.md")); !os.IsNotExist(statErr) { + t.Fatalf("expected source rule to stay skipped after applying existing draft, got err=%v", statErr) } profile, err := store.LoadProfile("weather") if err != nil { @@ -414,15 +417,15 @@ func TestRuntime_RunColdPathOnce_ApplyModeSkipsOrphanCandidateDraft(t *testing.T t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } - if _, err := os.Stat(filepath.Join(root, "skills", "orphan-weather", "SKILL.md")); !os.IsNotExist(err) { - t.Fatalf("orphan candidate draft should not be applied, got err=%v", err) + if _, statErr := os.Stat(filepath.Join(root, "skills", "orphan-weather", "SKILL.md")); !os.IsNotExist(statErr) { + t.Fatalf("orphan candidate draft should not be applied, got err=%v", statErr) } - if _, err := os.Stat(filepath.Join(root, "skills", "valid-weather", "SKILL.md")); err != nil { - t.Fatalf("expected current ready rule draft to be applied: %v", err) + if _, statErr := os.Stat(filepath.Join(root, "skills", "valid-weather", "SKILL.md")); statErr != nil { + t.Fatalf("expected current ready rule draft to be applied: %v", statErr) } drafts, err := store.LoadDrafts() if err != nil { @@ -483,10 +486,13 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra } rt, err := evolution.NewRuntime(evolution.RuntimeOptions{ - Config: config.EvolutionConfig{Enabled: true, Mode: "apply"}, - Now: func() time.Time { return time.Unix(1700001000, 0).UTC() }, - Store: store, - Applier: evolution.NewApplier(evolution.NewPaths(root, ""), func() time.Time { return time.Unix(1700001000, 0).UTC() }), + Config: config.EvolutionConfig{Enabled: true, Mode: "apply"}, + Now: func() time.Time { return time.Unix(1700001000, 0).UTC() }, + Store: store, + Applier: evolution.NewApplier( + evolution.NewPaths(root, ""), + func() time.Time { return time.Unix(1700001000, 0).UTC() }, + ), DraftGenerator: stubDraftGenerator{}, Organizer: evolution.NewOrganizer(evolution.OrganizerOptions{MinCaseCount: 3, MinSuccessRate: 0.7}), SkillsRecaller: evolution.NewSkillsRecaller(root), @@ -495,8 +501,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } data, err := os.ReadFile(filepath.Join(root, "skills", "calculate-100-via-theorems", "SKILL.md")) @@ -510,7 +516,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeNormalizesExistingCombinedCandidateDra if strings.Contains(content, "Learned") || strings.Contains(content, "Source Evidence") { t.Fatalf("deployed skill should not expose learning traces:\n%s", content) } - if strings.Contains(content, "messy raw component dump") || strings.Contains(content, "## Component Skill Breakdown") { + if strings.Contains(content, "messy raw component dump") || + strings.Contains(content, "## Component Skill Breakdown") { t.Fatalf("expected old verbose draft content to be cleaned:\n%s", content) } @@ -585,8 +592,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeRetargetsStableMultiSkillPathIntoCombi t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } skillPath := filepath.Join(root, "skills", "calculate-100-via-theorems", "SKILL.md") @@ -613,7 +620,8 @@ func TestRuntime_RunColdPathOnce_ApplyModeRetargetsStableMultiSkillPathIntoCombi if strings.Contains(content, "Learned") || strings.Contains(content, "Source Evidence") { t.Fatalf("deployed skill should not expose learning traces:\n%s", content) } - if !strings.Contains(content, "Add 31 to the input") || !strings.Contains(content, "Subtract 53 to produce the final result") { + if !strings.Contains(content, "Add 31 to the input") || + !strings.Contains(content, "Subtract 53 to produce the final result") { t.Fatalf("missing extracted component skill content:\n%s", content) } if strings.Contains(content, "Extracted guidance") { @@ -679,10 +687,13 @@ func TestRuntime_RunColdPathOnce_CombinedShortcutKeepsReadableLongGuidance(t *te } rt, err := evolution.NewRuntime(evolution.RuntimeOptions{ - Config: config.EvolutionConfig{Enabled: true, Mode: "apply"}, - Now: func() time.Time { return time.Unix(1700001000, 0).UTC() }, - Store: store, - Applier: evolution.NewApplier(evolution.NewPaths(root, ""), func() time.Time { return time.Unix(1700001000, 0).UTC() }), + Config: config.EvolutionConfig{Enabled: true, Mode: "apply"}, + Now: func() time.Time { return time.Unix(1700001000, 0).UTC() }, + Store: store, + Applier: evolution.NewApplier( + evolution.NewPaths(root, ""), + func() time.Time { return time.Unix(1700001000, 0).UTC() }, + ), DraftGenerator: stubDraftGenerator{draft: evolution.SkillDraft{ ID: "draft-1", WorkspaceID: root, @@ -704,8 +715,8 @@ func TestRuntime_RunColdPathOnce_CombinedShortcutKeepsReadableLongGuidance(t *te t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } data, err := os.ReadFile(filepath.Join(root, "skills", "calculate-with-theorem-chain-via-theorems", "SKILL.md")) @@ -915,8 +926,8 @@ func TestRuntime_RunColdPathOnce_FirstApplyFailureDoesNotCreateGhostProfile(t *t t.Fatalf("error = %v, want ErrApplyDraftFailed", err) } - if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) { - t.Fatalf("expected no profile after first apply failure, got err=%v", err) + if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) { + t.Fatalf("expected no profile after first apply failure, got err=%v", loadErr) } } @@ -987,8 +998,8 @@ func TestRuntime_RunColdPathOnce_DraftSaveFailureRollsBackAppliedSkill(t *testin if _, statErr := os.Stat(skillPath); !os.IsNotExist(statErr) { t.Fatalf("expected applied skill to be rolled back, got err=%v", statErr) } - if _, err := store.LoadProfile("weather"); !os.IsNotExist(err) { - t.Fatalf("expected no profile after draft save failure, got err=%v", err) + if _, loadErr := store.LoadProfile("weather"); !os.IsNotExist(loadErr) { + t.Fatalf("expected no profile after draft save failure, got err=%v", loadErr) } } @@ -1015,7 +1026,11 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) { t.Fatalf("MkdirAll: %v", err) } skillPath := filepath.Join(skillDir, "SKILL.md") - if err := os.WriteFile(skillPath, []byte("---\nname: stale-archived-skill\ndescription: stale\n---\n# Stale Archived Skill\n"), 0o644); err != nil { + if err := os.WriteFile( + skillPath, + []byte("---\nname: stale-archived-skill\ndescription: stale\n---\n# Stale Archived Skill\n"), + 0o644, + ); err != nil { t.Fatalf("WriteFile: %v", err) } if err := store.SaveProfile(evolution.SkillProfile{ @@ -1044,8 +1059,8 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } activeProfile, err := store.LoadProfile("stale-active-skill") @@ -1070,8 +1085,8 @@ func TestRuntime_RunColdPathOnce_AutoRunsLifecycleMaintenance(t *testing.T) { t.Fatalf("archived profile VersionHistory = %+v, want lifecycle:deleted entry", archivedProfile.VersionHistory) } - if _, err := os.Stat(skillPath); !os.IsNotExist(err) { - t.Fatalf("expected lifecycle delete to remove skill file, stat err = %v", err) + if _, statErr := os.Stat(skillPath); !os.IsNotExist(statErr) { + t.Fatalf("expected lifecycle delete to remove skill file, stat err = %v", statErr) } } diff --git a/pkg/evolution/runtime_cold_path_test.go b/pkg/evolution/runtime_cold_path_test.go index 4ef31b34a..19c23ebf0 100644 --- a/pkg/evolution/runtime_cold_path_test.go +++ b/pkg/evolution/runtime_cold_path_test.go @@ -136,8 +136,8 @@ func TestRuntime_RunColdPathOnce_GeneratesCandidateDraft(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -244,8 +244,8 @@ func TestRuntime_RunColdPathOnce_AdmitsOnlyRecordsApprovedBySuccessJudge(t *test t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } if len(judge.calls) != 2 || judge.calls[0] != "task-rejected" || judge.calls[1] != "task-admitted" { @@ -354,8 +354,8 @@ func TestRuntime_RunColdPathOnce_RejectsClusterBelowMinSuccessRatio(t *testing.T t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } patterns, err := store.LoadPatternRecords() @@ -442,8 +442,8 @@ func TestRuntime_RunColdPathOnce_FallbackUsesJudgeAdjustedSuccessRatio(t *testin t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } patterns, err := store.LoadPatternRecords() @@ -530,8 +530,8 @@ func TestRuntime_RunColdPathOnce_FallbackMarksAcceptedFailureEvidenceClustered(t t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } patterns, err := store.LoadPatternRecords() @@ -615,11 +615,15 @@ func TestRuntime_RunColdPathOnce_DraftEvidenceDoesNotCrossWorkspaceWithDuplicate t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), workspaceA); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), workspaceA); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } if len(generator.evidence.TaskRecords) != 1 { - t.Fatalf("evidence task count = %d, want 1: %#v", len(generator.evidence.TaskRecords), generator.evidence.TaskRecords) + t.Fatalf( + "evidence task count = %d, want 1: %#v", + len(generator.evidence.TaskRecords), + generator.evidence.TaskRecords, + ) } task := generator.evidence.TaskRecords[0] if task.WorkspaceID != workspaceA { @@ -651,7 +655,9 @@ func TestRuntime_RunColdPathOnce_AdmitsSingleSkillTaskButWaitsForMinTaskCount(t UsedSkillNames: []string{"weather"}, AddedSkillNames: []string{"weather"}, ToolKinds: []string{"read_file"}, - ToolExecutions: []evolution.ToolExecutionRecord{{Name: "read_file", Success: true, SkillNames: []string{"weather"}}}, + ToolExecutions: []evolution.ToolExecutionRecord{ + {Name: "read_file", Success: true, SkillNames: []string{"weather"}}, + }, AttemptTrail: &evolution.AttemptTrail{ AttemptedSkills: []string{"weather"}, FinalSuccessfulPath: []string{"weather"}, @@ -683,8 +689,8 @@ func TestRuntime_RunColdPathOnce_AdmitsSingleSkillTaskButWaitsForMinTaskCount(t t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } if len(judge.calls) != 1 || judge.calls[0] != "task-simple" { t.Fatalf("judge calls = %v, want [task-simple]", judge.calls) @@ -757,8 +763,8 @@ func TestRuntime_RunColdPathOnce_RejectsTaskWhenSuccessJudgeRejects(t *testing.T t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } allRecords, err := store.LoadLearningRecords() @@ -817,8 +823,8 @@ func TestRuntime_RunColdPathOnce_QuarantinesInvalidDraft(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -842,7 +848,11 @@ func TestRuntime_RunColdPathOnce_DoesNotWriteSkillFile(t *testing.T) { if err := os.MkdirAll(filepath.Dir(skillPath), 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } - if err := os.WriteFile(skillPath, []byte("---\nname: weather\ndescription: test\n---\n# Weather"), 0o644); err != nil { + if err := os.WriteFile( + skillPath, + []byte("---\nname: weather\ndescription: test\n---\n# Weather"), + 0o644, + ); err != nil { t.Fatalf("WriteFile: %v", err) } @@ -886,8 +896,8 @@ func TestRuntime_RunColdPathOnce_DoesNotWriteSkillFile(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } got, err := os.ReadFile(skillPath) @@ -926,8 +936,8 @@ func TestRuntime_RunColdPathOnce_UsesDefaultDraftGenerator(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -981,8 +991,8 @@ func TestRuntime_RunColdPathOnce_UsesLLMDraftGeneratorWhenProviderAvailable(t *t t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -1028,8 +1038,8 @@ func TestRuntime_RunColdPathOnce_UsesDefaultDraftGeneratorWhenFactoryHasNoProvid t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -1090,8 +1100,8 @@ func TestRuntime_RunColdPathOnce_UsesGeneratorFactoryWorkspaceForFallback(t *tes t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() @@ -1233,8 +1243,8 @@ func TestRuntime_RunColdPathOnce_RegeneratesAfterQuarantinedDraft(t *testing.T) t.Fatalf("NewRuntime: %v", err) } - if err := rt.RunColdPathOnce(context.Background(), root); err != nil { - t.Fatalf("RunColdPathOnce: %v", err) + if runErr := rt.RunColdPathOnce(context.Background(), root); runErr != nil { + t.Fatalf("RunColdPathOnce: %v", runErr) } drafts, err := store.LoadDrafts() diff --git a/pkg/evolution/runtime_test.go b/pkg/evolution/runtime_test.go index 8d0af4ea9..533294ae5 100644 --- a/pkg/evolution/runtime_test.go +++ b/pkg/evolution/runtime_test.go @@ -33,8 +33,8 @@ func TestRuntime_FinalizeTurnDisabledDoesNothing(t *testing.T) { } paths := evolution.NewPaths(workspace, "") - if _, err := os.Stat(paths.TaskRecords); !os.IsNotExist(err) { - t.Fatalf("task records file should not exist, stat err = %v", err) + if _, statErr := os.Stat(paths.TaskRecords); !os.IsNotExist(statErr) { + t.Fatalf("task records file should not exist, stat err = %v", statErr) } } @@ -46,11 +46,11 @@ func TestRuntime_FinalizeTurnWithEmptyWorkspaceDoesNothing(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ TurnID: "turn-1", Status: "completed", - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } } @@ -63,20 +63,20 @@ func TestRuntime_FinalizeTurnSkipsHeartbeat(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "heartbeat-turn", SessionKey: "heartbeat", Status: "completed", UserMessage: "# Heartbeat Check", FinalContent: "HEARTBEAT_OK", - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } paths := evolution.NewPaths(workspace, "") - if _, err := os.Stat(paths.TaskRecords); !os.IsNotExist(err) { - t.Fatalf("heartbeat should not create task records, stat err = %v", err) + if _, statErr := os.Stat(paths.TaskRecords); !os.IsNotExist(statErr) { + t.Fatalf("heartbeat should not create task records, stat err = %v", statErr) } } @@ -97,7 +97,7 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-1", SessionKey: "session-1", @@ -111,11 +111,11 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) { {Name: "read_file", Success: true}, }, ActiveSkillNames: []string{"skill-a"}, - }); err != nil { - t.Fatalf("FinalizeTurn first call: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn first call: %v", finalizeErr) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, WorkspaceID: "ws-explicit", TurnID: "turn-2", @@ -129,8 +129,8 @@ func TestRuntime_FinalizeTurnWritesRecordWithOverride(t *testing.T) { {Name: "bash", Success: false, ErrorSummary: "exit status 1"}, }, ActiveSkillNames: []string{"skill-b"}, - }); err != nil { - t.Fatalf("FinalizeTurn second call: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn second call: %v", finalizeErr) } paths := evolution.NewPaths(workspace, override) @@ -226,12 +226,12 @@ func TestRuntime_FinalizeTurnGeneratesUniqueTaskRecordIDsAcrossRestartedTurnSequ UserMessage: "summarize release notes", FinalContent: "done", } - if err := rt.FinalizeTurn(context.Background(), input); err != nil { - t.Fatalf("FinalizeTurn first: %v", err) + if finalizeErr := rt.FinalizeTurn(context.Background(), input); finalizeErr != nil { + t.Fatalf("FinalizeTurn first: %v", finalizeErr) } input.SessionKey = "session-b" - if err := rt.FinalizeTurn(context.Background(), input); err != nil { - t.Fatalf("FinalizeTurn second: %v", err) + if finalizeErr := rt.FinalizeTurn(context.Background(), input); finalizeErr != nil { + t.Fatalf("FinalizeTurn second: %v", finalizeErr) } store := evolution.NewStore(evolution.NewPaths(workspace, "")) @@ -285,24 +285,24 @@ func TestRuntime_FinalizeTurnSharedStateKeepsSkillProfilesScoped(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspaceA, TurnID: "turn-a", SessionKey: "session-a", Status: "completed", ActiveSkillNames: []string{"weather"}, - }); err != nil { - t.Fatalf("FinalizeTurn(workspaceA): %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn(workspaceA): %v", finalizeErr) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspaceB, TurnID: "turn-b", SessionKey: "session-b", Status: "completed", ActiveSkillNames: []string{"weather"}, - }); err != nil { - t.Fatalf("FinalizeTurn(workspaceB): %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn(workspaceB): %v", finalizeErr) } loadedA, err := storeA.LoadProfile("weather") @@ -347,7 +347,7 @@ func TestRuntime_FinalizeTurnWritesPotentiallyLearnableSignal(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-learnable", SessionKey: "session-learnable", @@ -363,8 +363,8 @@ func TestRuntime_FinalizeTurnWritesPotentiallyLearnableSignal(t *testing.T) { {Sequence: 1, Trigger: "initial_build", SkillNames: []string{"geocode"}}, {Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}}, }, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } paths := evolution.NewPaths(workspace, "") @@ -411,7 +411,7 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-skill-chain", SessionKey: "session-skill-chain", @@ -424,8 +424,8 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) { {Name: "read_file", Success: true, SkillNames: []string{"four-two"}}, {Name: "read_file", Success: true, SkillNames: []string{"five-three"}}, }, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } paths := evolution.NewPaths(workspace, "") @@ -446,7 +446,8 @@ func TestRuntime_FinalizeTurnUsesSkillNamesFromToolExecutions(t *testing.T) { if got := record.AddedSkillNames; len(got) != 0 { t.Fatalf("AddedSkillNames = %v, want empty", got) } - if got := record.UsedSkillNames; len(got) != 3 || got[0] != "three-one" || got[1] != "four-two" || got[2] != "five-three" { + if got := record.UsedSkillNames; len(got) != 3 || got[0] != "three-one" || got[1] != "four-two" || + got[2] != "five-three" { t.Fatalf("UsedSkillNames = %v, want [three-one four-two five-three]", got) } if got := record.AllLoadedSkillNames; len(got) != 0 { @@ -464,7 +465,7 @@ func TestRuntime_FinalizeTurnPreservesUTF8WhenTruncatingChineseOutput(t *testing } longChinese := strings.Repeat("中文输出", 500) - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-utf8", SessionKey: "session-utf8", @@ -472,8 +473,8 @@ func TestRuntime_FinalizeTurnPreservesUTF8WhenTruncatingChineseOutput(t *testing Status: "completed", UserMessage: "请处理这段中文输出", FinalContent: longChinese, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } paths := evolution.NewPaths(workspace, "") @@ -514,7 +515,7 @@ func TestRuntime_FinalizeTurnPrefersExplicitAttemptTrail(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-explicit-trail", SessionKey: "session-explicit-trail", @@ -528,8 +529,8 @@ func TestRuntime_FinalizeTurnPrefersExplicitAttemptTrail(t *testing.T) { {Sequence: 1, Trigger: "initial_build", SkillNames: []string{"weather"}}, {Sequence: 2, Trigger: "context_retry_rebuild", SkillNames: []string{"geocode", "weather"}}, }, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } paths := evolution.NewPaths(workspace, "") @@ -579,15 +580,15 @@ func TestRuntime_FinalizeTurnUpdatesSkillProfileUsage(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, TurnID: "turn-1", SessionKey: "session-1", AgentID: "agent-1", Status: "completed", ActiveSkillNames: []string{"skill-a", "skill-a"}, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } store := evolution.NewStore(evolution.NewPaths(workspace, "")) @@ -610,66 +611,37 @@ func TestRuntime_FinalizeTurnUpdatesSkillProfileUsage(t *testing.T) { } func TestRuntime_FinalizeTurnReactivatesColdSkill(t *testing.T) { - workspace := t.TempDir() - now := time.Unix(1700001000, 0).UTC() - store := evolution.NewStore(evolution.NewPaths(workspace, "")) - - if err := store.SaveProfile(evolution.SkillProfile{ - SkillName: "skill-cold", - WorkspaceID: workspace, - Status: evolution.SkillStatusCold, - Origin: "evolved", - HumanSummary: "cold skill", - LastUsedAt: now.Add(-24 * time.Hour), - UseCount: 2, - RetentionScore: 0.2, - }); err != nil { - t.Fatalf("SaveProfile: %v", err) - } - - rt, err := evolution.NewRuntime(evolution.RuntimeOptions{ - Config: config.EvolutionConfig{Enabled: true, Mode: "observe"}, - Now: func() time.Time { return now }, - Store: store, - }) - if err != nil { - t.Fatalf("NewRuntime: %v", err) - } - - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ - Workspace: workspace, - TurnID: "turn-cold", - Status: "completed", - ActiveSkillNames: []string{"skill-cold"}, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) - } - - profile, err := store.LoadProfile("skill-cold") - if err != nil { - t.Fatalf("LoadProfile: %v", err) - } - if profile.Status != evolution.SkillStatusActive { - t.Fatalf("Status = %q, want %q", profile.Status, evolution.SkillStatusActive) - } + assertFinalizeTurnReactivatesSkill(t, "skill-cold", evolution.SkillStatusCold, 2, 0.2, 24*time.Hour) } func TestRuntime_FinalizeTurnReactivatesArchivedSkill(t *testing.T) { + assertFinalizeTurnReactivatesSkill(t, "skill-archived", evolution.SkillStatusArchived, 5, 0.1, 48*time.Hour) +} + +func assertFinalizeTurnReactivatesSkill( + t *testing.T, + skillName string, + initialStatus evolution.SkillStatus, + useCount int, + retentionScore float64, + lastUsedAge time.Duration, +) { + t.Helper() workspace := t.TempDir() now := time.Unix(1700002000, 0).UTC() store := evolution.NewStore(evolution.NewPaths(workspace, "")) - if err := store.SaveProfile(evolution.SkillProfile{ - SkillName: "skill-archived", + if saveErr := store.SaveProfile(evolution.SkillProfile{ + SkillName: skillName, WorkspaceID: workspace, - Status: evolution.SkillStatusArchived, + Status: initialStatus, Origin: "evolved", - HumanSummary: "archived skill", - LastUsedAt: now.Add(-48 * time.Hour), - UseCount: 5, - RetentionScore: 0.1, - }); err != nil { - t.Fatalf("SaveProfile: %v", err) + HumanSummary: string(initialStatus) + " skill", + LastUsedAt: now.Add(-lastUsedAge), + UseCount: useCount, + RetentionScore: retentionScore, + }); saveErr != nil { + t.Fatalf("SaveProfile: %v", saveErr) } rt, err := evolution.NewRuntime(evolution.RuntimeOptions{ @@ -681,16 +653,16 @@ func TestRuntime_FinalizeTurnReactivatesArchivedSkill(t *testing.T) { t.Fatalf("NewRuntime: %v", err) } - if err := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ + if finalizeErr := rt.FinalizeTurn(context.Background(), evolution.TurnCaseInput{ Workspace: workspace, - TurnID: "turn-archived", + TurnID: "turn-" + skillName, Status: "completed", - ActiveSkillNames: []string{"skill-archived"}, - }); err != nil { - t.Fatalf("FinalizeTurn: %v", err) + ActiveSkillNames: []string{skillName}, + }); finalizeErr != nil { + t.Fatalf("FinalizeTurn: %v", finalizeErr) } - profile, err := store.LoadProfile("skill-archived") + profile, err := store.LoadProfile(skillName) if err != nil { t.Fatalf("LoadProfile: %v", err) } diff --git a/pkg/evolution/skills_recall_test.go b/pkg/evolution/skills_recall_test.go index 7fcfff629..13e27abbc 100644 --- a/pkg/evolution/skills_recall_test.go +++ b/pkg/evolution/skills_recall_test.go @@ -28,9 +28,21 @@ func TestRecallSimilarSkills_ReturnsWorkspaceSkillFirst(t *testing.T) { } } - mustWriteSkill(filepath.Join(workspace, "skills"), "weather", "---\nname: weather\ndescription: weather lookup\n---\n# Weather\nUse weather queries.\n") - mustWriteSkill(filepath.Join(globalHome, ".picoclaw", "skills"), "release", "---\nname: release\ndescription: release flow\n---\n# Release\nRelease build.\n") - mustWriteSkill(builtinRoot, "weather-fallback", "---\nname: weather-fallback\ndescription: weather backup\n---\n# Weather Fallback\nBackup weather path.\n") + mustWriteSkill( + filepath.Join(workspace, "skills"), + "weather", + "---\nname: weather\ndescription: weather lookup\n---\n# Weather\nUse weather queries.\n", + ) + mustWriteSkill( + filepath.Join(globalHome, ".picoclaw", "skills"), + "release", + "---\nname: release\ndescription: release flow\n---\n# Release\nRelease build.\n", + ) + mustWriteSkill( + builtinRoot, + "weather-fallback", + "---\nname: weather-fallback\ndescription: weather backup\n---\n# Weather Fallback\nBackup weather path.\n", + ) recaller := evolution.NewSkillsRecaller(workspace) matches, err := recaller.RecallSimilarSkills(evolution.LearningRecord{ diff --git a/pkg/evolution/store.go b/pkg/evolution/store.go index a430b82a1..2e7890799 100644 --- a/pkg/evolution/store.go +++ b/pkg/evolution/store.go @@ -80,8 +80,8 @@ func (s *Store) appendJSONLRecords(ctx context.Context, path string, records []L unlock := lockStoreFile(path) defer unlock() - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err + if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil { + return mkdirErr } f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) @@ -277,8 +277,8 @@ func (s *Store) saveJSONLRecords(path string, records []LearningRecord) error { } func (s *Store) saveJSONLRecordsLocked(path string, records []LearningRecord) error { - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err + if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil { + return mkdirErr } var buf bytes.Buffer @@ -383,8 +383,8 @@ func (s *Store) SaveProfile(profile SkillProfile) error { unlock := lockStoreFile(path) defer unlock() - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err + if mkdirErr := os.MkdirAll(filepath.Dir(path), 0o755); mkdirErr != nil { + return mkdirErr } data, err := json.MarshalIndent(profile, "", " ") @@ -418,14 +418,14 @@ func (s *Store) UpdateProfile( return err } - if err := update(&profile, exists); err != nil { - return err + if updateErr := update(&profile, exists); updateErr != nil { + return updateErr } if !exists && isZeroSkillProfile(profile) { return nil } - if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return err + if mkdirErr := os.MkdirAll(filepath.Dir(targetPath), 0o755); mkdirErr != nil { + return mkdirErr } data, err := json.MarshalIndent(profile, "", " ") diff --git a/pkg/evolution/store_test.go b/pkg/evolution/store_test.go index 5241266fa..7b9a78cb4 100644 --- a/pkg/evolution/store_test.go +++ b/pkg/evolution/store_test.go @@ -49,14 +49,14 @@ func TestStore_AppendLearningRecordsPersistsCaseAndRule(t *testing.T) { if loaded[1].Kind != evolution.RecordKindRule { t.Fatalf("loaded[1].Kind = %q, want %q", loaded[1].Kind, evolution.RecordKindRule) } - if _, err := os.Stat(paths.LearningRecords); !os.IsNotExist(err) { - t.Fatalf("legacy learning records file should not be written, stat err = %v", err) + if _, statErr := os.Stat(paths.LearningRecords); !os.IsNotExist(statErr) { + t.Fatalf("legacy learning records file should not be written, stat err = %v", statErr) } - if _, err := os.Stat(paths.TaskRecords); err != nil { - t.Fatalf("task records file should exist: %v", err) + if _, statErr := os.Stat(paths.TaskRecords); statErr != nil { + t.Fatalf("task records file should exist: %v", statErr) } - if _, err := os.Stat(paths.PatternRecords); err != nil { - t.Fatalf("pattern records file should exist: %v", err) + if _, statErr := os.Stat(paths.PatternRecords); statErr != nil { + t.Fatalf("pattern records file should exist: %v", statErr) } } @@ -77,11 +77,11 @@ func TestStore_LoadTaskRecordsMergesLegacyWhenSplitFileExists(t *testing.T) { if err != nil { t.Fatalf("Marshal legacy: %v", err) } - if err := os.MkdirAll(paths.RootDir, 0o755); err != nil { - t.Fatalf("MkdirAll: %v", err) + if mkdirErr := os.MkdirAll(paths.RootDir, 0o755); mkdirErr != nil { + t.Fatalf("MkdirAll: %v", mkdirErr) } - if err := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); err != nil { - t.Fatalf("WriteFile legacy: %v", err) + if writeErr := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); writeErr != nil { + t.Fatalf("WriteFile legacy: %v", writeErr) } current := evolution.LearningRecord{ @@ -92,8 +92,8 @@ func TestStore_LoadTaskRecordsMergesLegacyWhenSplitFileExists(t *testing.T) { Summary: "current task", Status: evolution.RecordStatus("new"), } - if err := store.AppendTaskRecord(context.Background(), current); err != nil { - t.Fatalf("AppendTaskRecord: %v", err) + if appendErr := store.AppendTaskRecord(context.Background(), current); appendErr != nil { + t.Fatalf("AppendTaskRecord: %v", appendErr) } records, err := store.LoadTaskRecords() @@ -126,11 +126,11 @@ func TestStore_LoadPatternRecordsMergesLegacyWhenSplitFileExists(t *testing.T) { if err != nil { t.Fatalf("Marshal legacy: %v", err) } - if err := os.MkdirAll(paths.RootDir, 0o755); err != nil { - t.Fatalf("MkdirAll: %v", err) + if mkdirErr := os.MkdirAll(paths.RootDir, 0o755); mkdirErr != nil { + t.Fatalf("MkdirAll: %v", mkdirErr) } - if err := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); err != nil { - t.Fatalf("WriteFile legacy: %v", err) + if writeErr := os.WriteFile(paths.LearningRecords, append(data, '\n'), 0o644); writeErr != nil { + t.Fatalf("WriteFile legacy: %v", writeErr) } current := evolution.LearningRecord{ @@ -141,8 +141,8 @@ func TestStore_LoadPatternRecordsMergesLegacyWhenSplitFileExists(t *testing.T) { Summary: "current pattern", Status: evolution.RecordStatus("ready"), } - if err := store.AppendPatternRecords([]evolution.LearningRecord{current}); err != nil { - t.Fatalf("AppendPatternRecords: %v", err) + if appendErr := store.AppendPatternRecords([]evolution.LearningRecord{current}); appendErr != nil { + t.Fatalf("AppendPatternRecords: %v", appendErr) } records, err := store.LoadPatternRecords() @@ -246,8 +246,8 @@ func TestStore_MergeKeepsSameRecordIDAcrossWorkspaces(t *testing.T) { t.Fatalf("len(loaded) = %d, want 2: %+v", len(loaded), loaded) } - if err := store.MarkTaskRecordsClustered([]string{"main-turn-1"}); err != nil { - t.Fatalf("MarkTaskRecordsClustered: %v", err) + if markErr := store.MarkTaskRecordsClustered([]string{"main-turn-1"}); markErr != nil { + t.Fatalf("MarkTaskRecordsClustered: %v", markErr) } loaded, err = store.LoadTaskRecords() if err != nil { @@ -409,12 +409,12 @@ func TestStore_LoadLearningRecordsIgnoresTruncatedTrailingLine(t *testing.T) { if err != nil { t.Fatalf("OpenFile: %v", err) } - if _, err := f.WriteString("{\"id\":\"broken\""); err != nil { + if _, writeErr := f.WriteString("{\"id\":\"broken\""); writeErr != nil { f.Close() - t.Fatalf("WriteString: %v", err) + t.Fatalf("WriteString: %v", writeErr) } - if err := f.Close(); err != nil { - t.Fatalf("Close: %v", err) + if closeErr := f.Close(); closeErr != nil { + t.Fatalf("Close: %v", closeErr) } loaded, err := store.LoadLearningRecords()