Skip to content

Latest commit

 

History

History
322 lines (268 loc) · 28.1 KB

File metadata and controls

322 lines (268 loc) · 28.1 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Authoritative documents — read before editing

The repo ships with a curated reading order. Skim these before non-trivial work:

  1. AGENTS.md — operating instructions for AI agents. This is the source of truth for invariants, process rules, and the "do-not-do" list. If anything here conflicts with AGENTS.md, AGENTS.md wins.
  2. docs/plans/overview.md — what Beacon is and why; contains a defense-acronym crash course (CJADC2, MEDEVAC 9-line, LOGSTAT, DDIL, supply classes, ATAK, AIP). Read it if you don't know those terms — don't guess.
  3. docs/plans/agent-spec.md — the design contract for the agent (class hierarchy, model selection, tool registry, system prompt, I/O schemas, eval framework). Governs anything touching the agent or its tools.
  4. docs/plans/implementation.md — the task-by-task TDD build plan, Phases 0–10. Follow it in order. Phase 0 is validation gates and must pass before Phase 1.
  5. docs/plans/dashboard-mockup.html — the visual spec for the dashboard (Phase 6). Match its layout, colors, animations, and demo flow.
  6. docs/plans/glossary.md — military + technical terms. First stop for unfamiliar acronyms. Note: the glossary now describes Lemonade as a multimodal server (LLM + Whisper STT + Kokoro TTS via OpenAI-compatible endpoints); Beacon calls gaia.audio.lemonade_audio.transcribe() rather than running Whisper in-process.
  7. docs/superpowers/specs/ — in-flight design notes for the mesh + dashboard integration (two-lane EventBus, peer-on-map, etc.).

Repo state

Phases 1–8 and Phase 10 are implemented and the agent works end-to-end on Lemonade. 397 tests collect; 4 are e2e-gated behind BEACON_E2E=1. The two-lane EventBus refactor (sync + trigger) has landed alongside MeshEnvelope / ConfirmPayload / RejectPayload schemas; mesh and AIP integrations subscribe through the bus rather than poking state directly. v0.1.0 cuts the first tagged release — full reasoning-trace coverage on every query lifecycle, 90 s per-query agent-runner timeout, orphaned-row reaper at lifespan startup, prompt v8.

