App reference

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): --parent on create, set-parent/promote, list flag, read shows steps.
  • MCP: get_item returns sub-tasks, add_item accepts parent_uid, complete_item works as-is, list_items excludes. 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 FindAllBy to Read. Both are single indexed JSONB queries.
  • Clients that flip complete via Update break β€” only the web sheet did, and it changes in the same release.
  • The sub_items projection 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 SubTask message on Item β€” smaller blast radius, but ownership, audit, points and notifications would all need re-implementing beside the svc seams 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.