gabriel / muse public
worktree.py python
728 lines 26.8 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """``muse worktree`` — manage multiple simultaneous branch checkouts.
2
3 Worktrees let you work on multiple branches at once without shelving or
4 switching — each worktree is an independent working directory, but they all
5 share the same ``.muse/`` object store.
6
7 This is especially powerful for agents: one agent per worktree, each
8 autonomously developing a feature on its own branch, with zero interference.
9 Use ``--json`` on any subcommand for machine-readable output.
10
11 Subcommands::
12
13 muse worktree add <name> <branch> [--path PATH] [-b NEW_BRANCH] [--json]
14 muse worktree list [--json]
15 muse worktree prune [--dry-run] [--json]
16 muse worktree remove <name> [--force] [--json]
17 muse worktree repair [--json]
18 muse worktree status <name> [--json]
19
20 Layout::
21
22 myproject/ ← main worktree (holds .muse/)
23 myproject-feat-audio/ ← linked worktree for feat/audio
24
25 Exit codes::
26
27 0 — success
28 1 — user error (invalid name/branch, worktree not found, path conflict)
29 2 — internal error
30 """
31
32 from __future__ import annotations
33
34 import argparse
35 import json
36 import logging
37 import pathlib
38 import sys
39 from typing import TypedDict
40
41 from muse.core._types import short_id
42 from muse.core.paths import ref_path as _ref_path
43 from muse.core.envelope import EnvelopeJson, make_envelope
44 from muse.core.errors import ExitCode
45 from muse.core.repo import require_repo
46 from muse.core.store import get_head_commit_id, write_text_atomic
47 from muse.core.timing import start_timer
48 from muse.core.validation import sanitize_display, validate_branch_name
49 from muse.core.worktree import (
50 WorktreeInfo,
51 WorktreeStatusResult,
52 add_worktree,
53 get_worktree_status,
54 list_worktrees,
55 prune_worktrees,
56 remove_worktree,
57 repair_worktree_pointers,
58 )
59
60 logger = logging.getLogger(__name__)
61
62
63 # ---------------------------------------------------------------------------
64 # Typed JSON schemas
65 # ---------------------------------------------------------------------------
66
67
68 class _WorktreeAddJson(EnvelopeJson):
69 """JSON output of ``muse worktree add --json``."""
70
71 name: str
72 branch: str
73 path: str
74 head_commit: str | None
75
76
77 class _WorktreeListEntryJson(TypedDict):
78 """One entry in the worktrees array from ``muse worktree list --json``."""
79
80 name: str
81 branch: str
82 path: str
83 head_commit: str | None
84 is_main: bool
85
86
87 class _WorktreeListJson(EnvelopeJson):
88 """JSON envelope from ``muse worktree list --json``."""
89
90 worktrees: list[_WorktreeListEntryJson]
91
92
93 class _WorktreeStatusJson(EnvelopeJson):
94 """JSON output of ``muse worktree status --json``."""
95
96 name: str
97 branch: str
98 path: str
99 head_commit: str | None
100 present: bool
101 is_main: bool
102
103
104 class _WorktreeRemoveJson(EnvelopeJson):
105 """JSON output of ``muse worktree remove --json``."""
106
107 name: str
108 status: str
109
110
111 class _WorktreePruneJson(EnvelopeJson):
112 """JSON output of ``muse worktree prune --json``."""
113
114 pruned: list[str]
115 count: int
116 dry_run: bool
117
118
119 class _WorktreeRepairJson(EnvelopeJson):
120 """JSON output of ``muse worktree repair --json``."""
121
122 repaired: list[str]
123
124
125 # ---------------------------------------------------------------------------
126 # Internal helpers
127 # ---------------------------------------------------------------------------
128
129
130 def _fmt_info(wt: WorktreeInfo) -> str:
131 """Format a worktree row for human-readable ``list`` output."""
132 prefix = "* " if wt.is_main else " "
133 head = short_id(wt.head_commit) if wt.head_commit else "(no commits)"
134 return (
135 f"{prefix}{sanitize_display(wt.name):<24} "
136 f"{sanitize_display(wt.branch):<30} "
137 f"{head} "
138 f"{sanitize_display(str(wt.path))}"
139 )
140
141
142 def _info_to_json(wt: WorktreeInfo) -> _WorktreeListEntryJson:
143 return _WorktreeListEntryJson(
144 name=sanitize_display(wt.name),
145 branch=sanitize_display(wt.branch),
146 path=str(wt.path),
147 head_commit=wt.head_commit,
148 is_main=wt.is_main,
149 )
150
151
152 # ---------------------------------------------------------------------------
153 # Subcommand handlers
154 # ---------------------------------------------------------------------------
155
156
157 def run_worktree_add(args: argparse.Namespace) -> None:
158 """Create a new linked worktree checked out at a branch.
159
160 Places the worktree at ``<repo-parent>/<repo-name>-<name>`` by default, or
161 at ``--path PATH`` when specified. With ``-b NEW_BRANCH``, creates a new
162 branch from the start-point BRANCH before checking out.
163
164 Agent quickstart::
165
166 muse worktree add feat-x feat/x --json
167 muse worktree add my-wt main -b feat/new --json
168 muse worktree add custom main --path /tmp/my-wt --json
169
170 JSON fields::
171
172 name Worktree name.
173 branch Checked-out branch.
174 path Absolute path of the worktree directory.
175 head_commit HEAD commit ID, or null if branch has no commits.
176 muse_version Muse release that produced this output.
177 schema Envelope schema version (int).
178 exit_code 0 success, 1 error.
179 duration_ms Wall-clock milliseconds for the command.
180 timestamp ISO-8601 UTC timestamp of command completion.
181 warnings List of non-fatal advisory messages.
182
183 Exit codes::
184
185 0 Success.
186 1 Invalid branch, branch missing, worktree or directory already exists.
187 """
188 name: str = args.name
189 branch: str = args.branch
190 create_branch: str | None = getattr(args, "create_branch", None)
191 worktree_path_arg: str | None = getattr(args, "worktree_path", None)
192 explicit_path: pathlib.Path | None = (
193 pathlib.Path(worktree_path_arg).resolve() if worktree_path_arg else None
194 )
195 json_out: bool = args.json_out
196
197 elapsed = start_timer()
198 root = require_repo()
199
200 # When -b is given, create the new branch from BRANCH (start point) first.
201 checkout_branch = branch
202 if create_branch is not None:
203 try:
204 validate_branch_name(create_branch)
205 except ValueError as exc:
206 print(f"❌ Invalid branch name: {sanitize_display(str(exc))}", file=sys.stderr)
207 raise SystemExit(ExitCode.USER_ERROR)
208
209 new_ref = _ref_path(root, create_branch)
210 if new_ref.exists():
211 print(
212 f"❌ Branch '{sanitize_display(create_branch)}' already exists.",
213 file=sys.stderr,
214 )
215 raise SystemExit(ExitCode.USER_ERROR)
216
217 start_commit = get_head_commit_id(root, branch)
218 if start_commit is None:
219 print(
220 f"❌ Start-point branch '{sanitize_display(branch)}' has no commits.",
221 file=sys.stderr,
222 )
223 raise SystemExit(ExitCode.USER_ERROR)
224
225 new_ref.parent.mkdir(parents=True, exist_ok=True)
226 write_text_atomic(new_ref, f"{start_commit}\n")
227 checkout_branch = create_branch
228
229 try:
230 wt_path = add_worktree(root, name, checkout_branch, path=explicit_path)
231 except ValueError as exc:
232 print(f"❌ {exc}", file=sys.stderr)
233 raise SystemExit(ExitCode.USER_ERROR)
234
235 if json_out:
236 print(json.dumps(_WorktreeAddJson(
237 **make_envelope(elapsed),
238 name=name,
239 branch=checkout_branch,
240 path=str(wt_path),
241 head_commit=get_head_commit_id(root, checkout_branch),
242 )))
243 else:
244 print(f"✅ Worktree '{sanitize_display(name)}' created at {sanitize_display(str(wt_path))}")
245 print(f" Branch: {sanitize_display(checkout_branch)}")
246
247
248 def run_worktree_list(args: argparse.Namespace) -> None:
249 """List all worktrees (main + linked).
250
251 Main worktree is always first; linked worktrees follow in lexicographic
252 order of name. An empty linked worktree list is still success.
253
254 Agent quickstart::
255
256 muse worktree list --json
257
258 JSON fields::
259
260 worktrees List of {name, branch, path, head_commit, is_main}.
261 muse_version Muse release that produced this output.
262 schema Envelope schema version (int).
263 exit_code Always 0.
264 duration_ms Wall-clock milliseconds for the command.
265 timestamp ISO-8601 UTC timestamp of command completion.
266 warnings List of non-fatal advisory messages.
267
268 Exit codes::
269
270 0 Success (even when no linked worktrees exist).
271 """
272 json_out: bool = args.json_out
273
274 elapsed = start_timer()
275 root = require_repo()
276 worktrees = list_worktrees(root)
277
278 if json_out:
279 print(json.dumps(_WorktreeListJson(
280 **make_envelope(elapsed),
281 worktrees=[_info_to_json(wt) for wt in worktrees],
282 )))
283 return
284
285 if not worktrees:
286 print("No worktrees.")
287 return
288 header = f"{' name':<26} {'branch':<30} {'HEAD':12} path"
289 print(header)
290 print("-" * len(header))
291 for wt in worktrees:
292 print(_fmt_info(wt))
293
294
295 def run_worktree_status(args: argparse.Namespace) -> None:
296 """Show the status of a single worktree.
297
298 Reports the branch, HEAD commit, and whether the working directory is
299 present on disk. Useful for agents checking whether a peer's worktree
300 is still active.
301
302 Agent quickstart::
303
304 muse worktree status feat-x --json
305
306 JSON fields::
307
308 name Worktree name.
309 branch Checked-out branch.
310 path Absolute path of the worktree directory.
311 head_commit HEAD commit ID, or null.
312 present true if the working directory exists on disk.
313 is_main true if this is the main worktree.
314 muse_version Muse release that produced this output.
315 schema Envelope schema version (int).
316 exit_code 0 success, 1 not found.
317 duration_ms Wall-clock milliseconds for the command.
318 timestamp ISO-8601 UTC timestamp of command completion.
319 warnings List of non-fatal advisory messages.
320
321 Exit codes::
322
323 0 Success.
324 1 Worktree does not exist, or name is invalid.
325 """
326 name: str = args.name
327 json_out: bool = args.json_out
328
329 elapsed = start_timer()
330 root = require_repo()
331 try:
332 status = get_worktree_status(root, name)
333 except ValueError as exc:
334 print(f"❌ {exc}", file=sys.stderr)
335 raise SystemExit(ExitCode.USER_ERROR)
336
337 if json_out:
338 print(json.dumps(_WorktreeStatusJson(
339 **make_envelope(elapsed),
340 name=sanitize_display(status["name"]),
341 branch=sanitize_display(status["branch"]),
342 path=sanitize_display(status["path"]),
343 head_commit=status["head_commit"],
344 present=status["present"],
345 is_main=status["is_main"],
346 )))
347 return
348
349 present_flag = "✅ present" if status["present"] else "❌ missing"
350 head = short_id(status["head_commit"]) if status["head_commit"] else "(no commits)"
351 main_flag = " [main]" if status["is_main"] else ""
352 print(f"worktree: {sanitize_display(status['name'])}{main_flag}")
353 print(f"branch: {sanitize_display(status['branch'])}")
354 print(f"HEAD: {head}")
355 print(f"path: {sanitize_display(status['path'])}")
356 print(f"status: {present_flag}")
357
358
359 def run_worktree_remove(args: argparse.Namespace) -> None:
360 """Remove a linked worktree and its working directory.
361
362 The branch is not deleted — only the worktree directory and its metadata.
363 The main worktree cannot be removed. Removal is refused if the path is a
364 symlink or resolves inside ``.muse/``.
365
366 Agent quickstart::
367
368 muse worktree remove feat-x --json
369 muse worktree remove feat-x --force --json
370
371 JSON fields::
372
373 name Worktree name that was removed.
374 status Always "removed" on success.
375 muse_version Muse release that produced this output.
376 schema Envelope schema version (int).
377 exit_code 0 success, 1 error.
378 duration_ms Wall-clock milliseconds for the command.
379 timestamp ISO-8601 UTC timestamp of command completion.
380 warnings List of non-fatal advisory messages.
381
382 Exit codes::
383
384 0 Success.
385 1 Worktree not found, invalid name, or removal refused.
386 """
387 name: str = args.name
388 force: bool = args.force
389 json_out: bool = args.json_out
390
391 elapsed = start_timer()
392 root = require_repo()
393 try:
394 remove_worktree(root, name, force=force)
395 except ValueError as exc:
396 print(f"❌ {exc}", file=sys.stderr)
397 raise SystemExit(ExitCode.USER_ERROR)
398
399 if json_out:
400 print(json.dumps(_WorktreeRemoveJson(
401 **make_envelope(elapsed),
402 name=sanitize_display(name), status="removed",
403 )))
404 else:
405 print(f"✅ Worktree '{sanitize_display(name)}' removed.")
406
407
408 def run_worktree_prune(args: argparse.Namespace) -> None:
409 """Remove metadata entries for worktrees whose directories no longer exist.
410
411 A worktree is stale when its registered path is absent from the filesystem.
412 Prune deletes the metadata file and HEAD pointer; does not touch branch refs.
413 Use ``--dry-run`` to preview without making changes.
414
415 Agent quickstart::
416
417 muse worktree prune --json
418 muse worktree prune --dry-run --json
419
420 JSON fields::
421
422 pruned List of stale worktree names that were (or would be) pruned.
423 count Number of entries pruned.
424 dry_run true when --dry-run was passed.
425 muse_version Muse release that produced this output.
426 schema Envelope schema version (int).
427 exit_code Always 0.
428 duration_ms Wall-clock milliseconds for the command.
429 timestamp ISO-8601 UTC timestamp of command completion.
430 warnings List of non-fatal advisory messages.
431
432 Exit codes::
433
434 0 Success (even when nothing was pruned).
435 """
436 json_out: bool = args.json_out
437 dry_run: bool = args.dry_run
438
439 elapsed = start_timer()
440 root = require_repo()
441 pruned = prune_worktrees(root, dry_run=dry_run)
442
443 if json_out:
444 print(json.dumps(_WorktreePruneJson(
445 **make_envelope(elapsed),
446 pruned=[sanitize_display(n) for n in pruned],
447 count=len(pruned),
448 dry_run=dry_run,
449 )))
450 return
451
452 if not pruned:
453 print("Nothing to prune.")
454 return
455 prefix = "[dry-run] would prune" if dry_run else "pruned"
456 for name in pruned:
457 print(f" {prefix}: {sanitize_display(name)}")
458 action = "Would prune" if dry_run else "Pruned"
459 print(f"{action} {len(pruned)} stale worktree(s).")
460
461
462 def run_worktree_repair(args: argparse.Namespace) -> None:
463 """Write missing ``.muse`` pointer files for all registered linked worktrees.
464
465 Idempotent — safe to run multiple times. Re-writes the pointer even when
466 it already exists. Worktrees whose directories are absent are skipped
467 (prune them with ``muse worktree prune`` instead).
468
469 Agent quickstart::
470
471 muse worktree repair --json
472
473 JSON fields::
474
475 repaired List of worktree names whose pointers were repaired.
476 muse_version Muse release that produced this output.
477 schema Envelope schema version (int).
478 exit_code Always 0.
479 duration_ms Wall-clock milliseconds for the command.
480 timestamp ISO-8601 UTC timestamp of command completion.
481 warnings List of non-fatal advisory messages.
482
483 Exit codes::
484
485 0 Success (even when nothing needed repair).
486 """
487 json_out: bool = args.json_out
488 elapsed = start_timer()
489 repo_root = require_repo()
490 repaired = repair_worktree_pointers(repo_root)
491 if json_out:
492 print(json.dumps(_WorktreeRepairJson(
493 **make_envelope(elapsed),
494 repaired=[sanitize_display(n) for n in repaired],
495 )))
496 else:
497 if repaired:
498 for name in repaired:
499 print(f" repaired {sanitize_display(name)}")
500 print(f"\n✅ {len(repaired)} worktree(s) repaired.")
501 else:
502 print("✅ All worktrees already have pointer files.")
503
504
505 # ---------------------------------------------------------------------------
506 # Registration
507 # ---------------------------------------------------------------------------
508
509
510 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
511 """Register the ``worktree`` subcommand."""
512 parser = subparsers.add_parser(
513 "worktree",
514 help="Manage multiple simultaneous branch checkouts.",
515 description=__doc__,
516 formatter_class=argparse.RawDescriptionHelpFormatter,
517 )
518 subs = parser.add_subparsers(dest="subcommand", metavar="SUBCOMMAND")
519 subs.required = True
520
521 # ── add ───────────────────────────────────────────────────────────────────
522 add_p = subs.add_parser(
523 "add",
524 help="Create a new linked worktree checked out at a branch.",
525 description=(
526 "Create a new linked worktree checked out at BRANCH.\n\n"
527 "The worktree is placed at <repo-parent>/<repo-name>-NAME by default,\n"
528 "or at --path PATH when specified.\n\n"
529 "Branch creation\n"
530 "---------------\n"
531 " -b NEW_BRANCH Create NEW_BRANCH from BRANCH (start point) and\n"
532 " check it out in the new worktree.\n\n"
533 "Agent quickstart\n"
534 "----------------\n"
535 " muse worktree add feat-x feat/x --json\n"
536 " muse worktree add my-wt main -b feat/new --json\n"
537 " muse worktree add custom main --path /tmp/my-wt --json\n\n"
538 "JSON output schema\n"
539 "------------------\n"
540 ' {"name": "<name>", "branch": "<branch>",\n'
541 ' "path": "<absolute-path>", "head_commit": "<sha256> | null"}\n\n'
542 "Exit codes\n"
543 "----------\n"
544 " 0 — success\n"
545 " 1 — invalid name/branch, branch missing, worktree/dir already exists\n"
546 " 2 — not inside a Muse repository\n"
547 ),
548 formatter_class=argparse.RawDescriptionHelpFormatter,
549 )
550 add_p.add_argument("name", metavar="NAME", help="Worktree name.")
551 add_p.add_argument("branch", metavar="BRANCH", help="Branch to check out.")
552 add_p.add_argument(
553 "--path", metavar="PATH", dest="worktree_path", default=None,
554 help=(
555 "Explicit filesystem path for the worktree directory. "
556 "When omitted, placed at <repo-parent>/<repo-name>-<name>."
557 ),
558 )
559 add_p.add_argument(
560 "-b", "--create-branch", metavar="NEW_BRANCH", dest="create_branch", default=None,
561 help=(
562 "Create NEW_BRANCH from BRANCH (start point) and check out the "
563 "new branch in the worktree."
564 ),
565 )
566 add_p.add_argument(
567 "--json", "-j", action="store_true", dest="json_out", default=False,
568 help="Emit machine-readable JSON to stdout.",
569 )
570 add_p.set_defaults(func=run_worktree_add, json_out=False)
571
572 # ── list ──────────────────────────────────────────────────────────────────
573 list_p = subs.add_parser(
574 "list",
575 help="List all worktrees (main + linked).",
576 description=(
577 "List all worktrees: the main worktree and every linked worktree.\n\n"
578 "The main worktree is always first; linked worktrees follow in\n"
579 "lexicographic order of name. Output is sanitized — ANSI control\n"
580 "sequences are stripped from name, branch, and path.\n\n"
581 "Agent quickstart\n"
582 "----------------\n"
583 " muse worktree list --json\n\n"
584 "JSON output schema (array element)\n"
585 "----------------------------------\n"
586 ' {"name": "<name>", "branch": "<branch>", "path": "<absolute-path>",\n'
587 ' "head_commit": "<sha256> | null", "is_main": true|false}\n\n'
588 "Exit codes\n"
589 "----------\n"
590 " 0 — success (even when no linked worktrees exist)\n"
591 " 2 — not inside a Muse repository\n"
592 ),
593 formatter_class=argparse.RawDescriptionHelpFormatter,
594 )
595 list_p.add_argument(
596 "--json", "-j", action="store_true", dest="json_out", default=False,
597 help="Emit machine-readable JSON array to stdout.",
598 )
599 list_p.set_defaults(func=run_worktree_list, json_out=False)
600
601 # ── prune ─────────────────────────────────────────────────────────────────
602 prune_p = subs.add_parser(
603 "prune",
604 help="Remove metadata entries for missing worktrees.",
605 description=(
606 "Remove metadata for worktrees whose directories no longer exist.\n\n"
607 "A worktree is stale when its registered path is absent from disk.\n"
608 "Prune removes the metadata file and HEAD pointer for each stale\n"
609 "entry; branch refs are never touched.\n\n"
610 "Use --dry-run to preview without making any changes.\n\n"
611 "Agent quickstart\n"
612 "----------------\n"
613 " muse worktree prune --json\n"
614 " muse worktree prune --dry-run --json\n\n"
615 "JSON output schema\n"
616 "------------------\n"
617 ' {"pruned": ["<name>", ...], "count": <int>, "dry_run": true|false}\n\n'
618 "Exit codes\n"
619 "----------\n"
620 " 0 — success (even when nothing was pruned)\n"
621 " 2 — not inside a Muse repository\n"
622 ),
623 formatter_class=argparse.RawDescriptionHelpFormatter,
624 )
625 prune_p.add_argument(
626 "--dry-run", action="store_true", dest="dry_run", default=False,
627 help="Preview what would be pruned without making changes.",
628 )
629 prune_p.add_argument(
630 "--json", "-j", action="store_true", dest="json_out", default=False,
631 help="Emit machine-readable JSON to stdout.",
632 )
633 prune_p.set_defaults(func=run_worktree_prune, json_out=False)
634
635 # ── remove ────────────────────────────────────────────────────────────────
636 remove_p = subs.add_parser(
637 "remove",
638 help="Remove a linked worktree and its working directory.",
639 description=(
640 "Remove a linked worktree: deletes the working directory and its\n"
641 "metadata. The branch is NOT deleted. Already-pushed commits\n"
642 "remain in the shared object store.\n\n"
643 "Safety guards refuse removal when the worktree path is a symlink\n"
644 "or resolves inside .muse/ (would corrupt the repository).\n\n"
645 "Agent quickstart\n"
646 "----------------\n"
647 " muse worktree remove feat-x --json\n\n"
648 "JSON output schema\n"
649 "------------------\n"
650 ' {"name": "<name>", "status": "removed"}\n\n'
651 "Exit codes\n"
652 "----------\n"
653 " 0 — success\n"
654 " 1 — worktree missing, invalid name, or removal refused\n"
655 " 2 — not inside a Muse repository\n"
656 ),
657 formatter_class=argparse.RawDescriptionHelpFormatter,
658 )
659 remove_p.add_argument("name", metavar="NAME", help="Worktree name to remove.")
660 remove_p.add_argument(
661 "--force", action="store_true",
662 help="Force removal (accepted for interface compatibility).",
663 )
664 remove_p.add_argument(
665 "--json", "-j", action="store_true", dest="json_out", default=False,
666 help="Emit machine-readable JSON to stdout.",
667 )
668 remove_p.set_defaults(func=run_worktree_remove, json_out=False)
669
670 # ── repair ────────────────────────────────────────────────────────────────
671 repair_p = subs.add_parser(
672 "repair",
673 help="Write missing .muse pointer files into existing linked worktrees.",
674 description=(
675 "Write (or re-write) the .muse pointer file in every registered\n"
676 "linked worktree whose directory is present on disk.\n\n"
677 "Idempotent — safe to run multiple times. Worktrees whose\n"
678 "directories are absent are skipped (use 'prune' for those).\n\n"
679 "Agent quickstart\n"
680 "----------------\n"
681 " muse worktree repair --json\n\n"
682 "JSON output schema\n"
683 "------------------\n"
684 ' {"repaired": ["<name>", ...]}\n\n'
685 "Exit codes\n"
686 "----------\n"
687 " 0 — success (even when nothing needed repair)\n"
688 " 2 — not inside a Muse repository\n"
689 ),
690 formatter_class=argparse.RawDescriptionHelpFormatter,
691 )
692 repair_p.add_argument(
693 "--json", "-j", action="store_true", dest="json_out", default=False,
694 help="Emit machine-readable JSON to stdout.",
695 )
696 repair_p.set_defaults(func=run_worktree_repair, json_out=False)
697
698 # ── status ────────────────────────────────────────────────────────────────
699 status_p = subs.add_parser(
700 "status",
701 help="Show the status of a single worktree.",
702 description=(
703 "Show branch, HEAD commit, path, and presence of a single worktree.\n\n"
704 "Pass 'main' or '(main)' to query the main worktree. Output fields\n"
705 "are sanitized — ANSI control sequences are stripped.\n\n"
706 "Agent quickstart\n"
707 "----------------\n"
708 " muse worktree status feat-x --json\n"
709 " muse worktree status main --json\n\n"
710 "JSON output schema\n"
711 "------------------\n"
712 ' {"name": "<name>", "branch": "<branch>", "path": "<absolute-path>",\n'
713 ' "head_commit": "<sha256> | null", "present": true|false,\n'
714 ' "is_main": true|false}\n\n'
715 "Exit codes\n"
716 "----------\n"
717 " 0 — success\n"
718 " 1 — worktree does not exist, or name is invalid\n"
719 " 2 — not inside a Muse repository\n"
720 ),
721 formatter_class=argparse.RawDescriptionHelpFormatter,
722 )
723 status_p.add_argument("name", metavar="NAME", help="Worktree name (or 'main' / '(main)').")
724 status_p.add_argument(
725 "--json", "-j", action="store_true", dest="json_out", default=False,
726 help="Emit machine-readable JSON to stdout.",
727 )
728 status_p.set_defaults(func=run_worktree_status, json_out=False)
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago