ClojureScript
ClojureScript (CLJS) is Clojure that compiles to JavaScript via the Google Closure Compiler. It shares Clojure's syntax, immutable data structures, and REPL-driven workflow, but runs on JS engines (browser, Node.js) and interoperates directly with JS objects, functions, and npm/Closure libraries.
This skill covers writing idiomatic CLJS, JS interop, dependency/build configuration, and the newer native async/await support. When in doubt about compiler flags or interop mechanics, check references/ rather than guessing — CLJS has a lot of build-tooling surface area that's easy to get subtly wrong.
Not for nbb. Babashka/nbb runs CLJS through an SCI interpreter (no Google Closure Compiler, no :optimizations/:advanced, no externs) for fast-starting Node.js scripts — a different language surface from what this skill covers. nbb is out of scope here — consult nbb's docs and examples for nbb.edn projects, anything run via nbb script.cljs/npx nbb.
Gotchas
:optimizations defaults to :none, not :advanced — a project with no explicit :optimizations setting is running unoptimized dev output.
- Symbol renaming under
:advanced breaks untyped JS interop. "Works in dev, breaks in prod" is almost always this — fix with an externs file or string-keyed access (goog.object/get), not by changing app logic.
:main under :optimizations :none only loads what's actually :required. A side-effect-only namespace not reached by the entry namespace's transitive requires silently doesn't run — add an explicit :require or put it in :preloads.
:output-to and :modules are mutually exclusive, not layered — :modules needs a per-module :output-to inside each module map, not a top-level one.
- No arbitrary-precision numbers. CLJS numbers are all JS
number (a double) — no JVM-style BigDecimal/BigInt/ratios. Watch for precision loss on large integers.
^:async metadata goes on the fn/name only, never the arg vector, and await only works inside a function actually marked :async — a nested fn doesn't inherit it and needs its own ^:async (see references/async-functions.md).
- An
:async function always returns a Promise, even when the body looks like it returns a plain value.
- Truthiness differs from JS. Only
false and nil are falsy — 0, "", js/NaN, [], and (array) are all truthy. (if 0 "yes" "no") → "yes", unlike JS. Easy to get backwards when translating JS/TS conditionals.
- No automatic tail-call optimization, same as JVM Clojure. CLJS functions run as ordinary JS function calls, and most JS engines don't reliably implement proper tail calls either — deep non-tail self-recursion will blow the JS call stack on large/unbounded input, even though it "works" for small test inputs. Reach for
recur (self-recursion, tail position only — see below), trampoline (mutual recursion), or wrap the recursive case in lazy-seq (sequence-producing recursion) rather than plain recursive calls whenever the input size isn't small and fixed.
Core language cheat sheet
CLJS syntax is essentially Clojure's, so standard Clojure knowledge (immutable persistent data structures, let/fn/defn, ->/->> threading, map/filter/reduce, multimethods, protocols, core.async) applies directly. For a categorized index of core-library functions when you need the exact name of something less common, see references/core-function-index.md (based on https://cljs.info/cheatsheet/). The CLJS-specific things to keep front of mind:
- Numbers: CLJS has only one numeric type backed by JS
number (a double). No arbitrary-precision integers/ratios like JVM Clojure — watch for precision issues with large integers.
- Namespaces map to JS modules/Closure namespaces.
(ns my.app.core (:require [my.app.util :as u])) — :require for CLJS/Closure namespaces, :import for Closure classes/enums (e.g. (:import [goog Timer])).
defprotocol/deftype/defrecord work like Clojure and compile to real JS constructor functions/prototypes.
js/ prefix accesses JS global objects: js/console, js/document, js/Math, js/window.
- No JVM interop — anything JVM-specific (
Thread, java.* classes, blocking IO) doesn't exist. Concurrency is single-threaded/event-loop based; use core.async channels or promises/async functions instead of threads or locks.
.cljc files hold code shared between Clojure and ClojureScript, guarded with reader conditionals: #?(:clj (do-jvm-thing) :cljs (do-js-thing)).
JavaScript interop
- Method call:
(.methodName obj arg1 arg2) → obj.methodName(arg1, arg2)
- Property access:
(.-propName obj) → obj.propName
- Property set:
(set! (.-propName obj) val)
- Constructor:
(SomeClass. arg1 arg2) → new SomeClass(arg1, arg2)
- Static/global access:
js/JSON.stringify, js/Object.keys
- Chained calls read top-down with
..: (.. js/document (getElementById "app") -innerHTML)
- JS↔CLJS data conversion:
(clj->js m) and (js->clj o :keywordize-keys true)
- Destructuring JS objects:
(let [{:keys [a b]} (js->clj obj :keywordize-keys true)] ...) or interop-style ^js type hints with .- accessors when you don't want a full conversion.
For consuming Closure Library, npm packages, foreign (non-Closure-compatible) JS, and CLJS libraries — including externs, advanced-compilation pitfalls, and CLJSJS — see references/dependencies-and-interop.md before guessing at a :require/:import shape or writing an externs file from scratch.
Recursion
Because CLJS has no automatic TCO (see Gotchas), which recursion tool to reach for depends on the shape of the problem:
recur — self-recursion only (a function calling itself, directly, not through an intermediate function), and only from the tail position. Compiles to a real loop, so it's stack-safe for arbitrarily large input. This is the default choice for producing a scalar or a small, fixed-size result — e.g. summing a collection with loop/recur rather than a plain recursive call.
trampoline — for mutual recursion (function A calls B calls A), where recur doesn't apply because it only optimizes self-calls. Wrap each recursive tail call in #(...) and drive the whole thing with (trampoline f args...); trampoline keeps calling the returned function until it gets back a non-function value. Don't reach for trampoline on a self-recursion — recur is simpler and equally stack-safe there.
lazy-seq — for recursion that produces a sequence, especially an unbounded/large one. Wrapping the recursive call in lazy-seq breaks the recursion into on-demand steps, so callers only pay for the elements they actually realize (take, first, etc.). This is usually the better fit over loop/recur whenever the output size varies or could be large — let take/drop on the caller side decide how much to compute.
memoize — trades space for time by caching by argument, useful for expensive recursive/mutually-recursive definitions with overlapping subproblems. Caveat: memoizing alone doesn't fix a stack-overflow risk — a cold cache still has to recurse all the way down on the first large call. If the recursion is deep, build the cache bottom-up by mapping the memoized function over a lazy range/seq ((map f (range))) rather than calling it directly on a large n.
Async / promises
CLJS added native ^:async functions with an await macro (since v1.12.145) as a lighter-weight alternative to core.async for promise-based code. This is a newer feature that's easy to get wrong or not know about at all — read references/async-functions.md before writing or reviewing any CLJS code that deals with Promises, fetch, or async/await-shaped logic (see Gotchas above for the two most common mistakes).
The single most common typo — ^:async sits on the function name, never the argument vector:
(defn ^:async load-user [id] (await (fetch-user id))) ; ✅ name
(defn load-user ^:async [id] ...) ; ❌ arg vector — never
``` One more rule worth holding in mind: prefer `Promise/all` (via `mapv`) over `map` when you need to await several promises produced in a loop.
If the codebase already uses `core.async` (`go`, `<!`, channels) for async control flow, stay consistent with that style rather than mixing in `^:async`/`await` unless asked to migrate.
## External resources
For idiomatic-Clojure background/mindset reading (design patterns translated to Clojure idioms, `core.async` worked examples, FP mindset shifts) beyond this skill's own reference material, plus community-curated library lists and real-world example codebases, see `references/external-resources.md`. Use it as supplementary color/perspective and pointers, not as the source of truth for CLJS-specific mechanics — those stay in this skill's other reference files.
## Build & compiler configuration
CLJS projects are almost always built with **shadow-cljs** (npm/JS-ecosystem friendly, handles npm deps automatically) or the **Clojure CLI (`deps.edn`) + `cljs.main`/`cljs-build-api`** combo; some older projects use Leiningen + `figwheel-main`. Don't assume the tool — check for `shadow-cljs.edn`, `deps.edn`, or `project.clj` in the project before giving setup instructions.
For the full list of compiler options (`:optimizations`, `:target`, `:main`, `:npm-deps`, `:modules`, `:source-map`, `:closure-defines`, warnings, etc.) with exact semantics and defaults, see `references/compiler-options.md`. Don't guess at option names or defaults — this is a large, easy-to-misremember surface (e.g. `:static-fns` defaults to `false` except under `:advanced`, `:npm-deps` defaults to `false`). See Gotchas above for the most common failure patterns tied to these options.
## Working style
**Default to idiomatic Clojure(Script), not a literal JS translation.** This is the top priority whenever writing or rewriting CLJS, and it should win over "closest to what the JS/TS looked like" unless the user's existing codebase clearly does otherwise. Concretely, that means:
- **Immutability and pure functions first.** Reach for persistent data structures (maps, vectors, sets) and pure transformations over mutable state, `atom`s, or `set!`. Only use an `atom`/`volatile!` for genuine local mutable state (e.g. component-local UI state), not as a default variable substitute.
- **Data over classes.** Model domain data as plain maps/records rather than JS-style classes with methods, unless polymorphism genuinely calls for `defprotocol`/`deftype`/`defrecord` or a JS class is required at an interop boundary (e.g. a React component).
- **Sequence/threading idioms over loops.** Prefer `map`/`filter`/`reduce`/`keep`/`for` and `->`/`->>` threading over `for`/`while`-style imperative loops, index juggling, or manual accumulation. This is a style preference, not a stack-safety rule: `loop`/`recur` is the *correct* (not just a perf-justified) choice for genuinely tail-recursive, scalar-producing logic — see Recursion above — reach for a transient only when profiling justifies it.
- **Destructuring over manual accessors.** Use `let`/fn-arg destructuring (`{:keys [...]}`, `[a b & rest]`) instead of repeated `(get m :k)` or positional indexing.
- **Small composed functions over long procedural bodies.** Break logic into small, named, testable functions and compose them, rather than one large function with sequential mutation-heavy steps.
- **Idiomatic control flow.** Prefer `cond`/`case`/`condp` and `when`/`if-let`/`when-let` over nested `if`s or JS-style early-return chains; prefer `^:async`/`await` or `core.async` (per the async section above) over manual `.then` chains.
- **Namespaced keywords and data-driven design** where they fit naturally (e.g. `:person/name` in shared/library code) rather than plain strings for map keys.
When converting JS/TS to CLJS specifically: re-derive the idiomatic CLJS shape of the logic rather than transliterating line-by-line — e.g. turn `.then` chains into `^:async`/`await` or `core.async`, turn classes into `defrecord`/protocols or plain maps + functions, turn `for`-loops with accumulator variables into `reduce` or `into`.
If the surrounding codebase has an established (even if less idiomatic) convention, match it for consistency rather than unilaterally "fixing" the whole file — but say so, and offer the idiomatic alternative as a suggestion rather than silently picking one or the other.
Other working-style notes:
- When debugging, ask whether the error is happening under `:none` (dev) or `:advanced`/`:simple` (prod) optimizations — the failure modes are very different (the former is usually a logic/require bug, the latter is very often a renaming/externs issue).
---
## Benchmark
Scenario: `.benchmarks/scenarios/clojurescript-001-async-await.md` · Run: 2026-08-31 (salience re-run `wf_9a5588bc`) · Log: `.benchmarks/runs/2026-08-31/clojurescript-001-async-await.json`
| Model | Without | With | Delta |
| ----------------- | ------- | ----- | ----- |
| claude-opus-4-8 | 67% | 100% | +33% |
| claude-sonnet-4-6 | 83% | 100% | +17% |
| claude-haiku-4-5 | 100% | 100% | +0% |
> **PASS (run 2026-08-31)**. Salience re-run (✅/❌ `^:async`-placement pair added to the Async section, wf_9a5588bc): sonnet's arg-vector typo cleared (+17); gains on all models. The prior single-run sonnet −17 was a salience gap, not noise. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.
1---2name: clojurescript3description: Write, review, debug, and configure ClojureScript code and projects. Use this whenever the user mentions ClojureScript, .cljs/.cljc files, shadow-cljs, figwheel, the CLJS compiler, JS interop from Clojure, Reagent/re-frame/Reagent-style UI code, or asks to convert JS/TS logic into ClojureScript. Also use it for questions about CLJS compiler options (:optimizations, :main, :npm-deps, :externs, etc.), consuming JS/npm libraries from CLJS, source maps, Google Closure Library usage, or the newer ^:async/await function support. Also trigger for CLJS recursion/stack-overflow/trampoline questions. Trigger even if the user just pastes CLJS code with an error and asks "what's wrong here" or asks to set up a new CLJS project. Do NOT use for nbb (babashka/nbb) scripts, `nbb.edn` projects, or anything meant to run via `nbb script.cljs`/`npx nbb` — those have a different (SCI-interpreted, no-Closure-Compiler) language surface; those are out of scope for this skill — consult nbb's own documentation.4---56# ClojureScript78ClojureScript (CLJS) is Clojure that compiles to JavaScript via the Google Closure Compiler. It shares Clojure's syntax, immutable data structures, and REPL-driven workflow, but runs on JS engines (browser, Node.js) and interoperates directly with JS objects, functions, and npm/Closure libraries.910This skill covers writing idiomatic CLJS, JS interop, dependency/build configuration, and the newer native async/await support. When in doubt about compiler flags or interop mechanics, check `references/` rather than guessing — CLJS has a lot of build-tooling surface area that's easy to get subtly wrong.1112> **Not for nbb.** [Babashka/nbb](https://github.com/babashka/nbb) runs CLJS through an SCI interpreter (no Google Closure Compiler, no `:optimizations`/`:advanced`, no externs) for fast-starting Node.js scripts — a different language surface from what this skill covers. nbb is out of scope here — consult [nbb's docs](https://github.com/babashka/nbb) and [examples](https://github.com/babashka/nbb/tree/main/examples) for `nbb.edn` projects, anything run via `nbb script.cljs`/`npx nbb`.1314## Gotchas1516- **`:optimizations` defaults to `:none`**, not `:advanced` — a project with no explicit `:optimizations` setting is running unoptimized dev output.17- **Symbol renaming under `:advanced` breaks untyped JS interop.** "Works in dev, breaks in prod" is almost always this — fix with an externs file or string-keyed access (`goog.object/get`), not by changing app logic.18- **`:main` under `:optimizations :none` only loads what's actually `:require`d.** A side-effect-only namespace not reached by the entry namespace's transitive requires silently doesn't run — add an explicit `:require` or put it in `:preloads`.19- **`:output-to` and `:modules` are mutually exclusive**, not layered — `:modules` needs a per-module `:output-to` inside each module map, not a top-level one.20- **No arbitrary-precision numbers.** CLJS numbers are all JS `number` (a double) — no JVM-style BigDecimal/BigInt/ratios. Watch for precision loss on large integers.21- **`^:async` metadata goes on the `fn`/name only, never the arg vector**, and `await` only works inside a function actually marked `:async` — a nested `fn` doesn't inherit it and needs its own `^:async` (see `references/async-functions.md`).22- **An `:async` function always returns a `Promise`**, even when the body looks like it returns a plain value.23- **Truthiness differs from JS.** Only `false` and `nil` are falsy — `0`, `""`, `js/NaN`, `[]`, and `(array)` are all truthy. `(if 0 "yes" "no")` → `"yes"`, unlike JS. Easy to get backwards when translating JS/TS conditionals.24- **No automatic tail-call optimization, same as JVM Clojure.** CLJS functions run as ordinary JS function calls, and most JS engines don't reliably implement proper tail calls either — deep non-tail self-recursion will blow the JS call stack on large/unbounded input, even though it "works" for small test inputs. Reach for `recur` (self-recursion, tail position only — see below), `trampoline` (mutual recursion), or wrap the recursive case in `lazy-seq` (sequence-producing recursion) rather than plain recursive calls whenever the input size isn't small and fixed.2526## Core language cheat sheet2728CLJS syntax is essentially Clojure's, so standard Clojure knowledge (immutable persistent data structures, `let`/`fn`/`defn`, `->`/`->>` threading, `map`/`filter`/`reduce`, multimethods, protocols, `core.async`) applies directly. For a categorized index of core-library functions when you need the exact name of something less common, see `references/core-function-index.md` (based on https://cljs.info/cheatsheet/). The CLJS-specific things to keep front of mind:2930- **Numbers**: CLJS has only one numeric type backed by JS `number` (a double). No arbitrary-precision integers/ratios like JVM Clojure — watch for precision issues with large integers.31- **Namespaces map to JS modules/Closure namespaces.** `(ns my.app.core (:require [my.app.util :as u]))` — `:require` for CLJS/Closure namespaces, `:import` for Closure classes/enums (e.g. `(:import [goog Timer])`).32- **`defprotocol`/`deftype`/`defrecord`** work like Clojure and compile to real JS constructor functions/prototypes.33- **`js/` prefix** accesses JS global objects: `js/console`, `js/document`, `js/Math`, `js/window`.34- **No JVM interop** — anything JVM-specific (`Thread`, `java.*` classes, blocking IO) doesn't exist. Concurrency is single-threaded/event-loop based; use `core.async` channels or promises/async functions instead of threads or locks.35- **`.cljc` files** hold code shared between Clojure and ClojureScript, guarded with reader conditionals: `#?(:clj (do-jvm-thing) :cljs (do-js-thing))`.3637## JavaScript interop3839- Method call: `(.methodName obj arg1 arg2)` → `obj.methodName(arg1, arg2)`40- Property access: `(.-propName obj)` → `obj.propName`41- Property set: `(set! (.-propName obj) val)`42- Constructor: `(SomeClass. arg1 arg2)` → `new SomeClass(arg1, arg2)`43- Static/global access: `js/JSON.stringify`, `js/Object.keys`44- Chained calls read top-down with `..`: `(.. js/document (getElementById "app") -innerHTML)`45- JS↔CLJS data conversion: `(clj->js m)` and `(js->clj o :keywordize-keys true)`46- Destructuring JS objects: `(let [{:keys [a b]} (js->clj obj :keywordize-keys true)] ...)` or interop-style `^js` type hints with `.-` accessors when you don't want a full conversion.4748For consuming Closure Library, npm packages, foreign (non-Closure-compatible) JS, and CLJS libraries — including externs, advanced-compilation pitfalls, and CLJSJS — see `references/dependencies-and-interop.md` before guessing at a `:require`/`:import` shape or writing an externs file from scratch.4950## Recursion5152Because CLJS has no automatic TCO (see Gotchas), which recursion tool to reach for depends on the shape of the problem:5354- **`recur`** — self-recursion only (a function calling itself, directly, not through an intermediate function), and only from the tail position. Compiles to a real loop, so it's stack-safe for arbitrarily large input. This is the default choice for producing a scalar or a small, fixed-size result — e.g. summing a collection with `loop`/`recur` rather than a plain recursive call.55- **`trampoline`** — for **mutual** recursion (function A calls B calls A), where `recur` doesn't apply because it only optimizes self-calls. Wrap each recursive tail call in `#(...)` and drive the whole thing with `(trampoline f args...)`; `trampoline` keeps calling the returned function until it gets back a non-function value. Don't reach for `trampoline` on a self-recursion — `recur` is simpler and equally stack-safe there.56- **`lazy-seq`** — for recursion that produces a sequence, especially an unbounded/large one. Wrapping the recursive call in `lazy-seq` breaks the recursion into on-demand steps, so callers only pay for the elements they actually realize (`take`, `first`, etc.). This is usually the better fit over `loop`/`recur` whenever the output size varies or could be large — let `take`/`drop` on the caller side decide how much to compute.57- **`memoize`** — trades space for time by caching by argument, useful for expensive recursive/mutually-recursive definitions with overlapping subproblems. Caveat: memoizing alone doesn't fix a stack-overflow risk — a cold cache still has to recurse all the way down on the first large call. If the recursion is deep, build the cache bottom-up by mapping the memoized function over a lazy range/seq (`(map f (range))`) rather than calling it directly on a large `n`.5859## Async / promises6061CLJS added native `^:async` functions with an `await` macro (since v1.12.145) as a lighter-weight alternative to `core.async` for promise-based code. This is a newer feature that's easy to get wrong or not know about at all — **read `references/async-functions.md` before writing or reviewing any CLJS code that deals with `Promise`s, `fetch`, or `async`/`await`-shaped logic** (see Gotchas above for the two most common mistakes).6263The single most common typo — `^:async` sits on the function **name**, never the argument vector:6465````clojure66(defn ^:async load-user [id] (await (fetch-user id))) ; ✅ name67(defn load-user ^:async [id] ...) ; ❌ arg vector — never68``` One more rule worth holding in mind: prefer `Promise/all` (via `mapv`) over `map` when you need to await several promises produced in a loop.6970If the codebase already uses `core.async` (`go`, `<!`, channels) for async control flow, stay consistent with that style rather than mixing in `^:async`/`await` unless asked to migrate.7172## External resources7374For idiomatic-Clojure background/mindset reading (design patterns translated to Clojure idioms, `core.async` worked examples, FP mindset shifts) beyond this skill's own reference material, plus community-curated library lists and real-world example codebases, see `references/external-resources.md`. Use it as supplementary color/perspective and pointers, not as the source of truth for CLJS-specific mechanics — those stay in this skill's other reference files.7576## Build & compiler configuration7778CLJS projects are almost always built with **shadow-cljs** (npm/JS-ecosystem friendly, handles npm deps automatically) or the **Clojure CLI (`deps.edn`) + `cljs.main`/`cljs-build-api`** combo; some older projects use Leiningen + `figwheel-main`. Don't assume the tool — check for `shadow-cljs.edn`, `deps.edn`, or `project.clj` in the project before giving setup instructions.7980For the full list of compiler options (`:optimizations`, `:target`, `:main`, `:npm-deps`, `:modules`, `:source-map`, `:closure-defines`, warnings, etc.) with exact semantics and defaults, see `references/compiler-options.md`. Don't guess at option names or defaults — this is a large, easy-to-misremember surface (e.g. `:static-fns` defaults to `false` except under `:advanced`, `:npm-deps` defaults to `false`). See Gotchas above for the most common failure patterns tied to these options.8182## Working style8384**Default to idiomatic Clojure(Script), not a literal JS translation.** This is the top priority whenever writing or rewriting CLJS, and it should win over "closest to what the JS/TS looked like" unless the user's existing codebase clearly does otherwise. Concretely, that means:8586- **Immutability and pure functions first.** Reach for persistent data structures (maps, vectors, sets) and pure transformations over mutable state, `atom`s, or `set!`. Only use an `atom`/`volatile!` for genuine local mutable state (e.g. component-local UI state), not as a default variable substitute.87- **Data over classes.** Model domain data as plain maps/records rather than JS-style classes with methods, unless polymorphism genuinely calls for `defprotocol`/`deftype`/`defrecord` or a JS class is required at an interop boundary (e.g. a React component).88- **Sequence/threading idioms over loops.** Prefer `map`/`filter`/`reduce`/`keep`/`for` and `->`/`->>` threading over `for`/`while`-style imperative loops, index juggling, or manual accumulation. This is a style preference, not a stack-safety rule: `loop`/`recur` is the *correct* (not just a perf-justified) choice for genuinely tail-recursive, scalar-producing logic — see Recursion above — reach for a transient only when profiling justifies it.89- **Destructuring over manual accessors.** Use `let`/fn-arg destructuring (`{:keys [...]}`, `[a b & rest]`) instead of repeated `(get m :k)` or positional indexing.90- **Small composed functions over long procedural bodies.** Break logic into small, named, testable functions and compose them, rather than one large function with sequential mutation-heavy steps.91- **Idiomatic control flow.** Prefer `cond`/`case`/`condp` and `when`/`if-let`/`when-let` over nested `if`s or JS-style early-return chains; prefer `^:async`/`await` or `core.async` (per the async section above) over manual `.then` chains.92- **Namespaced keywords and data-driven design** where they fit naturally (e.g. `:person/name` in shared/library code) rather than plain strings for map keys.9394When converting JS/TS to CLJS specifically: re-derive the idiomatic CLJS shape of the logic rather than transliterating line-by-line — e.g. turn `.then` chains into `^:async`/`await` or `core.async`, turn classes into `defrecord`/protocols or plain maps + functions, turn `for`-loops with accumulator variables into `reduce` or `into`.9596If the surrounding codebase has an established (even if less idiomatic) convention, match it for consistency rather than unilaterally "fixing" the whole file — but say so, and offer the idiomatic alternative as a suggestion rather than silently picking one or the other.9798Other working-style notes:99- When debugging, ask whether the error is happening under `:none` (dev) or `:advanced`/`:simple` (prod) optimizations — the failure modes are very different (the former is usually a logic/require bug, the latter is very often a renaming/externs issue).100101---102103104## Benchmark105106Scenario: `.benchmarks/scenarios/clojurescript-001-async-await.md` · Run: 2026-08-31 (salience re-run `wf_9a5588bc`) · Log: `.benchmarks/runs/2026-08-31/clojurescript-001-async-await.json`107108| Model | Without | With | Delta |109| ----------------- | ------- | ----- | ----- |110| claude-opus-4-8 | 67% | 100% | +33% |111| claude-sonnet-4-6 | 83% | 100% | +17% |112| claude-haiku-4-5 | 100% | 100% | +0% |113114> **PASS (run 2026-08-31)**. Salience re-run (✅/❌ `^:async`-placement pair added to the Async section, wf_9a5588bc): sonnet's arg-vector typo cleared (+17); gains on all models. The prior single-run sonnet −17 was a salience gap, not noise. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.115````