From 9e060f869ef2e8a55871633746c3cb16c0c2db54 Mon Sep 17 00:00:00 2001 From: Philip Zeyliger Date: Tue, 20 Jan 2026 04:49:49 +0000 Subject: [PATCH] shelley: read AGENTS.md from context directory at conversation start Prompt: fix https://github.com/boldsoftware/shelley/issues/30 Fix issue where the system prompt was generated before loading the cwd from the conversation record, causing it to use the server's working directory instead of the user's specified context directory. The cwd is now loaded from the conversation before generating the system prompt, ensuring that AGENTS.md and other guidance files from the context directory are properly included in the system prompt. Fixes https://github.com/boldsoftware/shelley/issues/30 Co-authored-by: Shelley --- server/convo.go | 15 +++---- server/cwd_test.go | 79 ++++++++++++++++++++++++++++++++++++ server/system_prompt_test.go | 72 ++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 server/system_prompt_test.go 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 +}