diff --git a/internal/parser/provider.go b/internal/parser/provider.go index f8413a11e..c2a56bd04 100644 --- a/internal/parser/provider.go +++ b/internal/parser/provider.go @@ -361,6 +361,8 @@ func providerFactoryForDef(def AgentDef) ProviderFactory { return newPiProviderFactory(def) case AgentQwen: return newQwenProviderFactory(def) + case AgentWorkBuddy: + return newWorkBuddyProviderFactory(def) case AgentZencoder: return newZencoderProviderFactory(def) default: diff --git a/internal/parser/provider_migration.go b/internal/parser/provider_migration.go index 27356b66e..aa1b8e323 100644 --- a/internal/parser/provider_migration.go +++ b/internal/parser/provider_migration.go @@ -45,7 +45,7 @@ var providerMigrationModes = map[AgentType]ProviderMigrationMode{ AgentKiroIDE: ProviderMigrationLegacyOnly, AgentCortex: ProviderMigrationLegacyOnly, AgentHermes: ProviderMigrationLegacyOnly, - AgentWorkBuddy: ProviderMigrationLegacyOnly, + AgentWorkBuddy: ProviderMigrationProviderAuthoritative, AgentForge: ProviderMigrationLegacyOnly, AgentPiebald: ProviderMigrationLegacyOnly, AgentWarp: ProviderMigrationLegacyOnly, diff --git a/internal/parser/types.go b/internal/parser/types.go index 7f83e38b9..3b4fc8997 100644 --- a/internal/parser/types.go +++ b/internal/parser/types.go @@ -456,15 +456,13 @@ var Registry = []AgentDef{ FindSourceFunc: FindHermesSourceFile, }, { - Type: AgentWorkBuddy, - DisplayName: "WorkBuddy", - EnvVar: "WORKBUDDY_PROJECTS_DIR", - ConfigKey: "workbuddy_project_dirs", - DefaultDirs: []string{".workbuddy/projects"}, - IDPrefix: "workbuddy:", - FileBased: true, - DiscoverFunc: DiscoverWorkBuddySessions, - FindSourceFunc: FindWorkBuddySourceFile, + Type: AgentWorkBuddy, + DisplayName: "WorkBuddy", + EnvVar: "WORKBUDDY_PROJECTS_DIR", + ConfigKey: "workbuddy_project_dirs", + DefaultDirs: []string{".workbuddy/projects"}, + IDPrefix: "workbuddy:", + FileBased: true, }, { Type: AgentForge, diff --git a/internal/parser/workbuddy.go b/internal/parser/workbuddy.go index 135f939f4..847a98393 100644 --- a/internal/parser/workbuddy.go +++ b/internal/parser/workbuddy.go @@ -5,108 +5,13 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" "time" "github.com/tidwall/gjson" ) -func DiscoverWorkBuddySessions(projectsDir string) []DiscoveredFile { - if projectsDir == "" { - return nil - } - - projects, err := os.ReadDir(projectsDir) - if err != nil { - return nil - } - - var files []DiscoveredFile - for _, projEntry := range projects { - if !isDirOrSymlink(projEntry, projectsDir) { - continue - } - project := projEntry.Name() - projectDir := filepath.Join(projectsDir, project) - entries, err := os.ReadDir(projectDir) - if err != nil { - continue - } - for _, entry := range entries { - name := entry.Name() - if !entry.IsDir() && strings.HasSuffix(name, ".jsonl") { - stem := strings.TrimSuffix(name, ".jsonl") - if IsValidSessionID(stem) { - files = append(files, DiscoveredFile{ - Path: filepath.Join(projectDir, name), - Project: project, - Agent: AgentWorkBuddy, - }) - } - continue - } - if !isDirOrSymlink(entry, projectDir) || !IsValidSessionID(name) { - continue - } - subagentsDir := filepath.Join(projectDir, name, "subagents") - subagents, err := os.ReadDir(subagentsDir) - if err != nil { - continue - } - for _, sub := range subagents { - if sub.IsDir() || !strings.HasSuffix(sub.Name(), ".jsonl") { - continue - } - files = append(files, DiscoveredFile{ - Path: filepath.Join(subagentsDir, sub.Name()), - Project: project, - Agent: AgentWorkBuddy, - }) - } - } - } - - sort.Slice(files, func(i, j int) bool { - return files[i].Path < files[j].Path - }) - return files -} - -func FindWorkBuddySourceFile(projectsDir, rawID string) string { - if projectsDir == "" { - return "" - } - rawID = strings.TrimPrefix(rawID, "workbuddy:") - sessionID, subagentID, hasSubagent := strings.Cut(rawID, ":subagent:") - if !IsValidSessionID(sessionID) { - return "" - } - if hasSubagent && !IsValidSessionID(subagentID) { - return "" - } - - projects, err := os.ReadDir(projectsDir) - if err != nil { - return "" - } - for _, projEntry := range projects { - if !isDirOrSymlink(projEntry, projectsDir) { - continue - } - projectDir := filepath.Join(projectsDir, projEntry.Name()) - candidate := filepath.Join(projectDir, sessionID+".jsonl") - if hasSubagent { - candidate = filepath.Join(projectDir, sessionID, "subagents", subagentID+".jsonl") - } - if _, err := os.Stat(candidate); err == nil { - return candidate - } - } - return "" -} - -func ParseWorkBuddySession(path, project, machine string) (*ParsedSession, []ParsedMessage, error) { +func parseWorkBuddySession(path, project, machine string) (*ParsedSession, []ParsedMessage, error) { info, err := os.Stat(path) if err != nil { return nil, nil, fmt.Errorf("stat %s: %w", path, err) diff --git a/internal/parser/workbuddy_provider.go b/internal/parser/workbuddy_provider.go new file mode 100644 index 000000000..d88ca5593 --- /dev/null +++ b/internal/parser/workbuddy_provider.go @@ -0,0 +1,129 @@ +package parser + +import ( + "context" + "path/filepath" + "strings" +) + +// WorkBuddy stores each session as a JSONL file in a project directory, with +// subagent transcripts nested under a "subagents" subdirectory. It is a +// directory-of-files provider: discovery, watching, change classification, +// lookup, and fingerprinting come from JSONLSourceSet, and the ParseFile option +// makes that source set a full SourceSet so it rides the generic factory. +func newWorkBuddyProviderFactory(def AgentDef) ProviderFactory { + return newSourceSetFactory( + def, + workBuddyProviderCapabilities(), + func(cfg ProviderConfig) SourceSet { return newWorkBuddySourceSet(cfg.Roots) }, + ) +} + +func newWorkBuddySourceSet(roots []string) JSONLSourceSet { + return newJSONLSourceSet(AgentWorkBuddy, roots, + withRecursive(), + withSymlinkFollowing(), + withContentHashing(), + withIncludePath(isWorkBuddySourcePath), + withProjectHint(workBuddyProjectHintFromPath), + withSessionIDFromPath(workBuddySessionIDFromPath), + withLookupIDValid(isWorkBuddyLookupID), + withParseFile(workBuddyParseFile), + ) +} + +func workBuddyParseFile( + _ context.Context, path string, req ParseRequest, +) ([]ParseResult, []string, error) { + sess, msgs, err := parseWorkBuddySession(path, req.Source.ProjectHint, req.Machine) + if err != nil { + return nil, nil, err + } + if sess == nil { + return nil, nil, nil + } + if req.Fingerprint.Hash != "" { + sess.File.Hash = req.Fingerprint.Hash + } + return []ParseResult{{Session: *sess, Messages: msgs}}, nil, nil +} + +func isWorkBuddySourcePath(root, path string) bool { + parts, ok := workBuddyPathParts(root, path) + if !ok { + return false + } + switch len(parts) { + case 2: + stem, ok := strings.CutSuffix(parts[1], ".jsonl") + return ok && IsValidSessionID(stem) + case 4: + return IsValidSessionID(parts[1]) && + parts[2] == "subagents" && + strings.HasSuffix(parts[3], ".jsonl") + default: + return false + } +} + +func workBuddyProjectHintFromPath(root, path string) string { + parts, ok := workBuddyPathParts(root, path) + if !ok || len(parts) < 2 { + return "" + } + return parts[0] +} + +func workBuddySessionIDFromPath(root, path string) string { + if !isWorkBuddySourcePath(root, path) { + return "" + } + parts, _ := workBuddyPathParts(root, path) + stem := strings.TrimSuffix(filepath.Base(path), ".jsonl") + if len(parts) == 4 { + return parts[1] + ":subagent:" + stem + } + return stem +} + +func isWorkBuddyLookupID(rawID string) bool { + if rawID == "" { + return false + } + sessionID, subagentID, hasSubagent := strings.Cut(rawID, ":subagent:") + if !IsValidSessionID(sessionID) { + return false + } + return !hasSubagent || IsValidSessionID(subagentID) +} + +func workBuddyPathParts(root, path string) ([]string, bool) { + rel, err := filepath.Rel(root, path) + if err != nil { + return nil, false + } + parts := strings.Split(rel, string(filepath.Separator)) + for _, part := range parts { + if part == "" || part == "." || part == ".." { + return nil, false + } + } + return parts, true +} + +func workBuddyProviderCapabilities() Capabilities { + return Capabilities{ + Source: jsonlFileProviderSourceCapabilities(), + Content: ContentCapabilities{ + FirstMessage: CapabilitySupported, + Cwd: CapabilitySupported, + Relationships: CapabilitySupported, + Subagents: CapabilitySupported, + ToolCalls: CapabilitySupported, + ToolResults: CapabilitySupported, + PerMessageTokenUsage: CapabilitySupported, + MalformedLineCount: CapabilitySupported, + Model: CapabilitySupported, + }, + } +} diff --git a/internal/parser/workbuddy_provider_test.go b/internal/parser/workbuddy_provider_test.go new file mode 100644 index 000000000..c411801c1 --- /dev/null +++ b/internal/parser/workbuddy_provider_test.go @@ -0,0 +1,238 @@ +package parser + +import ( + "context" + "crypto/sha256" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorkBuddyProviderFactoryReplacesLegacyAdapter(t *testing.T) { + factory, ok := ProviderFactoryByType(AgentWorkBuddy) + require.True(t, ok) + require.NotNil(t, factory) + + caps := factory.Capabilities() + assert.Equal(t, CapabilitySupported, caps.Source.DiscoverSources) + assert.Equal(t, CapabilitySupported, caps.Source.WatchSources) + assert.Equal(t, CapabilitySupported, caps.Source.ClassifyChangedPath) + assert.Equal(t, CapabilitySupported, caps.Source.FindSource) + assert.Equal(t, CapabilitySupported, caps.Source.CompositeFingerprint) + assert.Equal(t, CapabilitySupported, caps.Content.FirstMessage) + assert.Equal(t, CapabilitySupported, caps.Content.Cwd) + assert.Equal(t, CapabilitySupported, caps.Content.Relationships) + assert.Equal(t, CapabilitySupported, caps.Content.Subagents) + assert.Equal(t, CapabilitySupported, caps.Content.ToolCalls) + assert.Equal(t, CapabilitySupported, caps.Content.ToolResults) + assert.Equal(t, CapabilitySupported, caps.Content.PerMessageTokenUsage) + assert.Equal(t, CapabilitySupported, caps.Content.Model) + assert.Equal(t, CapabilitySupported, caps.Content.MalformedLineCount) + + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{ + Roots: []string{t.TempDir()}, + Machine: "devbox", + }) + require.True(t, ok) + require.NotNil(t, provider) +} + +func TestWorkBuddyProviderSourceMethods(t *testing.T) { + root := t.TempDir() + sessionID := "11111111-1111-4111-8111-111111111111" + subagentID := "agent-123" + projectDir := filepath.Join(root, "proj") + sourcePath := filepath.Join(projectDir, sessionID+".jsonl") + subagentPath := filepath.Join( + projectDir, sessionID, "subagents", subagentID+".jsonl", + ) + nonIDSubagentPath := filepath.Join( + projectDir, sessionID, "subagents", "2025.01.01.jsonl", + ) + writeSourceFile(t, sourcePath, workBuddyProviderFixture("hello")) + writeSourceFile(t, subagentPath, workBuddyProviderFixture("sub task")) + writeSourceFile(t, nonIDSubagentPath, workBuddyProviderFixture("dated sub task")) + writeSourceFile(t, filepath.Join(projectDir, "2025.01.01.jsonl"), "{}\n") + writeSourceFile(t, filepath.Join(projectDir, sessionID, "tool-results", "tool_123.txt"), "{}\n") + writeSourceFile(t, filepath.Join(root, sessionID+".jsonl"), "{}\n") + writeSourceFile(t, filepath.Join(projectDir, sessionID, "subagents", "nested", "deep.jsonl"), "{}\n") + + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + + discovered, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, discovered, 3) + assert.Equal( + t, + []string{sourcePath, nonIDSubagentPath, subagentPath}, + sourceDisplayPaths(discovered), + ) + assert.Equal(t, []string{"proj", "proj", "proj"}, sourceProjects(discovered)) + + plan, err := provider.WatchPlan(context.Background()) + require.NoError(t, err) + require.Len(t, plan.Roots, 1) + assert.Equal(t, root, plan.Roots[0].Path) + assert.True(t, plan.Roots[0].Recursive) + assert.Equal(t, []string{"*.jsonl"}, plan.Roots[0].IncludeGlobs) + + found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{ + FullSessionID: "host~workbuddy:" + sessionID, + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, sourcePath, found.DisplayPath) + + fingerprint, err := provider.Fingerprint(context.Background(), found) + require.NoError(t, err) + assert.Equal(t, sourcePath, fingerprint.Key) + assert.NotZero(t, fingerprint.Size) + assert.NotZero(t, fingerprint.MTimeNS) + + found, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + RawSessionID: sessionID + ":subagent:" + subagentID, + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, subagentPath, found.DisplayPath) + + _, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + RawSessionID: sessionID + ":subagent:../agent-123", + }) + require.NoError(t, err) + assert.False(t, ok) + + _, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + RawSessionID: sessionID + ":subagent:2025.01.01", + }) + require.NoError(t, err) + assert.False(t, ok) + + found, ok, err = provider.FindSource(context.Background(), FindSourceRequest{ + StoredFilePath: subagentPath, + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, subagentPath, found.DisplayPath) + + require.NoError(t, os.Remove(subagentPath)) + changed, err := provider.SourcesForChangedPath( + context.Background(), + ChangedPathRequest{Path: subagentPath, EventKind: "remove", WatchRoot: root}, + ) + require.NoError(t, err) + require.Len(t, changed, 1) + assert.Equal(t, subagentPath, changed[0].DisplayPath) +} + +func TestWorkBuddyProviderDiscoversSymlinkedProjectDirectory(t *testing.T) { + root := t.TempDir() + targetDir := t.TempDir() + sessionID := "11111111-1111-4111-8111-111111111111" + linkDir := filepath.Join(root, "proj") + sourcePath := filepath.Join(linkDir, sessionID+".jsonl") + writeSourceFile(t, filepath.Join(targetDir, sessionID+".jsonl"), workBuddyProviderFixture("hello")) + if err := os.Symlink(targetDir, linkDir); err != nil { + t.Skipf("symlink not supported: %v", err) + } + + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + + discovered, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, discovered, 1) + assert.Equal(t, sourcePath, discovered[0].DisplayPath) + + found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{ + FullSessionID: "host~workbuddy:" + sessionID, + }) + require.NoError(t, err) + require.True(t, ok) + assert.Equal(t, sourcePath, found.DisplayPath) +} + +func TestWorkBuddyProviderParseMainAndSubagent(t *testing.T) { + root := t.TempDir() + sessionID := "11111111-1111-4111-8111-111111111111" + subagentID := "agent-123" + sourcePath := filepath.Join(root, "proj", sessionID+".jsonl") + subagentPath := filepath.Join(root, "proj", sessionID, "subagents", subagentID+".jsonl") + mainContent := workBuddyProviderFixture("hello") + subContent := workBuddyProviderFixture("sub task") + writeSourceFile(t, sourcePath, mainContent) + writeSourceFile(t, subagentPath, subContent) + + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{ + Roots: []string{root}, + Machine: "devbox", + }) + require.True(t, ok) + + sources, err := provider.Discover(context.Background()) + require.NoError(t, err) + require.Len(t, sources, 2) + + mainFingerprint, err := provider.Fingerprint(context.Background(), sources[0]) + require.NoError(t, err) + mainOutcome, err := provider.Parse(context.Background(), ParseRequest{ + Source: sources[0], + Fingerprint: mainFingerprint, + }) + require.NoError(t, err) + require.True(t, mainOutcome.ResultSetComplete) + require.Len(t, mainOutcome.Results, 1) + mainResult := mainOutcome.Results[0] + assert.Equal(t, DataVersionCurrent, mainResult.DataVersion) + assert.Equal(t, "workbuddy:"+sessionID, mainResult.Result.Session.ID) + assert.Equal(t, "devbox", mainResult.Result.Session.Machine) + assert.Equal(t, + fmt.Sprintf("%x", sha256.Sum256([]byte(mainContent))), + mainResult.Result.Session.File.Hash, + ) + assert.Len(t, mainResult.Result.Messages, 3) + assert.Equal(t, "hello", mainResult.Result.Session.FirstMessage) + assert.True(t, mainResult.Result.Session.HasTotalOutputTokens) + + subFingerprint, err := provider.Fingerprint(context.Background(), sources[1]) + require.NoError(t, err) + subOutcome, err := provider.Parse(context.Background(), ParseRequest{ + Source: sources[1], + Fingerprint: subFingerprint, + }) + require.NoError(t, err) + require.True(t, subOutcome.ResultSetComplete) + require.Len(t, subOutcome.Results, 1) + subResult := subOutcome.Results[0] + assert.Equal(t, DataVersionCurrent, subResult.DataVersion) + assert.Equal( + t, + "workbuddy:"+sessionID+":subagent:"+subagentID, + subResult.Result.Session.ID, + ) + assert.Equal(t, "workbuddy:"+sessionID, subResult.Result.Session.ParentSessionID) + assert.Equal(t, RelSubagent, subResult.Result.Session.RelationshipType) + assert.Equal(t, + fmt.Sprintf("%x", sha256.Sum256([]byte(subContent))), + subResult.Result.Session.File.Hash, + ) +} + +func workBuddyProviderFixture(firstMessage string) string { + return fmt.Sprintf( + `{"id":"u1","timestamp":1778749186168,"type":"message","role":"user","content":[{"type":"input_text","text":%q}],"cwd":"/tmp/cwd-project"} +{"id":"a1","timestamp":1778749187168,"type":"message","role":"assistant","content":[{"type":"output_text","text":"hi"}],"providerData":{"model":"gpt-5.5","usage":{"inputTokens":20,"outputTokens":4,"cacheReadInputTokens":5}}} +{"id":"fc1","timestamp":1778749188168,"type":"function_call","name":"Bash","callId":"call_1","arguments":"{\"command\":\"pwd\"}"} +`, firstMessage) +} diff --git a/internal/parser/workbuddy_test.go b/internal/parser/workbuddy_test.go index d42024d78..19890e17c 100644 --- a/internal/parser/workbuddy_test.go +++ b/internal/parser/workbuddy_test.go @@ -1,6 +1,7 @@ package parser import ( + "context" "fmt" "os" "path/filepath" @@ -11,6 +12,47 @@ import ( "github.com/tidwall/gjson" ) +func parseWorkBuddyTestSession( + t testing.TB, + path, project, machine string, +) (*ParsedSession, []ParsedMessage, error) { + t.Helper() + return parseWorkBuddySession(path, project, machine) +} + +func discoverWorkBuddyTestSessions(t testing.TB, root string) []DiscoveredFile { + t.Helper() + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{Roots: []string{root}}) + require.True(t, ok) + sources, err := provider.Discover(context.Background()) + require.NoError(t, err) + + files := make([]DiscoveredFile, 0, len(sources)) + for _, source := range sources { + files = append(files, DiscoveredFile{ + Path: source.DisplayPath, + Project: source.ProjectHint, + Agent: source.Provider, + }) + } + return files +} + +func findWorkBuddyTestSourceFile(t testing.TB, root, rawID string) string { + t.Helper() + provider, ok := NewProvider(AgentWorkBuddy, ProviderConfig{Roots: []string{root}}) + require.True(t, ok) + source, found, err := provider.FindSource( + context.Background(), + FindSourceRequest{RawSessionID: rawID}, + ) + require.NoError(t, err) + if !found { + return "" + } + return source.DisplayPath +} + func TestDiscoverWorkBuddySessions(t *testing.T) { root := t.TempDir() mainPath := filepath.Join(root, "proj", "11111111-1111-4111-8111-111111111111.jsonl") @@ -21,7 +63,7 @@ func TestDiscoverWorkBuddySessions(t *testing.T) { require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644), "WriteFile(%q)", path) } - files := DiscoverWorkBuddySessions(root) + files := discoverWorkBuddyTestSessions(t, root) require.Len(t, files, 2) assert.Equal(t, mainPath, files[0].Path) assert.Equal(t, "proj", files[0].Project) @@ -44,7 +86,7 @@ func TestParseWorkBuddySession(t *testing.T) { `, cwd, cwd) require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, msgs, err := ParseWorkBuddySession(path, "proj", "local") + sess, msgs, err := parseWorkBuddyTestSession(t, path, "proj", "local") require.NoError(t, err) require.NotNil(t, sess, "session nil") assert.Equal(t, "workbuddy:11111111-1111-4111-8111-111111111111", sess.ID) @@ -76,7 +118,7 @@ func TestParseWorkBuddySessionDoesNotDoubleCountOpenAICachedTokens(t *testing.T) ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, msgs, err := ParseWorkBuddySession(path, "proj", "local") + sess, msgs, err := parseWorkBuddyTestSession(t, path, "proj", "local") require.NoError(t, err) require.NotNil(t, sess, "session nil") require.Len(t, msgs, 1) @@ -101,7 +143,7 @@ func TestParseWorkBuddySessionUsesCwdProjectAndFileSessionID(t *testing.T) { `, cwd) require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, _, err := ParseWorkBuddySession(path, "stored-project", "local") + sess, _, err := parseWorkBuddyTestSession(t, path, "stored-project", "local") require.NoError(t, err) assert.Equal(t, "workbuddy:22222222-2222-4222-8222-222222222222", sess.ID) assert.Equal(t, "cwd_project", sess.Project) @@ -114,7 +156,7 @@ func TestParseWorkBuddySessionNormalizesWindowsCwdProject(t *testing.T) { ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, _, err := ParseWorkBuddySession(path, "stored", "local") + sess, _, err := parseWorkBuddyTestSession(t, path, "stored", "local") require.NoError(t, err) assert.Equal(t, "report_builder", sess.Project) } @@ -126,7 +168,7 @@ func TestParseWorkBuddySessionFallsBackToDiscoveredProjectWhenCwdHasNoProject(t ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, _, err := ParseWorkBuddySession(path, "discovered-proj", "local") + sess, _, err := parseWorkBuddyTestSession(t, path, "discovered-proj", "local") require.NoError(t, err) assert.Equal(t, "discovered-proj", sess.Project) } @@ -138,7 +180,7 @@ func TestParseWorkBuddySessionOmitsAbsentTokenUsageKeys(t *testing.T) { ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - _, msgs, err := ParseWorkBuddySession(path, "proj", "local") + _, msgs, err := parseWorkBuddyTestSession(t, path, "proj", "local") require.NoError(t, err) require.Len(t, msgs, 1) m := msgs[0] @@ -160,7 +202,7 @@ func TestParseWorkBuddySubagentSession(t *testing.T) { ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, _, err := ParseWorkBuddySession(path, "proj", "local") + sess, _, err := parseWorkBuddyTestSession(t, path, "proj", "local") require.NoError(t, err) assert.Equal(t, "workbuddy:11111111-1111-4111-8111-111111111111:subagent:agent-123", sess.ID) assert.Equal(t, "workbuddy:11111111-1111-4111-8111-111111111111", sess.ParentSessionID) @@ -172,7 +214,7 @@ func TestFindWorkBuddySourceFile(t *testing.T) { path := filepath.Join(root, "proj", "11111111-1111-4111-8111-111111111111.jsonl") require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644)) - got := FindWorkBuddySourceFile(root, "workbuddy:11111111-1111-4111-8111-111111111111") + got := findWorkBuddyTestSourceFile(t, root, "workbuddy:11111111-1111-4111-8111-111111111111") assert.Equal(t, path, got) } @@ -182,7 +224,7 @@ func TestFindWorkBuddySourceFileRejectsInvalidSubagentID(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644)) - got := FindWorkBuddySourceFile(root, "workbuddy:11111111-1111-4111-8111-111111111111:subagent:../agent-123") + got := findWorkBuddyTestSourceFile(t, root, "workbuddy:11111111-1111-4111-8111-111111111111:subagent:../agent-123") assert.Empty(t, got, "want empty path") } @@ -193,7 +235,7 @@ func TestParseWorkBuddyProjectNamedSubagentsIsNotSubagent(t *testing.T) { ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - sess, _, err := ParseWorkBuddySession(path, "subagents", "local") + sess, _, err := parseWorkBuddyTestSession(t, path, "subagents", "local") require.NoError(t, err) assert.Equal(t, "workbuddy:11111111-1111-4111-8111-111111111111", sess.ID) assert.Empty(t, sess.ParentSessionID) @@ -207,7 +249,7 @@ func TestParseWorkBuddySessionDecodesObjectToolResultText(t *testing.T) { ` require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) - _, msgs, err := ParseWorkBuddySession(path, "proj", "local") + _, msgs, err := parseWorkBuddyTestSession(t, path, "proj", "local") require.NoError(t, err) require.Len(t, msgs, 1) require.Len(t, msgs[0].ToolResults, 1) diff --git a/internal/sync/engine.go b/internal/sync/engine.go index 27dfce404..e6f4830bb 100644 --- a/internal/sync/engine.go +++ b/internal/sync/engine.go @@ -1260,38 +1260,6 @@ func (e *Engine) classifyOnePath( } } - // WorkBuddy: //.jsonl - // or: ///subagents/*.jsonl - for _, workBuddyDir := range e.agentDirs[parser.AgentWorkBuddy] { - if workBuddyDir == "" { - continue - } - if rel, ok := isUnder(workBuddyDir, path); ok { - if !strings.HasSuffix(path, ".jsonl") { - continue - } - parts := strings.Split(rel, sep) - if len(parts) == 2 { - stem := strings.TrimSuffix(parts[1], ".jsonl") - if !parser.IsValidSessionID(stem) { - continue - } - return parser.DiscoveredFile{ - Path: path, - Project: parts[0], - Agent: parser.AgentWorkBuddy, - }, true - } - if len(parts) == 4 && parts[2] == "subagents" { - return parser.DiscoveredFile{ - Path: path, - Project: parts[0], - Agent: parser.AgentWorkBuddy, - }, true - } - } - } - // VSCode Copilot: /workspaceStorage//chatSessions/.{json,jsonl} // or: /globalStorage/emptyWindowChatSessions/.{json,jsonl} for _, vscDir := range e.agentDirs[parser.AgentVSCodeCopilot] { @@ -4496,8 +4464,6 @@ func (e *Engine) processFile( res = e.processCortex(file, info) case parser.AgentHermes: res = e.processHermes(file, info) - case parser.AgentWorkBuddy: - res = e.processWorkBuddy(file, info) case parser.AgentVibe: res = e.processVibe(file, info) case parser.AgentPositron: @@ -6607,35 +6573,6 @@ func (e *Engine) processHermes( } } -func (e *Engine) processWorkBuddy( - file parser.DiscoveredFile, info os.FileInfo, -) processResult { - if e.shouldSkipByPath(file.Path, info) { - return processResult{skip: true} - } - - sess, msgs, err := parser.ParseWorkBuddySession( - file.Path, file.Project, e.machine, - ) - if err != nil { - return processResult{err: err} - } - if sess == nil { - return processResult{} - } - - hash, err := ComputeFileHash(file.Path) - if err == nil { - sess.File.Hash = hash - } - - return processResult{ - results: []parser.ParseResult{ - {Session: *sess, Messages: msgs}, - }, - } -} - func (e *Engine) processVibe( file parser.DiscoveredFile, info os.FileInfo, ) processResult { @@ -9467,18 +9404,6 @@ func (e *Engine) SyncSingleSessionContext( file.Project = sess.Project } } - case parser.AgentWorkBuddy: - for _, workBuddyDir := range e.agentDirs[parser.AgentWorkBuddy] { - rel, ok := isUnder(workBuddyDir, path) - if !ok { - continue - } - parts := strings.Split(rel, string(filepath.Separator)) - if len(parts) == 2 || len(parts) == 4 && parts[2] == "subagents" { - file.Project = parts[0] - break - } - } } res := e.processFile(ctx, file) diff --git a/internal/sync/workbuddy_test.go b/internal/sync/workbuddy_test.go index 2b5ac9c79..18f0e312d 100644 --- a/internal/sync/workbuddy_test.go +++ b/internal/sync/workbuddy_test.go @@ -34,20 +34,22 @@ func TestEngineClassifyWorkBuddyPaths(t *testing.T) { require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644)) } - got, ok := engine.classifyOnePath(mainPath, nil) - require.True(t, ok, "main path did not classify") + files := engine.classifyPaths([]string{mainPath}) + require.Len(t, files, 1, "main path did not classify") + got := files[0] assert.Equal(t, mainPath, got.Path) assert.Equal(t, "proj", got.Project) assert.Equal(t, parser.AgentWorkBuddy, got.Agent) - got, ok = engine.classifyOnePath(subPath, nil) - require.True(t, ok, "subagent path did not classify") + files = engine.classifyPaths([]string{subPath}) + require.Len(t, files, 1, "subagent path did not classify") + got = files[0] assert.Equal(t, subPath, got.Path) assert.Equal(t, "proj", got.Project) assert.Equal(t, parser.AgentWorkBuddy, got.Agent) - got, ok = engine.classifyOnePath(toolPath, nil) - assert.False(t, ok, "tool result classified as %+v", got) + files = engine.classifyPaths([]string{toolPath}) + assert.Empty(t, files, "tool result classified as %+v", files) } func TestEngineClassifyWorkBuddyProjectNamedSubagentsAsMainSession(t *testing.T) { @@ -64,8 +66,9 @@ func TestEngineClassifyWorkBuddyProjectNamedSubagentsAsMainSession(t *testing.T) require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) require.NoError(t, os.WriteFile(path, []byte("{}\n"), 0o644)) - got, ok := engine.classifyOnePath(path, nil) - require.True(t, ok, "path did not classify") + files := engine.classifyPaths([]string{path}) + require.Len(t, files, 1, "path did not classify") + got := files[0] assert.Equal(t, path, got.Path) assert.Equal(t, "subagents", got.Project) assert.Equal(t, parser.AgentWorkBuddy, got.Agent)