diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 3d6c4487..c516b22f 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -915,7 +915,7 @@ func (e *Executor) executeTask(ctx context.Context, task *db.Task) { // This is important when attachments are added after the initial run or when resuming feedbackWithAttachments := retryFeedback if len(attachmentPaths) > 0 { - feedbackWithAttachments = retryFeedback + "\n" + e.getAttachmentsSection(task.ID, attachmentPaths) + feedbackWithAttachments = retryFeedback + "\n" + e.getAttachmentsSection(task.ID, attachmentPaths, workDir) } e.logLine(task.ID, "system", fmt.Sprintf("Resuming previous session with feedback (executor: %s)", executorName)) execResult := taskExecutor.Resume(taskCtx, task, workDir, prompt, feedbackWithAttachments) @@ -1094,7 +1094,9 @@ func (e *Executor) prepareAttachments(taskID int64, worktreePath string) ([]stri } // getAttachmentsSection returns a prompt section describing attachments. -func (e *Executor) getAttachmentsSection(taskID int64, paths []string) string { +// The worktreePath parameter is used to convert absolute paths to relative paths +// so they match the permission pattern Read(.claude/attachments/**). +func (e *Executor) getAttachmentsSection(taskID int64, paths []string, worktreePath string) string { if len(paths) == 0 { return "" } @@ -1103,7 +1105,13 @@ func (e *Executor) getAttachmentsSection(taskID int64, paths []string) string { section.WriteString("\n## Attachments\n\n") section.WriteString("The following files are attached to this task:\n") for _, p := range paths { - section.WriteString(fmt.Sprintf("- %s\n", p)) + // Convert absolute paths to relative paths so they match permission patterns + relPath := p + if worktreePath != "" && strings.HasPrefix(p, worktreePath) { + relPath = strings.TrimPrefix(p, worktreePath) + relPath = strings.TrimPrefix(relPath, string(filepath.Separator)) + } + section.WriteString(fmt.Sprintf("- %s\n", relPath)) } section.WriteString("\nYou can read these files using the Read tool.\n\n") return section.String() @@ -1134,8 +1142,8 @@ func (e *Executor) buildPrompt(task *db.Task, attachmentPaths []string) string { // Check for conversation history (from previous runs/retries) conversationHistory := e.getConversationHistory(task.ID) - // Get attachments section - attachments := e.getAttachmentsSection(task.ID, attachmentPaths) + // Get attachments section (use relative paths to match permission patterns) + attachments := e.getAttachmentsSection(task.ID, attachmentPaths, task.WorktreePath) // Look up task type instructions from database if task.Type != "" { diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 80d2a2a0..5a500f30 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -265,9 +265,10 @@ func TestAttachmentsInPrompt(t *testing.T) { } }) - t.Run("getAttachmentsSection uses Read tool", func(t *testing.T) { + t.Run("getAttachmentsSection uses Read tool with relative paths", func(t *testing.T) { + worktreePath := "/worktree" paths := []string{"/worktree/.claude/attachments/notes.txt", "/worktree/.claude/attachments/data.json"} - section := exec.getAttachmentsSection(task.ID, paths) + section := exec.getAttachmentsSection(task.ID, paths, worktreePath) if !strings.Contains(section, "## Attachments") { t.Error("section should contain Attachments header") @@ -278,13 +279,19 @@ func TestAttachmentsInPrompt(t *testing.T) { if strings.Contains(section, "View tool") { t.Error("section should NOT mention View tool") } - if !strings.Contains(section, "/worktree/.claude/attachments/notes.txt") { - t.Error("section should contain file path") + // Paths should be relative, not absolute + if !strings.Contains(section, ".claude/attachments/notes.txt") { + t.Error("section should contain relative file path") + } + if strings.Contains(section, "/worktree/.claude/attachments/notes.txt") { + t.Error("section should NOT contain absolute file path") } }) t.Run("buildPrompt includes attachments section", func(t *testing.T) { worktreePath := t.TempDir() + // Set task's WorktreePath so buildPrompt can convert to relative paths + task.WorktreePath = worktreePath paths, cleanup := exec.prepareAttachments(task.ID, worktreePath) defer cleanup() @@ -296,6 +303,10 @@ func TestAttachmentsInPrompt(t *testing.T) { if !strings.Contains(prompt, "Read tool") { t.Error("prompt should mention Read tool") } + // Verify paths are relative in the prompt + if !strings.Contains(prompt, ".claude/attachments/") { + t.Error("prompt should contain relative attachment paths") + } }) t.Run("retry feedback includes attachments when present", func(t *testing.T) { @@ -307,7 +318,7 @@ func TestAttachmentsInPrompt(t *testing.T) { retryFeedback := "Please fix the bug" feedbackWithAttachments := retryFeedback if len(paths) > 0 { - feedbackWithAttachments = retryFeedback + "\n" + exec.getAttachmentsSection(task.ID, paths) + feedbackWithAttachments = retryFeedback + "\n" + exec.getAttachmentsSection(task.ID, paths, worktreePath) } // Verify attachments are included in the retry feedback @@ -320,6 +331,10 @@ func TestAttachmentsInPrompt(t *testing.T) { if !strings.Contains(feedbackWithAttachments, "Please fix the bug") { t.Error("retry feedback should still contain original feedback") } + // Verify paths are relative + if !strings.Contains(feedbackWithAttachments, ".claude/attachments/") { + t.Error("retry feedback should contain relative attachment paths") + } }) }