Skip to content

Releases: warlockjs/framework

v5.41.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 29 Oct 16:39

Released 2026-10-29.

All 33 packages (@warlock.js/* and create-warlock) move to 5.41.0 together;
keep every family package on the same version. No breaking changes. This release adds
Mantine as a second UI kit for the design system, next to shadcn/ui.


1. Breaking changes

  • None. warlock add design-system without --kit installs the shadcn kit exactly as
    before.

2. Mantine UI kit (core, create-warlock, web)

  • Install: warlock add design-system --kit mantine (or warlock add mantine) in an app
    with web pages. It does not need Tailwind.
  • Same contract as the shadcn kit: the same component files and names under
    src/web/design-system/ (TextInput, EmailInput, PasswordInput, TextareaInput,
    SelectInput, MultiSelectInput, the date and time pickers, CheckboxInput, RadioInput,
    SwitchInput, FileInput, FieldFrame, SubmitButton), the same props and the same hooks.
    App code and generators work with either kit.
  • Forms stay on Mongez React Form. Every control binds through the shared hooks, and
    validation errors show through Mantine's error prop. @mantine/form is not installed or
    used.
  • Setup it writes: pinned @mantine/core, hooks, dates, notifications, modals and
    dayjs; postcss.config.mjs with postcss-preset-mantine; Mantine's stylesheets in
    src/web/app.css; and in src/web/root.tsx the colour-scheme script, mantineHtmlProps,
    and a provider that mounts MantineProvider, the modals manager and the notifications host.
    Existing files are kept, and a root with a custom shape is left alone with the lines to add
    printed.
  • Theme: src/web/design-system/mantine-theme.ts holds the design tokens you edit
    (primary colour, radius, fonts, spacing).
  • Extras: notify and notifyOnSubmit (toasts that plug into useSubmitForm and
    useSubmitAction), confirm() for confirmation dialogs, and PaginatedTable, which takes
    the paginated list response (rows plus pagination).
  • Kits exclude each other. Asking for one kit while the other is installed, or for both in
    one command, fails before anything is installed or written, with the steps to switch.

3. New projects (create-warlock)

  • --ui=<shadcn|mantine|none> chooses the UI kit of a web app; shadcn stays the default for
    --stack=web. --ui=mantine scaffolds without Tailwind.
  • The interactive stack menu has a "Full-stack web with Mantine" entry, and Customize lists
    Mantine among the rendering features.
  • The scaffold check in CI can create a Mantine or shadcn web project
    (npm run typecheck:scaffold -- --ui=mantine).

4. Upgrade notes

  1. Update the family. Run npx warlock update.
  2. Read what changed. Run npx warlock release-notes --from 5.40.1.
  3. Nothing to change for apps on the shadcn kit.
  4. To switch an app to Mantine, remove the shadcn kit's files and dependencies (the error
    message of warlock add design-system --kit mantine lists them), then run the command
    again. Your forms keep working because the component names and props are the same.

v5.40.1

Choose a tag to compare

@hassanzohdy hassanzohdy released this 29 Oct 15:52

Released 2026-10-29.

All 33 packages (@warlock.js/* and create-warlock) move to 5.40.1 together;
keep every family package on the same version. No breaking changes. This patch makes
create-warlock --stack=web work again and cleans up the design-system files it writes.


1. Breaking changes

  • None.

2. New web projects install their features again (create-warlock, core)

  • Since 5.35, create-warlock --stack=web (or any scaffold with web, tailwind, shadcn,
    design-system or sitemap) failed in the feature step: warlock add loads
    warlock.config.ts first, the web starter's config imports @warlock.js/web, and that
    package was only installed by the feature step itself. Every feature, even the database, was
    reported as failed and the project was left without them.
  • A web scaffold now lists @warlock.js/web in package.json from the start, and
    warlock add continues with a warning when the project config cannot load yet, so
    npx warlock add web also works on a project in that state.
  • CI now scaffolds and typechecks a web project as well as the API-only one.

3. Design-system files pass the app's lint (create-warlock, core)

  • The controls and hooks that warlock add design-system writes into src/web/design-system/
    failed the scaffolded Oxlint config, so warlock dev reported 13 lint errors in a fresh web
    app. They now pass, and a spec keeps it that way.

4. Upgrade notes

  1. Update the family. Run npx warlock update.
  2. A web app scaffolded on 5.35–5.40.0 without its features: run
    npx warlock add web (then any other features you chose, such as tailwind,
    design-system or your database) after updating.
  3. Lint errors in src/web/design-system/: rerun npx warlock add design-system to get the
    fixed files, or keep yours if you edited them.

v5.40.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 29 Oct 14:14

Released 2026-10-29.

All 33 packages (@warlock.js/* and create-warlock) move to 5.40.0 together;
keep every family package on the same version. No breaking changes. This release makes
warlock check finish on large apps, makes the development server steadier and quieter,
brings Windows up to a tested platform, and adds turn control, context editing, spend
limits and approvals to the AI agent loop.


1. Breaking changes

  • None. Two additions can surface in strict code; see the upgrade notes:
    FinishReason has a new member "tool_stop", and http.log: true now writes one summary
    line per request instead of one line per phase.

2. warlock check finishes on large apps (core)

  • On a large app the conventions step could run for tens of minutes at full CPU without
    printing anything. Two causes are fixed:
    • The .client boundary rule (WL9022) re-read, re-parsed and re-resolved every shared
      file for each controller, service, model and route it checked. Import chains are now read
      once per run, and files known to reach no .client module are never walked again.
    • Parsed syntax trees kept their parent links in shared lookup tables. With thousands of
      trees alive, memory cleanup stalled for seconds at a time. The links now live on the nodes,
      and the import rules reuse one parse per file.
  • On an app with about 7,500 source files, the conventions step now takes about 14 seconds.
  • Progress lines. warlock check prints one line to stderr when each step starts and
    ends, and the conventions step reports its milestones. stdout is unchanged, so --json
    still prints only the report.
  • Per-rule time budget. Each convention rule may use 30 seconds over a run. A rule that
    goes over is skipped for the remaining files and reported as a WL9026 warning, instead
    of hanging the command. Set ruleBudgetMs in .warlock-check.json; 0 turns it off.

3. A steadier, quieter development server (core, web, fs)

  • Spec and test files are never auto-loaded. Command discovery, main, routes,
    worker, events and locale files, and the standalone cascade migrate loader skip
    *.spec.* and *.test.* files, so a vi.mock() beside a command no longer breaks the CLI.
  • The first page after a restart waits for the compile. In development the page loader
    timeout defaults to 90 seconds and starts only after a reload in progress has settled.
    Production keeps 15 seconds; an explicit web.loaderTimeout still wins.
  • Smaller reloads. Importers that reach a changed file only through import type are no
    longer reloaded, only the changed files are re-read, and the +N modules count is per file.
    Adding files at boot no longer re-scans every tracked file.
  • Requests survive reloads. New requests wait while a reload runs; requests already
    running get up to 5 seconds to finish. A request held longer than 15 seconds gets a 503
    with Retry-After: 2. putJsonFile and the typings manifest are written atomically.
  • Module singletons. appSingleton(key, create, { dispose, disposeOnReload }) keeps one
    instance per process across reloads and across the copy that pages load through Vite, and
    disposes it on shutdown. A second supervisor() for the same registry and owner in one
    process returns the running supervisor instead of failing with a held lock.
  • Less log noise.
    • With web.moduleBoundary: "warn", each boundary violation prints once per dev session,
      then a count of repeats.
    • With http.log: true, each request writes one info line
      (GET /orders 200 12ms [request id]); the per-phase lines move to debug.
      http.logPhases: "info" restores the old output.
    • AI tool schema lint takes lint: { ignore: [codes] } per tool, in
      ai.config({ toolLint }) and on ai.mcp(server, { lint }), and prints each finding once
      per process.
  • App ready hook. Application.onceBooted() / whenBooted() runs after every connector
    has started; the docs now say when it fires.

4. Windows (process, core, fs, ai, web)

  • Process supervisor.
    • The guardian answers the resume request before it starts the program, so a slow start
      can no longer time out the ownership check.
    • observationTimeoutMs is a supervisor option: 2 seconds on Linux, 15 on Windows and
      macOS. The timeout error names the option.
    • pollIntervalMs defaults per platform: 100 ms on Linux, 500 ms on macOS, 1.5 s on
      Windows.
    • On Windows one long-lived helper answers every process lookup in batches, instead of two
      PowerShell processes per lookup, and the launch gate polls the guardian pipe directly.
  • Fixed on Windows: image variants answered 500 when the cache path went over 260
    characters; warlock typecheck, warlock check --changed and warlock inspect could not
    match the compiler's paths; the development model registry keyed a deleted model
    differently; end-to-end browsers ignored their profile; warlock release linked
    node_modules with a directory symlink, which needs administrator rights.
  • ai.snapshot.sqlite stores have close(), so their database file can be removed.
  • The framework's own test suites for fs, process, core, web and ai now run on
    Windows in CI.

5. AI agent loop (ai, ai-anthropic)

  • End a turn after a tool. stopAfterTools: [...], stopWhen(ctx) on the agent, or
    endsTurn on a tool ends the run with no further model call; the finish reason is
    "tool_stop".
  • Context editing. contextEditing: { clearToolResults } replaces old tool results with
    a placeholder before each trip, for every provider. Anthropic models use the provider's
    native clearing. It runs before compaction, which still summarises.
  • Prices and spend. ai.pricing({ tables }) builds one overridable price list from the
    provider tables. budget({ onSpend }) reports spend after every call, and
    scoped: { window: "all" } caps a session or a lifetime.
  • Approvals. ai.human.approval() takes per-tool modes (allow, deny, ask) and
    remember, so an approval can cover the rest of the session for that tool or those
    arguments.

6. AI providers and tools

  • xAI: calls that use native web or X search go through the Responses API; other calls
    stay on Chat Completions (api: "chat" | "responses" to choose).
  • OpenAI: a call with web_search is sent to the Responses API automatically; a model
    that cannot search fails with a clear error before any request.
  • Google: googleSearchBudget(n) stops sending search once n queries were used. The
    Gemini API has no per-call cap, so one running call can still go over.
  • Model listing: every first-party adapter implements models(), a live listing in one
    shape (ModelInfo).
  • Workspace patches: @warlock.js/ai-workspace adds an atomic apply_patch tool for
    unified diffs (modify, create, delete, rename), confined to the workspace.

7. Other fixes

  • Manual beginTransaction() covers the caller's queries (cascade). After
    await beginTransaction(), the queries the caller runs use the transaction until commit or
    rollback. Before, they ran outside it on MongoDB and on other pool connections on
    PostgreSQL. transaction(fn) is unchanged.
  • New API-only projects typecheck (create-warlock). A project created without web pages
    no longer receives src/config/web.ts, which imported a package it does not install.
  • New projects tell agents how to upgrade (create-warlock). The generated AGENTS.md has an
    "Upgrading Warlock.js" section: read npx warlock release-notes --from <current> (bundled with
    core, so it works offline), run npx warlock update, follow the upgrade notes, then
    npx warlock check. It also explains that each package's skills arrive as one router skill.

8. Upgrade notes

  1. Update the family. Run npx warlock update.
  2. Read what changed. Run npx warlock release-notes --from 5.39.2.
  3. Run npx warlock check. It should now finish on a large app. A WL9026 warning names
    a rule that ran out of time; raise ruleBudgetMs if your app needs more.
  4. Exhaustive FinishReason switches. Add a "tool_stop" case where you switch over
    every finish reason.
  5. Request logs. If you rely on per-phase request lines at info, set
    http.logPhases: "info".
  6. Replace app workarounds. Module-level singletons kept on globalThis can use
    appSingleton(). A pinned longer ownership timeout or poll interval on Windows can usually
    go. Specs moved away from command folders can come back.
  7. Review manual transactions. Code that calls beginTransaction() now runs its later
    queries inside the transaction, as the API always promised. Make sure each one commits or
    rolls back.

v5.39.2

Choose a tag to compare

@hassanzohdy hassanzohdy released this 27 Oct 21:17

Released 2026-10-27.

All 33 packages (@warlock.js/* and create-warlock) move to 5.39.2 together;
keep every family package on the same version. No breaking changes. This release lets
model.set() take an object and adds a check that catches a recurring deep-merge mistake.


1. Breaking changes

  • None. 5.39.2 has no breaking changes. merge() behaves exactly as before, and
    set(key, value) is unchanged.

2. model.set() accepts an object (cascade)

  • model.set({ ... }) sets several fields in one call. Each entry behaves exactly like
    set(key, value) for that key, with the same casting, dirty tracking and whole-field update.
  • One rule: set replaces, a dot path targets one leaf, and merge deep-merges. A
    top-level key replaces the whole field (objects and arrays are stored whole, keys the new
    value omits are removed when you save). A dot-path key such as "settings.theme" writes only
    that leaf and keeps its siblings.
  • It applies the same identity protection as merge(): on a persisted model, id, _id
    and the primary key in the object are ignored, so model.set(req.body) cannot retarget the
    save. Cascade has no guarded or fillable list, so choose which fields you pass.
  • Keys that overlap in one call (settings with settings.theme, or a.b with a.b.c) throw
    an error that names both keys, before anything is written.
  • In a typed model each key is a schema column, a system column or a dot path, and each value
    is checked against its column type. The method returns the model.
  • merge()'s editor documentation now says that nested objects are deep-merged and that
    removed keys are kept, and points to set({ ... }) and set(key, value).
// settings is stored as { theme: "dark", filters: { status: "open" } }
post.merge({ settings: { theme: "light" } }); // filters is kept
post.set({ settings: { theme: "light" } }); // filters is removed
post.set({ "settings.theme": "light" }); // only theme changes, filters is kept
await post.save();

3. warlock check flags merge() on object columns (core)

  • WL9025 (warning) reports a model.merge({ ... }) call whose object literal includes
    a column that the model's static schema declares as an object, record or array-of-objects
    validator. The message names the column, explains that removed keys are kept, and suggests
    set({ column: ... }) or set("column", value). It has no automatic fix, because the two
    methods behave differently.
  • Scalar columns, set(), spreads, variables, and merge() on objects that are not
    Cascade models are not reported. Test files are skipped.

4. Work scheduled inside a transaction no longer reuses its session (cascade)

  • A setTimeout, queue or fire-and-forget promise armed inside transaction() used to keep
    the transaction's session. After the transaction ended, a Cascade query in that work threw
    MongoExpiredSessionError on MongoDB, and on PostgreSQL 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: its queries are ordinary queries, afterCommit() runs on the next
    microtask, and a transaction() it starts opens a fresh transaction. The work is not rolled
    back with the transaction. For work that must run after the COMMIT, use afterCommit().
  • Covers transaction() on both MongoDB and PostgreSQL and MongoDB's beginTransaction(),
    including retried MongoDB attempts. The "Work scheduled inside a transaction" section of the
    transactions guide describes the behavior.

5. Upgrade notes

  1. Update the family. Run npx warlock update.
  2. Read what changed. Run npx warlock release-notes --from 5.39.1.
  3. Run npx warlock check. For each WL9025 warning, decide whether the update should
    replace the column. If it should, use set({ column: value }) or set("column", value).
    If merging nested keys is intended, keep merge().
  4. Replace app helpers (optional). A helper that calls set() once per column can become
    model.set(values).
  5. Keep afterCommit() workarounds. Code that already moves late work into afterCommit()
    needs no change; it is still the right tool for work that must follow a COMMIT.

Warlock.js 5.39.1

Choose a tag to compare

@hassanzohdy hassanzohdy released this 26 Oct 20:47

Released 2026-10-26.

All 33 packages (@warlock.js/* and create-warlock) move to 5.39.1 together;
keep every family package on the same version. No breaking changes. This is a fix
release for test-run exits, migrations, standalone builds and log channels.


1. Breaking changes

  • None. 5.39.1 has no breaking changes.

2. Test runs exit promptly (core, scheduler)

  • Running schedulers are stopped on shutdown. An app that calls scheduler.start()
    while its modules load (the stock template does this) kept the Vitest main process
    alive after the HTTP test server stopped. Vitest then waited out its 10-second close
    timeout and printed something prevents N Vite servers from exiting. The shutdown of
    the HTTP test server, warlock start and per-worker test teardown now stop every
    running @warlock.js/scheduler scheduler within app.shutdownTimeout.
  • scheduler.shutdown() clears its own cap timer when the jobs finish first, so it
    no longer holds the process open for up to 30 seconds.
  • If the message still appears, a handle the app opens itself (an interval, a socket, a
    queue worker) is keeping the process alive. npx vitest run --reporter=hanging-process
    names it.

3. Migrations close cleanly and never stall silently (core, cascade)

  • warlock migrate releases the migration lock and closes every data source before
    it exits: on success, on failure, with --pending, and on Ctrl+C / SIGTERM (exit
    130 / 143). Shutdown has a bounded wait, so it cannot hang.
  • Connecting to MongoDB is bounded (30 s for server selection) and fails with a clear
    message instead of hanging at Connecting to database. A stalled graceful close falls
    back to a forced close after 10 s.
  • Long migrations report progress. A 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.
  • Opt-in hard limit. warlock migrate --timeout <seconds> (or
    WARLOCK_MIGRATION_TIMEOUT_MS, in milliseconds) fails a migration that exceeds the limit,
    naming it, and exits 1.
  • Partial failures are explained. A failed migration names the migration and, on
    MongoDB, the step (step 3 of 5). It says that no ledger row was written, how many steps
    were applied, and how to recover: re-running is safe because index creation is
    idempotent, or --rollback starts over.

4. Standalone builds (core)

  • Vite is no longer bundled when the app lists it under devDependencies. Only
    dependencies, optionalDependencies and peerDependencies count as declared by the
    app. This removes about 60 MB of bundled Vite and the rolldown and lightningcss
    native addons from dist/.
  • Build-time tools stay out of the bundle. vite, rolldown, lightningcss,
    esbuild, oxc-parser, @vitejs/* and @babel/* (except @babel/runtime) are never
    bundled or staged when framework code imports them, even when the app lists them under
    dependencies. A third-party library that imports one at module level is still bundled,
    so it keeps working with no node_modules.
  • Aliased native packages are staged under the imported name. A package installed as
    "node-pty": "npm:@lydell/node-pty" is copied to dist/node_modules/node-pty, so the
    import resolves from dist/.
  • Only reachable native packages are staged, and the build warns about a native package
    it could not copy because its os, cpu or libc excludes the build machine.

5. Log channels (logger)

  • LogChannel.ready resolves once the channel's deferred init() has finished. Await
    it before relying on the channel being set up.
  • A channel disposed before its deferred init() ran no longer creates its directory or
    arms the 5-second flush interval afterwards.

6. Upgrade notes

  1. Update the family. Run npx warlock update.
  2. Read what changed. Run npx warlock release-notes --from 5.39.0.
  3. Remove workarounds (if added). Drop any build.external entries for vite,
    rolldown or lightningcss, any manual scheduler.shutdown() call in test teardown,
    and any raised Vitest teardownTimeout added for slow exits.
  4. Set a migration limit (optional). In CI, pass warlock migrate --timeout <seconds>
    if you prefer a failed run to a slow one.

Warlock.js 5.39.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 26 Oct 17:10

Released 2026-10-26.

Keep every family package (@warlock.js/* and create-warlock) on the same version.
There are no breaking changes in this release. Two changes alter behaviour without a
type error; review them before upgrading (section 1).


1. Breaking changes

  • None. 5.39.0 has no breaking changes.

Behaviour changes to review

  • --version and -v print the Warlock version only when no command is given. A
    command can take its own --version option (core).
  • Core-level refusals on page hosts now render the site's page for HTML requests
    (core, web). A GET or HEAD request that names text/html in Accept gets the
    page for a refused path such as /public/../x (403) or a malformed URL (400). API clients
    keep the JSON answer.

2. Standalone builds and fewer files (core, web)

  • Single bundle for web pages. build.singleBundle now works with web pages. The server
    pages and routes are inlined into one dist/app.js, and node dist/app.js starts from a
    copy of dist/ with no node_modules above it. A copy of dist/ also works from any
    working directory, because its client folder is found next to the running bundle.

  • Native addons are staged automatically. A package with a .node binary stays external
    and is copied into dist/node_modules/<package> with only what it needs to load. The build
    report names each one.

  • Optional integrations are detected. 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. The build report names it, so no hand-written
    external list is needed. An import the app makes, or a package the app declares, is still
    bundled or fails the build.

  • Build-time tools are left out. Vite, the development server, the page scanner and the
    Babel parser are not in the production bundle, and the server never loads Vite. The native
    parser oxc-parser is replaced by a module that throws if it is ever called.

  • Production React. A singleBundle build sets process.env.NODE_ENV to "production"
    inside react, react-dom and scheduler only, so React's development build is dropped.
    A process.env.NODE_ENV entry in build.define turns this off.

  • Client build options. webConnector({ build: { chunks, precompress } }) controls the
    client output:

    • chunks: "split" (the default) writes a React vendor chunk, the entry, a lazy chunk per
      page and shared chunks.
    • chunks: "merged" keeps the same lazy page loading and packs small shared chunks together
      by size, which gives far fewer files.
    • chunks: "single" writes one JavaScript entry and one stylesheet, with dynamic imports
      inlined. It is for single-site apps only.
    • precompress: false skips the .br and .gz copies of each client asset (default true).

    Final-bytes chunk names, dedupe, the integrity check, preload lists and payload budgets work
    in every mode.

  • Desktop and offline recipe. The desktop and offline app recipe sets both chunks and
    precompress. build.singleBundle alone does not change them.

  • Dist file summary. warlock build prints the number of files and the total size of
    dist/ when it finishes.


3. Release notes in every install (core)

  • warlock release-notes prints the family release summaries that ship inside the package
    (releases/<version>-summary.md, from 5.30.0 onward). It reads files only, so it needs no
    network and no booted app.
    • With no arguments it prints the installed version's summary.
    • --from <version> (optionally with --to <version>) prints every later release, oldest
      first, led by a combined list of breaking changes.
    • --version <v> prints one release, --list lists the versions with dates, and --json
      prints the same data as JSON.
    • A version the package does not ship is reported with its GitHub release URL.
  • Pointers from upgrade and update. warlock upgrade, warlock upgrade --check and
    warlock update end with What's new: npx warlock release-notes --from <old>. The AGENTS.md
    section that warlock agents-md generates names the installed version and the command.
  • Agent skills. The update-packages, deploy-release and upgrade-from-5-29 skills tell
    agents to run npx warlock release-notes --from <current> before upgrading.

4. Tests and tooling (core, web)

  • Config files no longer walk the app graph. Loading config registered each config file
    together with its whole import graph, so a config that imported an application service made
    every test worker spend minutes in setup (about 480 seconds for a 3,000-module graph, measured).
    Config files now register with their direct import edges only. The dev server still builds the
    full graph from its own scan.
  • onPageError(hook) (web, 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 queue, and returns a
    function that removes it. Use it instead of importing application code into src/config/web.ts.
    The types PageErrorHook, PageErrorInfo and PageErrorKind are exported.
  • warlock doctor check config-app-imports warns when a file under src/config imports,
    statically or with import(), anything under src/app. The message names the file and the
    import. Type-only imports are not reported.
  • warlock upgrade --check npm fix. npm 10.9 crashed on the family's lockfile with
    Cannot read properties of null (reading 'edgesOut'), which skipped every later step. When the
    npm on PATH is older than 11.6.2, the scratch install now runs npx -y npm@11.6.2, and --from
    is inferred from the installed @warlock.js/core.
  • WL9024 text. WL9024 applies to any integration spec: a *.spec.ts or *.test.ts file
    that is not *.unit.*, or any file under tests/. Its fix text now says to rename the file to
    <name>.unit.spec.ts outside tests/. The 5.38.0 note said *.test.ts to *.spec.ts, which
    was wrong.

5. Models (cascade)

  • System columns in typed get() and set(). On a model declared with a schema type, these
    columns are accepted without being declared: id (number), _id, created_at and
    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 belongs in the schema.
  • Clearer set() errors. A wrong value type is reported as a value mismatch, not as a
    misleading error about dot paths.
  • Typed runtime key. A column named at runtime is set with a key typed
    K extends ModelKey<Schema>. The new ModelSystemColumns and ModelColumns types are exported.

6. Web UX (web)

  • Refusal pages. For a GET or HEAD request that names text/html in Accept, a 401 or
    403 renders the site's forbidden.page.tsx. Any other refusal renders error.page.tsx, and
    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 keep the JSON answer.

7. New apps (create-warlock)

  • Installs on npm 10.9. 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 trigger that npm bug on their own. The edgesOut
    hint stays as a last resort.

8. Upgrade notes

  1. Rehearse first. Run npx warlock upgrade --check. It runs the upgrade on a scratch copy and
    never writes the real project. It reports the breaking changes that affect the app and the
    outcome of each step.
  2. Update the family. Run npx warlock update. It ends with a pointer to the release notes.
  3. Read what changed. Run npx warlock release-notes --from 5.38.0 to print the summaries of
    every release after 5.38.0, with the combined breaking-changes list first.
  4. Review the two behaviour changes in section 1: --version and -v with a command, and
    browser requests refused by core on a page host.
  5. Opt in to the standalone build (optional). Set build: { singleBundle: true } in the app's
    warlock.config.ts, then run npx warlock build. Read the build report for the native addons,
    optional integrations and React notes, and check the file count and size that the build prints.
    Test the output by running node dist/app.js from a copy of dist/.
  6. Choose the client chunk mode (optional). Set webConnector({ build: { chunks, precompress } })
    in the web connector to pick "split", "merged" or "single" chunks, and whether to write
    compressed copies. For a desktop or offline app, follow the desktop and offline recipe, which sets
    both options.
  7. Use onPageError for error reporting (optional). If src/config/web.ts imports application
    code only to report errors, register the hook with onPageError(hook) from main.ts instead and
    remove the import. Run warlock doctor to check for config-app-imports warnings.
  8. Rename integration specs (if flagged). A WL9024 warning names an integration spec that
    imports nothing needing the app. Rename it to <name>.unit.spec.ts outside tests/.
  9. Update typed model code (if needed). Columns that the schema does not declare are now
    accepted only when they are system columns (section 5). Other columns still need a schema entry or a dot path.

Warlock.js 5.38.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 25 Oct 21:25

Released 2026-10-25.

All 33 packages (@warlock.js/* and create-warlock) move to 5.38.0 together;
keep every family package on the same version. One change is breaking for typed
models (see section 1). The new defaults for test runtimes and the module boundary
apply to new apps or to new config only; existing apps keep their own settings.


1. Breaking changes

  • Typed model get() and set() reject unknown columns (cascade). For a model
    declared with a schema type, get() and set() with a top-level column name that is
    not in the schema are type errors. Add the column to the schema, use a dot path
    ("profile.city"), or declare the model without a schema type to keep loose access.
  • Nothing else in 5.38.0 is marked breaking. The changes below change behaviour
    without a type error. Review them before upgrading:
    • A 5xx is logged as one record with a new action name (section 2); log queries or
      alerts that match the old entries need updating.
    • A 401 or 403 on a page route renders the application's forbidden page (or the
      framework page) instead of a raw JSON body (section 7).
    • web.moduleBoundary defaults to "warn" for existing apps (section 4).

2. Request log (core, devtools, logger)

  • Persistent request log. With http.logging.requests on, each request writes one
    redacted JSON record after the response is sent: id, trace id, method, URL (credential
    query values masked), route, params, query, headers, session user, IP, status, duration,
    phases, log entries, a query summary (count, slowest query, N+1 findings) and, for a
    5xx or a thrown request, the error with its stack, phase, page file and site.
  • Defaults. On in development and off elsewhere; in development every request is
    kept, elsewhere only errors. Records go to a daily file under storage/logs/requests/
    by default. slowMs, sampleRate, include (headers, body, query, params, session,
    response, stack, logs, queries, phases), maxBodyBytes (16 KB by default) and
    retention: { maxDays, maxSizeMb } tune it. Outside development only allowlisted headers
    are recorded, and bodies are left out unless include.body or include.response is set.
  • One error record per 5xx. The three earlier error entries (request-error,
    model-write-validation-error, unhandled-request-error) are replaced by one error-level
    record with module: "http" and action: "request.<phase>", logged once per request. A
    returned 503 is treated as an intentional answer and is not logged. A 4xx is never logged
    as an error. The per-request http.log line is unchanged.
  • Warlock CLI and MCP. warlock logs requests (filters --status, --route, --since,
    --limit, --json) and warlock logs request <id> read the log without booting the app.
    The MCP tool request_log is read-only. A request-log skill ships with the release.
  • Devtools. The Requests panel reads the persisted log, so history survives a restart,
    and it filters by status, route, method, time and text. It copies any request as curl,
    with redacted values left as [REDACTED].
  • Agents tab. The development dashboard at /__warlock shows per-turn agent traces
    (section 5). It needs no Panoptic.

3. Tests (core, create-warlock)

  • Unit and integration projects. *.unit.spec.ts and *.unit.test.ts run in a Vitest
    unit project with no setup files and no app boot. Every other spec, and anything under
    tests/, runs in integration. guardUnitProject() fails a unit spec that imports a
    connector, the connectors manager, bootstrap or a test-server helper.
  • Worker lifetime for new apps. warlock add test and the starter set
    lifetime: "worker" and resetDatabase: true in src/config/tests.ts, and use one
    database per Vitest worker. WARLOCK_TEST_LIFETIME=file switches back. Existing files
    are never overwritten.
  • Per-file reset in worker lifetime. Open test HTTP servers are closed, the request
    context is cleared, the route table returns to the boot snapshot, and a real in-memory
    cache is flushed. setupTest({ lifetime: "file" }) opts one file out of the shared
    runtime.
  • Boot profile. tests.profileSetup: true or WARLOCK_TEST_PROFILE=1 prints per-phase
    timings, including each connector. WARLOCK_TEST_PROFILE_JSON=<path> writes them as JSON.
  • Fixes. The HTTP test server registers the connectors warlock.config.ts declares, so a
    page request no longer answers 404. A test run that booted the web connector exits cleanly
    (Vite, its HMR websocket and its watchers are closed on shutdown).

4. Build (web)

  • Payload budgets. warlock build measures build.budgets.payload for each page's
    translations and browser route table. A page over its limit fails the build with
    mode: "error", or prints a warning with mode: "warn".
  • Client chunk integrity. Each build writes client/.vite/warlock-integrity.json. The
    next build fails, naming each file, when a file name now maps to different bytes. The
    previousBuild option points the check at another build, such as the deployed release.
  • Stable chunk names. Production chunks are named after their final bytes. Byte-identical
    non-entry chunks are merged, and the manifest, preload lists, budget report and .br and
    .gz files use the final names.
  • Module boundary. The client build enforces web.moduleBoundary. Browser code may
    import only browser-safe files (*.client.ts(x), schema and type files, the module's
    web/ folder and non-code files). "warn" is the default, "error" fails the build and
    "off" disables the check. The starter sets "error".
  • Two tracing phases. translations and named-api-routes are reported through core's
    TracingHooks.onPhase.

5. Validation and AI (seal, core, ai)

  • Readable field names in validation messages. A field without a translated label is
    named in words: max_dumbbell_kg reads "Max dumbbell kg". Labels under attributes.<field>
    still win. validation.humanizeAttributes: false keeps the raw key.
  • Label check. warlock doctor check validation-labels (WL4002) is off by default
    and lists request-schema fields without a label when validation.warnMissingLabels is on.
  • Schema helpers. humanizeAttribute(key) is exported from seal.
    ObjectValidator#partial() keeps its types. .internal() marks a server-set field, and
    withoutInternal() and internalKeys() remove or list those fields.
  • OpenAI strict JSON Schema. An optional enum, literal, union or $ref is nullable in
    toJsonSchema("openai-strict"), so a client can send null. draft-2020-12 and
    openapi-3.0 nullable output is corrected too.
  • Tool schema lint. ai.tool() checks its schema against OpenAI strict, Anthropic and
    Google. In development it logs one warning per tool. lint: false or per-tool options
    tune it. describeToolSchema() and describeToolSchemas() return findings and an
    estimated token cost.
  • Tool-use contract (opt-in). Agents can set actWhenAsked, onToolRefusal and
    backstops. Agents without them behave as before.
  • Eval scorers. ai.eval.requiredToolCalled() and ai.eval.changeClaimedWithoutToolCall().
  • Agent traces. Each run records system prompt hash, tools offered, calls, refusals,
    retries, usage and cost. Traces are on in development and off under test and in
    production. trace sets this per agent. Traces are redacted and kept in an in-memory
    store. diffAgentTraces() and formatAgentTrace() help compare runs.
  • CLI. warlock ai tools lists per-tool token estimates and schema findings, and
    warlock ai trace lists or prints agent runs from the running dev server. warlock ai
    no longer marks --model as required in its option list.

6. Core tooling (core, logger, create-warlock)

  • warlock upgrade --check rehearses an upgrade on a scratch copy of the app and never
    writes the real project. It reports the breaking changes that affect the app and the
    outcome of each step. Child processes run one at a time with a memory cap
    (--memory-mb, default 4096) and a time limit (--timeout-minutes, default 20).
  • warlock doctor sites check renders each configured site's root page in
    development and fails (WL5005) on any 5xx.
  • Capabilities. defineCapability, provideCapability, useCapability,
    hasCapability and listCapabilities give a module-cycle-breaking slot a typed home.
    useCapability throws WL2008 when nothing provides it. warlock doctor warns about a
    required capability with no provider. A use-capabilities skill ships with the release.
  • warlock inspect --section wiring (and the MCP tool inspect_wiring) lists every
    auto-loaded file with its load order, then every capability slot with its provider and
    consumers.
  • Package manager detection follows the packageManager field, then the manager that
    installed node_modules, then the lockfile that matches package.json. A stale workspace
    file no longer wins. warlock update prints the manager it chose and why.
  • warlock agents-md lists the skill of a package that ships a single skills/SKILL.md
    (the AI provider packages and @warlock.js/sitemap).
  • Logger. LogChannel.passive marks a channel that only observes entries. The terminal
    fallback used when no log channel is configured ignores passive channels.
  • Loading the config keeps @warlock.js/* imports external, so a monorepo checkout no
    longer loads a second copy of core.

7. Web UX (web, create-warlock)

  • Forbidden page. forbidden.page.tsx (with an optional forbidden.setup.ts) is the
    page for a refused request. A 401 or 403 on a page route renders it with that status and
    receives { error, status } (plus debug outside production). The starter ships one, and...
Read more

Warlock.js 5.37.1

Choose a tag to compare

@hassanzohdy hassanzohdy released this 24 Oct 16:10

Released 2026-10-24.

All 33 packages (@warlock.js/* and create-warlock) move to 5.37.1 together;
keep every family package on the same version. No breaking changes. Upgrade notes
below are optional clean-ups.


1. Multi-site pages in development and tests (web)

  • Pages no longer answer 500 in multi-site apps under the development server and
    the HTTP test server. The browser route table is read from Core's router, never
    from a site's registration facade. With web.namedApiRoutes: false the table is
    not computed at all, and a failure while building it is logged with its own phase.

2. Every 5xx page failure is logged (web, core)

  • One error record per failed request, at error level, with the error message,
    the source-mapped stack, the phase (translations, module load, middleware, session,
    loader, metadata, render, stream, deferred data, error page), the page file, route,
    site and request id. 4xx outcomes stay out of the error log.
  • Development and test error pages show the same details in a panel; production
    stays generic. error.page.tsx receives them as props.debug outside production.
  • Translation lookups that ran before the handler's error handling now report as the
    translations phase instead of a bare 500. log is exported from @warlock.js/core.

3. Upgrading the family with npm (core)

  • warlock update on npm removes the family's (and Vite, Vitest, @vitest/*,
    @vitejs/*) lock entries and re-resolves them in one install, so an existing
    package-lock.json can step to the new version. npm 11.6.2 or newer is required for
    that step; older npm gets a warning naming the minimum. Family packages keep their
    exact pins on each other.
  • WL8010: warlock check reports installed @warlock.js/* packages whose
    versions differ.
  • warlock upgrade no longer rewrites packages that are already newer than
    --to, and its install hints name the package manager the project actually uses
    (the packageManager field, then a workspace file, then the newest lockfile).

4. Tests

  • Vitest 5 duplicate-plugin warning fixed. A config that lists
    lowerStage3Decorators() in the root and again in inline projects no longer warns.
    The vitest-inline-project-plugins upgrade codemod lists repeated calls for review.
  • Worker lifetime for HTTP and database suites. tests.resetDatabase truncates
    tables or collections before each file in worker lifetime (keeping the migration
    ledger); resetTestDatabase(), workerDatabaseName() and testWorkerId() come from
    @warlock.js/core/tests; test HTTP servers a file leaves open are closed at the
    next file. warlock add test writes a projects layout where
    WARLOCK_TEST_LIFETIME=worker turns worker lifetime, database reset and
    isolate: false on together, and the setup profiler recommends worker lifetime when
    setup takes at least half of a run.

5. Development server (web)

  • The HMR websocket attaches through Vite 8's server.ws, so the
    server.hmr.* deprecation warning is gone.
  • The client-bundle security check no longer reports the app's own index.html
    entry as an unjudged import.

6. Verification

The publish run repeats the full family gate (typecheck, tests, build, pack,
local-registry install and the blog app) before anything reaches npm, and checks the
blog against npm after publishing. Format, lint, public text, spec coverage, MDX,
shipped-docs and skill reference checks ran clean locally.

Warlock.js 5.37.0

Choose a tag to compare

@hassanzohdy hassanzohdy released this 24 Oct 11:25

1. Breaking changes

  • Document-store rollbacks no longer erase fields by default (cascade). On MongoDB,
    a plain dropColumn() or dropColumns() inside down() is skipped with a warning
    instead of running $unset on every document. Forward drops (up(), drop: [...])
    are unchanged. To erase the data on purpose, pass { destructive: true }.
  • Add-only alters derive their rollback (cascade). A Migration.alter that only adds
    columns or fields no longer needs a hand-written down(). Delete that down() so the
    rollback is derived; on SQL it drops the added columns, on document stores it only
    updates the ledger.
  • Rollback and fresh migrations ask first (core, cascade). warlock migrate --rollback,
    warlock migrate --fresh, cascade migrate:rollback and cascade migrate --fresh print a
    destructive-operation summary. Outside development (Warlock's environment for warlock migrate;
    NODE_ENV other than development for the cascade CLI), a rollback that removes data asks for confirmation and
    refuses without a terminal. Scripts and CI jobs must pass --yes (or --force). A rollback
    that removes no data does not ask. Programmatic rollbackMigrations() and freshMigrate()
    do not prompt; call confirmRollback() first to get the same guard.

No codemod is needed for 5.37.0. The changes above are manual: review the add-only
down() methods, pass { destructive: true } where erasing data is intended, and add
--yes to deploy scripts.


2. Named API route table controls and guard detection

  • web.namedApiRoutes controls what each page embeds. false embeds no named routes, so
    a named-route call throws (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 }), narrows the table. { include, privatePrefixes }
    combines both. Every form can only remove routes from what the page's client code reaches.
  • privatePrefixes keeps every route under the listed path prefixes (for example
    /api/admin/ or /api/webhooks/) out of every table, for any viewer and any site.
    Prefixes match whole path segments.
  • Per-site routes. The sites option on a route, or on router.group({ sites }), names
    the multi-site keys that may offer it in the browser. A route with no sites is shared by
    every site, and a route's own value replaces its group's.
  • Guarded routes for guests. A route with an auth guard in its middleware chain (group
    middleware included) is left out of the table for a guest. A viewer counts as a guest only
    when web.session is configured and resolves no user. Without web.session the viewer is
    unknown, and guarded routes stay in the table.
  • Guard detection in route inspection. warlock inspect and the generated AGENTS.md
    list of mutating routes without an auth guard now count middleware that declares itself a
    guard with markAuthGuard(), including one registered on a router.group. warlock check
    (WL9006) handles inline markAuthGuard(fn, false) and local guard factories used as
    middleware: [requireAdmin()].
  • Types and seams. The new option types are exported from web. The server seam
    resolveNamedApiRoutes(names, options) takes { config, site, guest }.

3. Safer migrations on document stores

  • Rollback plan. migrationRunner.planRollback(batches) lists, per migration, the
    collections or tables dropped or truncated and the fields or columns dropped or unset,
    with document and row counts. The counts come from new optional countRows and
    countWithColumns driver methods (MongoDB; countRows on Postgres).
  • Confirmation. confirmRollback({ all, batches, yes, development }) prints that plan and
    asks outside development. The CLI rollback and fresh commands accept --yes / -y and --force.
  • Guards in hand-written down(). Migration#isDocumentStore lets a migration skip a
    SQL-only drop on a document store.

4. warlock release for application deploys

warlock release is a deploy pipeline for applications, configured by a new release
section in warlock.config.ts:

  • candidate freezes a clean commit into .warlock/releases/<version>-<shortsha>/ with
    a manifest (version, commit, time, Node version, lockfile hash).
  • gate runs the named checks in order (default: typecheck, check, test, build) and
    stops at the first failure.
  • migrations --prove runs the pending migrations for real and checks the ledger, tables
    and indexes against the expected set.
  • stage, then activate checks the staged files, the gate and that the version is
    newer than the active one, switches current with an atomic symlink, keeps previous and
    runs the restart command you configure. Warlock never calls a process manager itself.
  • verify checks the version and commit reported at release.verify.url. rollback
    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).

Every action accepts --json, and each release keeps a manifest plus a shared release.log.


5. Eval repeats and variance-aware gating (ai)

  • agent.eval({ repeat }) runs every case several times (default 1). Results carry pass
    counts, per-run results and score and pass-rate intervals. Reports add overall and
    per-category statistics (EvalCase.category), plus stableFailures and flakyCases.
    toJUnit shows passed 2/3 runs.
  • gate: "interval" flags a regression only when the baseline interval lies above the
    current one by more than tolerance, so run-to-run noise no longer reads as a regression.
    The default gate: "point" is unchanged. failOnStableFailures fails the verdict when a
    case failed every run.
  • diff takes { tolerance, gate, confidence, failOnStableFailures } as its third
    argument; a plain tolerance number still works. EvalRegression adds regressedMetrics,
    stableFailures and flaky.
  • Baselines store the repeat count and intervals. Baselines saved by earlier versions
    still load, and each of their cases counts as one sample.

6. Fixes and docs

  • Validation messages (core). The default validation translation no longer
    returns a message with an unfilled :placeholder. The aliases :length and
    :matchingInput are mapped, and a translation that still cannot be filled falls back to
    seal's message. In development, the first unfillable validation.* key logs a warning.
    App overrides keep working.
  • Test processes exit (logger). FileLog's background flush timer no longer keeps the
    process alive, so finished Vitest runs and CLI commands stop waiting on it. flush() and
    flushSync() still write buffered entries.
  • Stylesheet imports in page discovery (core). The dev loader answers stylesheet imports
    (.css, .scss, .sass, .less, .styl) with an empty module, so sitemap discovery no
    longer fails with ERR_UNKNOWN_FILE_EXTENSION on a page that imports one.
  • Hydration mismatch hint (web). In development, a React hydration mismatch logs the
    first mismatching node: its element path, the server text and the client text. Production
    builds are unchanged.
  • Intl guidance (docs). The 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. They recommend a shared pre-computed table or rendering after
    hydration.

7. Verification

Checks ran on Node 22.15, each through a memory- and CPU-capped runner:

  • Typecheck and the strictness gate: clean.
  • Full test suite: core 4,552 passed, web 3,411 passed, the other packages 5,682
    passed, no failures. A new spec spawns Vitest on a fixture app in both test
    lifetimes and asserts the process exits on its own.
  • create-warlock standalone, release-tool and workspace script tests: passed.
  • Skill snippet, docs build, format, lint, public text, spec coverage, MDX and skill
    reference checks: clean.

The publish run repeats the full family gate (typecheck, tests, build, pack,
local-registry install and the blog app) before anything reaches npm.