Skip to content

Commit af1a5ae

Browse files
committed
chore: track example-plan.toml reference file
- Update .gitignore to include reference/example-plan.toml exception - Add example-plan.toml as reference for plan schema
1 parent fb03940 commit af1a5ae

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ tasks/
5050
# Development reference docs (not shipped)
5151
docs/dev/
5252
reference/
53+
!reference/example-plan.toml
5354

reference/example-plan.toml

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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

Comments
 (0)