Code Review Desk — API

Paste code from your own tools — verdict, findings, fixes, back as plain text.

API tokens Open the app

Review code from CI, a pre-commit hook or a bot — not from a browser tab

Code Review Desk splits a review in two. The deterministic half — guessing the language, counting the size, and matching the classic patterns (secret-shaped assignments, string-built SQL, dynamic execution, innerHTML writes, swallowed exceptions, debug prints, TODO markers) with line numbers — runs in the browser, free, in /review.js. This API is the other half: the metered review that reads the code, decides which of those hits is real and which is a false positive, finds what the pattern scan can never find, and returns a verdict with every finding carrying where it is, the problem with its impact, and the fix to apply.

The reply is plain text, not JSON. Four tagged header lines and five ## sections, in a fixed order. That contract is documented in full below, and /report.js is a vendored parser for it you can lift straight into your pipeline.

Basics

Base URL https://api.skillsafe.ai/v1/app-api. Every response is the same envelope: {"ok":true,"data":{...}} on success, {"ok":false,"error":{"code":"...","message":"...","details":{...}}} on failure. Check ok before touching data.

POST /guest GET /me POST /estimate POST /run GET /jobs/{id} POST /run-stream

There is no app slug in the path. The routes are exactly the ones above — there is no /apps/{slug}/ segment anywhere. The slug is bound to the token, once, by POST /guest with a body of {"slug":"code-review-desk"}. Every later call just sends that token as a bearer and the platform already knows which app it belongs to.

Money. Credits are ten-thousandths of a dollar. The app runs on the gpt-terra alias, which resolves to gpt-5.6-terra, at markup_bps: 1000 — you pay the model's metered cost plus the publisher's 10%, and nothing to open the page. /estimate is free and creates no job.

Error codes

HTTPcodeWhat to do
400validation_errorThe body is not the shape the app expects. On /guest this is almost always a missing slug in the body — an X-App-Slug header is not accepted. On /run and /estimate it is almost always the input wrapped in an {"input": {...}} envelope, which this API does not take: post the input object itself.
401unauthorizedNo bearer token, or an expired guest token. Mint a new one with POST /guest, or take a personal one from /tokens.html.
402payment_requiredThe balance is below min_credits. Call /estimate first and check it against /me: a 402 after submit is a client bug, not a user error.
404not_foundWrong slug on /guest, or a job id that does not belong to this token. Note that a path like /apps/code-review-desk/run also lands here — there is no slug path segment.
409conflictThe request conflicts with existing state. Note that a reused Idempotency-Key does not 409: it replays, answering {"job_id", "deduped": true} with the original job even when the body has changed. Change the key whenever you want a new answer.
429rate_limitedBack off and retry with the same idempotency key; do not tight-loop.
500599internalTransient platform or upstream failure. Retry with the same idempotency key so a run that actually started is not billed twice.
A reply that does not satisfy the output contract is not an HTTP error: the run succeeds and returns text your parser rejects. That is a client-side condition, and the app handles it with one automatic reformat retry — see retry_note in the input table and step 6.

Step 1 · A tiny client, and a token

Two ways in. A personal token is the one this browser already holds — open /tokens.html and press Copy shell export, so you never have to open a DevTools console or dig through storage by hand. A guest token is minted by POST /guest with the slug in the body; that call is what binds the token to this app. Guests can call /me and /estimate freely and can run only when the app sponsors them — /estimate reports that as sponsor_enabled.

# Option A - take the token this browser already has: open /tokens.html,
# press "Copy shell export", and paste the line it gives you.
export SKILLSAFE_TOKEN="aut_..."

# Option B - mint a guest token. The slug goes in the BODY, and this is the
# only call that ever mentions it. An X-App-Slug header is not accepted and
# answers 400 "slug is required".
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
  -H "Content-Type: application/json" \
  -d '{"slug":"code-review-desk"}'
# -> {"ok":true,"data":{"token":"aut_...","subject_type":"guest","credits":0}}

# Every later call sends it as a bearer token, and the path never names the app:
#   -H "Authorization: Bearer $SKILLSAFE_TOKEN"
import json, urllib.request

BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "code-review-desk"
TOKEN = "YOUR_TOKEN"          # from /tokens.html, or minted below


def call(path, body=None, method=None, token=None):
    """Every endpoint in this API is JSON in, {data}/{error} out."""
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data,
                                 method=method or ("POST" if data else "GET"))
    req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", "Bearer " + token)
    with urllib.request.urlopen(req) as r:
        payload = json.load(r)
    if not payload.get("ok"):
        raise RuntimeError(payload.get("error", {}).get("code", "unknown"))
    return payload["data"]


if TOKEN == "YOUR_TOKEN":
    TOKEN = call("/guest", {"slug": SLUG})["token"]   # slug in the body, not a header
print(TOKEN[:12] + "...")
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "code-review-desk";
// Read the token from a constant or an injected global - never from a Node env object.
let TOKEN = globalThis.SKILLSAFE_TOKEN || "YOUR_TOKEN";

async function call(path, body, method) {
  const res = await fetch(BASE + path, {
    method: method || (body ? "POST" : "GET"),
    headers: {
      "Content-Type": "application/json",
      ...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {})
    },
    body: body ? JSON.stringify(body) : undefined
  });
  const payload = await res.json();
  if (!payload.ok) throw new Error(payload.error.code + ": " + payload.error.message);
  return payload.data;
}

if (TOKEN === "YOUR_TOKEN") {
  TOKEN = "";                                  // no bearer on the guest call
  TOKEN = (await call("/guest", { slug: SLUG })).token;
}
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"os"
)

const base = "https://api.skillsafe.ai/v1/app-api"
const slug = "code-review-desk"

// os.Getenv keeps the secret out of the source; a literal works just as well.
var token = os.Getenv("SKILLSAFE_TOKEN") // from /tokens.html, or minted by guest()

type envelope struct {
	OK    bool            `json:"ok"`
	Data  json.RawMessage `json:"data"`
	Error *struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func call(path string, body any, method string) (json.RawMessage, error) {
	var rdr io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		rdr = bytes.NewReader(b)
		if method == "" {
			method = "POST"
		}
	}
	if method == "" {
		method = "GET"
	}
	req, _ := http.NewRequest(method, base+path, rdr)
	req.Header.Set("Content-Type", "application/json")
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	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, errors.New(env.Error.Code + ": " + env.Error.Message)
	}
	return env.Data, nil
}

func guest() error {
	raw, err := call("/guest", map[string]string{"slug": slug}, "")
	if err != nil {
		return err
	}
	var out struct{ Token string }
	if err := json.Unmarshal(raw, &out); err != nil {
		return err
	}
	token = out.Token
	return nil
}
import java.net.URI;
import java.net.http.*;

