diff --git a/server/convo.go b/server/convo.go index 73444de7fe5b802c64b4c6543df575df7ad9383f..68cfebe20b387c039673a9ad4dc30581e70a97d7 100644 --- a/server/convo.go +++ b/server/convo.go @@ -124,6 +124,14 @@ func (cm *ConversationManager) Hydrate(ctx context.Context) error { return fmt.Errorf("failed to get conversation history: %w", err) } + // Load cwd from conversation if available - must happen before generating system prompt + // so that the system prompt includes guidance files from the context directory + cwd := "" + if conversation.Cwd != nil { + cwd = *conversation.Cwd + } + cm.cwd = cwd + // Generate system prompt if missing: // - For user-initiated conversations: full system prompt // - For subagent conversations (has parent): minimal subagent prompt @@ -147,19 +155,12 @@ func (cm *ConversationManager) Hydrate(ctx context.Context) error { history, system := cm.partitionMessages(messages) - // Load cwd from conversation if available - cwd := "" - if conversation.Cwd != nil { - cwd = *conversation.Cwd - } - cm.mu.Lock() cm.history = history cm.system = system cm.hasConversationEvents = len(history) > 0 cm.lastActivity = time.Now() cm.hydrated = true - cm.cwd = cwd cm.mu.Unlock() cm.logSystemPromptState(system, len(messages)) diff --git a/server/cwd_test.go b/server/cwd_test.go index 45f7d82809b5eeb594848b7b21b22eab2e538e56..4bcc1e3bab35f0423fa96621b259c3a482b3aac1 100644 --- a/server/cwd_test.go +++ b/server/cwd_test.go @@ -1,6 +1,7 @@ package server import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -8,6 +9,9 @@ import ( "path/filepath" "strings" "testing" + + "shelley.exe.dev/db/generated" + "shelley.exe.dev/llm" ) // TestWorkingDirectoryConfiguration tests that the working directory (cwd) setting @@ -294,3 +298,78 @@ func TestConversationCwdReturnedInList(t *testing.T) { t.Error("conversation not found in list") } } + +// TestSystemPromptUsesCwdFromConversation verifies that when a conversation +// is created with a specific cwd, the system prompt is generated using that +// directory (not the server's working directory). This tests the fix for +// https://github.com/boldsoftware/shelley/issues/30 +func TestSystemPromptUsesCwdFromConversation(t *testing.T) { + // Create a temp directory with an AGENTS.md file + tmpDir, err := os.MkdirTemp("", "shelley_cwd_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create an AGENTS.md file with unique content we can search for + agentsContent := "UNIQUE_MARKER_FOR_CWD_TEST_XYZ123: This is test guidance." + agentsFile := filepath.Join(tmpDir, "AGENTS.md") + if err := os.WriteFile(agentsFile, []byte(agentsContent), 0o644); err != nil { + t.Fatalf("failed to write AGENTS.md: %v", err) + } + + h := NewTestHarness(t) + defer h.Close() + + // Create a conversation with the temp directory as cwd + h.NewConversation("bash: echo hello", tmpDir) + h.WaitToolResult() + + // Get the system prompt from the database + var messages []generated.Message + err = h.db.Queries(context.Background(), func(q *generated.Queries) error { + var qerr error + messages, qerr = q.ListMessages(context.Background(), h.ConversationID()) + return qerr + }) + if err != nil { + t.Fatalf("failed to get messages: %v", err) + } + + // Find the system message + var systemPrompt string + for _, msg := range messages { + if msg.Type == "system" && msg.LlmData != nil { + var llmMsg llm.Message + if err := json.Unmarshal([]byte(*msg.LlmData), &llmMsg); err == nil { + for _, content := range llmMsg.Content { + if content.Type == llm.ContentTypeText { + systemPrompt = content.Text + break + } + } + } + break + } + } + + if systemPrompt == "" { + t.Fatal("no system prompt found in messages") + } + + // Verify the system prompt contains our unique marker from AGENTS.md + if !strings.Contains(systemPrompt, "UNIQUE_MARKER_FOR_CWD_TEST_XYZ123") { + t.Errorf("system prompt should contain content from AGENTS.md in the cwd directory") + // Log first 1000 chars to help debug + if len(systemPrompt) > 1000 { + t.Logf("system prompt (first 1000 chars): %s...", systemPrompt[:1000]) + } else { + t.Logf("system prompt: %s", systemPrompt) + } + } + + // Verify the working directory in the prompt is our temp directory + if !strings.Contains(systemPrompt, tmpDir) { + t.Errorf("system prompt should reference the cwd directory: %s", tmpDir) + } +} diff --git a/server/system_prompt_test.go b/server/system_prompt_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2bc0c8643bfee3ff8235bf2e1fd8e1f67638a778 --- /dev/null +++ b/server/system_prompt_test.go @@ -0,0 +1,72 @@ +package server + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestSystemPromptIncludesCwdGuidanceFiles verifies that AGENTS.md from the working directory +// is included in the generated system prompt. +func TestSystemPromptIncludesCwdGuidanceFiles(t *testing.T) { + // Create a temp directory to serve as our "context directory" + tmpDir, err := os.MkdirTemp("", "shelley_test") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create an AGENTS.md file in the temp directory + agentsContent := "TEST_UNIQUE_CONTENT_12345: Always use Go for everything." + agentsFile := filepath.Join(tmpDir, "AGENTS.md") + if err := os.WriteFile(agentsFile, []byte(agentsContent), 0o644); err != nil { + t.Fatalf("failed to write AGENTS.md: %v", err) + } + + // Generate system prompt for this directory + prompt, err := GenerateSystemPrompt(tmpDir) + if err != nil { + t.Fatalf("GenerateSystemPrompt failed: %v", err) + } + + // Verify the unique content from AGENTS.md is included in the prompt + if !strings.Contains(prompt, "TEST_UNIQUE_CONTENT_12345") { + t.Errorf("system prompt should contain content from AGENTS.md in the working directory") + t.Logf("AGENTS.md content: %s", agentsContent) + t.Logf("Generated prompt (first 2000 chars): %s", prompt[:min(len(prompt), 2000)]) + } + + // Verify the file path is mentioned in guidance section + if !strings.Contains(prompt, agentsFile) { + t.Errorf("system prompt should reference the AGENTS.md file path") + } +} + +// TestSystemPromptEmptyCwdFallsBackToCurrentDir verifies that an empty workingDir +// causes GenerateSystemPrompt to use the current directory. +func TestSystemPromptEmptyCwdFallsBackToCurrentDir(t *testing.T) { + // Get current directory for comparison + currentDir, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get current directory: %v", err) + } + + // Generate system prompt with empty workingDir + prompt, err := GenerateSystemPrompt("") + if err != nil { + t.Fatalf("GenerateSystemPrompt failed: %v", err) + } + + // Verify the current directory is mentioned in the prompt + if !strings.Contains(prompt, currentDir) { + t.Errorf("system prompt should contain current directory when cwd is empty") + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +}