This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
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 nameDo 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 planSeven 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.
protocol— JSON-RPC 2.0 envelopes, transport-agnostic.handlers—McpHandlerstrait +FlowdHandlerswiring flowd-core services.server— dispatch + line-delimited stdio transport.tools— parameter structs, MCP result envelope, JSON-Schema fortools/list.compiler—StubPlanCompiler(deterministic, structured markdown),LlmPlanCompiler(prose → DAG via the configured LLM transport).llm—ClaudeCliCallback,OpenAiCompatibleCallback(Ollama /mlx_lm.server/ vLLM / llama.cpp), plus the reservedclaude-httpslot.observer—PlanEventObserverconsuming the broadcast channel fromflowd-core.
rules_check evaluates a non-overridable baseline — the flowd-detectors path / command floor, run via flowd-core's RuleEvaluator — before 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— realSqliteBackend, in-memory vector stub, real rules, real orchestrator. Canonical regression test for the agent-facing surface.
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 dedicatedgit worktrees underflowd/<project>/<plan>/<step>/.gate—resolve_workspace_root(MCP hint →FLOWD_WORKSPACE_ROOT→ cwd) plus the wrong-repo guard that verifiesgit rev-parse --show-toplevelandgit --git-common-dirmatch the resolvedproject_rootbefore any agent runs.integration— the locked contract forplan_integrate: manual confirm is the default mode, no push, fast-forward-only promotion, tip-only cherry-pick,KeepOnFailurecleanup. The CLI driver is incrates/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.
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.yaml — deny level). Append a new higher-numbered constant; never edit or reorder an existing one.
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.
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-coreis I/O-free. Do not addrusqlite,qdrant-client,ort,reqwest,hyper, ortonictocrates/flowd-core/Cargo.toml. Define the trait there, implement in the satellite crate. (crate-boundaries.yaml, deny.)flowd-detectorsis pure decision logic. Anything undercrates/flowd-detectors/**must stay I/O-free (nostd::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 therules_checkbaseline: detectors run before user rules and a baselinedenyis 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 (denyfor path/command,warnfor 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 useprintln!,print!, ordbg!. Diagnostics go to stderr viaeprintln!ortracing::*. A single stray write silently corrupts every connected client. (mcp-wire-discipline.yaml.) - Schema migrations are forward-only. New migration → append a
MIGRATION_NNNconstant incrates/flowd-storage/src/migrations.rsand add it to theMIGRATIONSslice. 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 viatracing::warn!/error!and returnOk(()). Do not propagate errors out ofrun/dispatch— a surfaced error hangs the IDE that invoked the hook. The one sanctioned non-zero exit is aPreToolUsedetector DENY (std::process::exit(DENY_EXIT_CODE), exit2); internal errors still log and exit0. Persistence guarantees live in MCP (memory_store), not hooks. (hook-error-swallowing.yaml.) cargo buildis off the agent loop. Usecargo check(workspace or-p <crate>) for feedback. Prefercargo nextest run -p <crate>overcargo test. (cargo-discipline.yaml.)- Crate version bumps ship in a separate PR. Feature/fix/refactor/docs commits must not edit
crates/*/Cargo.tomlversion lines. Bumps followchore(<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. Eachplan_integratetip commit becomes part of base history on promotion, so messages must read as coherent base-branch changes. Break-flag (!orBREAKING 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.)
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.