From 4b38d2b8ee6841467b2476f95bf618359fce2bda Mon Sep 17 00:00:00 2001 From: Koopa Date: Thu, 2 Jul 2026 10:50:12 +0800 Subject: [PATCH] fix(mcp): allow multi-line description in capture_inbox capture_inbox validated description with the strict single-line check (goal.ContainsControlChars), so any description containing a newline was rejected with "description must not contain control characters". The hermes proposal-loop pushes descriptions formatted as multi-line Markdown (blank-line paragraphs, bullet lists) per the capture format contract, so every surfaced proposal push has failed since this validation went live (21 items stuck retrying). Description is multi-line Markdown prose, the same shape as content body and review_note, which are already validated with containsProseControlChars (rejects all control chars except HT/LF/CR). Apply the same predicate to capture_inbox description. Title stays on the strict single-line check. Adds a regression test that a multi-line bulleted description passes validateCaptureInput. --- internal/mcp/capture.go | 2 +- internal/mcp/handler_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/mcp/capture.go b/internal/mcp/capture.go index 3ceefdf0d..b0e6a3060 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 52cbfe578..971117d65 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) {