Skip to content
Merged
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
34dce65
Remove trail enable/disable commands and related functionality
dipree Feb 23, 2026
567a6ce
Add interactive mode to `entire trail update`
dipree Feb 23, 2026
52998e4
Add trail package and register trail command
dipree Feb 23, 2026
110ec18
Add checkpoint-trail linking and trail title generation
dipree Feb 23, 2026
ab7673e
Add TextGenerator agent interface and trail title summarizer
dipree Feb 23, 2026
c6ff702
Update local dev settings with wingman and telemetry
dipree Feb 23, 2026
9a73094
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 23, 2026
3e5b0fb
Create branch when creating a trail
dipree Feb 23, 2026
7c07499
Always prompt for branch name in interactive trail create
dipree Feb 23, 2026
4b17b67
Prompt for title first in interactive trail create
dipree Feb 23, 2026
2d2b83d
Add description and status to interactive trail create
dipree Feb 23, 2026
6e687f7
Push branch and trail data to origin after trail create
dipree Feb 23, 2026
acc68d9
Use GitHub username as trail author
dipree Feb 23, 2026
af464a2
Fetch remote trails before listing
dipree Feb 23, 2026
85f50b7
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 24, 2026
9e79319
Address PR review feedback
dipree Feb 24, 2026
9d64e48
Rename trail "description" to "body"
dipree Feb 24, 2026
2df848d
Update CLAUDE.md trail docs: description → body
dipree Feb 24, 2026
488c4b0
Revert CLAUDE.md to main
dipree Feb 24, 2026
66935db
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 24, 2026
d1c6bc0
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 25, 2026
58e5f43
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 26, 2026
f2f2b7d
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Feb 27, 2026
9b93ae6
Remove automatic trail creation on session start
dipree Feb 27, 2026
189eb45
Hide trail command from main help output
dipree Feb 27, 2026
1411d16
Merge remote-tracking branch 'origin/main' into feat/trails
dipree Mar 2, 2026
cef3938
simplify
Soph Mar 2, 2026
8b01d8e
Remove trail title generation
dipree Mar 2, 2026
dd382d3
Rename trails branch from entire/trails to entire/trails/v1
dipree Mar 3, 2026
9a1ac03
Merge branch 'main' into feat/trails
dipree Mar 3, 2026
fd6ef8e
Align trail types with entire.io-1 web app
dipree Mar 3, 2026
bf021d4
refactoring to be in line with latest entire/checkpoints/v1 branch ha…
Soph Mar 3, 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
11 changes: 11 additions & 0 deletions cmd/entire/cli/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ type TokenCalculator interface {
CalculateTokenUsage(transcriptData []byte, fromOffset int) (*TokenUsage, error)
}

// TextGenerator is an optional interface for agents whose CLI supports
// non-interactive text generation (e.g., claude --print).
// Used for AI-powered metadata generation (trail titles, summaries).
type TextGenerator interface {
Agent

// GenerateText sends a prompt to the agent's CLI and returns the raw text response.
// model is a hint (e.g., "haiku", "sonnet"). Implementations may ignore if not applicable.
GenerateText(ctx context.Context, prompt string, model string) (string, error)
}

// HookResponseWriter is implemented by agents that support structured hook responses.
// Agents that implement this can output messages (e.g., banners) to the user via
// the agent's response protocol. For example, Claude Code outputs JSON with a
Expand Down
72 changes: 72 additions & 0 deletions cmd/entire/cli/agent/claudecode/generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package claudecode

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"strings"
)

