feat(tasks): allow subtasks up to three levels deep and re-parenting (backend)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 15/100
Research direction
The issue asks for either multi-level subtask nesting or re-parenting of a subtask, and does not choose between them. Before any code, a maintainer needs to decide the model and settle the effects on journal planning, completion rollup and the UI, which the payload lists but does not resolve. Done would mean an agreed design in the thread; the MCP side of get_journal is tracked separately in nevridge/taskflow-assistant#4.
Written by the indexing model from the issue text.
Description
Groomed 2026-10-30, updated after splitting. Backend half. Part of #366.
Followed by #369 (the frontend half: context headers at three levels, eligible-parent selectors, history display, error messages), which is blocked by this issue. Runs after #368 (this story reuses its completed-parent rule), #396 (the same rule also rejects reopening a child under a Completed parent, checking the direct parent only; the ancestor walk is this story's, see criterion 13a) and #387 (which replaces nested journal rows with rows and context headers). Related: #354 (dead cycle error code cleanup; do it first). Also waits for #405 (an unblock path for a Blocked parent with open subtasks, which changes the same parent rules) and the monorepo move (#397), so this is the first API feature built with the merged pipeline and its MCP change ships in the same PR.
Original Description
Problem
Subtasks are limited to one level. A task under nevridge/taskflow-assistant#17 wasn't possible because nevridge/taskflow-assistant#17 was itself a subtask.
Proposal
Either allow more than one nesting level, or allow re-parenting a subtask. Consider the effect on journal planning (removing a task also removes its subtasks from that day), completion rollup, and the UI.
Context
Found while using Claude via the MCP on 2026-10-12/13. Split out of #241. MCP side (listing subtasks in get_journal) is tracked in nevridge/taskflow-assistant#4.
User Story
As a TaskFlow user, I want to nest subtasks up to three levels deep (task, subtask, sub-subtask) and move a task to a different parent or clear its parent, so that I can break work down as far as it really goes, and I'm not blocked when the task I want to add under is itself a subtask (as with nevridge/taskflow-assistant#17).
Decisions made during grooming:
- Maximum depth is 3 levels (root, child, grandchild). It is defined in one named constant so it can be raised later.
- Auto-complete-parent cascades up every level.
- Moving a task to a different parent (including clearing it) records a new
ParentChangedhistory event. - A task that already has children may become a child (its whole subtree moves with it), as long as the resulting subtree stays within depth 3.
- Removing a task from a journal day also removes all its linked descendants from that day.
ChildTaskCountstays direct children only.- No database migration: the schema already allows any depth.
- Re-parenting through clear-parent, PUT and PATCH keeps working.
- No separate cycle check or error code. At a maximum depth of 3 a cycle can never reach a cycle check: a task with a descendant has a subtree at least 2 levels tall, so moving it under one of its own descendants always pushes the subtree to depth 4 or more, and the depth rule rejects it first. A self-parent is rejected by the self rule. This is the same reason the old cycle check was unreachable and was deleted in #353.
Acceptance Criteria
- Grandchild create. Given a root task A with child B, when I create task C with B as parent, then C is saved as a grandchild (depth 3) and B's
ChildTaskCountis 1. - Depth limit. Given a grandchild C (depth 3), when I create or move a task under C, then the request is rejected with
TASK_PARENT_DEPTH_NOT_ALLOWEDand nothing changes. - Subtree move. Given a task X with its own children, when I set its parent to a task at depth 2, then the request is rejected because the subtree would reach depth 4. When the parent is at depth 1 or is a root, it succeeds and all of X's descendants move with it.
- Self and descendant parent. Given a task, when I set its parent to itself (PUT or PATCH), then it is rejected with
TASK_PARENT_SELF_NOT_ALLOWED. When I set its parent to any of its own descendants, then it is rejected withTASK_PARENT_DEPTH_NOT_ALLOWED. Nothing changes in either case. No new cycle error code is added. The maximum-depth constant carries a comment saying that raising it to 4 or more requires a cycle check and its own error code, because a cycle then becomes reachable. - Re-parent history. Given a task, when I change its parent to another valid task or clear it (PUT or PATCH), then the change is saved and one
ParentChangedhistory event is recorded with a change summary. - No spurious events. Given a task, when an update leaves the parent unchanged, then no
ParentChangedevent is recorded. - Cascade up. Given a grandchild that is the last open child of a child, and that child is the last open child of a parent, and AutoCompleteParentWhenChildrenDone applies, when I complete the grandchild, then the child is completed and then the parent is completed.
- Cascade stops. Given a middle-level task that still has another open child, when the last open grandchild under one of its children is completed, then the cascade stops at the level that still has open children and does not complete anything above it.
- Complete guard. Given a task with any open descendant (child or grandchild), when I mark it complete, then the request is rejected with the existing open-children error (
TASK_PARENT_COMPLETE_BLOCKED_BY_CHILDREN). - Delete guard. Given a task with any descendant, when I delete it, then the request is rejected with the existing has-children error (
TASK_PARENT_DELETE_BLOCKED_BY_CHILDREN). - Journal removal. Given a task and its descendants linked to a journal day, when I remove the task from that day, then the task and all its linked descendants (children and grandchildren) are removed from that day. Descendants not linked to that day are unaffected.
- Child count. Given a task response, when it has children and grandchildren, then
ChildTaskCountequals the number of direct children only. - Completed parent. Given a Completed parent, when I create an open child under it or move an open task under it, then the request is rejected by the same check and error code that #368 adds. This story does not add a second check. #368 skipped a priority test against
TASK_PARENT_STATUS_DERIVEDbecause, at one level of depth, a task with open children hits the depth error first. At depth 3 that path becomes reachable (a request that moves a parent with open children under a Completed parent and also changes its status), so add the test here: decide which ofTASK_PARENT_STATUS_DERIVEDandTASK_PARENT_COMPLETED_NOT_ALLOWEDwins, and pin it for both PUT and PATCH.
13a. Completed ancestors. #368 (attach or create) and #396 (reopen) check the direct parent only, which is enough at one level. At three levels, extend the same check, with the same codeTASK_PARENT_COMPLETED_NOT_ALLOWEDand message, to any Completed ancestor, for attach, create, move and reopen. Given a Completed grandparent with an open parent, an open grandchild created or a Completed grandchild reopened under it is rejected; given an open chain up to the root, it is accepted. This story adds no second error code. - Documentation and client. The depth error message names the 3-level limit (it no longer says "one level").
docs/API.md(theTASK_PARENT_DEPTH_NOT_ALLOWEDentry) anddocs/FRONTEND.mderror list are updated. The API client is regenerated by the backend-builder only;sdk.gen.tsandtypes.gen.tsare never hand-edited.
Edge Cases
- A subtask whose parent isn't on its day is a row with a context header (#387). With three levels, the response must include the full path to the top-level goal.
- Recheck criterion 11 against #387's decision on planning goals and on existing parents already linked to a day.
- Carry-forward moves tasks independently, so a tree can split across days. Carry-forward semantics are unchanged, but check that the deeper tree still displays sensibly.
- Concurrent requests (for example two moves at once) could together exceed depth 3. Likelihood is low. Decide whether the check and the write need to be atomic.
- The
Includechains inJournalEntryRepository(four call sites) must load three levels, and the mapping must follow the row shape from #387. A missingIncludesilently drops grandchildren. - MCP clients will now receive deeper trees. This story waits for the move of
taskflow-assistantinto this repo (#397), so the MCP change ships in the same PR as this API change and runs the integration test (nevridge/TaskFlow#411) and the error-code mapping test (#399). When this story is built, add an assistant step (assistant-builder):get_journaland the tool descriptions show deeper trees, and the matching skills, agent, chat references and README change with it. - Rebase after #356, #361, #352, #350 and #387, which also edit
TaskRepository.UpdateAsync,ApplyUpdateAsyncandMapTodo. Line numbers in older notes are stale; find tests and code by name.
Out of Scope
- The frontend work (#369).
- MCP changes beyond showing deeper trees in
get_journal(listing subtasks was nevridge/taskflow-assistant#4, a separate repo until the move in #397). - A cycle check or cycle error code (unreachable at depth 3).
- Drag-and-drop re-parenting.
- Bulk subtree move or copy.
- Depth beyond 3 (the constant makes this a later change).
- Database migration.
- Changes to carry-forward semantics.
- Unrelated #241 work.
Implementation Notes
Guidance for builders, not acceptance criteria.
- Update
docs/API.mdanddocs/FRONTEND.md. - Tests to rewrite or extend, by name:
TaskItemsControllerV1TestsandTaskItemsControllerPatchTests(depth and parent tests),JournalTodosControllerV1Tests(orphan and child tests),TaskRepositoryTests(parent tests),JournalEntryRepositoryTests(child include and remove tests). - Backend touchpoints:
TaskItemsController(ApplyUpdateAsync,TryAutoCompleteParentAsyncneeds upward recursion, Create parent checks, Delete),TaskRepository.UpdateAsync(add theParentChangedevent),JournalTodosController.RemoveTodoandJournalEntryRepository.RemoveTodoAsync(full descendant cascade). The children-become-child case currently reuses the DEPTH error code and message.
- Dominant language
- C#
- Stars
- 1
- Forks
- 0
- Avg merge
- 2h 2m
- Merged PRs (30d)
- 98
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from nevridge/TaskFlow
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 1 day
-
tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
All issues in nevridge/TaskFlow
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
juandresrodca/AdminForge#54 ·
-
要トリアージ 辞書
Difficulty 1/5 1-3 hours Newbie friendliness 86/100
yksr-melt/Meltype#1625 · 1 reaction ·
Maintainers usually reply within 1 day
-
Docs typoOpen
Difficulty 1/5 Under an hour Newbie friendliness 78/100
MonoGame/docs.monogame.github.io#328 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
space-wizards/RobustToolbox#7197 ·
Maintainers usually reply within 1 day
-
documentation high priority
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
lachydotmcg/capri-desktop#201 ·
Maintainers usually reply within 1 day