Skip to content

Latest commit

 

History

History
105 lines (74 loc) · 11.2 KB

File metadata and controls

105 lines (74 loc) · 11.2 KB

CLAUDE.md

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

Project

flowd is a local-first memory, orchestration, and rules engine for AI coding agents. The flowd binary runs as a daemon and exposes an MCP server so Claude Code and Cursor can persist context, run multi-step plans, and gate actions against user-defined rules. Edition 2024, Rust 1.85.

Commands

Dev loop:

cargo check --workspace
cargo check -p <crate>                         # tighter loop on a single crate
cargo clippy --workspace --all-targets -- -D warnings
cargo nextest run -p <crate>                   # preferred; install with `cargo install cargo-nextest --locked`
cargo nextest run --workspace                  # pre-commit / CI
cargo test -p <crate>                          # libtest fallback when nextest is unavailable
cargo nextest run -p flowd-mcp --test e2e      # one integration suite
cargo nextest run -p flowd-mcp -E 'test(<name>)'  # single test by name

Do not use cargo build in the agent loop — it is wall-clock expensive and reserved for explicit release/smoke steps (.flowd/rules/cargo-discipline.yaml). cargo install --path crates/flowd-cli is also a release step, not a dev-loop action: hot-reinstalling swaps the MCP daemon under any connected client.

Runtime (when manually exercising the daemon):

flowd start              # foreground daemon; pair with Qdrant on :6334
flowd status             # PID, tier row counts, token/cost rollups
flowd stop               # SIGTERM the PID in $FLOWD_HOME/flowd.pid
flowd mcp                # stdio proxy for IDE clients (launched by Cursor / Claude Code)
flowd plan events <id>   # replay persisted lifecycle log for one plan
flowd plan usage <id>    # cost / token / cache rollup for one plan

Architecture

Seven workspace crates with strict layering — flowd-core defines traits and domain types and has no I/O dependencies (its one internal dependency, flowd-detectors, is itself pure decision logic, so the I/O-free guarantee holds). Concrete I/O lives only in the satellite crates:

Crate Role
flowd-core Traits + domain types: memory, rules, orchestration, the rules_check baseline screen. Tokio is OK; SQLite/Qdrant/reqwest/ort are not (.flowd/rules/crate-boundaries.yaml). Depends on flowd-detectors only, which is itself pure.
flowd-detectors Pure path / command / prompt detectors. I/O-free decision logic: the host resolves and canonicalises inputs and passes them in; the in-code deny floor cannot be relaxed by config (.flowd/rules/detector-discipline.yaml).
flowd-storage SQLite backend (rusqlite) with FTS5 keyword search and WAL mode.
flowd-vector Qdrant vector index backend.
flowd-onnx ONNX embedding provider (CPU thread pool via ort).
flowd-mcp JSON-RPC 2.0 MCP server. Generic over backends; pulls in no storage deps.
flowd-cli flowd binary. Composes the stack, owns daemon lifecycle and integration driver.

Search is hybrid (FTS5 + ANN vector results merged via Reciprocal Rank Fusion). Keyword search works without the daemon; vector search needs Qdrant and a running flowd start.

MCP layer (flowd-mcp)

  • protocol — JSON-RPC 2.0 envelopes, transport-agnostic.
  • handlersMcpHandlers trait + FlowdHandlers wiring flowd-core services.
  • server — dispatch + line-delimited stdio transport.
  • tools — parameter structs, MCP result envelope, JSON-Schema for tools/list.
  • compilerStubPlanCompiler (deterministic, structured markdown), LlmPlanCompiler (prose → DAG via the configured LLM transport).
  • llmClaudeCliCallback, OpenAiCompatibleCallback (Ollama / mlx_lm.server / vLLM / llama.cpp), plus the reserved claude-http slot.
  • observerPlanEventObserver consuming the broadcast channel from flowd-core.

rules_check evaluates a non-overridable baseline — the flowd-detectors path / command floor, run via flowd-core's RuleEvaluatorbefore any user rule, and folds both layers into one GateResult. allowed is false if either the baseline or a user deny rule trips; a baseline deny is final, so user rules can only further restrict it, never relax it (an empty ~/.flowd/rules/ is therefore still safe). The widened ProposedAction carries the raw command and a host-canonicalised canonical_path so the detectors can screen inside rules_check while flowd-core stays I/O-free. Every baseline deny/warn is audited to the security_events table.

Two integration suites worth knowing:

  • crates/flowd-mcp/tests/integration.rs — wire protocol against stub handlers.
  • crates/flowd-mcp/tests/e2e.rs — real SqliteBackend, in-memory vector stub, real rules, real orchestrator. Canonical regression test for the agent-facing surface.

Orchestration (flowd-core::orchestration)

Plan lifecycle is Draft → (optional clarify/refine loop) → Confirmed → Running → Completed/Failed/Cancelled. Submodules:

  • compiler, clarification, template — prose-first plan input and the open-question loop.
  • executor, layer_runner — topological scheduling and per-layer execution; parallel layers run agents in dedicated git worktrees under flowd/<project>/<plan>/<step>/.
  • gateresolve_workspace_root (MCP hint → FLOWD_WORKSPACE_ROOT → cwd) plus the wrong-repo guard that verifies git rev-parse --show-toplevel and git --git-common-dir match the resolved project_root before any agent runs.
  • integration — the locked contract for plan_integrate: manual confirm is the default mode, no push, fast-forward-only promotion, tip-only cherry-pick, KeepOnFailure cleanup. The CLI driver is in crates/flowd-cli/src/integration.rs.
  • plan_events, observer, store — broadcast channel + persisted lifecycle log + per-plan snapshot store.