// GenerateText sends a prompt to the Claude CLI and returns the raw text response.
// Implements the agent.TextGenerator interface.
// The model parameter hints which model to use (e.g., "haiku", "sonnet").
// If empty, defaults to "haiku" for fast, cheap generation.
func (c *ClaudeCodeAgent) GenerateText(ctx context.Context, prompt string, model string) (string, error) {
claudePath := "claude"
if model == "" {
model = "haiku"
}

cmd := exec.CommandContext(ctx, claudePath,
"--print", "--output-format", "json",
"--model", model, "--setting-sources", "")

// Isolate from the user's git repo to prevent recursive hook triggers
// and index pollution (same approach as summarize/claude.go).
cmd.Dir = os.TempDir()
cmd.Env = stripGitEnv(os.Environ())
cmd.Stdin = strings.NewReader(prompt)

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
var execErr *exec.Error
if errors.As(err, &execErr) {
return "", fmt.Errorf("claude CLI not found: %w", err)
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return "", fmt.Errorf("claude CLI failed (exit %d): %s", exitErr.ExitCode(), stderr.String())
}
return "", fmt.Errorf("failed to run claude CLI: %w", err)
}

// Parse the {"result": "..."} envelope
var response struct {
Result string `json:"result"`
}
if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
return "", fmt.Errorf("failed to parse claude CLI response: %w", err)
}

return response.Result, nil
}

// stripGitEnv returns a copy of env with all GIT_* variables removed.
// This prevents a subprocess from discovering or modifying the parent's git repo.
// Duplicated from summarize/claude.go — simple filter not worth extracting to shared package.
func stripGitEnv(env []string) []string {
filtered := make([]string, 0, len(env))
for _, e := range env {
if !strings.HasPrefix(e, "GIT_") {
filtered = append(filtered, e)
}
}
return filtered
}
34 changes: 34 additions & 0 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,40 @@ func GetGitAuthorFromRepo(repo *git.Repository) (name, email string) {
return name, email
}

// CreateCommit creates a git commit object with the given tree, parent, message, and author.
// If parentHash is ZeroHash, the commit is created without a parent (orphan commit).
func CreateCommit(repo *git.Repository, treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string) (plumbing.Hash, error) {
now := time.Now()
sig := object.Signature{
Name: authorName,
Email: authorEmail,
When: now,
}

commit := &object.Commit{
TreeHash: treeHash,
Author: sig,
Committer: sig,
Message: message,
}

if parentHash != plumbing.ZeroHash {
commit.ParentHashes = []plumbing.Hash{parentHash}
}

obj := repo.Storer.NewEncodedObject()
if err := commit.Encode(obj); err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to encode commit: %w", err)
}

hash, err := repo.Storer.SetEncodedObject(obj)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to store commit: %w", err)
}

return hash, nil
}

// readTranscriptFromTree reads a transcript from a git tree, handling both chunked and non-chunked formats.
// It checks for chunk files first (.001, .002, etc.), then falls back to the base file.
// The agentType is used for reassembling chunks in the correct format.
Expand Down
31 changes: 1 addition & 30 deletions cmd/entire/cli/checkpoint/temporary.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,36 +764,7 @@ func (s *GitStore) buildTreeWithChanges(

// createCommit creates a commit object.
func (s *GitStore) createCommit(treeHash, parentHash plumbing.Hash, message, authorName, authorEmail string) (plumbing.Hash, error) {
now := time.Now()
sig := object.Signature{
Name: authorName,
Email: authorEmail,
When: now,
}

commit := &object.Commit{
TreeHash: treeHash,
Author: sig,
Committer: sig,
Message: message,
}

// Add parent if not a new branch
if parentHash != plumbing.ZeroHash {
commit.ParentHashes = []plumbing.Hash{parentHash}
}

obj := s.repo.Storer.NewEncodedObject()
if err := commit.Encode(obj); err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to encode commit: %w", err)
}

hash, err := s.repo.Storer.SetEncodedObject(obj)
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to store commit: %w", err)
}

return hash, nil
return CreateCommit(s.repo, treeHash, parentHash, message, authorName, authorEmail)
}

