gabriel / muse public
feat patch feat/todo-domain-plugin #1 / 1
AI Agent gabriel · 2 hours ago · Sep 12, 2026 · Diff

feat: add the todo domain plugin -- Build With Muse Episode 06

A minimal, real reference domain built live for the episode: one tracked file (todo.txt), one task per line, tasks addressed by their own content rather than line position. Implements the required six-method core protocol only (no address-keyed merge extension, no CRDT) -- starts from muse/plugins/scaffold, most of which is unmodified.

Registered in muse/plugins/registry.py alongside the existing scaffold/ timeline/social/etc. plugins -- no core engine changes needed, per the whole point of the six-method protocol (Episode 05).

Three real bugs were found and fixed while building this on camera for the episode script, kept as regression tests here:

1. snapshot()/merge() initially hashed raw bytes for object IDs. Muse's real object format is blob_id() -- sha256("blob <size>\0" + data), Episode 03's own subject -- not a bare sha256 of the content. This made muse commit fail outright with a content-integrity error.

2. diff()'s ops were originally keyed entirely by task-level address ("todo.txt::<hash>"). muse checkout restores/deletes files by matching op["address"] against the snapshot manifest's *file* paths, so task-level addresses never matched anything -- checkout silently left stale content in the working tree with no error. Fixed by nesting task-level AddressedInsertOp/AddressedDeleteOp entries as child_ops under a single file-level PatchOp keyed by "todo.txt", matching the real pattern already used by richer domains (see the code domain's own patch/child_ops shape from Episode 02/03).

3. drift() never forwarded repo_root into diff(), so _load_tasks saw repo_root=None on both sides and returned empty sets unconditionally -- muse status on a dirty tree would have silently reported zero changes. Caught only by writing tests afterward, not live on camera.

Tests: tests/test_todo_plugin.py -- 12 tests covering schema, snapshot, diff (including regression tests for bugs 1 and 2), merge (including a real two-independent-additions-never-conflict case), and drift (including a regression test for bug 3). Full sweep alongside test_plugin_registry.py, test_domain_command_registry.py, and test_scaffold_plugin.py: 44 passed, no regressions.

Verified end-to-end via muse-dev against a real disposable repo: init, commit, two independent branches, structured diff between them, a real three-way merge with zero conflicts, and status/diff against a dirty working tree.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:832d1ca80cb25129c91d8c8c190ec30af2e638a3e8160431f4704fd881bceaee sha
+61 ~1 symbols
4 changed · 1188 in snapshot files
sha256:0192ccc2ce37fc58da0e4873ca3c1a34ead66e514316f190933522fddfd78800 snapshot
+61
symbols added
~1
symbol modified
4
files changed
1188
files in snapshot
0
dead code introduced
Semantic Changes 62 symbols
~ muse/plugins/todo/plugin.py .py 34 symbols added
+ TodoPlugin class class TodoPlugin L76–205
+ apply method method apply L188–191
+ diff method method diff L92–147
+ drift method method drift L178–186
+ merge method method merge L149–176
+ schema method method schema L193–205
+ snapshot method method snapshot L79–90
+ _DOMAIN_NAME variable variable _DOMAIN_NAME L35–35
+ _TASK_FILE variable variable _TASK_FILE L36–36
+ _load_tasks function function _load_tasks L48–63
+ _task_address function function _task_address L39–41
+ _task_content_id function function _task_content_id L44–45
+ _write_tasks function function _write_tasks L66–73
+ AddressedDeleteOp import import AddressedDeleteOp L22–22
+ AddressedInsertOp import import AddressedInsertOp L22–22
+ DomainSchema import import DomainSchema L20–20
+ DriftReport import import DriftReport L22–22
+ LiveState import import LiveState L22–22
+ Manifest import import Manifest L21–21
+ MergeResult import import MergeResult L22–22
+ PatchOp import import PatchOp L22–22
+ SetSchema import import SetSchema L20–20
+ SnapshotManifest import import SnapshotManifest L22–22
+ StateDelta import import StateDelta L22–22
+ StateSnapshot import import StateSnapshot L22–22
+ StructuredDelta import import StructuredDelta L22–22
+ __version__ import import __version__ L18–18
+ _stat import import _stat L16–16
+ blob_id import import blob_id L21–21
+ hashlib import import hashlib L13–13
+ os import import os L14–14
+ pathlib import import pathlib L15–15
+ read_object import import read_object L19–19
+ write_object import import write_object L19–19
~ tests/test_todo_plugin.py .py 26 symbols added
+ TestDiff class class TestDiff L68–108
+ test_added_and_removed_tasks_both_reported method method test_added_and_removed_tasks_both_reported L102–108
+ test_child_ops_carry_task_level_detail method method test_child_ops_carry_task_level_detail L91–100
+ test_no_change_when_hashes_match method method test_no_change_when_hashes_match L69–73
+ test_top_level_op_is_keyed_by_file_not_task method method test_top_level_op_is_keyed_by_file_not_task L75–89
+ TestDrift class class TestDrift L140–164
+ test_clean_tree_has_no_drift method method test_clean_tree_has_no_drift L159–164
+ test_uncommitted_change_is_detected method method test_uncommitted_change_is_detected L141–157
+ TestMerge class class TestMerge L111–137
+ test_task_removed_on_one_side_stays_removed method method test_task_removed_on_one_side_stays_ L126–137
+ test_two_independent_additions_never_conflict method method test_two_independent_additions_never_conflict L112–124
+ TestSchema class class TestSchema L39–46
+ test_merge_mode_is_three_way method method test_merge_mode_is_three_way L45–46
+ test_top_level_is_content_addressed_set method method test_top_level_is_content_addressed_set L40–43
+ TestSnapshot class class TestSnapshot L49–65
+ test_empty_repo_has_no_files method method test_empty_repo_has_no_files L50–53
+ test_object_id_uses_canonical_blob_format method method test_object_id_uses_canonical_blob_format L55–65
+ _commit_tasks function function _commit_tasks L29–36
+ _repo function function _repo L24–26
+ TodoPlugin import import TodoPlugin L21–21
+ _TASK_FILE import import _TASK_FILE L21–21
+ annotations import import annotations L15–15
+ blob_id import import blob_id L20–20
+ pathlib import import pathlib L17–17
+ read_object import import read_object L19–19
+ write_object import import write_object L19–19
~ muse/plugins/registry.py .py 1 symbol added, 1 symbol modified
+ TodoPlugin import import TodoPlugin L30–30
Files Changed
+3 ~1
1188 in snapshot
← Older Oldest on feat/todo-domain-plugin
All commits
Newer → Latest on feat/todo-domain-plugin

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:832d1ca80cb25129c91d8c8c190ec30af2e638a3e8160431f4704fd881bceaee --body "your comment"