gabriel / muse public
test_branch_intent_created_by.py python
617 lines 24.5 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago
1 """TDD tests for two new ``muse branch`` features.
2
3 Feature 1 — Branch intent + resumable
4 --------------------------------------
5 ``muse branch <name> [--intent TEXT] [--resumable]``
6
7 - Stores ``intent`` and ``resumable`` in ``.muse/config.toml`` under
8 ``[branch."<name>"]`` on create.
9 - Surfaces both fields in ``branch --json`` listing output.
10 - ``muse branch --resumable`` filters the listing to resumable branches only.
11
12 Feature 2 — created_by from tip commit
13 ---------------------------------------
14 ``branch --json`` surfaces ``created_by`` (the ``agent_id`` from the tip
15 commit's :class:`CommitRecord`) on every listing entry. Falls back to
16 ``""`` when the branch has no commits or the commit has no agent attribution.
17
18 Test categories
19 ---------------
20 - unit : config.py helpers (write_branch_meta, read_branch_meta)
21 - integration : parser flags, config.toml round-trip, listing JSON schema
22 - e2e : full CLI round-trips via CliRunner
23 - security : intent injection (ANSI, newlines, TOML metacharacters)
24 - data_integrity: resumable flag, intent survive save → list cycle
25 - performance : listing 50 branches with intent under 1 s
26 """
27
28 from __future__ import annotations
29
30 import json
31 import os
32 import pathlib
33 import time
34 import tomllib
35
36 import pytest
37
38 from tests.cli_test_helper import CliRunner, InvokeResult
39 from muse.core.store import get_head_commit_id
40
41 runner = CliRunner()
42
43
44 # ---------------------------------------------------------------------------
45 # Helpers
46 # ---------------------------------------------------------------------------
47
48
49 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
50 saved = os.getcwd()
51 try:
52 os.chdir(repo)
53 return runner.invoke(None, args)
54 finally:
55 os.chdir(saved)
56
57
58 def _branch(repo: pathlib.Path, *extra: str) -> InvokeResult:
59 return _invoke(repo, ["branch", *extra])
60
61
62 def _commit(repo: pathlib.Path, msg: str = "commit") -> InvokeResult:
63 return _invoke(repo, ["commit", "-m", msg])
64
65
66 def _config(repo: pathlib.Path) -> dict:
67 p = repo / ".muse" / "config.toml"
68 if not p.exists():
69 return {}
70 with p.open("rb") as f:
71 return tomllib.load(f)
72
73
74 @pytest.fixture()
75 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
76 saved = os.getcwd()
77 try:
78 os.chdir(tmp_path)
79 runner.invoke(None, ["init"])
80 finally:
81 os.chdir(saved)
82 (tmp_path / "a.py").write_text("x = 1\n")
83 _commit(tmp_path, "initial")
84 return tmp_path
85
86
87 @pytest.fixture()
88 def agent_repo(tmp_path: pathlib.Path) -> pathlib.Path:
89 """Repo with an agent-attributed commit on main."""
90 saved = os.getcwd()
91 try:
92 os.chdir(tmp_path)
93 runner.invoke(None, ["init"])
94 finally:
95 os.chdir(saved)
96 (tmp_path / "a.py").write_text("x = 1\n")
97 _invoke(tmp_path, [
98 "commit", "-m", "agent commit",
99 "--agent-id", "claude-code",
100 "--model-id", "claude-sonnet-4-6",
101 ])
102 return tmp_path
103
104
105 # ===========================================================================
106 # Unit: config.py helpers
107 # ===========================================================================
108
109
110 class TestWriteBranchMeta:
111 """write_branch_meta persists intent + resumable to config.toml."""
112
113 def test_writes_intent_to_config(self, repo: pathlib.Path) -> None:
114 from muse.cli.config import write_branch_meta
115 write_branch_meta(repo, "feat/x", intent="refactor auth")
116 data = _config(repo)
117 assert data["branch"]["feat/x"]["intent"] == "refactor auth"
118
119 def test_writes_resumable_true(self, repo: pathlib.Path) -> None:
120 from muse.cli.config import write_branch_meta
121 write_branch_meta(repo, "task/y", resumable=True)
122 data = _config(repo)
123 assert data["branch"]["task/y"]["resumable"] is True
124
125 def test_writes_resumable_false(self, repo: pathlib.Path) -> None:
126 from muse.cli.config import write_branch_meta
127 write_branch_meta(repo, "task/z", resumable=False)
128 data = _config(repo)
129 assert data["branch"]["task/z"]["resumable"] is False
130
131 def test_writes_both_fields(self, repo: pathlib.Path) -> None:
132 from muse.cli.config import write_branch_meta
133 write_branch_meta(repo, "feat/both", intent="doing X", resumable=True)
134 data = _config(repo)
135 sec = data["branch"]["feat/both"]
136 assert sec["intent"] == "doing X"
137 assert sec["resumable"] is True
138
139 def test_does_not_clobber_upstream_fields(self, repo: pathlib.Path) -> None:
140 """Existing remote/merge keys must survive a write_branch_meta call."""
141 p = repo / ".muse" / "config.toml"
142 p.write_text(
143 '[branch."main"]\nremote = "origin"\nmerge = "refs/heads/main"\n'
144 )
145 from muse.cli.config import write_branch_meta
146 write_branch_meta(repo, "main", intent="track origin")
147 data = _config(repo)
148 sec = data["branch"]["main"]
149 assert sec.get("remote") == "origin"
150 assert sec.get("merge") == "refs/heads/main"
151 assert sec.get("intent") == "track origin"
152
153 def test_updates_existing_entry(self, repo: pathlib.Path) -> None:
154 from muse.cli.config import write_branch_meta
155 write_branch_meta(repo, "feat/up", intent="first intent", resumable=False)
156 write_branch_meta(repo, "feat/up", intent="updated intent", resumable=True)
157 data = _config(repo)
158 sec = data["branch"]["feat/up"]
159 assert sec["intent"] == "updated intent"
160 assert sec["resumable"] is True
161
162 def test_multiple_branches_independent(self, repo: pathlib.Path) -> None:
163 from muse.cli.config import write_branch_meta
164 write_branch_meta(repo, "feat/a", intent="alpha")
165 write_branch_meta(repo, "feat/b", intent="beta", resumable=True)
166 data = _config(repo)
167 assert data["branch"]["feat/a"]["intent"] == "alpha"
168 assert "resumable" not in data["branch"]["feat/a"]
169 assert data["branch"]["feat/b"]["intent"] == "beta"
170 assert data["branch"]["feat/b"]["resumable"] is True
171
172 def test_creates_config_file_if_absent(self, repo: pathlib.Path) -> None:
173 p = repo / ".muse" / "config.toml"
174 p.unlink(missing_ok=True)
175 from muse.cli.config import write_branch_meta
176 write_branch_meta(repo, "new-branch", intent="fresh")
177 assert p.exists()
178 data = _config(repo)
179 assert data["branch"]["new-branch"]["intent"] == "fresh"
180
181
182 class TestReadBranchMeta:
183 """read_branch_meta returns the stored dict (or empty) for a branch."""
184
185 def test_returns_intent_and_resumable(self, repo: pathlib.Path) -> None:
186 from muse.cli.config import write_branch_meta, read_branch_meta
187 write_branch_meta(repo, "feat/r", intent="do X", resumable=True)
188 meta = read_branch_meta(repo, "feat/r")
189 assert meta.get("intent") == "do X"
190 assert meta.get("resumable") is True
191
192 def test_returns_empty_for_unknown_branch(self, repo: pathlib.Path) -> None:
193 from muse.cli.config import read_branch_meta
194 assert read_branch_meta(repo, "nonexistent") == {}
195
196 def test_returns_empty_when_no_config(self, repo: pathlib.Path) -> None:
197 from muse.cli.config import read_branch_meta
198 (repo / ".muse" / "config.toml").unlink(missing_ok=True)
199 assert read_branch_meta(repo, "main") == {}
200
201
202 # ===========================================================================
203 # Unit: parser flags
204 # ===========================================================================
205
206
207 class TestParserFlags:
208 def _parse(self, *args: str) -> object:
209 import argparse
210 from muse.cli.commands.branch import register
211 p = argparse.ArgumentParser()
212 sub = p.add_subparsers()
213 register(sub)
214 return p.parse_args(["branch", *args])
215
216 def test_intent_flag(self) -> None:
217 ns = self._parse("new-branch", "--intent", "refactor the thing")
218 assert ns.intent == "refactor the thing"
219
220 def test_intent_default_none(self) -> None:
221 ns = self._parse("new-branch")
222 assert ns.intent is None
223
224 def test_resumable_flag(self) -> None:
225 ns = self._parse("new-branch", "--resumable")
226 assert ns.resumable is True
227
228 def test_resumable_default_false(self) -> None:
229 ns = self._parse("new-branch")
230 assert ns.resumable is False
231
232 def test_resumable_filter_flag(self) -> None:
233 ns = self._parse("--resumable")
234 assert ns.resumable is True
235
236
237 # ===========================================================================
238 # Integration: --intent / --resumable on create
239 # ===========================================================================
240
241
242 class TestCreateWithIntent:
243 def test_create_with_intent_exits_0(self, repo: pathlib.Path) -> None:
244 result = _branch(repo, "feat/x", "--intent", "do the thing")
245 assert result.exit_code == 0
246
247 def test_create_stores_intent_in_config(self, repo: pathlib.Path) -> None:
248 _branch(repo, "feat/config-test", "--intent", "store me")
249 data = _config(repo)
250 assert data["branch"]["feat/config-test"]["intent"] == "store me"
251
252 def test_create_stores_resumable_in_config(self, repo: pathlib.Path) -> None:
253 _branch(repo, "feat/res", "--resumable")
254 data = _config(repo)
255 assert data["branch"]["feat/res"]["resumable"] is True
256
257 def test_create_without_intent_no_config_entry(self, repo: pathlib.Path) -> None:
258 _branch(repo, "feat/plain")
259 data = _config(repo)
260 branch_sec = data.get("branch", {})
261 assert "feat/plain" not in branch_sec
262
263 def test_create_json_includes_intent(self, repo: pathlib.Path) -> None:
264 result = _branch(repo, "feat/j", "--intent", "json intent", "--json")
265 assert result.exit_code == 0
266 data = json.loads(result.output)
267 assert data.get("intent") == "json intent"
268
269 def test_create_json_includes_resumable(self, repo: pathlib.Path) -> None:
270 result = _branch(repo, "feat/jr", "--resumable", "--json")
271 data = json.loads(result.output)
272 assert data.get("resumable") is True
273
274 def test_create_json_resumable_false_when_not_set(self, repo: pathlib.Path) -> None:
275 result = _branch(repo, "feat/nores", "--json")
276 data = json.loads(result.output)
277 assert data.get("resumable") is False
278
279
280 # ===========================================================================
281 # Integration: listing JSON includes intent, resumable, created_by
282 # ===========================================================================
283
284
285 class TestListJsonNewFields:
286 def test_list_json_has_intent_field(self, repo: pathlib.Path) -> None:
287 _branch(repo, "feat/listed", "--intent", "listed intent")
288 result = _branch(repo, "--json")
289 data = json.loads(result.output)
290 entry = next(b for b in data if b["name"] == "feat/listed")
291 assert "intent" in entry
292 assert entry["intent"] == "listed intent"
293
294 def test_list_json_intent_null_for_plain_branch(self, repo: pathlib.Path) -> None:
295 _branch(repo, "feat/no-intent")
296 result = _branch(repo, "--json")
297 data = json.loads(result.output)
298 entry = next(b for b in data if b["name"] == "feat/no-intent")
299 assert entry.get("intent") is None
300
301 def test_list_json_has_resumable_field(self, repo: pathlib.Path) -> None:
302 _branch(repo, "feat/reslist", "--resumable")
303 result = _branch(repo, "--json")
304 data = json.loads(result.output)
305 entry = next(b for b in data if b["name"] == "feat/reslist")
306 assert "resumable" in entry
307 assert entry["resumable"] is True
308
309 def test_list_json_resumable_false_for_plain_branch(self, repo: pathlib.Path) -> None:
310 result = _branch(repo, "--json")
311 data = json.loads(result.output)
312 main = next(b for b in data if b["name"] == "main")
313 assert main.get("resumable") is False
314
315 def test_list_json_has_created_by_field(self, repo: pathlib.Path) -> None:
316 result = _branch(repo, "--json")
317 data = json.loads(result.output)
318 assert "created_by" in data[0]
319
320 def test_list_json_created_by_from_agent_commit(
321 self, agent_repo: pathlib.Path
322 ) -> None:
323 result = _branch(agent_repo, "--json")
324 data = json.loads(result.output)
325 main = next(b for b in data if b["name"] == "main")
326 assert main["created_by"] == "claude-code"
327
328 def test_list_json_created_by_empty_for_human_commit(
329 self, repo: pathlib.Path
330 ) -> None:
331 result = _branch(repo, "--json")
332 data = json.loads(result.output)
333 main = next(b for b in data if b["name"] == "main")
334 # Human commit has no agent_id — empty string or null
335 assert main["created_by"] in ("", None)
336
337 def test_list_json_created_by_empty_for_empty_branch(
338 self, repo: pathlib.Path
339 ) -> None:
340 (repo / ".muse" / "refs" / "heads" / "empty").write_text("")
341 result = _branch(repo, "--json")
342 data = json.loads(result.output)
343 entry = next(b for b in data if b["name"] == "empty")
344 assert entry["created_by"] in ("", None)
345
346 def test_schema_complete(self, repo: pathlib.Path) -> None:
347 """All new fields must appear in the listing schema."""
348 result = _branch(repo, "--json")
349 data = json.loads(result.output)
350 required = {"name", "current", "commit_id", "committed_at",
351 "last_message", "upstream", "intent", "resumable", "created_by"}
352 missing = required - set(data[0].keys())
353 assert not missing, f"branch --json missing fields: {missing}"
354
355
356 # ===========================================================================
357 # E2E: --resumable listing filter
358 # ===========================================================================
359
360
361 class TestResumableFilter:
362 def test_resumable_filter_shows_only_resumable(self, repo: pathlib.Path) -> None:
363 _branch(repo, "task/resumable-1", "--resumable")
364 _branch(repo, "task/resumable-2", "--resumable")
365 _branch(repo, "task/not-resumable")
366 result = _branch(repo, "--resumable", "--json")
367 assert result.exit_code == 0
368 data = json.loads(result.output)
369 names = [b["name"] for b in data]
370 assert "task/resumable-1" in names
371 assert "task/resumable-2" in names
372 assert "task/not-resumable" not in names
373 assert "main" not in names
374
375 def test_resumable_filter_empty_when_none(self, repo: pathlib.Path) -> None:
376 result = _branch(repo, "--resumable", "--json")
377 assert result.exit_code == 0
378 data = json.loads(result.output)
379 assert data == []
380
381 def test_resumable_filter_text_output(self, repo: pathlib.Path) -> None:
382 _branch(repo, "task/res", "--resumable")
383 result = _branch(repo, "--resumable")
384 assert result.exit_code == 0
385 assert "task/res" in result.output
386
387 def test_resumable_filter_all_resumable_returned(self, repo: pathlib.Path) -> None:
388 for i in range(5):
389 _branch(repo, f"task/r-{i}", "--resumable")
390 result = _branch(repo, "--resumable", "--json")
391 data = json.loads(result.output)
392 assert len(data) == 5
393
394 def test_resumable_combined_with_merged_filter(self, repo: pathlib.Path) -> None:
395 """--resumable and --merged can be combined."""
396 _branch(repo, "task/merged-resumable", "--resumable")
397 result = _branch(repo, "--resumable", "--merged", "--json")
398 assert result.exit_code == 0
399 data = json.loads(result.output)
400 names = [b["name"] for b in data]
401 # task/merged-resumable shares HEAD with main, so it's merged
402 assert "task/merged-resumable" in names
403
404
405 # ===========================================================================
406 # E2E: full round-trips
407 # ===========================================================================
408
409
410 class TestE2eRoundTrips:
411 def test_intent_survives_list_cycle(self, repo: pathlib.Path) -> None:
412 _branch(repo, "feat/rt", "--intent", "round-trip test", "--resumable")
413 result = _branch(repo, "--json")
414 data = json.loads(result.output)
415 entry = next(b for b in data if b["name"] == "feat/rt")
416 assert entry["intent"] == "round-trip test"
417 assert entry["resumable"] is True
418
419 def test_created_by_survives_new_branch_on_agent_repo(
420 self, agent_repo: pathlib.Path
421 ) -> None:
422 _branch(agent_repo, "child-branch")
423 result = _branch(agent_repo, "--json")
424 data = json.loads(result.output)
425 # child-branch points at same commit as main
426 child = next(b for b in data if b["name"] == "child-branch")
427 assert child["created_by"] == "claude-code"
428
429 def test_create_intent_resumable_json_schema(self, repo: pathlib.Path) -> None:
430 result = _branch(repo, "feat/full", "--intent", "full schema", "--resumable", "--json")
431 data = json.loads(result.output)
432 assert data["action"] == "created"
433 assert data["intent"] == "full schema"
434 assert data["resumable"] is True
435 assert "branch" in data
436 assert "commit_id" in data
437
438 def test_resumable_filter_with_r_flag(self, repo: pathlib.Path) -> None:
439 """--resumable must not conflict with -r (remote-tracking) flag."""
440 _branch(repo, "task/local-res", "--resumable")
441 # -r with no remotes returns empty; should not crash
442 result = _branch(repo, "-r", "--resumable", "--json")
443 assert result.exit_code == 0
444 assert json.loads(result.output) == []
445
446
447 # ===========================================================================
448 # Security: intent injection
449 # ===========================================================================
450
451
452 class TestIntentSecurity:
453 def _has_ansi(self, s: str) -> bool:
454 return "\x1b[" in s
455
456 def test_ansi_in_intent_stripped_from_output(self, repo: pathlib.Path) -> None:
457 _branch(repo, "sec/ansi", "--intent", "\x1b[31mevil\x1b[0m")
458 result = _branch(repo, "--json")
459 data = json.loads(result.output)
460 entry = next(b for b in data if b["name"] == "sec/ansi")
461 assert not self._has_ansi(str(entry.get("intent", "")))
462
463 def test_newline_in_intent_escaped_in_toml(self, repo: pathlib.Path) -> None:
464 """Intent with newline must not break TOML file structure."""
465 _branch(repo, "sec/nl", "--intent", "line1\nline2")
466 # Config file must still be parseable
467 data = _config(repo)
468 assert isinstance(data, dict)
469
470 def test_toml_metachar_in_intent_safe(self, repo: pathlib.Path) -> None:
471 """TOML-special chars in intent must not allow section injection."""
472 _branch(repo, "sec/toml", '--intent', '[evil]\nkey = "injected"')
473 data = _config(repo)
474 # No top-level 'evil' section should have been injected
475 assert "evil" not in data
476
477 def test_intent_truncated_to_reasonable_length(self, repo: pathlib.Path) -> None:
478 """Very long intent must not crash or produce a corrupt config."""
479 long_intent = "x" * 10_000
480 result = _branch(repo, "sec/long", "--intent", long_intent)
481 assert result.exit_code == 0
482 data = _config(repo)
483 stored = data.get("branch", {}).get("sec/long", {}).get("intent", "")
484 assert isinstance(stored, str)
485
486
487 # ===========================================================================
488 # Data integrity
489 # ===========================================================================
490
491
492 class TestDataIntegrity:
493 def test_intent_not_lost_on_second_branch_create(self, repo: pathlib.Path) -> None:
494 """Creating a second branch must not overwrite the first's intent."""
495 _branch(repo, "feat/first", "--intent", "first intent")
496 _branch(repo, "feat/second", "--intent", "second intent")
497 data = _config(repo)
498 assert data["branch"]["feat/first"]["intent"] == "first intent"
499 assert data["branch"]["feat/second"]["intent"] == "second intent"
500
501 def test_resumable_preserved_across_other_branch_operations(
502 self, repo: pathlib.Path
503 ) -> None:
504 _branch(repo, "task/keep", "--resumable")
505 _branch(repo, "task/other", "--intent", "unrelated")
506 data = _config(repo)
507 assert data["branch"]["task/keep"]["resumable"] is True
508
509 def test_config_toml_valid_toml_after_write(self, repo: pathlib.Path) -> None:
510 _branch(repo, "feat/valid", "--intent", 'quotes "and" stuff', "--resumable")
511 # tomllib.load must succeed
512 p = repo / ".muse" / "config.toml"
513 with p.open("rb") as f:
514 parsed = tomllib.load(f)
515 assert isinstance(parsed, dict)
516
517
518 # ===========================================================================
519 # Performance
520 # ===========================================================================
521
522
523 class TestPerformance:
524 def test_list_50_branches_with_intent_under_1s(self, repo: pathlib.Path) -> None:
525 for i in range(50):
526 _branch(repo, f"perf/task-{i:03d}", "--intent", f"task {i}", "--resumable")
527
528 start = time.monotonic()
529 result = _branch(repo, "--json")
530 elapsed = time.monotonic() - start
531
532 assert result.exit_code == 0
533 data = json.loads(result.output)
534 assert len(data) == 51 # main + 50
535 assert elapsed < 1.0, f"listing 51 branches with intent took {elapsed:.2f}s"
536
537 def test_resumable_filter_50_branches_under_500ms(
538 self, repo: pathlib.Path
539 ) -> None:
540 for i in range(50):
541 _branch(repo, f"filter/task-{i:03d}", "--resumable")
542
543 start = time.monotonic()
544 result = _branch(repo, "--resumable", "--json")
545 elapsed = time.monotonic() - start
546
547 assert result.exit_code == 0
548 data = json.loads(result.output)
549 assert len(data) == 50
550 assert elapsed < 0.5, f"--resumable filter on 50 branches took {elapsed:.2f}s"
551
552
553 # ---------------------------------------------------------------------------
554 # Metadata update on existing branch
555 # ---------------------------------------------------------------------------
556
557
558 class TestBranchMetaUpdate:
559 """--intent / --resumable on an already-existing branch update metadata."""
560
561 def test_set_intent_on_existing_branch(self, repo: pathlib.Path) -> None:
562 _branch(repo, "existing")
563 result = _branch(repo, "existing", "--intent", "added later")
564 assert result.exit_code == 0
565
566 def test_update_action_in_json(self, repo: pathlib.Path) -> None:
567 _branch(repo, "upd")
568 result = _branch(repo, "upd", "--intent", "my intent", "--json")
569 assert result.exit_code == 0
570 data = json.loads(result.output)
571 assert data["action"] == "updated"
572 assert data["branch"] == "upd"
573 assert data["intent"] == "my intent"
574
575 def test_intent_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
576 _branch(repo, "later")
577 _branch(repo, "later", "--intent", "set after creation")
578 listing = json.loads(_branch(repo, "--json").output)
579 entry = next(e for e in listing if e["name"] == "later")
580 assert entry["intent"] == "set after creation"
581
582 def test_set_resumable_on_existing_branch(self, repo: pathlib.Path) -> None:
583 _branch(repo, "checkpoint")
584 result = _branch(repo, "checkpoint", "--resumable", "--json")
585 assert result.exit_code == 0
586 data = json.loads(result.output)
587 assert data["resumable"] is True
588
589 def test_resumable_visible_in_listing_after_update(self, repo: pathlib.Path) -> None:
590 _branch(repo, "chkpt2")
591 _branch(repo, "chkpt2", "--resumable")
592 listing = json.loads(_branch(repo, "--json").output)
593 entry = next(e for e in listing if e["name"] == "chkpt2")
594 assert entry["resumable"] is True
595
596 def test_update_does_not_overwrite_unspecified_fields(
597 self, repo: pathlib.Path
598 ) -> None:
599 """Setting resumable later must not wipe a previously stored intent."""
600 _branch(repo, "preserve", "--intent", "keep me")
601 _branch(repo, "preserve", "--resumable")
602 listing = json.loads(_branch(repo, "--json").output)
603 entry = next(e for e in listing if e["name"] == "preserve")
604 assert entry["intent"] == "keep me"
605 assert entry["resumable"] is True
606
607 def test_update_with_start_point_still_errors(self, repo: pathlib.Path) -> None:
608 """Passing a start_point to an existing branch is still an error."""
609 _branch(repo, "existing2")
610 result = _branch(repo, "existing2", "main", "--intent", "x")
611 assert result.exit_code != 0
612
613 def test_no_meta_flags_still_errors_on_existing(self, repo: pathlib.Path) -> None:
614 """Plain `muse branch <existing>` (no --intent/--resumable) still errors."""
615 _branch(repo, "plain")
616 result = _branch(repo, "plain")
617 assert result.exit_code != 0
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 139 days ago