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-leveland--log-filetake 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
startshows its greeting again --create-routecraft0.7.0 rancraft startat the defaultwarnlevel, which hides the sample'sinfogreeting; the script is nowcraft start --log-level info. LOG_LEVELandLOG_FILEwork from env files --.env.<profile>, a profile'senv: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 --
craftexits2naming the path and where it came from (a flag,LOG_FILE, orcraft.log.js/.cjs) instead of running on with its logs diverted to the temporary directory. See basic usage. - A forced
--onceshutdown names the right option -- its hint said to raiseshutdown.timeoutMs, which 0.7 refuses; it now saysshutdown.timeout. See shutdown. - A named env file that cannot be read stops the command --
--env <path>or a profile'senv: "<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 exits2naming the path. A.envcascade 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 startcarried them,agent-browserwith Playwright and WebdriverIO included. A project that usescron(),mail(),html(),xml(),csv(),carddav(), JWKS verification, OpenTelemetry tracing,shell()oragentBrowser()now lists the package itself, and one that does not fails withRC5017and the install command, when the route starts or the first time the adapter runs. Each adapter's reference page now names its package. craft runruns a file outside any project -- with nonode_modulesabove 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
craftin a production install --create-routecraftlisted@routecraft/clias a dev dependency, sobun install --productionleft an image without the binarycraft startneeds. A project scaffolded earlier runsbun remove @routecraft/cli && bun add @routecraft/cli, since a barebun addkeeps the entry underdevDependencies.
Core
defineConfigrejects keysCraftConfigdoes 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 aCraftConfigannotation reports. The helper returnsCraftConfigrather 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 watchingroute:step:*orroute: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 theircontext:errorand the events of routes they call throughdirect(), are no longer delivered to it; its lifecycle events still are. See the event adapter. - Browser preflights name
Authorization--Access-Control-Allow-HeadersisAuthorization, *. The Fetch spec leavesAuthorizationout 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.urlfails withRC5003-- on the HTTP transport, a missing or plain-http URL outsidedevelopmentandtest, and an unparseable one in any environment, threw a bareTypeError; it now carries a code, a docs link, and the fix (NODE_ENV=developmentfor local work, an HTTPS URL behind a proxy). The stdio transport, which ignoresresource, 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
Originor an unknownHostwith 403 untilbrowserOriginsorallowedHostnameslists them, which the guide did not say. See servers and mounts. - The 0.5 to 0.6 migration guide covers the spy logger --
t.loggerstopped beingvi.fn()-based in 0.6, so runner matchers needtestContext({ fn }). - Event names match what the framework emits -- reference pages, the events namespace map and JSDoc still used bare
exchange:*,step:*anderror:caughtnames 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 theMssuffix is gone --intervalMsisinterval,timeoutMsistimeout,backoffMsisbackoff,jitterMsismaxJitter,shutdown.timeoutMsisshutdown.timeout, and so on acrosstimer,cron,http,mail,.retry(),.circuitBreaker(),.debounce(),.batch(),.sample(),.timeout(),.delay(),defineIndicator,mcpPluginand@routecraft/testing. Existing numbers keep working under the new names,'1m'is now accepted everywhere, and a stale name fails the build withRC5003naming its replacement instead of being silently dropped. See the migration guide. - Named servers replace listener options --
http.port/http.hostandmcp.port/mcp.hostare removed; declare listeners underserversand select one by name. Listener events areserver:listening/server:failed/server:closed, andplugin:mcp:server:listeningis gone. The per-routehttp({ auth })option is removed: a mount decides authentication, and oneauthvocabulary (unset inherits the server validator, a config walls,falseopens) applies to http, mcp and ops.mcpPlugin({ transport: 'http' })requires an explicit HTTPSresource.urloutsidedevelopmentandtest. 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 after72hunlessdeferral: { defaultTtl: 'never' }says otherwise. See the migration guide. - Lifecycle -- a context is single-use (
start()afterstop()refuses withRC1004);context.stop()resolves{ forced, pending }andcontext:stoppedcarries the same;plugin:startednow marks the newstart()phase done and the apply-phase events areplugin:applying/plugin:applied;Route.signalfires only when in-flight work is abandoned andRoute.intakeSignalcarries the old meaning. Shutdown drains in-flight work before abandoning it, bounded byshutdown.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 whoseSTOPSIGNALisSIGQUITgets 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 thanRC5012. 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 -- raisemaxBodySizeon a call that moves more; exceeding it fails withRC5061. Thebodyoption's type is narrowed toHttpRequestPayload, which only stopsbigintandsymbolbodies compiling.timer({ exactTime })andtimer({ timePattern })are removed -- neither worked; usecron()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
RC5048and re-asks its approver, so settle those before upgrading. Zod digests are byte-identical. ops.auth: falseis meaningful -- it removes the wall from the ops mount instead of throwing, which closes thehealth.detailsgate.
Core
- Durable defer and resume --
.defer()defers an exchange in a store (SQLite by default,memoryfor tests, or aDeferralStoreof your own) and answers the caller with aDeferredacknowledgment;.resume()revives it by signed token from any transport at the same position, with.resume({ authorize })as the one policy point,metatravelling with the deferral, per-call credentials throughtokenFor, 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 --
/healthwith liveness and readiness,defineIndicatorfor 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, andGET /ops/eventstailing 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 ascraft ops deferrals.DeferralStoregainslist()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 withRC5066naming the member rather than answering an empty page.- Remotes --
defineConfig({ remotes })makes another instance's dispatchable routes direct endpoints here, named git-style (hellofor the default remote,lab:hellofor 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, andcontext.reevaluateEnablement()brings it up without a restart. See enabled.- Authorization and identity --
authorize()gainsanyScope(any one of a set) andeffective(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;timingSafeStringEqualis exported for custom validators. - Plugins gain a
start()phase running after every route is ready,teardownreceives{ partial, started },build()unwinds applied plugins when a later step fails, a boot summary line reports what started, and astop()racing boot waits for the in-flight hook. - Exports --
isStandardSchema,validateAgainst,isDropped,wasOutputValidated,parseDuration,anySignal,isRedirect,createOpsHttpClientandcontributeOpsIndicator. - 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}/exchangeswhose 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 failingRC5004on 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 -- anAsyncIterablebody answers as Server-Sent Events and aReadableStreampasses through;respondacknowledges a webhook before the pipeline runs;signature: { scheme: 'standard-webhooks' }verifies Resend, Bird and Svix deliveries;maxStreamingRequestsandidleTimeoutbound a listener. See the http reference.http()client:maxBodySize,redirectandresponseBody: '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 (unshareon Linux, ordockeras a throwaway container per command), with a granted rather than inherited environment anduntrusted()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()andcron()takemaxJitter, and.throttle({ per })accepts aDuration.
AI & MCP Breaking
currentTime()andrandomUuid()are removed -- declare the fn inline under the same tool name; everytools([...])reference keeps working. See the migration guide.FnHandlerContext.checkpointIdisdeferralId, anddeferis a required member, so a hand-built handler context must provide one.- Agent files --
agents/is walked recursively and identity comes from frontmattername(the filename no longer has to match), duplicate names throw, andskills: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 advertisedoutputSchemais enforced (AI2001); primitive and array outputs now carrystructuredContentin the protocol's envelope;McpIcon.sizesis readonly and the default icon set carries a PNG per theme. - Canary-only -- the
surface()params callback dropssessionId, the SQLite session schema is version 2 and discards canary rows, andstartedByon the sessions resource isowner.
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 advertisesoneOf: [Output, Deferred]; a cancelled run fails withAI1005instead 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: truecancels the running turn, asessionsstore (SQLite by default) holds the records, andGET /ops/agent-sessionslists 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:ondefineConfig(oracpPlugin()) 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 startand 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 throughregisterProjectDiscoverer. See project structure.- Model controls --
reasoningandproviderOptionsonllm()andagent(), the full sampling block onagent()andagentPlugin({ 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 --
replaceDeferredThreadrewrites a deferred agent's thread in place (AI1008refuses a malformed one), andAI1009names 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-flashis offered by autocomplete; a secondagentPlugininstall'sdefaultOptionsapply in full; twotools()entries for one tool compose their guards instead of replacing.
CLI Breaking
craft chatis removed -- an editor speaking the Agent Client Protocol is the interactive interface; seecraft acpbelow.
CLI
craft start,craft exec <route>andcraft ops health | ready | routes | deferrals | indicatorsboot a project and drive a running instance;craft acpis 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 theunshareanddockerisolation tiers;shell({ timeout })takes aDuration.@routecraft/eslint-plugin-routecraft--require-untrusted-shell-argswarns on a shell argument that should be markeduntrusted().@routecraft/testing-- at.contextLoggerspy,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'spackage.jsoninstead of replacing it, accepts a/tree/<branch>URL without a subpath, and matches the copy filter on path segments.- Peer ranges --
@routecraft/ai,@routecraft/osand@routecraft/testingadmit 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
- 0.6.x to 0.7.0 migration guide -- every breaking change above with its before and after.
- New pages -- durable agents, talk from your editor, project structure, and reference pages for the ops, servers, acp and remotes plugins and the shell and surface adapters.
- Every TypeScript example compiles -- the docs build compiles every fenced block against the workspace packages, and a block that is not meant to compile says why.
Known limitations
- Plugin ordering on the array path --
plugins: [acpPlugin()]must be listed after theagentPlugin()that registers the agents it serves, or the build fails withRC5003saying so. Theacp: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:failedbecome a fixed set (route:exchange:failed) withrouteIdindetails. Wildcard patterns onctx.on()/ctx.once()are replaced by exact names, the"*"catch-all, and theforRoute()filter helper (theevent()source adapter keeps its pattern support); ecosystem packages declare events by merging intoEventDetailsMap.plugin:registeredis removed (it duplicatedplugin:starting). Subscriptionsource contract -- source adapters receive a singleSubscriptionobject ({ context, signal, meta, ready(), complete(), emit() }) instead of five positional parameters..from()additionally accepts async generator functions and (async) iterables, and@routecraft/testingadds atestSubscription()helper.StepOutcomestep contract -- customStepimplementations 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 sharedStepinstance. Custom aggregators return{ body, headers? }instead of a fabricatedExchange.- Namespaced error-code registry -- ecosystem packages own codes under a claimed namespace via
registerErrorCodes()plusErrorCodeRegistrydeclaration merging;RCis reserved for core. AddsRC1003(error-code registration failed). - Type-enforced builder positioning --
craft()returns a pre-frombuilder, so pipeline operations before.from()are compile errors; builder generics move to a state bag (RouteBuilder<{ body: T }>,AnyRouteBuilderfor lists). - Splitters return child bodies --
.split()callbacks return values (orsplitChild(body, headers)for per-child header overrides) instead of hand-builtExchangeinstances; the framework owns child construction. - Consumer SPI --
Consumer.registerreceives theMessageenvelope and consumer classes construct from oneConsumerDepsbag;Message,ProcessingQueue,ConsumerType, andConsumerDepsare exported. - Per-adapter header key objects --
HeadersKeyskeeps framework keys only; adapter keys move toTimerHeaders/CronHeaders/FileHeaders/CsvHeaders/JsonlHeaders/MailHeaders/CarddavHeaders;HEADER_MAIL_*/HEADER_CARDDAV_*andHeaderKeysRegistryare removed (wire keys unchanged)..header()rejects every engine-ownedroutecraft.*key up front. client.sendDirectand public capability discovery --CraftClient.sendis renamedsendDirect(response generic defaults tounknown);context.capabilities()replaces reads of the internal direct registry, andADAPTER_DIRECT_REGISTRY/getDirectChannel/sanitizeEndpointare no longer exported.- Naming sweeps --
CardDAV*exports becomeCarddav*(acronym casing per theHttpprecedent), the carddav option types adopt the two-sided Server/Client naming (CarddavServerOptionsfor the read role,CarddavClientOptionsfor writes and deletes), and jsonl's three file option types fold into oneJsonlFileOptions. choice()variadic surface;BranchBuilderrenamed,ChoiceSubBuilderremoved -- the fluent callback.choice(c => c.when(p, fn).otherwise(fn))becomes variadic.choice(when(p, fn), ..., otherwise(fn))with standalonewhen/otherwisehelpers imported from@routecraft/routecraft, the path surface now shared with the newmulticast.BranchBuilderis renamedPathBuilder;ChoiceSubBuilderis gone.- Adapter role model:
Source/Destination/Enricher--Destination.sendis now strictly void and the newEnricher.fetchpulls 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/osand@routecraft/testingraise 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()andnone()still merge, andreplace()is deleted. See the migration guide.- File-family adapters drop
mode--file,csv,json,jsonl,xmlandhtmltake their role from the operation keyword, withappend: true/delete: trueselecting send behaviour. Two silent flips to audit:jsonlsends now overwrite by default, and a migrated.tap(json({ path }))writes wheremode: 'read'used to read and discard. See the migration guide. MailSendResult,CarddavWriteResultandCarddavDeleteResultdeleted -- sends that produce a receipt now surface it onroutecraft.mail.*androutecraft.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-undefinedpathon the codec adapters now throwsRC5003instead 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 emitroute:exchange:dropped, they take the normal error path..retry({ exponential })removed in favour offactor-- migrateexponential: truetofactor: 2andexponential: falsetofactor: 1, the new default; the old option throwsRC5003at build with a hint. See the retry reference for the backoff, cap and jitter options.authorize()defaults toactor: 'none'--Principalbecomes delegation-aware (RFC 8693act/may_act) and the newdelegate()helper mints delegated principals, so a principal carrying an actor, including a Clerk impersonation session, is rejected until the route declares its permitted actors. AddsRC5034toRC5038. See the migration guide and securing capabilities.
Core
- Recovery directives --
.error()handlers may returnrecovery.drop(reason?)(discard the failing exchange) orrecovery.rethrow()(decline recovery) instead of a recovery body or a manual throw. - Open error and principal models --
rcErroraccepts a per-occurrenceretryableoverride;RCMeta.categoryandPrincipal.kindaccept ecosystem-defined strings alongside the known values. - Plugin identity and lifecycle -- plugins may declare
name(used aspluginIdon events and logs) and reservedependsOn;registerTeardowncallbacks unwind LIFO;getRoutes()returns a copy. route:source:failedlifecycle event -- fires when a source subscription rejects (the source gave up producing), with{ routeId, route, adapter?, error }. Unlikeroute:stoppingit 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.dispatchload-balancing operation --.dispatch(strategy, ...targets)runs exactly one of several targets by failover, round-robin, weighted or sticky strategy, the sibling ofmulticastandchoice. See the dispatch reference.debounceflow-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.sampleanddedupeflow-control operations --sample()passes every Nth exchange or the first in each time window, anddedupe()suppresses duplicates by a derived key. Both drop silently, like afilterreturning false. See the sample and dedupe references..timeout()propagates anAbortSignal-- an expired deadline now cancels the wrapped step instead of leaving abandoned work running in the background, andhttp()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()andjwks()surfaceclockToleranceSec-- a consumer re-checking a verified principal'sexpiresAtcan now see the skew the verifier allowed. The expiry boundary is also inclusive everywhere, matching jose and RFC 7519, wherejwt()previously honoured an expired token for one further second. See the migration guide.
AI & MCP Breaking
-
AI error codes renamed --
RC5025/RC5026/RC5027becomeAI1001/AI1002/AI1003under the newAInamespace; update any code or alerting that matches onerror.rc. -
Agent blocks replace skills --
AgentOptions.skillsandagentPlugin({ skills })are removed in favour of ablocksrecord that unifies skills, memory, identity, and instructions, with progressive disclosure now the default. -
skills({ source })andfromFile(path)builders --skillsnow returns ablocksrecord to spread intoblocks: { ... };fromFilereads a UTF-8 file at resolution time. -
Nested block groups -- a
blocksvalue can be a single block or a nestedblocksgroup, soskills({ source })can stay grouped under one key (blocks: { skills: await skills(...) }) instead of being spread flat. Groups flatten togroup__leafnames. -
Tag selectors on
tools()removed -- the{ tagged }/{ tagged, from }variants and thetagsoverride ondirectToolare gone. Use the newtools((catalog) => [...])builder form for dynamic selection. -
Block-loader calls partitioned out of
toolCalls-- progressive loads surface onAgentResult.blocksLoadedand emitagent:block:*events instead ofagent:tool:*. -
skills:frontmatter onagents()rejected -- supplyblocksthrough 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 becomedirect__<routeId>and_block__load__<name>. Fn ids andmcp__<server>__<tool>are unchanged, and theDirect(...)/MCP(...)authoring grammar does not change. Update anything pinning a generated name: tool-name guards, assertions ontoolCalls[].toolNameorblocksLoaded[].toolName, recorded transcripts, evals. See the migration guide. -
ResolvedTool.sourceis a new required field -- a resolver-setfn/direct/mcp/blockdiscriminant. Affects only code that hand-constructs aResolvedTool. -
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 raisesRC5003naming the offending character or length, rather than being rejected by the provider later. Expose an unsafe route id under a tool-safe alias withdirectTool(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 --
mcpPluginbuilds a fresh server per request, so any replica can answer any request behind a plain load balancer. Sessions, their events andMcpHeadersKeys.SESSIONare gone, and the@modelcontextprotocol/sdkv1 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. Passissuerinstead. See the migration guide. -
MCP client names reject
__and a trailing_-- such a name composed an ambiguousmcp__<server>__<tool>that resolved to the wrong tool, silently, somcpPlugin()now throwsRC5003at 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), eachtrue,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:deniedevent -- emitted once per tool refused admission by a policy, carryingagentName,toolName,toolKind, and areasonofrule,rule-error, orunknown-provenance, so denials are alertable and auditable rather than only logged.mcpPlugin({ proxy })re-exposes client tools -- proxy tools from registeredclientsthrough the Routecraft MCP server without a route per tool, with an optional per-toolguard. 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.baseURLhonoured by the Anthropic and Gemini providers -- previously only OpenAI honoured it, so explicit config lost to the ambientANTHROPIC_BASE_URLenvironment variable.
Internals
- Engine restructuring --
CraftContextdelegates events to an internalEventBus; adapter config keys (cron,direct,mail,telemetry,http) move to per-module config appliers; the route engine splits intopipeline/modules (executor, validation, synthetic steps). Two behavioural notes: context store seeding for adapter config now happens ininitPlugins()(called automatically bystart()), and plugin teardown (includingregisterTeardowncallbacks) drains in reverse order. - Uniform factory tagging -- every public adapter factory is tagged for
mockAdapter(), enforced by a conformance test; previouslydirect,simple,timer,cron,log,noop, and others (plus two transformer-mode branches ofhtml()/json()) were silently unmockable. - Every optional peer loads through
loadOptionalPeer-- the mail drivers andagentBrowser()now surface a missing package asRC5017with 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
sideEffectsallowlist let esbuild prune every core config applier out of the published bundle, sodefineConfig({ mail: { accounts } })typechecked but never applied at runtime. A post-build guard now asserts every applier is live in the registry, and an unrecogniseddefineConfigkey warns instead of being a silent no-op. @routecraft/ai,@routecraft/osand@routecraft/testingdeclare 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/cliandcreate-routecraftmove to their newest in-range minor and patch releases, and core's optionalfast-xml-parserpeer floor rises to^5.10.1to exclude a DOCTYPE entity-expansion advisory. No majors are included, and theimapflowfloor is held back deliberately.
Adapters
- HTTP source Breaking --
http()is now a two-sided adapter.http({ path, method? })exposes a route over HTTP viadefineConfig({ http: { port, host, auth } }); Bun runtimes bind throughBun.serveand Node 22+ uses a zero-dependencynode:httpshim. Global auth acceptsjwt()/jwks()bearer orapiKey({...}); per-route constraints reuse.authorize({...}). Per-route auth handling has three modes viahttp({ 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.jsonendpoints register automatically. Each is configured via the uniformhttp: { builtins: { health, ready, openapi } }block with{ enabled, requireAuth }per endpoint (Spring-Actuator-inspired). Defaults gate theroutescount on/readyfrom anonymous callers (requireAuth: true) and keep/openapi.jsonpublic (requireAuth: false, matching the Stripe / GitHub / Twilio convention). Request bodies are parsed byContent-Type(JSON / text / urlencoded / multipart), capped bymaxBodySize. Adds error codesRC5018(request rejected) andRC5019(server bind failed). Breaking: the destination option typeHttpOptions<T>is renamedHttpClientOptions<T>(the source usesHttpServerOptions); a type-only change with no runtime impact. - CSV and JSONL decode transformers -- calling
csv()orjsonl()with nopathnow returns a transformer that parses a CSV / JSONL string already in the body (for example anhttp()response), matching the existingjson()andhtml()decode transformers. AddsCsvTransformerOptions,CsvFileOptions, andJsonlTransformerOptions;csv()'spathis now optional. A dynamic (function) path used as a destination now works forhtml()too, where it previously threw at construction. xmladapter -- read, write and transform XML through a plain-object representation, mirroring thejsonandcsvcodec adapters.fast-xml-parserloads as an optional peer. See the xml reference.directoryadapter -- 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 withchunked: true. See the directory reference.- CSV appends no longer splice records together -- appending
a,band thenc,dthrough.to(csv({ append: true }))used to writea,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, andhttp({ rawBody: true })exposes those bytes for any other scheme. AddsRC5039. See securing capabilities. /openapi.jsonnever advertises a workspace container -- when the nearestpackage.jsonis 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 source envelope moves to headers Breaking --
.from(mail(...))now follows the payload-on-body, envelope-on-headersconvention shared with the HTTP source. The exchangebodyis aMailBody({ text?, html?, attachments? }) and the envelope (from,to,cc,bcc,subject,date,messageId,replyTo,flags,sender,rawHeaders) lands onroutecraft.mail.*headers, declaration-merged intoRoutecraftHeadersand exported on theMailHeaderskey object..input({ body })now validates against the message content alone. The fetch destination (.enrich(mail(...))) still returnsMailMessage[]unchanged. New exported typeMailBody. - 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 staysdirect/verifiedinstead ofunverified. 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? } | falseoption onMailServerOptions(defaults match the old hardcoded 30 / 1s / 60s;maxAttempts: Infinitynever gives up;falsefails fast). Because the initial connect retries, the source signals readiness before the first connection succeeds, soroute:startedno longer guarantees the mailbox was reachable. New exported typeMailReconnectOptions. When any source gives up for good, the new coreroute:source:failedevent fires with{ routeId, route, adapter?, error }so a dead channel can be alarmed on (#425). - Threading and custom headers on the send payload --
inReplyTo,referencesandheaders, so agent replies stitch into the original email thread. - IMAP operations report their metadata again --
move,copy,delete,flag,unflagandappendlost 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/osand the standalone@routecraft/browserpackage 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
Exchangeis frozen with explicit copy-on-write; state is unified on{ body, headers }. .authorize()route-entry guard -- a route-only authorization validator that replacesrequirePrincipaland raisesRC5020when a credential expires mid-run.- Field-shaping helpers
keepandmask-- two.transform()helpers:keepis grant-based, fail-closed allowlisting;maskobfuscates values regardless of caller. - Choice operation -- a conditional routing primitive with
transform()andenrich()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.agentsis a record (nodefineAgent), andsystem/useraccept 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
userinfoenrichment --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 byuserinfoenrichment; the token-level mappers remain.- New error codes
RC5020-RC5022-- token expired during processing, principal enrichment failed, and userinfosubinvariant violated.
Adapters
- Adapter mocking --
mockAdapterswaps any tagged adapter in tests; thefile,csv,json,jsonl, andhtmlfactories 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
MailMessagebody, and a verify-sender option. - Optional peer loader everywhere -- every optional-peer import now routes through
loadOptionalPeerand emitsRC5017with an install hint.
Telemetry Breaking
- Bun-only SQLite sink -- the built-in telemetry sink uses
bun:sqlite;better-sqlite3is 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
craftCLI -- the published binary now requires Bun >= 1.1.0. - Bun monorepo -- installs, scripts, and lockfile migrate from pnpm to Bun.
create-routecraftrefactor -- scaffolder extracted into a library with expanded test coverage.bun:testeverywhere -- 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 --
loganddebugdocumented;mapandschemaclarified. - 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 --
cronanddirectadapters 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/testingfor 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/testingpackage -- 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/aipackage -- 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
directadapter validation -- improved validation and error messages for inter-capability communication.aggregateoperation -- default aggregator now flattens arrays and combines scalars automatically.batchoperation -- new ESLint rule (batch-before-from) enforces correct batch positioning at the route level.pseudoadapter -- 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.localsupport -- environment variables in.env.localare 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 runandcraft watchcommands. create-routecraft-- project scaffolding tool.- ESLint plugin --
require-named-routerule out of the box. - Test utilities --
@routecraft/testingpackage withtestContextandspyadapter.