A guided tour for navigating, tracing, debugging, and testing the codebase. Read architecture.md first for the why; this document is the where and how.
Audience & purpose: contributors navigating, debugging, or testing the codebase — a companion to development.md, the developer manual. If you're looking for what the extension's tools do rather than how they're implemented, see user-guide.md instead.
New to Rust? Read the Rust tutorial alongside this — it teaches Rust using this repository's own code.
Small, dependency-light, and the vocabulary for everything else.
| File | What lives there |
|---|---|
src/lib.rs |
Re-exports everything; the crate's public API is this one page |
src/ids.rs |
uuid_id! macro generating the 9 id newtypes (ProjectId, RunId, PolicyViolationId, …) |
src/version.rs |
ProtocolVersion, VersionRange |
src/rpc.rs |
JSON-RPC envelopes, InitializeParams/Result, ClientAuth roles, RuntimeStatus, error_code constants (CrewMethod itself now lives in method.rs, re-exported here) |
src/method.rs |
CrewMethod — every JSON-RPC method name, foundation and orchestration alike |
src/event.rs |
EventEnvelope, RuntimeEvent, Timestamp, ContentClass/Classified<T>, RunFlags, DiagnosticLevel, EventSource, RuntimeEventKind |
src/task.rs |
TaskRef |
src/worker.rs |
WorkerProfileRef, Worker |
src/run.rs |
Run, RunSpec, RunState (+ can_transition_to/is_terminal) |
src/message.rs |
RunMessage, MessageKind, DeliveryState |
src/approval.rs |
ApprovalRequest, ApprovalDecision |
src/coordination.rs |
worker-safe request/result types, COORDINATION_PAYLOAD_MAX_BYTES, COORDINATION_RATE_LIMIT_PER_MINUTE, CoordinationAskPolicyParams, CoordinationChildDecision, CoordinationPeersParams, CoordinationPublishArtifactParams, CoordinationReportBlockedParams, CoordinationRequestChildParams, CoordinationSendParams, CoordinationTaskParams |
src/workspace.rs |
LeaseRequest, LeaseMode, ReleaseRequest, ApplyRequest, ApplyStrategy, ApplyResult, InspectRequest, InspectResult, IsolationKind, WorkspaceEvent, WorkspaceInfo, WorkspaceLease, WorkspaceState |
src/display.rs |
DisplayBackend, DisplayConfig, DisplayPlacement, DisplayStatus |
src/artifact.rs |
Artifact, ArtifactFetchResult, ArtifactFetchRequest, ArtifactKind, ArtifactListRequest, ArtifactListResult |
tests/wire_contract.rs |
Proves camelCase + deny_unknown_fields on the wire |
tests/domain_contract.rs |
RunState lifecycle table, RunFlags field names, CrewMethod orchestration variants |
tests/coordination_contract.rs |
Message kinds, delivery states, coordination request/result wire shapes |
tests/fixtures.rs |
Deserializes the golden fixtures through the real types |
| File | What lives there |
|---|---|
src/main.rs |
Thin entry point; calls cli::run() |
src/cli.rs |
clap definitions for serve/status/stop/version/schema/monitor/audit/doctor/config/coordination-mcp/display/conformance/adapters/lease/attach; maps outcomes to exit codes (73 = lost the singleton race) |
src/lifecycle.rs |
serve()/status()/stop(): flock singleton, lock metadata, idle shutdown, graceful-stop ordering, log routing, doctor integration |
src/doctor.rs |
Doctor — health checking: adapter availability, configuration validity, and more |
src/recovery.rs |
RecoveryCoordinator — recovers every non-terminal run at startup (ownership, not age); recover_paused/recover_waiting gates; DEFAULT_STALE_RUN_THRESHOLD for the doctor's read-only stale_runs report |
src/paths.rs |
RuntimePaths::resolve, VCS-root discovery, repository_id_from_canonical_root |
src/security/mod.rs |
StateRoot::resolve precedence, ensure_private_dir/ensure_private_file (0700/0600, atomic) |
src/security/rules.rs |
Built-in redaction regex patterns (AWS keys, API keys, GitHub tokens) |
src/security/redaction.rs |
Redactor, RawRuntimeEvent, PersistableEvent, SanitizedJson — the redaction boundary |
src/db/actor.rs |
DatabaseHandle + the actor thread owning the SQLite connection |
src/db/migrations.rs |
PRAGMAs, migration 1 (events, operations), migration 2 (worker_profiles, tasks, workers, runs, messages, approvals) |
src/db/models.rs |
Row types (ReplayedEvent, OperationIntent, Diagnostics) |
src/ipc/mod.rs |
ServerConfig, ClientPrincipal + role method tables, PeerCredentials reader trait, WorkerCredentialVerifier trait, IpcError |
src/ipc/server.rs |
Socket bind (owner-only, SUN_LEN guard), accept loop, UID admission, idle bookkeeping, constructs OrchestrationService/CoordinationBroker and the one events_tx broadcast channel they share |
src/ipc/connection.rs |
Per-connection reader/writer split, initialize handshake, method dispatch (routes orchestration methods to OrchestrationService, coordination/* to CoordinationBroker), replay/subscribe |
src/domain/repository.rs |
DomainRepository — every projection-mutating command; append_and_apply (event + projection, one transaction); Committed, embed_envelope/take_envelope |
src/domain/transitions.rs |
check_transition, TransitionError::Illegal — the canonical RunState lifecycle relation |
src/service/orchestration.rs |
OrchestrationService — routes every Task/Worker/Run/Message/Approval/Reconcile method to DomainRepository or service/query.rs |
src/service/query.rs |
Read-only lookup closures (task_get_op, run_state_op, etc.) run through DatabaseHandle::run_domain_op |
src/service/run_driver.rs |
RunDriver trait, RunDriverContext, FakeRunDriver (queued -> starting -> working) |
src/adapter/trait.rs |
Adapter trait with start/resume/send/cancel/dispose |
src/adapter/registry.rs |
AdapterRegistry — implements RunDriver against five worker adapters: four pseudo-terminal vendor adapters and one that drives claude over its streaming-JSON protocol (experimental, under evaluation, not yet recommended for use). The earlier headless control plane was retired and the headless mode name stays deserializable but typed-rejected; the protocol adapter is a fresh implementation against the vendor's current protocol, not that code returning (crew-v2 gap-closure, docs/adr/0026-headless-retirement.md). AdapterAuthorization trait, FixtureAuthorization/DenyByDefaultAuthorization |
src/adapter/event_sink.rs |
DomainAdapterEventSink — sanitizes, journals, and broadcasts adapter events |
src/adapter/run_lifecycle.rs |
RunLifecycleSink — applies queued -> starting -> working, and the terminal edge from adapter evidence together with whether the run's turn had settled: a clean exit with no settled turn is failed, not success |
src/adapter/error.rs |
AdapterError — adapter-specific error types |
src/adapter/capability.rs |
AdapterCapabilities — capability declarations for each adapter |
src/adapter/mcp_config.rs |
MCP configuration generation for adapter processes |
src/adapter/profile.rs |
WorkerProfile — worker profile definitions |
src/adapter/profile_store.rs |
ProfileStore — adapter profile persistence |
src/adapter/terminal.rs |
Terminal adapter backend |
src/adapter/tui/adapter.rs |
TuiAdapter<V: TuiVendor> — the shared PTY-driven adapter generic over all four vendors |
src/adapter/tui/claude.rs |
ClaudeTuiVendor — drives the real interactive claude CLI |
src/adapter/tui/codex.rs |
CodexTuiVendor — drives the real interactive codex CLI |
src/adapter/tui/copilot.rs |
CopilotTuiVendor — drives the real interactive copilot CLI |
src/adapter/tui/copilot_compatibility.rs |
Copilot CLI/ACP version compatibility checks (moved from the retired headless copilot adapter, see docs/adr/0026-headless-retirement.md) |
src/adapter/tui/omp.rs |
OmpTuiVendor — drives the real interactive omp CLI |
src/adapter/tui/discovery.rs |
Vendor-pane discovery/attach helpers |
src/adapter/tui/tailer.rs |
Session-transcript tailing shared across vendors |
src/adapter/claude_protocol/adapter.rs |
ClaudeProtocolAdapter — drives claude over streaming JSON on pipes; spawn, turn, settle, teardown |
src/adapter/claude_protocol/launch.rs |
The argv, with each permission-relevant flag passed explicitly |
src/adapter/claude_protocol/posture.rs |
Asserts at session start that the vendor reports the posture crew passed |
src/adapter/claude_protocol/reader.rs |
Stream classification into adapter events; the denial reconciliation at end of turn |
src/adapter/claude_protocol/approval_bridge.rs |
Bridges a vendor permission request into ApprovalService — the first adapter that calls into it |
src/adapter/claude_protocol/reconcile.rs |
Compares the run's journal against the vendor's own transcript, read as a file; detects gaps, does not repair them |
src/adapter/claude_protocol/trust.rs |
Reads the vendor's workspace-trust record before spawn; fails closed, never writes it |
src/adapter/claude_protocol/pane.rs |
The crew-rendered pane (a view, and not independent evidence) |
src/adapter/tui/{claude,codex,copilot,omp}_conformance.rs |
Per-vendor fixture/live conformance scenario probes |
src/coordination/broker.rs |
CoordinationBroker — record-before-delivery messaging, sweep_unacknowledged_as_unknown |
src/coordination/scope_token.rs |
ScopeTokenStore (mint/verify), PidAncestryChecker |
src/coordination/rate_limit.rs |
RateLimiter — single 30-calls/minute/sender sliding window, charged by every journaling coordination call (coordination/send, coordination/requestChild, coordination/publishArtifact) |
src/coordination/mcp.rs |
MCP tool registry proxy |
src/coordination/mcp_protocol.rs |
MCP protocol types |
src/approval/service.rs |
ApprovalService — request/decide, ownership/idempotency/settled-run enforcement, ApprovalCallback seam |
src/supervisor/process.rs |
Process-group scoped spawn with bounded stdio and escalation |
src/supervisor/environment.rs |
Redacted environment snapshots |
src/supervisor/output.rs |
Rotating stdout/stderr capture |
src/workspace/lease.rs |
Lease arbitration |
src/workspace/materialize.rs |
Materializes workspace changes |
src/workspace/apply.rs |
Applies workspace changes |
src/workspace/inspect.rs |
Inspects workspace state |
src/workspace/copy.rs |
Workspace file copying utilities |
src/workspace/artifact_store.rs |
Stores build artifacts |
src/workspace/git.rs |
Git integration for workspace operations |
src/display/terminal.rs |
Terminal display backend |
src/display/herdr.rs |
Herdr display backend |
src/display/tmux.rs |
Tmux display backend |
src/canonical_json.rs |
Canonical, recursively key-sorted JSON bytes for hashing and durable storage |
src/config/mod.rs |
RuntimePolicy adapter and resolve_policy() layer merging |
src/config/crew.rs |
CrewConfig loader with strict unknown-key rejection |
src/policy/evaluate.rs |
RuntimePolicy with SHA-256 fingerprint, RolloutGates |
src/audit/export.rs |
JSONL export |
src/audit/retention.rs |
Event retention and pruning |
src/conformance/scenario.rs |
Adapter conformance test scenarios |
src/conformance/report.rs |
Conformance test reporting |
Integration tests in crates/runtime/tests/ are the daemon's behavioural spec — one file per |
|
subsystem (paths, database, redaction, redaction_boundary, ipc, lifecycle, |
|
domain_repository, orchestration_rpc, coordination, coordination_mcp, approval, |
|
adapter_contract, adapter_registry, supervisor, supervisor_pty, workspace_apply, |
|
workspace_lease, workspace_materialize, display_registry, herdr_display, tmux_display, |
|
terminal_adapter, monitor_cli, audit, config, config_cli, config_artifacts, |
|
crew_config, conformance, attach, dashboard, escalations, lease_cli, lease_db, |
|
recovery, run_result, run_lifecycle, vendor_cli_availability, kill_switch_authorization, |
|
claude_protocol_reconcile, and the several race-condition-named files, e.g. |
|
approval_decide_race, task_revision_race). Four of the five worker adapters are TUI-only |
|
(crew-v2 gap-closure; docs/adr/0026-headless-retirement.md) — their own real-process coverage |
|
lives in tui_adapter.rs (the shared TuiAdapter<V> machinery against a scripted mock vendor), |
|
tui_claude_registry.rs (a real, TUI-mode Claude run through the registry), tui_tailer.rs, and |
|
claude_tui_fixture.rs (the committed claude-tui fixture); run_lifecycle.rs and |
|
orchestration_rpc.rs additionally drive a real spawned OS process through |
|
tests/support/spawn_evidence_adapter.rs — a small, protocol-agnostic, test-only Adapter (not a |
|
| shipped adapter kind) built to keep that specific "a real process, not a test-fake" property | |
| provable once the headless control plane's adapters (which those two files used to borrow for | |
| exactly this) were deleted. The fifth adapter, the claude protocol path (experimental, under | |
evaluation, not yet recommended for use), has its own suite in claude_protocol_reconcile.rs, |
|
which links this crate as an ordinary dependency rather than compiling with cfg(test) — proving |
|
| a property of the shipping binary itself, not only of its own test build. The lifecycle tests run | |
the real compiled binary (env!("CARGO_BIN_EXE_crewd")) as real processes. |
One file (src/main.rs): generate [--check] (schema + ts-rs bindings, deterministic,
temp-dir byte-compare in check mode) and package --target <triple> --binary <path> (installs a
binary into a leaf package with a deterministic manifest).
| File | What lives there |
|---|---|
src/index.ts |
Default-export extension factory; registers crew_health, crew_doctor, crew_config, /crew (monitor with subcommands health, run, runs, export, clean, reopen, doctor, config) and /crew-install, the 11 orchestration tools (via tools/index.ts), OMP-native lifecycle listeners (omp-native/), and the embedded monitor (monitor/controller.ts) |
src/status.ts |
getRuntimeStatus(ctx) and resolveClient(ctx) — the shared status path and liveness-aware client resolver; reuses the cached connection while open, reconnects on demand |
src/doctor.ts |
crew_doctor tool / /crew doctor command — shells out to crewd doctor --json, no live connection required |
src/client.ts |
CrewClient — NDJSON framing, byte-exact caps, request correlation, Ajv validation, and isClosed liveness flag for cache invalidation |
src/runtime.ts |
ensureRuntime (connect-or-spawn, authenticates as ompExtension), buildServeArgs, resolveOverride (OMP_CREW_BINARY validation), repositoryIdFromRoot |
src/state.ts |
resolveStateRoot(env, home) — must stay semantically identical to Rust's StateRoot::resolve |
src/platform.ts |
resolveCrewd tuple mapping, integrity/version checks, typed errors, detectLibc |
src/integrity.ts |
sha256File |
src/approval-ui.ts |
Approval UI components |
src/tools/index.ts |
Registers all 11 tools with OMP |
src/tools/shared.ts |
callOrchestration — the one execute body every orchestration tool uses; maps JsonRpcRemoteError to a stable tool error |
src/tools/{profiles,workers,tasks,runs,workspaces,artifacts,children,violations,messages,approvals,reconcile}.ts |
crew_profile, crew_worker, crew_task, crew_run, crew_workspace, crew_artifact, crew_child, crew_violation, crew_message, crew_approval, crew_reconcile — see user-guide.md for what each does |
src/omp-native/events.ts |
Normalizes task:subagent:lifecycle|progress|event bus payloads into OmpNativeAgentFact |
src/omp-native/reconcile.ts |
OmpNativeReconciler (150 ms progress coalescing, terminal-immediate), reconcileAcrossRestart (undetected parent-scoped runs become lost), createOmpProcessEpoch, reconcileWithRuntime |
src/monitor/model.ts |
reduceEvent — the pure event-reducer building MonitorState |
src/monitor/render.ts |
Turns MonitorState into the widget's concise lines + per-run status detail |
src/monitor/controller.ts |
registerMonitor — replay-first session_start wiring, /crew [run <runId>], retry-on-reconnect |
src/monitor/compat.ts |
Test-only assertCompatiblePiCodingAgentVersion (never called at runtime — see engineering-lessons.md) |
Each module has a sibling *.test.ts. client.test.ts and index.test.ts spawn the real daemon.
src/generated/*.ts and schema/crew.schema.json are build outputs — regenerate, never edit.
src/validate.ts is hand-written: it compiles Ajv validators once (validateInitializeResult,
validateRuntimeStatus, validateEventEnvelope, the JSON-RPC envelope validators) and exports
assertValid + ValidationError.
If Rust and TypeScript must agree on something, a fixture pins it: protocol frames
(fixtures/protocol/), state-root precedence (fixtures/state/), repository-id hashing
(fixtures/repo-id/), and the status result shape (fixtures/omp/). Both language test suites
consume them, so unilateral drift fails tests.
Follow this once with the files open and you will have seen every layer.
- Registration — OMP loads the extension and calls the default export
(
index.ts:crewExtension), which registers the tool and command; both handlers callgetRuntimeStatus(ctx)with the context fromcontext.ts:buildStatusContext. - Client acquisition —
status.ts:resolveClientchecks the cachedCrewClient'sisClosedflag. If the socket is still open, it returns the cached instance immediately. If the client is closed (daemon idle-exited or socket errored), it tears down the stale reference and callsruntime.ts:ensureRuntimeto reconnect. - Connect-or-spawn —
ensureRuntimecomputes the socket path (resolveStateRoot+repositoryId) and tries to connect. If nothing answers:selectBinaryvalidatesOMP_CREW_BINARY(or asksplatform.ts:resolveCrewdfor a packaged binary), spawnscrewd serve --state-dir … --repo … --idle-seconds …detached, and retries with backoff (≤5 s). - Daemon startup —
cli.rsparses args →lifecycle.rs:serveresolvesRuntimePaths, takes the flock (loser exits 73), opensDatabaseHandle(migrations + PRAGMAs), appends a redactedruntimeStartedevent through theRedactor, binds the owner-only socket (ipc/server.rs:bind), starts logging (runtime.logwhen detached). - Handshake — the client sends
initialize(first frame, 4 MiB bootstrap cap). The server already checked the peer UID at accept time.connection.rsvalidates the version range, canonicalizes the ompExtension agent directory, negotiatesmaxFrameBytes, computesnextSequenceviamax_sequence(), and returnsInitializeResultwith the role's allowed methods. The client Ajv-validates the result. - The call —
client.request("runtime/status", …)→ dispatch checks the role table →RuntimeStatuscomes back → Ajv validates →status.tsformatscontenttext and returns the validated object asdetails. On any failure,failureResultreturns{ isError, code, message (generic), doctorCommand }— no paths, no stack traces.
The event path is the same shape on the write side:
RawRuntimeEvent → Redactor::sanitize → PersistableEvent → DatabaseHandle::append_event
(commit, then reply) → events/event notification to subscribers / events/replay for
reconnecting clients.
Same idea as §2, but through the orchestration surface — follow it once and you've seen how a
mutation, the durable journal, and the embedded monitor connect. This trace is the symbol-level
companion to the design-level sequence in
architecture.md § Data Flow: Event Lifecycle.
- The tool call — the model calls
crew_runwith{ op: "submit", taskId, workerId }(tools/runs.ts);executecallsctx.getClient(cwd)(the same cachedompExtensionclient every orchestration tool and the monitor share — seeengineering-lessons.mdfor why its role matters) andcallOrchestration(client, "run/submit", params)(tools/shared.ts) — nothing more; no worker selection, no retry, no lifecycle inference here. - Dispatch —
connection.rs::dispatchseesCrewMethod::RunSubmitis one of the orchestration methods, forwards the raw params toOrchestrationService::dispatch(service/orchestration.rs), which the role table (Appendix A's "Role Table Summary" inarchitecture.md) already confirmed this connection'sompExtensionprincipal may call. - The mutation —
run_submitbuilds aRun { state: queued, ... }and callsDomainRepository::submit_runinside arun_domain_opclosure.append_and_apply(domain/repository.rs) inserts the event row, learns itssequencefrom the rowid, rewritesevent_jsonwith the bareRuntimeEvent::RunEvent { kind: RunQueued, ... }, inserts therunsprojection row, and commits — one transaction, both writes or neither. - The broadcast — back in
run_submit,embed_envelope/take_envelopecarry the returnedCommitted.envelopeacross therun_domain_opboundary, andself.broadcast(&mut result)sends it onShared.events_txbefore the JSON-RPC response is built. Any connection currently inspawn_subscriptionreceives it as anevents/eventnotification in the same tick — this is the fix for the bug described inengineering-lessons.md. - The adapter seam —
run_submitthen calls the injectedRunDriver(theAdapterRegistryby default): it resolves the worker profile, checksAdapterAuthorization(deny-by-default in production, configurable in tests), constructs the matchingTuiAdapter<V>(Vone of the four vendors — Claude/Codex/Copilot/OMP-RPC, all TUI-driven since crew-v2 gap-closure), and spawns the vendor CLI on a real PTY viaSupervisor/PtyProcess. With no driver, it returnsadapter_unavailableafter the queued run already committed in step 3 — the run is never silently dropped just because nothing can start it yet. With the registry wired (production,run_lifecycle.rs:261-273), the run now advancesqueued -> starting -> workingas the adapter journals evidence, and terminalizes on process exit. - The monitor observes it —
monitor/controller.ts'sclient.subscribecallback (already running fromsession_start) receives the notification from step 4,model.ts::reduceEventbuilds/updates the run's row from theRunEventpayload, andrefresh()callsctx.ui.setWidget— the row appears in/crewwithout the extension ever polling or reconnecting.
See what the daemon thinks is happening.
crewd status --wait-seconds 5 --state-dir <root> --repo <repo> # JSON snapshot
tail -f <root>/repos/<repo-id>/runtime.log | jq . # structured log (detached mode)Find <repo-id>: it's the only directory under <root>/repos/ for that repo, or compute it —
first 32 hex chars of sha256 of the canonical VCS root path.
Run the daemon in the foreground while iterating. --foreground puts the same structured
records on stderr and keeps the process attached to your terminal:
RUST_LOG=debug ./target/debug/crewd serve --foreground --state-dir /tmp/bs --repo "$PWD"(tracing-subscriber's env-filter is compiled in; RUST_LOG controls verbosity.)
Inspect the journal. It's plain SQLite:
sqlite3 <root>/repos/<repo-id>/runtime.db \
'SELECT sequence, timestamp, event_json FROM events ORDER BY sequence;'
sqlite3 <root>/repos/<repo-id>/runtime.db \
'SELECT operation_id, kind, acknowledged_at FROM operations;'
sqlite3 <root>/repos/<repo-id>/runtime.db \
'SELECT run_id, state, flags_protocol_unhealthy FROM runs;' # orchestration projectionsIf you ever see a raw secret in there, that is a P0 — the redaction boundary exists to make it impossible.
Talk raw JSON-RPC to the socket. Useful for protocol debugging without the TS client:
printf '%s\n' '{"jsonrpc":"2.0","id":"1","method":"initialize","params":{...}}' \
| nc -U <root>/repos/<repo-id>/runtime.sock(Steal a valid params object from fixtures/protocol/initialize.request.json and fix
repository/agentDirectory for your machine.)
Decode common exits and errors.
| Signal | Meaning |
|---|---|
exit 73 + already_running JSON |
Lost the singleton flock race — a daemon already serves this repo |
NOT_INITIALIZED (-32001) |
You sent a method before initialize |
INCOMPATIBLE_VERSION (-32002) |
Version ranges don't overlap protocol 1.0 |
METHOD_NOT_FOUND for a method you know exists |
Your role's method table hides it — check ClientPrincipal::allowed_methods; if it's ompExtension-only, also check you didn't authenticate as display (see engineering-lessons.md) |
ILLEGAL_TRANSITION (-32100) |
The requested RunState edge isn't in the canonical lifecycle relation (domain/transitions.rs) — check RunState::can_transition_to |
adapter_unavailable from run/submit |
Expected without a wired RunDriver (none by default this milestone) — the run is still queued, check run/list/run/get |
RATE_LIMITED from coordination/send, coordination/requestChild, or coordination/publishArtifact |
More than 30 journaling calls/minute from one sender — the three methods share one 30-calls-per-minute budget per sender (coordination/rate_limit.rs) |
| Connection dropped with no JSON error | Peer-UID mismatch (dropped before parsing) or an over-cap frame |
ValidationError in the TS client |
The daemon sent a frame the schema rejects — regenerate bindings or find the drift |
Orphan hunting. Tests and smoke runs are disciplined about cleanup, but if something leaks:
pgrep -fl crewd, then crewd stop (preferred) or kill <pid>. The kernel releases the flock
on death, so the next start recovers automatically.
Run conformance tests. To verify adapter implementations match their protocol specs:
# Run all conformance tests (crewd CLI, black-box)
cargo test --test conformance
# The claude-tui committed fixture directly
cargo test --test claude_tui_fixture
# Per-vendor fixture/live scenario probes live as lib-side unit tests, not
# a standalone per-adapter test binary (fixture mode is TUI-sourced now):
cargo test -p crew-runtime --lib adapter::tui::claude_conformance
cargo test -p crew-runtime --lib adapter::tui::codex_conformance
cargo test -p crew-runtime --lib adapter::tui::copilot_conformance
cargo test -p crew-runtime --lib adapter::tui::omp_conformanceExport audit events. For offline analysis:
crewd audit export --state-dir <root> --repo <repo> --output /tmp/audit.jsonlPhilosophy: integration tests exercise real things — real processes, real sockets, real SQLite files, byte-scans of real WAL files. Mocks appear only at injection seams that exist for the purpose (peer-credential reader, worker-credential verifier, packaged-binary resolver, uid provider).
Where to add a test:
| You changed… | Put the test in… |
|---|---|
| A wire type / serde shape | crates/protocol/tests/wire_contract.rs (+ regenerate, + fixture if cross-language) |
Domain record shape, RunState lifecycle edges, RunFlags |
crates/protocol/tests/domain_contract.rs |
| Coordination message kinds, delivery states, request/result shapes | crates/protocol/tests/coordination_contract.rs |
| Path/identity/permission logic | crates/runtime/tests/paths.rs (+ fixtures/repo-id or fixtures/state + the mirrored TS test) |
| DB actor commands or migrations | crates/runtime/tests/database.rs |
| Anything touching what gets persisted | crates/runtime/tests/redaction_boundary.rs — extend the byte-scan |
| Foundation protocol methods, negotiation, roles | crates/runtime/tests/ipc.rs |
| Locking, shutdown, idle, CLI | crates/runtime/tests/lifecycle.rs (real-process tests; keep timers ~1 s) |
DomainRepository transactions, projection rollback, event rebuild |
crates/runtime/tests/domain_repository.rs |
task/worker/run/message/approval/reconcile RPC methods |
crates/runtime/tests/orchestration_rpc.rs — remember the broadcast half (see engineering-lessons.md and §3 above) |
| Coordination broker behavior (bounds, rate limits, scope tokens) | crates/runtime/tests/coordination.rs |
| Approval ownership, idempotency, callback, recovery | crates/runtime/tests/approval.rs |
| Adapter contract and registry | crates/runtime/tests/adapter_contract.rs, adapter_registry.rs |
| Claude/Codex/Copilot/OMP-RPC adapters (TUI-only) | crates/runtime/tests/tui_adapter.rs (shared TuiAdapter<V> machinery), tui_claude_registry.rs, tui_tailer.rs, claude_tui_fixture.rs; per-vendor fixture/live scenario probes are lib-side unit tests under crates/runtime/src/adapter/tui/{claude,codex,copilot,omp}_conformance.rs |
| Supervisor (process management) | crates/runtime/tests/supervisor.rs |
| Workspace operations (lease, apply, materialize) | crates/runtime/tests/{workspace_lease,workspace_apply,workspace_materialize}.rs |
| Display backends (terminal, herdr, tmux) | crates/runtime/tests/terminal_adapter.rs, herdr_display.rs, tmux_display.rs, display_registry.rs |
| Configuration loading and merging (crew.json) | crates/runtime/tests/crew_config.rs |
| Conformance test scenarios | crates/runtime/tests/conformance.rs |
| Coordination MCP proxy | crates/runtime/tests/coordination_mcp.rs |
| Redaction rules | crates/runtime/tests/redaction.rs |
| Audit export/retention | crates/runtime/tests/audit.rs |
| Monitor CLI | crates/runtime/tests/monitor_cli.rs |
| TS client/launcher/extension logic | Sibling *.test.ts in packages/extension/src/ |
| Orchestration tool registration/schema/dispatch | packages/extension/src/tools/tools.test.ts |
OMP-native event mapping, coalescing, restart/lost |
packages/extension/src/omp-native/reconcile.test.ts |
| Monitor event-reducer or rendering | packages/extension/src/monitor/model.test.ts / render.test.ts |
Conventions that reviews enforce: test the real serialized JSON shape (not just round-trips); no sleeps papering over races (event-driven waits with a deadline); every spawned process is reaped even on assertion failure; test output stays pristine (a stray warning is a finding); follow TDD — the suite's failure message before implementation is part of the evidence.
Fast loops:
cargo test -p crew-runtime --test ipc -- --nocapture some_test_name # one Rust test, with output
bun test packages/extension/src/client.test.ts -t "frame" # TS tests matching a namecrates/protocol/bindings/fills with.tsfiles when you runcargo test(ts-rs side effect). It is gitignored scratch; the real bindings arepackages/protocol-ts/src/generated/.- Type-checking is a real gate:
bun run typecheck(tsc --noEmitagainst the roottsconfig.json) runs locally, insidebun run check, and as its own CI job.skipLibCheckis on solely for three unfixable third-party.d.tserrors (pi-catalog'smodels.jsonimport, pi-coding-agent'swrapper.d.tsvariance, pi-mnemopi's optionalfastembedpeer); first-party errors are never silenced. crewd schemaprints the schema embedded at compile time (include_str!). After changing protocol types,bun run generateand rebuild the binary, or the printed schema lags the types.generate --checkin CI catches the committed-file half of this.- Unix socket paths are capped (~104 bytes on macOS). Deep
--state-dirpaths fail fast with an explicit error — use/tmp/...in tests. - Lock files are never deleted; ownership is the flock, not file existence. Don't "clean up"
runtime.lockin scripts — deleting it while a daemon runs is harmless to the daemon but makes the metadata unreadable tostatus/stop. - Never resolve a peer package's own metadata (
import ... "@pkg/name/package.json" with { type: "json" }, or equivalent) at extension-load time or module scope. It resolves fine underbun test/bun runin this repo but hangs the realompbinary loading the extension (its own bundled module graph, different resolution entirely), and can crash a multi-filebun testrun with an unrelated Bun resolver defect. If you need a peer's installed version, read itspackage.jsonwith a plainfswalk (seemonitor/compat.ts). Full story:engineering-lessons.md. - A cached client shared by multiple callers needs the union of every role they need, not
whichever role the first caller happened to need.
ensureRuntime's client is shared by every orchestration tool and the monitor; it authenticates asompExtensionfor exactly this reason. Full story:engineering-lessons.md. - A
DomainRepositorymutation that doesn't broadcast itsCommitted.envelopebreaks the monitor silently — no error, no test failure, just a widget that never updates for that one mutation. See §3 above before adding one. Full story:engineering-lessons.md. - Recovery runs automatically after each
servecommand, before the daemon starts serving, and sweeps every run the journal still calls non-terminal — there is no age threshold on it, because nothing can be live at boot. If you're debugging why a stuck run transitions tofailed/cancelled,crates/runtime/tests/recovery.rsis the matrix;recover_paused/recover_waitingare the only knobs. There's no flag to trigger recovery on demand — usedoctor'sstale_runscheck (five-minute silence threshold, read-only) to see a wedged run without forcing a restart. - Rollout gates are retired. The old YAML config's six
advisory "rollout gate" booleans (and the
native_discovery_reviewedgate that actually blocked authorization) had no equivalent increw.json's schema and were config-sourced from a layer that was never actually wired up end to end — seefuture-features.mdfor the decision trigger if that surface returns.Doctor::check()no longer has arollout_gates_resolvedcheck. - Adapter authorization goes through
PolicyEvaluatorin production. Tests injectFixtureAuthorizationto allow/deny as needed.PolicyEvaluatoritself now only enforces the concurrency ceiling (limits.maxConcurrentWorkers) and a nested-worker pre-authorization check — the model/adapter allowlists and cost ceilings it used to also own are retired along with the rollout gates above.