Getting Started

Changelog

All notable changes to Routecraft.

Routecraft is in active development -- APIs may change between minor versions.


v0.7.1 In development

CLI

  • --log-level and --log-file take effect again -- in 0.7.0 both flags were ignored and every log line went to stdout, which corrupts an MCP server run over stdio. See basic usage.
  • A scaffolded project's start shows its greeting again -- create-routecraft 0.7.0 ran craft start at the default warn level, which hides the sample's info greeting; the script is now craft start --log-level info.
  • LOG_LEVEL and LOG_FILE work from env files -- .env.<profile>, a profile's env: and --env <file> were loaded after the logger was built, so log settings there were ignored; a flag still beats them. See the environment a profile selects.
  • An unwritable log file stops the command -- craft exits 2 naming the path and where it came from (a flag, LOG_FILE, or craft.log.js / .cjs) instead of running on with its logs diverted to the temporary directory. See basic usage.
  • A forced --once shutdown names the right option -- its hint said to raise shutdown.timeoutMs, which 0.7 refuses; it now says shutdown.timeout. See shutdown.
  • A named env file that cannot be read stops the command -- --env <path> or a profile's env: "<file>" pointing at nothing was skipped with a note hidden at the default log level, and the command ran against an environment nobody chose; it now exits 2 naming the path. A .env cascade file that exists but cannot be read stops the command the same way. See the environment a profile selects.
  • The CLI stops installing adapter dependencies it never uses -- it declared thirteen packages it does not import, so every image running craft start carried them, agent-browser with Playwright and WebdriverIO included. A project that uses cron(), mail(), html(), xml(), csv(), carddav(), JWKS verification, OpenTelemetry tracing, shell() or agentBrowser() now lists the package itself, and one that does not fails with RC5017 and the install command, when the route starts or the first time the adapter runs. Each adapter's reference page now names its package.
  • craft run runs a file outside any project -- with no node_modules above it, Bun fetched a second copy of core into its cache and the file could not find its adapters' packages or even start the logger. The file now runs on the core that ships with the CLI, and an adapter's package is found next to the file or next to a global CLI. See run.
  • A scaffolded project keeps craft in a production install -- create-routecraft listed @routecraft/cli as a dev dependency, so bun install --production left an image without the binary craft start needs. A project scaffolded earlier runs bun remove @routecraft/cli && bun add @routecraft/cli, since a bare bun add keeps the entry under devDependencies.

