Agent skill
stack-patterns
Reference for Outfitter Stack patterns including Result types, Handler contract, Error taxonomy, and @outfitter/* package conventions. Use when learning the stack, looking up patterns, understanding packages, or when "Result", "Handler", "error taxonomy", "OutfitterError", "CLI output", "pagination", "MCP server", "MCP tool", "structured logging", "redaction", "test handler", "daemon", "IPC", or "@outfitter/*" are mentioned.
Install this agent skill to your Project
npx add-skill https://github.com/outfitter-dev/agents/tree/main/plugins/outfitter-stack/skills/stack-patterns
SKILL.md
Outfitter Stack Patterns
Primary reference for @outfitter/* package conventions.
Handler Contract
Handlers are pure functions that:
- Accept typed input and context
- Return
Result<TOutput, TError> - Know nothing about transport (CLI flags, HTTP headers, MCP tool schemas)
type Handler<TInput, TOutput, TError extends OutfitterError> = (
input: TInput,
ctx: HandlerContext
) => Promise<Result<TOutput, TError>>;
Example
import { Result, NotFoundError, type Handler } from "@outfitter/contracts";
export const getUser: Handler<{ id: string }, User, NotFoundError> = async (input, ctx) => {
ctx.logger.debug("Fetching user", { userId: input.id });
const user = await db.users.findById(input.id);
if (!user) {
return Result.err(new NotFoundError("user", input.id));
}
return Result.ok(user);
};
Why? Testability (just call the function), reusability (same handler for CLI/MCP/HTTP), type safety (explicit types), composability (handlers wrap handlers).
Result Types
Uses Result<T, E> from better-result for explicit error handling.
import { Result } from "@outfitter/contracts";
// Create
const ok = Result.ok({ name: "Alice" });
const err = Result.err(new NotFoundError("user", "123"));
// Check
if (result.isOk()) {
console.log(result.value); // TypeScript knows T
} else {
console.log(result.error); // TypeScript knows E
}
// Pattern match
const message = result.match({
ok: (user) => `Found ${user.name}`,
err: (error) => `Error: ${error.message}`,
});
// Combine
const combined = combine2(result1, result2); // tuple or first error
Error Taxonomy
Ten categories map to exit codes and HTTP status:
| Category | Exit | HTTP | When to Use |
|---|---|---|---|
validation |
1 | 400 | Invalid input, schema failures |
not_found |
2 | 404 | Resource doesn't exist |
conflict |
3 | 409 | Already exists, version mismatch |
permission |
4 | 403 | Forbidden action |
timeout |
5 | 504 | Operation took too long |
rate_limit |
6 | 429 | Too many requests |
network |
7 | 503 | Connection failures |
internal |
8 | 500 | Unexpected errors, bugs |
auth |
9 | 401 | Authentication required |
cancelled |
130 | 499 | User interrupted (Ctrl+C) |
import { ValidationError, NotFoundError, getExitCode } from "@outfitter/contracts";
new ValidationError("Invalid email", { field: "email" });
new NotFoundError("user", "user-123");
getExitCode(error.category); // 2 for not_found
getStatusCode(error.category); // 404 for not_found
Validation
Use Zod with createValidator for type-safe validation returning Results:
import { createValidator } from "@outfitter/contracts";
import { z } from "zod";
const InputSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const validateInput = createValidator(InputSchema);
// In handler
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value; // typed as z.infer<typeof InputSchema>
Context
HandlerContext carries cross-cutting concerns:
import { createContext } from "@outfitter/contracts";
const ctx = createContext({
logger: myLogger, // structured logger
config: resolvedConfig, // merged configuration
signal: controller.signal, // cancellation
workspaceRoot: "/project",
});
// ctx.requestId is auto-generated UUIDv7 for tracing
| Field | Type | Description |
|---|---|---|
requestId |
string |
Auto-generated UUIDv7 |
logger |
Logger |
Structured logger |
config |
ResolvedConfig |
Merged config |
signal |
AbortSignal |
Cancellation signal |
workspaceRoot |
string |
Project root |
cwd |
string |
Current directory |
Package Reference
| Package | Purpose | When to Use |
|---|---|---|
@outfitter/contracts |
Result types, errors, Handler contract | Always (foundation) |
@outfitter/types |
Type utilities, collection helpers | Type manipulation |
@outfitter/cli |
CLI commands, output modes, formatting | CLI applications |
@outfitter/mcp |
MCP server, tool registration, Zod schemas | AI agent tools |
@outfitter/config |
XDG paths, config loading, env handling | Configuration needed |
@outfitter/logging |
Structured logging, sinks, redaction | Logging needed |
@outfitter/daemon |
Background services, IPC, health checks | Long-running services |
@outfitter/file-ops |
Secure paths, atomic writes, file locking | File operations |
@outfitter/state |
Pagination, cursor state | Paginated data |
@outfitter/testing |
Test harnesses, fixtures, Bun test | Testing |
Selection guidance:
- All projects start with
@outfitter/contracts - CLI apps add
@outfitter/cli(includes UI components) - MCP servers add
@outfitter/mcp - Projects with config add
@outfitter/config - File operations need
@outfitter/file-opsfor safety
Type Utilities
@outfitter/types provides collection helpers and type utilities:
Collection Helpers
import { sortBy, dedupe, chunk } from "@outfitter/types";
// Sort by property
const users = [{ name: "Bob" }, { name: "Alice" }];
sortBy(users, "name"); // [{ name: "Alice" }, { name: "Bob" }]
sortBy(users, u => u.name); // Same, with accessor function
// Remove duplicates
dedupe([1, 2, 2, 3, 3, 3]); // [1, 2, 3]
dedupe(users, u => u.name); // Dedupe by property
// Split into chunks
chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
Type Utilities
Standard TypeScript utility types for common patterns:
import type { Prettify, DeepPartial, Nullable } from "@outfitter/types";
// Prettify: Flatten complex intersection types for better IntelliSense
type Combined = { a: string } & { b: number };
type Pretty = Prettify<Combined>; // Shows { a: string; b: number }
// DeepPartial: Make all properties optional recursively
type Config = { db: { host: string; port: number } };
type PartialConfig = DeepPartial<Config>;
// Nullable: T | null
type MaybeUser = Nullable<User>;
Domain Error Mapping
Map your domain errors to the 10 taxonomy categories:
| Domain Error | Stack Category | Error Class | Exit | HTTP |
|---|---|---|---|---|
| Not found | not_found |
NotFoundError |
2 | 404 |
| Invalid input | validation |
ValidationError |
1 | 400 |
| Already exists | conflict |
ConflictError |
3 | 409 |
| No permission | permission |
PermissionError |
4 | 403 |
| Auth required | auth |
AuthError |
9 | 401 |
| Timed out | timeout |
TimeoutError |
5 | 504 |
| Connection failed | network |
NetworkError |
7 | 503 |
| Limit exceeded | rate_limit |
RateLimitError |
6 | 429 |
| Bug/unexpected | internal |
InternalError |
8 | 500 |
| User cancelled | cancelled |
CancelledError |
130 | 499 |
Mapping examples:
// "User not found" -> NotFoundError
new NotFoundError("user", userId);
// "Invalid email format" -> ValidationError
new ValidationError("Invalid email", { field: "email" });
// "User already exists" -> ConflictError
new ConflictError("Email already registered", { email });
// "Cannot delete admin" -> PermissionError
new PermissionError("Cannot delete admin users");
// Unexpected errors -> InternalError
new InternalError("Database connection failed", { cause: error });
Bun-First APIs
Prefer Bun-native APIs:
| Need | Bun API |
|---|---|
| Hashing | Bun.hash() |
| Globbing | Bun.Glob |
| Semver | Bun.semver |
| Shell | Bun.$ |
| Colors | Bun.color() |
| String width | Bun.stringWidth() |
| SQLite | bun:sqlite |
| UUID v7 | Bun.randomUUIDv7() |
References
Core Patterns
- Handler Contract
- Error Taxonomy
- Result Utilities
- Conversion Patterns
Package Deep Dives
- CLI Patterns - Output modes, pagination, formatting utilities
- MCP Patterns - Tool registration, resources, schemas
- File Operations - Atomic writes, locking, secure paths
- Logging Patterns - Structured logging, sinks, redaction
- Testing Patterns - Test harnesses, fixtures
- Daemon Patterns - Lifecycle, IPC, health checks
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
stack-feedback
Creates GitHub issues for problems discovered while using @outfitter/* packages. Use when finding bugs, missing features, unclear documentation, or improvement opportunities.
stack-architecture
Design stack-based systems using @outfitter/* packages. Use when planning new projects, choosing packages, designing handler architecture, or when "architecture", "design", "structure", "plan handlers", or "error taxonomy" are mentioned.
stack-templates
Templates for creating handlers, CLI commands, MCP tools, and daemon services following Outfitter Stack conventions. Use when scaffolding new components, creating handlers, adding commands, or when "create handler", "new command", "add tool", "scaffold", "template", or "daemon service" are mentioned.
stack-audit
Scan codebase for Outfitter Stack adoption candidates. Identifies throw statements, console usage, hardcoded paths, and custom errors. Use when assessing adoption scope or checking readiness.
stack-review
Audits code for Outfitter Stack compliance including Result types, error handling, logging patterns, and path safety. Use for pre-commit reviews, code quality checks, migration validation, or when "audit", "check compliance", "review stack", or "stack patterns" are mentioned.
stack-debug
Troubleshoots Outfitter Stack issues including Result handling, MCP problems, CLI output, exit codes, and logging. Use when debugging stack-specific issues, unexpected errors, wrong output modes, or when "debug Result", "MCP not working", "wrong exit code", or "logging issue" are mentioned.
Didn't find tool you were looking for?