Initial commit
diff --git a/loop/agent_git_test.go b/loop/agent_git_test.go
new file mode 100644
index 0000000..399943b
--- /dev/null
+++ b/loop/agent_git_test.go
@@ -0,0 +1,263 @@
+package loop
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+)
+
+// TestGitCommitTracking tests the git commit tracking functionality
+func TestGitCommitTracking(t *testing.T) {
+	// Create a temporary directory for our test git repo
+	tempDir := t.TempDir() // Automatically cleaned up when the test completes
+
+	// Initialize a git repo in the temp directory
+	cmd := exec.Command("git", "init")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to initialize git repo: %v", err)
+	}
+
+	// Configure git user for commits
+	cmd = exec.Command("git", "config", "user.name", "Test User")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to configure git user name: %v", err)
+	}
+
+	cmd = exec.Command("git", "config", "user.email", "test@example.com")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to configure git user email: %v", err)
+	}
+
+	// Make an initial commit
+	testFile := filepath.Join(tempDir, "test.txt")
+	if err := os.WriteFile(testFile, []byte("initial content\n"), 0o644); err != nil {
+		t.Fatalf("Failed to write file: %v", err)
+	}
+
+	cmd = exec.Command("git", "add", "test.txt")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to add file: %v", err)
+	}
+
+	cmd = exec.Command("git", "commit", "-m", "Initial commit")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to create initial commit: %v", err)
+	}
+
+	// Get the initial commit hash
+	cmd = exec.Command("git", "rev-parse", "HEAD")
+	cmd.Dir = tempDir
+	initialCommitOutput, err := cmd.Output()
+	if err != nil {
+		t.Fatalf("Failed to get initial commit hash: %v", err)
+	}
+	initialCommit := strings.TrimSpace(string(initialCommitOutput))
+
+	// Create agent with the temp repo
+	agent := &Agent{
+		workingDir:    tempDir,
+		repoRoot:      tempDir, // Set repoRoot to same as workingDir for this test
+		outbox:        make(chan AgentMessage, 100),
+		seenCommits:   make(map[string]bool),
+		initialCommit: initialCommit,
+	}
+
+	// Make a new commit
+	if err := os.WriteFile(testFile, []byte("updated content\n"), 0o644); err != nil {
+		t.Fatalf("Failed to update file: %v", err)
+	}
+
+	cmd = exec.Command("git", "add", "test.txt")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to add updated file: %v", err)
+	}
+
+	cmd = exec.Command("git", "commit", "-m", "Second commit\n\nThis commit has a multi-line message\nwith details about the changes.")
+	cmd.Dir = tempDir
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("Failed to create second commit: %v", err)
+	}
+
+	// Call handleGitCommits and verify we get a commit message
+	ctx := context.Background()
+	_, err = agent.handleGitCommits(ctx)
+	if err != nil {
+		t.Fatalf("handleGitCommits failed: %v", err)
+	}
+
+	// Check if we received a commit message
+	var commitMsg AgentMessage
+	select {
+	case commitMsg = <-agent.outbox:
+		// We got a message
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("Timed out waiting for commit message")
+	}
+
+	// Verify the commit message
+	if commitMsg.Type != CommitMessageType {
+		t.Errorf("Expected message type %s, got %s", CommitMessageType, commitMsg.Type)
+	}
+
+	if len(commitMsg.Commits) < 1 {
+		t.Fatalf("Expected at least 1 commit, got %d", len(commitMsg.Commits))
+	}
+
+	// Find the second commit
+	var commit *GitCommit
+	found := false
+	for _, c := range commitMsg.Commits {
+		if strings.HasPrefix(c.Subject, "Second commit") {
+			commit = c
+			found = true
+			break
+		}
+	}
+
+	if !found {
+		t.Fatalf("Could not find 'Second commit' in commits")
+	}
+	if !strings.HasPrefix(commit.Subject, "Second commit") {
+		t.Errorf("Expected commit subject 'Second commit', got '%s'", commit.Subject)
+	}
+
+	if !strings.Contains(commit.Body, "multi-line message") {
+		t.Errorf("Expected body to contain 'multi-line message', got '%s'", commit.Body)
+	}
+
+	// Test with many commits
+	if testing.Short() {
+		t.Skip("Skipping multiple commits test in short mode")
+	}
+
+	// Make multiple commits (more than 100)
+	for i := 0; i < 110; i++ {
+		newContent := []byte(fmt.Sprintf("content update %d\n", i))
+		if err := os.WriteFile(testFile, newContent, 0o644); err != nil {
+			t.Fatalf("Failed to update file: %v", err)
+		}
+
+		cmd = exec.Command("git", "add", "test.txt")
+		cmd.Dir = tempDir
+		if err := cmd.Run(); err != nil {
+			t.Fatalf("Failed to add updated file: %v", err)
+		}
+
+		cmd = exec.Command("git", "commit", "-m", fmt.Sprintf("Commit %d", i+3))
+		cmd.Dir = tempDir
+		if err := cmd.Run(); err != nil {
+			t.Fatalf("Failed to create commit %d: %v", i+3, err)
+		}
+	}
+
+	// Reset the outbox channel and seen commits map
+	agent.outbox = make(chan AgentMessage, 100)
+	agent.seenCommits = make(map[string]bool)
+
+	// Call handleGitCommits again - it should still work but only show at most 100 commits
+	_, err = agent.handleGitCommits(ctx)
+	if err != nil {
+		t.Fatalf("handleGitCommits failed: %v", err)
+	}
+
+	// Check if we received a commit message
+	select {
+	case commitMsg = <-agent.outbox:
+		// We got a message
+	case <-time.After(500 * time.Millisecond):
+		t.Fatal("Timed out waiting for commit message")
+	}
+
+	// Should have at most 100 commits due to the -n 100 limit in git log
+	if len(commitMsg.Commits) > 100 {
+		t.Errorf("Expected at most 100 commits, got %d", len(commitMsg.Commits))
+	}
+
+	if len(commitMsg.Commits) < 50 {
+		t.Errorf("Expected at least 50 commits, but only got %d", len(commitMsg.Commits))
+	}
+
+	t.Logf("Received %d commits out of 112 total", len(commitMsg.Commits))
+}
+
+// TestParseGitLog tests the parseGitLog function
+func TestParseGitLog(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    string
+		expected []GitCommit
+	}{
+		{
+			name:     "Empty input",
+			input:    "",
+			expected: []GitCommit{},
+		},
+		{
+			name:  "Single commit",
+			input: "abcdef1234567890\x00Initial commit\x00This is the first commit\x00",
+			expected: []GitCommit{
+				{Hash: "abcdef1234567890", Subject: "Initial commit", Body: "This is the first commit"},
+			},
+		},
+		{
+			name: "Multiple commits",
+			input: "abcdef1234567890\x00Initial commit\x00This is the first commit\x00" +
+				"fedcba0987654321\x00Second commit\x00This is the second commit\x00" +
+				"123456abcdef7890\x00Third commit\x00This is the third commit\x00",
+			expected: []GitCommit{
+				{Hash: "abcdef1234567890", Subject: "Initial commit", Body: "This is the first commit"},
+				{Hash: "fedcba0987654321", Subject: "Second commit", Body: "This is the second commit"},
+				{Hash: "123456abcdef7890", Subject: "Third commit", Body: "This is the third commit"},
+			},
+		},
+		{
+			name:  "Commit with multi-line body",
+			input: "abcdef1234567890\x00Commit with multi-line body\x00This is a commit\nwith a multi-line\nbody message\x00",
+			expected: []GitCommit{
+				{Hash: "abcdef1234567890", Subject: "Commit with multi-line body", Body: "This is a commit\nwith a multi-line\nbody message"},
+			},
+		},
+		{
+			name:  "Commit with empty body",
+			input: "abcdef1234567890\x00Commit with empty body\x00\x00",
+			expected: []GitCommit{
+				{Hash: "abcdef1234567890", Subject: "Commit with empty body", Body: ""},
+			},
+		},
+		{
+			name:  "Empty parts removed",
+			input: "\x00abcdef1234567890\x00Initial commit\x00This is the first commit\x00\x00",
+			expected: []GitCommit{
+				{Hash: "abcdef1234567890", Subject: "Initial commit", Body: "This is the first commit"},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actual := parseGitLog(tt.input)
+
+			if len(actual) != len(tt.expected) {
+				t.Fatalf("Expected %d commits, got %d", len(tt.expected), len(actual))
+			}
+
+			for i, commit := range actual {
+				expected := tt.expected[i]
+				if commit.Hash != expected.Hash || commit.Subject != expected.Subject || commit.Body != expected.Body {
+					t.Errorf("Commit %d doesn't match:\nExpected: %+v\nGot:      %+v", i, expected, commit)
+				}
+			}
+		})
+	}
+}