Core

  • defineConfig rejects keys CraftConfig does not declare -- in 0.7.0 a misspelled or 0.6 key beside a valid one (http: { host, port, auth }, shutdown.timeoutMs) compiled and was ignored or failed at boot; it is now a compile error at any depth, the same one a CraftConfig annotation reports. The helper returns CraftConfig rather than the literal type of its argument. See configuration and the migration guide.
  • An event() route no longer feeds on its own events -- a route watching route:step:* or route:exchange:* received the events its own exchanges emitted, started an exchange for each, and never let the event loop yield, so the process hung. Events its own exchanges cause, including their context:error and the events of routes they call through direct(), are no longer delivered to it; its lifecycle events still are. See the event adapter.
  • Browser preflights name Authorization -- Access-Control-Allow-Headers is Authorization, *. The Fetch spec leaves Authorization out of the bare wildcard, so a bearer-token call from a browser (the MCP Inspector's Direct mode) depended on browsers not enforcing that yet.

AI & MCP

  • A misconfigured resource.url fails with RC5003 -- on the HTTP transport, a missing or plain-http URL outside development and test, and an unparseable one in any environment, threw a bare TypeError; it now carries a code, a docs link, and the fix (NODE_ENV=development for local work, an HTTPS URL behind a proxy). The stdio transport, which ignores resource, no longer checks it.

Docs site

  • The 0.6 to 0.7 migration guide covers the Host and Origin checks -- MCP and ACP mounts refuse a browser Origin or an unknown Host with 403 until browserOrigins or allowedHostnames lists them, which the guide did not say. See servers and mounts.
  • The 0.5 to 0.6 migration guide covers the spy logger -- t.logger stopped being vi.fn()-based in 0.6, so runner matchers need testContext({ fn }).
  • Event names match what the framework emits -- reference pages, the events namespace map and JSDoc still used bare exchange:*, step:* and error:caught names and route ids inside names, none of which a subscription matches since 0.6. See events.
  • mcpPlugin documents resource -- the options table had no row for the protected-resource URL that production requires. See mcpPlugin.

v0.7.0 Pre-release

September 2026

This section covers every change since the v0.6.0 release. 0.7.0 is the durable release: a capability can defer mid-pipeline and continue days later from another transport, an agent can do the same in the middle of a conversation, and a running instance can be driven, watched and reached over HTTP, from a terminal, from an editor, or from another instance. See the 0.6.x to 0.7.0 migration guide for every upgrade step.

Safety fix

  • Downstream deferral receipts no longer expose resume tokens through agent tool results or background session inboxes. Ordinary calls receive a pending marker; background delivery fails with AI1006 while the underlying approval stays pending. Resolve that approval through the application rather than automatically retrying the action. See background tools.

Core Breaking

  • Every authored time option takes Duration, and the Ms suffix is gone -- intervalMs is interval, timeoutMs is timeout, backoffMs is backoff, jitterMs is maxJitter, shutdown.timeoutMs is shutdown.timeout, and so on across timer, cron, http, mail, .retry(), .circuitBreaker(), .debounce(), .batch(), .sample(), .timeout(), .delay(), defineIndicator, mcpPlugin and @routecraft/testing. Existing numbers keep working under the new names, '1m' is now accepted everywhere, and a stale name fails the build with RC5003 naming its replacement instead of being silently dropped. See the migration guide.
  • Named servers replace listener options -- http.port / http.host and mcp.port / mcp.host are removed; declare listeners under servers and select one by name. Listener events are server:listening / server:failed / server:closed, and plugin:mcp:server:listening is gone. The per-route http({ auth }) option is removed: a mount decides authentication, and one auth vocabulary (unset inherits the server validator, a config walls, false opens) applies to http, mcp and ops. mcpPlugin({ transport: 'http' }) requires an explicit HTTPS resource.url outside development and test. See the migration guide.
  • Durable deferral is new -- 0.6 had no way to pause an exchange, so there is nothing to migrate. .defer({ schema, ttl, meta }) pauses mid-pipeline and .resume() continues from the same position, by signed token, from any transport. A deferral expires after 72h unless deferral: { defaultTtl: 'never' } says otherwise. See the migration guide.
  • Lifecycle -- a context is single-use (start() after stop() refuses with RC1004); context.stop() resolves { forced, pending } and context:stopped carries the same; plugin:started now marks the new start() phase done and the apply-phase events are plugin:applying / plugin:applied; Route.signal fires only when in-flight work is abandoned and Route.intakeSignal carries the old meaning. Shutdown drains in-flight work before abandoning it, bounded by shutdown.timeout (30s). A second SIGINT or SIGTERM no longer forces an exit; SIGQUIT (Ctrl+Backslash) and SIGBREAK (Ctrl+Break) are the force signals, so a container whose STOPSIGNAL is SIGQUIT gets a force exit instead of a drain. See shutdown helpers and the migration guide.
  • forward() carries the caller's identity and trace -- a forwarded call arrives with the caller's headers, so a target's .authorize() sees the caller rather than nobody, a strict .input({ headers }) target may now reject it, and an expired principal surfaces as expiry rather than RC5012. Every route ingress mints a fresh exchange id and drops the split hierarchy.
  • html() text extraction keeps whitespace and escaped markup -- extract: 'text' no longer collapses whitespace inside the match or deletes escaped tags; collapse in the route where you relied on the old shape, and escape at the sink.
  • http() client caps the response body at 10 MB -- raise maxBodySize on a call that moves more; exceeding it fails with RC5061. The body option's type is narrowed to HttpRequestPayload, which only stops bigint and symbol bodies compiling.
  • timer({ exactTime }) and timer({ timePattern }) are removed -- neither worked; use cron() with a timezone.
  • .retry() reaches resumed continuations -- a route-scope .retry() alongside .defer() now gives the steps after the deferral at-least-once execution, so make them idempotent. .concurrency() and .timeout() apply to continuations too.
  • An ArkType deferral digest changes -- a callable schema is hashed by its rendered JSON Schema; a deferred ArkType exchange resumes into RC5048 and re-asks its approver, so settle those before upgrading. Zod digests are byte-identical.
  • ops.auth: false is meaningful -- it removes the wall from the ops mount instead of throwing, which closes the health.details gate.

Core

  • Durable defer and resume -- .defer() defers an exchange in a store (SQLite by default, memory for tests, or a DeferralStore of your own) and answers the caller with a Deferred acknowledgment; .resume() revives it by signed token from any transport at the same position, with .resume({ authorize }) as the one policy point, meta travelling with the deferral, per-call credentials through tokenFor, and expiry, crash-safe delivery and retention handled by a sweeper that scans at boot. Re-entrant defer sites let an adapter defer the step it is executing, which is what durable agents build on.
  • Named servers and mounts -- one listener carries http routes, MCP, ACP and the ops surface on distinct paths, or each gets its own port; mounts claim paths at bind time and own their authentication. See the servers plugin and the http plugin.
  • The ops plugin -- /health with liveness and readiness, defineIndicator for dependencies the framework cannot see, the management API (GET /ops/routes, POST /ops/routes/{id}/exchanges, contributed resources) behind scope-gated tiers that answer 404 until named, and GET /ops/events tailing the event bus as Server-Sent Events. See the ops plugin.
  • GET /ops/deferrals -- what an instance is waiting on, without invoking anything: one row per deferral with the route, the state, what it waits for, whether a delivery claim is outstanding, when it was deferred and when it comes due, and the outcome of a settled one. Never the resume token and never the stored exchange. Waiting by default, filterable by state and route, paged oldest first, and reachable as craft ops deferrals. DeferralStore gains list() for it, which both shipped backends satisfy under one contract suite. The member is required, so a custom store stops compiling until it implements it; one that reaches a running instance without it (a JavaScript store, or an upgrade that skipped a typecheck) is refused with RC5066 naming the member rather than answering an empty page.
  • Remotes -- defineConfig({ remotes }) makes another instance's dispatchable routes direct endpoints here, named git-style (hello for the default remote, lab:hello for any other), with a local route winning a name clash loudly. See the remotes plugin.
  • .enabled() -- a route declares whether it runs at all; a disabled route is known, not started and not offered to an agent, with its reason on the health report, and context.reevaluateEnablement() brings it up without a restart. See enabled.
  • Authorization and identity -- authorize() gains anyScope (any one of a set) and effective (read the delegating agent's scopes too); direct({ internal: true }) closes a subroutine's external doors; every http surface serves RFC 9728 protected-resource metadata and every bearer 401 points at it; timingSafeStringEqual is exported for custom validators.
  • Plugins gain a start() phase running after every route is ready, teardown receives { partial, started }, build() unwinds applied plugins when a later step fails, a boot summary line reports what started, and a stop() racing boot waits for the in-flight hook.
  • Exports -- isStandardSchema, validateAgainst, isDropped, wasOutputValidated, parseDuration, anySignal, isRedirect, createOpsHttpClient and contributeOpsIndicator.
  • Fixes -- a dispatch to a remote is sent on a connection of its own where the runtime would otherwise re-send it, so a POST /ops/routes/{id}/exchanges whose socket dropped mid-flight can no longer run the route twice while the caller is told once (Bun below 1.3.14; the reads beside it keep the pool, and a fixed runtime or Node pays nothing); a capability is removed when its route stops, so a stopped route leaves the agent tool surface and the ops listing instead of staying dispatchable and failing RC5004 on the call, and a dispatch to one is refused naming the state rather than advising a .from(direct()) it already has; an IMAP pool drained mid-connect no longer throws; two SQLite stores on one path are refused at boot, and a file carries its store's identity; a stream's expiry check applies the clock tolerance that admitted it; the deferral counter refuses corruption (RC5057) instead of reusing an id; retention counts from settlement rather than from the deferral; a declared server with nothing mounted names the mount topology.

Adapters

  • http() source: streaming, early answers and signed webhooks -- an AsyncIterable body answers as Server-Sent Events and a ReadableStream passes through; respond acknowledges a webhook before the pipeline runs; signature: { scheme: 'standard-webhooks' } verifies Resend, Bird and Svix deliveries; maxStreamingRequests and idleTimeout bound a listener. See the http reference.
  • http() client: maxBodySize, redirect and responseBody: 'bytes' -- a capped body, a redirect the route can inspect and re-validate, and binary responses that arrive intact.
  • shell() in @routecraft/os -- runs a command without ever invoking a shell, isolated by default (unshare on Linux, or docker as a throwaway container per command), with a granted rather than inherited environment and untrusted() marking values that came from outside. See the shell reference.
  • surface() in @routecraft/ai -- a capability reaches the person's editor over the Agent Client Protocol: surface(method, params) asks and waits, surface.notify() tells. See the surface reference.
  • timer() and cron() take maxJitter, and .throttle({ per }) accepts a Duration.

AI & MCP Breaking

  • currentTime() and randomUuid() are removed -- declare the fn inline under the same tool name; every tools([...]) reference keeps working. See the migration guide.
  • FnHandlerContext.checkpointId is deferralId, and defer is a required member, so a hand-built handler context must provide one.
  • Agent files -- agents/ is walked recursively and identity comes from frontmatter name (the filename no longer has to match), duplicate names throw, and skills: frontmatter declares where skills come from rather than naming blocks.
  • MCP tools -- a route that drops the exchange answers an error result (AI2002) instead of echoing the request; the advertised outputSchema is enforced (AI2001); primitive and array outputs now carry structuredContent in the protocol's envelope; McpIcon.sizes is readonly and the default icon set carries a PNG per theme.
  • Canary-only -- the surface() params callback drops sessionId, the SQLite session schema is version 2 and discards canary rows, and startedBy on the sessions resource is owner.

AI & MCP

  • Durable agents -- ctx.defer() inside a tool defers the whole tool loop through the core store; the loop survives a restart and resumes mid-conversation with the answer swapped into the tool result; MCP advertises oneOf: [Output, Deferred]; a cancelled run fails with AI1005 instead of returning a partial result. See durable agents.
  • Sessions -- agent(name, { session }) keeps a durable transcript with an inbox and one turn at a time, interrupt: true cancels the running turn, a sessions store (SQLite by default) holds the records, and GET /ops/agent-sessions lists them. Background tools (directTool(id, { background: true })) hand long work to a route and post the result to the session's inbox when it finishes. See the agent adapter.
  • Talk to an agent from your editor -- acp: on defineConfig (or acpPlugin()) serves the Agent Client Protocol beside MCP; a conversation belongs to the person who started it and outlives the connection, the editor picks the model and the thinking level from lists the agent file advertises, a message sent mid-turn is answered under that message, and tool calls stream as they run. See talk from your editor and the acp plugin.
  • craft start and the project runtime -- a project boots from its folder layout (craft.config.ts, plugins/, agents/, skills/, capabilities/), Claude Code agent files load unchanged, and a package claims a convention folder through registerProjectDiscoverer. See project structure.
  • Model controls -- reasoning and providerOptions on llm() and agent(), the full sampling block on agent() and agentPlugin({ defaultOptions }), content parts (text, file, image) in the user prompt, agent({ stream: true }) for token deltas over SSE, and prompt callbacks typed from the route input.
  • Compaction primitives -- replaceDeferredThread rewrites a deferred agent's thread in place (AI1008 refuses a malformed one), and AI1009 names a prompt that does not fit the model's context window.
  • Fixes -- a failed MCP tool call no longer repeats its own message; gemini:gemini-3.7-flash is offered by autocomplete; a second agentPlugin install's defaultOptions apply in full; two tools() entries for one tool compose their guards instead of replacing.

CLI Breaking

  • craft chat is removed -- an editor speaking the Agent Client Protocol is the interactive interface; see craft acp below.

CLI

  • craft start, craft exec <route> and craft ops health | ready | routes | deferrals | indicators boot a project and drive a running instance; craft acp is the bridge an editor runs, and it reconnects on its own when the instance behind it restarts. A personal settings file (.routecraft/settings.yaml) with profiles (url, token, agent, env) points every command at a laptop or a company instance, .env.<profile> joins the env cascade, a refusal names who issues acceptable tokens and which scope is missing, a blank flag no longer overrides the file, and a failed dispatch shows the framework's error code. See the CLI reference.

Packages

  • @routecraft/os -- shell() with the unshare and docker isolation tiers; shell({ timeout }) takes a Duration.
  • @routecraft/eslint-plugin-routecraft -- require-untrusted-shell-args warns on a shell argument that should be marked untrusted().
  • @routecraft/testing -- a t.contextLogger spy, thenableSchema(), startAndWaitReady() no longer rejects on a failing startup exchange, and readiness settles a route that starts or is disabled.
  • create-routecraft -- scaffolding from a repository keeps the files it copies, merges the example's package.json instead of replacing it, accepts a /tree/<branch> URL without a subpath, and matches the copy filter on path segments.
  • Peer ranges -- @routecraft/ai, @routecraft/os and @routecraft/testing admit the canaries of their line (>=0.7.0-0 <1.0.0), with a contract test that fails when a range stops admitting the version that governs it.

Docs site

Known limitations

  • Plugin ordering on the array path -- plugins: [acpPlugin()] must be listed after the agentPlugin() that registers the agents it serves, or the build fails with RC5003 saying so. The acp: config key applies in a fixed order and has no such constraint.

v0.6.0 Pre-release

August 2026

This section covers every change since the v0.5.0 release. 0.6.0 is the architecture release before v1: the contracts that freeze at v1 changed shape once, now, so they do not have to change after, and the engine rework brings a significant performance improvement to route and event processing. See the 0.5.x to 0.6.0 migration guide for all upgrade steps.

Core Breaking

  • Fixed event names; identity in the payload -- hierarchical names like route:<id>:exchange:failed become a fixed set (route:exchange:failed) with routeId in details. Wildcard patterns on ctx.on() / ctx.once() are replaced by exact names, the "*" catch-all, and the forRoute() filter helper (the event() source adapter keeps its pattern support); ecosystem packages declare events by merging into EventDetailsMap. plugin:registered is removed (it duplicated plugin:starting).
  • Subscription source contract -- source adapters receive a single Subscription object ({ context, signal, meta, ready(), complete(), emit() }) instead of five positional parameters. .from() additionally accepts async generator functions and (async) iterables, and @routecraft/testing adds a testSubscription() helper.
  • StepOutcome step contract -- custom Step implementations return what happened (continue / complete / drop / branch / fanOut) and the executor owns all scheduling; the wrapper buffer/relay protocol is gone. Per-execution metadata rides the outcome instead of mutating the shared Step instance. Custom aggregators return { body, headers? } instead of a fabricated Exchange.
  • Namespaced error-code registry -- ecosystem packages own codes under a claimed namespace via registerErrorCodes() plus ErrorCodeRegistry declaration merging; RC is reserved for core. Adds RC1003 (error-code registration failed).
  • Type-enforced builder positioning -- craft() returns a pre-from builder, so pipeline operations before .from() are compile errors; builder generics move to a state bag (RouteBuilder<{ body: T }>, AnyRouteBuilder for lists).
  • Splitters return child bodies -- .split() callbacks return values (or splitChild(body, headers) for per-child header overrides) instead of hand-built Exchange instances; the framework owns child construction.
  • Consumer SPI -- Consumer.register receives the Message envelope and consumer classes construct from one ConsumerDeps bag; Message, ProcessingQueue, ConsumerType, and ConsumerDeps are exported.
  • Per-adapter header key objects -- HeadersKeys keeps framework keys only; adapter keys move to TimerHeaders / CronHeaders / FileHeaders / CsvHeaders / JsonlHeaders / MailHeaders / CarddavHeaders; HEADER_MAIL_* / HEADER_CARDDAV_* and HeaderKeysRegistry are removed (wire keys unchanged). .header() rejects every engine-owned routecraft.* key up front.
  • client.sendDirect and public capability discovery -- CraftClient.send is renamed sendDirect (response generic defaults to unknown); context.capabilities() replaces reads of the internal direct registry, and ADAPTER_DIRECT_REGISTRY / getDirectChannel / sanitizeEndpoint are no longer exported.
  • Naming sweeps -- CardDAV* exports become Carddav* (acronym casing per the Http precedent), the carddav option types adopt the two-sided Server/Client naming (CarddavServerOptions for the read role, CarddavClientOptions for writes and deletes), and jsonl's three file option types fold into one JsonlFileOptions.
  • choice() variadic surface; BranchBuilder renamed, ChoiceSubBuilder removed -- the fluent callback .choice(c => c.when(p, fn).otherwise(fn)) becomes variadic .choice(when(p, fn), ..., otherwise(fn)) with standalone when / otherwise helpers imported from @routecraft/routecraft, the path surface now shared with the new multicast. BranchBuilder is renamed PathBuilder; ChoiceSubBuilder is gone.
  • Adapter role model: Source / Destination / Enricher -- Destination.send is now strictly void and the new Enricher.fetch pulls a value in, so the operation keyword selects the role instead of an option value. Pull-in adapters are renamed to *EnricherAdapter, and @routecraft/ai, @routecraft/os and @routecraft/testing raise their core peer range to >=0.6.0. See the migration guide.
  • .enrich() replaces the body by default -- with the aggregator omitted it now replaces rather than spread-merges, so audit every bare .enrich(). only() and none() still merge, and replace() is deleted. See the migration guide.
  • File-family adapters drop mode -- file, csv, json, jsonl, xml and html take their role from the operation keyword, with append: true / delete: true selecting send behaviour. Two silent flips to audit: jsonl sends now overwrite by default, and a migrated .tap(json({ path })) writes where mode: 'read' used to read and discard. See the migration guide.
  • MailSendResult, CarddavWriteResult and CarddavDeleteResult deleted -- sends that produce a receipt now surface it on routecraft.mail.* and routecraft.carddav.* headers instead of replacing the body. See the migration guide.
  • Option laws: arity is not a discriminant, key presence means supplied -- mail() returns one read adapter for both call shapes rather than changing role with a second argument, and a supplied-but-undefined path on the codec adapters now throws RC5003 instead of silently selecting the transformer role. Both changes are additive for code that already compiled. See the migration guide.
  • .input() validation folds into the pre-from filter chain -- .error() can now observe and recover an input failure, which previously bypassed it. Migrate observers: validation failures no longer emit route:exchange:dropped, they take the normal error path.
  • .retry({ exponential }) removed in favour of factor -- migrate exponential: true to factor: 2 and exponential: false to factor: 1, the new default; the old option throws RC5003 at build with a hint. See the retry reference for the backoff, cap and jitter options.
  • authorize() defaults to actor: 'none' -- Principal becomes delegation-aware (RFC 8693 act / may_act) and the new delegate() helper mints delegated principals, so a principal carrying an actor, including a Clerk impersonation session, is rejected until the route declares its permitted actors. Adds RC5034 to RC5038. See the migration guide and securing capabilities.

Core

  • Recovery directives -- .error() handlers may return recovery.drop(reason?) (discard the failing exchange) or recovery.rethrow() (decline recovery) instead of a recovery body or a manual throw.
  • Open error and principal models -- rcError accepts a per-occurrence retryable override; RCMeta.category and Principal.kind accept ecosystem-defined strings alongside the known values.
  • Plugin identity and lifecycle -- plugins may declare name (used as pluginId on events and logs) and reserve dependsOn; registerTeardown callbacks unwind LIFO; getRoutes() returns a copy.
  • route:source:failed lifecycle event -- fires when a source subscription rejects (the source gave up producing), with { routeId, route, adapter?, error }. Unlike route:stopping it never fires for an orderly shutdown, so it is the signal to alarm on for a dead channel.
  • concurrency (bulkhead) wrapper operation -- .concurrency({ max }) bounds how many exchanges run an operation at once, the sibling of .throttle(), which bounds a rate. See the concurrency reference.
  • dispatch load-balancing operation -- .dispatch(strategy, ...targets) runs exactly one of several targets by failover, round-robin, weighted or sticky strategy, the sibling of multicast and choice. See the dispatch reference.
  • debounce flow-control operation -- .debounce({ waitMs }) releases only the last exchange in a burst after a quiet period, for file-change batching and search-as-you-type. Route scope only, and a pending exchange is flushed on drain rather than lost. See the debounce reference.
  • sample and dedupe flow-control operations -- sample() passes every Nth exchange or the first in each time window, and dedupe() suppresses duplicates by a derived key. Both drop silently, like a filter returning false. See the sample and dedupe references.
  • .timeout() propagates an AbortSignal -- an expired deadline now cancels the wrapped step instead of leaving abandoned work running in the background, and http() forwards the signal into its fetch automatically. The .timeout(ms) surface is unchanged. See the timeout reference.
  • .input({ body }) retypes the builder -- the following .from(source) opens the pipeline with the schema's inferred output type, so the duplicated .from<T>() generic is no longer needed.
  • jwt() and jwks() surface clockToleranceSec -- a consumer re-checking a verified principal's expiresAt can now see the skew the verifier allowed. The expiry boundary is also inclusive everywhere, matching jose and RFC 7519, where jwt() previously honoured an expired token for one further second. See the migration guide.

AI & MCP Breaking

  • AI error codes renamed -- RC5025 / RC5026 / RC5027 become AI1001 / AI1002 / AI1003 under the new AI namespace; update any code or alerting that matches on error.rc.

  • Agent blocks replace skills -- AgentOptions.skills and agentPlugin({ skills }) are removed in favour of a blocks record that unifies skills, memory, identity, and instructions, with progressive disclosure now the default.

  • skills({ source }) and fromFile(path) builders -- skills now returns a blocks record to spread into blocks: { ... }; fromFile reads a UTF-8 file at resolution time.

  • Nested block groups -- a blocks value can be a single block or a nested blocks group, so skills({ source }) can stay grouped under one key (blocks: { skills: await skills(...) }) instead of being spread flat. Groups flatten to group__leaf names.

  • Tag selectors on tools() removed -- the { tagged } / { tagged, from } variants and the tags override on directTool are gone. Use the new tools((catalog) => [...]) builder form for dynamic selection.

  • Block-loader calls partitioned out of toolCalls -- progressive loads surface on AgentResult.blocksLoaded and emit agent:block:* events instead of agent:tool:*.

  • skills: frontmatter on agents() rejected -- supply blocks through the per-agent overrides map instead.

  • New error codes AI1001-AI1003 -- block resolution failure, name collision / reserved _block_ prefix, and block misconfiguration.

  • direct_<routeId> and _block_load_<name> tool names renamed -- synthetic tool names now use __ as their sole structural separator, so they become direct__<routeId> and _block__load__<name>. Fn ids and mcp__<server>__<tool> are unchanged, and the Direct(...) / MCP(...) authoring grammar does not change. Update anything pinning a generated name: tool-name guards, assertions on toolCalls[].toolName or blocksLoaded[].toolName, recorded transcripts, evals. See the migration guide.

  • ResolvedTool.source is a new required field -- a resolver-set fn / direct / mcp / block discriminant. Affects only code that hand-constructs a ResolvedTool.

  • Direct(<routeId>) and fn ids validated against the provider charset -- a name that cannot survive as a provider tool name (/^[A-Za-z0-9_-]{1,64}$/) now raises RC5003 naming the offending character or length, rather than being rejected by the provider later. Expose an unsafe route id under a tool-safe alias with directTool(routeId). An MCP client tool whose remote name cannot form a valid wire name is dropped with a warning instead of failing the dispatch.

  • MCP protocol revision 2026-07-28: the server is stateless -- mcpPlugin builds a fresh server per request, so any replica can answer any request behind a plain load balancer. Sessions, their events and McpHeadersKeys.SESSION are gone, and the @modelcontextprotocol/sdk v1 peer is replaced by the v2 package split. 2025-era clients keep working. See the migration guide.

  • oauth({ endpoints, client }) removed; oauth() becomes a resource-server helper -- it no longer proxies your Authorization Server, so point clients at your IdP's own endpoints, which they discover from the RFC 9728 metadata Routecraft serves. Pass issuer instead. See the migration guide.

  • MCP client names reject __ and a trailing _ -- such a name composed an ambiguous mcp__<server>__<tool> that resolved to the wrong tool, silently, so mcpPlugin() now throws RC5003 at startup with a suggested replacement. A single underscore inside the name is unaffected. See the migration guide.

AI & MCP

  • agentPlugin({ toolPolicy }) -- repository-wide admission control for the agent tool surface, keyed by tool kind (fn / direct / mcp), each true, false, or a predicate over a read-only tool descriptor. Omit it and nothing changes; supply it and the surface becomes an allowlist where every kind must be decided. Enforced at the single point every agent form converges on, so inline, registered, markdown, and nested agents are all covered, and multiple installs compose with AND. See the tool policy reference.
  • route:agent:tool:denied event -- emitted once per tool refused admission by a policy, carrying agentName, toolName, toolKind, and a reason of rule, rule-error, or unknown-provenance, so denials are alertable and auditable rather than only logged.
  • mcpPlugin({ proxy }) re-exposes client tools -- proxy tools from registered clients through the Routecraft MCP server without a route per tool, with an optional per-tool guard. Proxied calls run no route pipeline and do not forward the caller's principal, so reserve them for simple read-only tools. See the expose as MCP guide.
  • baseURL honoured by the Anthropic and Gemini providers -- previously only OpenAI honoured it, so explicit config lost to the ambient ANTHROPIC_BASE_URL environment variable.

Internals

  • Engine restructuring -- CraftContext delegates events to an internal EventBus; adapter config keys (cron, direct, mail, telemetry, http) move to per-module config appliers; the route engine splits into pipeline/ modules (executor, validation, synthetic steps). Two behavioural notes: context store seeding for adapter config now happens in initPlugins() (called automatically by start()), and plugin teardown (including registerTeardown callbacks) drains in reverse order.
  • Uniform factory tagging -- every public adapter factory is tagged for mockAdapter(), enforced by a conformance test; previously direct, simple, timer, cron, log, noop, and others (plus two transformer-mode branches of html() / json()) were silently unmockable.
  • Every optional peer loads through loadOptionalPeer -- the mail drivers and agentBrowser() now surface a missing package as RC5017 with an install hint rather than a raw module-not-found, and detection no longer misses the phrasing Bun uses for a subpath import. A contract test scans all four code packages for bare external dynamic imports.
  • Config appliers restored in the published bundles -- a sideEffects allowlist let esbuild prune every core config applier out of the published bundle, so defineConfig({ mail: { accounts } }) typechecked but never applied at runtime. A post-build guard now asserts every applier is live in the registry, and an unrecognised defineConfig key warns instead of being a silent no-op.
  • @routecraft/ai, @routecraft/os and @routecraft/testing declare core as a peer -- at >=0.6.0 <1.0.0, with a workspace devDependency for development, instead of duplicating core as a regular dependency.
  • Dependency floors refreshed -- runtime ranges on @routecraft/ai, @routecraft/cli and create-routecraft move to their newest in-range minor and patch releases, and core's optional fast-xml-parser peer floor rises to ^5.10.1 to exclude a DOCTYPE entity-expansion advisory. No majors are included, and the imapflow floor is held back deliberately.

Adapters

  • HTTP source Breaking -- http() is now a two-sided adapter. http({ path, method? }) exposes a route over HTTP via defineConfig({ http: { port, host, auth } }); Bun runtimes bind through Bun.serve and Node 22+ uses a zero-dependency node:http shim. Global auth accepts jwt() / jwks() bearer or apiKey({...}); per-route constraints reuse .authorize({...}). Per-route auth handling has three modes via http({ auth: "required" | "optional" | "skip" }): secure-by-default "required", "optional" (admit anonymously, attach principal when a valid credential is present, reject invalid credentials), and "skip" (bypass the middleware entirely for truly identity-free routes like RSS or probes). Built-in /health, /ready, and /openapi.json endpoints register automatically. Each is configured via the uniform http: { builtins: { health, ready, openapi } } block with { enabled, requireAuth } per endpoint (Spring-Actuator-inspired). Defaults gate the routes count on /ready from anonymous callers (requireAuth: true) and keep /openapi.json public (requireAuth: false, matching the Stripe / GitHub / Twilio convention). Request bodies are parsed by Content-Type (JSON / text / urlencoded / multipart), capped by maxBodySize. Adds error codes RC5018 (request rejected) and RC5019 (server bind failed). Breaking: the destination option type HttpOptions<T> is renamed HttpClientOptions<T> (the source uses HttpServerOptions); a type-only change with no runtime impact.
  • CSV and JSONL decode transformers -- calling csv() or jsonl() with no path now returns a transformer that parses a CSV / JSONL string already in the body (for example an http() response), matching the existing json() and html() decode transformers. Adds CsvTransformerOptions, CsvFileOptions, and JsonlTransformerOptions; csv()'s path is now optional. A dynamic (function) path used as a destination now works for html() too, where it previously threw at construction.
  • xml adapter -- read, write and transform XML through a plain-object representation, mirroring the json and csv codec adapters. fast-xml-parser loads as an optional peer. See the xml reference.
  • directory adapter -- scan a directory and list its entries as a source, or pull a listing in mid-route with .enrich(). Emits one exchange with the full listing by default, or one per entry with chunked: true. See the directory reference.
  • CSV appends no longer splice records together -- appending a,b and then c,d through .to(csv({ append: true })) used to write a,bc,d. Appends are also serialised per path, so concurrent writes can no longer both emit the header.
  • Signed webhooks on the http() source -- http({ signature }) verifies the raw request bytes before the route runs, covering the GitHub, legacy HMAC-SHA1 and Stripe timestamped schemes, and http({ rawBody: true }) exposes those bytes for any other scheme. Adds RC5039. See securing capabilities.
  • /openapi.json never advertises a workspace container -- when the nearest package.json is a monorepo root, auto-detection serves the neutral fallbacks instead of the container's private, often stale identity. Apps run from their own directory are unaffected. See the http reference.

Mail

  • Mail source envelope moves to headers Breaking -- .from(mail(...)) now follows the payload-on-body, envelope-on-headers convention shared with the HTTP source. The exchange body is a MailBody ({ text?, html?, attachments? }) and the envelope (from, to, cc, bcc, subject, date, messageId, replyTo, flags, sender, rawHeaders) lands on routecraft.mail.* headers, declaration-merged into RoutecraftHeaders and exported on the MailHeaders key object. .input({ body }) now validates against the message content alone. The fetch destination (.enrich(mail(...))) still returns MailMessage[] unchanged. New exported type MailBody.
  • Direct mail no longer misclassified as auto-forwarded -- a single first-hop ARC seal (i=1, cv=none) added by the delivering MX is no longer read as forwarding, so DMARC-aligned direct mail stays direct / verified instead of unverified. Mailing-list and validated-forward classification are unchanged.
  • Connection recovery covers every failure path, and is configurable -- IDLE-mode fetch failures and the initial connect at route start now go through the same reconnect-with-backoff loop as IDLE drops and poll fetch failures (previously they killed the route), and the folder is drained right after a reconnect so mail that arrived during an outage is delivered immediately. New reconnect: { maxAttempts?, baseDelayMs?, maxDelayMs? } | false option on MailServerOptions (defaults match the old hardcoded 30 / 1s / 60s; maxAttempts: Infinity never gives up; false fails fast). Because the initial connect retries, the source signals readiness before the first connection succeeds, so route:started no longer guarantees the mailbox was reachable. New exported type MailReconnectOptions. When any source gives up for good, the new core route:source:failed event fires with { routeId, route, adapter?, error } so a dead channel can be alarmed on (#425).
  • Threading and custom headers on the send payload -- inReplyTo, references and headers, so agent replies stitch into the original email thread.
  • IMAP operations report their metadata again -- move, copy, delete, flag, unflag and append lost theirs when the role model split the observability hooks.

Packages Breaking

  • @routecraft/prettier-plugin-routecraft -- new package, formatting DSL chains compactly so a route reads as one shape rather than one operation per line. See formatting.
  • agentBrowser() moves to @routecraft/os -- browser automation folds into @routecraft/os and the standalone @routecraft/browser package is deprecated. Update imports; the factory, options and result shape are unchanged.

Docs site

  • Blog at /blog -- Markdoc-backed posts with a featured + latest layout.
  • Cheat sheet at /cheat-sheet -- searchable single-page DSL reference, print-to-PDF friendly.
  • 0.5.x to 0.6.0 migration guide -- step-by-step upgrade notes for every breaking change above.

v0.5.0 Pre-release

May 2026

Several breaking changes across the core, AI, mail, telemetry, logger, and CLI surfaces. See the 0.4.x to 0.5.0 migration guide for the full public-API diff and step-by-step upgrade notes.

Core

  • Dual-mode wrapper pattern -- .error() becomes a route-level wrapper rather than a top-level method, and source-level parse errors flow through the same handler.
  • Immutable Exchange -- the Exchange is frozen with explicit copy-on-write; state is unified on { body, headers }.
  • .authorize() route-entry guard -- a route-only authorization validator that replaces requirePrincipal and raises RC5020 when a credential expires mid-run.
  • Field-shaping helpers keep and mask -- two .transform() helpers: keep is grant-based, fail-closed allowlisting; mask obfuscates values regardless of caller.
  • Choice operation -- a conditional routing primitive with transform() and enrich() on branch builders.
  • Discovery metadata on the route builder -- route id, description, and validation move from source options to the builder.

AI & MCP Breaking

  • Agent runtime -- tool-calling loop, streaming via onEvent / onDelta, agent destination, and per-binding tool description overrides.
  • tools() DSL -- declarative tool registration, selection, and resolution.
  • Agent configuration overhaul -- agentPlugin.agents is a record (no defineAgent), and system / user accept a string or function. See the migration guide.
  • MCP OAuth 2.1 server -- OAuth 2.1 provider with principal hierarchy, plus a general MCP HTTP auth surface and tool annotations.
  • MCP protected-resource metadata -- resource identity moves to mcpPlugin({ title, resource }); both validator and OAuth-proxy modes auto-mount RFC 9728 metadata. Field-by-field moves are in the migration guide.
  • Plugin-level userinfo enrichment -- mcpPlugin({ userinfo }) hydrates the principal after verification, enabling the WorkOS AuthKit pattern. Lives on the plugin, orthogonal to the auth mode.
  • ClaimMappers.{email,name,roles} removed -- superseded by userinfo enrichment; the token-level mappers remain.
  • New error codes RC5020-RC5022 -- token expired during processing, principal enrichment failed, and userinfo sub invariant violated.

Adapters

  • Adapter mocking -- mockAdapter swaps any tagged adapter in tests; the file, csv, json, jsonl, and html factories are tagged out of the box.
  • direct<TIn, TOut>() distinct types -- a route can accept one body shape and emit another.
  • Mail (IMAP) reliability -- reconnect on transient fetch failures, a reshaped MailMessage body, and a verify-sender option.
  • Optional peer loader everywhere -- every optional-peer import now routes through loadOptionalPeer and emits RC5017 with an install hint.

Telemetry Breaking

  • Bun-only SQLite sink -- the built-in telemetry sink uses bun:sqlite; better-sqlite3 is removed. Node deployments that relied on it must bring their own sink.

Logger

  • stdout default -- the logger writes to stdout instead of stderr.

CLI & Tooling

  • Bun-only craft CLI -- the published binary now requires Bun >= 1.1.0.
  • Bun monorepo -- installs, scripts, and lockfile migrate from pnpm to Bun.
  • create-routecraft refactor -- scaffolder extracted into a library with expanded test coverage.
  • bun:test everywhere -- the internal suite migrates off vitest, retained only for the cross-runtime tests.

Docs

  • Migration guide -- new 0.4.x to 0.5.0 migration guide.
  • Canary docs at /next/ -- canary builds deploy alongside the stable build at the root.
  • Operator reference -- log and debug documented; map and schema clarified.
  • Claude Code skills -- Agent Skills for authoring adapters and capabilities bundled at the repo root.

v0.4.0 Pre-release

March 2026

Adapters

  • Cron source -- new adapter for scheduling capabilities with cron expressions.
  • JSONL adapter and chunked mode -- read and write line-delimited JSON with chunked streaming for large files.
  • Modular adapter structure -- adapters refactored into a consistent file layout with a unified DSL registration system.
  • Merged options -- cron and direct adapters now support merged options across config and route.

AI & MCP

  • stdio MCP client -- spawn and manage stdio-based MCP servers with a unified tool registry.
  • Bearer token authentication -- secure MCP HTTP transport with bearer tokens.

Framework

  • Terminal UI -- new TUI for inspecting running contexts and routes.
  • Reduced public API surface -- internal-only exports are no longer published, tightening the long-term API contract.

TypeScript

  • Declaration-merging registries -- compile-time adapter safety via type registries that adapter packages can extend.

Testing

  • Spy adapter assertions -- richer assertion helpers in @routecraft/testing for spying on capability output.

Docs

  • Light mode -- hero section and syntax highlighting now respect light mode.
  • Copy-to-clipboard -- code blocks gain a copy button.
  • Community resources -- new section linking external content and contributors.
  • Dark-mode contrast -- prose strong text is more readable on dark backgrounds.

v0.3.0 Pre-release

March 2026

Adapters

  • Agent, embedding, and LLM adapters -- new adapters for integrating AI agent workflows, embedding models, and large language models directly into capabilities.
  • HTTP adapter -- first-class HTTP source and destination support.
  • Browser and HTML adapters -- interact with web pages and parse HTML content.
  • JSON adapter -- dedicated adapter for JSON data sources.
  • Grouping adapter -- group messages by key before forwarding.
  • File adapter -- read and write text, JSON, and CSV files with a unified adapter.

AI & MCP

  • @routecraft/testing package -- expanded testing utilities with MCP integration support.
  • Consistent adapter pattern -- all adapters now follow a unified pattern for configuration, lifecycle, and error handling.

Events

  • Hierarchical event model -- new operation-level events with parent-child relationships, enabling fine-grained observability across capability execution.

TypeScript

  • TypeScript support -- author capabilities in TypeScript with full type inference and compile-time validation.

Docs

  • Capability-centric terminology -- all documentation renamed from "routes" to "capabilities" for consistency.
  • Advanced guides -- new documentation covering advanced patterns, capability composition, and adapter authoring.

v0.2.0 Pre-release

February 2026

AI & MCP

  • New @routecraft/ai package -- MCP integration with full schema validation via Zod. Expose any capability as an MCP tool for Claude Desktop, Cursor, and other MCP clients.
  • MCP server support -- run your capabilities as an MCP server with a single CLI command.
  • MCP client support -- call external MCP servers from within a capability using the mcpPlugin.

Adapters & Operations

  • direct adapter validation -- improved validation and error messages for inter-capability communication.
  • aggregate operation -- default aggregator now flattens arrays and combines scalars automatically.
  • batch operation -- new ESLint rule (batch-before-from) enforces correct batch positioning at the route level.
  • pseudo adapter -- new adapter for stubbing sources and destinations in tests and local development.

Framework

  • Cross-instance identity -- supports multiple package copies and npx-based installs resolving to the same context identity.
  • Logging configuration -- enhanced logging setup with more control over levels and output format.

v0.1.1 Pre-release

November 2025

Quality-of-life improvements.

Adapters

  • Custom log messages -- adapters and operations now support custom log message overrides.
  • Fetch adapter -- automatically parses JSON responses, no manual parsing needed.

Framework

  • .env.local support -- environment variables in .env.local are loaded automatically alongside .env.

Tooling

  • create-routecraft -- project scaffolding now supports example selection and template file configuration.
  • CodeSandbox -- added online playground link in the installation docs for zero-install experimentation.

v0.1.0 Pre-release

October 2025

Initial release.

Framework

  • Fluent DSL -- craft().from().to() builder syntax for authoring capabilities.
  • Core operations -- transform, filter, enrich, aggregate, split, validate, tap, process, header, and more.
  • Backpressure -- simple and batch consumers with built-in backpressure support.
  • CraftContext -- route lifecycle management with hot reload in development.
  • Error handling -- structured RC error codes with Pino logging.

Adapters

  • Built-in adapters -- simple, timer, direct, log, noop, fetch.

Tooling

  • CLI -- craft run and craft watch commands.
  • create-routecraft -- project scaffolding tool.
  • ESLint plugin -- require-named-route rule out of the box.
  • Test utilities -- @routecraft/testing package with testContext and spy adapter.
Previous
Installation