Key invariant: project (namespace label) and project_root (absolute execution path) are independent. The daemon never falls back to its own current_dir() if a higher-priority signal (plan_create arg, FLOWD_WORKSPACE_ROOT) is available.

Storage (flowd-storage)

sqlite.rs is the entry point; plan_store.rs, plan_event_store.rs, step_branch_store.rs, and security_event_store.rs are the row-level stores. The security_events table (added in MIGRATION_009) is the dedicated audit log for detector verdicts — written by both the PreToolUse hook and the rules_check baseline, kept off the observations hybrid-search / embedding path the same way plan-lifecycle events are. Schema migrations live as inline const MIGRATION_NNN: &str items in crates/flowd-storage/src/migrations.rs and are forward-only (.flowd/rules/schema-migrations.yamldeny level). Append a new higher-numbered constant; never edit or reorder an existing one.

CLI (flowd-cli)

main.rs is a thin clap entry point; subcommand handlers live under src/commands/ (start, mcp, stop, search, history, plan, rules, security, observe, status, export, init, hook). flowd security events reads the security_events audit table (filterable by --detector / --decision / --since / --project, newest first). daemon.rs owns PID-file and socket lifecycle, paths.rs resolves $FLOWD_HOME, spawner.rs and integration.rs drive worktree-based agent execution and the post-completion plan_integrate cherry-pick.

Hard rules (from .flowd/rules/)

These are project-policy rules. The rules engine itself can only inspect { tool, file_path, project }, so the agent must honor them in the diff or command:

  • flowd-core is I/O-free. Do not add rusqlite, qdrant-client, ort, reqwest, hyper, or tonic to crates/flowd-core/Cargo.toml. Define the trait there, implement in the satellite crate. (crate-boundaries.yaml, deny.)
  • flowd-detectors is pure decision logic. Anything under crates/flowd-detectors/** must stay I/O-free (no std::fs, network, or db) — the host resolves paths and config and passes the results in. The deny-list constants (SECRET_DIRS, FETCHERS, INTERPRETERS) are a hard floor that lives in code, not config; config may only add path allowlist entries, never subtract from the floor (the secrets check runs before the allowlist). The same floor backs the rules_check baseline: detectors run before user rules and a baseline deny is non-overridable — user rules can only further restrict, never relax it. Per-detector [security] levels may only raise a verdict's severity above its in-code floor (deny for path/command, warn for prompt); a below-floor value is rejected at startup. (detector-discipline.yaml.)
  • MCP stdout is reserved for JSON-RPC frames. Anywhere under crates/flowd-mcp/src/**, never use println!, print!, or dbg!. Diagnostics go to stderr via eprintln! or tracing::*. A single stray write silently corrupts every connected client. (mcp-wire-discipline.yaml.)
  • Schema migrations are forward-only. New migration → append a MIGRATION_NNN constant in crates/flowd-storage/src/migrations.rs and add it to the MIGRATIONS slice. Never edit or reorder existing ones. (schema-migrations.yaml, deny.)
  • Hook handlers must swallow errors. In crates/flowd-cli/src/commands/hook.rs, every error path must log via tracing::warn!/error! and return Ok(()). Do not propagate errors out of run / dispatch — a surfaced error hangs the IDE that invoked the hook. The one sanctioned non-zero exit is a PreToolUse detector DENY (std::process::exit(DENY_EXIT_CODE), exit 2); internal errors still log and exit 0. Persistence guarantees live in MCP (memory_store), not hooks. (hook-error-swallowing.yaml.)
  • cargo build is off the agent loop. Use cargo check (workspace or -p <crate>) for feedback. Prefer cargo nextest run -p <crate> over cargo test. (cargo-discipline.yaml.)
  • Crate version bumps ship in a separate PR. Feature/fix/refactor/docs commits must not edit crates/*/Cargo.toml version lines. Bumps follow chore(<crate-short>): bump version to vX.Y.Z. Decide SemVer impact from the diff — public API, CLI, MCP wire format, persisted schema, and rules-engine behavior are all "public". (crate-version-bumps.yaml.)
  • Conventional Commits, with ! for breaks. Each plan_integrate tip commit becomes part of base history on promotion, so messages must read as coherent base-branch changes. Break-flag (! or BREAKING CHANGE:) is required for any change to a documented contract (public API, MCP wire format, CLI flags, persisted schema, rules behavior). (commit-message-discipline.yaml.)

Config

flowd.toml at the repo root is a reference config — the daemon reads $FLOWD_HOME/flowd.toml (default ~/.flowd/flowd.toml). To activate this repo's config either symlink it, copy it, or launch with FLOWD_HOME="$PWD" flowd start. The shipped settings pin the LLM-backed compiler to Opus 4.7 via claude-cli with a two-tier escalation that bumps effort = "max" for plan_refine.