Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 ""
}
Expand All @@ -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()
Expand Down Expand Up @@ -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 != "" {
Expand Down
25 changes: 20 additions & 5 deletions internal/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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()

Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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")
}
})
}

Expand Down