feat(tasks): allow subtasks up to three levels deep and re-parenting (backend)
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
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.
索引モデルが issue の本文から書いたものです。
説明
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.
- 主要言語
- C#
- スター
- 1
- フォーク
- 0
- 平均マージ
- 2時間 2分
- マージ済み PR(30日)
- 98
環境構築
- Dockerfile または Docker Compose ファイルあり
- プルリクエストのテンプレートなし
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
nevridge/TaskFlow のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
メンテナーはふだん 1 日以内に返信
-
tech-debt
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
nevridge/TaskFlow の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
juandresrodca/AdminForge#54 ·
-
要トリアージ 辞書
難易度 1/5 1〜3時間 初心者へのやさしさ 86/100
yksr-melt/Meltype#1625 · リアクション 1 件 ·
メンテナーはふだん 1 日以内に返信
-
Docs typoオープン
難易度 1/5 1時間未満 初心者へのやさしさ 78/100
MonoGame/docs.monogame.github.io#328 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
space-wizards/RobustToolbox#7197 ·
メンテナーはふだん 1 日以内に返信
-
documentation high priority
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
lachydotmcg/capri-desktop#201 ·
メンテナーはふだん 1 日以内に返信