Plan Conventions & Best Practices
Reference document for writing Nexus implementation plans. Covers mise task usage, integration test standards, and plan document structure.
1. mise Tasks Over Raw Cargo
Plans MUST use mise run <task> instead of raw cargo commands. This ensures all developers and CI run the same commands, and makes it possible to add pre/post hooks or change underlying tooling without updating every plan.
Current mise tasks
| Task | Description | Underlying command |
|---|---|---|
build | Build all crates (debug) | cargo build |
build:release | Build all crates (release) | cargo build --release |
test | Run all workspace tests | cargo test --workspace |
test:unit | Run unit tests only (no integration) | cargo test --workspace --lib |
test:lib | Run tests for nexus-lib | cargo test -p nexus-lib |
test:nexusd | Run tests for nexusd | cargo test -p nexusd |
test:nexusctl | Run tests for nexusctl | cargo test -p nexusctl |
run | Run nexusd (debug build) | cargo run -p nexusd -- |
run:nexusctl | Run nexusctl (debug build) | cargo run -p nexusctl -- |
check | Check all crates for errors | cargo check --workspace |
clippy | Run clippy lints | cargo clippy --workspace |
install:nexusd:debug | Install nexusd debug build + systemd unit | (copy + systemctl) |
install:nexusd:release | Install nexusd release build + systemd unit | (copy + systemctl) |
install:nexusctl:debug | Install nexusctl debug build | (copy) |
install:nexusctl:release | Install nexusctl release build | (copy) |
install:debug | Install all binaries (debug) | depends on above |
install:release | Install all binaries (release) | depends on above |
Command mapping for plan authors
| What you want to do | Wrong (raw cargo) | Right (mise) |
|---|---|---|
| Build everything | cargo build | mise run build |
| Build for release | cargo build --release | mise run build:release |
| Run all tests | cargo test --workspace | mise run test |
| Run nexus-lib tests | cargo test -p nexus-lib | mise run test:lib |
| Run nexusd tests | cargo test -p nexusd | mise run test:nexusd |
| Run nexusctl tests | cargo test -p nexusctl | mise run test:nexusctl |
| Run unit tests only | cargo test --workspace --lib | mise run test:unit |
| Check for errors | cargo check --workspace | mise run check |
| Run clippy | cargo clippy --workspace | mise run clippy |
| Start nexusd | cargo run -p nexusd -- | mise run run |
| Start nexusctl | cargo run -p nexusctl -- | mise run run:nexusctl |
Exception: targeted test runs
When running a specific test by name during TDD (e.g., verifying a single failing test), raw cargo is acceptable inline because there is no mise task for arbitrary test filters:
# Acceptable in TDD steps:
cargo test -p nexus-lib store::sqlite::tests::open_creates_new_database
If a plan repeatedly runs the same filtered test command, consider whether a new mise task would help. The threshold: if 3+ plans would use the same filter, add a task.
When to add new mise tasks
Add a new task to .mise.toml when:
- A plan needs a command that will be reused across multiple steps/plans
- The command involves multiple chained operations (build + copy + reload)
- The command has flags that are easy to get wrong
Do NOT add a task for:
- One-off manual verification steps
- Targeted test runs with specific test name filters
Proposed missing tasks
| Task | Description | When to add |
|---|---|---|
test:integration | Run only integration tests | cargo test --workspace --test '*' |
fmt | Format all code | cargo fmt --all |
fmt:check | Check formatting without modifying | cargo fmt --all -- --check |
2. Integration Test Standards
The problem today
Both nexusd/tests/daemon.rs and nexusctl/tests/cli.rs contain duplicated boilerplate:
- Daemon startup –
Command::new(binary).env(...).arg(...).spawn() - Readiness polling – loop 50 times, sleep 100ms, try HTTP GET
- Graceful shutdown –
signal::kill(Pid::from_raw(...), SIGTERM) - Port allocation – hardcoded ports (9600 for nexusd, 9601 for nexusctl)
- Temp DB path –
tempfile::tempdir().join("test.db") - Temp config – write YAML to temp file with custom listen address
This boilerplate will grow with every new integration test added in steps 4-10.
Proposed shared test harness
A TestDaemon helper that lives in nexus-lib as a test utility, gated behind a test-support feature flag so it doesn’t affect production builds.
Location: nexus-lib/src/test_support.rs (exported only when #[cfg(test)] or feature test-support is active)
Sketch:
#![allow(unused)]
fn main() {
// nexus-lib/src/test_support.rs
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::time::Duration;
use nix::sys::signal::{self, Signal};
use nix::unistd::Pid;
/// A running nexusd instance for integration tests.
/// Stops the daemon on drop.
pub struct TestDaemon {
child: Child,
pub addr: String,
pub port: u16,
pub db_path: PathBuf,
_tmp_dir: tempfile::TempDir,
_config_dir: tempfile::TempDir,
}
impl TestDaemon {
/// Start a daemon on a random available port with a temp database.
/// Polls until the health endpoint responds or timeout (5s).
pub async fn start() -> Self {
Self::start_with_binary(Self::find_nexusd_binary()).await
}
/// Start with an explicit binary path (for cross-package tests).
pub async fn start_with_binary(binary: PathBuf) -> Self {
let port = free_port();
let addr = format!("127.0.0.1:{port}");
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
let db_path = tmp_dir.path().join("test.db");
let config_dir = tempfile::tempdir().expect("failed to create config dir");
let config_path = config_dir.path().join("nexus.yaml");
let config_yaml = format!("api:\n listen: \"{addr}\"");
std::fs::write(&config_path, config_yaml)
.expect("failed to write test config");
let child = Command::new(&binary)
.env("RUST_LOG", "info")
.arg("--config")
.arg(&config_path)
.arg("--db")
.arg(&db_path)
.spawn()
.unwrap_or_else(|e| panic!("failed to start {}: {e}", binary.display()));
let daemon = TestDaemon {
child,
addr: addr.clone(),
port,
db_path,
_tmp_dir: tmp_dir,
_config_dir: config_dir,
};
daemon.wait_ready().await;
daemon
}
pub fn health_url(&self) -> String {
format!("http://{}/v1/health", self.addr)
}
async fn wait_ready(&self) {
let client = reqwest::Client::new();
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(100)).await;
if client.get(self.health_url()).send().await.is_ok() {
return;
}
}
panic!("daemon did not become ready within 5 seconds on {}", self.addr);
}
/// Find the nexusd binary relative to the test binary location.
fn find_nexusd_binary() -> PathBuf {
let mut path = std::env::current_exe().expect("cannot get test binary path");
path.pop(); // remove test-<hash>
path.pop(); // remove deps
path.join("nexusd")
}
}
impl Drop for TestDaemon {
fn drop(&mut self) {
let _ = signal::kill(
Pid::from_raw(self.child.id() as i32),
Signal::SIGTERM,
);
let _ = self.child.wait();
}
}
/// Find a free TCP port by binding to port 0 and reading the assigned port.
fn free_port() -> u16 {
let listener = TcpListener::bind("127.0.0.1:0")
.expect("failed to bind to port 0");
listener.local_addr().unwrap().port()
}
}
Port allocation strategy
Current approach (hardcoded ports) – fragile. Tests fail when ports collide or are in use.
Recommended approach (port 0) – bind a TcpListener to 127.0.0.1:0, read the assigned port, drop the listener, then pass the port to the daemon config. There is a small TOCTOU window where another process could grab the port between the listener drop and the daemon bind, but this is extremely unlikely in practice and far better than hardcoded ports.
This eliminates all port conflict issues when cargo test --workspace runs test binaries in parallel.
Temp DB and config handling
- Every integration test MUST use
tempfile::tempdir()for its database. Never use the real$XDG_STATE_HOMEpath. - Every integration test that needs a non-default listen address MUST write a temp config file with the test port.
- The
TestDaemonhelper handles both automatically. - Temp directories are cleaned up on drop via
tempfile::TempDir.
Where the harness lives
Option A (recommended): nexus-lib with a test-support feature
# nexus-lib/Cargo.toml
[features]
test-support = ["dep:tempfile", "dep:nix", "dep:reqwest"]
[dependencies]
tempfile = { version = "3", optional = true }
nix = { version = "0.30", features = ["signal"], optional = true }
reqwest = { version = "0.12", features = ["json"], optional = true }
Both nexusd and nexusctl add:
[dev-dependencies]
nexus-lib = { path = "../nexus-lib", features = ["test-support"] }
Why not a separate crate: A nexus-test-support crate adds workspace complexity for minimal benefit. The feature flag keeps test utilities out of production builds while avoiding a new crate.
Usage after adoption
#![allow(unused)]
fn main() {
// nexusd/tests/daemon.rs
use nexus_lib::test_support::TestDaemon;
#[tokio::test]
async fn daemon_starts_serves_health_and_stops() {
let daemon = TestDaemon::start().await;
let client = reqwest::Client::new();
let resp = client.get(daemon.health_url()).send().await.unwrap();
assert_eq!(resp.status(), 200);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["status"], "ok");
assert!(body["database"]["table_count"].is_number());
assert!(daemon.db_path.exists());
// daemon stops on drop
}
}
#![allow(unused)]
fn main() {
// nexusctl/tests/cli.rs
use nexus_lib::test_support::TestDaemon;
#[tokio::test]
async fn status_when_daemon_running() {
let daemon = TestDaemon::start().await;
let output = Command::new(env!("CARGO_BIN_EXE_nexusctl"))
.args(["--daemon", &daemon.addr, "status"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success());
assert!(stdout.contains("ok"));
// daemon stops on drop
}
}
Migration path
This harness does not need to exist before step 4. The current tests work. Introduce TestDaemon as part of step 4 (VM Records) when the third integration test file is added, then backfill the existing tests.
3. Plan Document Conventions
Standard structure
Every plan document MUST follow this structure. Sections marked (required) cannot be omitted.
# Step N: <Title> -- Implementation Plan (required)
> **For Claude:** REQUIRED SUB-SKILL: ... (required)
**Goal:** <one sentence> (required)
**Architecture:** <paragraph> (required)
**Tech Stack (additions to existing):** (required if adding deps)
- `crate_name` version -- purpose
**XDG Directory Layout:** (if new paths are introduced)
---
## Task 1: <Title> (required, repeat per task)
**Files:** (required)
- Create: `path/to/file`
- Modify: `path/to/file`
**Step 1: Write the failing test** (TDD tasks)
**Step 2: Run tests to verify they fail**
**Step 3: Implement**
**Step 4: Run tests to verify they pass**
**Step 5: Commit**
---
## Verification Checklist (required)
- [ ] `mise run build` succeeds
- [ ] `mise run test` passes
- [ ] ...
TDD flow conventions
For tasks that add new functionality with testable behavior:
- Write the failing test first. Include the full test code in the plan.
- Show the run command. Use mise tasks (see Section 1). For targeted test runs, raw cargo with a filter is acceptable.
- State the expected failure. E.g., “FAIL –
Configtype does not exist yet.” - Implement the code. Include the full implementation.
- Show the verification command. Same command as step 2.
- State the expected success. E.g., “All 4 tests PASS.”
- Commit.
For tasks where TDD doesn’t apply (systemd unit files, config wiring, mise task updates):
- Implement.
- Show verification. Manual smoke test or build command.
- State the expected result.
- Commit.
Commit message conventions
Format: <type>(<scope>): <description>
Types:
feat– new functionalityfix– bug fixtest– adding or updating tests (no production code change)chore– build system, tooling, mise tasks, CIrefactor– code restructuring with no behavior changedocs– documentation only
Scope: the crate name (nexusd, nexusctl, nexus-lib) or mise for task changes. Omit scope for cross-cutting changes.
Examples:
feat(nexus-lib): add Config type with YAML loading and defaults
feat(nexusd): add GET /v1/health endpoint
test(nexusd): add integration test for daemon lifecycle
chore(mise): add nexusctl to build and install tasks
feat(nexus-lib): add NexusClient HTTP client for daemon API
test: update integration tests for database initialization
Verification checklist conventions
Every plan MUST end with a verification checklist. Include:
- Build verification:
mise run buildsucceeds with no warnings - Test verification:
mise run test– all tests pass - Lint verification:
mise run clippy– no warnings (from step 3 onward) - Functional verification: manual smoke tests for each user-visible feature added
- Negative verification: behavior when things go wrong (missing config, daemon not running, etc.)
Use - [ ] checkboxes. The implementer checks these off as they complete the plan.
Updating the progress tracker
The codex has a progress dashboard at src/tracker/progress.md. Update it when a step changes status. Four edits are needed:
- Kanban board — move the step card to the correct column (Complete / In Progress / Pending).
- Pie chart — update the counts to match.
- Details table — update the Status column and add a plan link if a new plan was written.
- Header line — update the “N of 10 steps complete” count.
Example: moving step 4 from Pending to In Progress:
In Progress
- No active steps
+ Step 4: VM Records CRUD
Pending
- Step 4: VM Records CRUD
Step 5: btrfs Workspaces
Update the tracker at these points in the workflow:
- When starting a step — move from Pending to In Progress
- When completing a step — move from In Progress to Complete, add plan link to details table
- When abandoning a step — remove from the board, note in the Issues section
Rebuild the codex after changes: cd ~/Work/WorkFort/codex && mise run build
Feasibility assessments
Plans that introduce new dependencies, external APIs, or system-level interfaces go through a technical feasibility assessment before being committed. The assessment is a temporary document — a code review for the plan — that is deleted after its findings are incorporated. See Assessment Conventions for the full process.
Key rules:
- All MUST FIX items from the assessment MUST be incorporated into the plan before committing.
- The plan is committed as a single commit with all corrections already applied.
- Plans MUST NOT contain links or references to individual feasibility assessments.
Working directory
All commands in plans assume the working directory is ~/Work/WorkFort/nexus/ (the Rust workspace root). If a command needs a different directory, state it explicitly. Prefer absolute paths in cd commands:
# Good
cd ~/Work/WorkFort/nexus && mise run test
# Avoid
cd nexus && cargo test
File paths
Always use paths relative to the workspace root in the Files section:
**Files:**
- Create: `nexus/nexus-lib/src/store/schema.rs`
- Modify: `nexus/nexusd/src/main.rs`
In code blocks (Cargo.toml, Rust files), include a comment with the file path on the first line:
#![allow(unused)]
fn main() {
// nexus/nexusd/src/api.rs
use axum::{Json, Router, routing::get};
}
# nexus/nexus-lib/Cargo.toml
[package]
name = "nexus-lib"