code_query.py
python
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40
docs: add | jq convention to --json section of agent-guide
Sonnet 4.6
1 day ago
| 1 | """``muse code code-query`` — predicate query over code commit history. |
| 2 | |
| 3 | Search the commit graph for code changes matching a structured predicate:: |
| 4 | |
| 5 | muse code code-query "symbol == 'my_function' and change == 'added'" |
| 6 | muse code code-query "language == 'Python' and author == 'agent-x'" |
| 7 | muse code code-query "agent_id == 'claude' and sem_ver_bump == 'major'" |
| 8 | muse code code-query "file == 'src/core.py'" |
| 9 | muse code code-query "change == 'removed' and kind == 'class'" |
| 10 | muse code code-query "model_id contains 'claude'" |
| 11 | muse code code-query "symbol endswith _handler" |
| 12 | muse code code-query "author == 'gabriel'" --since 2026-01-01 |
| 13 | muse code code-query "sem_ver_bump == 'major'" --count |
| 14 | |
| 15 | Fields |
| 16 | ------ |
| 17 | |
| 18 | ``symbol`` Qualified symbol name (e.g. ``"MyClass.method"``). |
| 19 | ``file`` Workspace-relative file path. |
| 20 | ``language`` Language name (``"Python"``, ``"TypeScript"``…). |
| 21 | ``kind`` Symbol kind (``"function"``, ``"class"``, ``"method"``…). |
| 22 | ``change`` ``"added"``, ``"removed"``, or ``"modified"``. |
| 23 | ``author`` Commit author string. |
| 24 | ``agent_id`` Agent identity from commit provenance. |
| 25 | ``model_id`` Model ID from commit provenance. |
| 26 | ``toolchain_id`` Toolchain string from commit provenance. |
| 27 | ``sem_ver_bump`` ``"none"``, ``"patch"``, ``"minor"``, or ``"major"``. |
| 28 | ``branch`` Branch name. |
| 29 | |
| 30 | Operators: ``==``, ``!=``, ``contains``, ``startswith``, ``endswith`` |
| 31 | |
| 32 | Usage:: |
| 33 | |
| 34 | muse code code-query QUERY |
| 35 | muse code code-query QUERY --branch dev --max 100 |
| 36 | muse code code-query QUERY --since 2026-01-01 --until 2026-06-30 |
| 37 | muse code code-query QUERY --limit 10 |
| 38 | muse code code-query QUERY --count |
| 39 | muse code code-query QUERY --json |
| 40 | """ |
| 41 | |
| 42 | import argparse |
| 43 | import datetime |
| 44 | import json |
| 45 | import logging |
| 46 | import pathlib |
| 47 | import sys |
| 48 | from typing import TypedDict |
| 49 | |
| 50 | from muse.core.query_engine import QueryMatch, format_matches, walk_history |
| 51 | from muse.core.repo import parse_date_arg, require_repo |
| 52 | from muse.core.refs import read_current_branch |
| 53 | from muse.core.timing import start_timer |
| 54 | from muse.core.envelope import EnvelopeJson, make_envelope |
| 55 | from muse.plugins.code._code_query import build_evaluator |
| 56 | from muse.core.validation import clamp_int |
| 57 | |
| 58 | logger = logging.getLogger(__name__) |
| 59 | |
| 60 | class _CodeQueryOutputJson(EnvelopeJson): |
| 61 | """Top-level JSON envelope emitted by ``muse code code-query --json``. |
| 62 | |
| 63 | Fields |
| 64 | ------ |
| 65 | total Total number of matching commits/symbols found. |
| 66 | results List of match dicts (commit_id, message, author, …). |
| 67 | exit_code Always 0 — errors raise SystemExit before JSON is emitted, |
| 68 | so agents can treat this as a reliable success indicator. |
| 69 | duration_ms Wall-clock time for the full query in milliseconds |
| 70 | (non-negative float). |
| 71 | """ |
| 72 | |
| 73 | total: int |
| 74 | results: list[QueryMatch] |
| 75 | |
| 76 | def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None: |
| 77 | """Register the code-query subcommand.""" |
| 78 | parser = subparsers.add_parser( |
| 79 | "code-query", |
| 80 | help="Query the code commit history using a structured predicate.", |
| 81 | description=__doc__, |
| 82 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 83 | ) |
| 84 | parser.add_argument( |
| 85 | "query", |
| 86 | help="Query expression (see muse code code-query --help).", |
| 87 | ) |
| 88 | parser.add_argument( |
| 89 | "--branch", default=None, |
| 90 | help="Branch to search (default: HEAD branch).", |
| 91 | ) |
| 92 | parser.add_argument( |
| 93 | "--max", type=int, default=200, dest="max_commits", |
| 94 | help="Maximum commits to inspect (walk depth). Default: 200.", |
| 95 | ) |
| 96 | parser.add_argument( |
| 97 | "--limit", type=int, default=None, dest="limit", |
| 98 | help="Maximum matches to display. Does not affect walk depth (see --max).", |
| 99 | ) |
| 100 | parser.add_argument( |
| 101 | "--since", default=None, metavar="DATE", |
| 102 | help="Only include commits on or after DATE (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).", |
| 103 | ) |
| 104 | parser.add_argument( |
| 105 | "--until", default=None, metavar="DATE", |
| 106 | help="Only include commits on or before DATE (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS).", |
| 107 | ) |
| 108 | parser.add_argument( |
| 109 | "--count", action="store_true", |
| 110 | help="Print only the total match count, not the matches themselves.", |
| 111 | ) |
| 112 | parser.add_argument( |
| 113 | "--json", "-j", action="store_true", dest="json_out", |
| 114 | help="Emit JSON array of matches.", |
| 115 | ) |
| 116 | parser.set_defaults(func=run) |
| 117 | |
| 118 | def run(args: argparse.Namespace) -> None: |
| 119 | """Query the code commit history using a structured predicate. |
| 120 | |
| 121 | Walks up to ``--max`` commits from HEAD on the specified branch and returns |
| 122 | all commits (and symbol-level changes) matching the predicate. Predicates |
| 123 | can filter by symbol name, agent ID, semantic-version bump, author, file, or |
| 124 | commit date. Use ``--count`` to just count matching commits. |
| 125 | |
| 126 | Agent quickstart |
| 127 | ---------------- |
| 128 | :: |
| 129 | |
| 130 | muse code query "symbol == 'parse_query' and change == 'added'" --json |
| 131 | muse code query "agent_id contains 'claude'" --since 2026-01-01 --json |
| 132 | muse code query "sem_ver_bump == 'major'" --count --json |
| 133 | |
| 134 | JSON fields |
| 135 | ----------- |
| 136 | query The predicate string as passed. |
| 137 | branch Branch walked. |
| 138 | total Total number of matching commits. |
| 139 | truncated ``true`` if ``--max`` was reached before the full history. |
| 140 | commits List of matching commit objects: ``commit_id``, ``message``, |
| 141 | ``committed_at``, ``author``, ``agent_id``, ``symbol_changes`` |
| 142 | (list of ``{symbol, change, file}``). |
| 143 | |
| 144 | Exit codes |
| 145 | ---------- |
| 146 | 0 Query completed. |
| 147 | 1 Query parse error or invalid arguments. |
| 148 | 2 Not inside a Muse repository. |
| 149 | """ |
| 150 | elapsed = start_timer() |
| 151 | query: str = args.query |
| 152 | branch: str | None = args.branch |
| 153 | max_commits: int = clamp_int(args.max_commits, 1, 100_000, 'max_commits') |
| 154 | limit: int | None = args.limit |
| 155 | json_out: bool = args.json_out |
| 156 | count_only: bool = args.count |
| 157 | |
| 158 | since: datetime.datetime | None = ( |
| 159 | parse_date_arg(args.since, "--since") if args.since else None |
| 160 | ) |
| 161 | until: datetime.datetime | None = ( |
| 162 | parse_date_arg(args.until, "--until") if args.until else None |
| 163 | ) |
| 164 | |
| 165 | root: pathlib.Path = require_repo() |
| 166 | |
| 167 | try: |
| 168 | evaluator = build_evaluator(query) |
| 169 | except ValueError as exc: |
| 170 | print(f"❌ Query parse error: {exc}", file=sys.stderr) |
| 171 | raise SystemExit(1) from exc |
| 172 | |
| 173 | resolved_branch = branch or read_current_branch(root) |
| 174 | |
| 175 | # The code evaluator reads commit.structured_delta only — it never touches |
| 176 | # the snapshot manifest. Skipping manifest I/O cuts one file-read per commit. |
| 177 | matches = walk_history( |
| 178 | root, |
| 179 | resolved_branch, |
| 180 | evaluator, |
| 181 | max_commits=max_commits, |
| 182 | load_manifest=False, |
| 183 | since=since, |
| 184 | until=until, |
| 185 | ) |
| 186 | |
| 187 | if count_only: |
| 188 | print(len(matches)) |
| 189 | return |
| 190 | |
| 191 | if json_out: |
| 192 | print(json.dumps(_CodeQueryOutputJson( |
| 193 | **make_envelope(elapsed), |
| 194 | total=len(matches), |
| 195 | results=matches, |
| 196 | ))) |
| 197 | return |
| 198 | |
| 199 | display_limit = limit if limit is not None else 50 |
| 200 | print(format_matches(matches, max_results=display_limit)) |
File History
1 commit
sha256:e6465e8a9b7fa8e6223ed4a3576e96c568c913ae2caeb9c31f15e7a81b250b40
docs: add | jq convention to --json section of agent-guide
Sonnet 4.6
1 day ago