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

Invalid task status: {task.status}

Error message

Invalid task status: {task.status}

What it means

Raised by TaskStore.load() in s10_task_system/code.py:143 when a task file's status field is anything other than 'pending', 'in_progress', or 'completed'. These three strings are the task lifecycle's closed vocabulary; the check runs after the ID-match check so the file is known to be well-located but its content is stale or hand-edited. Any other value — including old vocabulary like 'done', 'blocked', capitalized variants, or null — is rejected.

Source

Thrown at s10_task_system/code.py:143

                    json.dump(asdict(task), handle, indent=2)
                return task
            except FileExistsError:
                continue
        raise RuntimeError("Could not allocate a unique task ID")

    def save(self, task: Task) -> None:
        self._path(task.id, create_root=True).write_text(
            json.dumps(asdict(task), indent=2),
            encoding="utf-8",
        )

    def load(self, task_id: str) -> Task:
        data = json.loads(self._path(task_id).read_text(encoding="utf-8"))
        task = Task(**data)
        if task.id != task_id:
            raise ValueError(f"Task file ID does not match {task_id}")
        if task.status not in ("pending", "in_progress", "completed"):
            raise ValueError(f"Invalid task status: {task.status}")
        return task

    def list(self) -> list[Task]:
        if not self.directory.exists():
            return []
        root = self._root()
        return [self.load(path.stem)
                for path in sorted(root.glob("task_*.json"))]


TASKS = TaskStore(TASKS_DIR)


def create_task(subject: str, description: str = "",
                blockedBy: list[str] | None = None) -> Task:
    return TASKS.create(subject, description, blockedBy)

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Map legacy statuses onto the allowed set when loading: 'done'/'finished' -> 'completed', 'active'/'running' -> 'in_progress', anything else -> 'pending', then save(task) to persist the corrected value.
  2. Write status changes only through TaskStore.save() and the provided transition helpers so vocabulary stays closed.
  3. If a new status is genuinely needed, extend the tuple in load() and the creation code together, and migrate existing files.

Example fix

# before
data['status'] = 'done'

# after
STATUS_MAP = {'done': 'completed', 'finished': 'completed',
              'active': 'in_progress', 'running': 'in_progress'}
data['status'] = STATUS_MAP.get(data['status'], 'pending')
Defensive patterns

Strategy: fallback

Validate before calling

ALLOWED = {'pending', 'in_progress', 'completed'}

def status_is_loadable(status) -> bool:
    return status in ALLOWED

Type guard

def is_task_status(value) -> bool:
    return value in ('pending', 'in_progress', 'completed')

Try / catch

try:
    task = store.load(task_id)
except ValueError as e:
    if 'Invalid task status' in str(e):
        legacy = {'done': 'completed', 'active': 'in_progress'}
        task = patch_status(store, task_id, legacy)  # rewrite file, reload
    else:
        raise

Prevention

When it happens

Trigger: Hand-editing .tasks/task_xxx.json and setting status to 'done'; loading a store written by an older/newer version that used a different status vocabulary; external scripts writing tasks with status 'blocked'; a JSON null or missing field surfacing as None. Task(**data) would already fail on a missing key with TypeError, so this raise specifically hits present-but-wrong values.

Common situations: Schema drift between versions of the task system; users 'finishing' tasks with a text editor; integration code that assumed an open-ended status enum.

Related errors


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