Skip to content

feat(tasks): let a task complete with its subtasks - #157

Closed
chriscoveries wants to merge 2 commits into
ChesterRa:mainfrom
chriscoveries:feat/task-rollup
Closed

chriscoveries wants to merge 2 commits into
ChesterRa:mainfrom
chriscoveries:feat/task-rollup

Conversation

@chriscoveries

Copy link
Copy Markdown
Contributor

A task that groups subtasks has to be closed by hand once the last of them
is done, and nothing reopens it when one of them comes back. So a parent
can say done while work under it is open again, and whoever runs the board
has to keep checking parents against their subtasks.

A task can now set rollup: true, on task.create or task.update (and
on the MCP cccc_task create and update actions). Its status then follows
its subtasks, the tasks whose parent_id is that task, not counting
archived ones:

  • when an operation leaves every subtask done, the task moves to done in
    the same write;
  • when a subtask leaves done, or a new one is added, while the task is
    done, the task moves back to active if a subtask is active, otherwise to
    planned;
  • the same rule then applies to the task's own parent, if it has the flag;
  • moving the task to done while a subtask is open is rejected, and the
    error names the open subtasks.

Only a change to the subtasks (a move, a re-parent, a create or a delete)
or setting the flag triggers this, so a direct move of the task itself
stands. Each automatic move is listed in the change entry of the operation
that caused it (rollup: [{task_id, status}]), so the context.sync
event in the ledger records it and who caused it. The flag is off unless
set; existing tasks are unchanged.

The automatic move does not check whether the caller may move the parent:
a peer that finishes the last subtask completes a parent it does not own.
Setting the flag is the parent owner's opt-in to that.

How we built it. Base 3d3d10cb. Before each task operation,
apply_all notes every task's status, parent and flag; after it, only the
parents whose subtasks changed are settled, walking up the tree
(task_apply.rs: roll_up, settle). task.move refuses done on a
rollup task with open subtasks. The MCP mapping passes rollup through on
create and update, and the tool schema and CCCC_CONTEXT_OPS_V1.md
describe it. 7 files, +387/−8, about half of it tests.

How we tested it.

  • Core test (context_storage.rs), through the real context store: the
    refusal while subtasks are open; done with the last subtask, and the
    change entry; back to active when a subtask reopens; archived subtasks
    not counted; back to planned when a new subtask is added; a rollup
    grandparent following in the same operation; nothing moves without the
    flag; a non-boolean flag rejected.
  • Daemon test (integration_ops.rs), through the daemon's context_sync
    with a foreman and a peer: the foreman cannot close the task while work
    is open; the peer finishing the last subtask completes the foreman's
    task, and the ledger's context.sync event records the rollup with the
    peer as its author.
  • On upstream main both tests fail: red reference run with only the tests
    on 3d3d10cb,
    https://github.com/chriscoveries/cccc/actions/runs/39704475937 (2683
    passed, 2 failed).
  • claw, 2026-10-22 01:39Z: both tests pass at this head, with the rest of
    the core and MCP suites and the daemon's integration ops (578 tests);
    clippy -D warnings and rustfmt clean. At the red reference each of the
    two targets fails exactly its new test.
  • Live, in a throwaway home with the real daemon, every task call made
    through the cccc mcp stdio server as the Actor making it (a foreman
    and a peer). With 0.4.44 the same calls leave the parent planned after
    its last subtask is done; once the foreman closes it by hand, it stays
    done when a subtask reopens or a new one is added; and closing a rollup
    task with an open subtask is accepted. With this build each step behaves
    as described above, across a daemon restart, and the refused close reads
    task T005 completes with its subtasks; still open: T006.
  • Fork CI green (fmt, clippy -D warnings, nextest, Windows smoke):
    https://github.com/chriscoveries/cccc/actions/runs/39704473266

Tested on: claw, Debian 13, Skylake 8vCPU AVX2/AVX-512, 15G RAM.

A task that groups subtasks has to be closed by hand once the last of them
is done, and nothing reopens it when one of them comes back, so a parent
can say done while work under it is open again.

A task can now set `rollup: true`. Its completion then follows its
subtasks. When the last subtask that is not archived is done, the task
moves to done in the same write. When a subtask leaves done, or a new one
is added, while the task is done, the task moves back to active (or to
planned if no subtask is active), and the same rule applies to its own
parent. A rollup task cannot be moved to done while a subtask is open.

Only a change to the subtasks, or setting the flag, triggers this, so a
direct move of the task itself stands. Each automatic move is listed in
the change entry of the operation that caused it
(`rollup: [{task_id, status}]`), so the `context.sync` event records it.
The flag is off unless set, and existing tasks are unchanged.

Copy link
Copy Markdown
Owner

The opt-in is preferable to unconditional rollup, but I am deferring this feature because it changes task completion semantics and has no corresponding Web indication or controls. A parent can represent an aggregate, or work that still needs its own validation after all children finish; those meanings should be distinguishable to the user.

Please first define the smallest complete behavior: whether parent acceptance/checklist work remains, how explicit parent moves interact with rollup, and what happens on reopen, reparenting, archive and an empty child set. Show the rollup state and explain blocked completion in Web, rather than having status changes or rejected moves appear without context. Keep non-rollup tasks and current authorization unchanged.

A derived child-progress display would be a lower-risk first step if automatic parent completion is not essential. This is a product-semantics decision, not a regression demonstrated merely by new-feature tests failing on main.

Brings in caller task ids (`client_request_id`, `parent_ref`,
`blocked_by_refs`) and the ordered persistence of task targets.

- context/apply.rs: `apply_all` keeps main's `task.create` path, which
  records the created or replayed task id, and wraps every task op in the
  rollup shape check, so a create's rollup moves land in the same change
  entry.
- context/task_apply.rs: `create` is main's, with the `rollup` flag
  validated before the task is built.
- MCP mapping, tool schema and the Context ops standard list both sets of
  fields.
- Tests: main's versions, with the rollup tests appended unchanged.
- Persistence (yaml_storage.rs, yaml_files.rs) is main's, unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants