Skip to content

Latest commit

 

History

History
100 lines (69 loc) · 8.57 KB

File metadata and controls

100 lines (69 loc) · 8.57 KB

Distribution & CLI

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.

1. Context

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."

2. The circuit CLI — lifecycle-only

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.

3. Repo-independence — one jar, one home

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 resourcecircuit up extracts the pinned docker-compose.yml from the jar; it does not read a repo file.
  • Dumps come from the Releasecircuit 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.

4. Deployment topology — Neo4j-in-Docker + CLI-as-local-jar

Per ADR-0043:

  • circuit upNeo4j-only compose. The shipped docker-compose.yml becomes effectively docker-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-app web 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 ghost SPRING_PROFILES_ACTIVE=reproducible profile (no application-reproducible.yml exists → it boots base application.yml and misses tools.disabled=cypher, re-exposing cypherADR-0040) and a stale SPRING_SHELL_INTERACTIVE_ENABLED. Dropping it removes the only path that could leak cypher.
  • The plugin only wires Claude Code → the installed CLI: plugin.json + .mcp.json (commandcircuit serve on PATH) + bundled skills (the usage playbook) + a thin bin/circuit launcher. 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).

5. Neo4j provisioning

  • 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 via NEO4J_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 by up/restore) fetches the pinned jars once into ~/.circuit/plugins (skip-if-present); the procedures are whitelisted manually (dbms.security.procedures.unrestricted=gds.*,apoc.*) since NEO4J_PLUGINS no longer does it. Pinned versions live in circuit-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 exceeds db.memory.transaction.total.max at a 2g max-heap; chunkedRun exists 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 up needs networkNeo4jPlugins.ensure fetches the jars once; every subsequent up/restore is 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.

6. Distribution mechanics

  • install.sh + GitHub Release — fat jar + corpus dumps as Release assets; the script puts circuit on PATH under ~/.circuit/. The repo stays cloneable for build-from-source but is not the install path.
  • Self-hosted plugin marketplace — a marketplace.json repo Claude Code fetches; the user never clones the plugin. Plus a committed bare .mcp.json + a documented claude mcp add one-liner (and a Codex config.toml snippet) as the no-plugin path.
  • Dockerfile is retained only for build-from-source, not the deployment path.

No-plugin wiring (the exact artifacts)

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 serve

Codex — add to ~/.codex/config.toml:

[mcp_servers.circuit]
command = "circuit"
args = ["serve"]

7. circuit doctor

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).