bug: `tasks.json` schema corruption — `id` number→string + `parentId: "undefined"` injected on every `set-status` write

Author: alchimie-di-circeCreated Jun 7, 2026Updated Aug 2, 2026
Labelsbughigh-priorityarea:task-managementarea:cli

Summary

Calling task-master set-status --id=<n> --status=<s> against a tasks.json file with numeric task ids and subtasks using the canonical legacy parentTaskId: <number> field corrupts the file:

  1. Top-level task.id is converted from number to string for every task in the active tag.
  2. Every existing subtask is mutated: a new field "parentId": "undefined" (literal string) is appended, alongside the still-present parentTaskId.

A subsequent task-master add-subtask partially "heals" the file (restores numeric task.id) but leaves the "parentId": "undefined" strings permanently on the legacy subtasks, and creates new subtasks with parentTaskId only — yielding a mixed schema that drifts on every write.

Reproduces 100% on [email protected] (CLI) and the corresponding MCP path; same path on 0.43.x.

Environment

  • Version: [email protected] (installed; reproduced on 0.43.x)
  • Node: any recent
  • OS: macOS 14 (Darwin 24.6, Apple Silicon)
  • File entry point: legacy-format tasks.json with the { "master": { "tasks": [...] } } tagged wrapper

Minimal repro

bash
REPRO=$(mktemp -d)
mkdir -p "$REPRO/.taskmaster/tasks"
cat > "$REPRO/.taskmaster/tasks/tasks.json" <<'JSON'
{
  "master": {
    "tasks": [
      {
        "id": 1,
        "title": "T1",
        "description": "d",
        "status": "pending",
        "dependencies": [],
        "priority": "medium",
        "details": "",
        "testStrategy": "",
        "subtasks": []
      },
      {
        "id": 2,
        "title": "T2",
        "description": "d",
        "status": "pending",
        "dependencies": [],
        "priority": "medium",
        "details": "",
        "testStrategy": "",
        "subtasks": [
          { "id": 1, "title": "S2.1", "description": "", "status": "pending",
            "dependencies": [], "details": "", "parentTaskId": 2 }
        ]
      }
    ],
    "metadata": {}
  }
}
JSON

cd "$REPRO"
task-master set-status --id=1 --status=done
cat .taskmaster/tasks/tasks.json

Observed output (after set-status --id=1)

json
{
  "master": {
    "tasks": [
      { "id": "1", "title": "T1", "status": "done", ... },   // <-- id is now a STRING
      { "id": "2", "title": "T2", "status": "pending", ...,
        "subtasks": [
          {
            "id": 1,
            "title": "S2.1",
            "status": "pending",
            "parentTaskId": 2,
            "parentId": "undefined"          // <-- INJECTED literal "undefined"
          }
        ]
      }
    ]
  }
}

Expected output

json
{
  "master": {
    "tasks": [
      { "id": 1, "title": "T1", "status": "done", ... },     // numeric
      { "id": 2, "title": "T2", "status": "pending", ...,
        "subtasks": [
          { "id": 1, "title": "S2.1", "status": "pending", "parentTaskId": 2 }
        ]
      }
    ]
  }
}

Suspected root cause

There is a schema mismatch between the legacy CLI path (scripts/modules/task-manager/) and the newer packages/tm-core/ TS layer that some commands now route through:

  • Legacy path uses task.id: number + subtask.parentTaskId: number (see scripts/modules/task-manager/add-subtask.js:95: parentTaskId: parentIdNum).
  • packages/tm-core/ defines Task.id: string (task.entity.ts:18) and uses
    typescript
    // packages/tm-core/src/modules/storage/adapters/file-storage/format-handler.ts:225-231
    id: String(task.id),
    ...
    id: Number(subtask.id),
    parentId: String(subtask.parentId)   // ← String(undefined) === "undefined"

When set-task-status serializes through the tm-core format handler with a legacy file that has parentTaskId (not parentId), subtask.parentId is undefined, and the cast produces the literal string "undefined". Top-level ids are forced to strings by the same path.

add-subtask still goes through the legacy path → restores numeric task.id on next invocation but the injected parentId: "undefined" is not cleaned.

Impact

  • Silent drift on every write: each set-status mutates the file even when the user only intended a single-field status change.
  • Multi-agent automations (where several sessions call task-master in parallel) accelerate the entropy.
  • tasks.json becomes hard to read and tooling fragile: downstream consumers (linters, custom scripts) trip on the duplicate parentTaskId / parentId: "undefined" and on the string/number id polymorphism.
  • The corruption is not detected by validate-dependencies or any current CI step.

Suggested fix direction

Pick one canonical schema and migrate explicitly:

  • If string ids is the target, add-subtask/update-task need to be moved to the same path (so the file is consistently stringified), and a one-shot migration step should rewrite parentTaskId → parentId on read.
  • If numeric task ids + parentTaskId is the target, tm-core format-handler should drop the String(task.id) / String(subtask.parentId) casts and read parentTaskId instead.

Either way, never write String(undefined) as a literal — guard the field with if (parentId !== undefined).

Workaround for users

Until a fix lands, do not call task-master set-status (CLI or MCP) on files with numeric ids + legacy parentTaskId. Edit tasks.json manually using a JSON parser that preserves the canonical schema (numeric ids, no parentId on subtasks).


Happy to send a PR if the canonical schema decision is clarified.

Source: eyaltoledano/claude-task-master