diff --git a/internal/mcp/capture.go b/internal/mcp/capture.go index 3ceefdf0..b0e6a306 100644 --- a/internal/mcp/capture.go +++ b/internal/mcp/capture.go @@ -103,7 +103,7 @@ func validateCaptureInput(input *CaptureInboxInput) (due *time.Time, rec todo.Re if goal.ContainsControlChars(input.Title) { return nil, rec, false, fmt.Errorf("title must not contain control characters") } - if goal.ContainsControlChars(input.Description) { + if containsProseControlChars(input.Description) { return nil, rec, false, fmt.Errorf("description must not contain control characters") } if input.Energy != nil && *input.Energy != "" && !isValidEnergy(*input.Energy) { diff --git a/internal/mcp/handler_test.go b/internal/mcp/handler_test.go index 52cbfe57..971117d6 100644 --- a/internal/mcp/handler_test.go +++ b/internal/mcp/handler_test.go @@ -56,6 +56,15 @@ func TestCaptureInbox_Validation(t *testing.T) { } } +func TestCaptureInbox_MultilineDescription(t *testing.T) { + // Description is multi-line Markdown prose (rendered as paragraphs and + // bullets), so LF/CR/HT are legitimate — same treatment as content body. + input := CaptureInboxInput{Title: "ok", Description: "- noticed\n\n- proposal\n\n- value"} + if _, _, _, err := validateCaptureInput(&input); err != nil { + t.Fatalf("multi-line description rejected: %v", err) + } +} + // --- plan_day --- func TestPlanDay_Validation(t *testing.T) {