// Helper functions extracted from strategy/common.go
Expand Down
10 changes: 5 additions & 5 deletions cmd/entire/cli/integration_test/deferred_finalization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func TestShadow_CarryForward_ActiveSession(t *testing.T) {
// indicate a regression in carry-forward cleanup.
branchesAfterAll := env.ListBranchesWithPrefix("entire/")
for _, b := range branchesAfterAll {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
t.Errorf("Unexpected shadow branch after all files committed: %s", b)
}
}
Expand Down Expand Up @@ -576,7 +576,7 @@ func TestShadow_AgentCommitsMidTurn_UserCommitsRemainder(t *testing.T) {
// No shadow branches should remain after all files are committed
branchesAfter := env.ListBranchesWithPrefix("entire/")
for _, b := range branchesAfter {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
t.Errorf("Unexpected shadow branch after all files committed: %s", b)
}
}
Expand Down Expand Up @@ -1180,7 +1180,7 @@ func TestShadow_EndedSession_UserCommitsRemainingFiles(t *testing.T) {
// No shadow branches should remain
branchesAfter := env.ListBranchesWithPrefix("entire/")
for _, b := range branchesAfter {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
t.Errorf("Unexpected shadow branch after all files committed: %s", b)
}
}
Expand Down Expand Up @@ -1274,7 +1274,7 @@ func TestShadow_DeletedFiles_CheckpointAndCarryForward(t *testing.T) {
// doesn't produce full metadata (known limitation).
branchesAfter := env.ListBranchesWithPrefix("entire/")
for _, b := range branchesAfter {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
t.Logf("Shadow branch remaining after commits (may be expected for deleted files): %s", b)
}
}
Expand Down Expand Up @@ -1368,7 +1368,7 @@ func TestShadow_CarryForward_ModifiedExistingFiles(t *testing.T) {
// No shadow branches should remain
branchesAfter := env.ListBranchesWithPrefix("entire/")
for _, b := range branchesAfter {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
t.Errorf("Unexpected shadow branch after all files committed: %s", b)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestShadowStrategy_MidSessionCommit_FromTranscript(t *testing.T) {
shadowBranches := env.ListBranchesWithPrefix("entire/")
hasShadowBranch := false
for _, b := range shadowBranches {
if b != paths.MetadataBranchName {
if b != paths.MetadataBranchName && b != paths.TrailsBranchName {
hasShadowBranch = true
break
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/entire/cli/paths/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const (
// MetadataBranchName is the orphan branch used by manual-commit strategy to store metadata
const MetadataBranchName = "entire/checkpoints/v1"

// TrailsBranchName is the orphan branch used to store trail metadata.
// Trails are branch-centric work tracking abstractions that link to checkpoints by branch name.
const TrailsBranchName = "entire/trails/v1"

// CheckpointPath returns the sharded storage path for a checkpoint ID.
// Uses first 2 characters as shard (256 buckets), remaining as folder name.
// Example: "a3b2c4d5e6f7" -> "a3/b2c4d5e6f7"
Expand Down
1 change: 1 addition & 0 deletions cmd/entire/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func NewRootCmd() *cobra.Command {
cmd.AddCommand(newVersionCmd())
cmd.AddCommand(newExplainCmd())
cmd.AddCommand(newDoctorCmd())
cmd.AddCommand(newTrailCmd())
cmd.AddCommand(newSendAnalyticsCmd())
cmd.AddCommand(newCurlBashPostInstallCmd())

Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/strategy/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ var shadowBranchPattern = regexp.MustCompile(`^entire/[0-9a-fA-F]{7,}(-[0-9a-fA-
// commit hash is at least 7 hex characters and worktree hash is 6 hex characters.
// The "entire/checkpoints/v1" branch is NOT a shadow branch.
func IsShadowBranch(branchName string) bool {
// Explicitly exclude entire/checkpoints/v1
if branchName == paths.MetadataBranchName {
// Explicitly exclude metadata and trails branches
if branchName == paths.MetadataBranchName || branchName == paths.TrailsBranchName {
return false
}
return shadowBranchPattern.MatchString(branchName)
Expand Down
2 changes: 2 additions & 0 deletions cmd/entire/cli/strategy/manual_commit_condensation.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ func (s *ManualCommitStrategy) CondenseSession(ctx context.Context, repo *git.Re
SessionID: state.SessionID,
CheckpointsCount: state.StepCount,
FilesTouched: sessionData.FilesTouched,
Prompts: sessionData.Prompts,
TotalTranscriptLines: sessionData.FullTranscriptLines,
Transcript: sessionData.Transcript,
}, nil
}

Expand Down
36 changes: 36 additions & 0 deletions cmd/entire/cli/strategy/manual_commit_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/entireio/cli/cmd/entire/cli/session"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/stringutil"
"github.com/entireio/cli/cmd/entire/cli/trail"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/redact"

Expand Down Expand Up @@ -1015,6 +1016,16 @@ func (s *ManualCommitStrategy) condenseAndUpdateState(
return false
}

// Link checkpoint to trail (best-effort)
branchName := GetCurrentBranchName(repo)
if branchName != "" && branchName != GetDefaultBranchName(repo) {
store := trail.NewStore(repo)
existing, findErr := store.FindByBranch(branchName)
if findErr == nil && existing != nil {
appendCheckpointToExistingTrail(store, existing.TrailID, result.CheckpointID, head.Hash(), result.Prompts)
}
}

// Track this shadow branch for cleanup
shadowBranchesToDelete[shadowBranchName] = struct{}{}

Expand Down Expand Up @@ -2230,3 +2241,28 @@ func (s *ManualCommitStrategy) carryForwardToNewShadowBranch(
slog.Int("remaining_files", len(remainingFiles)),
)
}

// appendCheckpointToExistingTrail links a checkpoint to the given trail.
// Best-effort: silently returns on any error (trails are non-critical metadata).
func appendCheckpointToExistingTrail(store *trail.Store, trailID trail.ID, cpID id.CheckpointID, commitSHA plumbing.Hash, prompts []string) {
var summary *string
if len(prompts) > 0 {
s := truncateForSummary(prompts[len(prompts)-1], 200)
summary = &s
}

//nolint:errcheck,gosec // best-effort: trail checkpoint linking is non-critical
store.AddCheckpoint(trailID, trail.CheckpointRef{
CheckpointID: cpID.String(),
CommitSHA: commitSHA.String(),
CreatedAt: time.Now().UTC(),
Summary: summary,
})
}

// truncateForSummary takes the first line of s and truncates to maxLen runes.
func truncateForSummary(s string, maxLen int) string {
line, _, _ := strings.Cut(s, "\n")
line = strings.TrimSpace(line)
return stringutil.TruncateRunes(line, maxLen, "...")
}
5 changes: 4 additions & 1 deletion cmd/entire/cli/strategy/manual_commit_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ import (
// - "prompt" (default): ask user with option to enable auto
// - "false"/"off"/"no": never push
func (s *ManualCommitStrategy) PrePush(ctx context.Context, remote string) error {
return pushSessionsBranchCommon(ctx, remote, paths.MetadataBranchName)
if err := pushSessionsBranchCommon(ctx, remote, paths.MetadataBranchName); err != nil {
return err
}
return PushTrailsBranch(ctx, remote)
}
4 changes: 3 additions & 1 deletion cmd/entire/cli/strategy/manual_commit_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ type CondenseResult struct {
SessionID string
CheckpointsCount int
FilesTouched []string
TotalTranscriptLines int // Total lines in transcript after this condensation
Prompts []string // User prompts from the condensed session
TotalTranscriptLines int // Total lines in transcript after this condensation
Transcript []byte // Raw transcript bytes for downstream consumers (trail title generation)
}

// ExtractedSessionData contains data extracted from a shadow branch.
Expand Down
Loading