Repository navigation
Releases: MohammadMD1383/jdx
Release list
v1.6.1
jdx v1.6.1 — usages --kind throw says what it means
Patch release: no behaviour change, no output shape change, no exit code
change. jdx usages --kind throw has always read throws declarations
(declaration-level — "who declares this type"), never ATHROW throw sites
("who actually throws it"). Attributing a throw site needs data-flow analysis
the reference extractor deliberately skips. Until now that took source-diving
to learn; now the help text, the MCP tool description, and the docs say it
everywhere (closes
#7).
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.6.1-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.6.1-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
What changed
throw = throws declarations, not ATHROW sites — stated explicitly in:
jdx usages --helpand the--kindflag help (UsagesCommand)- the MCP
usagestoolkindparameter description docs/COMMANDS.md(command table),docs/PROPOSAL.md(§7.3),
docs/guide/navigating-code.md(kind table + Limitations bullet)- KDoc on
JdxService(UsageKindFilter, T-075 comment,
collectMetadataUsages),ReferenceExtractor(ATHROWintentionally not
extracted),RpcDispatch(usageskindcontract)
The follow-up commit also fixes the guide naming a nonexistent jdx flow:
throw-site attribution is deferred to future work without naming a command
that does not exist yet.
Known limitations
usages --kind throwremains declaration-level: methods whosethrows
clause declares the type. Throw-site (ATHROW) attribution needs data-flow
analysis and is deferred to future work.- Same standing notes as
v1.6.0: two
setup backends still missing (Windsurf
#38, Aider
#41); version-probe
caveats
#84 /
#85;MULTI_RELEASE_VARIANT
provenance
#29.
Testing
- Existing
UsagesServiceTest.kind throw shows throws declarationsalready
pins the semantics;UsagesServiceTest(34 passed),UsagesCommandTest
(6 passed),lintclean (per PR #93). - Release gate: full
./gradlew check -Pciplus per-OS distribution builds
and launcher smoke tests, all green.
PRs in this release
| PR | What |
|---|---|
| #93 | Clarify throw as throws declarations (closes #7) |
Full Changelog: v1.6.0...v1.6.1
v1.6.0
jdx v1.6.0 — docs website live, Antigravity setup, sturdier body
Minor release: the documentation website is live (generated from the docs and
the real CLI, with gates that stop it drifting), jdx setup grew its eighth
agent backend (Antigravity, verified against a real install), and jdx body
no longer gives up on one bad Vineflower token. No query command changes
behaviour, no output shape changes, and no command exit code changes — if you
script against jdx, nothing here needs re-learning.
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.6.0-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.6.0-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
Docs website is live
The GitHub Pages site (mohammadmd1383.github.io/jdx)
is generated from the repository's Markdown files and the freshly built jdx
binary by a new build-time-only :site Gradle module — never shipped in the
product distribution.
What keeps it honest, on every PR:
- Exec blocks:
console execcode blocks in the docs are executed against
the built binary and their real output is embedded — if an exit code or an
output line changes, the build fails instead of publishing stale docs. - Doc lint: every
jdx <cmd> --flagmention in the docs is validated
against the real CLI's--help, so a renamed flag breaks the build, not a
user's copy-paste. - Link checker: all internal links and anchors must resolve.
- Autogenerated reference: the command reference pages are rendered from
actual help output, and every HTML page gets a Markdown twin for
language-model consumption.
Riding along in the same PR: layered user guides and FAQ, the green accent
colour, release-note heading nesting without skipped levels, and Lighthouse as
a warn-only gate that never blocks deploy.
jdx setup --agent antigravity
The eighth backend, verified against the real agy 1.3.0 binary (installed via
the official installer — not just the docs):
--agent |
Project scope | System scope | Version probe |
|---|---|---|---|
antigravity |
.agents/mcp_config.json, walk-up |
~/.gemini/config/mcp_config.json |
agy --version |
The entry is the minimal shape agy mcp list recognises:
{ "mcpServers": { "jdx": { "command": "jdx", "args": ["mcp"] } } }$ jdx setup --agent antigravity --scope project
antigravity project setup installed (mcpServers entry)
/home/you/project/.agents/mcp_config.json
next: restart antigravity (config loads once at startup)Real-binary findings: system scope writes the user-level file only (all of
agy mcp add/list/enable/disable/remove operate there); agy mcp
never writes the project file, so jdx hand-edits it in the same mcpServers
shape. Bidirectional interop was proven with the built jdx both ways —
jdx-written files pass agy mcp list, agy-written bytes pass jdx --check
(exit 0, byte-identical no-op) — and jdx --remove leaves agy mcp list
empty. Same merge discipline as the other seven backends: unrelated servers
survive, re-runs are byte-exact no-ops, --check reports without writing
(exit 1 when absent).
Fix: jdx body survives one bad Vineflower token (#87)
body / source --around locate members by parsing decompiled text with
JavaParser. A single rejected token — Vineflower emitting import foo.1; /
this.1 for synthetic $1 siblings (Found ".1" expected "." ";") — failed
the whole file, so a member that members lists and javap proves was
reported not found, while source served the same file fine unparsed.
The fix adds a lenient seam (parseJavaUnitLenient / loadJavaUnitLenient /
findJavaBodiesLenient): the offending Found "..." token is blanked to
spaces — line breaks and lengths preserved, so ranges still address the
original lines and slices stay verbatim — and parsing retried, up to 10x.
Paired -sources.jar input stays strict (still an honest ParseError /
exit 5). If even lenient parsing fails, the error now says the member exists
in bytecode and names the file view that still works
(jdx source 'TYPE' --engine vineflower) alongside the --engine javap
hatch. Exit codes are unchanged, so the default-ladder retry still works.
Test infrastructure: categories, budgets, progress
Also in this release: tests are categorised, per-test time budgets are
enforced (test stays a fast unit loop), and the runner logs live progress —
so a slow or hung suite points at the culprit instead of a silent minute.
Known limitations
- Two agents from the umbrella are still missing: Windsurf
(#38) and Aider
(#41), tracked under
#32. Antigravity (#34)
closed with this release. - Version detection is a best-effort probe: the raw probe output is
not collapsed to one line, so a chatty binary spills a multi-line,
absolute-path-bearingWARNINGacross thedetected:line in text mode
(#84; the--json
envelope escapes it correctly and is fine); and the agent-config root comes
from the JVM'suser.home, which on Linux comes fromgetpwuid()and
therefore ignores$HOME
(#85). - Attribution still covers reader warnings; the
MULTI_RELEASE_VARIANT
provenance line does not say which variant was selected
(#29).
Testing
- Website: the
pagesworkflow builds and validates the site on the PR;
lint,:site:testand:site:buildSitegreen. - Antigravity: 20 new
SetupServiceTestcases (incl. a real-agy-bytes
golden pin and generative fixed-point families), 9 newSetupCommandTest
cases, 3 new byte-pinned goldens, 3 new doctor tests — all with fake homes;
545 unit tests, full integration suite,lint,:site:test,
:site:buildSitegreen. - Lenient slicing: 8 new
JavaBodiesLenientTesttests (incl. a property
test: injected bad imports still slice via lenient over generated members),
plusBodyServiceTest(repairable forced-vineflower succeeds exit 0 with
vineflower provenance; unrecoverable names the file view + javap in text
and JSON) andSourceServiceTest(--aroundwith one bad token succeeds);
:sources:integrationTest140 pass,:index:check651 pass,:cli:check
green.
PRs in this release
| PR | What |
|---|---|
| #89 | GitHub Pages site generated from the docs and the real CLI |
| #90 | jdx body survives a bad Vineflower token |
| #91 | Antigravity setup support, agy-verified |
Full Changelog: v1.5.0...v1.6.0
v1.5.0
jdx v1.5.0 — jdx setup: every agent you actually use
Minor release: jdx setup grew from one agent backend to seven,
jdx doctor gained an AppCDS row, and three defects were fixed — two of which
could make jdx report the wrong thing, and one of which had a quality gate
silently not running at all. No query command changes behaviour, no output
shape changes, and no command exit code changes — if you script against jdx,
nothing here needs re-learning. jdx setup --agent opencode behaves exactly as
it did in v1.4.0, byte for byte.
In v1.4.0 you could paste an opencode.json snippet by hand and jdx would
write it correctly. The obvious follow-up question was the one everybody
actually has: which config file does my agent read? This release answers
it for the seven agents the project verified against real installs, writes the
entry in that agent's own shape, and reports which line it found — rather than
guessing a shape and hoping.
--agent |
Project scope | System scope | Version probe |
|---|---|---|---|
opencode (v1.4.0) |
opencode.json/opencode.jsonc, walk-up |
~/.config/opencode |
opencode --version → v1/v2 line |
claude-code (alias claude) |
.mcp.json, walk-up |
~/.claude.json |
claude --version |
cursor |
.cursor/mcp.json, walk-up |
~/.cursor/mcp.json |
cursor-agent --version (legacy agent shim fallback) |
kilo |
.kilo/kilo.json[c], else kilo.json[c], walk-up (fresh default .kilo/kilo.json) |
~/.config/kilo/kilo.json[c] |
none (Kilo has no classified line) |
cline |
~/.cline/data/settings/cline_mcp_settings.json |
same single global file | none |
codex |
.codex/config.toml, walk-up |
$CODEX_HOME/config.toml, else ~/.codex/config.toml |
codex --version |
copilot |
.mcp.json, walk-up, .github/mcp.json fallback |
$COPILOT_HOME/mcp-config.json, else ~/.copilot/mcp-config.json |
copilot --version |
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.5.0-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.5.0-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
jdx setup --agent claude-code
The shape is the one claude mcp add jdx -- jdx mcp would write, so an agent
that already knows the entry recognises it:
$ jdx setup --agent claude-code --scope project
claude-code project setup installed (mcpServers entry)
/home/you/project/.mcp.json
detected: claude-code (2.1.283 (Claude Code))
next: restart claude-code (config loads once at startup){ "mcpServers": { "jdx": { "command": "jdx", "args": ["mcp"] } } }Project scope walks up for an existing .mcp.json and writes one in the
checkout root if there is none; system scope targets ~/.claude.json.
jdx setup --agent cursor
Same mcpServers.jdx shape — Cursor reads it from .cursor/mcp.json (project)
or ~/.cursor/mcp.json (system), verified against
cursor-agent 2026.09.26-dd393fe via agent mcp list.
jdx setup --agent kilo
Kilo Code is an OpenCode fork sharing the mcp map shape, so it gets the
single mcp.jdx entry (not OpenCode's v1+v2 pair — there is no second line
to cover). Fresh kilo.json[c] files carry no $schema. Project scope walks
up, preferring the .kilo/ variant at each level — within one directory the
order is .kilo/kilo.jsonc, .kilo/kilo.json, kilo.jsonc, kilo.json — and
a checkout with no Kilo config gets a fresh .kilo/kilo.json.
Kilo Code and Cline are the two backends that report no detected: line:
neither has a config-line version to classify, and jdx does not invent one.
jdx setup --agent cline
Cline's entry is nested one level deeper — it is a transport object:
{ "mcpServers": { "jdx": { "transport": { "type": "stdio", "command": "jdx", "args": ["mcp"] } } } }Verified against the real install (cline CLI 3.0.65 + extension 4.1.21:
resolveMcpSettingsPath; cline/cline#2418 open): Cline keeps no
project-level MCP file, so --scope project and --scope system name the
same global ~/.cline/data/settings/cline_mcp_settings.json. That is the
verified layout, not a fallback, so the output says so out loud rather than
letting a project-scoped run look checkout-local:
$ jdx setup --agent cline --scope project
cline project setup installed (global file — Cline keeps no project-level MCP config)
/home/you/.cline/data/settings/cline_mcp_settings.json
next: restart Cline (config loads once at startup)The probe also accepts the legacy flat command/args shape, so --check and
doctor report a Cline entry written by an older jdx.
jdx setup --agent codex
Codex CLI config is TOML, so the entry is a [mcp_servers.jdx] table and the
merge is line-based rather than a JSON tree rewrite — every other key in
config.toml survives as written:
[mcp_servers.jdx]
command = "jdx"
args = ["mcp"]Verified against codex-cli 0.157.1. Project scope targets
.codex/config.toml (Codex only reads it in a trusted checkout); system scope
honours $CODEX_HOME.
jdx setup --agent copilot
The GitHub Copilot CLI entry is mcpServers.jdx with "type": "local", in
.mcp.json (project — the nearest one walking up, .github/mcp.json as
fallback, trusted directories) or $COPILOT_HOME/mcp-config.json, else
~/.copilot/mcp-config.json. Verified against GitHub Copilot CLI 1.0.88. A
hand-written "type": "stdio" entry probes as installed; jdx writes local.
Every backend still merges, never clobbers
Unchanged from v1.4.0 and now true for all seven: unrelated servers, models,
providers and every unrelated top-level key survive; only the jdx entry is
added, replaced, or removed. Re-runs are byte-exact no-ops, so comments and
formatting survive. --check reports without writing (exit 1 when absent),
--remove uninstalls cleanly, and exit codes are unchanged: 0
installed/removed/present, 1 checked-absent, 3 bad usage, 5
unreadable config.
One backend-specific fix worth calling out: isJdxCommand accepts Windows
shims (jdx.exe, jdx.cmd, jdx.bat) and backslash paths on every host,
so --check and the doctor row stay truthful on Windows instead of reporting
"not installed" for an entry jdx itself wrote.
jdx doctor grew an appcds row
The AppCDS archive (libs/jdx.jsa, trained per-OS by :app:installDist) was
invisible to doctor — the one thing users asked about when cold start felt
slow. It is now a stat-only row (size + mtime against the fat jar, no JVM
probe, no network):
| State | Reported |
|---|---|
| present | ok — path and size |
| absent | warn — cold start without AppCDS; rebuild with ./gradlew :app:installDist |
| empty (0 bytes) | warn — corrupt or truncated; rebuild |
| older than the fat jar | warn — stale; rebuild |
| unresolvable (dev layout, no jar) | warn — ship with :app:installDist |
Absent or stale is a warn, never a fail: the archive only affects speed, and
-Xshare:auto degrades to a plain run. Only the generic probe wrapper turning a
thrown exception into fail can make this row fail.
install.sh now also prints a one-line note when it installs from a build with
no archive, pointing at this row rather than failing.
The setup row was widened in the same change: it now reports wiring for all
seven backends, on both scopes, with each backend's version classification.
Fix: a stale fat jar can no longer report the wrong version (#68)
A versioned -PjdxVersion build on top of a default one left both
jdx-0.1.0-SNAPSHOT-all.jar and jdx-1.3.0-all.jar in app/build/libs. The
launcher globbed and took the first, so jdx version could report the wrong
version — and jdx upgrade, which decides what to do from that version, was
misled with it. Fixed on both sides:
- the
appbuild evicts any supersededjdx-*-all.jarafter writing its own,
solibsholds exactly one; - the launcher treats more than one fat jar as an ambiguous install and
fails loudly, naming every jar it found, instead of picking one:
$ jdx version
jdx: multiple jdx fat jars in '/opt/jdx/libs': '/opt/jdx/libs/jdx-1.4.0-all.jar' '/opt/jdx/libs/jdx-1.5.0-all.jar' — rebuild to leave exactly one: ./gradlew :app:clean :app:installDistFix: a stale AppCDS archive is now silent (#54, #22)
The archive is keyed to the exact JDK build, so on a different vendor or
update the JVM rejects it and logged a [warning][cds] version-mismatch notice.
Output and exit codes were always correct — but the notice lands on stdout,
where it corrupts piped and --json output, and on stderr it reads like a
crash for a command that then works fine. Every launcher now passes
-Xlog:cds=off (after -Xshare:auto, before -XX:SharedArchiveFile), so the
degradation is silent: exit 0, correct output, empty stderr, just without the
archive speedup. Pinned end-to-end by a launcher test that feeds a garbage
archive and still requires a clean jdx version.
CI: the core mutation gate can run again (#66)
The core ≥ 80 % mutation gate has never once run in CI. PIT aborts in its
pre-scan — before generating a single mutant, with no score produced at all —
when the fixture corpus is missing, and on a clean runner it always was. Two
independent causes: a PIT minion is a fresh JVM that inherits neither the
test/tier2Test system properties nor a dependsOn on :testfixtures:build,
so it found no corpus. This is worse than no gate: a gate that cannot run
looks exactly like a passing one.
Fixed by wiring both corpus properties (jdx.fixturesDir, `jdx.diff...
v1.4.0
jdx v1.4.0 — jdx setup: jdx in your agent, in one command
Minor release: one new command, one exit-code fix that closes a hole in the
contract, one diff provenance fix. No query command changes behaviour and no
output shape changes — if you script against jdx, the only thing to re-learn is
that a mistyped command now fails instead of exiting 0.
Two things an agent could not do before: wire jdx into its own editor's
config (paste a JSON snippet from the README in the right shape for the right
OpenCode line, and get mcp.servers vs mcp right), and trust that a usage
error is an error — jdx nosuchcommand exited 0, so a typo looked like a
successful run, in every release up to v1.3.0.
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.4.0-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.4.0-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
jdx setup --agent opencode
One command wires jdx into OpenCode, in the checkout or for the whole machine:
$ jdx setup --agent opencode --scope project
opencode project setup installed (v1+v2 entries)
/home/you/project/opencode.json
detected: opencode v2 (opencode v2.0.18)
next: restart opencode (config loads once at startup)Both OpenCode lines get wired, always, because they disagree about the shape
of an MCP entry and only one of them is what a given install actually reads:
- v1 (
opencode 1.x): servers sit directly under the top-levelmcp
object keyed by name, each
{ "type": "local", "command": ["jdx","mcp"], "enabled": true }. - v2 (
0.0.0-next-*/0.0.0-beta-*/0.0.0-dev-*/2.x): servers sit
undermcp.servers, andenabledis replaced by its inversedisabled.
So setup writes both entries and reports the line it detected rather than
guessing which one your install reads:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"jdx": { "type": "local", "command": ["jdx", "mcp"], "enabled": true },
"servers": {
"jdx": { "type": "local", "command": ["jdx", "mcp"], "disabled": false }
}
}
}It merges; it never clobbers. Every other server in mcp and in
mcp.servers, every model/provider/theme key, and every unrelated top-level key
survives — only mcp.jdx and mcp.servers.jdx are added, replaced, or removed.
The file is read leniently (JSONC comments and trailing commas, which is what a
real opencode.jsonc carries), and a second run is a byte-exact no-op, so
your comments and formatting survive a re-run:
$ jdx setup --agent opencode --scope project
opencode project setup already installed (no changes)
/home/you/project/opencode.json
detected: opencode v2 (opencode v2.0.18)A v1 server entry literally named servers cannot share the key with the v2
namespace, so the foreign entry wins untouched and only the v1 jdx entry is
written. Merge discipline beats completeness.
Flags and exit codes
| Flag | Meaning |
|---|---|
--agent <name> |
Only opencode so far; parsed case- and separator-insensitively (OpenCode, open-code). Other backends follow the same seam — see the limitations. |
--scope project|system |
project (default): the nearest opencode.json[c] walking up from the checkout, else a fresh opencode.json there. system: the existing ~/.config/opencode/opencode.jsonc (what a real install carries), else a fresh opencode.json in ~/.config/opencode. |
--check |
Report without writing anything. Exit 1 when absent, 0 when present — a CI step can assert wiring without changing it. |
--remove |
Uninstall: deletes both jdx entries, drops an emptied mcp.servers and then an emptied mcp, keeps every other server. Idempotent. |
--json |
The same information in the standard envelope. |
Exits: 0 installed / removed / checked-present, 1 checked-absent, 3 bad
usage, 5 an unreadable or corrupt config (the file is named in the message and
is never rewritten).
$ jdx setup --agent opencode --scope project --check
opencode project setup not installed
/home/you/project/opencode.json
detected: opencode v2 (opencode v2.0.18)
next: jdx setup --agent opencode --scope project
$ echo $?
1
$ jdx setup --agent cursor
usage error: unsupported --agent 'cursor' (only --agent opencode so far)
$ echo $?
3Restart the agent afterwards — it reads its config once at startup.
jdx doctor grew a setup row
jdx doctor now answers "is jdx wired into my agent, and which line is reading
it?" without you having to ask:
$ jdx doctor
jdx doctor
jdk: ok (21 (/usr/lib/jvm/java-21-openjdk))
…
setup: ok (opencode v2 (opencode v2.0.18): installed (/home/you/project/opencode.json); no system config (/home/you/.config/opencode/opencode.json))The row is ok when either scope is wired and warn when neither is — setup is
optional, so it never fails — and it names the file it looked at plus the exact
command to fix it. The version probe is the same pure seam setup uses, so
doctor and setup can never disagree about which line is installed.
Fix: a usage error is now an error (exit 3)
This closed the largest hole in the exit-code contract. Clikt reports a usage
error by handing a status code to its own Context.exitProcess seam, and
clikt-core's default seam is a no-op (the real one lives in the mordant flavor
jdx excludes), so every one of these printed a perfectly correct message and
exited 0:
$ jdx members ; echo $?
Usage: jdx members [<options>] <ref>
Error: missing argument <ref>
0 # v1.3.0 and earlier — v1.4.0 prints 3
$ jdx nosuchcommand ; echo $?
Usage: jdx [<options>] <command> [<args>]...
Error: no such subcommand nosuchcommand
3
$ jdx members java.lang.String --nope ; echo $?
Usage: jdx members [<options>] <ref>
Error: no such option --nope. (Possible options: --no-daemon, --no-jdk, --no-color, --repo)
3They now exit 3 — the code AGENTS.md §6 has reserved for usage errors since
v1.0.0 — while --help and --version still exit 0 and every service-level
code is untouched (3 for a bad flag value, 5 for an unreadable artifact, …). The
mapping is 1 → 3, installed once on the root Context so every subcommand
inherits it; nothing about the message format changed.
If a script of yours ran jdx … and checked only for 0, a typo in it now
stops the pipeline instead of passing silently. That is the point.
Fix: a diff warning now names the artifact it came from (#64)
Found by dogfooding jdx diff on the gson 2.11.0 → 2.14.0 upgrade. gson's
nest-marker classes have binary names that start with an empty $ segment
(com/google/gson/internal/$Gson$Types.class), which the reader cannot model, so
it degrades. With two inputs the warning could not say which side produced it —
and "was this class dropped from the old jar, or is it new in the new one" is
exactly the question a diff exists to answer:
$ jdx diff ~/.gradle/caches/…/gson-2.11.0.jar ~/.gradle/caches/…/gson-2.14.0.jar
api diff gson-2.11.0.jar -> gson-2.14.0.jar (public surface)
…
warning MULTI_RELEASE_VARIANT: in gson-2.11.0.jar: multi-release gson-2.11.0.jar serves version-specific variants for this JDK (21)
warning UNNAMEABLE_CLASS: in gson-2.11.0.jar: unnameable class name (empty name segment in [, Gson, Types]): com/google/gson/internal/$Gson$Types.classThat single word — in gson-2.11.0.jar: — immediately disproved the assumption
the report used to hide behind: those classes are in the old jar, not the
new one. Both sides' warnings are now attributed before they are unioned, and
the consequences are deliberate in both directions:
- the same unreadable class in two differently-named jars is two facts, so
it now reports once per side instead of collapsing into one anonymous line; - a self-diff (
jdx diff a.jar a.jar) still prints one line, because the two
attributed messages are then identical.
The reader's own warning text is unchanged — it is shared by every command and
pinned in other goldens. Only diff's union copies it, so no other command's
output moved a byte.
Known limitations
- Only OpenCode is wired.
jdx setup --agent cursor|copilot|cline|windsurf| kilo-code|aider|codexexits 3 naming what is supported. The eight follow-ups
are filed and tracked (#36–#42);
each is one enum entry plus a merge rule behind the same seam. - The v1 MCP entry is deprecated upstream and slated for removal by
OpenCode. jdx keeps writing both entries until v1 support is dropped from jdx
— a deliberate, temporary compatibility cost, not an oversight. jdx setupis not served by the daemon and has no MCP tool or HTTP
route: it writes to your config, not to a workspace, and there is nothing for a
remote adapter to answer. It is a local maintenance command likejdx upgrade
andjdx cache.- Version detection is a best-effort probe of
opencodeonPATH(falling back
to the legacyopencode2shim only when noopencodeexists). A binary that
answers with something unparseable reportsopencode version unknown (<raw output>)so a human can judge — jdx never guesses a line. - Attribution now covers reader warnings; the
MULTI_RELEASE_VARIANTprovenance
line still does not say which variant was selected, only which side produced
the warning. - Unchanged since v1.2.x and still shipped: the per-OS
libs/jdx.jsalogs
shared archive file version … does not matchnoise on a JDK build ...
v1.3.0
jdx v1.3.0 — jdx diff: public-API diff with binary-compat warnings
Minor release: one new command, and nothing that already shipped changes
behaviour. jdx diff answers the question a dependency upgrade poses — what
changed, and can I still compile and link against it? — with every difference
named by a rule code an agent can filter on, most breaking first.
A real upgrade, on the surface a consumer compiles against — this one is clean, so
the CI gate passes and the answer is that you can take the upgrade:
$ jdx diff gson-2.11.0.jar gson-2.14.0.jar --fail-on breaking
api diff gson-2.11.0.jar -> gson-2.14.0.jar (public surface)
info TYPE_ADDED com.google.gson.internal.GsonTypes
info TYPE_ADDED com.google.gson.internal.bind.JsonElementTypeAdapter
info MEMBER_ADDED com.google.gson.FieldNamingStrategy#alternateNames(java.lang.reflect.Field)
…
0 breaking · 0 suspicious · 17 informational · 2 types added · 0 types removed
fail-on breaking: no breaking changes (exit 0)The same two jars on the all surface, where the internal churn a consumer
never sees is also reported — 118 breaking findings, which is the point of the
flag for refactors and mod/mixin work:
$ jdx diff gson-2.11.0.jar gson-2.14.0.jar --visibility all --severity breaking
api diff gson-2.11.0.jar -> gson-2.14.0.jar (all surface)
breaking TYPE_REMOVED com.google.gson.Gson$1
breaking TYPE_REMOVED com.google.gson.Gson$2
…
118 breaking · 11 suspicious · 101 informational · 32 types added · 29 types removedBoth sides may be a jar, a class directory, a glob, or a Maven coordinate
(--fetch to download; ~/.gradle/caches and ~/.m2 are used without it). No
workspace is needed and no index is built — a diff names its own two artifacts.
Warm, two real jars compare in 230–250 ms.
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.3.0-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.3.0-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
What jdx diff reports
47 rules at three severities (16 breaking, 11 suspicious, 20
informational). The rule id is the CompatRule enum name, so a caller filters
on a code and never on prose — MEMBER_REMOVED, RETURN_TYPE_CHANGED,
STATIC_TO_INSTANCE, ENUM_CONSTANT_REMOVED, KOTLIN_SUSPEND_CHANGED, and so
on. Full set in docs/PROPOSAL.md §7.7.
A member's identity is its name plus the erased parameter list, so a
covariant return is one RETURN_TYPE_CHANGED rather than a removal plus an
addition, and a field's type is part of its identity. Two pairing passes keep
the report free of remove/add avalanches: shape (name + arity) first, then
Kotlin name and Kotlin arity for JVM renames and suspend changes. An
ambiguous bucket is never paired — with two removals of one shape, guessing
which addition replaced which is worse than reporting both.
The inheritance check refuses to guess. A member gone from its type but
declared by one of the type's new supertypes is MEMBER_MOVED_TO_SUPERTYPE,
not a removal — the caller's invokevirtual still resolves through the chain.
When the walk reaches a supertype the artifact does not hold, absence proves
nothing, so the removal stays MEMBER_REMOVED and the detail names the
supertype it could not read. A diff never downgrades a breaking change on a
maybe. The universal roots (java.lang.Object and friends) are excluded from
that walk, because counting java.lang.Object as "missing from the artifact"
put an "inheritance not checked" caveat on every removal in every jar.
Structure comes from bytecode alone — no sources jar, no decompilation, no
classpath — because a diff has no "flesh" to recover.
Exit codes — one documented exception
A comparison that ran exits 0 whatever it found. The answer is in the
report, not the process status, because "no differences" and "differences that
do not matter to you" are the same outcome and only you can tell them apart.
--fail-on breaking (or any) is the CI gate and the only way a successful
diff exits non-zero:
jdx diff old.jar new.jar --fail-on breaking # exit 1 when a breaking change is presentThe verdict is also in the envelope as result.gate, so a consumer that cannot
read exit codes still gets the answer. This is the single documented exception
to the exit-code table in AGENTS.md §6 / PROPOSAL.md §16; everything else is
unchanged (3 for a bad spec or flag, 5 for an unreadable artifact).
Flags
| Flag | Meaning |
|---|---|
--visibility public|all |
public (default) is the surface a consumer compiles against; all compares every declared member, for refactors and mod/mixin work. |
--severity all|suspicious|breaking |
Cumulative: suspicious = breaking + suspicious. The per-severity tally always describes the whole comparison, never only what was printed. |
--fail-on none|breaking|any |
Turn the report into a process status. |
--limit <int> |
Maximum findings shown (default 200); the tail this cuts is by construction the least severe. |
--include-synthetic |
Compare bridge/synthetic members too, hidden by default. |
--fetch, --repo |
Maven coordinate resolution, as on every other command. |
All four interfaces
CLI, MCP (jdx_diff), the daemon wire, and HTTP (GET /v1/diff?query=<old>&new=<new>)
— the same JdxService.diff call, verified byte-identical between the CLI and
HTTP adapters.
jdx diff is deliberately not forwarded to the daemon, and there is
deliberately no --no-daemon flag: a daemon warm on one workspace's
classpath holds nothing a two-artifact comparison can reuse, and a --fail-on
verdict has no way back over the v1 wire. A flag that does nothing is a lie, so
it is absent rather than inert — jdx --no-daemon diff a b parses,
jdx diff --no-daemon a b does not. The daemon still serves diff.
Fixes that shipped with it
Found by the new test families, not by inspection — each is a case where the
report was confidently wrong:
- A spurious caveat on every removal.
MEMBER_REMOVEDcarried "supertype
java.lang.Objectis not in new.jar; inheritance not checked" on every
removal in every jar, because every class extendsjava.lang.Object. The
universal roots are now skipped in the inheritance walk, and a private removal
skips the walk entirely (JLS §8.2). SUPERTYPE_REMOVED java.lang.Object. A class that becomes an interface, or
that now names a real superclass, dropsjava.lang.Objectfrom its header —
reported as a lost supertype, on a change already reported as
TYPE_KIND_CHANGEDorSUPERTYPE_ADDED.FIELD_TYPE_CHANGEDprinted its ref twice. A field's canonical ref carries
no type, so the detail readnow: pkg.Type#fieldon both sides of the arrow.
It now readsint -> long, which is the whole news.ANNOTATION_VALUES_CHANGED kotlin.Metadataon every Kotlin edit. The
compiler's own bookkeeping changed value on every declaration touched, so
every Kotlin diff carried a line that is always true, burying the real
findings. The internal annotations are now excluded; a JetBrains@Nullableis
still compared, because that one is real declared API.- Every degradation warning was printed twice on a self-diff.
- macOS and Windows could not build the new test fixtures. The Kotlin plugin
contributes a defaultsrc/diffV1/kotlinfor source setdiffV1; an added
src/diffv1/kotlinis a different path on Linux and the same directory on a
case-insensitive filesystem, so every fixture reached the compiler twice
(CLASSIFIER_REDECLARATION). Fixed withsetSrcDirsand source-set-cased
directory names. - Windows could not run the glob tests.
*is not a legal character in a
Windows path, so four tests that built a glob argument with
Path.resolve("*.jar")threw before their first assertion. A spec is matched,
never opened — a glob is a perfectly good argument.
Known limitations
- A public member that is no longer declared but was an override of a
universal root's method (toString,equals, …) still links through that
root, and is reported asMEMBER_REMOVED. Resolving it needs the roots' member
names, which is a table, i.e. a guess — so the case stays conservative and
documented on the rule. - A degradation warning does not yet say which of the two artifacts produced it
(#64); a multi-release jar
tripsMULTI_RELEASE_VARIANTfor the side that carries it. VARARGS_CHANGEDis only reachable from hand-modified bytecode:javacgives
f(String)andf(String...)different descriptors, so that change is
reported asPARAMETER_TYPE_CHANGED. Pinned by unit tests and listed as
unreachable-from-javacin the coverage test, so the gap is deliberate rather
than invisible.- Unrelated to
diff, and pre-existing in every release so far: a Clikt-level
usage error — a missing argument, an unknown command, an unknown flag — prints
the right message but exits 0
(#65). Service-level exits
are correct. - The
coremutation gate cannot currently run in CI
(#66): the PIT pre-scan
needs the fixture corpus, which three modules do not hand to the minion JVM, so
PIT aborts before mutating anything on a clean checkout. Pre-existing since at
least v1.0.0.
Testing
206 tests and 22 goldens for this feature alone: 73 per-rule examples and 12
property-based families in core (self-identity, total order,...
v1.2.1
jdx v1.2.1 — Windows correctness + full CI on all three OSes
Patch release: no new commands, no output-shape changes on Linux. Windows
users get working --jars globs, Kotlin KDoc from CRLF checkouts, and JDK
sources; everyone gets the CI suite proving it — full check (tests + lint
- coverage gates) green on Linux, macOS and Windows on every push.
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.2.1-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.2.1-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
What's changed since v1.2.0
- JDK sources resolve (
body/source/docoverjava.*): JDK 9+
src.zipnests files under the module directory
(java.base/java/util/ArrayList.java), which the source mapping missed —
every JDK query errored instead of serving sources on JDKs that ship it. - Windows
--jarsglobs work:*?[{are illegal path characters on
Windows, so expansion threwInvalidPathExceptioninstead of matching;
glob roots now split with string math, mismatches exit 5, hits sort
deterministically on every filesystem. - Kotlin KDoc from CRLF checkouts: the PSI doc association misses every
KDoc under CRLF text while bodies still slice — CRLF sources (every
Windowstext=autocheckout) silently lost all Kotlin KDoc. Sources now
read LF-pinned, so parse offsets and served text agree on all platforms. - Windows output spellings: source-scan mention paths join with
/
(one spelling for agents), directory listings sort by name string
(Windows orders case-insensitively), fault JSON names JSON-escaped paths. jdx doctorKotlin row probes the resolved cache root, honouring
JDX_CACHE_DIRinstead of the ambient home.- Full CI/CD suite (#55–#61): Linux/macOS/Windows
checklegs with
JUnit + coverage uploads, coverage gates ridingcheck, nightly
soak/bench and weekly mutation tiers, workflow hardening (concurrency,
minimal permissions, timeouts) and a release check-gate. CI mode (-Pci)
keeps slow-runner timing report-only instead of red.
Full Changelog: v1.2.0...v1.2.1
v1.2.0
jdx v1.2.0 — full multiplatform support
Every OS gets a native bundle in this release: jdx-<version>-<os>.tar.gz and
jdx-<version>-<os>.zip (<os> = linux, macos, windows), each with a
.sha256 checksum and a jdx.jsa AppCDS archive trained on that OS
(the archive only engages when your JDK build matches the CI JDK —
otherwise it logs [cds] noise and runs normally; see #54). The bare
jdx-<version>.tar.gz/.zip aliases are the Linux assets, kept so the installer
and jdx upgrade default URLs keep working.
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashWindows users who prefer an Explorer-first bundle: download
jdx-1.2.0-windows.zip from the assets below and run
sh install-release.sh --tarball jdx-1.2.0-windows.zip.
Release installs self-update with jdx upgrade.
Releases are unsigned: expect a first-run warning —
Unblock-Fileon
Windows (PowerShell),xattr -d com.apple.quarantine <install-dir>/jdxon
macOS.
What's changed since v1.1.0
- Windows launcher (
jdx.bat+jdx.ps1): JDK discovery via%JAVA_HOME%,
where java.exe, registry and%ProgramFiles%probes; same flags and exit-6
contract as the POSIX launcher (#46). - Per-OS directories: cache/config/daemon-socket/install roots follow the
platform table (Linux XDG, macOS~/Library, WindowsLOCALAPPDATA/APPDATA);
old dot-dir locations migrate automatically; a missingXDG_RUNTIME_DIRfalls
back instead of exiting 3 (#44, #45). - Daemon starts on Windows/macOS with portable detach,
sun_pathlength
guard, start mutual exclusion, and pid-validated stop (#49, #50). - POSIX hardening for macOS: no GNU-only flags in the launcher or
installers; JDK via/usr/libexec/java_home(#47). - Tool + filesystem correctness on Windows:
.exeprobes, drive/UNC-safe
globs, case-insensitive.jar,USERPROFILEfallback (#51). - Per-OS release matrix with zip bundles, per-leg checksums, and per-OS
jdx.jsa(#48). - README rework (hook/demo/install/quickstart, command catalogue moved to
docs/COMMANDS.md) plus reproducible benchmark proof.
Full Changelog: v1.1.0...v1.2.0
v1.1.0
What's new: jdx upgrade self-update
Release installs can now self-update from GitHub Releases — no reinstall script
needed:
jdx upgrade --check # report only, never touches the install
jdx upgrade # update to the latest release
jdx upgrade --version v1.1.0 --json # pinned version, machine-readableThe updater resolves the target tag, verifies the SHA-256 checksum, and swaps
the release tree with a backup-restore guard. Source builds (no .jdx-release
marker) are refused with exit 3 — they were never release installs.
One-time note for v1.0.0 installs
v1.0.0 predates the .jdx-release marker, so jdx upgrade on a v1.0.0 install
reports "not a release install". Reinstall once with the script below; from
v1.1.0 on, self-update works:
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bashFull Changelog: v1.0.0...v1.1.0
v1.0.0
jdx v1.0.0 — initial release
jdx is an IDE for AI agents, as a command line tool. It answers the questions
agents working on JVM codebases ask dozens of times per task — what members a
class has, what a method does, who calls it, what implements an interface —
directly from .jar, -sources.jar, class/source directories, Maven
coordinates, and the JDK itself. Every command returns the smallest correct
answer with text + --json output and stable exit codes.
Read: declaration, members, structure
jdx show,members,outline,signature,doc,body,source- Structure from bytecode (ASM, never class-loaded), flesh from sources;
no sources jar → Vineflower decompile (labelled⚠ reconstructed) →
javapfallback. Kotlin signatures from@Metadata; Kotlin bodies/KDoc via
thejdx kotlin installsidecar.
Search and browse
jdx search(incl. camel-hump + fuzzy),resolve,ls,tree
Graph: usages, hierarchy, calls, samples
jdx usages(call/read/write/ref/new/throw/annotation),hierarchy,
implementors,callers,calls,samples— live over bytecode roots,
plus--srctextual scan of project source dirs
Environment, maintenance, serving
jdx version,doctor,ws(named workspaces + Gradle/Maven
auto-discovery),cache,kotlin install,help --agent,benchjdx daemon(warm JVM),mcp(20 typed tools),serve(HTTP/JSON),batch
(NDJSON) — all byte-identical to--json
Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash
jdx members java.util.HashMap --limit 5Requires JDK 21+. Verify with jdx version / jdx doctor.
Full Changelog: https://github.com/MohammadMD1383/jdx/commits/v1.0.0