Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dcd84a0
feat(settings): add EvolveSettings for evolution loop configuration
alishakawaguchi Mar 24, 2026
095b13a
refactor: extract shared Claude CLI execution to llmcli package
alishakawaguchi Mar 24, 2026
fc42303
refactor: extract shared terminal styles to termstyle package
alishakawaguchi Mar 24, 2026
9a034c3
feat(strategy): compute session quality score at commit time
alishakawaguchi Mar 25, 2026
c788f86
feat(improve): add context file detection, friction analyzer, and sug…
alishakawaguchi Mar 25, 2026
2b743e8
feat: add entire insights command for session quality scoring
alishakawaguchi Mar 25, 2026
a06f854
feat(evolve): add evolution loop trigger, tracker, and notification
alishakawaguchi Mar 25, 2026
7c1b491
feat: add entire improve command for context file suggestions
alishakawaguchi Mar 25, 2026
7816386
fix: resolve lint issues and add missing files for agent improvement …
alishakawaguchi Mar 25, 2026
1716fa7
chore: remove nolint:ireturn comments from agent capabilities
alishakawaguchi Mar 25, 2026
4c095d6
fix: simplify code and fix token calculation bug in insights
alishakawaguchi Mar 25, 2026
dab6998
feat: display token usage and cost after entire improve
alishakawaguchi Mar 25, 2026
dff7537
merge: resolve conflict in manual_commit_condensation.go
alishakawaguchi Mar 25, 2026
2f8f4bd
feat: backfill missing summaries and fix scoring formulas
alishakawaguchi Mar 25, 2026
4d85473
feat: add structured session facets for improve
alishakawaguchi Mar 26, 2026
e07b227
chore: commit remaining insights changes
alishakawaguchi Mar 26, 2026
48bf34a
docs: add heavyweight memory loop design and plan
alishakawaguchi Mar 26, 2026
ae6f1b5
feat: add heavyweight memory loop workflow
alishakawaguchi Mar 26, 2026
5a987c0
feat(memory-loop): add TUI dashboard with memories tab implementation
alishakawaguchi Mar 26, 2026
cf05fde
fix: address 4 code review issues in memory loop TUI dashboard
alishakawaguchi Mar 26, 2026
e559463
feat(memory-loop): implement injection, history, settings tabs and ad…
alishakawaguchi Mar 26, 2026
391b630
fix(memory-loop-tui): orange accent color, visible inactive tabs, det…
alishakawaguchi Mar 26, 2026
783a567
fix: improve memory loop TUI dashboard layout and interactions
alishakawaguchi Mar 26, 2026
2b24a2c
fix(memory-loop-tui): visible borders (245 gray), spacing between sec…
alishakawaguchi Mar 26, 2026
b4be1b4
fix(memory-loop-tui): settings cards, injection table overflow, setti…
alishakawaguchi Mar 26, 2026
65078b6
fix(memory-loop-tui): colored settings chips, remove debug logging
alishakawaguchi Mar 26, 2026
f9b642b
docs: add memory-loop TUI redesign design
alishakawaguchi Mar 27, 2026
d4a1f54
docs: add memory-loop TUI restyle design
alishakawaguchi Mar 27, 2026
3683cfc
feat(memory-loop-tui): tab bar with underline, filter chips, and DETA…
alishakawaguchi Mar 27, 2026
14e5f89
fix(memory-loop-tui): improve details card spacing and readability
alishakawaguchi Mar 27, 2026
04a016a
fix(memory-loop-tui): improve injection and history tab spacing and c…
alishakawaguchi Mar 27, 2026
19da155
feat: add skill improvement engine with interactive TUI dashboard
alishakawaguchi Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions cmd/entire/cli/evolve/evolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Package evolve implements the evolution loop that tracks sessions since the
// last improvement run and triggers suggestions after a configurable threshold.
package evolve

import "time"

// State tracks the evolution loop's progress.
// Stored in SQLite and mirrored to insights/evolution.json on the checkpoint branch.
type State struct {
LastRunAt time.Time `json:"last_run_at"`
SessionsSinceLastRun int `json:"sessions_since_last_run"`
TotalRuns int `json:"total_runs"`
SuggestionsGenerated int `json:"suggestions_generated"`
SuggestionsAccepted int `json:"suggestions_accepted"`
}

