Drive Clj Desk from your own code
Base URL https://api.skillsafe.ai/v1/app-api. One Clojure namespace goes in, one
structured result comes back. Everything on this page uses the same token the web app uses, which
you can read off the tokens page.
The task field comes first
This app has four lanes over one work object, and task selects which one you get. It
is the field to decide before any other: it changes the body key you get back, the
posture vocabulary, the area vocabulary, the artifacts, and the price.
Omit it and the model picks the lane your input best fits and names its choice in
verdict — convenient interactively, not something to rely on in a script.
task | Lane | What it reads | Body key | posture | Artifacts |
|---|---|---|---|---|---|
review | Style review | Every definition in file order: naming (nouns for pure functions, ! for side effects, ? for predicates, kebab-case, clojure.core shadowing), docstrings on public vars, private-by-default, form length and nesting, the style guide’s “use that instead” table, and the REST, module and Toucan conventions where present. | vars[] | mergeable / mergeable-with-changes / needs-rework | a rewritten form, a paste-ready PR comment block |
docstrings | Docstring contracts | The prose on its own. Every sentence against the rewrite test and the ownership test; implementation context relocated to an inline comment rather than deleted; only blather deleted. Per-var verdict plus the full replacement text. | docstrings[] | contracts-clear / contracts-mixed / contracts-misleading | one Clojure block of rewritten defn heads |
schemas | Malli schemas | Route params, query params, body and response for every endpoint — or, with no endpoints, the fn-arg and fn-return schemas of the mu/defn surface. Checks the validation-timing trap first. | schemas[] | fully-specified / partly-specified / unspecified | the mr/def registrations and rewritten endpoint heads |
mutants | Mutation coverage | Named mutants per operator — conditional boundary, negated conditional, arithmetic, constant, boolean, return value, collection, guard removal — each marked killed, survived or uncertain against the assertions you actually pasted. | mutants[] | well-covered / partly-covered / weakly-covered | deftest forms that kill the survivors |
The envelope
Every response has the same shape. Check ok before touching data.
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "...", "details": { ... }}}
| HTTP | error.code | What to do |
|---|---|---|
| 401 | UNAUTHORIZED | The token is missing, malformed or expired. Get a fresh one from the tokens page. |
| 402 | INSUFFICIENT_CREDITS | The balance is below min_credits. /estimate is free, so check it first. |
| 403 | FORBIDDEN | A guest token tried a metered call. /run and /run-stream need a personal token. |
| 404 | NOT_FOUND | Usually a job id that does not exist, or a mistyped path. |
| 409 | IDEMPOTENCY_CONFLICT | The same Idempotency-Key was reused with a different body. Change the key or send the original body. |
| 422 | VALIDATION_ERROR | The input object is the wrong shape. Note that the body is the input object — do not wrap it in an input key. |
| 429 | RATE_LIMITED | Back off and retry; do not tight-loop. |
| 503 | UPSTREAM_UNAVAILABLE | The model provider is briefly unavailable. Retry with the same idempotency key. |
Input fields
The request body is the input object itself. Do not wrap it in an
input key: a wrapped body returns 200 while hiding task from the model,
so you silently get whichever lane it guessed.
| Field | Type | Required | Meaning |
|---|---|---|---|
task | string | recommended | One of review, docstrings, schemas, mutants. |
source | string | yes | The Clojure text. Two namespaces in one string is the normal case — a source file and its test file pasted back to back are told apart by content, because the reader groups top-level forms under whichever ns form precedes them. A ;; file: name.clj line is honoured as a separator but never required. |
notes | string | no | What you are about to do with the namespace. Short and concrete sharpens the result a lot. |
prescan | object | no | The deterministic facts and flags the browser’s Clojure reader computes: the ns form and its aliases, every definition with its arglists and docstring size, the per-function mutation surface, the anti-pattern shapes, the test cross-reference. The web app always sends it, and the prompt requires one coverage_check entry per flag id. Omit it and you get a result with an empty coverage_check — still useful, but nothing holds the model to arithmetic. |
clip_note | string | no | Set this when you have truncated a long namespace yourself, so the model knows what it is missing. Cut on whole top-level forms; half a function is worse than no function. |
Output contract
job.output is a JSON string; parse it. Inside is one object: a common
envelope shared by all four lanes, plus that lane’s own body key from the table above. Taken
from the parser the web app itself uses.
{
"task": "review" | "docstrings" | "schemas" | "mutants",
"title": string,
"posture": one of the lane's three values,
"confidence": "high" | "medium" | "low",
"verdict": string, // one sentence, PR-comment ready
"exec_summary": string,
"findings": [{"id": "R-001" | "D-001" | "S-001" | "M-001",
"severity": "critical" | "high" | "medium" | "low",
"area": one of the lane's areas,
"target": string, // the var, the route, the line
"title": string,
"evidence": string,
"impact": string,
"remedy": string,
"blocks": boolean, // must land before merge
"cites": [string]}], // reader flag ids and var names
"coverage_check": [{"id": prescan flag id,
"status": "confirmed" | "set-aside" | "contradicted",
"note": string}],
"artifacts": [{"name": string, "language": "clojure" | "markdown" | "csv" | "edn" | "text",
"content": string}],
"assumptions": [string],
"open_questions": [string],
"next_steps": [string],
"summary": string,
// exactly one of these, matching "task":
"vars": [{"name","visibility","kind","lines","grade","issue","note"}],
"docstrings": [{"var","verdict","grade","was","now","relocated_to","issue"}],
"schemas": [{"target","layer","current","proposed","grade","issue"}],
"mutants": [{"id","target","operator","mutation","status","killed_by","note"}]
}
| Enum | Values |
|---|---|
grade | ok · watch · broken |
visibility | public · private |
kind | fn · macro · multi · method · protocol · record · type · interface · var · schema · endpoint · setting · test |
docstrings[].verdict | keep · tighten · relocate · delete · add |
schemas[].layer | route-param · query-param · body · response · fn-arg · fn-return |
mutants[].operator | conditional-boundary · negate-conditional · arithmetic · boolean-literal · constant · return-value · collection · guard-removal |
mutants[].status | killed · survived · uncertain |
area, review | naming · documentation · organization · style · tests · modules · rest-api · database · correctness · portability |
area, docstrings | contract · implementation-leak · ownership · duplication · absent · formatting · style |
area, schemas | route-params · query-params · body · response · timing · nullability · reuse · error-messages · naming |
area, mutants | conditional-boundary · negate-conditional · arithmetic · boolean-literal · constant · return-value · collection · guard-removal · coverage-gap |
1. A tiny client helper
Every endpoint below returns the same {ok, data, error} envelope and takes the same two headers, so one small helper covers the whole API. Get your token from the tokens page — no developer console required.
# Every call needs the same two headers. Keep the token out of your shell history:
# read it from a file you control, or paste it into a variable in a subshell.
TOKEN="YOUR_TOKEN"
BASE="https://api.skillsafe.ai/v1/app-api"
call() { # call <method> <path> [json]
curl -sS -X "$1" "$BASE/$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
${3:+--data "$3"}
}
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://clj-desk.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(method, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
body = json.load(r)
if not body.get("ok"):
raise RuntimeError(body.get("error"))
return body["data"]
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(method, path, payload) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: payload === undefined ? undefined : JSON.stringify(payload)
});
const body = await res.json();
if (!body.ok) throw new Error(JSON.stringify(body.error));
return body.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from /tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error json.RawMessage `json:"error"`
}
func call(method, path string, payload any) (json.RawMessage, error) {
var body io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
body = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s", env.Error)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class UrdfDesk {
static final String TOKEN = "YOUR_TOKEN"; // from /tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String json) throws Exception {
HttpRequest.BodyPublisher body = (json == null)
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(json);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, body)
.build();
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
return res.body(); // {"ok":true,"data":{...}} — check ok before using data
}
}
require "json"
require "net/http"
TOKEN = "YOUR_TOKEN" # from /tokens.html
BASE = URI("https://api.skillsafe.ai/v1/app-api")
def call(method, path, payload = nil)
uri = URI.join(BASE.to_s + "/", path.sub(%r{^/}, ""))
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload) if payload
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
body = JSON.parse(res.body)
raise body["error"].to_s unless body["ok"]
body["data"]
end
<?php
const TOKEN = "YOUR_TOKEN"; // from /tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
function call(string $method, string $path, ?array $payload = null) {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
],
]);
if ($payload !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($body["ok"])) {
throw new RuntimeException(json_encode($body["error"] ?? null));
}
return $body["data"];
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
class UrdfDesk {
const string Token = "YOUR_TOKEN"; // from /tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new HttpClient();
static async Task<JsonElement> Call(HttpMethod method, string path, object? payload = null) {
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (payload is not null) {
req.Content = new StringContent(
JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
}
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean()) {
throw new Exception(doc.RootElement.GetProperty("error").ToString());
}
return doc.RootElement.GetProperty("data");
}
}
2. Check the session and the balance
GET /me tells you whether the token is a guest or a personal session, and what the credit balance is. Do this before a metered call so a shortfall is something you handle rather than a 402 you are surprised by.
call GET me
me = call("GET", "/me")
print(me["type"], me.get("credits"))
const me = await call("GET", "/me");
console.log(me.type, me.credits);
data, err := call("GET", "/me", nil)
if err != nil {
panic(err)
}
fmt.Println(string(data))
System.out.println(call("GET", "/me", null));
me = call("GET", "/me")
puts "#{me["type"]} #{me["credits"]}"
$me = call("GET", "/me");
echo $me["type"], " ", $me["credits"] ?? "n/a", PHP_EOL;
var me = await Call(HttpMethod.Get, "/me");
Console.WriteLine(me.GetProperty("type"));
3. Price a lane for free
POST /estimate costs nothing and starts no job. It returns hold_credits (what will be reserved, priced against the full output cap), min_credits, and the model binding. The hold differs per lane, because the lanes have different prompts and output caps — estimate the lane you are actually about to run.
# /estimate is FREE and runs no job. It is the honest way to price a lane.
SRC=$(python3 -c 'import json,sys;print(json.dumps(open("reports_api.clj").read()))')
call POST estimate "{\"task\":\"review\",\"source\":$SRC}"
payload = {
"task": "review",
"source": open("reports_api.clj").read(),
"notes": "Opening a PR tomorrow; the POST endpoint is new and the schemas are thin."
}
est = call("POST", "/estimate", payload)
print(est["model"], est["model_alias"], est["markup_bps"])
print("reserved:", est["hold_credits"], "minimum:", est["min_credits"])
const payload = {
task: "review",
source: sourceText,
notes: "Freshly generated from CAD."
};
const est = await call("POST", "/estimate", payload);
console.log(est.model, est.hold_credits);
payload := map[string]any{
"task": "review",
"source": sourceText,
}
data, err := call("POST", "/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
String payload = """
{"task":"review","source":%s}
""".formatted(jsonQuote(sourceText));
System.out.println(call("POST", "/estimate", payload));
est = call("POST", "/estimate", {
"task" => "review",
"source" => File.read("reports_api.clj")
})
puts "#{est["model"]} reserves #{est["hold_credits"]}"
$est = call("POST", "/estimate", [
"task" => "review",
"source" => file_get_contents("reports_api.clj"),
]);
echo $est["model"], " reserves ", $est["hold_credits"], PHP_EOL;
var est = await Call(HttpMethod.Post, "/estimate", new {
task = "review",
source = File.ReadAllText("reports_api.clj")
});
Console.WriteLine(est.GetProperty("hold_credits"));
4. Run a lane and poll for it
POST /run is metered and returns a job_id; poll GET /jobs/{id} until status is succeeded or failed. Always send an Idempotency-Key: it makes a retry after a network blip free instead of double-billing. Include the lane in the key — two lanes over one namespace are two distinct runs and must not collide.
# Metered. The Idempotency-Key makes a retry safe: the same key returns the
# same job instead of billing twice.
KEY="clj-desk:review:$(shasum -a 256 reports_api.clj | cut -c1-16):a0"
SRC=$(python3 -c 'import json,sys;print(json.dumps(open("reports_api.clj").read()))')
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data "{\"task\":\"review\",\"source\":$SRC}" \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until terminal.
while :; do
OUT=$(call GET "jobs/$JOB")
echo "$OUT" | grep -q '"status":"succeeded"' && break
echo "$OUT" | grep -q '"status":"failed"' && { echo "$OUT"; exit 1; }
sleep 2
done
echo "$OUT"
import hashlib, time
source = open("reports_api.clj").read()
key = "clj-desk:review:%s:a0" % hashlib.sha256(source.encode()).hexdigest()[:16]
req = urllib.request.Request(BASE + "/run",
data=json.dumps({"task": "review", "source": source}).encode(),
method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
job = json.load(urllib.request.urlopen(req))["data"]["job_id"]
while True:
j = call("GET", "/jobs/" + job)
if j["status"] in ("succeeded", "failed"):
break
time.sleep(2)
review = json.loads(j["output"])
print(review["posture"], "-", review["verdict"])
const key = `clj-desk:review:${hash16(sourceText)}:a0`;
const res = await fetch(BASE + "/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key
},
body: JSON.stringify({ task: "review", source: sourceText })
});
const { data } = await res.json();
let job;
do {
await new Promise(r => setTimeout(r, 2000));
job = await call("GET", `/jobs/${data.job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
const review = JSON.parse(job.output);
console.log(review.posture, review.verdict);
b, _ := json.Marshal(map[string]any{
"task": "review", "source": sourceText,
})
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "clj-desk:review:"+hash16(sourceText)+":a0")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
// decode {"ok":true,"data":{"job_id":"..."}} then poll GET /jobs/{id}
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "clj-desk:review:" + hash16(sourceText) + ":a0")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
String created = HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
// read data.job_id, then poll GET /jobs/{id} until status is terminal
uri = URI.join(BASE.to_s + "/", "run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "clj-desk:review:#{Digest::SHA256.hexdigest(source)[0, 16]}:a0"
req.body = JSON.dump({ "task" => "review", "source" => source })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("GET", "/jobs/#{job_id}")
break puts JSON.parse(job["output"])["verdict"] if job["status"] == "succeeded"
raise job.to_s if job["status"] == "failed"
sleep 2
end
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: clj-desk:review:" . substr(hash("sha256", $source), 0, 16) . ":a0",
],
CURLOPT_POSTFIELDS => json_encode(["task" => "review", "source" => $source]),
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
do {
sleep(2);
$job = call("GET", "/jobs/" . $jobId);
} while (!in_array($job["status"], ["succeeded", "failed"], true));
$review = json_decode($job["output"], true);
echo $review["posture"], ": ", $review["verdict"], PHP_EOL;
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", $"clj-desk:review:{Hash16(source)}:a0");
req.Content = new StringContent(
JsonSerializer.Serialize(new { task = "review", description = source }),
Encoding.UTF8, "application/json");
var created = JsonDocument.Parse(
await (await Http.SendAsync(req)).Content.ReadAsStringAsync());
var jobId = created.RootElement.GetProperty("data").GetProperty("job_id").GetString();
// then poll GET /jobs/{jobId} until status is terminal
5. Stream it instead
POST /run-stream returns server-sent events. A full lane takes tens of seconds, so streaming lets you show progress. Concatenate every delta, then parse the accumulated text as one JSON object. The same Idempotency-Key rules apply.
# Server-sent events. Each data: line carries a delta; the terminal event
# carries the whole output. Useful because a full review takes tens of seconds.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data "{\"task\":\"mutants\",\"source\":$SRC}"
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps({"task": "mutants", "source": source}).encode(),
method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
buf = ""
with urllib.request.urlopen(req) as r:
for raw in r:
line = raw.decode().strip()
if line.startswith("data:"):
chunk = line[5:].strip()
if chunk and chunk != "[DONE]":
buf += json.loads(chunk).get("delta", "")
review = json.loads(buf[buf.index("{"):buf.rindex("}") + 1])
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key
},
body: JSON.stringify({ task: "mutants", source: sourceText })
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
for (const line of dec.decode(value).split("\n")) {
if (!line.startsWith("data:")) continue;
const chunk = line.slice(5).trim();
if (chunk && chunk !== "[DONE]") buf += (JSON.parse(chunk).delta || "");
}
}
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var buf bytes.Buffer
for sc.Scan() {
line := sc.Text()
if after, ok := strings.CutPrefix(line, "data:"); ok {
// unmarshal {"delta":"..."} and append
_ = after
}
}
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HTTP.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data:"))
.forEach(l -> appendDelta(l.substring(5).trim()));
uri = URI.join(BASE.to_s + "/", "run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump({ "task" => "mutants", "source" => source })
buf = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |seg|
seg.each_line do |line|
next unless line.start_with?("data:")
chunk = line[5..].strip
buf << (JSON.parse(chunk)["delta"] || "") unless chunk.empty? || chunk == "[DONE]"
end
end
end
end
$buf = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode(["task" => "mutants", "source" => $source]),
CURLOPT_WRITEFUNCTION => function ($ch, $seg) use (&$buf) {
foreach (explode("\n", $seg) as $line) {
if (str_starts_with($line, "data:")) {
$chunk = trim(substr($line, 5));
if ($chunk !== "" && $chunk !== "[DONE]") {
$buf .= json_decode($chunk, true)["delta"] ?? "";
}
}
}
return strlen($seg);
},
]);
curl_exec($ch);
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Content = new StringContent(payloadJson, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var buf = new StringBuilder();
while (await reader.ReadLineAsync() is string line) {
if (!line.StartsWith("data:")) continue;
var chunk = line[5..].Trim();
if (chunk.Length > 0 && chunk != "[DONE]") {
buf.Append(JsonDocument.Parse(chunk).RootElement
.GetProperty("delta").GetString());
}
}
One worked example per lane
The same namespace, four requests. Only task changes.
task: "review" — Style review
// request
{"task": "review",
"source": "(ns acme.reports.api\n \"HTTP surface for saved reports.\"\n ...)",
"notes": "Opening a PR tomorrow.",
"prescan": {"facts": {...}, "flags": [{"id": "C-NO-DOCSTRING:score", ...}], "counts": {...}}}
// response body, abridged
{"task": "review", "posture": "mergeable-with-changes", "confidence": "high",
"verdict": "Two blockers before merge: a process-wide atom and a mutating fn with no bang.",
"findings": [{"id": "R-001", "severity": "high", "area": "organization",
"target": "cache", "blocks": true, "cites": ["C-TOP-LEVEL-ATOM:21"], ...}],
"vars": [{"name": "score", "visibility": "public", "kind": "fn", "lines": 9,
"grade": "watch", "issue": "No docstring on a public ranking function.",
"note": "Turns a report map into a ranking number."}],
"coverage_check": [{"id": "C-NO-DOCSTRING:score", "status": "confirmed", "note": "..."}],
"artifacts": [{"name": "pr-comments.md", "language": "markdown", "content": "..."}]}
task: "docstrings" — Docstring contracts
// request
{"task": "docstrings", "source": "...", "prescan": {...}}
// response body, abridged
{"task": "docstrings", "posture": "contracts-mixed",
"verdict": "Four contracts are fine; one narrates the implementation and asserts conformance.",
"docstrings": [{"var": "normalize-tags",
"verdict": "tighten",
"grade": "watch",
"was": "This function loops over the tags and internally lowercases ...",
"now": "The tags, lower-cased, blanks removed, duplicates dropped, order kept.",
"relocated_to": "an inline comment above the ->> thread, where distinct+vec is chosen",
"issue": "Every sentence describes the implementation or asserts a default."}],
"artifacts": [{"name": "docstrings.clj", "language": "clojure", "content": "..."}]}
Note the key is literally "var". was is the current docstring verbatim;
now is the complete replacement text, not a diff. A var with no docstring comes back
with verdict: "add" and an empty was.
task: "schemas" — Malli schemas
// request
{"task": "schemas", "source": "...", "prescan": {...}}
// response body, abridged
{"task": "schemas", "posture": "partly-specified",
"verdict": "The response schema will fail on every request: ms/TemporalString cannot match a Java Time object.",
"findings": [{"id": "S-001", "severity": "critical", "area": "timing",
"target": "::Report", "blocks": true, ...}],
"schemas": [{"target": "::Report", "layer": "response",
"current": "[:created_at ms/TemporalString]",
"proposed": "[:created_at :any]",
"grade": "broken",
"issue": "Response schemas validate before JSON serialisation."},
{"target": "POST /report", "layer": "query-param",
"current": "",
"proposed": "[:map [:archived {:default false} [:maybe ms/BooleanValue]]]",
"grade": "broken",
"issue": "No schema on any parameter position."}],
"artifacts": [{"name": "schemas.clj", "language": "clojure", "content": "..."}]}
task: "mutants" — Mutation coverage
// request
{"task": "mutants", "source": "the namespace AND its test namespace", "prescan": {...}}
// response body, abridged
{"task": "mutants", "posture": "weakly-covered",
"verdict": "Nine of fourteen mutants survive; score is never named in a test.",
"mutants": [{"id": "M3", "target": "score", "operator": "conditional-boundary",
"mutation": "(> views 1000) -> (>= views 1000)",
"status": "survived", "killed_by": "",
"note": "ranking-test only compares two reports at 2000 and 5 views, so no assertion sits on the boundary."},
{"id": "M1", "target": "get-report-title", "operator": "negate-conditional",
"mutation": "(not (nil? (:name report))) -> (nil? (:name report))",
"status": "killed", "killed_by": "get-report-title-test",
"note": "Both branches are asserted, so inverting the test flips both results."}],
"artifacts": [{"name": "killing-tests.clj", "language": "clojure", "content": "..."}]}
A killed row always names a real deftest from the source you sent. If it
cannot, the status comes back uncertain instead — nothing here runs a mutation
harness, so a guess would be worse than an honest gap. Send the test namespace: without it every
status is uncertain and the lane becomes test authoring rather than coverage analysis.
Costs, and what not to do on a schedule
/me, /estimate and /guest are free and start no job.
/run and /run-stream are metered against the caller’s wallet. The
hold you see from /estimate prices the full output cap; what you are actually charged
is usually far lower and comes back as charged_credits on the finished job.
The holds differ per lane, and by a lot: mutants and schemas write more
output than review does. Estimate the lane you are about to run, not the one you ran
last time. If you wire this into CI, send an Idempotency-Key derived from the file
content and the lane so a re-run on an unchanged file costs nothing new, and gate the call on the
file having actually changed.
What this app does not do
It reads Clojure text and returns judgement over it. There is no REPL, no nREPL, no clj-kondo, no
test runner and no mutation harness behind this API. Nothing is evaluated, compiled, linted or
measured by running it. A mutant’s status is a read of the assertions you sent; a response
schema is derived from what the body plainly returns. The prompt forbids claiming otherwise and
reports what only a real run could settle in open_questions.
Some conventions are the Metabase house style rather than universal Clojure — the module
layout, the ms/* schema aliases, Toucan 2, api.macros/defendpoint. Where
the code you send is plainly not a Metabase namespace, the general Clojure rule is applied and the
finding says so.
Credits
Clj Desk is a derived work built on four agent skills published by the Metabase team: @metabase/clojure-review, @metabase/clojure-write, @metabase/add-malli-schemas and @metabase/mutation-testing. It is not a republication of those skills.