Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/project-agent-todo-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,14 @@ be bypassed. An evidence-backed peer
transition. This closeout records `self_merged=false` and does not
create a successor review todo for observation-only work.

User-role todos may still write `done` through `todo update --status done`;
when the todo declares `validation_command` (or the `validation_command_argv`
declared via `--validation-command-json`),
that update runs the same completion validation gate as `todo complete` and
fails closed with a typed `validation_blocked_completion` receipt instead of
committing `done`. Todos without a declared command keep the unchanged fast
path.

Use `--resume-when` when deferring a successor that should wake up after a
machine-readable condition instead of living only in prose:

Expand Down
9 changes: 4 additions & 5 deletions loopx/todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -1262,11 +1262,10 @@ def update_goal_todo(
# validation command). Returns a typed failure payload (ok=False) when
# validation blocks; otherwise None.
if status:
try:
update_completes_todo = normalize_todo_status(status) == TODO_STATUS_DONE
except ValueError:
update_completes_todo = False # invalid status surfaces in-lock, unchanged
if update_completes_todo:
# normalize_todo_status returns None (never raises) for an invalid
# status, which simply skips this gate; the in-lock write path then
# surfaces the same invalid-status error as before.
if normalize_todo_status(status) == TODO_STATUS_DONE:
update_block_match = find_todo_block(
resolved_state_file.read_text(encoding="utf-8").splitlines(),
todo_id=todo_id,
Expand Down
Loading