"""Muse CLI — entry point for the ``muse`` console script. Two-tier command architecture ------------------------------- **Tier 1 — Core + Low-level** (top-level ``muse …``) All VCS commands live here — both human-facing and the machine-readable, JSON-outputting primitives that scripts and agents use. Agents reach every command in one token. **Tier 2 — Semantic** (``muse midi …``, ``muse code …``, ``muse coord …``) Domain-specific commands that interpret multidimensional state. Each sub-namespace is served by the corresponding ``muse/plugins/`` plugin. Top-level commands (alphabetical):: agent annotate archive attributes auth bisect blame branch bundle cat cat-object check check-attr check-ignore check-ref-format checkout cherry-pick clean clone code commit commit-graph commit-tree config conflicts content-grep coord describe diff domain domain-info domains fetch for-each-ref gc hash-object hub init log ls-files ls-remote merge merge-base mist midi name-rev pack-objects migrate path pull push rebase read-commit read-snapshot reflog release remote reset resolve revert rev-parse rm shortlog read show-ref sign snapshot snapshot-diff shelf status symbolic-ref tag unpack-objects update-ref verify verify-object verify-pack workspace worktree Identity & hub fabric:: auth keygen muse auth keygen [--hub HUB] auth register muse auth register [--hub HUB] [--handle NAME] [--agent] auth whoami muse auth whoami [--json] auth logout muse auth logout [--hub HUB] hub connect muse hub connect hub status muse hub status [--json] hub disconnect muse hub disconnect hub ping muse hub ping config show muse config show [--json] config get muse config get config set muse config set config edit muse config edit MIDI domain commands (``muse midi …``, alphabetical) — disabled pending rollout:: agent-map arpeggiate cadence check compare contour density find-phrase harmony humanize instrumentation invert mix motif normalize note-blame note-hotspots note-log notes piano-roll quantize query retrograde rhythm scale shard tempo tension transpose velocity-profile voice-leading Code domain commands (``muse code …``, alphabetical):: add age api-surface blast-risk blame breakage cat checkout-symbol clones code-check code-query codemap compare contract coupling coverage dead deps detect-refactor docs entangle find-symbol gravity grep hotspots impact index invariants languages lineage narrative patch predict query query-history rename reset semantic-cherry-pick semantic-test-coverage stable symbol-log symbols test type velocity Coordination commands (``muse coord …``, alphabetical):: cancel-task claim complete dag enqueue fail-task forecast gc heartbeat intent list plan-merge reconcile release reserve shard sync tasks watch """ import argparse import difflib import os import signal import sys # Restore the default SIGPIPE handler so that piping muse output to commands # that read partial output (head, grep, jq, etc.) exits cleanly instead of # raising BrokenPipeError and printing a Python traceback. This matters most # for agents that routinely pipe muse JSON output to filters. if hasattr(signal, "SIGPIPE"): signal.signal(signal.SIGPIPE, signal.SIG_DFL) from muse.cli.commands import ( age as age_cmd, agent as agent_cmd, agent_config, # agent_map, # midi — disabled until midi rollout annotate, api_surface, apply, apply_patch, archive, # arpeggiate, # midi — disabled until midi rollout attributes, auth, trust, bisect, blame, blast_risk, branch, breakage, bridge, bundle, # cadence, # midi — disabled until midi rollout cat, core_cat, check, checkout, checkout_symbol, cherry_pick, clean, clone, clones, code_check, code_query, code_stage, codemap, commit, compare, config_cmd, conflicts, content_grep, contract, # contour, # midi — disabled until midi rollout coord_gc, coord_sync, core_blame, coupling, coverage, dag as dag_cmd, dead, # density, # midi — disabled until midi rollout deps, describe, detect_refactor, diff, docs_cmd, domain_cmd, domains, entangle, fetch, # find_phrase, # midi — disabled until midi rollout find_symbol, forecast, gc, gravity, grep, harmony, # midi_harmony, # midi — disabled until midi rollout heartbeat_coord, hotspots, hub, # humanize, # midi — disabled until midi rollout impact, index_rebuild, init, # instrumentation, # midi — disabled until midi rollout intent, invariants, # invert, # midi — disabled until midi rollout languages, lineage, list_coord, log, maintenance, merge, merge_base, migrate, migrate_cmd, mist as mist_cmd, mv, # midi_check, # midi — disabled until midi rollout # midi_compare, # midi — disabled until midi rollout # midi_query, # midi — disabled until midi rollout # midi_shard, # midi — disabled until midi rollout # mix, # midi — disabled until midi rollout # motif_detect, # midi — disabled until midi rollout narrative, # note_blame, # midi — disabled until midi rollout # note_hotspots, # midi — disabled until midi rollout # note_log, # midi — disabled until midi rollout # notes, # midi — disabled until midi rollout patch, path_cmd, # piano_roll, # midi — disabled until midi rollout plan_merge, predict, prune, pull, push, # quantize, # midi — disabled until midi rollout query, query_history, range_diff, rebase, reconcile, reflog, release as release_cmd, release_coord, remote, rename, reserve, reset, resolve as resolve_cmd, restore, rm, # retrograde, # midi — disabled until midi rollout revert, # rhythm, # midi — disabled until midi rollout # scale_detect, # midi — disabled until midi rollout semantic_cherry_pick, semantic_test_coverage, shard, shortlog, read, sign, snapshot_cmd, shelf, social as social_cmd, stable, status, switch, symbol_log, symlog as symlog_cmd, symbols, label, tag, task_queue as task_queue_cmd, # tempo, # midi — disabled until midi rollout # tension, # midi — disabled until midi rollout test_cmd, # transpose, # midi — disabled until midi rollout type_cmd, velocity, # velocity_normalize, # midi — disabled until midi rollout # velocity_profile, # midi — disabled until midi rollout verify, # voice_leading, # midi — disabled until midi rollout watch_coord, workspace, worktree, ) from muse.cli.commands import ( apply_patch, cat_object, check_attr, count_objects, check_ignore, check_ref_format, commit_graph, commit_tree, domain_info, for_each_ref, format_patch, hash_object, ls_files, ls_remote, ls_tree, merge_base, merge_tree, name_rev, pack_objects, patch_id, read_commit, read_snapshot, rev_list, rev_parse, show_ref, snapshot_diff, sparse_checkout, symbolic_ref, unpack_objects, update_ref, verify_commit, verify_object, verify_pack, verify_tag, ) def _no_command(parser: argparse.ArgumentParser) -> None: """Print help when no subcommand is provided.""" parser.print_help() raise SystemExit(0) class _MuseArgumentParser(argparse.ArgumentParser): """ArgumentParser subclass that adds 'did you mean?' suggestions on error. When the user mistypes a subcommand (e.g. ``muse comit``), argparse normally prints only "argument COMMAND: invalid choice: 'comit'". This subclass intercepts that error, checks for close matches among the registered subcommand names, and appends a suggestion line so agents and humans can self-correct without consulting the full ``--help`` output. Example output:: muse: error: argument COMMAND: invalid choice: 'comit' Did you mean: commit? """ def error(self, message: str) -> None: # Extract the unknown token from messages like: # "argument COMMAND: invalid choice: 'comit' (choose from ...)" unknown: str | None = None if "invalid choice:" in message: try: start = message.index("'") + 1 end = message.index("'", start) unknown = message[start:end] except ValueError: pass if unknown is not None: choices: list[str] = [] for action in self._actions: if isinstance(action, argparse._SubParsersAction): choices.extend(action.choices.keys()) suggestions = difflib.get_close_matches(unknown, choices, n=3, cutoff=0.6) if suggestions: message = f"{message}\n Did you mean: {', '.join(suggestions)}?" super().error(message) def main(argv: list[str] | None = None) -> None: """Parse arguments and dispatch to the appropriate command handler. Exit codes ---------- 0 Success — command completed normally. 1 Runtime error — e.g. ``-C DIR`` directory does not exist. 2 Argument error — unknown command, missing required argument, or a namespace command (``code``/``midi``/``coord``) was invoked without a subcommand. argparse prints the error before this function raises. Flag processing order --------------------- 1. ``--version`` / ``-V`` — print version and exit 0, no further parsing. 2. ``-C DIR`` — ``os.chdir(DIR)`` before dispatch; exit 1 on ``OSError`` (non-existent or permission-denied). 3. Subcommand dispatch — ``args.func(args)`` called once. No-subcommand behaviour ----------------------- ``muse`` with no subcommand prints the help text and exits 0. ``muse code`` / ``muse midi`` / ``muse coord`` with no subcommand exits 2 (argparse enforces ``subs.required = True``). Calling from tests ------------------ Pass an explicit ``argv`` list to avoid reading ``sys.argv``:: from tests.cli_test_helper import CliRunner result = CliRunner().invoke(None, ["--version"]) assert "muse" in result.output """ parser = _MuseArgumentParser( prog="muse", description="Muse — domain-agnostic version control for multidimensional state.", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "--version", "-V", "-v", action="store_true", dest="show_version", help="Print 'muse ' to stdout and exit 0. " "Agents: parse with output.split()[1] for the bare version string.", ) parser.add_argument( "-C", metavar="DIR", dest="chdir", default=None, help="Change to DIR before running the command (DIR must exist).", ) subparsers = parser.add_subparsers(dest="command", metavar="COMMAND") # ``muse help`` — ergonomic alias for ``muse --help``. # Agents and humans reach for ``help`` by reflex; honour it. subparsers.add_parser("help", help="Show this help message and exit.") # ------------------------------------------------------------------ # All top-level commands — alphabetical order. # All top-level commands are interleaved alphabetically # so that ``muse --help`` presents one sorted list. # ------------------------------------------------------------------ agent_cmd.register(subparsers) agent_config.register(subparsers) annotate.register(subparsers) apply.register(subparsers) # "apply" apply_patch.register(subparsers) # "apply-patch" archive.register(subparsers) attributes.register(subparsers) auth.register(subparsers) bisect.register(subparsers) core_blame.register(subparsers) # "blame" branch.register(subparsers) bridge.register(subparsers) bundle.register(subparsers) core_cat.register(subparsers) # "cat" — file-level cat_object.register(subparsers) # "cat-object" check.register(subparsers) check_attr.register(subparsers) # "check-attr" check_ignore.register(subparsers) # "check-ignore" check_ref_format.register(subparsers) # "check-ref-format" checkout.register(subparsers) cherry_pick.register(subparsers) # "cherry-pick" clean.register(subparsers) clone.register(subparsers) # ── code namespace (muse code …) ────────────────────────────────── code_parser = subparsers.add_parser( "code", help="[Tier 3] Code domain semantic commands — symbol graph, call graph, and provenance.", description="Code domain semantic commands.", ) code_subs = code_parser.add_subparsers(dest="code_command", metavar="CODE_COMMAND") code_subs.required = True code_stage.register_add(code_subs) # "add" age_cmd.register(code_subs) # "age" api_surface.register(code_subs) # "api-surface" blast_risk.register(code_subs) # "blast-risk" blame.register(code_subs) # "blame" breakage.register(code_subs) # "breakage" cat.register(code_subs) # "cat" checkout_symbol.register(code_subs) # "checkout-symbol" clones.register(code_subs) # "clones" code_check.register(code_subs) # "code-check" code_query.register(code_subs) # "code-query" codemap.register(code_subs) # "codemap" compare.register(code_subs) # "compare" contract.register(code_subs) # "contract" coupling.register(code_subs) # "coupling" coverage.register(code_subs) # "coverage" dead.register(subparsers) # "dead" — register wraps in code namespace deps.register(code_subs) # "deps" detect_refactor.register(code_subs) # "detect-refactor" docs_cmd.register(code_subs) # "docs" entangle.register(code_subs) # "entangle" find_symbol.register(code_subs) # "find-symbol" gravity.register(code_subs) # "gravity" grep.register(code_subs) # "grep" hotspots.register(code_subs) # "hotspots" impact.register(code_subs) # "impact" index_rebuild.register(code_subs) # "index" invariants.register(code_subs) # "invariants" languages.register(code_subs) # "languages" lineage.register(code_subs) # "lineage" migrate.register(code_subs) # "migrate" narrative.register(code_subs) # "narrative" patch.register(code_subs) # "patch" predict.register(code_subs) # "predict" query.register(code_subs) # "query" query_history.register(code_subs) # "query-history" rename.register(code_subs) # "rename" code_stage.register_reset(code_subs) # "reset" semantic_cherry_pick.register(code_subs) # "semantic-cherry-pick" semantic_test_coverage.register(code_subs) # "semantic-test-coverage" stable.register(code_subs) # "stable" symbol_log.register(code_subs) # "symbol-log" symbols.register(code_subs) # "symbols" test_cmd.register(code_subs) # "test" type_cmd.register(code_subs) # "type" velocity.register(code_subs) # "velocity" # ───────────────────────────────────────────────────────────────── commit.register(subparsers) commit_graph.register(subparsers) # "commit-graph" commit_tree.register(subparsers) # "commit-tree" config_cmd.register(subparsers) # "config" conflicts.register(subparsers) content_grep.register(subparsers) # "content-grep" count_objects.register(subparsers) # "count-objects" # ── coord namespace (muse coord …) ─────────────────────────────── coord_parser = subparsers.add_parser( "coord", help="[Tier 3] Multi-agent coordination commands — reservations, intent, conflict forecasting.", description="Multi-agent coordination commands.", ) coord_subs = coord_parser.add_subparsers(dest="coord_command", metavar="COORD_COMMAND") coord_subs.required = True task_queue_cmd.register_cancel_task(coord_subs) # "cancel-task" task_queue_cmd.register_claim(coord_subs) # "claim" task_queue_cmd.register_complete(coord_subs) # "complete" dag_cmd.register(coord_subs) # "dag" task_queue_cmd.register_enqueue(coord_subs) # "enqueue" task_queue_cmd.register_fail_task(coord_subs) # "fail-task" forecast.register(coord_subs) # "forecast" coord_gc.register(coord_subs) # "gc" heartbeat_coord.register(coord_subs) # "heartbeat" intent.register(coord_subs) # "intent" list_coord.register(coord_subs) # "list" plan_merge.register(coord_subs) # "plan-merge" reconcile.register(coord_subs) # "reconcile" release_coord.register(coord_subs) # "release" reserve.register(coord_subs) # "reserve" shard.register(coord_subs) # "shard" coord_sync.register(coord_subs) # "sync" task_queue_cmd.register_tasks(coord_subs) # "tasks" watch_coord.register(coord_subs) # "watch" # ───────────────────────────────────────────────────────────────── describe.register(subparsers) diff.register(subparsers) domain_cmd.register(subparsers) # "domain" domain_info.register(subparsers) # "domain-info" domains.register(subparsers) fetch.register(subparsers) for_each_ref.register(subparsers) # "for-each-ref" format_patch.register(subparsers) # "format-patch" gc.register(subparsers) hash_object.register(subparsers) # "hash-object" hub.register(subparsers) init.register(subparsers) log.register(subparsers) ls_files.register(subparsers) # "ls-files" ls_remote.register(subparsers) # "ls-remote" ls_tree.register(subparsers) # "ls-tree" maintenance.register(subparsers) # "maintenance" merge.register(subparsers) migrate_cmd.register(subparsers) # "migrate" merge_base.register(subparsers) # "merge-base" merge_tree.register(subparsers) # "merge-tree" mist_cmd.register(subparsers) # "mist" mv.register(subparsers) harmony.register(subparsers) social_cmd.register(subparsers) # "social" # ── midi namespace (muse midi …) — disabled until midi rollout ─── # midi_parser = subparsers.add_parser( # "midi", # help="[Tier 3] MIDI domain semantic commands.", # description="MIDI domain semantic commands — analysis, transformation, and multi-agent.", # ) # midi_subs = midi_parser.add_subparsers(dest="midi_command", metavar="MIDI_COMMAND") # midi_subs.required = True # # agent_map.register(midi_subs) # "agent-map" # arpeggiate.register(midi_subs) # "arpeggiate" # cadence.register(midi_subs) # "cadence" # midi_check.register(midi_subs) # "check" # midi_compare.register(midi_subs) # "compare" # contour.register(midi_subs) # "contour" # density.register(midi_subs) # "density" # find_phrase.register(midi_subs) # "find-phrase" # harmony.register(midi_subs) # "harmony" # humanize.register(midi_subs) # "humanize" # instrumentation.register(midi_subs) # "instrumentation" # invert.register(midi_subs) # "invert" # mix.register(midi_subs) # "mix" # motif_detect.register(midi_subs) # "motif" # velocity_normalize.register(midi_subs) # "normalize" # note_blame.register(midi_subs) # "note-blame" # note_hotspots.register(midi_subs) # "note-hotspots" # note_log.register(midi_subs) # "note-log" # notes.register(midi_subs) # "notes" # piano_roll.register(midi_subs) # "piano-roll" # quantize.register(midi_subs) # "quantize" # midi_query.register(midi_subs) # "query" # retrograde.register(midi_subs) # "retrograde" # rhythm.register(midi_subs) # "rhythm" # scale_detect.register(midi_subs) # "scale" # midi_shard.register(midi_subs) # "shard" # tempo.register(midi_subs) # "tempo" # tension.register(midi_subs) # "tension" # transpose.register(midi_subs) # "transpose" # velocity_profile.register(midi_subs) # "velocity-profile" # voice_leading.register(midi_subs) # "voice-leading" # ───────────────────────────────────────────────────────────────── name_rev.register(subparsers) # "name-rev" pack_objects.register(subparsers) # "pack-objects" patch_id.register(subparsers) # "patch-id" path_cmd.register(subparsers) # "path" prune.register(subparsers) pull.register(subparsers) push.register(subparsers) range_diff.register(subparsers) # "range-diff" rebase.register(subparsers) read_commit.register(subparsers) # "read-commit" read_snapshot.register(subparsers) # "read-snapshot" reflog.register(subparsers) release_cmd.register(subparsers) # "release" remote.register(subparsers) reset.register(subparsers) resolve_cmd.register(subparsers) # "resolve" restore.register(subparsers) rm.register(subparsers) revert.register(subparsers) rev_list.register(subparsers) # "rev-list" rev_parse.register(subparsers) # "rev-parse" shortlog.register(subparsers) read.register(subparsers) # "read" show_ref.register(subparsers) # "show-ref" sign.register(subparsers) # "sign" snapshot_cmd.register(subparsers) # "snapshot" snapshot_diff.register(subparsers) # "snapshot-diff" sparse_checkout.register(subparsers) # "sparse-checkout" shelf.register(subparsers) status.register(subparsers) switch.register(subparsers) symlog_cmd.register(subparsers) # "symlog" symbolic_ref.register(subparsers) # "symbolic-ref" label.register(subparsers) tag.register(subparsers) trust.register(subparsers) unpack_objects.register(subparsers) # "unpack-objects" update_ref.register(subparsers) # "update-ref" verify.register(subparsers) verify_commit.register(subparsers) # "verify-commit" verify_object.register(subparsers) # "verify-object" verify_pack.register(subparsers) # "verify-pack" verify_tag.register(subparsers) # "verify-tag" workspace.register(subparsers) worktree.register(subparsers) # ------------------------------------------------------------------ # Parse and dispatch # ------------------------------------------------------------------ args = parser.parse_args(argv) if args.show_version: from muse._version import __version__ # Output format is stable: exactly "muse \n" on stdout, exit 0. # Agents and scripts can parse the version without --json: # # version = subprocess.check_output(["muse", "--version"]).decode().split()[1] # # The token at index 1 (space-split) is always the bare semver string. # This format will not change without a major-version bump. print(f"muse {__version__}") raise SystemExit(0) if args.chdir is not None: from muse.core.validation import sanitize_display try: os.chdir(args.chdir) except OSError as exc: safe_dir = sanitize_display(args.chdir) print(f"muse: cannot change to directory '{safe_dir}': {exc}", file=sys.stderr) raise SystemExit(1) if args.command is None or args.command == "help": _no_command(parser) return if not hasattr(args, "func"): # A namespace command (midi/code/coord) was given without a subcommand. # argparse already printed an error above via subs.required = True. raise SystemExit(2) args.func(args) if __name__ == "__main__": main()