What's wired today (verify with the named test file before you touch it):

  • Mesh (Phase 8)src/beacon/mesh.py is UDPMeshTransport (alias BeaconMeshAdapter). It consumes MeshService/mesh/transport.UDPTransport, subscribes to state.subscribe_sync to broadcast MeshEnvelope JSON, and pipes inbound packets through state.apply_remote_event (mesh-id filtered, loopback-dropped, bus-silenced). /mesh/{status,peers,enable, disable,test/ping,test/event} endpoints are live. Tested in test_mesh_adapter.py, test_mesh_endpoints.py, test_mesh_envelope.py, test_mesh_id_filter.py, test_mesh_identity.py. BEACON_MESH=1 auto-starts at lifespan; BEACON_MESH=0 is a hard env lock that no-ops /mesh/enable.
  • AIP (Phase 10)src/beacon/aip.py subscribes to subscribe_trigger only (never sync). On confirm envelopes it re-fetches the canonical record by ref_event_uuid and pushes; rejects are no-op. /aip/entities pulls the overlay; when AIP_BASE_URL is unset it returns {entities:[], connected:false} — the real "not configured" state, not a mock. Tested in test_aip.py with FakeHTTP.
  • Voice (Phase 7)src/beacon/voice.py is a thin wrapper over gaia.audio.lemonade_audio.transcribe(). /voice/transcribe returns 200 + transcript on success, 502 on backend down — not 503, no in-process Whisper fallback. Tested in test_voice.py. The dashboard push-to-talk button was removed; dashboard/src/voice.ts remains for headset clients that POST WAV bytes directly. Reports in the dashboard come in via text or the demo runner.
  • RAGBeaconAgent inherits RAGToolsMixin, which registers 9 tools (query_documents, index_document, rag_status, …) into GAIA's module-global _TOOL_REGISTRY alongside Beacon's 7 doctrinal tools (request_medevac, request_fires, report_consumption, report_event, forecast_burndown, recommend_reroute, query_state). Eager-indexes doctrine/*.pdf next to beacon.db at startup. /rag/* endpoints return 503 / enabled:false when self.rag is None.
  • Autonomous loopsrc/beacon/agent_runner.py owns an asyncio.Queue (cap 64) and exposes /agent/{enqueue,status,start,pause,resume,stop}. The dashboard's status pill (OFF · IDLE · PROCESSING · PAUSED · ERROR) reads from here. 90 s per-query wall-clock cap (BEACON_AGENT_TIMEOUT_S, set 0 to disable); a wedged Lemonade call is abandoned, the row is marked error, and the runner keeps draining. reap_orphaned_processing() fires from lifespan startup before the runner spawns — flips rows stuck in processing (from a prior SIGKILL / OOM / reload) to error with an honest reason so the dashboard reflects reality on every boot.
  • Insights + reasoning tracesrc/beacon/insights.py is now agent-only (heuristics deleted, commit 95e64d6 + the agent-only rewrite). It subscribes to the trigger lane and runs agent.analyze() (tool-less, JSON-array contract) on each confirmed state change, with a 1s trailing debounce that coalesces multi-tool-call bursts from one operator query into a single analyzer call. No periodic timer, no rate gate beyond debounce. src/beacon/reasoning.py is a 500-step in-memory ring buffer streamed at /agent/reasoning/stream. As of v0.1.0, every query lifecycle bookends visibly: AgentRunner emits OBSERVE on dequeue and SURFACE (or DECIDE/error) on completion; _execute_tool records TOOL (dispatching + result phases); pending-confirm tool returns upgrade to PROPOSE; /agent/confirm and /agent/reject record an operator-disposition OBSERVE step; InsightsEngine records QUERY/SURFACE; _capture_reasoning_content records REASONING (THINKING in the dashboard) when an LLM round-trip comes back with a reasoning_content payload — dormant under the Qwen enable_thinking=False default, ready when streaming-thinking is wired.

Phase 9.2 (eval framework) is implemented (src/beacon/eval/__main__.py, beacon-eval CLI, uses gaia.eval.claude.ClaudeClient + gaia.eval.scorecard) with 24 scenarios across 8 categories (iran-showcase, mixed, rag, medevac, logstat, doctrinal, edge, showcase). Spec target was ~40 across more axes; treat the corpus as a sanity gate, not a release gate.

Phase 0 hardware probes were not run because the dev box is a Mac M4 Pro, not Ryzen AI silicon. Lemonade still works (CPU/GPU path) — see Smoke baseline below.

Smoke baseline (2026-05-03, Mac M4 Pro, Qwen3-4B-Instruct-2507-GGUF on Lemonade)

Head-to-head bench across 4 candidate models on the iran-showcase corpus (6 multi-event scenarios), same hardware, same prompt v6:

  model                            pass   avg    batch    p50/scenario
  Qwen3-4B-Instruct-2507-GGUF      5/6    6.7    58s      ~5s    ← default
  Gemma-4-E4B-it-GGUF              6/6    7.0    170s     14s
  Qwen3.5-4B-GGUF                  4/6    6.4    80s      9s
  Gemma-4-E2B-it-GGUF              0/6    5.2    64s      10s

Qwen3-4B-Instruct-2507 wins the tradeoff: highest pass-rate-per-second, sub-5s on 4 of 6 scenarios on M4 Pro, AND it's a non-thinking-only model per the HF card — so no enable_thinking patch tower is needed (eliminates the chain-of-thought-leakage class of failures we saw on Qwen3.5).

Gemma-4-E4B-it-GGUF remains the highest-accuracy alternate at the cost of 3× wall-time; pick it via BEACON_MODEL=Gemma-4-E4B-it-GGUF when the multi-axis-summary stress test must pass.

Latency on this dev box is not representative of fielding hardware (Ryzen AI NPU acceleration is Windows/Linux only). On a Toughbook-class laptop the spec's 5s p50 / 8s p95 target should be reachable.

Common commands

# Setup
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

# Run (4 separate processes)
lemonade-server serve              # local LLM backend, required unless BEACON_USE_OPENAI=1
beacon seed                        # populate SQLite with synthetic demo data
beacon serve                       # FastAPI on :8888 (Lemonade owns :8001/:8002)
beacon ask --auto-confirm "Bravo Co at 38SMB12345678, 2 wounded gunshot urgent surgical"
cd dashboard && npm install && npm run dev   # Vite dev server (default :5173, but :5173 is Claudia
                                              # in this dev box — start with `--port 5174`)

# Confirm-required tools (request_medevac, recommend_reroute, request_fires) land in pending_actions.
# Operator confirms via the dashboard, or:
curl -X POST http://localhost:8888/agent/confirm/<event_uuid>
curl -X POST http://localhost:8888/agent/reject/<event_uuid>

# MeshService standalone server (cross-device demos; the in-process
# UDPMeshTransport at src/beacon/mesh.py is what /mesh/* runs against):
cd MeshService
uv run --with-requirements requirements.txt \
  python server.py --id alpha --name "Alice" --port 5681 --http-port 8000

# Tests
pytest tests/ -xvs                                          # all, verbose, fail-fast
pytest tests/test_state.py -v                               # one file
pytest tests/test_state_two_lane_bus.py -v                  # the new two-lane bus tests
pytest tests/test_state.py::test_apply_remote_event_idempotent -v  # one test
BEACON_E2E=1 pytest tests/test_agent_e2e.py                 # live LLM tests
                                                            # (Lemonade running OR
                                                            #  BEACON_USE_OPENAI=1 with key OR
                                                            #  BEACON_USE_CLAUDE=1 with key)

# Eval (the runner is implemented; the judge is always Claude)
# Two judge backends are supported:
#   --judge-backend claude-code   shells out to `claude -p`, uses your Claude Code
#                                 subscription auth — NO ANTHROPIC_API_KEY NEEDED.
#                                 Prefer this when the dev box already has Claude Code.
#   --judge-backend anthropic-sdk uses gaia.eval.claude.ClaudeClient and requires
#                                 ANTHROPIC_API_KEY (default backend).
beacon-eval --output eval/runs/baseline-$(date +%s)/ --judge-backend claude-code  # subscription auth
beacon-eval --category mixed --output eval/runs/mixed-only/ --judge-backend claude-code
beacon-eval --judge claude-sonnet-4-5-20250929              # override judge model (anthropic-sdk only)

# Lint
ruff check .

CI (.github/workflows/test.yml) runs ruff check . then pytest tests/ -v on Python 3.11 for push and PR to main.

Architecture (big picture)

A single Python agent feeds a single SQLite event log; that log is the canonical state, and several transports / engines fan out from it via a two-lane EventBus:

voice/text  →  BeaconAgent (Lemonade-served LLM + GAIA tools + RAG mixin)
                         │
                         ▼ Pydantic-validated tool calls
                  StateManager (SQLite + two-lane EventBus)
                  ┌──────┴──────┐
                  ▼             ▼
            subscribe_sync    subscribe_trigger
            (peers see        (only confirmed
             pending too)      / non-pending)
                  │             │
                  ▼             ▼
              Mesh transport  AIP push  +  InsightsEngine
              (UDP via        (Palantir     (agent-only,
               MeshService)    REST)         1s debounce)

  → FastAPI/SSE → dashboard (reads /state, /state/stream,
                              /insights/stream,
                              /agent/reasoning/stream)

Key seams that span multiple files:

  • BeaconAgent (src/beacon/agent.py) subclasses gaia.agents.base.agent.Agent (with RAGToolsMixin) and registers 7 tools from src/beacon/tools/: request_medevac, request_fires (call-for-fire, FM 3-09; confirm-required like medevac), report_consumption, report_event, forecast_burndown, recommend_reroute, query_state (read-only lookup so the agent can answer "what happened with X?" without keeping everything in conversation memory). Plus 9 RAG tools from the mixin when self.rag is not None. Tools follow the factory pattern make_<tool_name>(state) so state is injected, not imported. Doctrinal pin-cites are attached to each tool step in the reasoning trace (FM 4-02.2, FM 3-09, FM 4-0, ADP 6-0, FM 3-90).

  • Parallel-tool-call interceptor (agent.py lines 406–555): GAIA's parser raises on parallel tool_calls; Beacon stashes #2..N in _pending_tool_calls, dedupes by (tool, args) signature across LLM retries, and monkey-wraps chat.send_messages to drain the queue without round-tripping. Tied to amd/gaia#944 — delete-when-upstream-lands.

  • Schema-first: every structured output is a Pydantic model in src/beacon/schemas.py. Doctrinal correctness is enforced by Pydantic, not by prompting. Tools return one of {"status": "ok"|"submitted"|"pending_confirm", ...} on success or {"status": "error", "error": str(e)} on ValidationError. The schema set now also includes MeshEnvelope (typed wrapper for everything that crosses a transport), MedevacPayload / ConsumptionPayload / EventPayload / ReroutePayload / ConfirmPayload / RejectPayload / PingPayload / TestEventPayload / MeshPayload / ChatPayload, and RerouteAction enum.

  • Two-lane EventBus (StateManager.subscribe_sync and subscribe_trigger):

    • Sync lane fires on every state change including pending. Mesh transports subscribe here so peers mirror situational awareness immediately.
    • Trigger lane fires only on confirmed / non-pending writes. AIP push (and any other irreversible side-effect) subscribes here. This is the AGENTS.md § 2.2 enforcement seam — getting it wrong leaks pending proposals to the data fabric.
    • Subscribers receive MeshEnvelope instances. Failures are swallowed per subscriber so a broken transport never blocks local writes.
    • The legacy subscribe() shim still exists; it maps to subscribe_trigger with a deprecation warning.
  • Confirm-required flow: request_medevac, recommend_reroute, and request_fires write the proposed record to pending_actions and return status="pending_confirm". The trigger lane stays silent. The dashboard confirms via POST /agent/confirm/{uuid} (promotes to canonical table, fires trigger lane) or POST /agent/reject/{uuid} (drops the row, no emission). Both endpoints record an operator-disposition OBSERVE step into the reasoning log so the panel closes the loop visibly. For eval/test runs, BeaconAgent(confirm_mode="never") auto-confirms in the agent's _execute_tool override and strips the confirm_required flag from the return value.

  • GAIA adaptations (the spec's assumed API differs from reality):

    • Agent.__init__ takes model_id, base_url, skip_lemonade, use_claude, use_chatgpt — NOT system_prompt or model. Override _get_system_prompt() for the prompt.
    • Entry point is process_query(text), not run(text). BeaconAgent.run() is a thin wrapper.
    • Tools live in module-global _TOOL_REGISTRY keyed by f.__name__. Two BeaconAgent instances in one process rebind every tool to the second agent's state — known limitation, would require forking GAIA to fix.
    • Agent.__init__ calls LemonadeManager.ensure_ready() unless skip_lemonade=True. Tests pass skip_lemonade=True.
    • GAIA dependency is currently pinned to feat/lemonade-audio-endpoints (not @main) because Beacon's voice layer depends on gaia.audio.lemonade_audio. Switch back to @main when that branch lands upstream. uv.lock carries the cached SHA so existing checkouts stay reproducible.
  • Lazy server agent construction: beacon.server does NOT build the agent at import; it builds in the FastAPI lifespan startup. /health works even if Lemonade is down. Set BEACON_DEFER_AGENT=1 to defer construction to first /agent/run call.

  • Mesh sync (MeshService/ + src/beacon/mesh.py): the MeshService/ UDP transport (UDPTransport) is implemented, tested, and consumed by Beacon through UDPMeshTransport (alias BeaconMeshAdapter). The adapter (a) wraps UDPTransport, (b) subscribes to state.subscribe_sync to broadcast MeshEnvelope JSON, and (c) decodes incoming UDP packets through state.apply_remote_event(envelope). Mesh-id namespacing keeps separate pools from talking; node-id stamping on every envelope drives "no-loopback" via the _silence() context manager during peer-applied writes. The standalone MeshService/server.py (with flat-namespaced /status, /peers, /messages, /send, /broadcast, /ws) still runs for cross-device demos.

  • AIP push (src/beacon/aip.py): subscribes to subscribe_trigger only (per AGENTS.md § 2.2 — pending proposals must never reach the data fabric). On confirm envelopes it re-fetches the canonical record by ref_event_uuid and pushes; rejects are no-op. Beacon-kind → AIP object-type mapping: medevac→MedevacRequest, consumption→SupplyStatus, event→BattlefieldEvent, reroute→RerouteRecommendation. /aip/entities pulls the overlay; offline state when AIP_BASE_URL is unset is a real {connected:false} payload, not a mock.

  • InsightsEngine + ReasoningLog: src/beacon/insights.py is a trigger-lane subscriber with a 1s trailing debounce that calls agent.analyze(snapshot_json) — a tool-less LLM pass that returns a JSON array of Insight. Heuristics were removed (commit 95e64d6 + the agent-only rewrite per docs/superpowers/specs/2026-05-03-remove-heuristic-insights-design.md). Per AGENTS.md § 2.3, parse failures and connection errors return [] and log; we do not fabricate insights to fill the gap. src/beacon/reasoning.py is a 500-step in-memory ring buffer; _execute_tool writes TOOL steps, the InsightsEngine writes QUERY/SURFACE. The dashboard's reasoning column streams from /agent/reasoning/stream.

  • Local-first inference: Lemonade Server is the default. The agent's DEFAULT_MODEL is Qwen3-4B-Instruct-2507-GGUF (Qwen 3 4B Instruct, July 2025 release, GGUF-quantised) — picked from the head-to-head bench above because it ties the throughput sweet spot (5/6 PASS in 58s) with being a non-thinking-only model (no enable_thinking patches needed, no chain-of-thought leakage). Same Hermes tool-calling + Qwen sampling distribution as Qwen3.5, so the parallel-call interceptor and Qwen-specific sampling overrides apply unchanged. Override with BEACON_MODEL=… (other tested options: Gemma-4-E4B-it-GGUF for highest accuracy at 3× latency cost, Qwen3.5-4B-GGUF for vision support). The 30B-class model the spec originally called for is still too large for current dev hardware; swap when Phase 0.1 is re-run on Ryzen AI silicon. OpenAI fallback is opt-in via BEACON_USE_OPENAI=1; Claude fallback is BEACON_USE_CLAUDE=1. If Lemonade is unreachable and no opt-in is set, the server returns 502 with an actionable detail — never silently fall back. The backend lifespan POSTs Lemonade /api/v1/load at startup with ctx_size=BEACON_CTX_SIZE (default 32768) so the active model serves with enough context for multi-turn extraction — Lemonade's server-wide ctx_size config (set once via lemonade config set ctx_size=32768) overrides per-request values, so changing BEACON_CTX_SIZE alone won't take effect unless the server config matches.

    Qwen-specific tuning (lives in BeaconAgent._intercept_chat_send, fires whenever "qwen" in self.model_id.lower()):

    • Thinking mode is force-disabled. Qwen3-family GGUFs ship with reasoning ON by default — the model emits a multi-page Thinking Process: chain-of-thought before any answer. With thinking ON, agent.analyze()'s JSON-array-output contract burns 4+ minutes generating reasoning and never reaches the final array (content stays empty, 0 insights). We pass chat_template_kwargs={"enable_thinking": False} on every send and bump max_tokens to 16384 so non-thinking outputs have room. If/when token-level streaming to the dashboard reasoning panel lands (Phase 2), re-enable thinking and use _capture_reasoning_content to surface reasoning_content live as REASONING reasoning steps.
    • Server-generated insight_uuid. Qwen3-* routinely emits placeholder UUIDs from training data (a1b2c3d4-e5f6-4g7h-...) that fail Pydantic's hex validation and silently throw away otherwise-valid insights. _parse_insights_response overwrites the LLM's value with a fresh uuid4() — safe because dedupe is keyed on dedupe_key, citations use sources (event UUIDs), and the dashboard treats insight_uuid only as a React key + ack/dismiss target.
    • min_context_size is 32768, not 131072. Earlier 131072 caused lemonade_manager._try_reload_with_ctx to silently fail with reported=0 (need 131072), leaving Lemonade in a state where chat calls hang for 5min before timing out. 32K matches Qwen3.5-4B's native context and the lifespan /api/v1/load POST. Override via BEACON_CTX_SIZE when running with a model + hardware that can do better.

Invariants (do not violate)

These come straight from AGENTS.md; they're non-obvious and easy to violate by accident:

  1. Schema-first, then test, then code, then commit. Update or add the Pydantic schema in schemas.py before writing the tool. Write the failing test before the implementation. Never invert this order.
  2. Never bypass the confirm flow for request_medevac, recommend_reroute, or request_fires in tests, demos, or eval runs — except via the explicit confirm_mode="never" opt-in. The trigger lane is the enforcement point.
  3. No mocks in shipped code. If a backend dep isn't available, surface a real "not configured" / "offline" / 502 state — not fake data. Test fakes (FakeHTTP, monkeypatched modules) are fine; they live in tests/.
  4. No silent fallbacks. No broad except Exception. The only place catching-and-returning-error-dict is acceptable is GAIA _TOOL_REGISTRY tool functions — that's the contract for tool error reporting. Everywhere else, raise a typed Beacon error with what failed and what the caller should do.
  5. Synthetic data only. No real OPORDs, casualty data, unit positions, MGRS coordinates, supply consumption rates, or any real military information. New demo data must be fictional and disclaimed.
  6. Do not vendor or fork GAIA. It is consumed via pip install from git+https://github.com/amd/gaia.git@<branch>. File upstream issues; don't copy code.
  7. No AI-attribution in any artifact — no 🤖 Generated with Claude Code footers, no Co-Authored-By: Claude trailers, no "Authored by AI" mentions in commits, PRs, code comments, or docs. The human contributor is the author of record.
  8. Eval before merging prompt or schema changes. Every system-prompt or tool-schema change requires a baseline-vs-iter eval comparison. Prompt versions live as full files (v1.py, v2.py, …) so diffs show actual text changes.
  9. Don't change the LOTR brand framing ("Beacons of Gondor", "we feed the palantíri") without discussing — it's deliberately tied to partner-pitch resonance.
  10. The judge is always Claude. The agent backend can be Lemonade / OpenAI / Claude; the eval judge is gaia.eval.claude.ClaudeClient regardless. ANTHROPIC_API_KEY is required for any eval run.

Server endpoints (live)

All endpoints below are wired to real backends — no stub routes returning fixed JSON. "Offline" means a dependency is unreachable or unconfigured; the response shape carries that as data, not a 503.

Method Path Purpose
GET /health Liveness probe; works even when Lemonade is down
GET /state Full snapshot (medevacs, consumption, events, reroutes, pending)
GET /state/stream SSE stream of state snapshots, ~1Hz polling
POST /agent/run Submit a free-form report; returns {response} (state arrives via SSE)
POST /agent/confirm/{uuid} Promote a pending action; fires the trigger lane
POST /agent/reject/{uuid} Drop a pending action; no emission
POST /agent/enqueue Non-blocking submit into AgentRunner's asyncio.Queue (cap 64)
GET /agent/status AgentRunner state — OFF · IDLE · PROCESSING · PAUSED · ERROR + queue depth
POST /agent/start /pause /resume /stop AgentRunner control
GET /agent/reasoning Recent reasoning steps (in-memory ring buffer)
GET /agent/reasoning/stream SSE per-turn reasoning trace
POST /voice/transcribe Multipart WAV → gaia.audio.lemonade_audio.transcribe(); 502 on backend down
GET /mesh/status UDP transport health + peer table
GET /mesh/peers Peers subset of /mesh/status
POST /mesh/enable /disable Start/stop the transport (respects BEACON_MESH=0 env lock)
POST /mesh/test/ping /test/event Broadcast debug envelopes
GET /aip/entities Pull AIP overlay (units / OPORDs / FOBs); {connected:false} if AIP_BASE_URL unset
GET /insights Current insight list
GET /insights/stream SSE diff stream
POST /insights/{uuid}/acknowledge /dismiss Operator actions on insights
GET /rag/status Corpus stats; enabled:false when self.rag is None
GET /rag/list Per-document listing (503 when RAG disabled)
POST /rag/index Upload + index PDF/txt; 415 on encrypted PDF
DELETE /rag/index/{filename} Remove a document
POST /rag/query Semantic search debug surface
POST /demo/play /stop /pause /resume /reset /inject Timeline playback + custom queries
GET /demo/status Playback snapshot

Where new code goes

What Where
New Pydantic schema src/beacon/schemas.py
New tool src/beacon/tools/<name>.py (factory make_<name>(state))
State manager change src/beacon/state.py (touch subscribe_sync / subscribe_trigger carefully)
Server endpoint src/beacon/server.py
CLI subcommand src/beacon/cli.py
Mesh adapter (Beacon side) src/beacon/mesh.py (consumes MeshService/mesh/transport.UDPTransport)
Mesh transport itself MeshService/mesh/ — keep the standalone service intact
AIP integration src/beacon/aip.py (subscribes to trigger lane only)
Voice (Lemonade-routed) src/beacon/voice.py
Autonomous loop src/beacon/agent_runner.py
Insights (agent-only) src/beacon/insights.py
Reasoning trace src/beacon/reasoning.py
Demo runner / playback src/beacon/demo.py
Identity / node fingerprint src/beacon/identity.py
Dashboard component dashboard/src/<Component>.tsx (MeshPanel, MeshStatusChip, AIPPanel, InsightsPanel, AgentReasoningPanel, AgentStatusPill, BurndownPanel, DoctrineCorpusPanel, DemoControls, CounterfactualPane already exist)
Eval scenario eval/scenarios/<category>/<id>.yaml
Demo data demo/
Architecture diagram docs/architecture-diagram.html (single-file, mirrors dashboard styling)
Test for module X tests/test_<module>.py (one file per module)

Known gotchas

  • beacon serve --db <path> unconditionally writes BEACON_DB_PATH from the CLI default (beacon.db) and clobbers any env var the shell already set. Pass --db <abs-path> explicitly until the CLI is fixed to honor an existing env var.
  • beacon serve --reload re-imports beacon.server on every change, which re-runs Lemonade probing and re-decorates tools into GAIA's module-global _TOOL_REGISTRY. Expect surprises.
  • Lemonade owns ports 8001/8002 when its workers are warm — the dashboard / API on those ports will collide. Beacon defaults to :8888.
  • Vite default port :5173 collides with Claudia in this dev environment. Start the dashboard with npm run dev -- --port 5174.
  • tests/test_tools_reroute.py:46 has (8001, 6000) as MGRS easting/northing — not a port. Don't sed-replace it.

Commit style

Conventional commits: feat(scope):, fix(scope):, docs(scope):, test(scope):, chore(scope):, refactor(scope):. Imperative mood, short summary, optional longer body. Examples in AGENTS.md § 3.5.