|
| 1 | +# example-plan.toml — Kestrel: a lightweight webhook relay service |
| 2 | +# |
| 3 | +# This file is a reference example showing every supported field. |
| 4 | +# Start here, trim what you don't need, and fill in your own project details. |
| 5 | +# |
| 6 | +# Run: |
| 7 | +# wiggum validate example-plan.toml |
| 8 | +# wiggum generate example-plan.toml --dry-run |
| 9 | +# wiggum generate example-plan.toml |
| 10 | + |
| 11 | +# ─── Project ────────────────────────────────────────────────────────────────── |
| 12 | + |
| 13 | +[project] |
| 14 | +name = "kestrel" |
| 15 | +description = """ |
| 16 | +Kestrel is a lightweight HTTP webhook relay service written in Rust. |
| 17 | +It receives inbound webhooks, validates HMAC-SHA256 signatures, persists |
| 18 | +events to a SQLite queue, and fans them out to one or more configurable |
| 19 | +downstream targets with automatic exponential-backoff retry on failure. |
| 20 | +""" |
| 21 | +language = "rust" |
| 22 | +path = "/path/to/kestrel" # absolute path to the target project root |
| 23 | +architecture = "hexagonal" # hexagonal | layered | modular | flat |
| 24 | + |
| 25 | +# ─── Preflight ──────────────────────────────────────────────────────────────── |
| 26 | +# Commands run by every subagent before marking a task complete. |
| 27 | +# Language defaults are applied automatically; override them here if needed. |
| 28 | + |
| 29 | +[preflight] |
| 30 | +build = "cargo build --workspace" |
| 31 | +test = "cargo test --workspace" |
| 32 | +lint = "cargo clippy --workspace -- -D warnings" |
| 33 | + |
| 34 | +# ─── Orchestrator ───────────────────────────────────────────────────────────── |
| 35 | +# Controls the persona and rules baked into every generated subagent prompt. |
| 36 | + |
| 37 | +[orchestrator] |
| 38 | +persona = "You are a senior Rust engineer specialising in async HTTP services and hexagonal architecture." |
| 39 | +strategy = "standard" # standard | tdd | gsd |
| 40 | +rules = [ |
| 41 | + "Rust edition 2024, stable toolchain only — no nightly features.", |
| 42 | + "Keep the domain module free of I/O dependencies (no tokio, no reqwest, no sqlx).", |
| 43 | + "All error types use thiserror; no .unwrap() outside of tests.", |
| 44 | + "HMAC secrets must never appear in log output at any log level.", |
| 45 | + "Use sqlx with compile-time query macros; migrations live in migrations/.", |
| 46 | + "Every public function must have at least one unit or integration test.", |
| 47 | + "Use Coraline (coraline_* MCP tools) for code navigation when exploring the codebase.", |
| 48 | +] |
| 49 | + |
| 50 | +# ─── Phase 1: Workspace Scaffold ────────────────────────────────────────────── |
| 51 | + |
| 52 | +[[phases]] |
| 53 | +name = "Workspace Scaffold" |
| 54 | +order = 1 |
| 55 | + |
| 56 | +[[phases.tasks]] |
| 57 | +slug = "workspace-scaffold" |
| 58 | +title = "Cargo workspace, CI pipeline, and repo hygiene" |
| 59 | +goal = "Bootstrap the project with a compilable stub workspace, GitHub Actions CI, and linting config." |
| 60 | +depends_on = [] |
| 61 | +hints = [ |
| 62 | + "Single crate for now (src/lib.rs + src/main.rs); extract sub-crates only if clearly needed later.", |
| 63 | + "Add deny.toml (cargo-deny) for supply-chain checks.", |
| 64 | + "GitHub Actions: build + clippy + test on ubuntu-latest and macos-latest.", |
| 65 | + "Use Rust edition 2024 in Cargo.toml.", |
| 66 | +] |
| 67 | +test_hints = [ |
| 68 | + "CI must pass: green build + clippy with -D warnings + cargo test.", |
| 69 | +] |
| 70 | +must_haves = [ |
| 71 | + "Cargo.toml with edition = \"2024\"", |
| 72 | + ".github/workflows/ci.yml runs cargo build, clippy, and test", |
| 73 | + "deny.toml present", |
| 74 | +] |
| 75 | + |
| 76 | +# ─── Phase 2: Domain Model ──────────────────────────────────────────────────── |
| 77 | + |
| 78 | +[[phases]] |
| 79 | +name = "Domain Model" |
| 80 | +order = 2 |
| 81 | + |
| 82 | +[[phases.tasks]] |
| 83 | +slug = "domain-types" |
| 84 | +title = "Core entities, value objects, and port traits" |
| 85 | +goal = "Define the pure domain layer: WebhookEvent, DeliveryTarget, RetryPolicy, and the port traits with no I/O dependencies." |
| 86 | +depends_on = ["workspace-scaffold"] |
| 87 | +hints = [ |
| 88 | + "WebhookEvent: id (Uuid), source (String), payload (Bytes), received_at (DateTime<Utc>), signature (Option<String>).", |
| 89 | + "DeliveryTarget: id (Uuid), url (String), secret (Option<String>), retry_policy (RetryPolicy).", |
| 90 | + "RetryPolicy: max_attempts (u8), backoff_base_ms (u64) — exponential jitter.", |
| 91 | + "Port traits: EventStore (save/load), TargetStore (list), Dispatcher (dispatch).", |
| 92 | + "No tokio, no reqwest, no sqlx in the domain module.", |
| 93 | +] |
| 94 | +test_hints = [ |
| 95 | + "Unit-test RetryPolicy::next_delay(attempt) for exponential backoff correctness.", |
| 96 | + "Test that WebhookEvent rejects an empty source string.", |
| 97 | +] |
| 98 | +must_haves = [ |
| 99 | + "All domain types derive Serialize/Deserialize", |
| 100 | + "Port traits are object-safe", |
| 101 | + "Zero I/O dependencies in the domain module (enforced by a #![forbid(unsafe_code)] stub)", |
| 102 | +] |
| 103 | + |
| 104 | +[[phases.tasks]] |
| 105 | +slug = "signature-validation" |
| 106 | +title = "HMAC-SHA256 webhook signature validator" |
| 107 | +goal = "Implement constant-time HMAC-SHA256 signature validation for incoming webhook payloads." |
| 108 | +depends_on = ["domain-types"] |
| 109 | +hints = [ |
| 110 | + "Use the hmac + sha2 crates; match GitHub-style X-Hub-Signature-256 header format (sha256=<hex>).", |
| 111 | + "Use subtle::ConstantTimeEq for the comparison — never a plain == on secret material.", |
| 112 | + "Expose as a pure function: validate_signature(secret: &[u8], payload: &[u8], header: &str) -> bool.", |
| 113 | +] |
| 114 | +test_hints = [ |
| 115 | + "Test: valid signature passes.", |
| 116 | + "Test: tampered payload fails.", |
| 117 | + "Test: wrong secret fails.", |
| 118 | + "Test: missing header (empty string) returns false when a secret is configured.", |
| 119 | +] |
| 120 | +must_haves = [ |
| 121 | + "Constant-time comparison — no timing side-channel", |
| 122 | + "Function is pure (no I/O, no side effects)", |
| 123 | +] |
| 124 | + |
| 125 | +# ─── Phase 3: HTTP Inbound Layer ────────────────────────────────────────────── |
| 126 | + |
| 127 | +[[phases]] |
| 128 | +name = "HTTP Inbound Layer" |
| 129 | +order = 3 |
| 130 | + |
| 131 | +[[phases.tasks]] |
| 132 | +slug = "inbound-router" |
| 133 | +title = "Axum router — POST /webhook and GET /healthz" |
| 134 | +goal = "Stand up an Axum HTTP server that receives webhook POSTs, validates signatures, and hands events to the domain for persistence." |
| 135 | +depends_on = ["signature-validation"] |
| 136 | +hints = [ |
| 137 | + "Use axum 0.8.x with the tokio runtime.", |
| 138 | + "POST /webhook — extract the raw body before any JSON parsing so the HMAC covers the exact bytes received; call validate_signature; persist via EventStore port.", |
| 139 | + "GET /healthz — return 200 OK with {\"status\":\"ok\"} for load-balancer probes.", |
| 140 | + "Return 400 for invalid signature, 500 for store errors (structured JSON error body in both cases).", |
| 141 | + "App state holds Arc<dyn EventStore + Send + Sync> and the server config.", |
| 142 | +] |
| 143 | +test_hints = [ |
| 144 | + "Integration test: POST with a valid signature → 202 Accepted.", |
| 145 | + "Integration test: POST with an invalid signature → 400 Bad Request.", |
| 146 | + "Integration test: GET /healthz → 200 OK.", |
| 147 | +] |
| 148 | +must_haves = [ |
| 149 | + "Raw body read before JSON deserialization", |
| 150 | + "Secrets never logged", |
| 151 | + "Structured JSON error responses (not plain text)", |
| 152 | +] |
| 153 | + |
| 154 | +# ─── Phase 4: Outbound Dispatch ─────────────────────────────────────────────── |
| 155 | + |
| 156 | +[[phases]] |
| 157 | +name = "Outbound Dispatch" |
| 158 | +order = 4 |
| 159 | + |
| 160 | +[[phases.tasks]] |
| 161 | +slug = "http-dispatcher" |
| 162 | +title = "reqwest-based outbound dispatcher with retry" |
| 163 | +goal = "Implement the Dispatcher port using reqwest: POST events to downstream targets with exponential-backoff retry and delivery logging." |
| 164 | +depends_on = ["inbound-router"] |
| 165 | +hints = [ |
| 166 | + "Use reqwest with rustls-tls (no native-tls).", |
| 167 | + "Respect RetryPolicy from the target config; log attempt number and final outcome at debug level.", |
| 168 | + "On exhausted retries mark the delivery as failed in DeliveryLog.", |
| 169 | + "Sign outbound requests with the target's secret (same HMAC-SHA256 scheme) so downstream can verify.", |
| 170 | +] |
| 171 | +test_hints = [ |
| 172 | + "Use wiremock to simulate a failing downstream — assert retry count matches the target's RetryPolicy.", |
| 173 | + "Assert sign-then-verify round-trip: sign outbound payload, validate with the same secret.", |
| 174 | +] |
| 175 | +must_haves = [ |
| 176 | + "TLS via rustls, not native-tls", |
| 177 | + "Retry count and final outcome persisted to DeliveryLog", |
| 178 | + "Outbound requests signed with target secret", |
| 179 | +] |
| 180 | + |
| 181 | +[[phases.tasks]] |
| 182 | +slug = "sqlite-adapter" |
| 183 | +title = "SQLite EventStore and DeliveryLog adapter (sqlx)" |
| 184 | +goal = "Implement the EventStore port and a DeliveryLog adapter backed by SQLite using sqlx with compile-time checked queries." |
| 185 | +depends_on = ["http-dispatcher"] |
| 186 | +hints = [ |
| 187 | + "Migrations in migrations/ using sqlx-cli format; run automatically on startup.", |
| 188 | + "Tables: events (id, source, payload, received_at), deliveries (id, event_id, target_id, attempt, status, dispatched_at).", |
| 189 | + "Use sqlx::query_as! macros for compile-time checking. DATABASE_URL must be set (use .env for dev).", |
| 190 | + "Accept Arc<SqlitePool> through app state; expose via the EventStore trait impl.", |
| 191 | +] |
| 192 | +test_hints = [ |
| 193 | + "Use an in-memory SQLite DB (sqlite::memory:) in tests to avoid filesystem side-effects.", |
| 194 | + "Test save-then-load round-trip for WebhookEvent.", |
| 195 | + "Test that failed delivery status is recorded correctly.", |
| 196 | +] |
| 197 | +must_haves = [ |
| 198 | + "All SQL uses compile-time checked query macros", |
| 199 | + "Migrations applied automatically at startup", |
| 200 | + "In-memory test DB used in all adapter tests", |
| 201 | +] |
| 202 | + |
| 203 | +# ─── Phase 5: Config and CLI ────────────────────────────────────────────────── |
| 204 | + |
| 205 | +[[phases]] |
| 206 | +name = "Config and CLI" |
| 207 | +order = 5 |
| 208 | + |
| 209 | +[[phases.tasks]] |
| 210 | +slug = "config-parsing" |
| 211 | +title = "TOML config with environment-variable overrides" |
| 212 | +goal = "Define and parse the application configuration from kestrel.toml with KESTREL_-prefixed environment variable overrides." |
| 213 | +depends_on = ["workspace-scaffold"] |
| 214 | +hints = [ |
| 215 | + "Config sections: [server] (host, port), [database] (url), [[targets]] (array of DeliveryTarget configs).", |
| 216 | + "Use the figment crate for layered config (TOML file → env vars).", |
| 217 | + "Env var prefix: KESTREL_ (e.g. KESTREL_SERVER_PORT=8080 overrides server.port).", |
| 218 | + "Validate at load time: all target URLs must be https:// outside of dev/test mode.", |
| 219 | +] |
| 220 | +test_hints = [ |
| 221 | + "Test that KESTREL_SERVER_PORT env var overrides the file value.", |
| 222 | + "Test that a non-HTTPS target URL fails validation outside dev mode.", |
| 223 | + "Test that missing required fields produce clear error messages.", |
| 224 | +] |
| 225 | +must_haves = [ |
| 226 | + "Config validated at startup — process exits with a clear error message if invalid", |
| 227 | + "No secrets in the committed default config file", |
| 228 | +] |
| 229 | + |
| 230 | +[[phases.tasks]] |
| 231 | +slug = "cli-entrypoint" |
| 232 | +title = "clap CLI — serve, validate-config, version" |
| 233 | +goal = "Wire the application entry point with clap: subcommands for serve, validate-config, and version output." |
| 234 | +depends_on = ["config-parsing", "inbound-router", "sqlite-adapter"] |
| 235 | +hints = [ |
| 236 | + "Use the clap derive API.", |
| 237 | + "serve: load config, run migrations, bind the Axum server.", |
| 238 | + "validate-config [--config <path>]: parse and validate config then exit 0 or 1 with a clear error.", |
| 239 | + "version: print semver + git commit SHA (embed with env!(\"CARGO_PKG_VERSION\") and a build.rs SHA).", |
| 240 | + "Structured JSON logging via tracing + tracing-subscriber; default level INFO, override with RUST_LOG.", |
| 241 | +] |
| 242 | +test_hints = [ |
| 243 | + "CLI integration test: kestrel validate-config --config tests/fixtures/valid.toml → exit 0.", |
| 244 | + "CLI integration test: kestrel validate-config --config tests/fixtures/invalid.toml → exit 1 with message.", |
| 245 | +] |
| 246 | +must_haves = [ |
| 247 | + "All three subcommands present and functional", |
| 248 | + "Startup log line includes version and bound address", |
| 249 | + "RUST_LOG respected", |
| 250 | +] |
0 commit comments