// SuggestionRecord tracks a suggestion through its lifecycle.
type SuggestionRecord struct {
ID string `json:"id"`
Title string `json:"title"`
FileType string `json:"file_type"`
Priority string `json:"priority"`
Status string `json:"status"` // "pending", "accepted", "rejected"
CreatedAt time.Time `json:"created_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
PreAvgScore *float64 `json:"pre_avg_score,omitempty"`
PostAvgScore *float64 `json:"post_avg_score,omitempty"`
}
19 changes: 19 additions & 0 deletions cmd/entire/cli/evolve/notify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package evolve

import (
"fmt"
"io"

"github.com/entireio/cli/cmd/entire/cli/settings"
)

// CheckAndNotify increments the session counter and notifies the user
// when the evolution threshold is reached. Called after session condensation.
func CheckAndNotify(w io.Writer, config settings.EvolveSettings, state *State) {
IncrementSessionCount(state)
if !ShouldTrigger(config, *state) {
return
}
fmt.Fprintf(w, "\n Tip: %d sessions since last improvement analysis.\n", state.SessionsSinceLastRun)
fmt.Fprintln(w, " Run `entire improve` to get context file suggestions.")
}
66 changes: 66 additions & 0 deletions cmd/entire/cli/evolve/notify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package evolve_test

import (
"bytes"
"strings"
"testing"

"github.com/entireio/cli/cmd/entire/cli/evolve"
"github.com/entireio/cli/cmd/entire/cli/settings"
)

func TestCheckAndNotify_ThresholdMet(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
config := settings.EvolveSettings{Enabled: true, SessionThreshold: 5}
state := evolve.State{SessionsSinceLastRun: 4} // will become 5 after increment

evolve.CheckAndNotify(&buf, config, &state)

if state.SessionsSinceLastRun != 5 {
t.Errorf("expected SessionsSinceLastRun=5, got %d", state.SessionsSinceLastRun)
}
output := buf.String()
if !strings.Contains(output, "entire improve") {
t.Errorf("expected output to mention 'entire improve', got: %q", output)
}
if !strings.Contains(output, "5") {
t.Errorf("expected output to mention session count 5, got: %q", output)
}
}

func TestCheckAndNotify_BelowThreshold(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
config := settings.EvolveSettings{Enabled: true, SessionThreshold: 5}
state := evolve.State{SessionsSinceLastRun: 2} // will become 3 after increment

evolve.CheckAndNotify(&buf, config, &state)

if state.SessionsSinceLastRun != 3 {
t.Errorf("expected SessionsSinceLastRun=3, got %d", state.SessionsSinceLastRun)
}
if buf.Len() != 0 {
t.Errorf("expected no output below threshold, got: %q", buf.String())
}
}

func TestCheckAndNotify_Disabled(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
config := settings.EvolveSettings{Enabled: false, SessionThreshold: 5}
state := evolve.State{SessionsSinceLastRun: 10}

evolve.CheckAndNotify(&buf, config, &state)

// Counter still increments even when disabled
if state.SessionsSinceLastRun != 11 {
t.Errorf("expected SessionsSinceLastRun=11, got %d", state.SessionsSinceLastRun)
}
if buf.Len() != 0 {
t.Errorf("expected no output when disabled, got: %q", buf.String())
}
}
87 changes: 87 additions & 0 deletions cmd/entire/cli/evolve/tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package evolve

import (
"fmt"
"time"
)

// Tracker manages the lifecycle of improvement suggestions.
type Tracker struct {
Records map[string]*SuggestionRecord
}

// NewTracker creates a new Tracker with an empty record map.
func NewTracker() *Tracker {
return &Tracker{Records: make(map[string]*SuggestionRecord)}
}

// AddSuggestion registers a new suggestion.
func (t *Tracker) AddSuggestion(rec SuggestionRecord) {
t.Records[rec.ID] = &rec
}

// Get returns a suggestion by ID, or nil if not found.
func (t *Tracker) Get(id string) *SuggestionRecord {
return t.Records[id]
}

// Accept marks a suggestion as accepted and records the resolution time.
func (t *Tracker) Accept(id string) error {
rec, ok := t.Records[id]
if !ok {
return fmt.Errorf("suggestion %q not found", id)
}
now := time.Now()
rec.Status = "accepted"
rec.ResolvedAt = &now
return nil
}

// Reject marks a suggestion as rejected and records the resolution time.
func (t *Tracker) Reject(id string) error {
rec, ok := t.Records[id]
if !ok {
return fmt.Errorf("suggestion %q not found", id)
}
now := time.Now()
rec.Status = "rejected"
rec.ResolvedAt = &now
return nil
}

// MeasureImpact sets the pre/post average scores for impact analysis.
// Returns the updated record, or nil if the ID is not found.
func (t *Tracker) MeasureImpact(id string, scoresBefore, scoresAfter []float64) *SuggestionRecord {
rec, ok := t.Records[id]
if !ok {
return nil
}
rec.PreAvgScore = avgOrNil(scoresBefore)
rec.PostAvgScore = avgOrNil(scoresAfter)
return rec
}

// Pending returns all suggestions with "pending" status.
func (t *Tracker) Pending() []SuggestionRecord {
var result []SuggestionRecord
for _, rec := range t.Records {
if rec.Status == "pending" {
result = append(result, *rec)
}
}
return result
}

// avgOrNil computes the average of a float64 slice.
// Returns nil if the slice is empty.
func avgOrNil(scores []float64) *float64 {
if len(scores) == 0 {
return nil
}
var sum float64
for _, s := range scores {
sum += s
}
avg := sum / float64(len(scores))
return &avg
}
185 changes: 185 additions & 0 deletions cmd/entire/cli/evolve/tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package evolve_test

import (
"math"
"testing"
"time"

"github.com/entireio/cli/cmd/entire/cli/evolve"
)

func TestTracker_AddAndGet(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
rec := evolve.SuggestionRecord{
ID: "rec-1",
Title: "Add lint instructions",
FileType: "CLAUDE.md",
Priority: "high",
Status: "pending",
CreatedAt: time.Now(),
}
tr.AddSuggestion(rec)

got := tr.Get("rec-1")
if got == nil {
t.Fatal("expected to find record by ID, got nil")
}
if got.Title != "Add lint instructions" {
t.Errorf("expected Title=%q, got %q", "Add lint instructions", got.Title)
}
}

func TestTracker_Accept(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
tr.AddSuggestion(evolve.SuggestionRecord{
ID: "rec-2",
Status: "pending",
CreatedAt: time.Now(),
})

before := time.Now()
if err := tr.Accept("rec-2"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
after := time.Now()

got := tr.Get("rec-2")
if got.Status != "accepted" {
t.Errorf("expected Status=%q, got %q", "accepted", got.Status)
}
if got.ResolvedAt == nil {
t.Fatal("expected ResolvedAt to be set")
}
if got.ResolvedAt.Before(before) || got.ResolvedAt.After(after) {
t.Errorf("expected ResolvedAt to be approximately now, got %v", got.ResolvedAt)
}
}

func TestTracker_Reject(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
tr.AddSuggestion(evolve.SuggestionRecord{
ID: "rec-3",
Status: "pending",
CreatedAt: time.Now(),
})

before := time.Now()
if err := tr.Reject("rec-3"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
after := time.Now()

got := tr.Get("rec-3")
if got.Status != "rejected" {
t.Errorf("expected Status=%q, got %q", "rejected", got.Status)
}
if got.ResolvedAt == nil {
t.Fatal("expected ResolvedAt to be set")
}
if got.ResolvedAt.Before(before) || got.ResolvedAt.After(after) {
t.Errorf("expected ResolvedAt to be approximately now, got %v", got.ResolvedAt)
}
}

func TestTracker_AcceptNotFound(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()

if err := tr.Accept("nonexistent"); err == nil {
t.Error("expected error for unknown ID, got nil")
}
}

func TestTracker_RejectNotFound(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()

if err := tr.Reject("nonexistent"); err == nil {
t.Error("expected error for unknown ID, got nil")
}
}

func TestTracker_MeasureImpact(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
tr.AddSuggestion(evolve.SuggestionRecord{
ID: "rec-4",
Status: "accepted",
CreatedAt: time.Now(),
})

scoresBefore := []float64{0.6, 0.8, 0.7}
scoresAfter := []float64{0.9, 0.85, 0.95}
got := tr.MeasureImpact("rec-4", scoresBefore, scoresAfter)

if got == nil {
t.Fatal("expected non-nil SuggestionRecord")
}
if got.PreAvgScore == nil {
t.Fatal("expected PreAvgScore to be set")
}
expectedPre := (0.6 + 0.8 + 0.7) / 3
if math.Abs(*got.PreAvgScore-expectedPre) > 1e-9 {
t.Errorf("expected PreAvgScore=%f, got %f", expectedPre, *got.PreAvgScore)
}
if got.PostAvgScore == nil {
t.Fatal("expected PostAvgScore to be set")
}
expectedPost := (0.9 + 0.85 + 0.95) / 3
if math.Abs(*got.PostAvgScore-expectedPost) > 1e-9 {
t.Errorf("expected PostAvgScore=%f, got %f", expectedPost, *got.PostAvgScore)
}
}

func TestTracker_MeasureImpact_EmptySlices(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
tr.AddSuggestion(evolve.SuggestionRecord{
ID: "rec-5",
Status: "accepted",
CreatedAt: time.Now(),
})

got := tr.MeasureImpact("rec-5", nil, nil)
if got == nil {
t.Fatal("expected non-nil SuggestionRecord")
}
// Empty slices should result in nil scores (no data to average)
if got.PreAvgScore != nil {
t.Errorf("expected PreAvgScore=nil for empty slice, got %f", *got.PreAvgScore)
}
if got.PostAvgScore != nil {
t.Errorf("expected PostAvgScore=nil for empty slice, got %f", *got.PostAvgScore)
}
}

func TestTracker_Pending(t *testing.T) {
t.Parallel()

tr := evolve.NewTracker()
tr.AddSuggestion(evolve.SuggestionRecord{ID: "p1", Status: "pending", CreatedAt: time.Now()})
tr.AddSuggestion(evolve.SuggestionRecord{ID: "p2", Status: "pending", CreatedAt: time.Now()})
tr.AddSuggestion(evolve.SuggestionRecord{ID: "a1", Status: "accepted", CreatedAt: time.Now()})
tr.AddSuggestion(evolve.SuggestionRecord{ID: "r1", Status: "rejected", CreatedAt: time.Now()})

pending := tr.Pending()

if len(pending) != 2 {
t.Errorf("expected 2 pending records, got %d", len(pending))
}
for _, rec := range pending {
if rec.Status != "pending" {
t.Errorf("expected Status=pending, got %q", rec.Status)
}
}
}
Loading
Loading