public class CodeReviewDesk {
  static final String BASE = "https://api.skillsafe.ai/v1/app-api";
  static final String SLUG = "code-review-desk";
  static String token = "YOUR_TOKEN";     // from /tokens.html, or minted by guest()
  static final HttpClient HTTP = HttpClient.newHttpClient();

  static String call(String path, String jsonBody, String method) throws Exception {
    HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
        .header("Content-Type", "application/json");
    if (token != null && !token.isEmpty() && !token.equals("YOUR_TOKEN"))
      b.header("Authorization", "Bearer " + token);
    if (jsonBody != null) b.method(method == null ? "POST" : method,
        HttpRequest.BodyPublishers.ofString(jsonBody));
    else b.method(method == null ? "GET" : method, HttpRequest.BodyPublishers.noBody());
    HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() >= 400) throw new RuntimeException(res.body());
    return res.body();   // {"ok":true,"data":{...}} - parse with your JSON library
  }

  static void guest() throws Exception {
    token = "";
    String body = call("/guest", "{\"slug\":\"" + SLUG + "\"}", null);
    token = body.split("\"token\":\"")[1].split("\"")[0];
  }
}
require "json"
require "net/http"

BASE = URI("https://api.skillsafe.ai/v1/app-api")
SLUG = "code-review-desk"
TOKEN = ENV["SKILLSAFE_TOKEN"]   # from /tokens.html, or minted below

def call(path, body = nil, method: nil, token: TOKEN)
  uri = URI(BASE.to_s + path)
  klass = method == "DELETE" ? Net::HTTP::Delete : (body ? Net::HTTP::Post : Net::HTTP::Get)
  req = klass.new(uri, "Content-Type" => "application/json")
  req["Authorization"] = "Bearer #{token}" if token && !token.empty?
  req.body = JSON.dump(body) if body
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
  payload["data"]
end

TOKEN2 = TOKEN || call("/guest", { "slug" => SLUG }, token: nil)["token"]
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "code-review-desk";
$token = getenv("SKILLSAFE_TOKEN") ?: "";   // from /tokens.html, or minted below

function call(string $path, $body = null, ?string $method = null) {
    global $token;
    $headers = ["Content-Type: application/json"];
    if ($token !== "") $headers[] = "Authorization: Bearer $token";
    $ch = curl_init(BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_CUSTOMREQUEST => $method ?? ($body === null ? "GET" : "POST"),
    ]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $payload = json_decode(curl_exec($ch), true);
    curl_close($ch);
    if (empty($payload["ok"])) {
        throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
    }
    return $payload["data"];
}

if ($token === "") {
    $token = call("/guest", ["slug" => SLUG])["token"];   // slug in the body
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

class CodeReviewDesk {
  const string Base = "https://api.skillsafe.ai/v1/app-api";
  const string Slug = "code-review-desk";
  static string Token = "YOUR_TOKEN";       // from /tokens.html, or minted by GuestAsync()
  static readonly HttpClient Http = new();

  static async Task<JsonElement> CallAsync(string path, object? body = null, HttpMethod? method = null) {
    var req = new HttpRequestMessage(method ?? (body is null ? HttpMethod.Get : HttpMethod.Post), Base + path);
    if (body is not null)
      req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    if (Token is { Length: > 0 } and not "YOUR_TOKEN")
      req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
    var res = await Http.SendAsync(req);
    var payload = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
    if (!payload.GetProperty("ok").GetBoolean())
      throw new Exception(payload.GetProperty("error").GetProperty("code").GetString());
    return payload.GetProperty("data");
  }

  static async Task GuestAsync() {
    Token = "";
    Token = (await CallAsync("/guest", new { slug = Slug })).GetProperty("token").GetString()!;
  }
}

Step 2 · Check who you are and what you can spend

GET /me tells you the subject type, the balance, and the app's own model and markup. Compare the balance against /estimate before you submit — a 402 after submit is a client-side failure, not a user error.

curl -s https://api.skillsafe.ai/v1/app-api/me \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"ok":true,"data":{
#      "subject_type":"user","credits":184203,"app":{"slug":"code-review-desk",
#      "model":"gpt-terra","markup_bps":1000,"price_credits":0}}}
#
# credits are in ten-thousandths of a dollar: 184203 = $18.42.
# subject_type is "user" for a personal token and "guest" for a minted one.
me = call("/me", token=TOKEN)
print(me["subject_type"], me["credits"] / 10000, "USD")
const me = await call("/me");
console.log(me.subject_type, me.credits / 10000, "USD");
raw, err := call("/me", nil, "")
// raw is {"subject_type":"user","credits":184203,...}
String me = call("/me", null, null);
System.out.println(me);
me = call("/me")
puts "#{me['subject_type']} #{me['credits'] / 10_000.0} USD"
$me = call("/me");
printf("%s %.2f USD\n", $me["subject_type"], $me["credits"] / 10000);
var me = await CallAsync("/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits").GetInt32() / 10000.0} USD");

Step 3 · Price the run, and prove the model binding

POST /estimate costs nothing and creates no job. Its body is the input object itself — the same one /run takes, documented under The input below. It returns model, model_alias, markup_bps, hold_credits, min_credits and sponsor_enabled. On this app model reads gpt-5.6-terra, model_alias reads gpt-terra and markup_bps is 1000 (10%). Assert on those three in CI: they are the authoritative proof that the app is wired to the right model at the right markup. hold_credits is what gets reserved — it prices the full output cap and is usually far more than you end up paying.

# body.json is the INPUT OBJECT ITSELF - not {"input": {...}}:
#   {"code":"...","notes":"...","focus":"Security audit","facts":"..."}
#
# /estimate is free: no job is created, no credits are held, nothing is charged.
curl -s -X POST https://api.skillsafe.ai/v1/app-api/estimate \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d @body.json
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#      "markup_bps":1000,"hold_credits":2860,"min_credits":380,
#      "sponsor_enabled":false}}
#
# hold_credits is RESERVED, not charged: it prices the full output cap. What you
# pay is charged_credits on the finished job, and it is usually far lower.
# sponsor_enabled tells you whether a guest token may run at all.
body = {
    "code": open("payments.py").read(),
    "notes": "Flask service behind an API gateway that already does auth.",
    "focus": "Security audit",
}
est = call("/estimate", body, token=TOKEN)          # free, no job created
assert est["model"] == "gpt-5.6-terra"
assert est["model_alias"] == "gpt-terra"
assert est["markup_bps"] == 1000
print("reserved up to", est["hold_credits"] / 10000, "USD")
if me["credits"] < est["min_credits"]:
    raise SystemExit("top up first - a 402 after submit is a client bug, not a user error")
