Click Audit — API

Paste the component and its store, get a behavioural click-path audit.

API tokens Open the app

Audit your click paths from your own scripts

Send component source together with the state store behind it — one file or several, each preceded by a // file: src/Name.tsx comment — and get back one JSON object: a posture, the store side-effect map with an ownership call on every action, every interactive touchpoint traced call by call with what each call writes and what it silently resets, and one finding per real bug, each with a corrected fragment. The bug class this is built for is the one static reading and ordinary review both miss: functions that are each correct in isolation and cancel each other out, so the handler is bound, nothing throws, the types are right — and the button still does nothing. Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS — so you can hang an audit off any pull request that touches a store or a handler. Pick a language once and the whole page follows.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api, app slug click-audit. Every request sends Authorization: Bearer <token> and JSON bodies with Content-Type: application/json. Responses are wrapped in an envelope: {"ok": true, "data": …} on success, {"ok": false, "error": {"code", "message", "status", "details"}} on failure — so read the result out of data and the reason out of error.message. The audit itself is produced by the gpt-terra model. Estimates are free; runs are metered against your credit balance. There is a single run task — one bundle of source in, one audit out, no follow-up calls and no session state to carry.

StatusMeaning
401Missing or expired token — create a new session.
402Not enough credits — top up at skillsafe.ai/account/credits.
403The token isn't allowed to do this (e.g. a guest auditing a very large paste).
404Unknown job or record id.
5xxTransient platform error — retry with backoff.

Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.

Step 0 — A tiny client

Every task below is a single HTTP call, so start with a short helper that adds the auth header, sends JSON and unwraps the data envelope. The later steps reuse it.

export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN"      # see step 1

# every call looks like:
#   curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, requests

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"  # see step 1 - read it from your shell environment in real code

def api(method, path, body=None, **headers):
    res = requests.request(method, API + path, json=body,
                           headers={"Authorization": f"Bearer {TOKEN}", **headers})
    payload = res.json()
    if not res.ok:
        raise RuntimeError(payload.get("error", {}).get("message", res.reason))
    return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see step 1 - read it from your shell environment in real code

async function api(method, path, body, extraHeaders = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
  return json.data;
}
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

const API = "https://api.skillsafe.ai/v1/app-api"

var token = os.Getenv("SKILLSAFE_TOKEN") // see step 1

func call(method, path string, body, out any) error {
	var buf bytes.Buffer
	if body != nil {
		json.NewEncoder(&buf).Encode(body)
	}
	req, _ := http.NewRequest(method, API+path, &buf)
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	var env struct {
		Data  json.RawMessage `json:"data"`
		Error *struct{ Message string `json:"message"` } `json:"error"`
	}
	json.NewDecoder(res.Body).Decode(&env)
	if res.StatusCode >= 400 {
		return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson...)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SkillSafe {
    static final String API = "https://api.skillsafe.ai/v1/app-api";
    static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see step 1
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String api(String method, String path, String jsonBody) throws Exception {
        var req = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + TOKEN)
            .header("Content-Type", "application/json")
            .method(method, jsonBody == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(jsonBody))
            .build();
        var res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() >= 400) throw new RuntimeException(res.body());
        return res.body(); // envelope: {"data": ...}
    }
}
require "net/http"
require "json"

API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN") # see step 1

def api(method, path, body = nil)
  uri = URI(API + path)
  req = Net::HTTP.const_get(method.capitalize).new(uri)
  req["Authorization"] = "Bearer #{TOKEN}"
  req["Content-Type"] = "application/json"
  req.body = body.to_json if body
  res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  payload = JSON.parse(res.body)
  raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
  payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN"); // see step 1

function api(string $method, string $path, ?array $body = null): mixed {
    global $TOKEN;
    $ch = curl_init(API . $path);
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer $TOKEN",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS     => $body === null ? null : json_encode($body),
    ]);
    $payload = json_decode(curl_exec($ch), true);
    $status  = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new Exception($payload["error"]["message"] ?? "HTTP $status");
    }
    return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;

static class SkillSafe
{
    const string Api = "https://api.skillsafe.ai/v1/app-api";
    static readonly HttpClient Http = new();

    static SkillSafe() =>
        Http.DefaultRequestHeaders.Authorization =
            new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see step 1

    public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null)
    {
        var req = new HttpRequestMessage(method, Api + path);
        if (body != null) req.Content = JsonContent.Create(body);
        var res = await Http.SendAsync(req);
        var json = await res.Content.ReadFromJsonAsync<JsonElement>();
        if (!res.IsSuccessStatusCode)
            throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
        return json.GetProperty("data");
    }
}

Step 1 — Get a token

POST /guest

A guest token lets you check balances and estimate costs for free. For metered audit runs billed to your own account, use your personal token: open the token page, sign in with SkillSafe, and press Copy shell export — it puts export SKILLSAFE_TOKEN="…" on your clipboard, which every example below reads. Treat the token like a password: it can spend your credits. For fully headless scripts, POST /guest mints a guest token with no browser involved.

