app.py file-level

at main · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:c docs(KD-STAGING): sync governance after KD-6b DONE Mirror workspace go… · aaronrene · Jul 10, 2026
1 """Muse CLI β€” entry point for the ``muse`` console script.
2
3 Two-tier command architecture
4 -------------------------------
5
6 **Tier 1 β€” Core + Low-level** (top-level ``muse …``)
7 All VCS commands live here β€” both human-facing porcelain and the
8 machine-readable, JSON-outputting primitives that scripts and agents use.
9 No ``plumbing`` sub-namespace: agents reach every command in one token.
10
11 **Tier 2 β€” Semantic Porcelain** (``muse midi …``, ``muse code …``, ``muse coord …``)
12 Domain-specific commands that interpret multidimensional state. Each
13 sub-namespace is served by the corresponding ``muse/plugins/`` plugin.
14
15 Top-level commands (alphabetical)::
16
17 annotate archive attributes auth
18 bisect blame branch bundle
19 cat cat-object check check-attr
20 check-ignore check-ref-format checkout cherry-pick
21 clean clone code commit
22 commit-graph commit-tree config conflicts
23 content-grep coord describe diff
24 domain-info domains fetch for-each-ref
25 gc hash-object hub init
26 log ls-files ls-remote merge
27 merge-base midi name-rev pack-objects
28 pull push rebase read-commit
29 read-snapshot reflog release remote
30 rerere reset revert rev-parse
31 rm
32 shortlog show show-ref snapshot
33 snapshot-diff stash status symbolic-ref
34 tag unpack-objects update-ref verify
35 verify-object verify-pack workspace
36 worktree
37
38 Identity & hub fabric::
39
40 auth keygen muse auth keygen [--hub HUB]
41 auth register muse auth register [--hub HUB] [--handle NAME] [--agent]
42 auth whoami muse auth whoami [--json]
43 auth logout muse auth logout [--hub HUB]
44 hub connect muse hub connect <url>
45 hub status muse hub status [--json]
46 hub disconnect muse hub disconnect
47 hub ping muse hub ping
48 config show muse config show [--json]
49 config get muse config get <key>
50 config set muse config set <key> <value>
51 config edit muse config edit
52
53 Tier 3 β€” MIDI domain commands (``muse midi …``, alphabetical) β€” disabled pending rollout::
54
55 agent-map arpeggiate cadence check
56 compare contour density find-phrase
57 harmony humanize instrumentation invert
58 mix motif normalize note-blame
59 note-hotspots note-log notes piano-roll
60 quantize query retrograde rhythm
61 scale shard tempo tension
62 transpose velocity-profile voice-leading
63
64 Tier 3 β€” Code domain commands (``muse code …``, alphabetical)::
65
66 add age api-surface blast-risk
67 blame breakage cat checkout-symbol
68 clones code-check code-query codemap
69 compare contract coupling coverage
70 dead deps detect-refactor docs
71 entangle find-symbol gravity grep
72 hotspots impact index invariants
73 languages lineage narrative patch
74 predict query query-history rename
75 reset semantic-cherry-pick semantic-test-coverage stable
76 symbol-log symbols test type
77 velocity
78
79 Tier 3 β€” Coordination commands (``muse coord …``, alphabetical)::
80
81 cancel-task claim complete dag
82 enqueue fail-task forecast gc
83 heartbeat intent list plan-merge
84 reconcile release reserve shard
85 sync tasks watch
86 """
87
88 from __future__ import annotations
89
90 import argparse
91 import difflib
92 import os
93 import signal
94 import sys
95
96 # Restore the default SIGPIPE handler so that piping muse output to commands
97 # that read partial output (head, grep, jq, etc.) exits cleanly instead of
98 # raising BrokenPipeError and printing a Python traceback. This matters most
99 # for agents that routinely pipe muse JSON output to filters.
100 if hasattr(signal, "SIGPIPE"):
101 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
102
103 from muse.cli.commands import (
104 age as age_cmd,
105 agent_config,
106 # agent_map, # midi β€” disabled until midi rollout
107 annotate,
108 api_surface,
109 archive,
110 # arpeggiate, # midi β€” disabled until midi rollout
111 attributes,
112 auth,
113 trust,
114 bisect,
115 blame,
116 blast_risk,
117 branch,
118 breakage,
119 bundle,
120 # cadence, # midi β€” disabled until midi rollout
121 cat,
122 check,
123 checkout,
124 checkout_symbol,
125 cherry_pick,
126 clean,
127 clone,
128 clones,
129 code_check,
130 code_query,
131 code_stage,
132 codemap,
133 commit,
134 compare,
135 config_cmd,
136 conflicts,
137 content_grep,
138 contract,
139 # contour, # midi β€” disabled until midi rollout
140 coord_gc,
141 coord_sync,
142 core_blame,
143 coupling,
144 coverage,
145 dag as dag_cmd,
146 dead,
147 # density, # midi β€” disabled until midi rollout
148 deps,
149 describe,
150 detect_refactor,
151 diff,
152 docs_cmd,
153 domains,
154 entangle,
155 fetch,
156 # find_phrase, # midi β€” disabled until midi rollout
157 find_symbol,
158 forecast,
159 gc,
160 gravity,
161 grep,
162 harmony,
163 heartbeat_coord,
164 hotspots,
165 hub,
166 # humanize, # midi β€” disabled until midi rollout
167 impact,
168 index_rebuild,
169 init,
170 # instrumentation, # midi β€” disabled until midi rollout
171 intent,
172 invariants,
173 # invert, # midi β€” disabled until midi rollout
174 languages,
175 lineage,
176 list_coord,
177 log,
178 merge,
179 migrate,
180 # midi_check, # midi β€” disabled until midi rollout
181 # midi_compare, # midi β€” disabled until midi rollout
182 # midi_query, # midi β€” disabled until midi rollout
183 # midi_shard, # midi β€” disabled until midi rollout
184 # mix, # midi β€” disabled until midi rollout
185 # motif_detect, # midi β€” disabled until midi rollout
186 narrative,
187 # note_blame, # midi β€” disabled until midi rollout
188 # note_hotspots, # midi β€” disabled until midi rollout
189 # note_log, # midi β€” disabled until midi rollout
190 # notes, # midi β€” disabled until midi rollout
191 patch,
192 # piano_roll, # midi β€” disabled until midi rollout
193 plan_merge,
194 predict,
195 pull,
196 push,
197 # quantize, # midi β€” disabled until midi rollout
198 query,
199 query_history,
200 rebase,
201 reconcile,
202 reflog,
203 release as release_cmd,
204 release_coord,
205 remote,
206 rename,
207 rerere,
208 reserve,
209 reset,
210 rm,
211 # retrograde, # midi β€” disabled until midi rollout
212 revert,
213 # rhythm, # midi β€” disabled until midi rollout
214 # scale_detect, # midi β€” disabled until midi rollout
215 semantic_cherry_pick,
216 semantic_test_coverage,
217 shard,
218 shortlog,
219 show,
220 snapshot_cmd,
221 stable,
222 stash,
223 status,
224 symbol_log,
225 symbols,
226 tag,
227 task_queue as task_queue_cmd,
228 # tempo, # midi β€” disabled until midi rollout
229 # tension, # midi β€” disabled until midi rollout
230 test_cmd,
231 # transpose, # midi β€” disabled until midi rollout
232 type_cmd,
233 velocity,
234 # velocity_normalize, # midi β€” disabled until midi rollout
235 # velocity_profile, # midi β€” disabled until midi rollout
236 verify,
237 verify_commit,
238 # voice_leading, # midi β€” disabled until midi rollout
239 watch_coord,
240
241 workspace,
242 worktree,
243 )
244
245 from muse.cli.commands.plumbing import (
246 cat_object,
247 check_attr,
248 check_ignore,
249 check_ref_format,
250 commit_graph,
251 commit_tree,
252 domain_info,
253 for_each_ref,
254 hash_object,
255 ls_files,
256 ls_remote,
257 merge_base,
258 name_rev,
259 pack_objects,
260 read_commit,
261 read_snapshot,
262 rev_parse,
263 show_ref,
264 snapshot_diff,
265 symbolic_ref,
266 unpack_objects,
267 update_ref,
268 verify_object,
269 verify_pack,
270 )
271
272
273 def _no_command(parser: argparse.ArgumentParser) -> None:
274 """Print help when no subcommand is provided."""
275 parser.print_help()
276 raise SystemExit(0)
277
278
279 class _MuseArgumentParser(argparse.ArgumentParser):
280 """ArgumentParser subclass that adds 'did you mean?' suggestions on error.
281
282 When the user mistypes a subcommand (e.g. ``muse comit``), argparse
283 normally prints only "argument COMMAND: invalid choice: 'comit'". This
284 subclass intercepts that error, checks for close matches among the
285 registered subcommand names, and appends a suggestion line so agents and
286 humans can self-correct without consulting the full ``--help`` output.
287
288 Example output::
289
290 muse: error: argument COMMAND: invalid choice: 'comit'
291 Did you mean: commit?
292 """
293
294 def error(self, message: str) -> None:
295 # Extract the unknown token from messages like:
296 # "argument COMMAND: invalid choice: 'comit' (choose from ...)"
297 unknown: str | None = None
298 if "invalid choice:" in message:
299 try:
300 start = message.index("'") + 1
301 end = message.index("'", start)
302 unknown = message[start:end]
303 except ValueError:
304 pass
305
306 if unknown is not None:
307 choices: list[str] = []
308 for action in self._actions:
309 if isinstance(action, argparse._SubParsersAction):
310 choices.extend(action.choices.keys())
311 suggestions = difflib.get_close_matches(unknown, choices, n=3, cutoff=0.6)
312 if suggestions:
313 message = f"{message}\n Did you mean: {', '.join(suggestions)}?"
314
315 super().error(message)
316
317
318 def main(argv: list[str] | None = None) -> None:
319 """Parse arguments and dispatch to the appropriate command handler.
320
321 Exit codes
322 ----------
323 0 Success β€” command completed normally.
324 1 Runtime error β€” e.g. ``-C DIR`` directory does not exist.
325 2 Argument error β€” unknown command, missing required argument, or a
326 namespace command (``code``/``midi``/``coord``) was invoked without a
327 subcommand. argparse prints the error before this function raises.
328
329 Flag processing order
330 ---------------------
331 1. ``--version`` / ``-V`` β€” print version and exit 0, no further parsing.
332 2. ``-C DIR`` β€” ``os.chdir(DIR)`` before dispatch; exit 1 on
333 ``OSError`` (non-existent or permission-denied).
334 3. Subcommand dispatch β€” ``args.func(args)`` called once.
335
336 No-subcommand behaviour
337 -----------------------
338 ``muse`` with no subcommand prints the help text and exits 0.
339 ``muse code`` / ``muse midi`` / ``muse coord`` with no subcommand exits 2
340 (argparse enforces ``subs.required = True``).
341
342 Calling from tests
343 ------------------
344 Pass an explicit ``argv`` list to avoid reading ``sys.argv``::
345
346 from tests.cli_test_helper import CliRunner
347 result = CliRunner().invoke(None, ["--version"])
348 assert "muse" in result.output
349 """
350 parser = _MuseArgumentParser(
351 prog="muse",
352 description="Muse β€” domain-agnostic version control for multidimensional state.",
353 formatter_class=argparse.RawDescriptionHelpFormatter,
354 )
355 parser.add_argument(
356 "--version", "-V", "-v",
357 action="store_true",
358 dest="show_version",
359 help="Print 'muse <version>' to stdout and exit 0. "
360 "Agents: parse with output.split()[1] for the bare version string.",
361 )
362 parser.add_argument(
363 "-C",
364 metavar="DIR",
365 dest="chdir",
366 default=None,
367 help="Change to DIR before running the command (DIR must exist).",
368 )
369
370 subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
371
372 # ``muse help`` β€” ergonomic alias for ``muse --help``.
373 # Agents and humans reach for ``help`` by reflex; honour it.
374 subparsers.add_parser("help", help="Show this help message and exit.")
375
376 # ------------------------------------------------------------------
377 # All top-level commands β€” alphabetical order.
378 # Low-level plumbing primitives and core porcelain are interleaved
379 # so that ``muse --help`` presents one sorted list.
380 # ------------------------------------------------------------------
381 agent_config.register(subparsers)
382 annotate.register(subparsers)
383 archive.register(subparsers)
384 attributes.register(subparsers)
385 auth.register(subparsers)
386 bisect.register(subparsers)
387 core_blame.register(subparsers) # "blame"
388 branch.register(subparsers)
389 bundle.register(subparsers)
390 cat.register(subparsers)
391 cat_object.register(subparsers) # "cat-object"
392 check.register(subparsers)
393 check_attr.register(subparsers) # "check-attr"
394 check_ignore.register(subparsers) # "check-ignore"
395 check_ref_format.register(subparsers) # "check-ref-format"
396 checkout.register(subparsers)
397 cherry_pick.register(subparsers) # "cherry-pick"
398 clean.register(subparsers)
399 clone.register(subparsers)
400
401 # ── code namespace (muse code …) ──────────────────────────────────
402 code_parser = subparsers.add_parser(
403 "code",
404 help="[Tier 3] Code domain semantic commands β€” symbol graph, call graph, and provenance.",
405 description="Code domain semantic commands.",
406 )
407 code_subs = code_parser.add_subparsers(dest="code_command", metavar="CODE_COMMAND")
408 code_subs.required = True
409
410 code_stage.register_add(code_subs) # "add"
411 age_cmd.register(code_subs) # "age"
412 api_surface.register(code_subs) # "api-surface"
413 blast_risk.register(code_subs) # "blast-risk"
414 blame.register(code_subs) # "blame"
415 breakage.register(code_subs) # "breakage"
416 cat.register(code_subs) # "cat"
417 checkout_symbol.register(code_subs) # "checkout-symbol"
418 clones.register(code_subs) # "clones"
419 code_check.register(code_subs) # "code-check"
420 code_query.register(code_subs) # "code-query"
421 codemap.register(code_subs) # "codemap"
422 compare.register(code_subs) # "compare"
423 contract.register(code_subs) # "contract"
424 coupling.register(code_subs) # "coupling"
425 coverage.register(code_subs) # "coverage"
426 dead.register(code_subs) # "dead"
427 deps.register(code_subs) # "deps"
428 detect_refactor.register(code_subs) # "detect-refactor"
429 docs_cmd.register(code_subs) # "docs"
430 entangle.register(code_subs) # "entangle"
431 find_symbol.register(code_subs) # "find-symbol"
432 gravity.register(code_subs) # "gravity"
433 grep.register(code_subs) # "grep"
434 hotspots.register(code_subs) # "hotspots"
435 impact.register(code_subs) # "impact"
436 index_rebuild.register(code_subs) # "index"
437 invariants.register(code_subs) # "invariants"
438 languages.register(code_subs) # "languages"
439 lineage.register(code_subs) # "lineage"
440 migrate.register(code_subs) # "migrate"
441 narrative.register(code_subs) # "narrative"
442 patch.register(code_subs) # "patch"
443 predict.register(code_subs) # "predict"
444 query.register(code_subs) # "query"
445 query_history.register(code_subs) # "query-history"
446 rename.register(code_subs) # "rename"
447 code_stage.register_reset(code_subs) # "reset"
448 semantic_cherry_pick.register(code_subs) # "semantic-cherry-pick"
449 semantic_test_coverage.register(code_subs) # "semantic-test-coverage"
450 stable.register(code_subs) # "stable"
451 symbol_log.register(code_subs) # "symbol-log"
452 symbols.register(code_subs) # "symbols"
453 test_cmd.register(code_subs) # "test"
454 type_cmd.register(code_subs) # "type"
455 velocity.register(code_subs) # "velocity"
456 # ─────────────────────────────────────────────────────────────────
457
458 commit.register(subparsers)
459 commit_graph.register(subparsers) # "commit-graph"
460 commit_tree.register(subparsers) # "commit-tree"
461 config_cmd.register(subparsers) # "config"
462 conflicts.register(subparsers)
463 content_grep.register(subparsers) # "content-grep"
464
465 # ── coord namespace (muse coord …) ───────────────────────────────
466 coord_parser = subparsers.add_parser(
467 "coord",
468 help="[Tier 3] Multi-agent coordination commands β€” reservations, intent, conflict forecasting.",
469 description="Multi-agent coordination commands.",
470 )
471 coord_subs = coord_parser.add_subparsers(dest="coord_command", metavar="COORD_COMMAND")
472 coord_subs.required = True
473
474 task_queue_cmd.register_cancel_task(coord_subs) # "cancel-task"
475 task_queue_cmd.register_claim(coord_subs) # "claim"
476 task_queue_cmd.register_complete(coord_subs) # "complete"
477 dag_cmd.register(coord_subs) # "dag"
478 task_queue_cmd.register_enqueue(coord_subs) # "enqueue"
479 task_queue_cmd.register_fail_task(coord_subs) # "fail-task"
480 forecast.register(coord_subs) # "forecast"
481 coord_gc.register(coord_subs) # "gc"
482 heartbeat_coord.register(coord_subs) # "heartbeat"
483 intent.register(coord_subs) # "intent"
484 list_coord.register(coord_subs) # "list"
485 plan_merge.register(coord_subs) # "plan-merge"
486 reconcile.register(coord_subs) # "reconcile"
487 release_coord.register(coord_subs) # "release"
488 reserve.register(coord_subs) # "reserve"
489 shard.register(coord_subs) # "shard"
490 coord_sync.register(coord_subs) # "sync"
491 task_queue_cmd.register_tasks(coord_subs) # "tasks"
492 watch_coord.register(coord_subs) # "watch"
493 # ─────────────────────────────────────────────────────────────────
494
495 describe.register(subparsers)
496 diff.register(subparsers)
497 domain_info.register(subparsers) # "domain-info"
498 domains.register(subparsers)
499 fetch.register(subparsers)
500 for_each_ref.register(subparsers) # "for-each-ref"
501 gc.register(subparsers)
502 hash_object.register(subparsers) # "hash-object"
503 hub.register(subparsers)
504 init.register(subparsers)
505 log.register(subparsers)
506 ls_files.register(subparsers) # "ls-files"
507 ls_remote.register(subparsers) # "ls-remote"
508 merge.register(subparsers)
509 merge_base.register(subparsers) # "merge-base"
510
511 # ── midi namespace (muse midi …) β€” disabled until midi rollout ───
512 # midi_parser = subparsers.add_parser(
513 # "midi",
514 # help="[Tier 3] MIDI domain semantic commands.",
515 # description="MIDI domain semantic commands β€” analysis, transformation, and multi-agent.",
516 # )
517 # midi_subs = midi_parser.add_subparsers(dest="midi_command", metavar="MIDI_COMMAND")
518 # midi_subs.required = True
519 #
520 # agent_map.register(midi_subs) # "agent-map"
521 # arpeggiate.register(midi_subs) # "arpeggiate"
522 # cadence.register(midi_subs) # "cadence"
523 # midi_check.register(midi_subs) # "check"
524 # midi_compare.register(midi_subs) # "compare"
525 # contour.register(midi_subs) # "contour"
526 # density.register(midi_subs) # "density"
527 # find_phrase.register(midi_subs) # "find-phrase"
528 # harmony.register(midi_subs) # "harmony"
529 # humanize.register(midi_subs) # "humanize"
530 # instrumentation.register(midi_subs) # "instrumentation"
531 # invert.register(midi_subs) # "invert"
532 # mix.register(midi_subs) # "mix"
533 # motif_detect.register(midi_subs) # "motif"
534 # velocity_normalize.register(midi_subs) # "normalize"
535 # note_blame.register(midi_subs) # "note-blame"
536 # note_hotspots.register(midi_subs) # "note-hotspots"
537 # note_log.register(midi_subs) # "note-log"
538 # notes.register(midi_subs) # "notes"
539 # piano_roll.register(midi_subs) # "piano-roll"
540 # quantize.register(midi_subs) # "quantize"
541 # midi_query.register(midi_subs) # "query"
542 # retrograde.register(midi_subs) # "retrograde"
543 # rhythm.register(midi_subs) # "rhythm"
544 # scale_detect.register(midi_subs) # "scale"
545 # midi_shard.register(midi_subs) # "shard"
546 # tempo.register(midi_subs) # "tempo"
547 # tension.register(midi_subs) # "tension"
548 # transpose.register(midi_subs) # "transpose"
549 # velocity_profile.register(midi_subs) # "velocity-profile"
550 # voice_leading.register(midi_subs) # "voice-leading"
551 # ─────────────────────────────────────────────────────────────────
552
553 name_rev.register(subparsers) # "name-rev"
554 pack_objects.register(subparsers) # "pack-objects"
555 pull.register(subparsers)
556 push.register(subparsers)
557 rebase.register(subparsers)
558 read_commit.register(subparsers) # "read-commit"
559 read_snapshot.register(subparsers) # "read-snapshot"
560 reflog.register(subparsers)
561 release_cmd.register(subparsers) # "release"
562 remote.register(subparsers)
563 rerere.register(subparsers)
564 reset.register(subparsers)
565 rm.register(subparsers)
566 revert.register(subparsers)
567 rev_parse.register(subparsers) # "rev-parse"
568 shortlog.register(subparsers)
569 show.register(subparsers)
570 show_ref.register(subparsers) # "show-ref"
571 snapshot_cmd.register(subparsers) # "snapshot"
572 snapshot_diff.register(subparsers) # "snapshot-diff"
573 stash.register(subparsers)
574 status.register(subparsers)
575 symbolic_ref.register(subparsers) # "symbolic-ref"
576 tag.register(subparsers)
577 trust.register(subparsers)
578 unpack_objects.register(subparsers) # "unpack-objects"
579 update_ref.register(subparsers) # "update-ref"
580 verify.register(subparsers)
581 verify_commit.register(subparsers) # "verify-commit" β€” Phase 7.5
582 verify_object.register(subparsers) # "verify-object"
583 verify_pack.register(subparsers) # "verify-pack"
584 workspace.register(subparsers)
585 worktree.register(subparsers)
586
587 # ------------------------------------------------------------------
588 # Parse and dispatch
589 # ------------------------------------------------------------------
590 args = parser.parse_args(argv)
591
592 if args.show_version:
593 from muse._version import __version__
594 # Output format is stable: exactly "muse <version>\n" on stdout, exit 0.
595 # Agents and scripts can parse the version without --json:
596 #
597 # version = subprocess.check_output(["muse", "--version"]).decode().split()[1]
598 #
599 # The token at index 1 (space-split) is always the bare semver string.
600 # This format will not change without a major-version bump.
601 print(f"muse {__version__}")
602 raise SystemExit(0)
603
604 if args.chdir is not None:
605 from muse.core.validation import sanitize_display
606 try:
607 os.chdir(args.chdir)
608 except OSError as exc:
609 safe_dir = sanitize_display(args.chdir)
610 print(f"muse: cannot change to directory '{safe_dir}': {exc}", file=sys.stderr)
611 raise SystemExit(1)
612
613 if args.command is None or args.command == "help":
614 _no_command(parser)
615 return
616
617 if not hasattr(args, "func"):
618 # A namespace command (midi/code/coord) was given without a subcommand.
619 # argparse already printed an error above via subs.required = True.
620 raise SystemExit(2)
621
622 args.func(args)
623
624
625 if __name__ == "__main__":
626 main()