const body = {
  code: source,                       // the code to review, as a string
  notes: "Express handler; the ORM is off the table.",
  focus: "Full review"
};
const est = await call("/estimate", body);              // free, no job created
console.assert(est.model_alias === "gpt-terra" && est.markup_bps === 1000);
if (me.credits < est.min_credits) throw new Error("top up first");
body := map[string]any{
	"code":  source,
	"notes": "Go HTTP handler; the pool is shared process-wide.",
	"focus": "Performance pass",
}
raw, err = call("/estimate", body, "")
var est struct {
	Model       string `json:"model"`
	ModelAlias  string `json:"model_alias"`
	MarkupBps   int    `json:"markup_bps"`
	HoldCredits int    `json:"hold_credits"`
	MinCredits  int    `json:"min_credits"`
	Sponsored   bool   `json:"sponsor_enabled"`
}
json.Unmarshal(raw, &est)
// est.Model == "gpt-5.6-terra", est.ModelAlias == "gpt-terra", est.MarkupBps == 1000
// bodyJson is the input object itself: {"code":"...","focus":"Full review",...}
String est = call("/estimate", bodyJson, null);
// assert est.contains("\"model_alias\":\"gpt-terra\"");
// assert est.contains("\"markup_bps\":1000");
body = {
  "code" => source,
  "notes" => "Rails service object; ActiveRecord is available.",
  "focus" => "Correctness and error handling"
}
est = call("/estimate", body)
raise "wrong model" unless est["model_alias"] == "gpt-terra" && est["markup_bps"] == 1000
puts "reserved up to #{est['hold_credits'] / 10_000.0} USD"
$body = [
    "code" => $source,
    "notes" => "Laravel controller; validation happens in a form request.",
    "focus" => "Full review",
];
$est = call("/estimate", $body);
assert($est["model_alias"] === "gpt-terra" && $est["markup_bps"] === 1000);
var body = new {
  code = source,
  notes = "ASP.NET minimal API handler; EF Core is in play.",
  focus = "Full review"
};
var est = await CallAsync("/estimate", body);
if (est.GetProperty("model_alias").GetString() != "gpt-terra") throw new Exception("wrong model");

Step 4 · Run the review and poll for it

POST /run is metered and returns {"job_id":"job_..."}; poll GET /jobs/{job_id} until status is succeeded or failed. The body is the input object directly, exactly as for /estimate. Always send an Idempotency-Key request header derived from the input — a network blip or a retry with the same key returns the same job instead of billing twice.

Key it from the input, never from a clock. A good shape is code-review-desk:<inputhash>:a1. Because the hash comes from the input, every transport retry of one attempt replays the same job. The reformat retry — the one extra run you make when the first reply does not satisfy the output contract, carrying retry_note — is a different input, so it needs a different key: reuse the same base and bump the attempt counter to :a2 (the page itself appends -reformat to the same base, which does the same job without ever colliding with the key a later deliberate re-run would use). That way a malformed first reply can never double-bill the identical attempt, and the two runs stay separable in the ledger. Reusing the key here would not save money — it would replay the original job and hand you back the same malformed reply the retry exists to replace.

output.output on the finished job is the plain-text review. It is a string, not JSON — do not call a JSON parser on it. Step 6 splits it.

# Metered. Always send Idempotency-Key: a retry with the same key returns the
# same job instead of billing twice. Derive it from the input, not from a clock.
HASH=$(python3 -c 'import hashlib;print(hashlib.sha256(open("body.json","rb").read()).hexdigest()[:16])')
KEY="code-review-desk:$HASH:a1"     # the reformat retry sends :a2 with retry_note

JOB=$(curl -s -X POST https://api.skillsafe.ai/v1/app-api/run \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @body.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')

# Poll until terminal.
while :; do
  OUT=$(curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" \
    -H "Authorization: Bearer $SKILLSAFE_TOKEN")
  ST=$(echo "$OUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["status"])')
  [ "$ST" = "succeeded" ] || [ "$ST" = "failed" ] && break
  sleep 2
done
echo "$OUT" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'
# The output field is PLAIN TEXT - the review contract documented below.
import hashlib, time

digest = hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()[:16]
key = "code-review-desk:" + digest + ":a1"
req = urllib.request.Request(BASE + "/run", data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)          # a retry with this key never double-bills
with urllib.request.urlopen(req) as r:
    job_id = json.load(r)["data"]["job_id"]

while True:
    job = call("/jobs/" + job_id, token=TOKEN)
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(2)

reply = job["output"]["output"]                 # plain text, NOT json.loads
review = parse_review(reply)                    # step 6
print(review["verdict"], review["confidence"], len(review["critical"]), "critical")
print("charged", job.get("charged_credits", 0) / 10000, "USD")
const enc = new TextEncoder().encode(JSON.stringify(body));
const digest = [...new Uint8Array(await crypto.subtle.digest("SHA-256", enc))]
  .map(b => b.toString(16).padStart(2, "0")).join("").slice(0, 16);

const started = await fetch(BASE + "/run", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": "code-review-desk:" + digest + ":a1"
  },
  body: JSON.stringify(body)
}).then(r => r.json());

let job;
do {
  await new Promise(r => setTimeout(r, 2000));
  job = await call("/jobs/" + started.data.job_id);
} while (job.status !== "succeeded" && job.status !== "failed");

const review = parseReview(job.output.output);   // plain text in, object out - step 6
console.log(review.verdict, review.critical.length, "critical findings");
// POST /run needs the Idempotency-Key header, so build the request directly.
b, _ := json.Marshal(body)
sum := sha256.Sum256(b)
idemKey := "code-review-desk:" + hex.EncodeToString(sum[:8]) + ":a1"
req, _ := http.NewRequest("POST", base+"/run", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", idemKey)
res, _ := http.DefaultClient.Do(req)
// decode {"data":{"job_id":"..."}} then poll GET /jobs/{id} until status is
// terminal; data.output.output is the plain-text review, not JSON.
String key = "code-review-desk:" + Integer.toHexString(bodyJson.hashCode()) + ":a1";
HttpRequest run = HttpRequest.newBuilder(URI.create(BASE + "/run"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
    .build();
String started = HTTP.send(run, HttpResponse.BodyHandlers.ofString()).body();
// extract job_id, then poll GET /jobs/{id} every two seconds until terminal.
// data.output.output is a STRING holding the review - feed it to parseReview().
require "digest"

key = "code-review-desk:#{Digest::SHA256.hexdigest(JSON.dump(body))[0, 16]}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
                          "Authorization" => "Bearer #{TOKEN2}",
                          "Idempotency-Key" => key)
req.body = JSON.dump(body)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]

job = nil
loop do
  job = call("/jobs/#{job_id}", token: TOKEN2)
  break if %w[succeeded failed].include?(job["status"])
  sleep 2
end
review = parse_review(job["output"]["output"])   # plain text - step 6
puts review[:verdict]
$key = "code-review-desk:" . substr(hash("sha256", json_encode($body)), 0, 16) . ":a1";
$ch = curl_init(BASE . "/run");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer $token",
        "Idempotency-Key: $key",
    ],
]);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);

