From 5b857c1a62a1fef69b27f0ee9c226e5d14b25505 Mon Sep 17 00:00:00 2001
From: swapnil
.*?)(?:\s+\[(?P[^\]]*)\])?$')
+
+
+class Graphs:
+ """the enclosing callable of a line, from every graph the repository holds (one per language)"""
+ def __init__(self, repo):
+ out = os.path.join(repo, '.axiomcode', 'out')
+ self.cons = []
+ for d in sorted(os.listdir(out)) if os.path.isdir(out) else []:
+ p = os.path.join(out, d, 'graph.sqlite')
+ if os.path.isfile(p):
+ try: self.cons.append(sqlite3.connect(f"file:{p}?mode=ro", uri=True))
+ except sqlite3.Error: pass
+
+ def enclosing(self, f, n):
+ best = None
+ for c in self.cons:
+ try:
+ r = c.execute("SELECT display, line, end_line FROM symbols WHERE (file = ? OR file LIKE ?) AND line <= ? AND end_line >= ? "
+ "AND method_id IS NOT NULL AND display NOT LIKE '%%' ORDER BY end_line - line LIMIT 1",
+ (f, '%/' + f, n, n)).fetchone()
+ except sqlite3.Error:
+ continue
+ if r and (best is None or r[2] - r[1] < best[2] - best[1]): best = r
+ return best
+
+
+def block(repo, f, marks, span):
+ """the lines to show for the marked lines of file f: the enclosing callable (whole, or header + a window around
+ each mark), numbered, every mark flagged"""
+ try:
+ with open(os.path.join(repo, f), encoding='utf-8', errors='replace') as h: L = h.read().split('\n')
+ except OSError:
+ return []
+ marks = sorted(n for n in marks if 0 < n <= len(L))
+ if not marks: return []
+ lo, hi = (span[1], span[2]) if span else (marks[0], marks[-1])
+ lo, hi = max(1, min(lo, marks[0])), min(len(L), max(hi, marks[-1]))
+ if hi - lo + 1 <= WHOLE:
+ keep = list(range(lo, hi + 1))
+ else:
+ keep = {lo}
+ for n in marks: keep |= set(range(max(lo, n - AROUND), min(hi, n + AROUND) + 1))
+ keep = sorted(keep)
+ w = len(str(keep[-1])); out = []; prev = None
+ for i in keep:
+ if prev is not None and i != prev + 1: out.append(' ' * (w + 4) + '…')
+ out.append(f"{'→' if i in marks else ' '} {str(i).rjust(w)} {L[i - 1].rstrip()}")
+ prev = i
+ return out
+
+
+# rows that add nothing an agent acts on: a word match offered only because nothing better was found (dropped when a
+# better row exists), and a module's own scope (its import lines)
+FILLER = 'best overall match'
+NOISE = ('module scope',)
+
+
+def render(verb, doc, repo):
+ code = ax_grep.Code(repo)
+ rows, _rest, foot = ax_grep.VERBS[{'find': 'context', 'tests': 'test-impact'}.get(verb, verb)](doc, code)
+ sites = []
+ for _k, line in rows:
+ m = SITE.match(line)
+ if m: sites.append((m.group('file'), int(m.group('line')), m.group('tag') or ''))
+ sites = [x for x in sites if not any(w in x[2] for w in NOISE)]
+ if any(FILLER not in t for _f, _n, t in sites): sites = [x for x in sites if FILLER not in x[2]]
+ # ONE PLACE PER FUNCTION: two relevant lines of one function are one block with both marked, in the order the
+ # verb ranked the first of them
+ graphs = Graphs(repo); places = {}
+ for f, n, t in sites:
+ span = graphs.enclosing(f, n)
+ key = (f, span[1], span[2]) if span else (f, n, n)
+ p = places.setdefault(key, {'f': f, 'span': span, 'marks': [], 'tags': []})
+ if n not in p['marks']: p['marks'].append(n)
+ t = t.split(' — ')[0].strip() # the tag's short form: what it is, not the explanation after the dash
+ if t and t not in p['tags']: p['tags'].append(t)
+ out = []
+ for i, p in enumerate(list(places.values())[:CAP], 1):
+ where = f"{p['f']}:{','.join(map(str, sorted(p['marks'])))}"
+ out.append(f"{i}. {where}" + (f" [{' | '.join(p['tags'][:2])}]" if p['tags'] else ''))
+ body = block(repo, p['f'], p['marks'], p['span'])
+ if body:
+ out.append(f" ```{FENCE.get(os.path.splitext(p['f'])[1], '')}")
+ out += [' ' + b for b in body]
+ out.append(' ```')
+ if not out: return None
+ if len(places) > CAP: out.append(f"… {len(places) - CAP} more place(s) not shown — ask a narrower question to see them")
+ out += [x for x in foot if x.startswith(('run:', 'verified'))][:2]
+ return out
+
+
+def verb_json(cmd):
+ import ax_exec
+ r = subprocess.run(ax_exec.program(cmd + ['--json']), stdout=subprocess.PIPE, text=True, encoding='utf-8', errors='replace')
+ try: doc = json.loads(r.stdout)
+ except ValueError: doc = None
+ return r, doc
+
+
+def edits(repo):
+ """impact with no name: what the working tree's edits changed, then what depends on those declarations, with code"""
+ r, doc = verb_json(['python3', os.path.join(H, 'axiomcode-changed'), repo])
+ if not isinstance(doc, dict):
+ sys.stdout.write(r.stdout); return r.returncode
+ ch = doc.get('changed') or []
+ targets = list(dict.fromkeys(c['target'] for c in ch if c.get('target')))
+ head = ["your edits: " + (', '.join(f"{c.get('kind')} {c.get('shown_target') or c.get('symbol')}" for c in ch[:8]) or 'none')
+ + (f" (+{len(ch) - 8} more)" if len(ch) > 8 else '')]
+ if not targets:
+ print('\n'.join(head + ["nothing edited is a declaration other code depends on" if ch else
+ "no edits against the commit the graph was built from"]))
+ return 0
+ r, doc = verb_json(['python3', os.path.join(H, 'axiomcode-impact')] + targets + [repo, '--tests'])
+ lines = render('impact', doc, repo) if isinstance(doc, dict) else None
+ print('\n'.join(head + (lines or [x for x in (doc or {}).get('prose', [])] or [r.stdout.strip()])))
+ return 0
+
+
+def main(argv):
+ verb, repo = argv[0], argv[1]
+ if verb == 'edits': return edits(repo)
+ cmd = argv[3:] if len(argv) > 2 and argv[2] == '--' else argv[2:]
+ r, doc = verb_json(cmd)
+ if not isinstance(doc, dict):
+ sys.stdout.write(r.stdout); return r.returncode
+ lines = render(verb, doc, repo) if r.returncode in (0, 1) or doc.get('called_undeclared') else None
+ if lines is None:
+ # a refusal or an answer with no place in it: the verb's own words are the answer
+ print('\n'.join(doc.get('prose') or []) or r.stdout.strip()); return r.returncode
+ print('\n'.join(lines))
+ return 0
+
+
+if __name__ == '__main__':
+ if len(sys.argv) < 3 or (sys.argv[1] != 'edits' and len(sys.argv) < 4): sys.exit(__doc__)
+ sys.exit(main(sys.argv[1:]))
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode
index c503f33d..cfcaf9ff 100755
--- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode
+++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode
@@ -1,5 +1,27 @@
#!/bin/bash
-# axiomcode — the one entry point. Every capability is a subcommand here; a new skill is a new subcommand, never a new tool.
+# axiomcode — ask the repository's call graph. Each answer is a numbered list of places, each with the code of the
+# function it sits in.
+#
+# axiomcode find ""
+# where the code for a task lives: the functions involved, most relevant first.
+# axiomcode impact []
+# who calls it, what a change to it reaches, and the tests that exercise it.
+# With no name: the same for the declarations your uncommitted edits changed.
+# axiomcode path
+# how A reaches B: every hop of the call chain, with the code at each call.
+# axiomcode tests
+# the tests your uncommitted edits reach, and the command that runs exactly those.
+# axiomcode index [] [--lang [,…]] [--src ] [--library [,…]]
+# build the graph (the first query builds it too). defaults to the current directory.
+#
+# Names are written as in the code: Owner.method, function, Type, or file.py:123. `axiomcode help ` for one verb.
+
+# THE HELP ABOVE IS THE PUBLIC SURFACE: helptext() prints the comment block up to the blank line above. The verbs below
+# are still dispatched and keep every flag -- the hooks, the test suites and scripts call them -- but they are internal
+# and are not advertised on any user- or agent-facing surface. find is context and tests is test-impact underneath;
+# at the front door (bin/axiomcode sets AXIOMCODE_FRONT, the MCP server AXIOMCODE_SURFACE=mcp) a query with no flag
+# answers as places with their code (ax_blocks.py), and any flag, AXIOMCODE_RAW=1 or a direct call gives the verb's
+# own answer.
#
# axiomcode index [] [--lang java|typescript|python|javascript|csharp] [--src ] [--library [,…]]
# the pipeline: parser → engine → .axiomcode/out/graph.sqlite (+ index). defaults to the current directory.
@@ -110,7 +132,12 @@ helptext(){ awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0";
# one verb's own usage, from the script that implements it: a python docstring, or a bash file's
# leading comment block. The verb documents itself once, where it is implemented.
verbhelp(){
- local f="$H/axiomcode-$1"; [ "$1" = index ] && f="$H/axiomcode-build"; [ "$1" = tests ] && f="$H/axiomcode-test-impact"
+ # a verb of the small surface is explained by its own entry in the help above: what it answers, no options
+ case "$1" in find|impact|path|tests)
+ helptext | awk -v v="$1" '$0 ~ "^ axiomcode "v"( |$)" {on=1; print; next} on && /^ axiomcode / {exit} on && /^$/ {exit} on {print}'
+ return 0 ;;
+ esac
+ local f="$H/axiomcode-$1"; [ "$1" = index ] && f="$H/axiomcode-build"; [ "$1" = tests ] && f="$H/axiomcode-test-impact"; [ "$1" = find ] && f="$H/axiomcode-context"
[ -f "$f" ] || { echo "axiomcode: no such verb '$1' — try: $(verbs | tr '\n' ' ')" >&2; return 2; }
if head -1 "$f" | grep -q python; then python3 -c 'import ast,sys; print(ast.get_docstring(ast.parse(open(sys.argv[1]).read())) or "")' "$f"
else awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$f"; fi
@@ -176,7 +203,7 @@ LASTPOS=""; [ "${#POS[@]}" -gt 0 ] && LASTPOS="${POS[${#POS[@]}-1]}"
case "$cmd" in
index|build) if [ "${#POS[@]}" -gt 0 ] && [ ! -d "${POS[0]}" ]; then gone "${POS[0]}"; fi ;;
graph) case "${POS[0]:-}" in ""|build|export|draw) ;; *) [ -d "${POS[0]}" ] || gone "${POS[0]}" ;; esac ;;
- context) if [ "${#POS[@]}" -gt 1 ] && [ ! -d "${POS[1]}" ]; then gone "${POS[1]}"; fi ;;
+ context|find) if [ "${#POS[@]}" -gt 1 ] && [ ! -d "${POS[1]}" ]; then gone "${POS[1]}"; fi ;;
path) if [ "${#POS[@]}" -gt 2 ] && [ ! -d "${POS[2]}" ]; then gone "${POS[2]}"; fi ;;
impact) if [ "${#POS[@]}" -gt 1 ] && [ ! -e "$LASTPOS" ] && dirlike "$LASTPOS"; then gone "$LASTPOS"; fi ;;
changed|test-impact|tests) if [ "${#POS[@]}" -gt 0 ] && [ ! -e "${POS[0]}" ] && dirlike "${POS[0]}"; then gone "${POS[0]}"; fi ;;
@@ -191,6 +218,25 @@ done
# shapes are one answer. Without it the answer is the verb's own, unchanged.
G=()
case "$cmd" in context|path|impact|test-impact|tests) [ -n "${GREP:-}" ] && G=(python3 "$H/ax_grep.py" "$cmd" "$FR" --limit "${GREP_LIMIT:-30}" --) ;; esac
+# THE SMALL SURFACE: find, impact, path and tests, asked with no flags at the front door (the installed `axiomcode` and
+# the MCP server set AXIOMCODE_FRONT), answer as numbered places, each with the code of the function it sits in
+# (ax_blocks.py), so a place needs no read to be understood. find is context; impact is impact with its tests,
+# and impact with no name answers for the declarations the working tree has edited; path is path; tests is
+# test-impact. A flag, AXIOMCODE_RAW, or a caller that runs this script directly (the hooks, the suites) gets the
+# verb's own answer.
+B=""; FRONT="${AXIOMCODE_FRONT:-}"; [ "${AXIOMCODE_SURFACE:-}" = mcp ] && FRONT=1; [ -n "${AXIOMCODE_RAW:-}${GREP:-}" ] && FRONT=""
+if [ -n "$FRONT" ]; then for a in ${ARGS[@]+"${ARGS[@]}"}; do case "$a" in -*) FRONT="" ;; esac; done; fi
+case "$cmd" in
+ find) cmd=context; [ -n "$FRONT" ] && B=find ;;
+ path) [ -n "$FRONT" ] && B=path ;;
+ tests|test-impact) [ -n "$FRONT" ] && B=tests ;;
+ impact) if [ -n "$FRONT" ]; then
+ named=""; for a in ${ARGS[@]+"${ARGS[@]}"}; do [ -d "$a" ] || named=1; done
+ [ -z "$named" ] && exec python3 "$H/ax_blocks.py" edits "$FR"
+ ARGS+=(--tests); B=impact
+ fi ;;
+esac
+[ -n "$B" ] && G=(python3 "$H/ax_blocks.py" "$B" "$FR" --)
# A REPOSITORY IN SEVERAL LANGUAGES has one graph per language (.axiomcode/lang/ beside the main one): a query
# asks every one of them (ax_langs.py), so no language's code is left out of an answer. A graph named in
# AXIOMCODE_GRAPH was chosen by the caller and is asked alone.
@@ -204,7 +250,7 @@ if [ -f "$FR/.axiomcode/out/graph.sqlite" ] || [ -L "$FR/.axiomcode/out/graph.sq
fi
case "$cmd" in
index|build) exec bash "$H/axiomcode-build" ${ARGS[@]+"${ARGS[@]}"} ;;
- context) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-context" ${ARGS[@]+"${ARGS[@]}"} ;;
+ context|find) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-context" ${ARGS[@]+"${ARGS[@]}"} ;;
path) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-path" ${ARGS[@]+"${ARGS[@]}"} ;;
impact) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
changed) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-changed" ${ARGS[@]+"${ARGS[@]}"} ;;
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-install b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-install
index 111cfa64..c562c060 100755
--- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-install
+++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-install
@@ -35,23 +35,23 @@ END = ''
BLOCK = """
## Finding code in this repository
-This repository has a resolved call graph. Ask it FIRST, through the
-`mcp__plugin_axiomcode_axiomcode__axiomcode_*` tools; no skill needs loading:
+This repository has a resolved call graph. Ask it FIRST, through the axiomcode MCP tools
+(`mcp__plugin_axiomcode_axiomcode__*`); no skill needs loading:
- axiomcode_context task="" # where the work is, when you have no name yet
- axiomcode_impact targets=[""] # what a change reaches: contract, users, tests
- axiomcode_path from_="" to="" # how A reaches B
+ find(question="") # where the code for a task lives
+ impact(name="") # who calls it, what a change reaches, its tests
+ impact() # the same for your uncommitted edits
+ path(start="", end="") # how A reaches B
+ tests() # the tests your edits reach, and how to run them
-Only when those tools are not in your list, the same from the shell: `axiomcode context ""`,
-`axiomcode impact `, `axiomcode path `.
+Only when those tools are not in your list, the same from the shell: `axiomcode find ""`,
+`axiomcode impact `, `axiomcode path `, `axiomcode tests`.
-**Trust the answer.** A `[resolved]` / `[sound]` row has already been looked up again in the graph
-(the `verified:` line) — do not re-derive it by grepping or opening the other files it names. Every
-answer ends with `next:`, the one step to take: read only the lines you will cite or change.
-`[by name]` / `[text]` rows are leads, not facts. An unresolved call means *unknown*, not *absent*.
+**Trust the answer.** Each place comes with the code of the function it sits in: answer from it. A
+`resolved` place has already been looked up again in the graph (the `verified:` line) — do not re-derive it
+by grepping. `by name` / `text` places are leads, not facts. An unresolved call means *unknown*, not *absent*.
Text search is still right for a string, a comment, a config value, or a file you already know.
-`axiomcode index` builds the graph if `.axiomcode/out/graph.sqlite` is absent.
"""
diff --git a/skills/axiomcode/SKILL.md b/skills/axiomcode/SKILL.md
index 9456d8e1..ff9eb64f 100644
--- a/skills/axiomcode/SKILL.md
+++ b/skills/axiomcode/SKILL.md
@@ -1,131 +1,74 @@
---
name: axiomcode
description: >-
- Use for any why, what or where question about code — how a codebase works or what a change to it would do: architecture, execution flow, where something lives, who calls it, what depends on it, what breaks if it changes, which tests cover an edit, whether it is safe to delete. Also use when resolving an issue or bug report, which names a symptom rather than a file. Examples: "How does X work?", "Where do I change Y?", "What calls this?", "What breaks if I change Z?", "Is this safe to delete?", "Fix this issue". No task is too small: if you are about to grep for a name, call this instead. Mandatory when .axiomcode/out/graph.sqlite exists — start here rather than grep, even when you already know the code. Answers come from a resolved call graph, so they include callers that never spell the name — through an interface, an override, a callback, dependency injection or a config key — each labelled with how certain it is. Call it directly, no need to load this skill first: the `axiomcode_context` MCP tool with source=True for how something works (the call flow with each step's code; from_= when you know where it begins), `axiomcode_impact` for what a change reaches, `axiomcode_path` for how A reaches B. Only when those tools are not in your list, the same from the shell: `axiomcode context "" --source`, `axiomcode impact `, `axiomcode path `. Java, TypeScript, Python, JavaScript, C#.
+ Use for any why, what or where question about code — how a codebase works, where something lives, who calls it, what a change to it breaks, which tests cover an edit. Also use when resolving an issue or bug report, which names a symptom rather than a file. Examples: "How does X work?", "Where do I change Y?", "What calls this?", "What breaks if I change Z?", "Which tests do I run?", "Fix this issue". No task is too small: if you are about to grep for a name, call this instead. Mandatory when .axiomcode/out/graph.sqlite exists — start here rather than grep, even when you already know the code. Answers come from a resolved call graph, so they include callers that never spell the name — through an interface, an override, a callback, dependency injection or a config key — and every place comes with the code of the function it sits in. Call the MCP tools directly, no need to load this skill first: find(question) for where the code for a task lives, impact(name) for who calls it and what a change reaches (with no name: your uncommitted edits), path(start, end) for how A reaches B, tests() for the tests your edits reach. Only when those tools are not in your list, the same from the shell: `axiomcode find ""`, `axiomcode impact `, `axiomcode path `, `axiomcode tests`. Java, TypeScript, Python, JavaScript, C#.
---
# axiomcode
-Prefer the MCP tools (`axiomcode_`; in Claude Code, `mcp__plugin_axiomcode_axiomcode__axiomcode_`) when they
-are in your tool list; otherwise run `/../../plugins/axiomcode/skills/axiomcode/scripts/axiomcode …` from the repository root. Same code, same
-verified output. `` defaults to the current directory. In Claude Code, a hook adds the graph's edges to your own
-Read / Grep results as `graph: …` lines.
-
-**Trust the answer, and know what it is.** A `[resolved]` / `[sound]` row has already been looked up again in the graph (the `verified:` line): do not re-derive it by grepping. Each answer ends with `next:` — the one step to take. For a CHANGE (who calls it, what breaks, which tests), read only the lines you will cite or change. To EXPLAIN how something works, the graph gives the reading order, not the explanation: read each step's body, and continue through every `⚠` (a call the graph lost). `[by name]` / `[text]` / `[approx]` rows are leads, not facts.
-
-**A list of sites comes the way grep prints it.** The MCP `impact`, `path`, `test_impact` and `context` (without
-`source` / `explain` / `from_`) answer one site per line: `path:line: [resolved · hop 2 · test …]`,
-surest first, capped with a count of the rest; `limit=N` lists more, `full=True` gives the sectioned answer with `next:`.
-From the shell the same shape is `--grep` (`--grep-limit N`); without it the answer is the prose.
-
-## Start here
-
-| the question in front of you | the call |
-|---|---|
-| **`.axiomcode/out/graph.sqlite` already exists** | **query it — do NOT run `index`** |
-| no graph at all | `axiomcode index` |
-| a task in words, no name to ask about yet | `axiomcode context ""` — then `--in ` it names |
-| "who calls X" / "what breaks if X changes" | `axiomcode impact X` |
-| "who writes this field" / "is it safe under concurrent access" | `axiomcode impact .` — ask of the FIELD |
-| one concept you can name ("the decryption code") | `axiomcode path decrypt '*'` |
-| "how does X work" · "explain / walk through X" | `axiomcode context "" --source` — the call flow in order with each step's code; answer from it, and open a file only for a step whose body was cut or a `⚠` call. `--from ` when you know where it begins |
-| "how does A reach B" · "everything that reaches X" | `axiomcode path A B` · `axiomcode path '*' X` |
-| "what did my edit touch" · "which tests do I run" | `axiomcode changed --impact` · `axiomcode test-impact` |
-| "is it safe to delete X" | `axiomcode impact X --delete` |
-| what an engine or rules change did to a graph · a before/after of one tree | `axiomcode diff