Skip to content

feat(tasks): ask a decision on a task and answer it with task.decide - #163

Closed
chriscoveries wants to merge 4 commits into
ChesterRa:mainfrom
chriscoveries:feat/task-decide-v2
Closed

chriscoveries wants to merge 4 commits into
ChesterRa:mainfrom
chriscoveries:feat/task-decide-v2

Conversation

@chriscoveries

Copy link
Copy Markdown
Contributor

Depends on #157 (subtask rollup). The branch is cut from #157's head, so
its one commit on top is this change alone; it lands after #157.

A task can wait on the user (waiting_on: "user"), but it has no place
for the question, and nothing answers it. The answer goes by chat,
someone moves the task to done by hand, and the board does not record
what was decided, by whom or when.

A task may now carry decision_request {question, options?, recommendation?}, set on create or update. task.decide {task_id, choice, text?, source?} answers it. It records decision {choice, text, source, by, at} on the task, sets waiting_on to none, moves the task
to done, and appends a decision coordination note that carries the
task's task_id. When the request lists options, the choice must be one
of them. A task is decided once: after that its request cannot change, and
a create cannot carry a decision of its own. A rollup task (#157) is
decided only once its subtasks are closed, the same check as moving it to
done.

The user may decide, and so may actors listed in a new group setting,
task_approvers (stored as tasks.approvers); an empty or absent list
names none. As everywhere in cccc's permission model, system and an
empty by take the user's path here, both for deciding and for changing
the list. The foreman is not an approver unless it is listed, and only
the user's path may change the list, so a foreman cannot approve its own
requests. The summary view of context_get now shows decision_request
and decision. The MCP cccc_task tool gains a decide action and
passes decision_request through.

How we built it. Base: #157's head 9cd296f8. task_apply::decide
checks the request and the choice, records the decision and returns the
note, which apply.rs adds with the existing add_note. Its open-subtask
check is #157's, moved into one helper that task.move and task.decide
share. Create and update check decision_request. The daemon's context
authorization admits task.decide for the user's path and for listed
approvers; group_settings_update maps task_approvers to its section and
lets only the user's path change it. The MCP mapping, the tool schema,
CCCC_CONTEXT_OPS_V1.md and the IPC doc describe it. 11 files,
+540/−15, of which 252 lines are tests.

How we tested it.

  • Core test (context_storage.rs), through the real context store: a
    request is stored trimmed; a request without a question, an empty
    option, a create carrying a decision, a choice outside the options and a
    decide on a task without a request are rejected and write nothing; a
    decide records the decision, clears waiting_on, moves the task to done
    and adds one decision note with the task id; a second decide and a
    change to the decided request are rejected; a rollup task with an open
    subtask cannot be decided, and deciding that subtask completes it.
  • Daemon test (integration_ops.rs), through context_sync,
    group_settings_update and context_get: the summary shows the request;
    the foreman and an unlisted peer are refused; the foreman cannot list
    itself; with an empty list the peer is still refused; system lists the
    foreman, which then decides; an empty by lists the peer instead, and
    the foreman is refused again while the peer decides; the user, system
    and an empty by each decide a task; each decision records its by,
    and the notes name all five tasks.
  • MCP mapping unit test: create keeps decision_request; decide maps to
    task.decide with only its four fields.
  • Without the change the three tests fail: red reference run with only
    the tests on feat(tasks): let a task complete with its subtasks #157's head, https://github.com/chriscoveries/cccc/actions/runs/39735827187
    (2685 passed, 3 failed).
  • claw, 2026-10-22 06:44Z: the three tests pass at this head, with the
    rest of the core and MCP suites, the daemon's integration ops and its
    settings and context unit tests (606 tests); clippy -D warnings
    and rustfmt clean. At the red reference each of the three targets fails
    exactly its new test.
  • Live, in a throwaway home with the real daemon: a foreman asks through
    the cccc mcp stdio server and over the daemon socket; a peer answers
    through MCP; the user, system and an empty by answer over the socket.
    With 0.4.44 and upstream main, 11 of the 12 checks fail: MCP drops the
    request, the summary view shows none, decide is unknown, a foreman can
    list itself as an approver, and nothing is recorded. The one that passes,
    system and an empty by changing the list, passes only because they
    keep task_approvers as an unknown flat setting that nothing reads.
    With this build all 12 checks pass: the summary shows the question; the foreman and an unlisted peer
    are refused, and so is the peer under an empty list; the foreman cannot
    list itself; a listed peer decides and the user decides; a second answer
    is refused; system and an empty by each decide and each change the
    list; a rollup task is refused while its subtask is open, and the peer's
    decision on the subtask completes it; each decision leaves a note naming
    its task.
  • Fork CI green (fmt, clippy -D warnings, nextest, Windows smoke):
    https://github.com/chriscoveries/cccc/actions/runs/39735823645

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.
A task that waits on the user has no place for the question, and no way
for the user to answer it: the answer travels as chat, someone moves the
task to done by hand, and nothing records what was decided, by whom or
when.

A task may now carry `decision_request {question, options?,
recommendation?}`, set on create or update. `task.decide {task_id,
choice, text?, source?}` answers it: it records `decision {choice, text,
source, by, at}` on the task, sets waiting_on to none, moves the task to
done, and appends a `decision` coordination note with the task's
`task_id`. When the request lists options, the choice must be one of
them. A task is decided once; after that its request cannot change, and a
create cannot carry a decision of its own. A rollup task is decided only
once its subtasks are closed, the same check as moving it to done.

The user may decide, and so may actors listed in the new group setting
`task_approvers` (stored as `tasks.approvers`); an empty or absent list
names none. `system` and an empty `by` take the user's path here, as they
do across the permission model, for deciding and for changing the list.
The foreman is not an approver unless listed, and only the user's path
may change the list, so a foreman cannot approve its own requests. The
summary view of `context_get` shows both fields, and the MCP `cccc_task`
tool gains a `decide` action and passes `decision_request` through.

Copy link
Copy Markdown
Owner

I am deferring this along with its #157 dependency. The main semantic issue is that every task.decide moves the task to done. Approving “proceed with deployment”, for example, records permission to do the work; it does not establish that deployment finished.

Please separate recording a decision from completing the work, or explicitly model a decision-only task whose deliverable is the decision itself. Keep the decision author and audit note, but make the subsequent work transition explicit. The approver policy also needs a usable Web path to inspect the question/options and answer as the user; adding the write contract without that leaves the central workflow incomplete.

Please propose that smaller end-to-end flow independently of automatic subtask rollup before extending the permission/settings surface further.

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.
This branch builds on feat/task-rollup, which now includes upstream main.
Merging that head brings in main, with the same resolutions, so the two
stay one stack.

- context/task_apply.rs: `create` is main's (caller ids and refs), with
  the rollup flag check and this branch's `decision_request` validation
  and the refusal of a direct `decision` kept ahead of building the task.
- MCP mapping: the create field list keeps main's caller-id fields, then
  `rollup` and `decision_request`; both mapping tests are kept whole.
- Everything else merged cleanly. This branch's own changes on top of
  feat/task-rollup are unchanged line for line, and persistence
  (yaml_storage.rs, yaml_files.rs) is main's.
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