do {
    sleep(2);
    $job = call("/jobs/$jobId");
} while (!in_array($job["status"], ["succeeded", "failed"], true));
$review = parse_review($job["output"]["output"]);   // plain text - step 6
var json = JsonSerializer.Serialize(body);
var key = "code-review-desk:" + Convert.ToHexString(
    System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..16] + ":a1";

var run = new HttpRequestMessage(HttpMethod.Post, Base + "/run") {
  Content = new StringContent(json, Encoding.UTF8, "application/json")
};
run.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
run.Headers.Add("Idempotency-Key", key);
var started = JsonDocument.Parse(await (await Http.SendAsync(run)).Content.ReadAsStringAsync()).RootElement;
var jobId = started.GetProperty("data").GetProperty("job_id").GetString();

JsonElement job;
do {
  await Task.Delay(2000);
  job = await CallAsync("/jobs/" + jobId);
} while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"));

var reply = job.GetProperty("output").GetProperty("output").GetString()!;   // plain text

Step 5 · Or stream it

POST /run-stream is the same metered run over server-sent events, and it is what the app itself uses. Same body, same Idempotency-Key header. Frame names arrive on the event: line, not as a type field inside the payload — switch on the event name. The job frame carries the job_id and arrives first; every delta frame carries a text fragment, and concatenating them in order rebuilds the review; the terminal frame (done, or error when the run failed) carries status, charged_credits and truncated.

Prefer the done payload's own output.output when it is present — that is what the app does, because an SSE stream can drop its tail and a review missing its last section fails the contract for no good reason. If truncated is true the balance capped the output: render what parsed and say so, rather than presenting a half-finished review as complete.

# Server-sent events. Frame names arrive on the `event:` line, not as a field in
# the payload - switch on the event name, not on data.type.
curl -N -s -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d @body.json
#
# event: job      data: {"job_id":"job_..."}
# event: delta    data: {"text":"VERDICT: Do not merge\nLANGUAGE: Py"}
# event: delta    data: {"text":"thon\nCONFIDENCE: 88\nSUMMARY: ..."}
# event: done     data: {"status":"succeeded","charged_credits":1740,"truncated":false}
#
# A failed run ends on `event: error` with {"code":"...","message":"..."} instead.
# Concatenate every delta.text in order: the result is the plain-text review. The
# `done` frame also carries output.output when the run finished - prefer it, an
# SSE stream can drop its tail. truncated:true means the balance capped the reply.
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(body).encode(), method="POST")
req.add_header("Content-Type", "application/json")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Idempotency-Key", key)

raw, event, job_id, final = "", None, None, None
with urllib.request.urlopen(req) as stream:
    for line in stream:
        line = line.decode().rstrip("\n")
        if line.startswith("event: "):
            event = line[7:].strip()             # the frame NAME lives here
        elif line.startswith("data: "):
            payload = json.loads(line[6:])
            if event == "job":
                job_id = payload.get("job_id")
            elif event == "delta":
                raw += payload.get("text", "")
            elif event == "done":
                final = (payload.get("output") or {}).get("output")
                if payload.get("truncated"):
                    print("cut short by the balance - showing what arrived")
            elif event == "error":
                raise RuntimeError(payload.get("code", "internal"))

review = parse_review(final or raw)              # the done payload is authoritative
const res = await fetch(BASE + "/run-stream", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + TOKEN,
    "Idempotency-Key": "code-review-desk:" + digest + ":a1"
  },
  body: JSON.stringify(body)
});

const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "", event = null, final = null;
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buf += dec.decode(value, { stream: true });
  const lines = buf.split("\n");
  buf = lines.pop();
  for (const line of lines) {
    if (line.startsWith("event: ")) event = line.slice(7).trim();
    else if (line.startsWith("data: ")) {
      const p = JSON.parse(line.slice(6));
      if (event === "delta") raw += p.text || "";
      if (event === "done") {
        final = p.output && p.output.output;
        if (p.truncated) console.warn("truncated - keep the partial and say so");
      }
      if (event === "error") throw new Error(p.code + ": " + p.message);
    }
  }
}
const review = parseReview(final || raw);        // the done payload is authoritative
req, _ = http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", idemKey)
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
var raw, event string
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event: "):
		event = strings.TrimSpace(line[7:])
	case strings.HasPrefix(line, "data: ") && event == "delta":
		var d struct{ Text string }
		json.Unmarshal([]byte(line[6:]), &d)
		raw += d.Text
	}
}
// raw is the plain-text review; hand it to parseReview (step 6). The `done`
// frame's output.output carries the same text and is safer against a lost tail.
HttpRequest stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .header("Idempotency-Key", key)
    .POST(HttpRequest.BodyPublishers.ofString(bodyJson))
    .build();

StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
  if (line.startsWith("event: ")) event[0] = line.substring(7).trim();
  else if (line.startsWith("data: ") && "delta".equals(event[0])) {
    String d = line.substring(6);
    int i = d.indexOf("\"text\":\"");
    if (i >= 0) raw.append(d.substring(i + 8, d.lastIndexOf("\"")));   // use a JSON library
  }
});
// raw holds the review text once every delta has been unescaped properly.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json",
                          "Authorization" => "Bearer #{TOKEN2}",
                          "Idempotency-Key" => key)
req.body = JSON.dump(body)

raw = ""
final = nil
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line.chomp!
        if line.start_with?("event: ") then event = line[7..].strip
        elsif line.start_with?("data: ")
          p = JSON.parse(line[6..])
          raw << (p["text"] || "") if event == "delta"
          final = p.dig("output", "output") if event == "done"
        end
      end
    end
  end
