AGENTS.md — jdx
Entry point. This file is the rules. Then read only what your change needs: the module
AGENTS.md(core/,index/, …), GitHub Issues (roadmap),docs/TESTING.md(before writing tests),CONTRIBUTING.md(before writing code). The full design lives indocs/PROPOSAL.md— read it lazily. (The v1 task board, session logs, and decision/lesson shards were removed post-v1; their durable content is distilled here and in the module notes, full history in git.)
1. What this project is#
jdx is an IDE for AI agents, as a command line tool.
An agent working on a JVM codebase constantly needs to know things that today cost it many
shell round-trips: what methods a class has, what a method actually does, who calls it, what
implements an interface. Today it does this with unzip, javap, grep, and by reading
1000-line source files into its context window. That is slow, noisy, and burns context.
jdx answers those questions directly, from .jar, -sources.jar, class directories,
source directories, Maven coordinates, and the JDK itself:
jdx members com.google.gson.Gson --inherited # what can I call on this? (IDE ".")
jdx body 'com.google.gson.Gson#toJson(Object)' # show me ONLY that method body
jdx usages com.google.gson.TypeAdapter # find usages
jdx hierarchy java.util.Map # who implements this?
jdx search '*Http*Client' --kind class # symbol searchDesign north star: every command answers one precise question with the smallest correct
output, and tells the agent exactly what to type next. Never dump a whole file when a
member was asked for. v1 is complete (all commands in jdx help --agent ship with
text + --json, exit codes, goldens).
2. Non-negotiable principles#
- Minimal correct output. Consumed by a model with a finite context window. Truncate intelligently and say how to get more — never silently, never by dumping more.
- Never guess a symbol. Ambiguous input exits
2with every candidate as a copy-pasteable canonical ref. Guessing wrong is worse than asking. - Every output line names its symbol canonically, so the next command is a mechanical copy-paste, not a reconstruction.
- Provenance always available. Which jar, which sources jar, bytecode vs source vs decompiled, which decompiler. An agent must be able to judge confidence.
- Deterministic. Same inputs → byte-identical output. Sorted. No ANSI unless TTY. No timestamps, no absolute paths, no hash values in default output.
- Meaningful exit codes (see §6) so agents can branch without parsing prose.
- Fast. Cold CLI under ~250 ms; warm daemon under ~20 ms. Called dozens of times per task.
- Degrade, don't fail. No sources jar → decompile. Decompiler fails →
javap. Kotlin metadata corrupt → JVM view. Always emit something, and say what it is. - Heavily tested, by construction.
coretest-first; everything else backed by generative families — differential againstjavap, property-based, metamorphic, fault-injection, corpus soak. Hand-written examples alone are not sufficient.
3. Locked rules (owner-settled — do not re-litigate; to change one, open an issue,#
get an explicit reversal, record it here and in the commit message)
- Truth model: structure from bytecode, flesh (bodies, param names, docs) from
sources; disagreement warns
SOURCES_VERSION_MISMATCH, never silently wins. - Read-only: parse with ASM, never load inspected classes (no static init ever
runs). Write only inside the platform dirs (
docs/PROPOSAL.md§17.1 table). - Per-OS dirs (D-044, issue #44): cache/config/socket/install defaults per OS —
Linux
~/.cache|~/.config|$XDG_RUNTIME_DIR(honouringXDG_*); macOS~/Library/Caches|~/Library/Application Support|$TMPDIR/jdx-$UID; Windows%LOCALAPPDATA%/jdx/cache|%APPDATA%/jdx|%LOCALAPPDATA%/jdx/run(AF_UNIX, Win10 17063+ floor); install~/.local/share/jdx+~/.local/bin(same on macOS),%LOCALAPPDATA%/Programs/jdx+ PATH shim on Windows. Supersedes D-013/D-027; a missingXDG_RUNTIME_DIRfalls back per table, neverexit 3. Precedence: CLI flagJDX_*>XDG_*> OS default. Migration: move (never merge) the old location on first run, warn if both exist. Full table indocs/PROPOSAL.md§17.1. - Output: text default,
--jsonenvelope complete (no text-only information). - Exit codes are a public contract — changing them breaks agents.
- Graph queries are live-roots-first:
usages/hierarchy/callers/calls/samplesre-scan bytecode roots per query (no persistent-index acceleration yet). - Network is opt-in per invocation (
--fetch); coordinates resolve from~/.gradle/caches+~/.m2first. - Push rule: no
git push/gh repo createwithout explicit owner go-ahead in the current conversation.
| Area | Rule |
|---|---|
| Language / build | Kotlin + Gradle (JDK 21 toolchain target) |
| Bytecode reading | ASM |
| Decompiler | Vineflower bundled (default); --engine javap for raw opcodes |
| Java sources | JavaParser |
| Kotlin sources | kotlin-compiler-embeddable PSI — side-loaded into an isolated classloader, never a compile dep, never in the fat jar |
| Kotlin binaries | @Metadata via kotlin-metadata-jvm → true Kotlin signatures |
| Index | Persistent on-disk index (SQLite), content-hash keyed, behind an IndexStore interface |
| Usages scope | All workspace jars + the project's own source dirs |
| Classpath input | explicit paths/globs/dirs + named workspaces + project auto-discovery (never run Gradle/Maven) + Maven coords (auto-download opt-in) + JDK stdlib via jrt-fs |
| Interfaces | one-shot CLI, MCP stdio server, daemon (5-min idle auto-shutdown), HTTP/JSON — all thin adapters over one JdxService, no logic in adapters |
| Testing | TDD mandatory in core + generative families; Pitest mutation gate ≥ 80 % on core |
| Deferred | mappings/remap, resources & services inspection, --since/API-level reporting |
| Repo | local git; public GitHub repo (github.com/MohammadMD1383/jdx), Apache-2.0 |
4. Repository layout#
jdx/
├── AGENTS.md ← you are here
├── README.md user-facing intro (hook, demo, install, quickstart, links)
├── docs/COMMANDS.md user-facing command catalogue (moved out of README)
├── CONTRIBUTING.md conventions, code style, definition of done
├── <module>/AGENTS.md per-module notes — read only the one(s) you touch
├── docs/
│ ├── PROPOSAL.md full design document (the spec)
│ ├── TESTING.md testing strategy — read before writing tests
│ ├── guide/ user guides (website "Getting started" + "Guides" sections)
│ └── FAQ.md user FAQ (website /faq/, FAQPage structured data)
├── gradle/libs.versions.toml version catalog (single source of dependency versions)
├── core/ model, symbol refs, resolution, rendering. Pure Kotlin, no IO.
├── index/ ASM readers, Kotlin metadata, SQLite store, indexer, JdxService
├── sources/ sources-jar handling, JavaParser, Kotlin PSI (isolated CL)
├── decompile/ Vineflower + javap engines
├── cli/ Clikt commands, text/JSON renderers
├── mcp/ MCP stdio server
├── server/ HTTP/JSON API + daemon
├── app/ fat-jar assembly + `jdx` launcher script
├── testfixtures/ nasty Java/Kotlin classes → binary jar + -sources.jar
└── site/ GitHub Pages website generator (build-time only; site/AGENTS.md)Dependency rule: core depends on nothing project-local. Everything depends on core.
cli/mcp/server are ADAPTERS and must contain no behaviour; logic tempted into a
Clikt command belongs in core or a service class so all four interfaces share it.
5. Symbol reference syntax (canonical)#
Javadoc style. Accept generously, print canonically.
com.google.gson.Gson type
com.google.gson.Gson$TypeAdapterRuntime nested (also accepts .Inner)
com.google.gson.Gson#toJson all overloads
com.google.gson.Gson#toJson(Object) simple param names OK
com.google.gson.Gson#toJson(java.lang.Object) fully-qualified params
com.google.gson.Gson#toJson(Ljava/lang/Object;)Ljava/lang/String; exact JVM descriptor
com.google.gson.Gson#<init>(...) constructor
Gson#toJson short form → resolved, or exit 2 w/ candidates:: and . are accepted in place of #. Always quote the ref in shell examples —
# and ( are shell-hostile. $ always separates nesting; the final dot-segment is
always the class. parse(print(ref)) == ref is a pinned property — don't break
round-tripping.
6. Exit codes (contract — do not change casually)#
| Code | Meaning |
|---|---|
0 |
Success, results found |
1 |
Valid query, no results (class/member not found) |
2 |
Ambiguous reference — candidates printed |
3 |
Usage / argument error |
4 |
No index or workspace resolved for this query |
5 |
Artifact read error (corrupt jar, unreadable file) |
6 |
Internal error |
The one documented exception (owner-approved, issue #23): jdx diff exits 0
whenever the comparison ran, whatever it found, and --fail-on breaking|any exits 1
when the gate trips. A diff has no "not found" outcome, and the verdict is also in the
envelope as result.gate, so a consumer that cannot read exit codes still gets the
answer. Nothing else may bend this table.
7. Build, test, lint#
- Build only via the Gradle wrapper (
./gradlew); JDK 21+ required. Categories:test(fast unit loop, per-test budget <= 2.5 s) ·integrationTest(integration suites, per-test budget <= 120 s) ·check(pre-commit: unit + integration + lint + coverage) ·soak/bench/mutationTeston demand (benchandmutationTestrequire explicit confirmation via-PconfirmTier4=trueor-PallowHeavy=true). coreTDD-mandatory, mutation gate ≥ 80%;index/sources/decompile≥ 85% line; adapters ungated (parity + goldens instead — gating thin code incentivises padding).- A gate that cannot run is not a gate.
checkrunsverifyPitestWiring(alongsidelint) because thecoremutation gate aborts in PIT's pre-scan — silently, with no score produced at all — when the fixture corpus is missing, and that only shows up on a clean checkout. Any precondition a gate needs gets its own cheap assertion incheck; don't let a red nightly be the thing that discovers it.docs/TESTING.md§10.1. - Every behaviour needs a generative family (property /
javap-differential / metamorphic / fault-injection / corpus), not just examples. - Root
lint(runs in everycheck): no trailing whitespace, no tabs, no bareTODO/FIXMEwithout a reference, noprintln/System.exit/printStackTracein library mains,allWarningsAsErrorson main + test compiles. -Pgolden.update=trueis the most dangerous command here: read every golden diff before committing it. An unread golden update is a deleted test.- CI mode
-Pci(also auto-detected viaCI/GITHUB_ACTIONS): wall-clock test gates relax on slow runners — per-test budget report-only, PIT timeouts 10 s → 60 s. Local loop unchanged. Product timeouts untouched. Seedocs/TESTING.md§2. - Line counting is POSIX: one trailing
\nis a terminator, not a line.
8. Working agreements#
- Keep GitHub Issues current — they are the roadmap. Move finished items out, record newly found ones (limitations, follow-ups, caveats). A change that alters behaviour or finds a caveat and leaves them stale is incomplete.
- Keep the
AGENTS.mdnotes true. If a change invalidates a module note (new invariant, new gotcha, new key file), update that note in the same change. A contributor who trusts a stale note pays twice. - Ask the owner about genuine ambiguity rather than picking. A silent wrong guess propagates through a codebase nobody fully reads.
- Work in the cadence: one small change → commit → push → next. Conventional Commits
with a module scope:
feat(index): parallel artifact indexer. If a change can't be finished and committed in one sitting, it is too big — split it first. An unpushed working tree is invisible to every other contributor. - Definition of done for a command: behaviour in
core/index/sources/decompile, thin adapter incli,--helptext, text renderer, JSON renderer, exit codes, truncation, golden tests for both renderers, at least one generative test family, docs/COMMANDS.md table row, Appendix B flag entry, GitHub Issue filed/updated if behaviour or limitations changed, and./gradlew :site:buildSitegreen (the website executes doc examples and checks every documented flag against--help— seesite/AGENTS.md). - Report honestly. Failing tests get pasted, not summarised away. Documented half-finished work is useful; half-finished work reported as done is a trap.
- Write for a stranger. Explicit over clever, named over inlined, invariants stated in comments where types don't enforce them.
- Batch independent tool calls in a single turn. Sequence calls only when one genuinely depends on another's output.
9. Specs and roadmap#
docs/PROPOSAL.md— design spec (§6 ref grammar, §8 output, §14 serving, Appendix B flag reference, §19 licenses). Code citesPROPOSAL.md §X; keep true.docs/TESTING.md— testing strategy; read before writing tests.- GitHub Issues — known limitations + phase-2 backlog (the roadmap).
- docs/COMMANDS.md is the user-facing catalogue; every shipped flag must appear in
jdx <cmd> --help, the docs/COMMANDS.md table, and Appendix B. - Website (mohammadmd1383.github.io/jdx) is
generated from README.md, docs/ and the built binary by
site/and deployed by.github/workflows/pages.ymlon every push tomainand every release. Never edit it by hand; change the source. Hierarchy and URLs live insite/.../SiteManifest.kt.
10. Dogfood rule — built by agents, for agents#
jdx is its own proof of work: when working on this repo, use jdx on this
repo first (own source dirs / built jars, third-party jars, the JDK) instead of
javap / grep / unzip / full-file reads for structural questions.
- A wrong answer, crash, or missing capability found while dogfooding is a bug
or feature gap in
jdx— file it as a GitHub Issue in the same session (gh issue create), with: the exactjdxcommand + inputs, when/where/why it happened, expected vs actual, the fallback used, and the proposed fix. - Never block on the fix: fall back to whatever works (
javap,grep, direct reads) and finish the task. Never work around silently without filing — an unfiled dogfood bug is an incomplete task.
11. Environment notes (this machine)#
- Arch Linux. JDK at
/usr/lib/jvm/default(java,javac,javap,jar,jdeps).JAVA_HOMEis unset — the launcher must resolve the JDK itself and not assume it. Other OSes (CI-verified viawindows.yml/macos.yml, issue #52): macOS JDK via/usr/libexec/java_home -v 21(override with$JDX_JAVA_HOME_HELPERin tests); Windows JDK via%JAVA_HOME%\bin\java.exe→where java.exe→ registry (JavaSoft/Adoptium) →%ProgramFiles%\Java(seeapp/AGENTS.md). Windows symlinks need Developer Mode, otherwise launcher/installer symlink suites skip with a reason; goldens stay LF (*.sh eol=lf,*.bat eol=crlf) with CRLF-tolerant compare (GoldenFiles.normaliseLineEndings,UnifiedDiff.splitLines). - No
mvn/gradleon PATH. Use the Gradle wrapper. - Maven Central and GitHub are reachable.
gitandghare installed. - In the
/opencodeGitHub Actions job,GH_TOKENis the job's ownGITHUB_TOKENscoped toactions: readon this repo — sogh run list,gh run view <id> --log,gh run watchwork. Read the logs instead of guessing at a CI failure, and never ask for a PAT to do it. - Rich local test corpus: ~2183 jars in
~/.gradle/caches, including-sources.jarartifacts (gson, msal4j, lwjgl, mojang-logging) and a large obfuscated/remappedminecraft-client.jarunder~/.gradle/caches/fabric-loom/— the scale/perf benchmark; gson is the everyday correctness fixture. - ASM and Vineflower already exist in the local Gradle cache.