From fe9297be3a68d696b58ad1e21db8a66e15dd692b Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Sat, 10 Oct 2026 13:46:05 -0700
Subject: [PATCH 01/17] release 0.1.10: every package and plugin manifest at
0.1.10 (the parser keeps its own version)
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
gemini-extension.json | 2 +-
package.json | 12 ++++++------
plugins/axiomcode/.claude-plugin/plugin.json | 2 +-
plugins/axiomcode/.codex-plugin/plugin.json | 2 +-
plugins/axiomcode/.cursor-plugin/plugin.json | 2 +-
5 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/gemini-extension.json b/gemini-extension.json
index ebb25063d..4e8cef176 100644
--- a/gemini-extension.json
+++ b/gemini-extension.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.1.9",
+ "version": "0.1.10",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"contextFileName": "plugins/axiomcode/AGENTS.md",
"mcpServers": {
diff --git a/package.json b/package.json
index 448fe5753..fbff87117 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@axiomcode/code-graph",
- "version": "0.1.9",
+ "version": "0.1.10",
"description": "AxiomCode Graph: a resolved call graph of your codebase grounded in formal methods, so you and your coding agents can see who calls what, what a change breaks, and which tests it reaches.",
"repository": {
"type": "git",
@@ -31,11 +31,11 @@
],
"license": "FSL-1.1-Apache-2.0",
"optionalDependencies": {
- "@axiomcode/engine-darwin-arm64": "0.1.9",
- "@axiomcode/engine-darwin-x64": "0.1.9",
- "@axiomcode/engine-linux-x64": "0.1.9",
- "@axiomcode/engine-linux-arm64": "0.1.9",
- "@axiomcode/engine-win32-x64": "0.1.9"
+ "@axiomcode/engine-darwin-arm64": "0.1.10",
+ "@axiomcode/engine-darwin-x64": "0.1.10",
+ "@axiomcode/engine-linux-x64": "0.1.10",
+ "@axiomcode/engine-linux-arm64": "0.1.10",
+ "@axiomcode/engine-win32-x64": "0.1.10"
},
"bin": {
"axiomcode": "bin/axiomcode.js"
diff --git a/plugins/axiomcode/.claude-plugin/plugin.json b/plugins/axiomcode/.claude-plugin/plugin.json
index 1c32d1929..98067cc05 100644
--- a/plugins/axiomcode/.claude-plugin/plugin.json
+++ b/plugins/axiomcode/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "axiomcode",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
- "version": "0.1.9",
+ "version": "0.1.10",
"author": {
"name": "AxiomCode"
}
diff --git a/plugins/axiomcode/.codex-plugin/plugin.json b/plugins/axiomcode/.codex-plugin/plugin.json
index 720690425..3940024ae 100644
--- a/plugins/axiomcode/.codex-plugin/plugin.json
+++ b/plugins/axiomcode/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.1.9",
+ "version": "0.1.10",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed.",
"author": { "name": "AxiomCode", "url": "https://github.com/AxiomCodeAI/axiomcodegraph" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
diff --git a/plugins/axiomcode/.cursor-plugin/plugin.json b/plugins/axiomcode/.cursor-plugin/plugin.json
index 3e3941cf7..ada8f9aab 100644
--- a/plugins/axiomcode/.cursor-plugin/plugin.json
+++ b/plugins/axiomcode/.cursor-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "axiomcode",
- "version": "0.1.9",
+ "version": "0.1.10",
"description": "Ask your repository how its code connects: who calls this, what breaks if I change it, which tests an edit reaches, how A reaches B. Answers come from a resolved call graph and are verified against it; nothing is guessed. Java, TypeScript, Python, JavaScript.",
"author": { "name": "AxiomCode" },
"homepage": "https://github.com/AxiomCodeAI/axiomcodegraph",
From f3f10fb1f3731e10f310d6c026f327ac8e971592 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Mon, 12 Oct 2026 02:09:34 -0700
Subject: [PATCH 02/17] plugin: show the user, after each turn, where an
AxiomCode answer verifiably helped the agent (#1935)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agents finish a task with and without the graph and rarely credit the answer they acted on. A Stop hook
(hooks/assist.py) now reads the session's transcript at the end of each turn and prints, under the reply,
one line per chain an answer started:
◆ AxiomCode this turn
• Helped: `impact _proxy_to_logger` showed _base.py (first seen there); edited it, read _native.py,
twisted.py; both picked test files pass
Labels, strongest first: Redirected (the work moved from files left behind to one the answer showed),
Helped (a file first seen in the answer was edited and the edit is kept, or a test verb's picked tests
passed), Useful (it was read, or its picked tests ran). "First seen" means no earlier prompt, tool call,
tool result or hook context named the file.
Nothing comes from the agent's text: its reply is on screen before a Stop hook runs. Every link is read
from tool calls and results and the working-tree diff, and an unchecked link is not printed. A shell line
that batches the query with grep or cat credits only rows shaped like an answer's. A shell edit counts
only for the file the line writes, and a test run is credited once. Turns where nothing came of a query
print nothing.
Chains go to .axiomcode/assists.jsonl, which only the hook writes. A chain prints once, and again only
when it gets stronger. `axiomcode report` renders them as .axiomcode/assists.html.
AXIOMCODE_ASSISTS=off turns it off.
tests/assists.py: 32 checks over synthetic transcripts, a case each rule must credit and a near case it
must not, plus the hook end to end.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
plugins/axiomcode/hooks/assist.py | 46 ++
plugins/axiomcode/hooks/hooks.json | 9 +
plugins/axiomcode/skills/axiomcode/SKILL.md | 7 +
.../skills/axiomcode/scripts/ax_assists.py | 596 ++++++++++++++++++
.../skills/axiomcode/scripts/axiomcode | 6 +-
skills/axiomcode/SKILL.md | 47 +-
tests/assists.py | 245 +++++++
tests/surfaces.py | 5 +-
8 files changed, 954 insertions(+), 7 deletions(-)
create mode 100644 plugins/axiomcode/hooks/assist.py
create mode 100644 plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
create mode 100644 tests/assists.py
diff --git a/plugins/axiomcode/hooks/assist.py b/plugins/axiomcode/hooks/assist.py
new file mode 100644
index 000000000..ccdee0263
--- /dev/null
+++ b/plugins/axiomcode/hooks/assist.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+"""Stop: when the turn's work stood on an AxiomCode answer, say so under the reply (#1935).
+
+Agents finish with and without the graph, and they rarely credit the answer they acted on, so the user never sees what
+it did. At the end of each turn this reads the session's transcript and prints, for each chain new in that turn, one
+line the user can check (skills/axiomcode/scripts/ax_assists.py holds the rules):
+
+ ◆ AxiomCode this turn
+ • Redirected: the agent was in OrderService.cs, Cart.cs; `path handleOrder PriceCalc` showed PriceCalc.cs …
+
+Rules it holds itself to:
+ · NOTHING FROM THE AGENT'S TEXT. The reply has already been shown when this runs; every link is read from the tool
+ calls and their results, and a link that was not checked is not printed.
+ · SILENT unless an answer led somewhere: no query, or a query nobody acted on, prints nothing.
+ · NEVER BLOCKS. It prints a message for the user and exits 0; the agent's turn is over.
+AXIOMCODE_ASSISTS=off turns it off.
+"""
+import json, os, subprocess, sys
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'skills', 'axiomcode', 'scripts'))
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+import _host
+
+if (os.environ.get('AXIOMCODE_ASSISTS') or '').lower() in ('off', '0', 'false', 'no'):
+ sys.exit(0)
+try:
+ ev = _host.read()
+except Exception:
+ sys.exit(0)
+if _host.CURSOR: # Cursor's stop hook takes no message for the user
+ sys.exit(0)
+tr = ev.get('transcript_path') or ''
+if not tr or not os.path.isfile(tr):
+ sys.exit(0)
+cwd = ev.get('cwd') or os.getcwd()
+try:
+ r = subprocess.run(['git', '-C', cwd, 'rev-parse', '--show-toplevel'], capture_output=True, text=True, timeout=5)
+ root = r.stdout.strip() if r.returncode == 0 and r.stdout.strip() else cwd
+except Exception:
+ root = cwd
+try:
+ import ax_assists
+ msg = ax_assists.at_stop(tr, os.path.realpath(root), str(ev.get('session_id') or ''))
+except Exception:
+ sys.exit(0) # a receipt is never worth a failed turn
+if msg:
+ print(json.dumps({'systemMessage': msg}))
diff --git a/plugins/axiomcode/hooks/hooks.json b/plugins/axiomcode/hooks/hooks.json
index a97dd87f0..afe4941a9 100644
--- a/plugins/axiomcode/hooks/hooks.json
+++ b/plugins/axiomcode/hooks/hooks.json
@@ -72,6 +72,15 @@
"timeout": 5
}
]
+ },
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/run.js\" assist.py",
+ "timeout": 15
+ }
+ ]
}
],
"SessionStart": [
diff --git a/plugins/axiomcode/skills/axiomcode/SKILL.md b/plugins/axiomcode/skills/axiomcode/SKILL.md
index 838be50f0..19c7057fd 100644
--- a/plugins/axiomcode/skills/axiomcode/SKILL.md
+++ b/plugins/axiomcode/skills/axiomcode/SKILL.md
@@ -123,6 +123,13 @@ With them, a call into a dependency resolves to its declaration and a chain is t
types (`client.post(...).json()`). Library bodies are not walked, so a dependency calling back into the project is
not found this way.
+## report
+
+At the end of each turn the plugin prints, under your reply, where an answer verifiably led your work: a file the
+answer showed first that you then read or edited, or the tests it picked that you ran (Redirected / Helped / Useful).
+It is read from the session's tool calls, not from what you write, so there is nothing to claim yourself.
+`axiomcode report` renders every recorded chain as one page, `.axiomcode/assists.html`.
+
## What it cannot see — say so instead of guessing
Reflection, string dispatch, event buses; receivers the engine could not type; callbacks invoked by a library; what a
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
new file mode 100644
index 000000000..73170c977
--- /dev/null
+++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
@@ -0,0 +1,596 @@
+#!/usr/bin/env python3
+"""Where an AxiomCode answer verifiably helped the agent, read from the session record (#1935).
+
+Agents finish a task with and without AxiomCode, and they rarely say which answer changed what they did, so the user
+never sees it. This reads the host's transcript (the agent's tool calls and their results, in order) and finds chains
+an answer started:
+
+ cause an AxiomCode answer (context / impact / path / tests) names a file
+ action the file was FIRST SEEN in that answer (no earlier prompt, tool call, tool result or reply named it), and
+ the agent then read it, edited it, or ran the tests the answer picked
+ effect the edit is still in the working tree, and the picked tests that ran passed
+
+Each chain gets the strongest label it qualifies for:
+
+ Redirected Helped, and before the answer the agent was reading or editing other files in that turn, which it never
+ touched again and which are not in the final diff: the answer moved the work
+ Helped a file first seen in the answer was edited and the edit is kept, or the answer's picked tests ran and passed
+ Useful a file first seen in the answer was read (or its picked tests ran), and nothing was edited from it
+
+Nothing here comes from what the agent SAYS: its reply has already been shown when a Stop hook runs, so a claim in it
+could not be checked before the user read it. Only links that were checked are printed; a chain is cut back to the
+last link that holds, and an answer that led to nothing prints nothing. No counterfactual is stated ("grep would have
+missed it"): only what happened in the session.
+
+Chains are appended to {e(os.path.basename(os.path.abspath(root)))} · "
+ f"{len(rows)} verified chain(s) over {len(sessions)} session(s). Every chain is read from the session record: "
+ "an answer showed a file first, and the agent then read or changed it. {re.sub(r'`([^`]+)`', r' {e(os.path.basename(os.path.abspath(root)))} · "
- f"{len(rows)} verified chain(s) over {len(sessions)} session(s). Every chain is read from the session record: "
- "an answer showed a file first, and the agent then read or changed it. {re.sub(r'`([^`]+)`', r'Where AxiomCode helped
\1', e(line(r).split(': ', 1)[1]))}
" if r.get('edited') else '')
+ + (f"read: {e(', '.join(r['read']))}
" if r.get('read') else '')
+ + (f"left: {e(', '.join(r['from']))}" if r.get('from') else '') + 'impact Alpha.run' in page, (out, n))
+
+# --- the hook end to end: a Stop event in, a message for the user out; switched off, nothing
+r = repo(); change(r, 'src/pkg/alpha.py')
+t = T(r).prompt('fix the bug in run').call(MCP, {'name': 'Alpha.run'}, IMPACT_A).edit('src/pkg/alpha.py')
+ev = {'hook_event_name': 'Stop', 'session_id': 's2', 'transcript_path': t.save(), 'cwd': r, 'stop_hook_active': False}
+h = subprocess.run([sys.executable, os.path.join(HOOKS, 'assist.py')], input=json.dumps(ev), capture_output=True, text=True)
+msg = json.loads(h.stdout).get('systemMessage', '') if h.stdout.strip() else ''
+check('the Stop hook prints the receipt as a message for the user', h.returncode == 0 and msg.startswith('◆ AxiomCode') and 'Helped' in msg,
+ (h.returncode, h.stdout, h.stderr))
+ev['session_id'] = 's3'
+h = subprocess.run([sys.executable, os.path.join(HOOKS, 'assist.py')], input=json.dumps(ev), capture_output=True, text=True,
+ env=dict(os.environ, AXIOMCODE_ASSISTS='off'))
+check('AXIOMCODE_ASSISTS=off prints nothing', h.returncode == 0 and not h.stdout.strip(), h.stdout)
+h = subprocess.run([sys.executable, os.path.join(HOOKS, 'assist.py')], input='{"hook_event_name":"Stop"}', capture_output=True, text=True)
+check('a Stop event with no transcript is silent and never fails', h.returncode == 0 and not h.stdout.strip(), (h.returncode, h.stdout))
+
+print(f'\n{len(fails)} failed' if fails else '\nall passed')
+sys.exit(1 if fails else 0)
diff --git a/tests/surfaces.py b/tests/surfaces.py
index c4eb20ab8..9674cea67 100644
--- a/tests/surfaces.py
+++ b/tests/surfaces.py
@@ -29,8 +29,9 @@
MCP = os.path.join(PLUG, 'mcp', 'server.py')
CLI = os.path.join(ROOT, 'bin', 'axiomcode') # the command an install puts on $PATH
-PUBLIC = ['index', 'impact', 'path', 'tests', 'context', 'link']
-NO_MCP = {'index': 'setup, not a question: the first query through the MCP server builds the graph itself'}
+PUBLIC = ['index', 'impact', 'path', 'tests', 'context', 'link', 'report']
+NO_MCP = {'index': 'setup, not a question: the first query through the MCP server builds the graph itself',
+ 'report': 'a page for the person, not a question for the agent: the end-of-turn receipts already reach both'}
# dispatched, not advertised: verb -> why
INTERNAL = {
'build': 'the old name of index',
From 72b2678eee948931454c72eb44bef8477f1ccf10 Mon Sep 17 00:00:00 2001
From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com>
Date: Mon, 12 Oct 2026 10:37:36 -0700
Subject: [PATCH 03/17] plugin: the receipt counts the answers that led the
work and names the files and tests behind each (#1935)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Each chain now reads cause → action → effect with the concrete files edited or read and the picked test files that
passed. The header counts the answers that led the agent's work; answers that led nowhere are never counted or
shown, and a label no chain earned gets no tile in the report.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
---
plugins/axiomcode/hooks/assist.py | 7 +-
.../skills/axiomcode/scripts/ax_assists.py | 183 +++++++++++++-----
tests/assists.py | 14 ++
3 files changed, 148 insertions(+), 56 deletions(-)
diff --git a/plugins/axiomcode/hooks/assist.py b/plugins/axiomcode/hooks/assist.py
index ccdee0263..b02ada2fa 100644
--- a/plugins/axiomcode/hooks/assist.py
+++ b/plugins/axiomcode/hooks/assist.py
@@ -5,13 +5,14 @@
it did. At the end of each turn this reads the session's transcript and prints, for each chain new in that turn, one
line the user can check (skills/axiomcode/scripts/ax_assists.py holds the rules):
- ◆ AxiomCode this turn
- • Redirected: the agent was in OrderService.cs, Cart.cs; `path handleOrder PriceCalc` showed PriceCalc.cs …
+ ◆ AxiomCode this turn · 1 answer led the agent's work
+ • Helped: `impact total` showed pricing.py (new to the agent) → it edited it → both picked test files (…) pass
Rules it holds itself to:
· NOTHING FROM THE AGENT'S TEXT. The reply has already been shown when this runs; every link is read from the tool
calls and their results, and a link that was not checked is not printed.
- · SILENT unless an answer led somewhere: no query, or a query nobody acted on, prints nothing.
+ · SILENT unless an answer led somewhere: no query, or a query nobody acted on, prints nothing. Only what helped is
+ counted; the answers that led nowhere are never shown.
· NEVER BLOCKS. It prints a message for the user and exits 0; the agent's turn is over.
AXIOMCODE_ASSISTS=off turns it off.
"""
diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
index 73170c977..07bfe1ecb 100644
--- a/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
+++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_assists.py
@@ -372,7 +372,8 @@ def chains(ss, root, diff=None):
frm, ev_from = [], []
out.append({'answer': ss[i]['id'], 'turn': turn_of[i], 'ts': ss[i]['ts'], 'verb': verb, 'arg': arg[:160],
'label': label, 'shown': len(shown), 'edited': edited, 'read': read, 'from': frm,
- 'tests_ran': len(ran), 'tests_passed': len(passed),
+ 'tests_ran': len(ran), 'tests_passed': len(passed), 'tests': sorted(ran),
+ 'tests_failed': sorted(ran - passed),
# the tool calls each link stands on, by the host's id, so every chain can be checked by hand
'evidence': {'answer': ss[i]['id'], 'from': ids(ev_from), 'read': ids(sorted(set(ev_read))),
'edit': ids(sorted(set(ev_edit))), 'test': ids(ev_test)}})
@@ -404,29 +405,45 @@ def _names(fs, n=3):
return ', '.join(b[:n]) + (f' +{len(b) - n}' if len(b) > n else '')
-def _picked(c):
- n, p = c['tests_ran'], c['tests_passed']
- res = ({1: 'it passes', 2: 'both pass'}.get(n, f'all {n} pass') if p == n else f'{p} of {n} pass')
- return f"{_q(c)} picked {n} test file{'s' if n > 1 else ''}; {res}"
+def _tests(c):
+ """the effect of the tests an answer picked: which ran, and whether they pass"""
+ n, t = c['tests_ran'], c.get('tests') or []
+ if not n:
+ return ''
+ names = f" ({_names(t)})" if t else ''
+ if c['tests_passed'] == n:
+ return f" → {({1: 'its', 2: 'both'}.get(n, f'all {n}'))} picked test file{'s' if n > 1 else ''}{names} {'passes' if n == 1 else 'pass'}"
+ ok = [f for f in t if f not in (c.get('tests_failed') or [])]
+ return f" → picked test file{'s' if len(ok) > 1 else ''} {_names(ok)} {'pass' if len(ok) > 1 else 'passes'}" if ok else ""
def line(c):
- tests, n = '', c['tests_ran']
- if n:
- every = {1: 'the picked test file passes', 2: 'both picked test files pass'}.get(n, f'all {n} picked test files pass')
- tests = f'; {every}' if c['tests_passed'] == n else f"; {c['tests_passed']} of {n} picked test files pass"
+ """one chain as cause → action → effect, naming the files and tests it stands on"""
+ q, ed, rd = _q(c), c['edited'], c['read']
+ them = lambda fs: 'it' if len(fs) == 1 else 'them'
if c['label'] == 'Redirected':
- return (f"Redirected: the agent was in {_names(c['from'])}; {_q(c)} showed {_names(c['edited'])} "
- f"(first seen there), and the change landed there{tests}")
- if c['label'] == 'Helped':
- if c['edited']:
- extra = f", read {_names(c['read'])}" if c['read'] else ''
- return (f"Helped: {_q(c)} showed {_names(c['edited'])} (first seen there); "
- f"edited {'it' if len(c['edited']) == 1 else 'them'}{extra}{tests}")
- return f"Helped: {_picked(c)}"
- if not c['read']:
- return f"Useful: {_picked(c)}"
- return f"Useful: {_q(c)} showed {_names(c['read'])} (first seen there); read {'them' if len(c['read']) > 1 else 'it'}{tests}"
+ return (f"Redirected: the agent was in {_names(c['from'])} → {q} showed {_names(ed)} (new to it) "
+ f"→ it left those and edited {them(ed)}{_tests(c)}")
+ if ed:
+ extra = f", read {_names(rd)}" if rd else ''
+ return f"{c['label']}: {q} showed {_names(ed)} (new to the agent) → it edited {them(ed)}{extra}{_tests(c)}"
+ if rd:
+ return f"{c['label']}: {q} showed {_names(rd)} (new to the agent) → it read {them(rd)}{_tests(c)}"
+ n = c['tests_ran']
+ ok = 'all pass' if c['tests_passed'] == n else ''
+ return (f"{c['label']}: {q} picked {n} test file{'s' if n > 1 else ''} ({_names(c.get('tests') or [])}) "
+ f"→ the agent ran {'it' if n == 1 else 'them'}" + (f" → {'it passes' if n == 1 else ok}" if ok else ''))
+
+
+def asked(ss):
+ """the answers the session holds: [(turn, step id)]"""
+ out, turn = [], -1
+ for s in ss:
+ if s['kind'] == 'prompt':
+ turn += 1
+ elif answer_of(s):
+ out.append((turn, s['id']))
+ return out
def _load(root):
@@ -436,9 +453,11 @@ def _load(root):
with open(p, encoding='utf-8') as f:
for l in f:
try:
- out.append(json.loads(l))
+ r = json.loads(l)
except ValueError:
- pass
+ continue
+ if 'answer' in r:
+ out.append(r)
except OSError:
pass
return out
@@ -448,7 +467,7 @@ def new_chains(found, recorded, session):
"""the chains not printed before, or grown since: a stronger label, or a file edited that was not"""
best = {}
for r in recorded:
- if r.get('session') == session:
+ if r.get('session') == session and 'answer' in r:
best[r['answer']] = r
out = []
for c in found:
@@ -459,7 +478,8 @@ def new_chains(found, recorded, session):
return out
-def receipt(fresh, totals=None):
+def receipt(fresh, totals=None, turn=None, session=None):
+ """`turn` / `session`: (answers asked, answers that led to work), for the count lines"""
if not fresh:
return ''
fresh = sorted(fresh, key=lambda c: -RANK[c['label']])
@@ -468,11 +488,21 @@ def receipt(fresh, totals=None):
l = line(c)
if l not in said:
said.append(l)
- lines = ['◆ AxiomCode this turn'] + [f' • {l}' for l in said[:5]]
+ head = '◆ AxiomCode this turn'
+ if turn and turn[1]:
+ head += f" · {turn[1]} answer{'s' if turn[1] > 1 else ''} led the agent's work"
+ earlier = len({c['answer'] for c in fresh} - set(turn[2] if turn and len(turn) > 2 else []))
+ if turn and earlier:
+ head += (', and ' if turn[1] else ' · ') + f"{earlier} earlier answer{'s' if earlier > 1 else ''} went further"
+ lines = [head] + [f' • {l}' for l in said[:5]]
if len(said) > 5:
lines.append(f' • and {len(said) - 5} more (axiomcode report)')
- if totals and sum(totals.values()) > len(fresh):
- lines.append(' session: ' + ' · '.join(f'{totals[k]} {k}' for k in ('Redirected', 'Helped', 'Useful') if totals.get(k)))
+ if totals and session and session[1] > len({c['answer'] for c in fresh}):
+ t = totals
+ lines.append(f" session: {session[1]} answer{'s' if session[1] > 1 else ''} led the work · " + ', '.join(
+ f'{t[k]} {k}' for k in ('Redirected', 'Helped', 'Useful') if t.get(k))
+ + (f" · {t['_edited']} file{'s' if t['_edited'] > 1 else ''} edited from them" if t.get('_edited') else '')
+ + (f" · {t['_tests']} picked test file{'s' if t['_tests'] > 1 else ''} run" if t.get('_tests') else ''))
return '\n'.join(lines)
@@ -487,27 +517,48 @@ def record(root, session, fresh):
pass
-def totals(recorded, session):
+def _best(recorded, session=None):
+ """each answer's strongest recorded chain"""
best = {}
for r in recorded:
- if r.get('session') == session:
- if RANK.get(r['label'], 0) >= RANK.get(best.get(r['answer'], ''), 0):
- best[r['answer']] = r['label']
- t = {}
- for l in best.values():
- t[l] = t.get(l, 0) + 1
+ if 'answer' in r and (session is None or r.get('session') == session):
+ k = (r.get('session'), r['answer'])
+ if k not in best or RANK.get(r['label'], 0) >= RANK.get(best[k]['label'], 0):
+ best[k] = r
+ return best
+
+
+def totals(recorded, session):
+ """labels per answer, plus `_edited` (files edited from an answer) and `_tests` (picked test files run)"""
+ t, edited, tests = {}, set(), set()
+ for r in _best(recorded, session).values():
+ t[r['label']] = t.get(r['label'], 0) + 1
+ for r in recorded:
+ if r.get('session') == session and 'answer' in r:
+ edited |= set(r.get('edited') or [])
+ tests |= set(r.get('tests') or [])
+ if edited:
+ t['_edited'] = len(edited)
+ if tests:
+ t['_tests'] = len(tests)
return t
def at_stop(transcript, root, session):
"""the receipt for the turn that just ended, recorded; '' when there is nothing new"""
- found = chains(steps(transcript), root, working_diff(root))
+ ss = steps(transcript)
+ found = chains(ss, root, working_diff(root))
recorded = _load(root)
fresh = new_chains(found, recorded, session)
+ q = asked(ss)
+ last = sum(1 for s in ss if s['kind'] == 'prompt') - 1
+ record(root, session, fresh)
if not fresh:
return ''
- record(root, session, fresh)
- return receipt(fresh, totals(recorded + [dict(c, session=session) for c in fresh], session))
+ led = {c['answer'] for c in found}
+ now = [i for t, i in q if t == last]
+ return receipt(fresh, totals(recorded + [dict(c, session=session) for c in fresh], session),
+ turn=(len(now), len(led & set(now)), now), session=(len(q), len(led)))
# ---- the report -----------------------------------------------------------------------------------------------------
@@ -524,39 +575,62 @@ def at_stop(transcript, root, session):
.s{margin:28px 0 8px;color:var(--mut);font-size:13px;text-transform:uppercase;letter-spacing:.04em}
.c{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin:8px 0}
.l{font-weight:600;font-size:13px}.Redirected{color:var(--r)}.Helped{color:var(--h)}.Useful{color:var(--u)}
-.c p{margin:4px 0 0}.c small{color:var(--mut)}code{font:13px ui-monospace,Menlo,monospace}
+.c p{margin:4px 0 0}.c ol{margin:6px 0 0;padding-left:20px;color:var(--mut);font-size:13px}.c ol b{color:var(--fg);font-weight:600}.tile small{font-size:14px;color:var(--mut)}.c small{color:var(--mut)}code{font:13px ui-monospace,Menlo,monospace}
"""
+def _steps_html(r):
+ """a chain as its links, each one checkable: what the answer gave, what the agent did, what came of it"""
+ e = html.escape
+ out = [f"{e(_q(r).strip('`'))}' + ''.join(out) + '
'
+
+
def report(root):
- rec = _load(root)
- best = {}
- for r in rec:
- k = (r.get('session'), r['answer'])
- if k not in best or RANK.get(r['label'], 0) >= RANK.get(best[k]['label'], 0):
- best[k] = r
- rows = sorted(best.values(), key=lambda r: r.get('at') or '', reverse=True)
+ rows = sorted(_best(_load(root)).values(), key=lambda r: r.get('at') or '', reverse=True)
t = {}
for r in rows:
t[r['label']] = t.get(r['label'], 0) + 1
sessions = {}
for r in rows:
sessions.setdefault(r.get('session') or '?', []).append(r)
+ edited = {(r.get('session'), f) for r in _load(root) for f in r.get('edited') or []}
+ tests = {(r.get('session'), f) for r in _load(root) for f in r.get('tests') or [] if f not in (r.get('tests_failed') or [])}
e = html.escape
body = [f"Where AxiomCode helped
\1', e(line(r).split(': ', 1)[1]))}
" if r.get('edited') else '')
- + (f"read: {e(', '.join(r['read']))}
" if r.get('read') else '')
- + (f"left: {e(', '.join(r['from']))}" if r.get('from') else '') + '
{e(_q(r).strip('`'))}{e(f)}{e(_q(r).strip('`'))}{e(f.rsplit('/', 1)[-1])} passed{e(x['out'])}" if x.get('out') else ''
+ out.append(f"{e(x['text'])}{o}{e(os.path.basename(os.path.abspath(root)))} · " - f"{len(sessions)} session(s). Every line is read from the session record, not from what the agent wrote: " - "an answer named a file the agent had not seen, and the agent then read or changed it, or ran the tests it " - "picked.
Across {pl(ns, 'coding session')}, AxiomCode identified files agents had not yet explored, " + f"guided the edits that followed, and selected tests the agents ran and passed.
Each answer below is counted under every way it showed.
{d}
The strongest chains, each read step by step from the session record.
") + for r in feat: + badge = 'Observed edit + tests passed' if r.get('edited') and r.get('tests_passed') else ( + 'Observed edit' if r.get('edited') else 'Observed read') + body.append(f"{e(_q(r).strip('`'))}Open any answer to see the query, AxiomCode's rows, and each later action with its sequence " + "number in the session.
{re.sub(r'`([^`]+)`', r'\1', e(line(r).split(': ', 1)[1]))}
\1', e(line(r).split(': ', 1)[1]))}file:line) before any earlier prompt, tool call,
+tool output or reply had named it.This report shows that AxiomCode's answers were followed by these agent actions. It does not measure what the +agent would have done without them, or whether the edits were correct; a passing test supports only that the selected test +run passed.