end
review = parse_review(final || raw)
$raw = "";
$final = null;
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($body),
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer $token",
        "Idempotency-Key: $key",
    ],
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$final, &$event) {
        foreach (explode("\n", $chunk) as $line) {
            if (str_starts_with($line, "event: ")) {
                $event = trim(substr($line, 7));
            } elseif (str_starts_with($line, "data: ")) {
                $p = json_decode(substr($line, 6), true);
                if ($event === "delta") $raw .= $p["text"] ?? "";
                if ($event === "done") $final = $p["output"]["output"] ?? null;
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$review = parse_review($final ?? $raw);
var stream = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream") {
  Content = new StringContent(json, Encoding.UTF8, "application/json")
};
stream.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
stream.Headers.Add("Idempotency-Key", key);

using var res2 = await Http.SendAsync(stream, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res2.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null, line;
while ((line = await reader.ReadLineAsync()) is not null) {
  if (line.StartsWith("event: ")) evt = line[7..].Trim();
  else if (line.StartsWith("data: ") && evt == "delta")
    raw.Append(JsonDocument.Parse(line[6..]).RootElement.GetProperty("text").GetString());
}
var review = ParseReview(raw.ToString());

Step 6 · Parse the reply

The reply is plain text. Split it in two passes: read the four tagged header lines off the top, then walk the ## headings and collect the - bullets underneath each. Bullets in the three issue sections split on " | " into exactly four fields. A section with nothing to report holds the single bullet - None., which decodes to an empty list.

Validate before you trust it. If any header line is missing, the confidence is not an integer in 0–100, or any of the five sections is absent, the reply failed the contract: retry once with retry_note set (a different idempotency key — bump to :a2), and if the second attempt fails too, surface the raw text rather than a half-parsed review. That is what the app does: it re-runs once under its own -reformat key, tells you in the panel that this is one extra run, and falls back to the raw reply if the second attempt misses the contract as well.

# Header lines off the top:
VERDICT=$(sed -n 's/^VERDICT: *//p'    review.txt | head -1)
LANGUAGE=$(sed -n 's/^LANGUAGE: *//p'  review.txt | head -1)
CONF=$(sed -n 's/^CONFIDENCE: *//p'    review.txt | head -1)

# The summary: everything after "SUMMARY: " up to the first blank line.
SUMMARY=$(awk '/^SUMMARY: /{f=1;sub(/^SUMMARY: /,"")} f&&NF{printf "%s ",$0} f&&!NF{exit}' review.txt)

# One section's bullets, e.g. the critical findings:
awk '/^## Critical issues/{f=1;next} /^## /{f=0} f&&/^- /' review.txt

# Split a finding bullet into its four fields:
awk '/^## Critical issues/{f=1;next} /^## /{f=0} f&&/^- /' review.txt |
  sed 's/^- //' |
  awk -F' \\| ' '{print "finding: "$1"\nwhere:   "$2"\nproblem: "$3"\nfix:     "$4"\n"}'

# Gate the build on the verdict:
[ "$VERDICT" = "Do not merge" ] && { echo "blocked: $SUMMARY"; exit 1; }
echo "$VERDICT ($LANGUAGE, confidence $CONF)"
import re

SECTIONS = ["Critical issues", "High priority", "Medium priority",
            "What's solid", "Recommendations"]
KEYS = ["critical", "high", "medium", "solid", "recs"]


def parse_review(text):
    """Plain text in, dict out. Returns None when the contract is not met."""
    text = text.strip()
    if text.startswith("```"):                       # tolerate a stray fence
        text = re.sub(r"^```[^\n]*\n?", "", text)
        text = re.sub(r"\n?```$", "", text).strip()

    head = {}
    for tag in ("VERDICT", "LANGUAGE", "CONFIDENCE"):
        m = re.search(r"^%s:\s*(.+)$" % tag, text, re.M)
        if not m:
            return None
        head[tag.lower()] = m.group(1).strip()

    m = re.search(r"^SUMMARY:\s*([\s\S]*?)(?:\n\s*\n|\n##\s)", text + "\n\n", re.M)
    summary = " ".join(m.group(1).split()) if m else ""

    body, current = {k: None for k in KEYS}, None
    for line in text.split("\n"):
        h = re.match(r"^#{2,3}\s+(.*?)\s*$", line)
        if h:
            name = h.group(1).rstrip(":")
            current = KEYS[SECTIONS.index(name)] if name in SECTIONS else None
            if current:
                body[current] = []
            continue
        if current is not None and line.startswith("- "):
            body[current].append(line[2:].strip())

    if any(v is None for v in body.values()) or not summary:
        return None
    if head["verdict"] not in ("Ship it", "Needs fixes", "Do not merge", "Not reviewable"):
        return None
    if not re.fullmatch(r"\d{1,3}", head["confidence"]) or int(head["confidence"]) > 100:
        return None

    out = {"verdict": head["verdict"], "language": head["language"],
           "confidence": int(head["confidence"]), "summary": summary}
    for key in KEYS:
        items = [b for b in body[key] if b.lower().rstrip(".") != "none"]
        if key in ("critical", "high", "medium"):
            rows = []
            for item in items:
                parts = [p.strip() for p in item.split(" | ")]
                parts += [""] * (4 - len(parts))
                rows.append(dict(zip(("finding", "where", "problem", "fix"), parts[:4])))
            out[key] = rows
        else:
            out[key] = items
    return out


review = parse_review(reply)
if review is None:
    body["retry_note"] = RETRY_NOTE     # then re-run with idempotency key ...:a2
elif review["verdict"] == "Do not merge":
    raise SystemExit("blocked: " + review["critical"][0]["finding"])
const SECTIONS = {
  "Critical issues": "critical", "High priority": "high", "Medium priority": "medium",
  "What's solid": "solid", "Recommendations": "recs"
};
const VERDICTS = ["Ship it", "Needs fixes", "Do not merge", "Not reviewable"];

function parseReview(text) {
  let t = String(text || "").trim();
  if (t.startsWith("```")) t = t.replace(/^```[^\n]*\n?/, "").replace(/\n?```$/, "").trim();

  const head = {};
  for (const tag of ["VERDICT", "LANGUAGE", "CONFIDENCE"]) {
    const m = t.match(new RegExp("^" + tag + ":\\s*(.+)$", "m"));
    if (!m) return null;
    head[tag.toLowerCase()] = m[1].trim();
  }
  if (!VERDICTS.includes(head.verdict)) return null;
  const conf = Number(head.confidence);
  if (!Number.isInteger(conf) || conf < 0 || conf > 100) return null;

  const lines = t.split("\n");
  const body = {}, summary = [];
  let mode = null;
  for (const line of lines) {
    const h = line.match(/^#{2,3}\s+(.*?)\s*$/);
    if (h) {
      mode = SECTIONS[h[1].replace(/:$/, "")] || null;
      if (mode) body[mode] = [];
      continue;
    }
    const s = line.match(/^SUMMARY:\s*(.*)$/);
    if (s && !summary.length) { if (s[1].trim()) summary.push(s[1].trim()); mode = "__sum__"; continue; }
    if (mode === "__sum__") { if (!line.trim()) { mode = null; continue; } summary.push(line.trim()); continue; }
    if (mode && body[mode] && /^\s*-\s+/.test(line)) body[mode].push(line.replace(/^\s*-\s+/, "").trim());
  }
  if (Object.values(SECTIONS).some(k => !body[k]) || !summary.length) return null;

  const drop = a => a.filter(x => !/^none\.?$/i.test(x));
  const rows = a => drop(a).map(item => {
    const p = item.split(" | ").map(s => s.trim());
    return { finding: p[0] || "", where: p[1] || "General", problem: p[2] || "", fix: p[3] || "" };
  });
  return {
    verdict: head.verdict, language: head.language, confidence: conf,
    summary: summary.join(" "),
    critical: rows(body.critical), high: rows(body.high), medium: rows(body.medium),
    solid: drop(body.solid), recs: drop(body.recs)
  };
}

const review = parseReview(reply);
if (!review) { /* re-run once with retry_note and idempotency key ...:a2 */ }
else if (review.verdict === "Do not merge") process.exitCode = 1;
// Two passes: the tagged header lines, then the ## sections.
type Finding struct{ Finding, Where, Problem, Fix string }

var sections = map[string]string{
	"Critical issues": "critical", "High priority": "high", "Medium priority": "medium",
	"What's solid": "solid", "Recommendations": "recs",
}

func parseReview(text string) (verdict, language string, confidence int,
	summary string, out map[string][]string, ok bool) {

	out = map[string][]string{}
	var mode string
	var sum []string

	for _, line := range strings.Split(strings.TrimSpace(text), "\n") {
		switch {
		case strings.HasPrefix(line, "## "):
			mode = sections[strings.TrimSuffix(strings.TrimSpace(line[3:]), ":")]
			if mode != "" {
				out[mode] = []string{}
			}
		case strings.HasPrefix(line, "VERDICT: "):
			verdict, mode = strings.TrimSpace(line[9:]), ""
		case strings.HasPrefix(line, "LANGUAGE: "):
			language, mode = strings.TrimSpace(line[10:]), ""
		case strings.HasPrefix(line, "CONFIDENCE: "):
			confidence, _ = strconv.Atoi(strings.TrimSpace(line[12:]))
			mode = ""
		case strings.HasPrefix(line, "SUMMARY: "):
			sum, mode = append(sum, strings.TrimSpace(line[9:])), "sum"
		case mode == "sum" && strings.TrimSpace(line) == "":
			mode = ""
		case mode == "sum":
			sum = append(sum, strings.TrimSpace(line))
		case mode != "" && strings.HasPrefix(line, "- "):
			if item := strings.TrimSpace(line[2:]); !strings.EqualFold(strings.TrimSuffix(item, "."), "none") {
				out[mode] = append(out[mode], item)
			}
		}
	}
	summary = strings.Join(sum, " ")
	ok = verdict != "" && language != "" && summary != "" && len(out) == 5 &&
		confidence >= 0 && confidence <= 100
	return
}

// A finding bullet splits into exactly four fields:
func splitFinding(item string) Finding {
	p := strings.SplitN(item, " | ", 4)
	for len(p) < 4 {
		p = append(p, "")
	}
	return Finding{p[0], p[1], p[2], p[3]}
}
import java.util.*;

record Finding(String finding, String where, String problem, String fix) {}

static final Map<String, String> SECTIONS = Map.of(
    "Critical issues", "critical", "High priority", "high", "Medium priority", "medium",
    "What's solid", "solid", "Recommendations", "recs");

static Map<String, Object> parseReview(String text) {
  Map<String, List<String>> body = new HashMap<>();
  Map<String, Object> head = new HashMap<>();
  StringBuilder summary = new StringBuilder();
  String mode = "";

  for (String line : text.strip().split("\n")) {
    if (line.startsWith("## ")) {
      mode = SECTIONS.getOrDefault(line.substring(3).strip().replaceAll(":$", ""), "");
      if (!mode.isEmpty()) body.put(mode, new ArrayList<>());
    } else if (line.startsWith("VERDICT: "))    { head.put("verdict", line.substring(9).strip());  mode = ""; }
    else if (line.startsWith("LANGUAGE: "))     { head.put("language", line.substring(10).strip()); mode = ""; }
    else if (line.startsWith("CONFIDENCE: "))   { head.put("confidence", Integer.parseInt(line.substring(12).strip())); mode = ""; }
    else if (line.startsWith("SUMMARY: "))      { summary.append(line.substring(9).strip()); mode = "sum"; }
    else if (mode.equals("sum") && line.isBlank()) mode = "";
    else if (mode.equals("sum"))                  summary.append(" ").append(line.strip());
    else if (!mode.isEmpty() && line.startsWith("- ")) {
      String item = line.substring(2).strip();
      if (!item.replaceAll("\\.$", "").equalsIgnoreCase("none")) body.get(mode).add(item);
    }
  }
  if (body.size() != 5 || head.size() != 3 || summary.isEmpty()) return null;  // contract failure
  head.put("summary", summary.toString());
  head.put("sections", body);
  return head;
}

// A finding bullet has exactly four " | "-separated fields:
static Finding splitFinding(String item) {
  String[] p = Arrays.copyOf(item.split(" \\| ", 4), 4);
  for (int i = 0; i < 4; i++) if (p[i] == null) p[i] = "";
  return new Finding(p[0].strip(), p[1].strip(), p[2].strip(), p[3].strip());
}
SECTIONS = {
  "Critical issues" => :critical, "High priority" => :high, "Medium priority" => :medium,
  "What's solid" => :solid, "Recommendations" => :recs
}.freeze
VERDICTS = ["Ship it", "Needs fixes", "Do not merge", "Not reviewable"].freeze

def parse_review(text)
  t = text.to_s.strip
  head = {}
  %w[VERDICT LANGUAGE CONFIDENCE].each do |tag|
    m = t[/^#{tag}:\s*(.+)$/, 1]
    return nil unless m
    head[tag.downcase.to_sym] = m.strip
  end
  return nil unless VERDICTS.include?(head[:verdict])
  return nil unless head[:confidence] =~ /\A\d{1,3}\z/ && head[:confidence].to_i <= 100

  body = {}
  summary = []
  mode = nil
  t.each_line do |line|
    line = line.chomp
    if (h = line[/^\#{2,3}\s+(.*?)\s*$/, 1])
      mode = SECTIONS[h.sub(/:$/, "")]
      body[mode] = [] if mode
    elsif line.start_with?("SUMMARY: ")
      summary << line[9..].strip
      mode = :sum
    elsif mode == :sum
      line.strip.empty? ? mode = nil : summary << line.strip
    elsif mode && body[mode] && line.start_with?("- ")
      item = line[2..].strip
      body[mode] << item unless item.sub(/\.$/, "").casecmp?("none")
    end
  end
  return nil if SECTIONS.values.any? { |k| body[k].nil? } || summary.empty?

  rows = ->(a) { a.map { |i| f = i.split(" | ").map(&:strip)
                            { finding: f[0].to_s, where: f[1] || "General",
                              problem: f[2].to_s, fix: f[3].to_s } } }
  head.merge(confidence: head[:confidence].to_i, summary: summary.join(" "),
             critical: rows.(body[:critical]), high: rows.(body[:high]),
             medium: rows.(body[:medium]), solid: body[:solid], recs: body[:recs])
end
<?php
const SECTIONS = [
    "Critical issues" => "critical", "High priority" => "high",
    "Medium priority" => "medium", "What's solid" => "solid",
    "Recommendations" => "recs",
];
const VERDICTS = ["Ship it", "Needs fixes", "Do not merge", "Not reviewable"];

function parse_review(string $text): ?array {
    $text = trim($text);
    $head = [];
    foreach (["VERDICT", "LANGUAGE", "CONFIDENCE"] as $tag) {
        if (!preg_match("/^$tag:\s*(.+)$/m", $text, $m)) return null;
        $head[strtolower($tag)] = trim($m[1]);
    }
    if (!in_array($head["verdict"], VERDICTS, true)) return null;
    if (!preg_match('/^\d{1,3}$/', $head["confidence"]) || (int) $head["confidence"] > 100) return null;

    $body = [];
    $summary = [];
    $mode = null;
    foreach (explode("\n", $text) as $line) {
        if (preg_match('/^#{2,3}\s+(.*?)\s*$/', $line, $h)) {
            $mode = SECTIONS[rtrim($h[1], ":")] ?? null;
            if ($mode) $body[$mode] = [];
        } elseif (str_starts_with($line, "SUMMARY: ")) {
            $summary[] = trim(substr($line, 9));
            $mode = "sum";
        } elseif ($mode === "sum") {
            if (trim($line) === "") { $mode = null; } else { $summary[] = trim($line); }
        } elseif ($mode && str_starts_with(ltrim($line), "- ")) {
            $item = trim(substr(ltrim($line), 2));
            if (strcasecmp(rtrim($item, "."), "none") !== 0) $body[$mode][] = $item;
        }
    }
    foreach (SECTIONS as $key) if (!isset($body[$key])) return null;
    if (!$summary) return null;

    $rows = function (array $items): array {
        return array_map(function ($item) {
            $p = array_pad(array_map("trim", explode(" | ", $item)), 4, "");
            return ["finding" => $p[0], "where" => $p[1] ?: "General",
                    "problem" => $p[2], "fix" => $p[3]];
        }, $items);
    };
    return $head + [
        "confidence" => (int) $head["confidence"],
        "summary" => implode(" ", $summary),
        "critical" => $rows($body["critical"]), "high" => $rows($body["high"]),
        "medium" => $rows($body["medium"]),
        "solid" => $body["solid"], "recs" => $body["recs"],
    ];
}
using System.Text.RegularExpressions;

record Finding(string Finding_, string Where, string Problem, string Fix);

static readonly Dictionary<string, string> Sections = new() {
  ["Critical issues"] = "critical", ["High priority"] = "high",
  ["Medium priority"] = "medium", ["What's solid"] = "solid",
  ["Recommendations"] = "recs"
};
static readonly string[] Verdicts = { "Ship it", "Needs fixes", "Do not merge", "Not reviewable" };

static Dictionary<string, object>? ParseReview(string text) {
  var body = new Dictionary<string, List<string>>();
  var summary = new List<string>();
  string verdict = "", language = "", mode = "";
  int confidence = -1;

  foreach (var line in text.Trim().Split('\n')) {
    var h = Regex.Match(line, @"^#{2,3}\s+(.*?)\s*$");
    if (h.Success) {
      mode = Sections.GetValueOrDefault(h.Groups[1].Value.TrimEnd(':'), "");
      if (mode.Length > 0) body[mode] = new List<string>();
    }
    else if (line.StartsWith("VERDICT: "))    { verdict = line[9..].Trim(); mode = ""; }
    else if (line.StartsWith("LANGUAGE: "))   { language = line[10..].Trim(); mode = ""; }
    else if (line.StartsWith("CONFIDENCE: ")) { int.TryParse(line[12..].Trim(), out confidence); mode = ""; }
    else if (line.StartsWith("SUMMARY: "))    { summary.Add(line[9..].Trim()); mode = "sum"; }
    else if (mode == "sum" && line.Trim().Length == 0) mode = "";
    else if (mode == "sum") summary.Add(line.Trim());
    else if (mode.Length > 0 && line.TrimStart().StartsWith("- ")) {
      var item = line.TrimStart()[2..].Trim();
      if (!string.Equals(item.TrimEnd('.'), "none", StringComparison.OrdinalIgnoreCase))
        body[mode].Add(item);
    }
  }
  if (body.Count != 5 || summary.Count == 0 || !Verdicts.Contains(verdict)
      || confidence < 0 || confidence > 100) return null;   // contract failure

  static Finding Split(string item) {
    var p = item.Split(" | ", 4);
    Array.Resize(ref p, 4);
    return new Finding(p[0] ?? "", p[1] ?? "General", p[2] ?? "", p[3] ?? "");
  }
  return new Dictionary<string, object> {
    ["verdict"] = verdict, ["language"] = language, ["confidence"] = confidence,
    ["summary"] = string.Join(" ", summary),
    ["critical"] = body["critical"].Select(Split).ToList(),
    ["high"] = body["high"].Select(Split).ToList(),
    ["medium"] = body["medium"].Select(Split).ToList(),
    ["solid"] = body["solid"], ["recs"] = body["recs"]
  };
}
You do not have to write that parser. /report.js is the app's own implementation and the single place this contract is decoded — see the last section for how to load it in Node.

The input

One flat object, posted directly as the body of /estimate and /run. There is no input wrapper: {"code": "...", "focus": "Full review"} is the body, and {"input": {"code": ...}} is a 400 validation_error.

FieldTypeNotes
codestring, requiredThe code to review: a function, a module, a script or a unified diff, in any mainstream language. The web app clips anything over 60,000 characters by dropping the middle on whole-line boundaries and keeping both ends, with an in-band marker line naming the original line range that was removed - the end of a file carries the error handling and the entry point, and the later hunks of a diff are as much the change as the first ones, so a plain head-truncation throws away the wrong half. Driving the API yourself, you choose your own clipping; if you clip, say so in-band. Line references in the reply count from 1 over the text exactly as you send it, so do not renumber or reindent it first.
notesstring, optional, max 6,000 charsWhat the code does and where it runs, what you want checked, and the constraints (“the ORM is off the table”, “this is a prototype”, “auth is handled by the gateway”). Often the highest-value field in the body: it is what stops the review recommending something the team cannot do, and a constraint you state is taken at its word — though a critical finding is still reported, with your caveat attached in its problem field.
focusstring, requiredExactly one of Full review, Security audit, Performance pass or Correctness and error handling. Any other value is a 400. Full review weighs all categories in priority order; the narrower focuses set where the depth goes and never suppress a critical finding from another category.
factsstring, optionalPlain text: the output of the browser-side mechanical scan — a language guess, size counts, and pattern hits with line numbers. A hint, not a verdict. The review cross-checks it against the code: a hit the code does not actually misuse is a false positive and is dropped silently, and a real issue the scan missed still gets reported. You can generate the exact same block yourself with /review.js (last section), or omit the field entirely.
retry_notestring, optionalSent only on the app's automatic one-shot reformat retry, when the first reply failed the parse contract. It restates the required output shape in full. Callers driving the API themselves normally omit it — add it only on a second attempt after a failed parse, and give that attempt its own idempotency key.

A complete body

{
  "code": "import sqlite3\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nAPI_KEY = \"sk_live_9f2c1b7d4e8a\"\n\ndef get_user(user_id):\n    conn = sqlite3.connect(\"app.db\")\n    query = \"SELECT * FROM users WHERE id = \" + str(user_id)\n    try:\n        return conn.execute(query).fetchone()\n    except Exception:\n        pass\n\n@app.route(\"/users\")\ndef users():\n    ids = request.args.get(\"ids\", \"\").split(\",\")\n    return jsonify([get_user(i) for i in ids])\n",
  "notes": "Small internal Flask service behind an API gateway that already handles authentication. The ids list is usually short. We cannot add an ORM.",
  "focus": "Security audit",
  "facts": "Mechanical scan of the pasted code (pattern-matching, not judgement):\n- 18 non-empty lines, 486 chars; reads like Python.\n- Pattern hits: 1 possible hardcoded secret(s); 1 string-built SQL statement(s); 1 swallowed exception(s)."
}
Send the code as it is. The reply cites Line 9 or function get_user, and those references are only useful if the text you sent is the text you have in front of you. For a unified diff the reply references the hunk or the added line's content instead of absolute numbers.

The output contract

The model returns plain text — not JSON, and with no code fence around the whole response. Four tagged header lines, then five ## sections in a fixed order. These are the rules the app's own render path enforces, so a client that parses the same way will not be surprised:

Cross-rules the verdict must satisfy

VerdictWhat must hold
Ship itCritical issues and High priority are both - None. Medium findings and recommendations may still be present.
Needs fixesHigh-priority findings exist that a competent author can fix before shipping.
Do not mergeCritical issues holds at least one finding.
Not reviewableAll three issue sections are - None., and the summary says what was supplied instead of reviewable code.

The verdict follows the findings, never the reverse. If a reply breaks one of these, the safe reading is the findings: a “Ship it” that still lists a critical finding should be treated as “Needs fixes” until you have checked it, which is exactly what the app flags on screen.

A complete reply

VERDICT: Do not merge
LANGUAGE: Python
CONFIDENCE: 88
SUMMARY: A small Flask module exposing a /users endpoint that looks up rows by id. The user id is
concatenated straight into a SQL string and a live-looking API key is hardcoded at module scope, so
this cannot merge as written. The endpoint also queries once per id and swallows every database
error, which will make the injection above hard to notice in production.

## Critical issues
- SQL injection in the user lookup | Line 9, function get_user | `user_id` is concatenated into the
  query string, so a crafted id runs arbitrary SQL against app.db - the endpoint takes ids straight
  from the query string, so this is remotely reachable | Use a parameterized query:
  `conn.execute("SELECT * FROM users WHERE id = ?", (user_id,))`
- Hardcoded API key in source | Line 5 | `API_KEY` holds what looks like a live secret, so anyone
  with repository read access has it and it survives in git history | Read it from configuration at
  startup and rotate the exposed value now.

## High priority
- Every database error is swallowed | Lines 12-13, function get_user | The bare `except Exception:
  pass` returns None on any failure, so a broken query is indistinguishable from a missing user and
  the injection above fails silently | Let the exception propagate, or log it and raise a typed
  error the route can turn into a 500.
- One query per id, and the connection is never closed | Lines 8 and 19 | The list comprehension
  opens a new sqlite connection per id and none of them are closed; a long ids list exhausts file
  handles | Open one connection with a context manager and fetch with a single
  `WHERE id IN (...)` query.

## Medium priority
- No validation on the ids parameter | Line 18, function users | `ids` is split on commas with no
  type check, so non-numeric input reaches the query builder | Coerce each entry with `int()` inside
  a try block and reject the request with a 400 on failure.

## What's solid
- The route layer stays thin and the lookup lives in its own function, so fixing the query is a
  one-place change.
- `jsonify` is used for the response rather than hand-built JSON, which gets the content type and
  escaping right.

## Recommendations
- Add a test that requests `ids=1%20OR%201=1` and asserts the response is empty, so the injection
  cannot come back unnoticed.
- Consider `sqlite3.Row` as the row factory so callers get named columns instead of positional
  tuples.
No emoji anywhere, and no zero-width or variation-selector characters — the contract forbids them, so a reply carrying one is a signal that something upstream mangled the text.

The free lane is client-side, and you can have it too

The facts block is produced by one vendored module in the bundle, /review.js, with no network access and no dependencies. It exposes window.ReviewScan.scan(text) for the structured result (language guess, line and character counts, and every pattern hit with its line number) and window.ReviewScan.summarize(text) for the exact plain-text block this API takes as facts. The companion /report.js is the other half: window.ReviewReport.parseResult(text) decodes a reply into the review object and returns null when the contract is not met — the same check step 6 rebuilds by hand.

So a pipeline that only wants the mechanical scan does not need this API at all: load those two files in a browser or a JS runtime and call them. The metered endpoint is the judgement half — deciding which hit is real, finding what no pattern can find, and writing the fix.

<!-- In a browser: two plain script tags, no bundler, no network. -->
<script src="/review.js"></script>
<script src="/report.js"></script>

// In Node: both files are plain scripts that assign to `window`, so pointing
// `window` at the global object and requiring them is all it takes. No bundler,
// no network.
const fs = require("fs");
global.window = global;
require("./review.js");
require("./report.js");

const source = fs.readFileSync("payments.py", "utf8");

// The same `facts` string the app sends - hand it straight to /estimate and /run.
const facts = window.ReviewScan.summarize(source);
console.log(facts);

// The structured form, if you would rather gate on it directly:
const scan = window.ReviewScan.scan(source);
console.log(scan.language, scan.lines, "lines,", scan.secrets, "secret-shaped hits");

// And the decoder for the reply, contract check included:
const review = window.ReviewReport.parseResult(replyText);
if (!review) throw new Error("reply failed the output contract - retry with retry_note");
console.log(review.verdict, review.findings.critical.length, "critical findings");
if (review.verdict === "Do not merge") process.exitCode = 1;
The scan is pattern-matching, not judgement: it will flag a string that merely looks like a secret and it will miss a real vulnerability that no regular expression can see. Never gate a build on the scan alone — gate it on the verdict, which is the half that actually read the code.