Status: Adopted (Sprint 6 Open, 2026-06-15)
Created: 2026-06-15
Scope: How CIRCUIT is delivered and operated as an out-of-box, self-hosted tool — the circuit picocli CLI (lifecycle surface), repo-independence (~/.circuit/), the Neo4j-only deployment topology, Neo4j provisioning, and the install / plugin / Release distribution. Binding decisions are in ADR-0043; this doc is the worked-out shape.
Vault relationship:
- Extends — vault's coarse layer is silent on packaging / distribution / plugin / CLI-lifecycle shape (repo-implementation territory); the picocli + MCP direction was set by ADR-0014.
- Process honesty: Sprint 6 Open drove from the repo Sprint-6 scope, not a fresh vault survey (the user's call). To re-validate at the user's vault review.
Sprint 6 ships CIRCUIT as a real developer tool: a stranger downloads it (no repo clone), follows the README, runs once, installs it as a Claude Code plugin, and can try it on their own repo. The golden path:
curl…|sh (circuit on PATH) → circuit up → (circuit restore <dump> | circuit ingest <repo>) → /plugin install → ask
Irreducible prereqs (circuit doctor checks, README states): Docker + Java 25 — not eliminable for a JVM + graph-DB tool. Everything below serves "install = download, run anywhere, no clone."
picocli (ADR-0014). The CLI is the control face; the five query tools stay MCP-only (locate / traverse / inspect / status / find_precedent) — Claude Code surfaces every tool call (name + args + result) inline, so a CLI mirror of the tools (or an interactive REPL) earns no demo value worth its cost, and the agent reasoning over the graph is the truer product story. So the CLI commands are:
| Command | Job |
|---|---|
circuit up / down |
Neo4j-only compose wrapper + bolt health poll |
circuit serve |
the MCP stdio server (wraps the existing serve JavaExec: --profile serve,embedding, web=none, stdout = JSON-RPC) |
circuit ingest <repoURL> / --scip <file> |
clone + scip-java + live fetch + embed (see scip-generation-driver.md); --scip is the BYO contract |
circuit restore <dump> |
fetch + load a Release dump; print its circuitCommitSha / schema version; warn on jar-vs-dump mismatch |
circuit doctor |
preflight Docker / Java 25 / Neo4j / scip-java (§7) |
circuit version |
report version; upgrade = re-run install.sh; uninstall = circuit down + rm ~/.circuit/ + drop the volume (documented) |
Each query-side command boots nothing new; lifecycle commands wrap docker + the existing JavaExec runners.
The CLI must run with no cloned repo present — this is what makes install = download:
- One home,
~/.circuit/— all state (cache, logs, restored dumps) under it. - Compose is an embedded jar resource —
circuit upextracts the pinneddocker-compose.ymlfrom the jar; it does not read a repo file. - Dumps come from the Release —
circuit restore <name>fetches the asset; big dumps stay out of git. - No reliance on a clone anywhere in the lifecycle path.
The plugin points at this one jar; there is no second jar home (no ${CLAUDE_PLUGIN_DATA} bootstrap) — one-home is the single source of truth.
Per ADR-0043:
circuit up→ Neo4j-only compose. The shippeddocker-compose.ymlbecomes effectivelydocker-compose.dev.yml+ ship heap + pinned versions.circuit serve→ the fat jar locally over stdio (the MCP server). Not a container, not a daemon.- The
circuit-appweb container is retired — the MCP server is a stdio per-session subprocess (Claude Code launches it), not an HTTP:8080 daemon; the container also carried a ghostSPRING_PROFILES_ACTIVE=reproducibleprofile (noapplication-reproducible.ymlexists → it boots baseapplication.ymland missestools.disabled=cypher, re-exposingcypher— ADR-0040) and a staleSPRING_SHELL_INTERACTIVE_ENABLED. Dropping it removes the only path that could leakcypher. - The plugin only wires Claude Code → the installed CLI:
plugin.json+.mcp.json(command→circuit serveonPATH) + bundled skills (the usage playbook) + a thinbin/circuitlauncher. The plugin's job is one-command wiring + cross-repo use, not one-command full install (the backend is irreducibly multi-step for a JVM + DB tool).
- GDS + APOC are both required, not optional. GDS powers centrality at ingest (PageRank / betweenness; absent → silently degraded graph, ADR-0036 / ADR-0038 context); APOC powers
traverse(apoc.path.spanningTree) +status(apoc.meta.data) at query, hard-breaking without it. They are pre-seeded into a bind-mounted/plugins, not installed viaNEO4J_PLUGINS(ADR-0045): the env mechanism re-downloads ~67 MB on every container start (never consults the volume, blocks bolt startup, multi-minute stall under CDN throttling). The CLI (Neo4jPlugins.ensure, called byup/restore) fetches the pinned jars once into~/.circuit/plugins(skip-if-present); the procedures are whitelisted manually (dbms.security.procedures.unrestricted=gds.*,apoc.*) sinceNEO4J_PLUGINSno longer does it. Pinned versions live incircuit-plugins.properties, the single source shared with the Testcontainers cache, so build + runtime plugin versions can't drift. Pin the Neo4j version so the plugin versions stay compatible. - Heap: max 4g (initial 2g, pagecache 1g) — mirrors
docker-compose.dev.yml(Fineract full-source ingest exceedsdb.memory.transaction.total.maxat a 2g max-heap;chunkedRunexists but no recorded run proves the default batch clears Fineract at 2g). Ship the headline corpus's proven heap; the e2e smoke is the gate. - Overridable — ports and
SPRING_NEO4J_URI(BYO-Neo4j escape hatch). - Only the first
upneeds network —Neo4jPlugins.ensurefetches the jars once; every subsequentup/restoreis offline + immediate. The earlier "bake into a custom image for offline-reproducible = future" is resolved by this pre-seed (ADR-0045); Phase 5 may additionally bundle the jars as Release assets for a fully-offline first install.
install.sh+ GitHub Release — fat jar + corpus dumps as Release assets; the script putscircuitonPATHunder~/.circuit/. The repo stays cloneable for build-from-source but is not the install path.- Self-hosted plugin marketplace — a
marketplace.jsonrepo Claude Code fetches; the user never clones the plugin. Plus a committed bare.mcp.json+ a documentedclaude mcp addone-liner (and a Codexconfig.tomlsnippet) as the no-plugin path. Dockerfileis retained only for build-from-source, not the deployment path.
Three equivalent ways to reach circuit serve without the plugin — each just spawns the on-PATH circuit:
A committed .mcp.json (repo root) — copy into any repo (CIRCUIT's own repo ships one), and Claude Code offers to start the server for that project:
{
"mcpServers": {
"circuit": { "type": "stdio", "command": "circuit", "args": ["serve"] }
}
}The claude mcp add one-liner — --scope user makes it available in every repo (the "try it on your own repo" path); --scope project instead writes a shareable ./.mcp.json:
claude mcp add --scope user circuit -- circuit serveCodex — add to ~/.codex/config.toml:
[mcp_servers.circuit]
command = "circuit"
args = ["serve"]Preflight: Docker running, Java 25 present, Neo4j reachable (bolt), scip-java available. It is also the home for the plugin bin/ launcher's JAVA_HOME / fat-jar discovery and a clear "Java 25 not found" message. doctor runs before the expensive build so cheap failures surface early (feeds the scip-generation-driver.md ladder).