bug: `tasks.json` schema corruption — `id` number→string + `parentId: "undefined"` injected on every `set-status` write
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:
- Top-level
task.idis converted fromnumbertostringfor every task in the active tag. - Every existing subtask is mutated: a new field
"parentId": "undefined"(literal string) is appended, alongside the still-presentparentTaskId.
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.jsonwith the{ "master": { "tasks": [...] } }tagged wrapper
Minimal repro
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.jsonObserved output (after set-status --id=1)
{
"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
{
"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(seescripts/modules/task-manager/add-subtask.js:95:parentTaskId: parentIdNum). packages/tm-core/definesTask.id: string(task.entity.ts:18) and uses// 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-statusmutates the file even when the user only intended a single-field status change. - Multi-agent automations (where several sessions call
task-masterin parallel) accelerate the entropy. tasks.jsonbecomes hard to read and tooling fragile: downstream consumers (linters, custom scripts) trip on the duplicateparentTaskId/parentId: "undefined"and on the string/number id polymorphism.- The corruption is not detected by
validate-dependenciesor any current CI step.
Suggested fix direction
Pick one canonical schema and migrate explicitly:
- If string ids is the target,
add-subtask/update-taskneed to be moved to the same path (so the file is consistently stringified), and a one-shot migration step should rewriteparentTaskId → parentIdon read. - If numeric task ids +
parentTaskIdis the target,tm-coreformat-handler should drop theString(task.id)/String(subtask.parentId)casts and readparentTaskIdinstead.
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