ADR-0025: Sub-tasks
Metadata
- Status: Accepted
- Date: 2026-08-08
- Deciders: EagraΓ Clainne Team
- Related: ADR-0009/0014 (item authorization), ADR-0015 (points auto-claim and repeat successors), ADR-0001 (JSONB dotted-path queries), system rule 1 (surface parity)
Context
Jobs are flat. Two needs push against that. A big chore is too vague for a child β "clean your room" hides the steps that make it doable. And a shared household project ("prepare the birthday party") is one job in spirit but many hands in practice, each piece separately owned and separately rewarded.
Both needs want decomposition under a job. Neither wants a second task
system: the existing invariants β owner-only completion, points
auto-claim, repeat chains, the svc mutation seams β must keep
working unchanged.
Decision
A sub-task is a full Item with a new parent_uid field. One level
deep, hard-enforced: an item with a parent can never gain children,
and an item with children can never gain a parent.
What a sub-task cannot have
A sub-task rejects, with named domain errors: a repeat rule, list membership, a deadline, and its own sub-tasks. Deadlines are banned because sub-tasks never appear on any deadline surface (below) β a deadline nobody can see is a trap. A step that needs its own date deserves to be its own job.
A sub-task CAN carry linked point rewards. In the shared-job case each member banks points for their piece through the unchanged ADR-0015 auto-claim path.
Visibility
Sub-tasks never appear outside their parent. ListItems excludes them
unless the new include_sub_tasks flag is set β the web and Android
caches set it (they hold the full collection and filter locally);
MCP and default CLI calls get the exclusion for free. The Jobs board,
Today, Calendar and MCP list_items show only parents; a parent card
carries a "2/5" progress chip as the outside signal.
Item gains a sub_items read-time projection, populated only by
ItemService.Read and never stored β the same invariant as
ItemList.items. It feeds the MCP get_item detail and the CLI read
view.
Completion
Completing a parent while any sub-task is open is rejected with a named reason carrying the open count. The last sub-task completing only unlocks the parent β no auto-complete. Points claim, successor spawning and the family notification stay on the parent's own completion, so owner-only completion (ADR-0009) is never exercised on someone's behalf. Sub-task completions are silent β the parent's "job done" is the announced moment.
Un-completing a sub-task after its parent completed is rejected β
parent completion freezes the bundle, mirroring how spawned_next
freezes a repeating item. Un-completing the parent (when no successor
spawned) is allowed, does not cascade, and lifts the freeze.
The Update back door closes
ItemService.Update previously accepted a complete flip, bypassing
CompleteItem β and would have bypassed the new gate. Update now
rejects completion-state changes with a named "use CompleteItem"
error. This is a small behaviour change: completion has exactly one
door.
Repeat
When a repeating parent completes and mints its successor, its sub-tasks clone inside the same transaction: fresh uids, incomplete, same title, description and owner, parented to the successor. Each sub-task's claimed POINTS rewards mirror fresh onto its clone, exactly as the parent's already do. Fresh uids per cycle mean only the current cycle's banked rewards are linked to each sub-task uid, so mirroring cannot double.
Ownership and roles
A new sub-task inherits the parent's owner β the child-checklist case works with zero extra taps; managers redistribute per step with the usual mark affordances. A CHILD may create sub-tasks under their own job (the in-list creation loosening precedent, ADR-0014 spirit), even though standalone creation stays manager-only.
Conversion
A new SetParent RPC (shaped like SetList) sets or clears
parent_uid. Setting rejects, with named reasons, a candidate that
has a repeat rule, list membership, a deadline or children β nothing
is silently stripped. The parent must exist, be a standalone
incomplete job, and not itself be a sub-task. Clearing (promotion to a
standalone job) is guard-free. Deleting a parent cascades to its
sub-tasks; deleting a sub-task simply removes it and may thereby
unlock the parent.
Storage
No new table. parent_uid lives in the item's protojson document;
children are found with FindAllBy("parentUid", uid) (ADR-0001) and
its locked variant inside transactions. No archive-registry or
integration-harness changes.
Surfaces (rule 1)
- Web (reference): everything β steps section in the job sheet with quick-add, per-step complete and owner change, progress chip, disabled-with-reason parent complete, conversion both ways.
- Android (daily parity): steps, quick-add, complete, owner change. Deliberate gap: no conversion UI β restructuring is an occasional act done at the web surface.
- CLI (full parity, deliberate promotion above its maintenance
tier):
--parenton create,set-parent/promote, list flag, read shows steps. - MCP:
get_itemreturns sub-tasks,add_itemacceptsparent_uid,complete_itemworks as-is,list_itemsexcludes. Deliberate gap: no conversion tool (matches the no-deletion rule's spirit β assistants do not restructure).
Consequences
- The completion gate adds one locked child-scan to parent
completion; sub-task reads add one
FindAllBytoRead. Both are single indexed JSONB queries. - Clients that flip
completevia Update break β only the web sheet did, and it changes in the same release. - The
sub_itemsprojection must never be populated before a table write. Handlers mutate rows loaded from storage (which lack it), so the invariant holds structurally today; new code must preserve it. - Repeat successor spawning grows a clone loop β the riskiest server piece, covered by unit and integration tests.
Alternatives considered
- Embedded
SubTaskmessage on Item β smaller blast radius, but ownership, audit, points and notifications would all need re-implementing beside thesvcseams instead of through them. Rejected: fights rule 4. - Unlimited nesting β no family use case survives contact with a child-facing UI or the reward math. Rejected for a hard one-level guard.
- Auto-completing the parent on the last step β one tap fewer, but a child's tap would trigger the parent owner's points claim and successor spawn. Rejected: completion stays owner-only and deliberate.
- Sub-task deadlines shown only inside the sheet β a half-alive deadline exempt from every overdue surface. Rejected in favour of banning them.