curl -s -X POST "$API/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"click-audit"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "click-audit"})["token"]
const { token } = await api("POST", "/guest", { slug: "click-audit" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "click-audit"}, &guest)
String envelope = api("POST", "/guest", """
    {"slug":"click-audit"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "click-audit" })["token"]
$token = api("POST", "/guest", ["slug" => "click-audit"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
    new { slug = "click-audit" });
var token = guest.GetProperty("token").GetString();

The app stores this browser's token under the localStorage key skillsafe_app_token:click-audit, on the app's own origin. The token page reads and manages it for you — you never need to open developer tools.

Step 2 — Check who you are and your balance

GET /me

Returns subject_type ("user" or "guest"), subject_id and your credits balance. Check this before auditing a large paste.

curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
me = api("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
	SubjectType string `json:"subject_type"`
	Credits     int64  `json:"credits"`
}
err := call("GET", "/me", nil, &me)
String envelope = api("GET", "/me", null);
// data.subject_type, data.credits
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");

Step 3 — Estimate the cost

POST /estimate

Send exactly the input you would send to /run; the response's hold_credits is the worst-case cost. Nothing is charged and no job is created, so estimating is free — useful when you are piping a whole feature folder in and want a ceiling before spending credits. The reply also carries model and model_alias (which model will run the audit, and the alias the app asked for), markup_bps (the platform markup in basis points) and min_credits (the balance a run needs just to start — below that the run is refused rather than cut short).

Input fieldTypeNotes
codestring, requiredThe pasted source: the component holding the control that misbehaves and the store, slice, reducer or context it writes to — the bug is almost always in the gap between the two, so one without the other cannot be audited. One file or several concatenated, each preceded by a // file: src/Name.tsx comment. This is the model's only evidence — nothing is executed and no browser is launched. Inputs longer than 100,000 characters are clipped middle-out, with a // [... clipped ...] marker showing where. At least 60 characters are needed for an audit.
stackstring, requiredreact-zustand | react-redux | react-context | vue-pinia | svelte | other. A hint, not a fact: if the code says otherwise the audit follows the code and records the discrepancy in assumptions.
scopestring, requiredpage (every touchpoint in the pasted components) | store (the store's actions and every consumer of them that appears in the paste) | flow (one end-to-end journey through the paste).
intentstring, optionalThe symptom in your own words: which control, what you expect it to do, what actually happens, whether it ever worked, what changed recently. This is the single highest-value field you can add — "the New message button does nothing, but only after you have opened a thread first" turns a general audit into a targeted one, and the audit is required to address it explicitly. Clipped at 20,000 characters.
prescan_factsobjectWhat a client-side scanner mechanically matched in the code, as three arrays: {"stores": [], "touchpoints": [], "flags": []}. Each entry is {id, label}. Store ids look like action:usemail.selectthread, touchpoint ids like tp:new-message-9, and flag ids are <check>:<name>undo:new-message-composemode, dangerous-reset:usemail.selectthread, race:save-draft-saving, stale:increment-setn, no-transition:save, dead-path:submit-isvalid, effect-undo:composemode, non-atomic:checkout, unbound:retry-84. Every flag id you send comes back in coverage_check. The web UI fills this from its own free scan; see the note below for what to send when you have no scanner.
retry_notestring, optionalOnly set by the app's automatic reformat retry when a first reply was not valid JSON. Leave it out.
cat > paste.tsx <<'CODE'
// file: src/store/mailStore.ts
export const useMail = create((set) => ({
  composeMode: false, selectedThreadId: null,
  setComposeMode: (v) => set({ composeMode: v }),
  selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
}));

// file: src/components/ThreadList.tsx
const onNew = () => { setComposeMode(true); selectThread(null); };
return <button onClick={onNew}>New message</button>;
CODE

jq -n --rawfile code paste.tsx \
  '{code: $code,
    stack: "react-zustand",
    scope: "page",
    intent: "The New message button does nothing once a thread is open.",
    prescan_facts: {
      stores: [{id: "action:usemail.selectthread",
                label: "useMail.selectThread sets {selectedThreadId} clears {composeMode}"}],
      touchpoints: [{id: "tp:new-message-9",
                     label: "New message [onClick ThreadList.tsx:9]"}],
      flags: [{id: "undo:new-message-composemode",
               label: "New message sets composeMode, then selectThread clears it"}]
    }}' > input.json

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d @input.json | jq '.data | {model, model_alias, markup_bps, hold_credits, min_credits}'
CODE = """// file: src/store/mailStore.ts
export const useMail = create((set) => ({
  composeMode: false, selectedThreadId: null,
  setComposeMode: (v) => set({ composeMode: v }),
  selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
}));

// file: src/components/ThreadList.tsx
const onNew = () => { setComposeMode(true); selectThread(null); };
return <button onClick={onNew}>New message</button>;
"""

payload = {
    "code": CODE,
    "stack": "react-zustand",
    "scope": "page",
    "intent": "The New message button does nothing once a thread is open.",
    "prescan_facts": {
        "stores": [{"id": "action:usemail.selectthread",
                    "label": "useMail.selectThread sets {selectedThreadId} clears {composeMode}"}],
        "touchpoints": [{"id": "tp:new-message-9",
                         "label": "New message [onClick ThreadList.tsx:9]"}],
        "flags": [{"id": "undo:new-message-composemode",
                   "label": "New message sets composeMode, then selectThread clears it"}],
    },
}

est = api("POST", "/estimate", payload)
print(est["model"], est.get("model_alias"), "markup", est.get("markup_bps"), "bps")
print("worst case:", est.get("hold_credits", est.get("credits")), "credits",
      "| minimum to start:", est.get("min_credits"))
const code = [
  '// file: src/store/mailStore.ts',
  'export const useMail = create((set) => ({',
  '  composeMode: false, selectedThreadId: null,',
  '  setComposeMode: (v) => set({ composeMode: v }),',
  '  selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),',
  '}));',
  '',
  '// file: src/components/ThreadList.tsx',
  'const onNew = () => { setComposeMode(true); selectThread(null); };',
  'return <button onClick={onNew}>New message</button>;',
].join("\n");

const payload = {
  code,
  stack: "react-zustand",
  scope: "page",
  intent: "The New message button does nothing once a thread is open.",
  prescan_facts: {
    stores: [{ id: "action:usemail.selectthread",
               label: "useMail.selectThread sets {selectedThreadId} clears {composeMode}" }],
    touchpoints: [{ id: "tp:new-message-9",
                    label: "New message [onClick ThreadList.tsx:9]" }],
    flags: [{ id: "undo:new-message-composemode",
              label: "New message sets composeMode, then selectThread clears it" }],
  },
};

const est = await api("POST", "/estimate", payload);
console.log(est.model, est.model_alias, "markup", est.markup_bps, "bps");
console.log("worst case:", est.hold_credits ?? est.credits, "credits",
            "| minimum to start:", est.min_credits);
const code = `// file: src/store/mailStore.ts
export const useMail = create((set) => ({
  composeMode: false, selectedThreadId: null,
  setComposeMode: (v) => set({ composeMode: v }),
  selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
}));

// file: src/components/ThreadList.tsx
const onNew = () => { setComposeMode(true); selectThread(null); };
return <button onClick={onNew}>New message</button>;`

payload := map[string]any{
	"code":   code,
	"stack":  "react-zustand",
	"scope":  "page",
	"intent": "The New message button does nothing once a thread is open.",
	"prescan_facts": map[string]any{
		"stores": []any{map[string]string{
			"id":    "action:usemail.selectthread",
			"label": "useMail.selectThread sets {selectedThreadId} clears {composeMode}",
		}},
		"touchpoints": []any{map[string]string{
			"id": "tp:new-message-9", "label": "New message [onClick ThreadList.tsx:9]",
		}},
		"flags": []any{map[string]string{
			"id":    "undo:new-message-composemode",
			"label": "New message sets composeMode, then selectThread clears it",
		}},
	},
}

var est struct {
	Model       string `json:"model"`
	ModelAlias  string `json:"model_alias"`
	MarkupBps   int64  `json:"markup_bps"`
	HoldCredits int64  `json:"hold_credits"`
	MinCredits  int64  `json:"min_credits"`
}
err := call("POST", "/estimate", payload, &est)
String code = """
    // file: src/store/mailStore.ts
    export const useMail = create((set) => ({
      composeMode: false, selectedThreadId: null,
      setComposeMode: (v) => set({ composeMode: v }),
      selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
    }));

    // file: src/components/ThreadList.tsx
    const onNew = () => { setComposeMode(true); selectThread(null); };
    return <button onClick={onNew}>New message</button>;
    """;

String jsonPayload = """
    {"code": %s,
     "stack": "react-zustand",
     "scope": "page",
     "intent": "The New message button does nothing once a thread is open.",
     "prescan_facts": {
       "stores": [{"id": "action:usemail.selectthread",
                   "label": "useMail.selectThread clears composeMode"}],
       "touchpoints": [{"id": "tp:new-message-9", "label": "New message"}],
       "flags": [{"id": "undo:new-message-composemode",
                  "label": "New message sets composeMode, then selectThread clears it"}]
     }}
    """.formatted(toJsonString(code));

String envelope = api("POST", "/estimate", jsonPayload);
// data.model, data.model_alias, data.markup_bps, data.hold_credits, data.min_credits
CODE = <<~JS
  // file: src/store/mailStore.ts
  export const useMail = create((set) => ({
    composeMode: false, selectedThreadId: null,
    setComposeMode: (v) => set({ composeMode: v }),
    selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
  }));

  // file: src/components/ThreadList.tsx
  const onNew = () => { setComposeMode(true); selectThread(null); };
  return <button onClick={onNew}>New message</button>;
JS

payload = { code: CODE,
            stack: "react-zustand",
            scope: "page",
            intent: "The New message button does nothing once a thread is open.",
            prescan_facts: {
              stores: [{ id: "action:usemail.selectthread",
                         label: "useMail.selectThread sets {selectedThreadId} clears {composeMode}" }],
              touchpoints: [{ id: "tp:new-message-9",
                              label: "New message [onClick ThreadList.tsx:9]" }],
              flags: [{ id: "undo:new-message-composemode",
                        label: "New message sets composeMode, then selectThread clears it" }]
            } }

est = api("POST", "/estimate", payload)
puts "#{est["model"]} (#{est["model_alias"]}) markup #{est["markup_bps"]} bps"
puts "worst case: #{est["hold_credits"] || est["credits"]} credits, min #{est["min_credits"]}"
$code = <<<'JS'
// file: src/store/mailStore.ts
export const useMail = create((set) => ({
  composeMode: false, selectedThreadId: null,
  setComposeMode: (v) => set({ composeMode: v }),
  selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
}));

// file: src/components/ThreadList.tsx
const onNew = () => { setComposeMode(true); selectThread(null); };
return <button onClick={onNew}>New message</button>;
JS;

$payload = [
    "code"   => $code,
    "stack"  => "react-zustand",
    "scope"  => "page",
    "intent" => "The New message button does nothing once a thread is open.",
    "prescan_facts" => [
        "stores" => [["id" => "action:usemail.selectthread",
                      "label" => "useMail.selectThread clears composeMode"]],
        "touchpoints" => [["id" => "tp:new-message-9", "label" => "New message"]],
        "flags" => [["id" => "undo:new-message-composemode",
                     "label" => "New message sets composeMode, then selectThread clears it"]],
    ],
];

$est = api("POST", "/estimate", $payload);
echo "{$est['model']} ({$est['model_alias']}) markup {$est['markup_bps']} bps\n";
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) .
     " credits, min {$est['min_credits']}\n";
var code = """
    // file: src/store/mailStore.ts
    export const useMail = create((set) => ({
      composeMode: false, selectedThreadId: null,
      setComposeMode: (v) => set({ composeMode: v }),
      selectThread: (id) => set({ selectedThreadId: id, composeMode: false }),
    }));

    // file: src/components/ThreadList.tsx
    const onNew = () => { setComposeMode(true); selectThread(null); };
    return <button onClick={onNew}>New message</button>;
    """;

var payload = new {
    code,
    stack = "react-zustand",
    scope = "page",
    intent = "The New message button does nothing once a thread is open.",
    prescan_facts = new {
        stores = new[] { new { id = "action:usemail.selectthread",
                               label = "useMail.selectThread clears composeMode" } },
        touchpoints = new[] { new { id = "tp:new-message-9", label = "New message" } },
        flags = new[] { new { id = "undo:new-message-composemode",
                              label = "New message sets composeMode, then selectThread clears it" } },
    },
};

var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"{est.GetProperty("model")} markup {est.GetProperty("markup_bps")} bps");
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits, " +
                  $"min {est.GetProperty("min_credits")}");

About prescan_facts. The three arrays are how the audit is pinned to what you already know. stores and touchpoints are inventory — every touchpoint you list should come back in the touchpoints table, and one deliberately left out is explained in assumptions. flags is the part that is reconciled: every flag id you send comes back in coverage_check exactly once, addressed by a finding or explicitly set aside with a reason. An API client that has no scanner of its own may send the three arrays empty — {"stores": [], "touchpoints": [], "flags": []} — and the audit still runs, but nothing is reconciled and coverage_check comes back empty. That is a real quality loss, not a formality: unreconciled, the audit can quietly skip the control you cared about and you have no field to assert on. Send what you know, even if it is one hand-written flag naming the button that misbehaves.

Step 4 — Run the audit and wait for the result

POST /run
GET /jobs/{job_id}

/run takes the same input as /estimate, places a credit hold and returns a job_id. Poll /jobs/{job_id} every 1–2 seconds until status is succeeded or failed (a run typically takes 30–90 s, since every touchpoint carries a call-by-call trace and every finding a corrected fragment). Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The audit is in output — usually nested as output.output, and as a JSON string, so parse defensively. The samples below print the posture, the store side-effect map, the touchpoint verdicts and the findings, then save the whole object to audit.json.

JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: ca-$(date +%s)" \
  -d @input.json | jq -r '.data.job_id')

while :; do
  JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
  STATUS=$(echo "$JOB" | jq -r '.data.status')
  [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
  sleep 2
done

# unwrap the audit once, then read it
echo "$JOB" | jq -r '.data.output.output' > audit.json

jq -r '
  "\(.audit_name) [\(.posture)]: \(.verdict)",
  "",
  "STORE MAP",
  (.store_map[] | "  \(.store).\(.action) sets {\(.sets | join(", "))} clears {\(.resets | join(", "))} [\(.danger)]"),
  "",
  "TOUCHPOINTS",
  (.touchpoints[] | "  \(.id) \(.label) (\(.location)) -> \(.verdict)"),
  "",
  "FINDINGS",
  (.findings[] | "  [\(.severity)] \(.id) \(.pattern) \(.touchpoint): \(.actual)"),
  "",
  "QUICK WINS",
  (.quick_wins[] | "  - \(.)"),
  "",
  "COVERAGE",
  (.coverage_check[] | "  \(.id): \(if .addressed then "ok" else "SET ASIDE" end) - \(.note)")' \
  audit.json

# fail the pipeline on anything critical
jq -e '[.findings[] | select(.severity == "critical")] | length == 0' audit.json > /dev/null \
  || { echo "critical findings present"; exit 1; }
import time

job_id = api("POST", "/run", payload,
             **{"Idempotency-Key": "ca-001"})["job_id"]

while True:
    job = api("GET", f"/jobs/{job_id}")
    if job["status"] in ("succeeded", "failed"):
        break
    time.sleep(1.5)

if job["status"] == "failed":
    raise RuntimeError(job.get("error", "run failed"))

raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
    raw = raw["output"]
audit = json.loads(raw) if isinstance(raw, str) else raw

print(f'{audit["audit_name"]} [{audit["posture"]}]: {audit["verdict"]}')
for a in audit["store_map"]:
    print(f'  {a["store"]}.{a["action"]:<18} sets={a["sets"]} clears={a["resets"]} '
          f'not-owned={a["not_owned"]} danger={a["danger"]}')
for tp in audit["touchpoints"]:
    print(f'  {tp["id"]} {tp["label"]} ({tp["location"]}) -> {tp["verdict"]}')
    for s in tp["trace"]:
        mark = "!!" if s["conflict"] else "  "
        print(f'    {mark} {s["step"]}. {s["call"]} writes={s["writes"]} clears={s["resets"]}')
    print(f'       expected: {tp["expected"]}')
    print(f'       actual:   {tp["actual"]}')
for f in audit["findings"]:
    print(f'  [{f["severity"]:>8}] {f["id"]} {f["pattern"]} {f["touchpoint"]}')
    print(f'      why: {f["why"]}')
    print(f'      fix: {f["fix"]}')
    if f["snippet"]:
        print("      snippet:", f["snippet"].splitlines()[0], "...")
for w in audit["quick_wins"]:
    print("  win:", w)
for c in audit["coverage_check"]:
    print(f'  {c["id"]}: {"ok" if c["addressed"] else "SET ASIDE"} - {c["note"]}')

with open("audit.json", "w", encoding="utf-8") as fh:
    json.dump(audit, fh, indent=2)

critical = [f for f in audit["findings"] if f["severity"] == "critical"]
if critical:
    raise SystemExit(f"{len(critical)} critical finding(s)")
import { writeFileSync } from "node:fs";

const { job_id } = await api("POST", "/run", payload,
  { "Idempotency-Key": crypto.randomUUID() });

let job;
do {
  await new Promise((r) => setTimeout(r, 1500));
  job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");

if (job.status === "failed") throw new Error(job.error ?? "run failed");

const raw = job.output?.output ?? job.output;
const audit = typeof raw === "string" ? JSON.parse(raw) : raw;

console.log(`${audit.audit_name} [${audit.posture}]: ${audit.verdict}`);
for (const a of audit.store_map) {
  console.log(`  ${a.store}.${a.action} sets {${a.sets.join(", ")}} ` +
              `clears {${a.resets.join(", ")}} danger=${a.danger}`);
}
for (const tp of audit.touchpoints) {
  console.log(`  ${tp.id} ${tp.label} (${tp.location}) -> ${tp.verdict}`);
  for (const s of tp.trace) {
    console.log(`    ${s.conflict ? "!!" : "  "} ${s.step}. ${s.call} ` +
                `writes {${s.writes.join(", ")}} clears {${s.resets.join(", ")}}`);
  }
}
for (const f of audit.findings) {
  console.log(`  [${f.severity}] ${f.id} ${f.pattern} ${f.touchpoint}`);
  console.log(`      ${f.actual} - fix: ${f.fix}`);
}
for (const w of audit.quick_wins) console.log(`  win: ${w}`);
for (const c of audit.coverage_check) {
  console.log(`  ${c.id}: ${c.addressed ? "ok" : "SET ASIDE"} - ${c.note}`);
}

writeFileSync("audit.json", JSON.stringify(audit, null, 2));

const critical = audit.findings.filter((f) => f.severity === "critical");
if (critical.length) process.exitCode = 1;
var started struct{ JobID string `json:"job_id"` }
if err := call("POST", "/run", payload, &started); err != nil {
	log.Fatal(err)
}

var job struct {
	Status string          `json:"status"`
	Error  string          `json:"error"`
	Output json.RawMessage `json:"output"`
}
for {
	if err := call("GET", "/jobs/"+started.JobID, nil, &job); err != nil {
		log.Fatal(err)
	}
	if job.Status == "succeeded" || job.Status == "failed" {
		break
	}
	time.Sleep(1500 * time.Millisecond)
}

// job.Output is {"output": "<json string>"} - unwrap, then unmarshal:
type Audit struct {
	AuditName     string   `json:"audit_name"`
	Posture       string   `json:"posture"`
	Verdict       string   `json:"verdict"`
	ExecSummary   string   `json:"exec_summary"`
	Assumptions   []string `json:"assumptions"`
	OpenQuestions []string `json:"open_questions"`
	StoreMap []struct {
		Store, Action, Danger, Note string
		Sets, Resets, NotOwned      []string `json:"-"`
	} `json:"store_map"`
	Touchpoints []struct {
		ID, Label, Location, Handler string
		Expected, Actual, Verdict    string
		Trace []struct {
			Step           int
			Call           string
			Writes, Resets []string
			Conflict       bool
		} `json:"trace"`
		FindingIDs []string `json:"finding_ids"`
	} `json:"touchpoints"`
	Findings []struct {
		ID, Pattern, Severity, Touchpoint string
		Expected, Actual, Why, Fix, Snippet string
	} `json:"findings"`
	CoverageCheck []struct {
		ID, Note  string
		Addressed bool
	} `json:"coverage_check"`
	QuickWins []string `json:"quick_wins"`
	Summary   string   `json:"summary"`
}
var wrapper struct{ Output string `json:"output"` }
json.Unmarshal(job.Output, &wrapper)
var audit Audit
json.Unmarshal([]byte(wrapper.Output), &audit)

fmt.Printf("%s [%s]: %s\n", audit.AuditName, audit.Posture, audit.Verdict)
for _, a := range audit.StoreMap {
	fmt.Printf("  %s.%s danger=%s: %s\n", a.Store, a.Action, a.Danger, a.Note)
}
for _, tp := range audit.Touchpoints {
	fmt.Printf("  %s %s (%s) -> %s\n", tp.ID, tp.Label, tp.Location, tp.Verdict)
	for _, s := range tp.Trace {
		fmt.Printf("    %d. %s conflict=%v\n", s.Step, s.Call, s.Conflict)
	}
}
for _, f := range audit.Findings {
	fmt.Printf("  [%s] %s %s %s: %s\n", f.Severity, f.ID, f.Pattern, f.Touchpoint, f.Actual)
}
os.WriteFile("audit.json", []byte(wrapper.Output), 0o644)
String envelope = api("POST", "/run", jsonPayload);
String jobId = /* data.job_id via your JSON library */;

while (true) {
    String job = api("GET", "/jobs/" + jobId, null);
    String status = /* data.status */;
    if (status.equals("succeeded") || status.equals("failed")) break;
    Thread.sleep(1500);
}
// The audit is at data.output.output as a JSON string - parse it again, then read
// audit_name, posture, verdict, exec_summary, assumptions[], open_questions[],
// store_map[] (store/action/sets[]/resets[]/not_owned[]/danger/note),
// touchpoints[] (id/label/location/handler/trace[]/expected/actual/verdict/finding_ids[]),
//   where each trace entry is {step, call, writes[], resets[], conflict},
// findings[] (id/pattern/severity/touchpoint/expected/actual/why/fix/snippet),
// coverage_check[] (id/addressed/note), quick_wins[] and summary.
// Finally keep the audit on disk:
//   Files.writeString(Path.of("audit.json"), auditJson);
started = api("POST", "/run", payload)

job = nil
loop do
  job = api("GET", "/jobs/#{started["job_id"]}")
  break if %w[succeeded failed].include?(job["status"])
  sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"

raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
audit = raw.is_a?(String) ? JSON.parse(raw) : raw

puts "#{audit["audit_name"]} [#{audit["posture"]}]: #{audit["verdict"]}"
audit["store_map"].each do |a|
  puts "  #{a["store"]}.#{a["action"]} sets {#{a["sets"].join(", ")}} " \
       "clears {#{a["resets"].join(", ")}} danger=#{a["danger"]}"
end
audit["touchpoints"].each do |tp|
  puts "  #{tp["id"]} #{tp["label"]} (#{tp["location"]}) -> #{tp["verdict"]}"
  tp["trace"].each do |s|
    puts "    #{s["conflict"] ? "!!" : "  "} #{s["step"]}. #{s["call"]}"
  end
end
audit["findings"].each do |f|
  puts "  [#{f["severity"]}] #{f["id"]} #{f["pattern"]} #{f["touchpoint"]}"
  puts "      #{f["actual"]} - fix: #{f["fix"]}"
end
audit["quick_wins"].each { |w| puts "  win: #{w}" }
audit["coverage_check"].each { |c| puts "  #{c["id"]}: #{c["addressed"] ? "ok" : "SET ASIDE"}" }

File.write("audit.json", JSON.pretty_generate(audit))
exit 1 if audit["findings"].any? { |f| f["severity"] == "critical" }
$started = api("POST", "/run", $payload);

do {
    sleep(2);
    $job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));

if ($job["status"] === "failed") {
    throw new Exception($job["error"] ?? "run failed");
}

$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$audit = is_string($raw) ? json_decode($raw, true) : $raw;

echo "{$audit['audit_name']} [{$audit['posture']}]: {$audit['verdict']}\n";
foreach ($audit["store_map"] as $a) {
    echo "  {$a['store']}.{$a['action']} sets {" . implode(", ", $a["sets"]) . "} " .
         "clears {" . implode(", ", $a["resets"]) . "} danger={$a['danger']}\n";
}
foreach ($audit["touchpoints"] as $tp) {
    echo "  {$tp['id']} {$tp['label']} ({$tp['location']}) -> {$tp['verdict']}\n";
    foreach ($tp["trace"] as $s) {
        echo "    " . ($s["conflict"] ? "!!" : "  ") . " {$s['step']}. {$s['call']}\n";
    }
}
foreach ($audit["findings"] as $f) {
    echo "  [{$f['severity']}] {$f['id']} {$f['pattern']} {$f['touchpoint']}\n";
    echo "      {$f['actual']} - fix: {$f['fix']}\n";
}
foreach ($audit["quick_wins"] as $w) {
    echo "  win: $w\n";
}
foreach ($audit["coverage_check"] as $c) {
    echo "  {$c['id']}: " . ($c["addressed"] ? "ok" : "SET ASIDE") . "\n";
}

file_put_contents("audit.json", json_encode($audit, JSON_PRETTY_PRINT));
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload);
var jobId = started.GetProperty("job_id").GetString();

JsonElement job;
while (true)
{
    job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
    var status = job.GetProperty("status").GetString();
    if (status is "succeeded" or "failed") break;
    await Task.Delay(1500);
}

var rawText = job.GetProperty("output").GetProperty("output").GetString();
using var doc = JsonDocument.Parse(rawText!);
var audit = doc.RootElement;

Console.WriteLine($"{audit.GetProperty("audit_name")} " +
                  $"[{audit.GetProperty("posture")}]: {audit.GetProperty("verdict")}");
foreach (var a in audit.GetProperty("store_map").EnumerateArray())
{
    Console.WriteLine($"  {a.GetProperty("store")}.{a.GetProperty("action")} " +
                      $"danger={a.GetProperty("danger")}: {a.GetProperty("note")}");
}
foreach (var tp in audit.GetProperty("touchpoints").EnumerateArray())
{
    Console.WriteLine($"  {tp.GetProperty("id")} {tp.GetProperty("label")} " +
                      $"({tp.GetProperty("location")}) -> {tp.GetProperty("verdict")}");
    foreach (var s in tp.GetProperty("trace").EnumerateArray())
        Console.WriteLine($"    {s.GetProperty("step")}. {s.GetProperty("call")} " +
                          $"conflict={s.GetProperty("conflict")}");
}
foreach (var f in audit.GetProperty("findings").EnumerateArray())
{
    Console.WriteLine($"  [{f.GetProperty("severity")}] {f.GetProperty("id")} " +
                      $"{f.GetProperty("pattern")} {f.GetProperty("touchpoint")}");
}

await File.WriteAllTextAsync("audit.json", rawText!);

The model is asked for one JSON object and nothing else, but a stray code fence or preamble is always possible. Strip a leading ```json fence, take the text between the first { and the last }, and only then parse — that is what the app does before it falls back to a retry_note reformat run.

The audit object — output schema

One JSON object, always the same shape. The audit is behavioural and grounded in the pasted source alone: every touchpoint, action, field and file it names appears in code, and a finding must state the mechanism — the actual call sequence that produces the wrong final state — not a smell. Ordinary code-quality issues (naming, memoisation, prop drilling, type style) are out of scope unless they cause a wrong final state. Where the paste is silent on something that changes a verdict you get an entry in assumptions and, if it would settle an unclear touchpoint, in open_questions. Clean is a real verdict: a paste with no click-path bug comes back posture: "clean" with an empty or near-empty findings array, and the value sits in store_map, touchpoints and quick_wins instead.

FieldTypeMeaning
audit_namestringA short title naming the surface audited, taken from the code's own naming — e.g. Mailbox compose flow. Falls back to Untitled click-path audit if the model omits it.
posturestringclean | suspect | broken. See the table below.
verdictstringOne sentence: the single most important thing found, or that nothing was found.
exec_summarystringTwo or three short paragraphs, separated by blank lines: what the surface does, what the audit found, what to fix first. When you sent intent, it is addressed here directly.
assumptionsstring[]What had to be assumed because the paste did not show it. Read these first — a wrong assumption invalidates the findings built on it.
open_questionsstring[]What would need to be seen to close an unclear touchpoint verdict.
store_maparray{store, action, sets[], resets[], not_owned[], danger, note} — the side-effect map. Columns below. This is the part the flagship bug class is invisible without.
touchpointsarrayEvery interactive control in scope, traced call by call. Columns below. Controls that behave are included too, with verdict: "ok".
findingsarrayOne entry per real bug, worst first — ids CP-001, CP-002, … in sequence, no gaps. Columns below. May be empty on a clean paste.
coverage_checkarray{id, addressed, note} — one entry per prescan_facts.flags id you sent, each appearing exactly once. Semantics below.
quick_winsstring[]Changes worth making now, one line each. On a clean paste this is where "what to watch as the code grows" lands.
summarystringTwo or three sentences an engineer could paste into a pull request.

The three posture values, which follow the findings:

postureWhat it means
cleanNo click-path bug in this paste — no findings, or only low ones. The controls end in the state their labels promise. Genuinely correct code lands here rather than having severity manufactured for it; the useful output is then the store map (which actions are risky for future callers) and the quick wins.
suspectThe worst finding is high or medium: a control that works sometimes, a wrong final state a user will reach in a corner, or a store action that is a real hazard to the next caller even though no broken touchpoint is visible in the paste.
brokenAt least one critical finding: a primary control does nothing, or destroys user data. The canonical case is a handler whose second call silently resets what the first one set.

Each entry in store_map:

ColumnMeaning
storeThe store, slice, reducer, context provider or useState cluster the action belongs to — e.g. useMail.
actionThe action or setter name. Entries without an action are dropped by the client.
setsstring[] — the fields this action writes on purpose.
resetsstring[] — the fields it resets or clears as a side effect.
not_ownedstring[] — the subset of resets that a different action owns. Empty when the action only touches its own state. A non-empty not_owned is the enabling condition for the whole sequential-undo family, so this is the column to read first.
dangerhigh | medium | none — how risky this action is to call from somewhere else.
noteOne sentence a future caller needs to know before calling it.

Each entry in touchpoints:

ColumnMeaning
idTP-01, TP-02, … — the stable handle for the control.
labelThe control's own visible label, e.g. New message. Defaults to control when the label cannot be read.
locationfile:line within the paste, e.g. src/components/ThreadList.tsx:42.
handlerThe handler that runs — a named function, or inline onClick.
tracearray of {step, call, writes[], resets[], conflict} — every call the handler makes, in order, with the state each one writes, the state it clears as a side effect (from the store map), and conflict: true on the step that undoes an earlier one. A one-step trace is fine. Steps without a call are dropped by the client.
expectedThe final state the control's own label promises.
actualThe final state that really results.
verdictbug | ok | unclear. unclear means something outside the paste is needed to decide — look for the matching entry in open_questions.
finding_idsstring[] — the findings that cover this touchpoint. The client filters this to ids that actually exist in findings, so a dangling reference never reaches you.

Each entry in findings:

ColumnMeaning
idSequential CP-001, CP-002, … — the handle referenced from touchpoints[].finding_ids and from coverage_check[].note.
patternsequential-undo | async-race | stale-closure | missing-transition | dead-path | effect-interference | dangerous-reset | non-atomic | unbound | other. sequential-undo is the flagship: a later call resets state an earlier one set.
severitycritical | high | medium | low — measured in user impact, not code ugliness. critical = a primary control does nothing or destroys user data; high = it works sometimes, or leaves a wrong state a user will hit; medium = wrong state in a corner, or a real risk to the next caller; low = latent. This is the field to gate a pipeline on.
touchpointThe control this is about, as label (file:line) — always something that appears in code.
expectedWhat the control promises to leave behind.
actualWhat it really leaves behind.
whyThe mechanism, in terms of the specific calls and fields — the sequence that produces the wrong final state.
fixWhat to change, in one or two sentences.
snippetThe corrected fragment, compilable in place, referencing only identifiers that exist in the paste — no fences, no line numbers. Empty string when code is not the fix.

coverage_check semantics:

CaseWhat you get
Every flag id you sentEach prescan_facts.flags id appears in coverage_check exactly once. Nothing you flagged is silently dropped, which makes this the field to assert on in a CI check. Ids in prescan_facts.stores and prescan_facts.touchpoints are not reconciled here — they shape the store map and the touchpoint table instead.
addressed: trueThe flag is real and a finding covers it; note names that finding id.
addressed: falseThe flag was a false positive or does not matter here, and note says why in one concrete sentence — "setDraft and saveDraft write draftDirty in the same handler, but the second is inside the .then, so they cannot interleave".
Nothing sentSend the three arrays empty and coverage_check comes back empty. The audit still runs, but it is unreconciled — see the note in step 3.

What the client normalizes before you see it

The app's own parser is worth copying, because it turns a plausible-looking reply into a contract you can code against. It:

BehaviourDetail
Strips fences, takes the outermost objectA leading ```json fence is removed, then the text from the first { to the last } is what gets parsed. Anything the model said around the object is discarded.
Rejects an audit that traced nothingA reply with neither a touchpoint nor a finding raises an error rather than rendering as "all good" — that is a format failure, not a clean verdict, and it goes down the retry_note reformat lane.
Falls back on unknown enum valuesAn unrecognised pattern becomes other, an unrecognised severity becomes medium, an unrecognised touchpoint verdict becomes unclear, an unrecognised danger becomes none, and an unrecognised posture becomes suspect. Comparison is lowercased and trimmed, so casing never breaks a match.
Raises the posture to match the findingsIf the findings imply a worse posture than the reply claimed — a critical finding under posture: "clean" — the findings win, the posture is raised, and the change is surfaced in the run meta. It is never lowered.
Fills missing ids and drops empty rowsMissing findings[].id becomes CP-00n and missing touchpoints[].id becomes TP-0n by position; findings with no why, fix or actual, store-map rows with no action, and trace steps with no call are dropped.

A small, realistic result for the snippet above, trimmed for length:

{
  "audit_name": "Mailbox compose flow",
  "posture": "broken",
  "verdict": "New message calls selectThread(null) after setComposeMode(true), and selectThread
              clears composeMode as a side effect, so the compose pane never opens.",
  "exec_summary": "The paste is one Zustand store and the control that opens the compose pane.
                   Both functions in the handler are correct on their own: setComposeMode writes
                   the field it owns, selectThread writes the selected thread.

                   The bug is the pair. selectThread also sets composeMode: false, which
                   setComposeMode owns, so the second call in the handler undoes the first and the
                   button appears dead - exactly the symptom described in intent. Nothing throws
                   and the types are right, which is why ordinary debugging clears it.",
  "assumptions": [
    "create() is Zustand's, since set() takes a partial object.",
    "The compose pane renders on composeMode alone; no other gate was pasted."
  ],
  "open_questions": [
    "Does any other caller rely on selectThread closing the compose pane?"
  ],
  "store_map": [
    { "store": "useMail", "action": "setComposeMode",
      "sets": ["composeMode"], "resets": [], "not_owned": [],
      "danger": "none", "note": "Owns composeMode; safe to call from anywhere." },
    { "store": "useMail", "action": "selectThread",
      "sets": ["selectedThreadId"], "resets": ["composeMode"], "not_owned": ["composeMode"],
      "danger": "high",
      "note": "Clears composeMode, which setComposeMode owns - never call it after opening
               compose." }
  ],
  "touchpoints": [
    { "id": "TP-01", "label": "New message",
      "location": "src/components/ThreadList.tsx:9", "handler": "onNew",
      "trace": [
        { "step": 1, "call": "setComposeMode(true)", "writes": ["composeMode=true"],
          "resets": [], "conflict": false },
        { "step": 2, "call": "selectThread(null)", "writes": ["selectedThreadId=null"],
          "resets": ["composeMode=false"], "conflict": true }
      ],
      "expected": "The compose pane opens with no thread selected.",
      "actual": "selectedThreadId is cleared and composeMode ends false - the pane stays shut.",
      "verdict": "bug",
      "finding_ids": ["CP-001"] }
  ],
  "findings": [
    { "id": "CP-001", "pattern": "sequential-undo", "severity": "critical",
      "touchpoint": "New message (src/components/ThreadList.tsx:9)",
      "expected": "composeMode is true after the click, so the compose pane renders.",
      "actual": "composeMode is true for one call, then false; nothing visible happens.",
      "why": "onNew calls setComposeMode(true) at step 1, then selectThread(null) at step 2.
              selectThread's set() includes composeMode: false, a field setComposeMode owns, so
              step 2 overwrites step 1 inside the same batch and no render ever sees
              composeMode: true.",
      "fix": "Stop clearing composeMode inside selectThread and let the compose pane's own control
              own that field; close compose explicitly where a thread click really should close it.",
      "snippet": "selectThread: (id) => set({ selectedThreadId: id })," }
  ],
  "coverage_check": [
    { "id": "undo:new-message-composemode", "addressed": true, "note": "Covered by CP-001." }
  ],
  "quick_wins": [
    "Order the two calls the other way round as a stopgap - selectThread(null) first, then
     setComposeMode(true) - but fix the store, since the next caller will hit this again."
  ],
  "summary": "New message is dead because selectThread clears composeMode, a field it does not own.
              Remove that reset from the store action and close the compose pane from the control
              that owns it. Audit the other callers of selectThread before shipping."
}

This is AI-generated analysis of source text, not a test run: it sees only what you sent, never the running app, the real state at click time or the rest of the codebase. Check assumptions and open_questions before you act on the findings, run every snippet through your own tests and linter, and keep a human reviewer in the loop.

Step 5 — Stream the audit as it is written

POST /run-stream

/run-stream takes exactly the same body as /run but answers with server-sent events, so you can show progress instead of a spinner — useful here because a full trace table plus corrected code makes for a long reply. This app's own progress panel is this endpoint. Events are separated by a blank line; each has an event: line and a data: line carrying JSON.

EventPayloadMeaning
job{job_id, status}Sent once, when the job is accepted — show "starting".
delta{text}A chunk of the reply, in order. Append it; the accumulated length is your only progress signal (the total is not known in advance). The app advances its step list by watching for the "audit_name", "posture", "store_map", "touchpoints", "findings", "coverage_check" and "summary" keys as they arrive.
done{job_id, status, charged_credits, output}The final, authoritative result — read the audit from output.output rather than trusting concatenated deltas, and the settled price from charged_credits.
error{code, message}Replaces done when the run fails.
# -N disables buffering so events print as they arrive
curl -N -s -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: ca-$(date +%s)" \
  -d @input.json

# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"audit_name\":\"Mailbox"}
# ...
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":612,"output":{"output":"{...}"}}
import json, requests

result = None
with requests.post(
    API + "/run-stream",
    headers={"Authorization": f"Bearer {TOKEN}",
             "Idempotency-Key": "ca-001"},
    json=payload,
    stream=True,
) as r:
    r.raise_for_status()
    event = None
    for line in r.iter_lines(decode_unicode=True):
        if not line:
            continue
        if line.startswith("event:"):
            event = line[len("event:"):].strip()
        elif line.startswith("data:"):
            data = json.loads(line[len("data:"):].strip())
            if event == "delta":
                print(".", end="", flush=True)          # live progress
            elif event == "done":
                result = data
            elif event == "error":
                raise RuntimeError(data.get("message", "run failed"))

audit = json.loads(result["output"]["output"])            # authoritative
print("charged:", result["charged_credits"], "-", audit["audit_name"])
print("posture:", audit["posture"])
for f in audit["findings"]:
    print(f'  [{f["severity"]}] {f["id"]} {f["pattern"]} {f["touchpoint"]}: {f["actual"]}')
with open("audit.json", "w", encoding="utf-8") as fh:
    json.dump(audit, fh, indent=2)
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify(payload),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", done = null;

for (;;) {
  const chunk = await reader.read();
  if (chunk.done) break;
  buf += decoder.decode(chunk.value, { stream: true });
  const frames = buf.split("\n\n");
  buf = frames.pop();
  for (const frame of frames) {
    const name = /^event:\s*(.+)$/m.exec(frame)?.[1];
    const body = /^data:\s*(.+)$/m.exec(frame)?.[1];
    if (!name || !body) continue;
    const data = JSON.parse(body);
    if (name === "delta") process.stdout.write(".");   // live progress
    if (name === "done") done = data;
    if (name === "error") throw new Error(data.message ?? "run failed");
  }
}

const audit = JSON.parse(done.output.output);
console.log(`\n${done.charged_credits} credits - ${audit.audit_name} [${audit.posture}]`);
for (const f of audit.findings) console.log(`  [${f.severity}] ${f.id} ${f.touchpoint}`);
writeFileSync("audit.json", JSON.stringify(audit, null, 2));
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "ca-001")

res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

var event string
var final map[string]any
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
	line := sc.Text()
	switch {
	case strings.HasPrefix(line, "event:"):
		event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
	case strings.HasPrefix(line, "data:"):
		var data map[string]any
		json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &data)
		switch event {
		case "delta":
			fmt.Print(".") // live progress
		case "done":
			final = data
		case "error":
			log.Fatal(data["message"])
		}
	}
}
// final["output"].(map[string]any)["output"].(string) is the audit JSON -
// unmarshal it into the Audit struct from step 4, then write it to audit.json.
// Java 17+ - read the stream line by line instead of buffering the body.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "ca-001")
    .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
    .build();

var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
String event = null, done = null;
for (String line : (Iterable<String>) res.body()::iterator) {
    if (line.startsWith("event:")) {
        event = line.substring(6).trim();
    } else if (line.startsWith("data:")) {
        String data = line.substring(5).trim();
        if ("delta".equals(event)) System.out.print(".");   // live progress
        else if ("done".equals(event)) done = data;
        else if ("error".equals(event)) throw new RuntimeException(data);
    }
}
// parse `done`, then parse data.output.output again - it is a JSON string holding
// audit_name, posture, verdict, store_map[], touchpoints[] with their trace[],
// findings[], coverage_check[], quick_wins[] and the rest.
require "net/http"
require "json"

uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "ca-001"
req.body = payload.to_json

event = nil
done = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      chunk.each_line do |line|
        line = line.strip
        if line.start_with?("event:")
          event = line.delete_prefix("event:").strip
        elsif line.start_with?("data:")
          data = JSON.parse(line.delete_prefix("data:").strip)
          case event
          when "delta" then print "."           # live progress
          when "done"  then done = data
          when "error" then raise (data["message"] || "run failed")
          end
        end
      end
    end
  end
end

audit = JSON.parse(done["output"]["output"])
puts "\n#{done["charged_credits"]} credits - #{audit["audit_name"]} [#{audit["posture"]}]"
audit["findings"].each { |f| puts "  [#{f["severity"]}] #{f["id"]} #{f["touchpoint"]}" }
File.write("audit.json", JSON.pretty_generate(audit))
$event = null;
$done  = null;

$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST       => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $TOKEN",
        "Content-Type: application/json",
        "Idempotency-Key: ca-001",
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done) {
        foreach (explode("\n", $chunk) as $line) {
            $line = trim($line);
            if (str_starts_with($line, "event:")) {
                $event = trim(substr($line, 6));
            } elseif (str_starts_with($line, "data:")) {
                $data = json_decode(trim(substr($line, 5)), true);
                if ($event === "delta") { echo "."; }        // live progress
                elseif ($event === "done") { $done = $data; }
                elseif ($event === "error") { throw new Exception($data["message"] ?? "run failed"); }
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);

$audit = json_decode($done["output"]["output"], true);
echo "\n{$done['charged_credits']} credits - {$audit['audit_name']} [{$audit['posture']}]\n";
foreach ($audit["findings"] as $f) {
    echo "  [{$f['severity']}] {$f['id']} {$f['touchpoint']}\n";
}
file_put_contents("audit.json", json_encode($audit, JSON_PRETTY_PRINT));
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
    Content = JsonContent.Create(payload),
};
req.Headers.Add("Idempotency-Key", "ca-001");

using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());

string? evt = null, done = null;
while (await reader.ReadLineAsync() is { } line)
{
    if (line.StartsWith("event:")) evt = line[6..].Trim();
    else if (line.StartsWith("data:"))
    {
        var data = line[5..].Trim();
        if (evt == "delta") Console.Write(".");            // live progress
        else if (evt == "done") done = data;
        else if (evt == "error") throw new Exception(data);
    }
}

using var final = JsonDocument.Parse(done!);
var text = final.RootElement.GetProperty("output").GetProperty("output").GetString();
using var auditDoc = JsonDocument.Parse(text!);
var audit = auditDoc.RootElement;
Console.WriteLine($"{audit.GetProperty("audit_name")} [{audit.GetProperty("posture")}]");
foreach (var f in audit.GetProperty("findings").EnumerateArray())
    Console.WriteLine($"  [{f.GetProperty("severity")}] {f.GetProperty("id")} " +
                      $"{f.GetProperty("touchpoint")}");
await File.WriteAllTextAsync("audit.json", text!);

In a browser, the native EventSource only speaks GET, and this endpoint is a POST — read the fetch response body incrementally, as the JavaScript sample above does. On an idempotent replay the server may answer with a plain JSON envelope instead of an event stream; check the Content-Type before you start parsing frames.