modules/programs/agents/shared/skills/effect-sdd/SKILL.md
Service-Driven Development workflow for Effect-TS. Schema-first, type-driven, property-tested. USE THIS SKILL WHEN: Designing a new feature spanning 3+ services, sketching service boundaries before implementation, user says 'design', 'sketch', 'SDD', 'service-driven', planning multi-service composition. NOT FOR: single-service CRUD, implementing against an existing spec. TRIGGERS ON: 'design services', 'sketch services', 'SDD', 'service-driven', 'type-driven design', 'design the API first', 'audit services', 'bring to SDD standard', 'SDD audit'
npx skillsauth add MichaelVessia/nixos-config effect-sddInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Design services as pure type-level interfaces. Constrain them with Schema. Test them with properties. Compose them. Iterate until the API is right. THEN implement.
The orchestrator defines the perfect DSL. You write the top-level composition first — as if ideal services already exist. The interfaces emerge from what the orchestrator needs, not from independent design.
"Code the simple version first. Code it before you have implemented the sub-layers. Just imagine they exist. Code as if the perfect API to implement this layer already existed. Then, once your code looks beautiful, go implement those things." — Grant Slatton
Why top-down? Bottom-up locks you into a design before you've written the software. When you implement layer N, you guess what layer N+1 needs. When you get to N+1, you work around a not-quite-right API you're hesitant to change. Top-down avoids this — each layer defines what the layer below should be.
CRITICAL: Before starting any phase, READ its reference doc. Each contains the gate criteria, required patterns, and anti-patterns for that phase.
| Phase | Name | Gate | Reference | |-------|------|------|-----------| | 1 | Model | tsc clean + property tests pass + ELS clean | phase-1-model.md | | 1.5 | Review | API review checklist passed | phase-2-validate.md §Review | | 2 | Validate | orchestration tests pass in @effect/vitest | phase-2-validate.md | | 3 | Implement | contract tests pass against real layers | phase-3-implement.md | | 4 | Wire | boundary tests pass + branch coverage target met | phase-4-wire.md | | 5 | Verify | CLI smoke tests pass + structured logs confirmed | phase-5-verify.md |
Single file. Schema domain models with constraints, Context.Tag service interfaces, orchestrator implementation, and property tests for all schemas.
The schema IS the constraint system. Every field gets evaluated: NonEmptyString? pattern()? between()? The property tests co-designed with schemas force constraint thinking.
Speed matters here. The sketch is intentionally disposable — find interface problems fast, don't polish. You can always backtrack and delete. A 30-minute sketch that reveals a bad decomposition saves 3 hours of implementation.
Output: One .sketch.ts file + one .test.ts file. Both typecheck.
-> Read phase-1-model.md
Before writing any implementation. A fresh reviewer examines the Context.Tag interfaces. Apply Musk's first three steps: question every requirement, delete aggressively, then simplify what remains.
"The most common error of a smart engineer is to optimize something that should simply not exist." — Elon Musk
-> Read phase-2-validate.md §API Review
Layer.mock stubs. Prove orchestration logic with @effect/vitest tests. Not bun scripts — real vitest suites with it.effect() and it.effect.prop().
Output: test/ directory with orchestration + property test suites. All pass.
-> Read phase-2-validate.md
Real layers. Split sketch into modules. Contract tests run against real implementations. Reference-model tests compare Layer.mock vs real Layer — same operations, same results.
Output: Module files + contract test suites + reference-model tests. All pass.
-> Read phase-3-implement.md
CLI/API integration. Error boundary mapping (catchTag chains). Integration tests. Branch coverage gate.
Orchestrator executes this phase — not delegated to sub-agents. Boundary code crosses module boundaries and requires understanding the full error architecture.
-> Read phase-4-wire.md
Runtime smoke test. Run actual CLI commands. Verify structured logs include Effect.fn spans and annotateLogs annotations. Confirm tsc + ELS clean. All tests pass. Zero regressions.
This is the "it actually works" gate. Tests prove correctness in isolation. Phase 5 proves it works when composed into the real application.
-> Read phase-5-verify.md
Testing is a GRADIENT, not a phase. It increases monotonically across all phases:
| Phase | What's tested | |-------|--------------| | 1 Model | Schema roundtrips + branded type invariants + algebraic properties | | 2 Validate | + orchestration with Layer.mock + error propagation | | 3 Implement | + contract tests against real layers + reference-model comparison | | 4 Wire | + boundary tests + integration tests + coverage gate | | 5 Verify | + CLI smoke tests + structured log confirmation + regression check |
Three-tier property testing (Slatton): reference-model -> invariant -> crash. All mandatory.
-> Read testing.md
# TypeScript (standard)
npx tsc -p tsconfig.json --noEmit
# Effect Language Service (quick fixes + diagnostics)
npx effect-language-service quickfixes --project tsconfig.json
# Tests
npx vitest run
# Coverage (Phase 4)
npx vitest run --coverage --coverage.branches=100
Zero diagnostics. Zero warnings. All tests pass. No exceptions.
| Signal | Go back to | |--------|-----------| | Orchestrator feels wrong | Phase 1 — reshape interfaces | | Test reveals missing method | Phase 1 — add to Context.Tag | | Implementation reveals interface is unimplementable | Phase 1 — DELETE sketch, redesign | | Agent output doesn't fit | Phase 1 — the interface was wrong | | Coverage gap in domain code | Phase 3 — add contract/property tests |
Backtrack, don't force. If a Context.Tag can't be implemented, delete and redesign. The sketch is disposable — it's the first pass, the implementation is the rewrite.
| Phase | Use | Why |
|-------|-----|-----|
| Design (sketch) | Context.Tag | No implementation required. Pure interface. |
| Implementation | Either | Context.Tag + Layer.effect for swappable. Effect.Service for single impl. |
| Testing | Layer.mock / Layer.succeed | Mock for partial stubs. Succeed for full doubles. |
SDD isn't only for greenfield. Audit mode brings existing Effect service code up to SDD standard.
Walk forward through phases 1->4, evaluate every gate. Then decide: remediate (patch gaps) or rewrite (the first impl was the prototype — rebuild properly from Phase 1).
-> Read audit.md
These violations were found across ALL models in blind testing. They're easy to write, hard to catch in review. Check explicitly.
// BAD — side effect escapes Effect tracking
const id = crypto.randomUUID()
// GOOD — Effect tracks the impure operation
const id = yield* Effect.sync(() => crypto.randomUUID())
// BAD — DateTime.unsafeNow bypasses Effect
const now = DateTime.unsafeNow
// GOOD — DateTime.now returns Effect<DateTime.Utc>
const now = yield* DateTime.now
// BAD — switch on _tag loses exhaustiveness
switch (event._tag) {
case "Click": return "engaged"
default: return "anonymous" // silent bug if new event added
}
// GOOD — Match.exhaustive is compile-time checked
Match.value(event).pipe(
Match.tag("Click", () => "engaged"),
Match.tag("Signup", () => "lead"),
Match.exhaustive // <- compiler error if case missing
)
// BAD — imperative loop with mutation
let total = 0
for (const e of events) { total += e.count }
// GOOD — functional fold
const total = events.reduce((acc, e) => acc + e.count, 0)
// GOOD (effectful) — for tracked accumulation
const total = yield* Ref.make(0)
yield* Effect.forEach(events, (e) => Ref.update(total, (n) => n + e.count))
Effect.fn("name")(function* () { ... }) — this IS the canonical Effect.fn pattern per docsSchema.OptionFromNullOr and Schema.OptionFromUndefinedOr are valid — use FromNullOr for JSON-encoded data, FromUndefinedOr for JS object propertiesIf you have effect-mcp configured, verify APIs before writing. Use the effect_docs_search and get_effect_doc MCP tools:
Schema.Class, Schema.TaggedError, Context.Tag to confirm current API shapesSchema.OptionFromNullOr or Schema.optional?"), search effect-mcpEffect.fn, Layer.effect, Layer.mock signaturesnpx tsc --noEmit AND npx effect-language-service quickfixes --project tsconfig.jsonOtherwise, check https://effect.website/docs for API reference.
The Effect Language Service catches issues that tsc misses — missing service providers, unnecessary Effect.gen wrapping, and more.
Patterns that consistently produced better output in blind testing:
isValidTransition, targetStageForEvent) separate from orchestration. Every future orchestrator gets these for free.query({ personId?, limit?, since? }) instead of queryByPerson, queryBySince, etc. Extensible without breaking callers.Match.tags({ Click: () => ..., Follow: () => ... }) instead of chaining .tag() calls.Effect.all([a(), b(), c()]) for independent operations in dashboard/summary orchestrators.SDD answers HOW to build. It assumes you've already decided WHAT to build. If available, apply Decision Razors first to define scope. Otherwise, manually answer:
Apply razors first. Then enter SDD with a clear, minimal scope.
effect_docs_search + get_effect_doc (if configured)tools
User guide for the local squash-safe `stack` CLI for stacked PR/MR repair on GitHub and GitLab. Use when someone asks how to inspect, track, sync, merge, document, or undo stacked pull requests / merge requests in squash-merge repositories. Prefer this tool over GitHub's `gh stack` command for this workflow.
tools
Control herdr from inside it. Manage workspaces and tabs, split panes, spawn agents, read output, and wait for state changes — all via CLI commands that talk to the running herdr instance over a local unix socket. Use when running inside herdr (HERDR_ENV=1).
development
Inspect my self-hosted AutoCaliWeb library. Use when the user asks about AutoCaliWeb, books in Calibre, OPDS status, recent imports, shelves, catalog stats, or wants to search the ebook library.
data-ai
Subscribe a YouTube channel in TubeArchivist, queue its top videos by views from recent uploads, wait for the first file, scan Jellyfin, and rename + lock the channel folder to a friendly display name. Use when the user says "add a youtube channel", "subscribe to <channel>", or wants a curated TubeArchivist + Jellyfin import for a creator.