Document version: Build 165 · v0.3.8 · 2026-08-06
This document describes the architecture as of the build above. If the build counter in
mix.exsis higher, sections may be out of date — re-audit against the codebase.
Giulia is a persistent, local-first code intelligence daemon built in Elixir/OTP. It runs as a persistent background daemon inside Docker, exposing an HTTP REST API on port 4000. Any client -- Claude Code, a CLI escript, an editor plugin -- talks to the daemon over plain HTTP/JSON. The daemon never restarts between terminal sessions; it keeps AST caches, property graphs, and embedding vectors warm in memory across invocations.
+-----------+ +-----------+ +----------------+
| Claude | | CLI | | Editor Plugin |
| Code | | (escript) | | (future) |
+-----+-----+ +-----+-----+ +-------+--------+
| | |
+-------+-------+-----------------+
|
HTTP / JSON or MCP
|
v
+-----------------------------+
| giulia-worker :4000 |
| (Bandit + Plug.Router) |
| 88 API endpoints |
| MCP server (/mcp) |
+-----------------------------+
The daemon holds per-project state in ETS tables, a libgraph-based property graph, CubDB persistence for warm restarts, and an optional ArcadeDB connection for cross-build historical analysis.
Giulia ships as a single Docker image (giulia/core:latest). Two containers are
started from that image, differentiated by the GIULIA_ROLE environment variable:
+-------------------------------------------------------------------+
| Docker Network |
| |
| +-----------------------------+ +---------------------------+ |
| | giulia-worker | | giulia-monitor | |
| | GIULIA_ROLE=worker | | GIULIA_ROLE=monitor | |
| | Port 4000 (HTTP API) | | Port 4001 (HTTP API) | |
| | Port 4369 (EPMD) | | Port 4369 (EPMD) | |
| | Ports 19100-19105 (dist) | | Ports 19110-19115 (dist) | |
| | | | | |
| | - AST indexing | | - Distributed Erlang | |
| | - Property Graph | | connection to worker | |
| | - Semantic search | | - Burst detection | |
| | - EmbeddingServing | | - High-frequency runtime | |
| | - Inference engine | | snapshots | |
| | - 88 API endpoints | | - Performance profiling | |
| | - MCP server (75 tools) | | | |
| | - CubDB persistence | | Skips: | |
| | - ArcadeDB L3 snapshots | | EmbeddingServing (~90MB) | |
| | | | Inference pools | |
| | | | SemanticIndex | |
| +-------------+---------------+ +-------------+-------------+ |
| | | |
| +---- Erlang Distribution ----------+ |
| (cookie-authenticated) |
+-------------------------------------------------------------------+
|
| HTTP (host.docker.internal:2480)
v
+-------------------+
| ArcadeDB |
| (standalone) |
| Port 2480 |
+-------------------+
Worker (giulia-worker): The primary daemon. Runs all static analysis (AST
scanning, property graph construction, semantic embeddings) and serves all 88
API endpoints. Memory limit: 4GB. (A legacy local-chat inference subsystem also
loads under TIER 3 — deprecated as of v0.3.8, see Section 18 / Document History.)
Monitor (giulia-monitor): A lightweight observer node. Connects to the worker
via distributed Erlang on startup (AutoConnect GenServer). Its job is runtime
introspection: periodic BEAM health snapshots, burst detection (spikes in
reductions/memory), and performance profiling triggered by bursts. It skips
EmbeddingServing, SemanticIndex, and all Inference children to save approximately
200MB of RAM. Memory limit: 2GB.
The monitor depends_on the worker being healthy (curl health check on :4000 with
30s interval). Both containers share the same Erlang cookie (GIULIA_COOKIE,
default giulia_dev) for authenticated distribution.
Giulia.Application.start/2 detects whether it is running in client mode (thin
HTTP client, empty supervision tree) or daemon mode. In daemon mode, it starts
children under a single :one_for_one supervisor (Giulia.Supervisor) in five
tiers:
Giulia.Supervisor (:one_for_one)
|
|-- TIER 1: Base (always started)
| |-- Registry (Elixir.Registry, :unique, name: Giulia.Registry)
| |-- Task.Supervisor (name: Giulia.TaskSupervisor)
| |-- Context.Store (ETS tables for AST data)
| |-- Persistence.Store (CubDB lifecycle, one instance per project)
| |-- Persistence.Writer (async write-behind, 100ms debounce)
| |-- Tools.Registry (auto-discovers tool modules on boot)
| |-- Context.Indexer (background AST scanner, Task.async_stream)
| |-- Knowledge.Store (libgraph in-memory directed graph)
| |-- Persistence.WarmRestore (boot-time L2→L1 restore, non-blocking)
| |-- Storage.Arcade.Indexer (L3 graph sync on {:graph_ready})
| +-- Storage.Arcade.Consolidator (periodic cross-build analysis)
|
|-- TIER 2: Heavy (skipped when GIULIA_ROLE=monitor)
| |-- Intelligence.EmbeddingServing (Bumblebee + all-MiniLM-L6-v2)
| +-- Intelligence.SemanticIndex (cosine similarity search)
|
|-- TIER 3: Inference (DEPRECATED as of v0.3.8; skipped when GIULIA_ROLE=monitor)
| |-- Provider.Supervisor (DynamicSupervisor for LLM connections)
| |-- Inference.Trace (debug storage for inference runs)
| |-- Inference.Events (SSE event broadcaster)
| |-- Inference.Approval (interactive consent gate)
| +-- Inference.Supervisor (pools with back-pressure)
|
|-- TIER 4: Tail (always started)
| |-- Monitor.Store (rolling event buffer + SSE pub/sub)
| |-- Core.ProjectSupervisor (DynamicSupervisor for per-project contexts)
| |-- Core.ContextManager (routes requests to correct ProjectContext)
| |-- Runtime.Collector (periodic BEAM health snapshots)
| |-- Runtime.IngestStore (Monitor->Worker snapshot pipeline)
| |-- Runtime.Observer (async observation controller)
| |-- Runtime.AutoConnect (returns :ignore if GIULIA_CONNECT_NODE unset)
| +-- Runtime.Monitor (returns :ignore unless GIULIA_ROLE=monitor)
|
|-- TIER 5: MCP (only started if GIULIA_MCP_KEY is set)
| +-- Giulia.MCP.Server (Anubis StreamableHTTP transport)
|
+-- Bandit (HTTP endpoint, skipped in MIX_ENV=test)
plug: Giulia.Daemon.Endpoint
port: GIULIA_PORT (default 4000)
After the supervisor starts successfully, Giulia.Monitor.Telemetry.attach/0 hooks
:telemetry handlers for the cognitive flight recording system (7 events across
the inference pipeline — deprecated subsystem, see Section 18).
Giulia.Supervisor uses :one_for_one: a crash in one child restarts only that
child, not its siblings. A failure in Intelligence.SemanticIndex does not take
down Knowledge.Store or the HTTP endpoint, so the daemon stays available
through partial failures.
The trade-off is that a restarted child loses its in-memory state, and downstream consumers may briefly hold stale references. Two mechanisms close that gap, both required by the GIULIA.md "Restart-time state recovery" invariant:
-
ETS survival via
EtsKeeper. ETS tables owned byContext.StoreandKnowledge.StoreregisterGiulia.EtsKeeperas their:heir. When an owner crashes, BEAM transfers the table to the keeper instead of deleting it; the restarted owner reclaims it viaEtsKeeper.claim/1. Cache data survives the common case — an owner crash — without a rebuild. -
Lost
{:graph_ready}messages.Knowledge.StorenotifiesStorage.Arcade.Indexerof completed builds with a baresend/2to awhereislookup, which drops silently if the Indexer is mid-restart.Arcade.Indexercompensates with a 5-minute reconcile pass that walks every active project and re-snapshots any whose current build is missing from ArcadeDB.
If EtsKeeper itself is restarted, its inherited tables are lost; that case is
covered by Persistence.WarmRestore, which rebuilds L1 from the L2 CubDB cache
on boot, verifying each entry against its SHA-256 content hash.
Giulia uses a three-tier storage model. Each tier serves a different latency and durability requirement.
The hot path. All API reads hit L1 first.
-
Context.Store: ETS table keyed by
{:ast, project_path, file_path}. Stores parsed AST data, module metadata, function signatures, specs, callbacks, and struct definitions. Rebuilt on every scan. -
Knowledge.Store: An in-memory
Graph(libgraph directed graph) holding module dependency relationships. Supports queries like dependents, dependencies, centrality, impact maps, and shortest path. Rebuilt after every scan from the AST data in Context.Store. -
Metric caches: Computed lazily and cached in Knowledge.Store's GenServer state. Five cached metrics:
heatmap,change_risk,god_modules,dead_code,coupling. Warmed eagerly after graph rebuild via a background Task. Sub-10ms on warm reads (was 570-1166ms before caching).
On-disk key-value store for surviving restarts without re-scanning.
-
Location:
{project}/.giulia/cache/cubdb/(one CubDB instance per project). In test mode (MIX_ENV=test), routed to/tmpto avoid corrupting the dev daemon's data. -
Contents: AST entries, serialized property graph, metric caches, embedding vectors (module + function).
-
Writer:
Persistence.Writerbatches writes with a 100ms debounce. Multiple writes within the window are coalesced into a single CubDB transaction. -
Loader:
Persistence.Loaderrestores L1 from L2 on startup. Detects stale files by comparing SHA-256 content hashes of source files against stored hashes. Stale entries trigger incremental re-scanning rather than a full rebuild. -
WarmRestore:
Persistence.WarmRestoreis the startup driver. On boot it walks/projects/*(andGIULIA_PROJECTS_PATHif set) for directories containing the role-specific.giulia/cache/cubdb[_<role>]/layout and callsLoader.restore_graph/1+restore_metrics/1for each. The work runs inhandle_info(:run, _)scheduled frominit/1viasend/2so supervisor start isn't blocked on I/O. This is what keeps/api/projectspopulated acrossdocker compose restartwithout forcing a scan. -
Merkle tree:
Persistence.Merklebuilds a SHA-256 Merkle tree over all cached entries. Used for integrity verification (POST /api/index/verify) and detecting corruption (if build version mismatches, the entire L2 cache is discarded and a cold start occurs). -
Code-digest envelope: graph and metrics are persisted wrapped in a
%{digest: <12-char hex>, payload: <data>}envelope. The digest is computed byKnowledge.CodeDigestfrom the BEAM md5 of eleven code-tier modules (the graph/metric tier —Builder,Metrics,Behaviours,DispatchPatterns,DeadCodeClassifier,TemplateReferences— plus the enrichment tier) and the content md5 of six config files (scoring.json,dispatch_patterns.json,scan_defaults.json,enrichment_sources.json,dispatch_invariants.json,otp_checks.json). On warm-restore, the loader compares the stored digest against the current digest:- Match → load cache as-is
- Mismatch → log
"Code digest changed (X -> Y) — invalidating … cache", drop the cache, force a rebuild on next scan
This automates the "I edited the builder/scoring.json, restart, expect fresh metrics" workflow without per-edit ceremony. The AST cache is intentionally NOT in the digest — re-extracting 580+ files takes seconds-to-tens-of-seconds, so AST invalidation stays under user control via
?force=trueon/api/index/scan. Only the cheap downstream path (graph rebuild + metric recompute from existing ASTs) is auto-invalidated.See
CONFIGURATION.mdfor the full invalidation contract and the operator workflow for tuning each config surface.
External multi-model graph database for cross-build analysis. Not on the hot path.
-
Deployment: Standalone container (
arcadedata/arcadedb:latest) on port 2480. Not managed by the Giulia docker-compose file. Worker reaches it viaARCADEDB_URL(default:http://host.docker.internal:2480). -
Query languages: Cypher, SQL, and sqlscript. The Req-based HTTP client (
Giulia.Storage.Arcade.Client) supports all three. -
Schema:
- Vertex types:
Module,Function,File,Insight - Edge types:
DEPENDS_ON,CALLS,DEFINED_IN - All records carry
project,build_id, andindexed_atfields - Composite unique indexes on
(project, name)per vertex type
- Vertex types:
-
Indexer:
Giulia.Storage.Arcade.Indexerhooks into the{:graph_ready}event and snapshots the entire L1 graph into ArcadeDB after every successful build. -
Consolidator:
Giulia.Storage.Arcade.Consolidatorruns on a 30-minute schedule (or on-demand viaPOST /api/index/compact?include=arcade). Until v0.3.7 this was a Build-137 skeleton — the timer fired but did nothing; v0.3.7 wired it to real pruning (prune_old_builds/2) and three cross-build analyses:complexity_drift,coupling_drift(fan-in/fan-out), andhotspotdetection. Retention window controlled byarcade_history_buildsinpriv/config/scan_defaults.json(default10, clamped ≥3 — drift / coupling / hotspot detectors require ≥3 builds of history). Analysis results stored asInsightvertices. -
Purpose: ETS + libgraph stays L1 for real-time queries. ArcadeDB is for history -- trend analysis, regression detection, cross-build comparisons. Typical warm query latency is ~100ms, acceptable for L2/L3 but not the hot path.
Tunable behaviour lives in JSON, not in module attributes. The contract:
- Edit + restart, no recompile. Daemon restart picks up changes in seconds; the BEAM and ETS state aren't rebuilt.
:persistent_termcache. Each loader reads its file once at boot and stores the parsed structure in:persistent_term. Reads are free (no copy, no message-passing).- CodeDigest envelope tracking. Config files are part of the digest that decides whether L2 caches survive a restart. Edit a config file, the next warm-start invalidates derived metrics so they're recomputed against the new values.
- Universal defaults. The shipped values must produce correct output on every codebase, no per-project opt-in. Tightening a default that's wrong somewhere is a release-gating bug, not a "user can override" out.
- Fail-loud on missing/malformed. Loaders raise at boot rather than silently degrade — a typo in JSON should crash the daemon at startup, not produce wrong-but-running analysis hours later.
This pattern is why v0.3.x extracted four module-attribute datasets to JSON across multiple releases (scoring constants in v0.3.0, dispatch patterns in v0.3.x, dispatch invariants and relevance buckets in v0.3.8). The architectural commitment is "configuration is data, not code."
Five JSON files in priv/config/ control behaviour that should be tunable
without recompilation:
| File | Loader | Controls |
|---|---|---|
scoring.json |
Knowledge.ScoringConfig |
Heatmap weights/normalization/zones, change_risk weights, god_modules weights, unprotected_hubs thresholds |
dispatch_patterns.json |
Knowledge.DispatchPatterns |
Runtime-dispatch patterns the AST walker can't see (Mix Release shell overlays, ExMachina factories, the Phoenix-style __using__/apply idiom) |
scan_defaults.json |
Context.ScanConfig |
Universal source-root list for Mix projects (lib, test/support, test/test_helper.exs); ArcadeDB history retention |
dispatch_invariants.json |
Config.DispatchInvariants |
Project-root markers, OTP/framework implicit functions, known external behaviour callback signatures, Phoenix HTTP verbs (v0.3.8+) |
relevance.json |
Config.Relevance |
Bucket boundaries for ?relevance=high|medium|all on dead_code / conventions / duplicates (v0.3.8+) |
All are loaded once at daemon startup and cached in :persistent_term.
All except relevance.json are tracked by the CodeDigest envelope (see
L2 section above), so edits propagate via daemon restart + automatic cache
invalidation on warm-restore. relevance.json is excluded deliberately:
its buckets filter a response at read time and are never baked into the
cached graph or metrics, so an edit takes effect on the next request with
no invalidation needed.
See CONFIGURATION.md for the per-variable
reference and operator workflow.
Giulia merges two views of the project that most analysis tools keep separate: a static call graph extracted from source via Sourceror, and a live BEAM-runtime feed (process counts, message-queue lengths, reduction deltas, memory) collected by the monitor node. The Knowledge layer reads both and answers questions like "this hub function has 47 callers AND its process accumulated 200k reductions in the last burst" in one query — the caller-count comes from the AST graph, the burst signal comes from the runtime collector, the join happens at the function-vertex level.
.ex source files
|
v
Sourceror.parse_string/1 (pure Elixir AST parser)
|
v
Giulia.AST.Extraction Macro.traverse/4 with enclosing-module
| stack — recognizes defmodule / defprotocol
| / defimpl; nested names qualified
| (Outer.Inner, three levels deep);
| function_info carries :module so sibling
| modules don't collapse on {name, arity}
v
Context.Store (ETS) {:ast, project_path, file_path}
| modules, functions, specs, structs
v
Knowledge.Store (libgraph) module dependency graph
| edges from alias/import/use analysis
v
Metric caches heatmap, complexity, coupling,
dead_code, god_modules, change_risk
Target BEAM node (self or remote)
|
v
Runtime.Inspector :erlang.memory/0, Process.info/2,
| :erlang.statistics/1, :erlang.trace/3
v
Runtime.Collector periodic snapshots (configurable interval)
|
v
Burst detection spike in reductions/memory triggers
| high-frequency capture mode
v
Performance profiling function-level trace during burst window
Two sources ship today: Credo (JSON, 5-entry severity map) and Dialyzer (text, 47-entry severity map covering dialyxir's full warning catalogue). Adding a new source = parser module + JSON entry; no core changes.
External tool (Credo, Dialyzer, ...)
|
| mix credo --format json > /tmp/credo.json
| ~/.mix/escripts/credo --format json --working-dir . > ... (escript form)
| mix dialyzer --format short > /tmp/dialyzer.out
v
POST /api/index/enrichment validates payload_path against allowlist
| (priv/config/scan_defaults.json
| :enrichment_payload_roots)
v
Giulia.Enrichment.Ingest dispatches to source module via Registry
|
v
Giulia.Enrichment.Sources.{Credo,Dialyzer,...}
| parse tool output into normalized
| finding/0 records. Three-path arity-
| resolution waterfall (same shape across
| sources):
| 1. line-range against function vertices
| (single match, multi-arity, ambiguous)
| 2. all-arities fallback for the named fn
| 3. module-only attach (with
| :resolution_ambiguous flag when
| multiple different-name candidates
| cover the line)
| severity_map per source lives in
| priv/config/enrichment_sources.json;
| parser asks Registry.severity_for/2.
v
Giulia.Enrichment.Writer replace-on-ingest inside CubDB.transaction:
| delete prior {:enrichment, tool, project, *}
| keys, write new ones, stamp per-finding
| provenance (tool_version, run_at,
| source_digest_at_run), write sentinel
v
CubDB {:enrichment, tool, project_path, target}
target = "Mod.fn/N" | "Mod" | :__ingested__
preserved across clear_project (source
rescans don't wipe enrichments)
Read side:
GET /api/intelligence/enrichments?mfa=...
| (uncapped drill-down for agents
| wanting full per-MFA findings)
v
Giulia.Enrichment.Reader fetch_for_mfa / fetch_for_module:
distinguishes %{} (never ingested for
project) from %{credo: []} (ingested,
no findings on this MFA) via sentinel
key probe — different signals, agent
reasons differently about each.
POST /api/knowledge/pre_impact_check
|
v
Knowledge.Insights.Impact attaches :enrichments per affected_caller
via Giulia.Enrichment.Consumer.attach/2
GET /api/knowledge/dead_code
|
v
Knowledge.Metrics.dead_code attaches :enrichments per dead entry via
the same Consumer.attach/2
(both call) Giulia.Enrichment.Consumer.apply_response_cap/1
shared cap logic from priv/config/scoring.json
(errors uncapped, top-3 warnings/entry,
drop info, per-response cap of 30
dedup'd by {check, severity}; capped
responses get a project-wide
:enrichments_summary on the first entry)
| Module | Responsibility |
|---|---|
Enrichment.Source |
Behaviour contract — tool_name/0, target_granularity/0, parse/2 |
Enrichment.Registry |
Loads priv/config/enrichment_sources.json, caches in :persistent_term. Exposes sources/0, fetch_source/1, config_for/1, severity_for/2 (translates tool-emitted category/check string into canonical severity using per-source severity_map) |
Enrichment.Sources.Credo |
JSON parser; 5-entry severity map; three-path arity resolution |
Enrichment.Sources.Dialyzer |
Text parser for --format short; 47-entry severity map covering dialyxir's full catalogue; same three-path resolution |
Enrichment.Writer |
Replace-on-ingest CubDB writes, provenance stamping, sentinel marker |
Enrichment.Reader |
Per-MFA / per-module lookups with never-ingested vs no-findings distinction |
Enrichment.Ingest |
Orchestrator; emits [:giulia, :enrichment, :ingest | :parse_error | :read] telemetry |
Enrichment.Consumer |
Shared helper used by pre_impact_check and dead_code to attach :enrichments per entry and apply per-entry / per-response caps |
Architectural commitment: tool ingest cadence is decoupled from source-extraction cadence. CI may push Credo findings on every PR; the daemon scans source on a different schedule. Findings persist independently — Persistence.Writer.clear_project/1 filters out enrichment keys precisely so re-extraction doesn't force re-ingestion.
The /api/runtime/hot_spots endpoint is the fusion point. It:
- Reads top processes from the target BEAM node (by reductions or memory)
- Resolves PIDs to module names via
Process.info(pid, :dictionary) - Looks up each module in the Property Graph for centrality, complexity, and zone
- Returns a merged view: runtime activity annotated with static analysis metadata
The Observer (running on the monitor node) pushes snapshots to the worker via HTTP. The worker finalizes each snapshot with static+runtime correlation.
The Knowledge layer is not a single module. Knowledge.Store is the GenServer
coordinator, but the actual logic is split across purpose-built modules:
| Module | Responsibility |
|---|---|
Knowledge.Builder |
Graph construction from AST data (11-pass pure functions — see Builder Passes below) |
Knowledge.Topology |
Pure graph traversal: stats, centrality, reachability, cycles, paths |
Knowledge.Metrics |
Quantitative metrics: heatmap, change_risk, god_modules, dead_code, coupling |
Knowledge.Behaviours |
Behaviour integrity checking (callback validation, macro-aware) |
Knowledge.Conventions |
Convention violation detection via AST (Tier 1 metadata + Tier 2 patterns) |
Knowledge.Insights |
High-level code insights: orphan specs, logic flow, style oracle |
Knowledge.Insights.Impact |
Pre-impact risk analysis for rename/remove/refactor operations |
Knowledge.Analyzer |
Facade delegating to Topology, Metrics, Behaviours, Insights |
Knowledge.MacroMap |
Static mapping of use Module to injected function signatures |
Knowledge.DispatchPatterns |
Runtime-dispatch patterns loaded from priv/config/dispatch_patterns.json |
Knowledge.ScoringConfig |
Heatmap/change_risk/god_modules/unprotected_hubs constants from priv/config/scoring.json |
Knowledge.CodeDigest |
Identity hash of code-tier modules + config files for L2 cache invalidation |
Knowledge.Store.Reader |
Direct ETS reads bypassing GenServer (concurrent bulk reads) |
The graph is built in 11 sequential passes over the file-keyed AST data map. Each pass is a pure function that takes a graph and returns a graph; all intermediate state lives in the function arguments. Passes 7-11 synthesize edges for runtime-dispatched call sites that the static AST walker cannot resolve directly.
| Pass | What it does | Edge label |
|---|---|---|
| 1. Vertices | Adds module, function, struct, behaviour vertices | (none — vertices) |
| 2. Dependency / implements edges | import/alias/use/require → :depends_on; use/require of a project module → :implements |
:depends_on, :implements |
| 3. xref call edges | Optional, requires compiled BEAMs in target project | {:calls, :xref} |
| 4. Function-level call edges (AST) | Walks each function body for remote and local calls; module-stack-aware so def nodes attribute to their real enclosing defmodule |
{:calls, :direct | :alias_resolved | :erlang_atom | :local} |
| 5. Module edge promotion | Collapses MFA → MFA :calls edges into module-level edges |
:calls |
| 6. Module-reference edges | Catches modules passed as atom args to framework macros (Phoenix router, plug, supervision children, struct literals) | :references |
| 7. Protocol-dispatch edges | defimpl impl modules → protocol module: synthesizes function-level edges from the protocol to each impl's function vertices |
{:calls, :protocol_impl} |
| 8. Behaviour-dispatch edges | External-framework behaviours (GenServer, Ecto.Type, Phoenix.LiveView, etc.) → callback functions in implementer modules | {:calls, :behaviour_impl} |
| 9. Phoenix router-dispatch edges | Parses get/post/put/..., resources, scoped routes; emits edges from router module to each controller action |
{:calls, :router_dispatch} |
| 10. Function-reference edges | Detects four runtime-reference forms: literal MFA tuples {Mod, :fn, [args]}, function captures &Mod.fn/N, apply/3, and any 3-arg call carrying MFA-shape args (Task.start_link(M, F, A), supervisor child specs, etc.) |
{:calls, :mfa_ref | :capture_ref | :apply_ref | :mfa_arg_ref} |
| 11. Use-injected import edges | Detects modules whose defmacro __using__/1 injects import N directives, then resolves unqualified calls in consumer files against those imports |
{:calls, :use_import_ref} |
| 12. Supervision topology | Parses Supervisor.start_link/2, Supervisor.init/2 and DynamicSupervisor.start_link/1,2 into the supervision tree. Every endpoint is keyed name_option || module, because supervision identity is not module identity — a root supervisor is typically a registered name with no defmodule, several children are external modules, and two {DynamicSupervisor, name: X} declarations share one module |
{:supervises, %{restart, order, strategy, conditional}} |
Pass 12 only ever adds. It never re-labels an existing vertex, because
Topology.stats/1, Topology.blast_radius/3 and Insights filter with exact
equality (labels == [:module]), so a second label would silently drop the
vertex from those results. The :supervisor / :process labels are minted only
for vertices Pass 12 creates — registered names and external modules, which no
existing filter sees.
The libgraph mechanism, verified rather than assumed:
| call | on a NEW vertex | on an EXISTING vertex |
|---|---|---|
Graph.add_vertex/3 |
sets the label | no-op — the label is silently discarded |
Graph.label_vertex/3 |
— | accumulates — [:module] becomes [:module, :x] |
An earlier version of this section claimed add_vertex/3 accumulates. It does
not, and the distinction is load-bearing in both directions: add_vertex/3
cannot corrupt an existing vertex's labels, which is what makes Pass 12 safe —
and it equally cannot add one, which is why an attempt to mark supervisors
with a :children_unresolved label silently did nothing.
Consequence: the graph cannot carry supervision metadata. A supervisor whose
key collides with a vertex an earlier pass already minted keeps that pass's
labels, so any label-based query is blind to it. Plausible.Supervisor is the
worked example — not a defmodule, appearing exactly once as a name: option,
yet already a vertex by the time Pass 12 ran, which made the whole tree report
supervisor_count: 0, roots: []. Whether the tree worked depended on whether an
unrelated pass happened to mint the same key first.
GET /api/knowledge/supervision therefore builds from extraction declarations
(Supervision.tree_from_declarations/1), which carry strategy, dynamic,
children_unresolved and registered_name directly. Pass 12's edges remain for
graph-shaped consumers — blast radius, traversal — where the flags do not
matter.
Edges from Passes 7-11 are consumed by dead_code_with_asts/3 via a
single reference_targets set (functions referenced via any of the
synthesized edge labels are exempted from the dead-code report). They
are also visible in verify_l3's stratified sample-identity check —
the bucket names match the edge sub-labels.
Knowledge.Store orchestrates: it owns the Graph struct in its state, delegates
computation to the pure modules above, and caches results in its state map. The
Store.Reader module provides a fast path for bulk extraction (all_modules,
all_functions, all_dependencies) that reads directly from ETS without going through
the GenServer mailbox.
Beyond embedding and search, the Intelligence layer provides four briefing and validation modules used by the API:
| Module | Responsibility |
|---|---|
Intelligence.ArchitectBrief |
Single-call project briefing with topology and health metrics |
Intelligence.Preflight |
Contract checklist pipeline (6 sections) with semantic tool ranking |
Intelligence.SurgicalBriefing |
Layer 1+2 preprocessing: semantic search + knowledge graph enrichment |
Intelligence.PlanValidator |
Graph-aware validation for code change plans (cycles, hub risk, blast radius) |
Canonical LLM integration path. Giulia is the eyes; the LLM lives in the client. External clients (Claude Code, Claude Desktop, or anything speaking MCP / REST) call Giulia for read-only data; reasoning happens client-side. This supersedes the legacy local-chat inference subsystem (Section 18 / TIER 3).
Giulia exposes a native Model Context Protocol (MCP) server alongside the REST API. MCP enables AI assistants like Claude Code to discover and call Giulia's tools directly as structured tool calls, without constructing HTTP requests.
| Module | Responsibility |
|---|---|
MCP.Server |
Anubis MCP server — handles tools/call, tools/list, resources/read |
MCP.ToolSchema |
Auto-generates 77 MCP tool definitions from @skill annotations on sub-routers (80 skills minus 3 non-MCP-compatible monitor endpoints: GET /api/monitor and GET /api/monitor/graph are HTML dashboards, GET /api/monitor/stream is an SSE event stream) |
MCP.ResourceProvider |
5 resource templates (giulia://projects/, giulia://modules/, giulia://graph/, giulia://skills/, giulia://status) |
Daemon.Plugs.McpAuth |
Bearer token authentication via GIULIA_MCP_KEY env var (constant-time comparison) |
Daemon.Plugs.McpForward |
Runtime forwarder to Anubis StreamableHTTP transport (defers init to avoid persistent_term race) |
The MCP server is conditional — it only starts if GIULIA_MCP_KEY is set.
Tool schemas are generated at boot from the same @skill annotations that power
the Discovery API, ensuring REST and MCP always expose identical capabilities.
Client configuration (.mcp.json):
{
"mcpServers": {
"giulia": {
"type": "http",
"url": "http://localhost:4000/mcp",
"headers": {
"Authorization": "Bearer <GIULIA_MCP_KEY value>"
}
}
}
}The Runtime subsystem has grown beyond the core Inspector + Collector pair:
| Module | Responsibility |
|---|---|
Runtime.Inspector |
BEAM introspection via :erlang APIs (memory, stats, processes) |
Runtime.Inspector.Trace |
Short-lived per-module call tracing with 5-second kill switch |
Runtime.Collector |
Periodic snapshot collector with burst detection (IDLE/CAPTURING FSM) |
Runtime.Profiler |
Performance profile generator (template-based, offline, pure functions) |
Runtime.IngestStore |
Buffers runtime snapshots from Monitor, fuses with static knowledge data |
Runtime.Observer |
Observation controller for async collection sessions and HTTP push |
Runtime.AutoConnect |
Auto-connect to target BEAM node on startup with exponential backoff |
Runtime.Monitor |
Monitor lifecycle orchestrator (BOOT -> CONNECT -> WATCH -> PROFILING) |
A typical API request follows this path:
HTTP request
|
v
Bandit (HTTP server)
|
v
Plug.Telemetry emits [:giulia, :http] telemetry event
|
v
Plug.Logger
|
v
Plug.Router (:match, :fetch_query_params, Plug.Parsers)
|
v
Endpoint.ex core routes + forward declarations
|
+-- forward "/api/index" --> Routers.Index
+-- forward "/api/knowledge" --> Routers.Knowledge
+-- forward "/api/intelligence" --> Routers.Intelligence
+-- forward "/api/briefing" --> Routers.Intelligence (alias)
+-- forward "/api/brief" --> Routers.Intelligence (alias)
+-- forward "/api/plan" --> Routers.Intelligence (alias)
+-- forward "/api/runtime" --> Routers.Runtime
+-- forward "/api/search" --> Routers.Search
+-- forward "/api/transaction" --> Routers.Transaction
+-- forward "/api/approval" --> Routers.Approval
+-- forward "/api/monitor" --> Routers.Monitor
+-- forward "/api/discovery" --> Routers.Discovery
+-- forward "/mcp" --> Plugs.McpForward (MCP protocol)
|
v
Sub-router (e.g., Routers.Knowledge)
|
v
Daemon.Edge.resolve_ready/1 path resolution + scan-readiness (shared with MCP dispatch)
Knowledge.Facade / Search.Facade string->typed coercion, defaults, response shape (shared with MCP)
| (the per-domain facade is the single place these live; the REST route
| and the MCP dispatch handler are thin renderers over the same call)
v
GenServer.call to Knowledge.Store / Context.Store
|
v
ETS / libgraph lookup
|
v
Helpers.send_json/3 (REST renders the facade result as JSON / 4xx / 409;
MCP renders the same result as {:ok, _} | {:error, msg})
Core routes that remain in Endpoint.ex (not forwarded):
GET /health-- health check (node name, version)POST /api/command-- main chat/command entry pointPOST /api/command/stream-- SSE streaming inferencePOST /api/ping-- lightweight path validationGET /api/status-- uptime, active project countGET /api/projects-- list active projectsPOST /api/init-- initialize a project contextGET /api/debug/paths-- path mapping diagnosticsGET /api/agent/last_trace-- last inference traceGET /api/approvals-- pending approval requestsGET /favicon.ico-- static favicon
Before Build 94, Endpoint.ex was 1,331 lines containing all route handlers. The refactoring split it into 9 domain-specific sub-routers, reducing Endpoint to forwarding declarations plus core route handlers.
Each sub-router uses the Giulia.Daemon.SkillRouter macro:
defmodule Giulia.Daemon.Routers.Knowledge do
use Giulia.Daemon.SkillRouter
@skill %{
intent: "Get modules that depend on a given module",
endpoint: "GET /api/knowledge/dependents",
params: %{module: "Elixir module name"},
returns: "List of dependent modules",
category: "knowledge"
}
get "/dependents" do
# ...
end
endThe use Giulia.Daemon.SkillRouter macro provides:
use Plug.Routerwith standard plugs (match, fetch_query_params, JSON parser)import Giulia.Daemon.Helpersfor shared response/path functions@skillas an accumulate attribute for route metadata__skills__/0function generated at compile time (via@before_compile)
The __skills__/0 function powers the Discovery Engine (/api/discovery/skills,
/categories, /search), which allows clients to discover available endpoints
at runtime without hardcoding route tables.
Sub-routers and their domains:
| Prefix | Router | Routes | Domain |
|---|---|---|---|
| /api/index | Routers.Index | 10 | Module/function index, scan, verify, compact, complexity |
| /api/knowledge | Routers.Knowledge | 27 | Graph queries, metrics, insights, topology, conventions |
| /api/intelligence | Routers.Intelligence | 6 | Briefing, preflight, architect, validate, report_rules |
| /api/runtime | Routers.Runtime | 16 | BEAM introspection, trace, connect, profiles, ingest, observations |
| /api/search | Routers.Search | 3 | Text search, semantic search, semantic status |
| /api/transaction | Routers.Transaction | 3 | Transactional file operations |
| /api/approval | Routers.Approval | 2 | Interactive consent gate |
| /api/monitor | Routers.Monitor | 7 | Dashboard, Graph Explorer, SSE stream, history, observe start/stop/status |
| /api/discovery | Routers.Discovery | 4 | Skill introspection, search, report rules |
| (core endpoint) | Endpoint | 10 | health, command, ping, status, projects, init, debug, trace, approvals |
Note: /api/briefing, /api/brief, and /api/plan all forward to
Routers.Intelligence as aliases.
REST and MCP both front the same internal handlers, but originally each re-implemented param required-ness, defaults, and coercion (the MCP dispatch was meant to be a thin proxy and had drifted into a parallel implementation). That let the two protocols' param contracts diverge independently. Build 163 introduced a shared edge + per-domain facades so the contract lives once:
| Module | Responsibility |
|---|---|
Daemon.Edge |
Domain-agnostic protocol edge: resolve_ready/1 (host->container path resolution + scan-readiness, returns plain data + actionable hint) and not_ready_message/1. Shared by every domain and both protocols. |
Knowledge.Facade |
Per-domain coerce + Store call for knowledge endpoints (impact, style_oracle, unprotected_hubs, duplicates, pre_impact_check): owns defaults (depth/top_k/thresholds), response normalization, the schema_version stamp, and the embedding-availability check. |
Search.Facade |
Same for the search domain (semantic): canonical result shape + top_k default. |
Three layers: edge (resolve + readiness) -> domain facade (coerce +
defaults + Store call) -> Store (typed, pure semantics — never learns
about strings, HTTP, or defaults). A REST route body and the matching MCP
dispatch handler are now thin renderers over the same facade call, each
rendering the result in its own idiom (REST: JSON + 4xx/409; MCP: {:ok, _} |
{:error, message} with the hint preserved as a binary string).
test/giulia/mcp/rest_mcp_parity_test.exs is the standing guarantee: for every
facade-routed endpoint, the same input through REST and through MCP must agree.
This also closed pre-existing divergences — MCP gained the scan-readiness and
embedding-availability signals it lacked, and search/semantic converged on one
canonical shape (it previously emitted raw structs via MCP and a functions-only
count).
Giulia embeds module and function descriptions into a vector space for semantic similarity search.
Model: sentence-transformers/all-MiniLM-L6-v2, loaded via Bumblebee into an
Nx.Serving (Intelligence.EmbeddingServing). The model is approximately 90MB and
is the primary reason the monitor node skips this child.
Indexing: On every scan, Intelligence.SemanticIndex embeds all module
descriptions and function signatures. Vectors are stored in ETS and persisted to
CubDB (L2) for warm restarts.
Search: Given a query string, the serving generates an embedding vector. The
SemanticIndex computes cosine similarity against all stored vectors using Nx.dot,
then ranks results with Nx.top_k.
Preflight integration: The /api/briefing/preflight endpoint uses semantic
search to match a user's prompt against the skill intents declared across all
sub-routers. The response includes a suggested_tools list ranked by cosine
similarity, allowing clients to discover which API endpoints are most relevant to
their current task. Graceful degradation: if EmbeddingServing is unavailable (model
failed to load, or running on monitor node), suggested_tools returns an empty list.
Giulia runs inside Docker but receives file paths from clients on the host machine. Two modules handle path security and translation.
Giulia.Core.PathMapper translates between host paths and container paths using a
prefix swap strategy.
Host: /srv/code/MyProject/lib/foo.ex
Container: /projects/MyProject/lib/foo.ex
Mapping: GIULIA_HOST_PROJECTS_PATH="/srv/code"
Container prefix: /projects
The translation:
- Normalizes Windows backslashes to forward slashes
- Performs case-insensitive prefix matching for Windows drive letters only
- Swaps the host prefix with the mapped container prefix
GIULIA_PATH_MAPPING adds further prefixes beyond /projects, written
host=container and separated by ;. Where several mappings could apply, the
longest prefix wins, and a prefix matches only on a path-segment boundary, so
/srv/code claims /srv/code/app but never /srv/codex.
No host path is compiled into the module: every mapping is supplied by the
environment. One image therefore behaves the same under Docker Desktop on
Windows, OrbStack on macOS, and native Linux, and a difference in behaviour can
only follow from a difference in configuration. Case-insensitivity is confined
to Windows drive-letter prefixes because /Users and /users are distinct
directories on the Linux filesystem inside the container.
An unmapped path is returned unchanged. diagnostics/0 reports the active table
and is surfaced on GET /health, so a misconfigured mount is visible in a single
request rather than as an empty scan result later.
The reverse translation (to_host/1) inverts the same table for responses that
include file paths, so clients see paths they can open locally.
Giulia.Core.PathSandbox ensures Giulia can only access files under the project
root -- the directory containing GIULIA.md (the project constitution). It:
- Expands the requested path to an absolute path (resolving
.., symlinks) - Verifies the expanded path starts with the sandbox root
- Rejects any path that escapes containment
This prevents the LLM from requesting reads of /etc/passwd, ~/.ssh/config, or
any file outside the project boundary, regardless of how the path is constructed.
Giulia ships two browser-based dashboards, both served as static HTML from the
daemon's /api/monitor prefix.
Real-time telemetry dashboard. Every HTTP request, inference step, LLM call, and
tool execution emits a :telemetry event, captured by Monitor.Telemetry handlers
and pushed to Monitor.Store (a rolling 50-event buffer with SSE pub/sub).
Features:
- SSE streaming: live event feed via
/api/monitor/stream - Category filters: API, OODA, LLM, TOOL — toggle visibility per event type
- Project scoping: dropdown auto-populated from events, filters by project path
- Endpoint exclusion: right-click to exclude noisy paths (persisted in localStorage)
- Response panel: click any API event to see its JSON response body
- Think Stream: real-time display of LLM
<think>blocks during inference - Cache/Graph panels: live project health (Merkle root, graph stats, top hubs)
- Scans panel: scan event history with warm/cold/incremental badges
Events carry a project field (extracted from HTTP ?path= params via PathMapper,
or from inference metadata). This enables per-project filtering when multiple
codebases are being analyzed concurrently.
Interactive dependency graph visualization powered by Cytoscape.js (loaded from CDN).
Data source: GET /api/knowledge/topology returns the full module graph in
Cytoscape-ready format (nodes with heatmap scores/centrality, edges with labels).
Four view modes:
- Dependency: full module topology, nodes colored by heatmap zone, sized by fan-in
- Heatmap: emphasizes red/yellow modules, dims healthy green nodes
- Blast Radius: click any module to highlight depth-1 (orange) and depth-2 (blue) impact
- Hub Map: highlights high-degree modules, dims low-degree periphery
Layout options: force-directed (cose), hierarchical (breadthfirst), circle, concentric. Click any node for a details panel showing score, zone, fan-in/out, complexity, and test status. Hover to highlight connected edges.
The topology endpoint combines data from three sources in a single call:
Knowledge.Store.all_dependencies/1— edge list with labelsKnowledge.Store.heatmap/1— per-module scores and zonesKnowledge.Store.find_fan_in_out/1— centrality data
Both dashboards share a navigation bar for switching between Monitor and Graph Explorer views.
Giulia enforces cross-store sync and extraction-output stability through three test-surface layers that ship alongside the code. Each catches a different class of regression that property-style or example-style tests miss on their own.
Giulia.Persistence.Verifier and Giulia.Storage.Arcade.Verifier implement
round-trip integrity checks between the three storage tiers. They're exposed
both as HTTP endpoints for live-daemon use and as mix-test jobs for CI:
GET /api/knowledge/verify_l2?path=...&check=all— L1 ETS ↔ L2 CubDB round-trip for graph, AST, and metric caches. Vertex-set parity, edge-count parity, and stratified sample identity per payload.GET /api/knowledge/verify_l3?path=...&sample_per_bucket=N— L1 → L3 ArcadeDB CALLS. Stratified sample across:viabuckets (:direct,:alias_resolved,:erlang_atom,:local) plus a?/!orthogonal cross-cut, and total-count parity.test/giulia/persistence/verifier_test.exs(11 tests) andtest/giulia/storage/arcade/verifier_test.exs(4 tests) drive the same verifier functions from mix test, with both happy-path assertions and drift-detection cases (deliberately corrupted L2/L3 state that the verifier must classify correctly).
For every filter predicate in a cross-store pipeline, both sides are tested:
drop-side fixtures parametric over the filter's criteria, AND
pass-through fixtures strictly larger than the drop set. Pass-through is
what catches silent over-match — a predicate that rejects more than it claims
to. Applied to four surfaces, this pattern caught 11 distinct silent-over-match
bugs on first run (Indexer.ignored?/1 multi-segment dir, ToolSchema.mcp_ compatible?/1 /stream substring, Conventions.check_try_rescue_flow_control/ 3 source-text String.contains?, Topology.fuzzy_score/2 empty-needle
String.contains?(_, "") == true).
StreamData properties cover the pure-function layer: Knowledge.Builder. build_graph/1 (determinism, module vertex parity, function vertex coverage,
label sanity), Topology.fuzzy_score/2 (bounded tier set, empty-needle
absorbent, reflexive 100), Conventions.walk_ast/3 (determinism, no-crash on
generated Elixir, violations well-formed), and the extraction passes
themselves (module/function shape, per-module attribution).
Golden fixtures in test/fixtures/extraction/ freeze the full file_info
output from Processor.analyze/2 for six curated source cases (predicate/
bang names, default args, moduledoc variants, framework callbacks, protocols
- defimpl, nested modules, macros + guards). Any drift in extraction output
produces a visible diff against the frozen
.expected.exsthat the reviewer must ratify. Regeneration:GOLDEN_UPDATE=1 mix test test/giulia/ast/golden_ fixtures_test.exs.
Static analysis cannot see everything that happens at runtime. The 12-pass builder + the dispatch-patterns config close most of the gap; this section names what remains, deliberately, so consumers know where "this looks dead" is a tool limitation rather than a real finding.
| Pattern | Example | Why it's invisible | Mitigation |
|---|---|---|---|
| Variable-bound module dispatch | mod = pick_provider(); mod.run(args) |
The callee module is a runtime value, not a literal __aliases__ AST node |
Out of scope — the call graph cannot be sound here |
Module.concat/1,2 runtime construction |
apply(Module.concat([prefix, suffix]), :run, []) |
The atom is composed at runtime from arbitrary inputs | Out of scope |
| Telemetry handler attachment | :telemetry.attach(:id, [:event], &Mod.fn/4, _) |
Captures are stored in :telemetry's ETS table at runtime; the static walker sees the &Mod.fn/4 capture (Pass 10 picks it up as :capture_ref) but not the activation site |
Pass 10 covers the capture; activation invisible by design |
| Mox / dynamic mocks | Mox.defmock(MockedX, for: X); MockedX.fn() |
Mock modules are defined at compile time of the test file, only exist in :test env, and are routed via Mox at runtime |
Tests are excluded from the scan; :test_only category surfaces residuals when a function is referenced from tests but called nowhere else |
apply/2,3 with a computed atom argument |
apply(@modules, mod, args) |
Pass 10 handles literal apply(M, :f, [_, _]) but not when M is a variable |
Out of scope — covered by library_public_api / genuine classification when residuals surface |
| Macro-injected calls outside known behaviours | A library's __using__/1 injects def x, do: ... plus calls into another library not in MacroMap |
The injected definition isn't in the source AST; the call site is also injected | priv/config/dispatch_invariants.json (known_behaviour_callbacks) covers ~24 stdlib/ecosystem behaviours; library-specific macros need a project-side @dead_code_ignore or a dispatch_patterns.json entry |
| Runtime-registered routes | Phoenix.Router macros are AST-visible (Pass 9 covers them); custom routers building routes from a config map at boot time are not |
The route table is data computed at runtime | Out of scope; manual dispatch_patterns.json entry if the project uses one |
| Pattern | Example | Why it's invisible | Mitigation |
|---|---|---|---|
| DynamicSupervisor children | DynamicSupervisor.start_child(sup, spec) |
Children are started at runtime, never declared | Supervisor vertex emitted with dynamic: true; an empty child list here is correct, not unresolved |
Child list built by a function call, Enum.* or comprehension |
Supervisor.start_link(List.flatten(children), opts) — Plausible's actual shape |
Outside the bounded binding resolution (single-assignment vars, literal lists, ++ chains, enclosing function only) |
Supervisor emitted with children_unresolved: true and no child edges — an explicit gap, surfaced on the supervisor node in /api/knowledge/supervision. A partial list presented as complete would become a false negative in every check reasoning over the tree |
| Children of external supervisors | {Registry, …}, {Bandit, …} |
Defined in dependency source, outside the scanned project | Vertex emitted with external: true; the tree stops there rather than descending into deps |
| Registry-based via-tuples | {:via, Registry, {R, id}} |
Process identity is runtime data | Module-level resolution only; dependent checks carry a confidence flag |
| Process identity vs module identity | one module started N times | A module started repeatedly is one vertex | cross_process_call_cycle reports high confidence only when every endpoint is a name: __MODULE__ singleton, medium otherwise |
After detection, every dead-code candidate is routed through
Giulia.Knowledge.DeadCodeClassifier. Categories in precedence order:
:test_only— referenced from*_test.exsfiles. Tests are excluded from the scan, so the function is reachable from tests but unreached from production code.:library_public_api—def(public, notdefp) on a project whosemix.exshas noapplication/0 :modentry (i.e. a library, not an OTP app). Public functions in a library are exported for downstream consumers the analyzer cannot see.:genuine— none of the above. Most likely real dead code.:uncategorized— reserved. Future signals (variable-bound dispatch detectors, etc.) can land additively.
The ?relevance=high|medium|all filter (v0.3.8+) on /api/knowledge/dead_code
maps directly: high = :genuine only; medium = :genuine + :uncategorized
(matches the actionable rollup); all = unfiltered.
Reference points captured during the empirical-refactor loop. These are the residuals the tool considers irreducible against well-known codebases — useful as smoke-test signal that a new detector pass hasn't regressed:
| Codebase | Residuals | Categorized as |
|---|---|---|
| Plausible CE (analytics-master) | 3 | 2 true positives + 1 SiteEncrypt accept |
| AlexClaw | 0 | — |
| Plug 1.19.1 | 1 | 1 :library_public_api |
| Bandit 1.10.4 | 2 | 2 :library_public_api |
A new detector or scoring change that moves these numbers warrants a
slice-skeptical sanity check (per the empirical-refactor loop in
GIULIA.md) before being declared an improvement.
@dead_code_ignore truemodule attribute — opt-out for an entire module, used when the static analyzer demonstrably cannot resolve a project-specific runtime dispatch pattern. Used sparingly — the default is to extenddispatch_patterns.jsonwith a universal pattern rather than per-module suppression.?suppress=rule:Mod1,Mod2;rule2:Mod3on/api/knowledge/conventions— per-rule per-module suppression for conventions that legitimately don't apply (e.g.process_dictionary:Auth.Contextfor code that has documented reasons to use it).GIULIA.mdin the project root — the project constitution. Used by the indexer for project-root detection and as a free-form notes surface for human/agent context that doesn't fit any of the above.
Early Giulia (v0.x) shipped a self-hosted inference subsystem — Giulia
itself ran an internal Observe-Orient-Decide-Act loop, calling out to
LLM providers (LM Studio, Anthropic, Gemini, Groq, Ollama) and
dispatching write-tools (patch_function, bulk_replace,
rename_mfa) behind interactive approval gates. Entry point:
POST /api/command and POST /api/command/stream.
That model is deprecated as of v0.3.8. Giulia's canonical role is the read-only data surface (REST + MCP); the LLM is somebody else's problem (Claude Code, Claude Desktop, any MCP/REST client). The subsystem still loads and the endpoints still respond — for backwards compatibility — but no new work goes there.
Modules in the deprecation set (will be removed in v0.4.0):
lib/giulia/inference/— 32 modules (Inference.Pool,Inference.Approval,Inference.Events,Inference.Trace,Inference.Supervisor,Inference.ContextBuilder,Inference.ToolDispatch,Inference.Transaction,Inference.Escalation,Inference.RenameMFA,Inference.BulkReplace,Inference.Verification, …)lib/giulia/provider/— 6 LLM provider modules (Anthropic, Gemini, Groq, LM Studio, Ollama, Router)lib/giulia/tools/{patch_function, bulk_replace, rename_mfa}.ex— write-tools only ever invoked through the inference dispatcher- HTTP endpoints
POST /api/command,POST /api/command/stream,/api/approval/*,/api/transaction/* - MCP tools under
approval_*andtransaction_*prefixes - Compose env vars
LM_STUDIO_URL,ANTHROPIC_API_KEY,GROQ_API_KEY,GEMINI_API_KEY
Why deprecated rather than removed today: the cleanup is a half-day of cross-cutting work (24 test files, supervision-tree changes, endpoint removals, env-var cleanup, CHANGELOG breaking-change entry). v0.4.0 cuts it.
One-line per release listing what changed in this document. The codebase itself is the source of truth; this is a navigation aid for readers returning after a few releases.
| Doc version | Date | Highlights |
|---|---|---|
| Build 164 / v0.3.8 | 2026-06-09 | Plug-collaudo bug-fix round (3 fixes). Conventions attribution: extracted Context.LineResolver (line→function→module) shared by the conventions analyzer and the Credo correlator, fixing multi-module files that mis-attributed every finding to the file's first module. Enrichment payload_path: project-relative paths (tmp/credo.json) now resolve against the project dir, matching the documented contract. dead_code cache: invalidated on enrichment ingest via a telemetry handler ([:giulia, :enrichment, :ingest] → Knowledge.Store) — a direct call would close a 139-module dependency cycle. Report Stage 7 existence check switched from a shape test to the positive tools_ingested != [] signal (aligned with Section 10). |
| Build 163 / v0.3.8 | 2026-06-08 | New Edge / Facade layer subsection (Section 13) + request-flow diagram updated: the MCP-thin-proxy refactor introduced Daemon.Edge (shared resolution + scan-readiness) and per-domain Knowledge.Facade / Search.Facade (coercion, defaults, response shape) so REST and MCP route through one contract instead of re-implementing it. Closed REST/MCP divergences (MCP gained scan-readiness + embedding-availability signals; search/semantic converged on one canonical shape + total count; schema_version single-sourced). Build 162: @skill params migrated to structured maps (discovery/MCP-inputSchema metadata). |
| Build 161 / v0.3.8 | 2026-04-29 | Configuration surface table grew to 5 entries (dispatch_invariants.json, relevance.json added). Section 5 split: External Tool Enrichment, Knowledge Graph, Intelligence Layer, MCP Layer, and Runtime Layer promoted from H3 to H2. New Configuration Surface H2 (lifted from a Section 4 subsection with brief design-philosophy framing). New Known Blind Spots H2 listing what the AST passes structurally cannot see and the residual taxonomy. Consolidator framing corrected: doc now acknowledges it was a skeleton until v0.3.7. The 3 non-MCP-compatible endpoints named inline. Inference / local-chat subsystem (TIER 3, OODA-loop pipeline, POST /api/command, write-tools, LLM provider tree) marked deprecated — canonical LLM integration is now external clients calling Giulia over MCP / REST. Removal scheduled for v0.4.0. |
| Build 160 / v0.3.7 | 2026-04-29 | Doc baseline alongside the 2026-04-29 trio (orchestration lift, self-scan SIGSEGV fix, verifier parity). |