gabriel / muse public
test_core_ignore.py python
553 lines 21.9 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for muse/core/ignore.py — .museignore TOML parser and path filter."""
2
3 import pathlib
4
5 import pytest
6
7 from muse.core.ignore import (
8 MuseIgnoreConfig,
9 _matches,
10 is_ignored,
11 load_ignore_config,
12 resolve_patterns,
13 )
14 from muse.core.snapshot import _BUILTIN_SECRET_PATTERNS, walk_workdir
15
16
17 # ---------------------------------------------------------------------------
18 # load_ignore_config
19 # ---------------------------------------------------------------------------
20
21
22 class TestLoadIgnoreConfig:
23 def test_returns_empty_when_no_file(self, tmp_path: pathlib.Path) -> None:
24 assert load_ignore_config(tmp_path) == {}
25
26 def test_empty_toml_file(self, tmp_path: pathlib.Path) -> None:
27 (tmp_path / ".museignore").write_text("")
28 assert load_ignore_config(tmp_path) == {}
29
30 def test_toml_comments_only(self, tmp_path: pathlib.Path) -> None:
31 (tmp_path / ".museignore").write_text("# just a comment\n")
32 assert load_ignore_config(tmp_path) == {}
33
34 def test_global_section_parsed(self, tmp_path: pathlib.Path) -> None:
35 (tmp_path / ".museignore").write_text(
36 '[global]\npatterns = ["*.tmp", "*.bak"]\n'
37 )
38 config = load_ignore_config(tmp_path)
39 assert config.get("global", {}).get("patterns") == ["*.tmp", "*.bak"]
40
41 def test_domain_section_parsed(self, tmp_path: pathlib.Path) -> None:
42 (tmp_path / ".museignore").write_text(
43 '[domain.midi]\npatterns = ["*.bak"]\n'
44 )
45 config = load_ignore_config(tmp_path)
46 domain_map = config.get("domain", {})
47 assert domain_map.get("midi", {}).get("patterns") == ["*.bak"]
48
49 def test_multiple_domain_sections_parsed(self, tmp_path: pathlib.Path) -> None:
50 content = (
51 '[domain.midi]\npatterns = ["*.bak"]\n'
52 '[domain.code]\npatterns = ["__pycache__/"]\n'
53 )
54 (tmp_path / ".museignore").write_text(content)
55 config = load_ignore_config(tmp_path)
56 domain_map = config.get("domain", {})
57 assert domain_map.get("midi", {}).get("patterns") == ["*.bak"]
58 assert domain_map.get("code", {}).get("patterns") == ["__pycache__/"]
59
60 def test_global_and_domain_sections_parsed(self, tmp_path: pathlib.Path) -> None:
61 content = (
62 '[global]\npatterns = ["*.tmp"]\n'
63 '[domain.midi]\npatterns = ["*.bak"]\n'
64 )
65 (tmp_path / ".museignore").write_text(content)
66 config = load_ignore_config(tmp_path)
67 assert config.get("global", {}).get("patterns") == ["*.tmp"]
68 domain_map = config.get("domain", {})
69 assert domain_map.get("midi", {}).get("patterns") == ["*.bak"]
70
71 def test_negation_pattern_preserved(self, tmp_path: pathlib.Path) -> None:
72 (tmp_path / ".museignore").write_text(
73 '[global]\npatterns = ["*.bak", "!keep.bak"]\n'
74 )
75 config = load_ignore_config(tmp_path)
76 assert config.get("global", {}).get("patterns") == ["*.bak", "!keep.bak"]
77
78 def test_invalid_toml_raises_value_error(self, tmp_path: pathlib.Path) -> None:
79 (tmp_path / ".museignore").write_text("this is not valid toml ][")
80 with pytest.raises(ValueError, match=".museignore"):
81 load_ignore_config(tmp_path)
82
83 def test_section_without_patterns_key(self, tmp_path: pathlib.Path) -> None:
84 # A section with no patterns key produces an empty DomainSection.
85 (tmp_path / ".museignore").write_text("[global]\n")
86 config = load_ignore_config(tmp_path)
87 assert config.get("global") == {}
88
89 def test_non_string_patterns_silently_dropped(
90 self, tmp_path: pathlib.Path
91 ) -> None:
92 # Non-string items in the patterns array are silently skipped.
93 (tmp_path / ".museignore").write_text(
94 '[global]\npatterns = ["*.tmp", 42, true, "*.bak"]\n'
95 )
96 config = load_ignore_config(tmp_path)
97 assert config.get("global", {}).get("patterns") == ["*.tmp", "*.bak"]
98
99
100 # ---------------------------------------------------------------------------
101 # resolve_patterns
102 # ---------------------------------------------------------------------------
103
104
105 class TestResolvePatterns:
106 def test_empty_config_returns_empty(self) -> None:
107 config: MuseIgnoreConfig = {}
108 assert resolve_patterns(config, "midi") == []
109
110 def test_global_only(self) -> None:
111 config: MuseIgnoreConfig = {"global": {"patterns": ["*.tmp", ".DS_Store"]}}
112 assert resolve_patterns(config, "midi") == ["*.tmp", ".DS_Store"]
113
114 def test_domain_only(self) -> None:
115 config: MuseIgnoreConfig = {"domain": {"midi": {"patterns": ["*.bak"]}}}
116 assert resolve_patterns(config, "midi") == ["*.bak"]
117
118 def test_global_and_matching_domain_merged(self) -> None:
119 config: MuseIgnoreConfig = {
120 "global": {"patterns": ["*.tmp"]},
121 "domain": {"midi": {"patterns": ["*.bak"]}},
122 }
123 result = resolve_patterns(config, "midi")
124 # Global comes first, then domain-specific.
125 assert result == ["*.tmp", "*.bak"]
126
127 def test_other_domain_patterns_excluded(self) -> None:
128 config: MuseIgnoreConfig = {
129 "global": {"patterns": ["*.tmp"]},
130 "domain": {
131 "midi": {"patterns": ["*.bak"]},
132 "code": {"patterns": ["node_modules/"]},
133 },
134 }
135 # Asking for "midi" — code patterns must not appear.
136 result = resolve_patterns(config, "midi")
137 assert "*.bak" in result
138 assert "node_modules/" not in result
139
140 def test_active_domain_not_in_config_returns_global_only(self) -> None:
141 config: MuseIgnoreConfig = {
142 "global": {"patterns": ["*.tmp"]},
143 "domain": {"midi": {"patterns": ["*.bak"]}},
144 }
145 # Active domain "genomics" has no section — only global patterns.
146 result = resolve_patterns(config, "genomics")
147 assert result == ["*.tmp"]
148
149 def test_global_section_without_patterns_key(self) -> None:
150 config: MuseIgnoreConfig = {"global": {}}
151 assert resolve_patterns(config, "midi") == []
152
153 def test_domain_section_without_patterns_key(self) -> None:
154 config: MuseIgnoreConfig = {"domain": {"midi": {}}}
155 assert resolve_patterns(config, "midi") == []
156
157 def test_order_preserved(self) -> None:
158 config: MuseIgnoreConfig = {
159 "global": {"patterns": ["a", "b", "c"]},
160 "domain": {"midi": {"patterns": ["d", "e"]}},
161 }
162 assert resolve_patterns(config, "midi") == ["a", "b", "c", "d", "e"]
163
164 def test_negation_in_global_preserved(self) -> None:
165 config: MuseIgnoreConfig = {
166 "global": {"patterns": ["*.bak", "!keep.bak"]},
167 }
168 patterns = resolve_patterns(config, "midi")
169 assert patterns == ["*.bak", "!keep.bak"]
170
171 def test_negation_in_domain_overrides_global(self) -> None:
172 # A negation in the domain section can un-ignore a globally ignored path.
173 config: MuseIgnoreConfig = {
174 "global": {"patterns": ["*.bak"]},
175 "domain": {"midi": {"patterns": ["!session.bak"]}},
176 }
177 patterns = resolve_patterns(config, "midi")
178 # session.bak is globally ignored but negated by domain section.
179 assert not is_ignored("session.bak", patterns)
180 # other.bak is globally ignored and not negated.
181 assert is_ignored("other.bak", patterns)
182
183
184 # ---------------------------------------------------------------------------
185 # _matches (internal — gitignore path semantics, unchanged)
186 # ---------------------------------------------------------------------------
187
188
189 class TestMatchesInternal:
190 """Verify the core matching logic in isolation."""
191
192 # ---- Patterns without slash: match any component ----
193
194 def test_ext_pattern_matches_top_level(self) -> None:
195 assert _matches("drums.tmp", "*.tmp")
196
197 def test_ext_pattern_matches_nested(self) -> None:
198 assert _matches("tracks/drums.tmp", "*.tmp")
199
200 def test_ext_pattern_matches_deep_nested(self) -> None:
201 assert _matches("a/b/c/drums.tmp", "*.tmp")
202
203 def test_ext_pattern_no_false_positive(self) -> None:
204 assert not _matches("tracks/drums.mid", "*.tmp")
205
206 def test_exact_name_matches_any_depth(self) -> None:
207 assert _matches("a/b/.DS_Store", ".DS_Store")
208
209 def test_exact_name_top_level(self) -> None:
210 assert _matches(".DS_Store", ".DS_Store")
211
212 # ---- Patterns with slash: match full path from right ----
213
214 def test_dir_ext_matches_direct_child(self) -> None:
215 import pathlib as pl
216 assert _matches(pl.PurePosixPath("tracks/drums.bak"), "tracks/*.bak")
217
218 def test_dir_ext_no_match_different_dir(self) -> None:
219 import pathlib as pl
220 assert not _matches(pl.PurePosixPath("exports/drums.bak"), "tracks/*.bak")
221
222 def test_double_star_matches_nested(self) -> None:
223 import pathlib as pl
224 assert _matches(pl.PurePosixPath("a/b/cache/index.dat"), "**/cache/*.dat")
225
226 def test_double_star_matches_shallow(self) -> None:
227 import pathlib as pl
228 # **/cache/*.dat should match cache/index.dat (** = zero components)
229 assert _matches(pl.PurePosixPath("cache/index.dat"), "**/cache/*.dat")
230
231 # ---- Anchored patterns (leading /) ----
232
233 def test_anchored_matches_root_level(self) -> None:
234 import pathlib as pl
235 assert _matches(pl.PurePosixPath("scratch.mid"), "/scratch.mid")
236
237 def test_anchored_no_match_nested(self) -> None:
238 import pathlib as pl
239 assert not _matches(pl.PurePosixPath("tracks/scratch.mid"), "/scratch.mid")
240
241 def test_anchored_dir_pattern_no_match_file(self) -> None:
242 import pathlib as pl
243 # /renders/*.wav anchored to root
244 assert _matches(pl.PurePosixPath("renders/mix.wav"), "/renders/*.wav")
245 assert not _matches(pl.PurePosixPath("exports/renders/mix.wav"), "/renders/*.wav")
246
247
248 # ---------------------------------------------------------------------------
249 # is_ignored — full rule evaluation with negation (unchanged layer)
250 # ---------------------------------------------------------------------------
251
252
253 class TestIsIgnored:
254 def test_empty_patterns_ignores_nothing(self) -> None:
255 assert not is_ignored("tracks/drums.mid", [])
256
257 def test_simple_ext_ignored(self) -> None:
258 assert is_ignored("drums.tmp", ["*.tmp"])
259
260 def test_simple_ext_nested_ignored(self) -> None:
261 assert is_ignored("tracks/drums.tmp", ["*.tmp"])
262
263 def test_non_matching_not_ignored(self) -> None:
264 assert not is_ignored("drums.mid", ["*.tmp"])
265
266 def test_directory_pattern_ignores_files_inside(self) -> None:
267 # Trailing / means "this directory and all its contents" — files inside
268 # the directory are ignored, matching gitignore semantics.
269 assert is_ignored("renders/mix.wav", ["renders/"])
270 assert is_ignored("renders/deep/session.mid", ["renders/"])
271 assert not is_ignored("other/mix.wav", ["renders/"])
272
273 def test_negation_un_ignores(self) -> None:
274 patterns = ["*.bak", "!keep.bak"]
275 assert is_ignored("session.bak", patterns)
276 assert not is_ignored("keep.bak", patterns)
277
278 def test_negation_nested_un_ignores(self) -> None:
279 patterns = ["*.bak", "!tracks/keeper.bak"]
280 assert is_ignored("tracks/session.bak", patterns)
281 assert not is_ignored("tracks/keeper.bak", patterns)
282
283 def test_last_rule_wins(self) -> None:
284 # First rule ignores, second negates, third re-ignores.
285 patterns = ["*.bak", "!session.bak", "*.bak"]
286 assert is_ignored("session.bak", patterns)
287
288 def test_anchored_pattern_root_only(self) -> None:
289 patterns = ["/scratch.mid"]
290 assert is_ignored("scratch.mid", patterns)
291 assert not is_ignored("tracks/scratch.mid", patterns)
292
293 def test_ds_store_at_any_depth(self) -> None:
294 patterns = [".DS_Store"]
295 assert is_ignored(".DS_Store", patterns)
296 assert is_ignored("tracks/.DS_Store", patterns)
297 assert is_ignored("a/b/c/.DS_Store", patterns)
298
299 def test_double_star_glob(self) -> None:
300 # Match *.pyc at any depth using a no-slash pattern.
301 assert is_ignored("__pycache__/foo.pyc", ["*.pyc"])
302 assert is_ignored("tracks/__pycache__/foo.pyc", ["*.pyc"])
303 # Pattern with embedded slash + ** at start.
304 assert is_ignored("cache/index.dat", ["**/cache/*.dat"])
305 assert is_ignored("a/b/cache/index.dat", ["**/cache/*.dat"])
306
307 def test_multiple_patterns_first_matches(self) -> None:
308 patterns = ["*.tmp", "*.bak"]
309 assert is_ignored("drums.tmp", patterns)
310 assert is_ignored("drums.bak", patterns)
311 assert not is_ignored("drums.mid", patterns)
312
313 def test_negation_before_rule_has_no_effect(self) -> None:
314 # Negation appears before the rule it would override — last rule wins,
315 # so the file ends up ignored.
316 patterns = ["!session.bak", "*.bak"]
317 assert is_ignored("session.bak", patterns)
318
319
320 # ---------------------------------------------------------------------------
321 # Integration: MidiPlugin.snapshot() honours .museignore TOML format
322 # ---------------------------------------------------------------------------
323
324
325 class TestMidiPluginSnapshotIgnore:
326 """End-to-end: .museignore TOML format filters paths during snapshot()."""
327
328 def _make_repo(self, tmp_path: pathlib.Path) -> pathlib.Path:
329 """Create a minimal repo structure with a state/ directory."""
330 workdir = tmp_path
331 return tmp_path
332
333 def test_snapshot_without_museignore_includes_all(
334 self, tmp_path: pathlib.Path
335 ) -> None:
336 from muse.plugins.midi.plugin import MidiPlugin
337
338 root = self._make_repo(tmp_path)
339 workdir = root
340 (workdir / "beat.mid").write_text("data")
341 (workdir / "session.tmp").write_text("temp")
342
343 plugin = MidiPlugin()
344 snap = plugin.snapshot(workdir)
345 assert "beat.mid" in snap["files"]
346 assert "session.tmp" in snap["files"]
347
348 def test_snapshot_excludes_global_pattern(self, tmp_path: pathlib.Path) -> None:
349 from muse.plugins.midi.plugin import MidiPlugin
350
351 root = self._make_repo(tmp_path)
352 workdir = root
353 (workdir / "beat.mid").write_text("data")
354 (workdir / "session.tmp").write_text("temp")
355 (root / ".museignore").write_text('[global]\npatterns = ["*.tmp"]\n')
356
357 plugin = MidiPlugin()
358 snap = plugin.snapshot(workdir)
359 assert "beat.mid" in snap["files"]
360 assert "session.tmp" not in snap["files"]
361
362 def test_snapshot_excludes_domain_specific_pattern(
363 self, tmp_path: pathlib.Path
364 ) -> None:
365 from muse.plugins.midi.plugin import MidiPlugin
366
367 root = self._make_repo(tmp_path)
368 workdir = root
369 (workdir / "beat.mid").write_text("data")
370 (workdir / "session.bak").write_text("backup")
371 (root / ".museignore").write_text(
372 '[domain.midi]\npatterns = ["*.bak"]\n'
373 )
374
375 plugin = MidiPlugin()
376 snap = plugin.snapshot(workdir)
377 assert "beat.mid" in snap["files"]
378 assert "session.bak" not in snap["files"]
379
380 def test_snapshot_domain_isolation_other_domain_ignored(
381 self, tmp_path: pathlib.Path
382 ) -> None:
383 from muse.plugins.midi.plugin import MidiPlugin
384
385 root = self._make_repo(tmp_path)
386 workdir = root
387 (workdir / "beat.mid").write_text("data")
388 (workdir / "requirements.txt").write_text("pytest\n")
389 # code-only ignore — must NOT apply to the midi plugin.
390 (root / ".museignore").write_text(
391 '[domain.code]\npatterns = ["requirements.txt"]\n'
392 )
393
394 plugin = MidiPlugin()
395 snap = plugin.snapshot(workdir)
396 # requirements.txt should remain because the [domain.code] section
397 # does not apply when the active domain is "midi".
398 assert "requirements.txt" in snap["files"]
399 assert "beat.mid" in snap["files"]
400
401 def test_snapshot_negation_keeps_file(self, tmp_path: pathlib.Path) -> None:
402 from muse.plugins.midi.plugin import MidiPlugin
403
404 root = self._make_repo(tmp_path)
405 workdir = root
406 (workdir / "session.tmp").write_text("temp")
407 (workdir / "important.tmp").write_text("keep me")
408 (root / ".museignore").write_text(
409 '[global]\npatterns = ["*.tmp", "!important.tmp"]\n'
410 )
411
412 plugin = MidiPlugin()
413 snap = plugin.snapshot(workdir)
414 assert "session.tmp" not in snap["files"]
415 assert "important.tmp" in snap["files"]
416
417 def test_snapshot_domain_negation_overrides_global(
418 self, tmp_path: pathlib.Path
419 ) -> None:
420 from muse.plugins.midi.plugin import MidiPlugin
421
422 root = self._make_repo(tmp_path)
423 workdir = root
424 (workdir / "session.bak").write_text("backup")
425 content = (
426 '[global]\npatterns = ["*.bak"]\n'
427 '[domain.midi]\npatterns = ["!session.bak"]\n'
428 )
429 (root / ".museignore").write_text(content)
430
431 plugin = MidiPlugin()
432 snap = plugin.snapshot(workdir)
433 # session.bak is globally ignored but un-ignored by the midi domain section.
434 assert "session.bak" in snap["files"]
435
436 def test_snapshot_nested_pattern(self, tmp_path: pathlib.Path) -> None:
437 from muse.plugins.midi.plugin import MidiPlugin
438
439 root = self._make_repo(tmp_path)
440 workdir = root
441 renders = workdir / "renders"
442 renders.mkdir()
443 (workdir / "beat.mid").write_text("data")
444 (renders / "preview.wav").write_text("audio")
445 (root / ".museignore").write_text(
446 '[global]\npatterns = ["renders/*.wav"]\n'
447 )
448
449 plugin = MidiPlugin()
450 snap = plugin.snapshot(workdir)
451 assert "beat.mid" in snap["files"]
452 assert "renders/preview.wav" not in snap["files"]
453
454 def test_snapshot_dotfiles_always_excluded(self, tmp_path: pathlib.Path) -> None:
455 from muse.plugins.midi.plugin import MidiPlugin
456
457 root = self._make_repo(tmp_path)
458 workdir = root
459 (workdir / "beat.mid").write_text("data")
460 (workdir / ".DS_Store").write_bytes(b"\x00" * 16)
461 # No .museignore — dotfiles excluded by the built-in plugin rule.
462
463 plugin = MidiPlugin()
464 snap = plugin.snapshot(workdir)
465 assert "beat.mid" in snap["files"]
466 assert ".DS_Store" not in snap["files"]
467
468 def test_snapshot_with_empty_museignore(self, tmp_path: pathlib.Path) -> None:
469 from muse.plugins.midi.plugin import MidiPlugin
470
471 root = self._make_repo(tmp_path)
472 workdir = root
473 (workdir / "beat.mid").write_text("data")
474 # Valid TOML — just a comment, no sections.
475 (root / ".museignore").write_text("# empty config\n")
476
477 plugin = MidiPlugin()
478 snap = plugin.snapshot(workdir)
479 assert "beat.mid" in snap["files"]
480
481 def test_snapshot_directory_pattern_excludes_files_inside(
482 self, tmp_path: pathlib.Path
483 ) -> None:
484 from muse.plugins.midi.plugin import MidiPlugin
485
486 root = self._make_repo(tmp_path)
487 workdir = root
488 renders = workdir / "renders"
489 renders.mkdir()
490 (renders / "mix.wav").write_text("audio")
491 # Directory pattern ignores all files inside it — gitignore semantics.
492 (root / ".museignore").write_text('[global]\npatterns = ["renders/"]\n')
493
494 plugin = MidiPlugin()
495 snap = plugin.snapshot(workdir)
496 assert "renders/mix.wav" not in snap["files"]
497
498
499 # ---------------------------------------------------------------------------
500 # Regression: _BUILTIN_SECRET_PATTERNS must not exclude .env.example
501 # ---------------------------------------------------------------------------
502
503
504 class TestBuiltinSecretPatterns:
505 """.env.example is the universal convention for a credential-free template.
506 It must never be caught by the builtin secrets blocklist.
507
508 Regression: .env.* was previously in _BUILTIN_SECRET_PATTERNS, which
509 caused walk_workdir / workdir_snapshot to exclude .env.example even when
510 .museignore had no such rule — making ``muse diff`` falsely report it as
511 deleted and blocking ``muse checkout`` with a false dirty-tree error.
512 """
513
514 def test_env_wildcard_not_in_builtin_patterns(self) -> None:
515 assert ".env.*" not in _BUILTIN_SECRET_PATTERNS, (
516 ".env.* must not be in _BUILTIN_SECRET_PATTERNS — it catches "
517 ".env.example, the standard credential-free template convention"
518 )
519
520 def test_env_example_not_ignored_by_builtins(self) -> None:
521 assert not is_ignored(".env.example", _BUILTIN_SECRET_PATTERNS), (
522 ".env.example must not be excluded by the builtin secret patterns"
523 )
524
525 def test_real_secret_files_still_ignored(self) -> None:
526 for path in (".env", ".env.local", ".env.staging", ".env.production",
527 ".env.prod", ".env.development", ".envrc"):
528 assert is_ignored(path, _BUILTIN_SECRET_PATTERNS), (
529 f"{path} must still be excluded by builtin secret patterns"
530 )
531
532 def test_walk_workdir_includes_env_example(self, tmp_path: pathlib.Path) -> None:
533 """walk_workdir must include .env.example with no .museignore present."""
534 (tmp_path / ".muse").mkdir()
535 (tmp_path / ".muse" / "repo.json").write_text('{"repo_id": "x", "domain": "code"}')
536 (tmp_path / ".env.example").write_text("DB_PASSWORD=changeme\n")
537 (tmp_path / "app.py").write_text("x = 1\n")
538
539 manifest = walk_workdir(tmp_path)
540 assert ".env.example" in manifest, (
541 "walk_workdir must include .env.example — it is not a secret file"
542 )
543
544 def test_walk_workdir_excludes_real_env_secrets(self, tmp_path: pathlib.Path) -> None:
545 """walk_workdir must still exclude real secret .env files."""
546 (tmp_path / ".muse").mkdir()
547 (tmp_path / ".muse" / "repo.json").write_text('{"repo_id": "x", "domain": "code"}')
548 (tmp_path / ".env").write_text("DB_PASSWORD=secret\n")
549 (tmp_path / ".env.local").write_text("DB_PASSWORD=local\n")
550
551 manifest = walk_workdir(tmp_path)
552 assert ".env" not in manifest
553 assert ".env.local" not in manifest
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago