{"record":{"id":"f39f0be239260e41","repo":"charmbracelet/crush","slug":"invalid-status-q-for-todo-q","errorCode":null,"errorMessage":"invalid status %q for todo %q","messagePattern":"invalid status %q for todo %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/todos.go","lineNumber":61,"sourceCode":"\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"session ID is required for managing todos\")\n\t\t\t}\n\n\t\t\tcurrentSession, err := sessions.Get(ctx, sessionID)\n\t\t\tif err != nil {\n\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"failed to get session: %w\", err)\n\t\t\t}\n\n\t\t\tisNew := len(currentSession.Todos) == 0\n\t\t\toldStatusByContent := make(map[string]session.TodoStatus)\n\t\t\tfor _, todo := range currentSession.Todos {\n\t\t\t\toldStatusByContent[todo.Content] = todo.Status\n\t\t\t}\n\n\t\t\tfor _, item := range params.Todos {\n\t\t\t\tswitch item.Status {\n\t\t\t\tcase \"pending\", \"in_progress\", \"completed\":\n\t\t\t\tdefault:\n\t\t\t\t\treturn fantasy.ToolResponse{}, fmt.Errorf(\"invalid status %q for todo %q\", item.Status, item.Content)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ttodos := make([]session.Todo, len(params.Todos))\n\t\t\tvar justCompleted []string\n\t\t\tvar justStarted string\n\t\t\tcompletedCount := 0\n\n\t\t\tfor i, item := range params.Todos {\n\t\t\t\ttodos[i] = session.Todo{\n\t\t\t\t\tContent:    item.Content,\n\t\t\t\t\tStatus:     session.TodoStatus(item.Status),\n\t\t\t\t\tActiveForm: item.ActiveForm,\n\t\t\t\t}\n\n\t\t\t\tnewStatus := session.TodoStatus(item.Status)\n\t\t\t\toldStatus, existed := oldStatusByContent[item.Content]\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/todos.go#L43-L79","documentation":"Each todo item's status must be exactly \"pending\", \"in_progress\", or \"completed\". The model (or caller) supplied some other string, so the tool rejects the whole batch before writing. This is a strict allow-list validation, not a type check — any casing variation or synonym fails.","triggerScenarios":"The LLM emits a status like \"done\", \"Done\", \"in-progress\", \"complete\", or \"In Progress\" instead of the exact lowercase snake_case tokens pending/in_progress/completed in the TodosParams.Todos payload.","commonSituations":"LLM hallucinating non-conforming status values; a client/library version using different status vocabularies; hand-crafted tool calls in tests with wrong enum strings.","solutions":["Use only the exact strings \"pending\", \"in_progress\", \"completed\" in the status field","Check the tool description/JSON schema the model was given matches these values","Pre-normalize statuses (trim, lowercase, map synonyms) before submitting the tool call"],"exampleFix":"// before\n{\"content\":\"Run tests\",\"status\":\"done\"}\n// after\n{\"content\":\"Run tests\",\"status\":\"completed\"}","handlingStrategy":"validation","validationCode":"valid := map[string]bool{\"pending\":true, \"in_progress\":true, \"completed\":true}\nfor _, t := range todos {\n    if !valid[t.Status] {\n        return fmt.Errorf(\"status %q invalid for todo %q\", t.Status, t.Content)\n    }\n}","typeGuard":"func isValidTodoStatus(s string) bool {\n    return s == \"pending\" || s == \"in_progress\" || s == \"completed\"\n}","tryCatchPattern":"if err != nil && strings.HasPrefix(err.Error(), \"invalid status\") {\n    // normalize statuses (lowercase, map synonyms) and resubmit\n}","preventionTips":["Use only the exact lowercase snake_case statuses the tool schema documents","Lowercase/trim model output before building the todos payload","Add a schema-level enum constraint so invalid statuses are rejected earlier"],"tags":["validation","enum","todo","llm-tool-call"],"backgroundTag":"invalid-enum-value","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}