shareAI-lab/learn-claude-code · error · ValueError

todos[{index}] requires content

Error message

todos[{index}] requires content

What it means

Raised by TodoManager.update in s05_todo_write/code.py when a todo object's content is missing, empty, or whitespace-only after strip(). The index in the message refers to the element's position in the submitted list, letting callers pinpoint the offender. The batch is rejected atomically, so previous todos survive.

Source

Thrown at s05_todo_write/code.py:138

                    todos = ast.literal_eval(todos)
                except (SyntaxError, ValueError) as e:
                    raise ValueError("todos must be a list or JSON array string") from e

        if not isinstance(todos, list):
            raise ValueError("todos must be a list")
        if len(todos) > 20:
            raise ValueError("Max 20 todos allowed")

        validated = []
        in_progress_count = 0
        for index, todo in enumerate(todos):
            if not isinstance(todo, dict):
                raise ValueError(f"todos[{index}] must be an object")

            content = str(todo.get("content", "")).strip()
            status = str(todo.get("status", "pending")).lower()
            if not content:
                raise ValueError(f"todos[{index}] requires content")
            if status not in ("pending", "in_progress", "completed"):
                raise ValueError(f"todos[{index}] has invalid status '{status}'")
            if status == "in_progress":
                in_progress_count += 1
            validated.append({"content": content, "status": status})

        if in_progress_count > 1:
            raise ValueError("Only one todo can be in_progress at a time")

        self.items = validated
        return self.render()

    def render(self) -> str:
        if not self.items:
            return "No todos."

        lines = []
        for todo in self.items:

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Give every item a descriptive non-empty content string
  2. Filter empty items before the call: [t for t in todos if str(t.get('content', '')).strip()]
  3. Use the reported index to fix just the offending element and resubmit the whole list

Example fix

// before
TODOS.update([{"status": "pending"}])
// after
TODOS.update([{"content": "write tests", "status": "pending"}])
Defensive patterns

Strategy: validation

Validate before calling

todos = [t for t in todos if str(t.get('content', '')).strip()]
assert all(str(t.get('content', '')).strip() for t in todos)
TODOS.update(todos)

Type guard

def has_content(todo: object) -> bool:
    return isinstance(todo, dict) and bool(str(todo.get('content', '')).strip())

Try / catch

try:
    TODOS.update(todos)
except ValueError as e:
    m = re.search(r"todos\[(\d+)\] requires content", str(e))
    if m:
        i = int(m.group(1))
        todos[i]['content'] = f"(step {i})"
        TODOS.update(todos)
    else:
        raise

Prevention

When it happens

Trigger: An element like {"status": "pending"} with no content key, {"content": ""}, or {"content": " "}. Note the module does not require activeForm (unlike s_full), only content and a valid status.

Common situations: LLM emitting a status-only item as a separator or header; whitespace content from trimmed plan text; optional-field confusion where the caller assumes content is optional.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/a22db88d031196fb. Report an issue: GitHub.