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