Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Binary file added .entire/insights.db
Binary file not shown.
3 changes: 3 additions & 0 deletions .entire/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"checkpoint_remote": {
"provider": "github",
"repo": "entireio/cli-checkpoints"
},
"summarize": {
"enabled": true
}
}
}
5 changes: 5 additions & 0 deletions .superset/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"setup": [],
"teardown": [],
"run": []
}
16 changes: 8 additions & 8 deletions cmd/entire/cli/agent/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type DeclaredCaps struct {

// AsHookSupport returns the agent as HookSupport if it both implements the
// interface and (for CapabilityDeclarer agents) has declared the capability.
func AsHookSupport(ag Agent) (HookSupport, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsHookSupport(ag Agent) (HookSupport, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -43,7 +43,7 @@ func AsHookSupport(ag Agent) (HookSupport, bool) { //nolint:ireturn // type-asse

// AsTranscriptAnalyzer returns the agent as TranscriptAnalyzer if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsTranscriptAnalyzer(ag Agent) (TranscriptAnalyzer, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsTranscriptAnalyzer(ag Agent) (TranscriptAnalyzer, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -59,7 +59,7 @@ func AsTranscriptAnalyzer(ag Agent) (TranscriptAnalyzer, bool) { //nolint:iretur

// AsTranscriptPreparer returns the agent as TranscriptPreparer if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsTranscriptPreparer(ag Agent) (TranscriptPreparer, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsTranscriptPreparer(ag Agent) (TranscriptPreparer, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -75,7 +75,7 @@ func AsTranscriptPreparer(ag Agent) (TranscriptPreparer, bool) { //nolint:iretur

// AsTokenCalculator returns the agent as TokenCalculator if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsTokenCalculator(ag Agent) (TokenCalculator, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsTokenCalculator(ag Agent) (TokenCalculator, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -91,7 +91,7 @@ func AsTokenCalculator(ag Agent) (TokenCalculator, bool) { //nolint:ireturn // t

// AsTextGenerator returns the agent as TextGenerator if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsTextGenerator(ag Agent) (TextGenerator, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsTextGenerator(ag Agent) (TextGenerator, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -107,7 +107,7 @@ func AsTextGenerator(ag Agent) (TextGenerator, bool) { //nolint:ireturn // type-

// AsHookResponseWriter returns the agent as HookResponseWriter if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsHookResponseWriter(ag Agent) (HookResponseWriter, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsHookResponseWriter(ag Agent) (HookResponseWriter, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -126,7 +126,7 @@ func AsHookResponseWriter(ag Agent) (HookResponseWriter, bool) { //nolint:iretur
// ExtractPrompts is conceptually part of transcript analysis, so it shares the same
// capability gate — this prevents calling extract-prompts on external agent binaries
// that never declared transcript_analyzer support.
func AsPromptExtractor(ag Agent) (PromptExtractor, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsPromptExtractor(ag Agent) (PromptExtractor, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand All @@ -142,7 +142,7 @@ func AsPromptExtractor(ag Agent) (PromptExtractor, bool) { //nolint:ireturn // t

// AsSubagentAwareExtractor returns the agent as SubagentAwareExtractor if it both
// implements the interface and (for CapabilityDeclarer agents) has declared the capability.
func AsSubagentAwareExtractor(ag Agent) (SubagentAwareExtractor, bool) { //nolint:ireturn // type-assertion helper must return interface
func AsSubagentAwareExtractor(ag Agent) (SubagentAwareExtractor, bool) { //nolint:ireturn // type-assertion helper
if ag == nil {
return nil, false
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/entire/cli/checkpoint/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ type WriteCommittedOptions struct {
// AuthorEmail is the email to use for commits
AuthorEmail string

// OwnerName and OwnerEmail identify who owned the session when it started.
// Unlike AuthorName/AuthorEmail, these are durable session-attribution fields,
// not metadata-branch commit authorship.
OwnerName string
OwnerEmail string

// MetadataDir is a directory containing additional metadata files to copy
// If set, all files in this directory will be copied to the checkpoint path
// This is useful for copying task metadata files, subagent transcripts, etc.
Expand Down Expand Up @@ -374,6 +380,10 @@ type CommittedMetadata struct {
// Model is the LLM model used during the session (e.g., "claude-sonnet-4-20250514")
Model string `json:"model,omitempty"`

// OwnerName and OwnerEmail identify who owned the session when it started.
OwnerName string `json:"owner_name,omitempty"`
OwnerEmail string `json:"owner_email,omitempty"`

// TurnID correlates checkpoints from the same agent turn.
// When a turn's work spans multiple commits, each gets its own checkpoint
// but they share the same TurnID for future aggregation/deduplication.
Expand Down
55 changes: 55 additions & 0 deletions cmd/entire/cli/checkpoint/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,61 @@ func TestWriteCommitted_AgentField(t *testing.T) {
}
}

func TestWriteCommitted_OwnerFieldsPersisted(t *testing.T) {
tempDir := t.TempDir()

repo, err := git.PlainInit(tempDir, false)
require.NoError(t, err)

worktree, err := repo.Worktree()
require.NoError(t, err)

readmeFile := filepath.Join(tempDir, "README.md")
require.NoError(t, os.WriteFile(readmeFile, []byte("# Test"), 0o644))
_, err = worktree.Add("README.md")
require.NoError(t, err)
_, err = worktree.Commit("Initial commit", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@test.com"},
})
require.NoError(t, err)

store := NewGitStore(repo)
checkpointID := id.MustCheckpointID("b1b2c3d4e5f6")

err = store.WriteCommitted(context.Background(), WriteCommittedOptions{
CheckpointID: checkpointID,
SessionID: "test-session-owner-meta",
Strategy: "manual-commit",
Transcript: []byte("test transcript content"),
AuthorName: "Condense Author",
AuthorEmail: "condense@example.com",
OwnerName: "Session Owner",
OwnerEmail: "owner@example.com",
})
require.NoError(t, err)

ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true)
require.NoError(t, err)
commit, err := repo.CommitObject(ref.Hash())
require.NoError(t, err)
tree, err := commit.Tree()
require.NoError(t, err)

checkpointTree, err := tree.Tree(checkpointID.Path())
require.NoError(t, err)
sessionTree, err := checkpointTree.Tree("0")
require.NoError(t, err)
sessionMetadataFile, err := sessionTree.File(paths.MetadataFileName)
require.NoError(t, err)
sessionContent, err := sessionMetadataFile.Contents()
require.NoError(t, err)

var sessionMetadata CommittedMetadata
require.NoError(t, json.Unmarshal([]byte(sessionContent), &sessionMetadata))
require.Equal(t, "Session Owner", sessionMetadata.OwnerName)
require.Equal(t, "owner@example.com", sessionMetadata.OwnerEmail)
}

// readLatestSessionMetadata reads the session-specific metadata from the latest session subdirectory.
// This is where session-specific fields like Summary are stored.
func readLatestSessionMetadata(t *testing.T, repo *git.Repository, checkpointID id.CheckpointID) CommittedMetadata {
Expand Down
2 changes: 2 additions & 0 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ func (s *GitStore) writeSessionToSubdirectory(ctx context.Context, opts WriteCom
FilesTouched: opts.FilesTouched,
Agent: opts.Agent,
Model: opts.Model,
OwnerName: opts.OwnerName,
OwnerEmail: opts.OwnerEmail,
TurnID: opts.TurnID,
IsTask: opts.IsTask,
ToolUseID: opts.ToolUseID,
Expand Down
2 changes: 2 additions & 0 deletions cmd/entire/cli/checkpoint/v2_committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ func (s *V2GitStore) writeMainSessionToSubdirectory(opts WriteCommittedOptions,
FilesTouched: opts.FilesTouched,
Agent: opts.Agent,
Model: opts.Model,
OwnerName: opts.OwnerName,
OwnerEmail: opts.OwnerEmail,
TurnID: opts.TurnID,
IsTask: opts.IsTask,
ToolUseID: opts.ToolUseID,
Expand Down
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())
}
}
78 changes: 78 additions & 0 deletions cmd/entire/cli/evolve/tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package evolve

import (
"fmt"
"time"
)

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

func NewTracker() *Tracker {
return &Tracker{Records: make(map[string]*SuggestionRecord)}
}

func (t *Tracker) AddSuggestion(rec SuggestionRecord) {
t.Records[rec.ID] = &rec
}

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 {
return t.resolve(id, "accepted")
}

// Reject marks a suggestion as rejected and records the resolution time.
func (t *Tracker) Reject(id string) error {
return t.resolve(id, "rejected")
}

func (t *Tracker) resolve(id, status string) error {
rec, ok := t.Records[id]
if !ok {
return fmt.Errorf("suggestion %q not found", id)
}
now := time.Now()
rec.Status = status
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
}

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

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