This commit is contained in:
lxowalle 2026-05-11 11:42:03 +08:00
parent 83de57c3b8
commit 07be3462df
25 changed files with 815 additions and 495 deletions

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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"
}

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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{

View file

@ -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.`,

View file

@ -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") {

View file

@ -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 == "" {

View file

@ -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)

View file

@ -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 {

View file

@ -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,
" -> ",
),
}
}

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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()

View file

@ -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)
}

View file

@ -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{

View file

@ -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, "", " ")

View file

@ -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()