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