Review your Swift concurrency from your own scripts
Send Swift — one file, a type, several files with // File: comment headers, or a
whole grab-bag of snippets — and get back one JSON object: an honest ready / adopt /
rework verdict, a health check across five concurrency areas, findings ranked by severity
each with corrected Swift, a ten-item checklist scored against the paste, a fully migrated
Swift 6.2 rewrite of what you pasted, and an ordered migration plan that starts with the
build settings. Everything this app does goes through the SkillSafe App API
— plain JSON over HTTPS — so you can wire the review into a CI gate, a
pull-request bot, or a pre-merge check that refuses a diff introducing a fresh
@unchecked Sendable over shared mutable state.
Every code step below is shown in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#;
pick a language once and the whole page follows.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api, app slug
swift-shift. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"data": …} on success, {"error": {"code", "message"}} on failure.
The review 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 paste
in, one review out, no follow-up calls and no session state to carry.
The input is the Swift you want migrated plus a little context: code, the
source_version it compiles under today, the target it builds into,
free-form notes, and an optional prescan object of mechanically
detected concurrency constructs — omit it and the review runs on the code alone. The
output is one JSON object: a ready / adopt / rework
verdict, a five-area health check, severity-ranked findings with corrected Swift, a ten-item
checklist, a fully migrated rewrite, and a migration_plan that
starts with build settings and ends with code changes.
| Status | Meaning |
|---|---|
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/credits. |
403 | The token isn't allowed to do this (e.g. a guest reviewing a very large paste). |
404 | Unknown job or record id. |
5xx | Transient 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
A guest token lets you check balances and estimate costs for free. For metered review 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":"swift-shift"}' | jq -r '.data.token'
token = api("POST", "/guest", {"slug": "swift-shift"})["token"]
const { token } = await api("POST", "/guest", { slug: "swift-shift" });
var guest struct{ Token string `json:"token"` }
err := call("POST", "/guest", map[string]string{"slug": "swift-shift"}, &guest)
String envelope = api("POST", "/guest", """
{"slug":"swift-shift"}""");
// token is at data.token in the returned JSON
token = api("POST", "/guest", { slug: "swift-shift" })["token"]
$token = api("POST", "/guest", ["slug" => "swift-shift"])["token"];
var guest = await SkillSafe.ApiAsync(HttpMethod.Post, "/guest",
new { slug = "swift-shift" });
var token = guest.GetProperty("token").GetString();
The app stores this browser's token under the localStorage key
skillsafe_app_token:swift-shift, 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
Returns subject_type ("user" or "guest"),
subject_id and your credits balance. Check this before reviewing
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
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 feeding in a whole diff or a directory of
source files and want a ceiling before spending credits.
| Input field | Type | Notes |
|---|---|---|
code | string, required | The Swift source to review, up to 100000 characters: one file, or several files concatenated with // File: Name.swift comment headers. Very long pastes are clipped middle-out, with a // [... clipped ...] marker showing where. |
source_version | string | swift5 | swift60 | swift61 | unknown — the language mode the code compiles under today. It calibrates the migration plan: how far the paste already is from Swift 6.2, and which diagnostics it is currently seeing. |
target | string | app | library | executable | unknown — what the code is. app and executable targets are candidates for MainActor default inference (SE-0466); library targets must keep their public isolation explicit, since callers depend on it. On unknown the review infers from the paste and says which it assumed. |
notes | string, optional | Extra context: what the code does, which APIs cannot break, what is intentionally unfinished, known hot paths. |
prescan | object, optional | What the client-side prescanner mechanically detected: patterns (array of {id, label, lines} — concurrency constructs matched by regex, with ids like dq:dispatch-queue, lock:mutex, cb:completion-handler, task:unstructured, ma:mainactor, conc:concurrent, noniso:nonisolated, unchecked:sendable, send:sendable, global:mutable-static, async:async-func, await:await-expr, file:multi-file), types (array of {id, label, lines} — declared class/struct/actor/enum names, with ids like type:actor:JobStore), and signals (counts plus the derived booleans legacyGcd, callbackStyle and sharedMutableGlobals). Every patterns and types id you send must be reconciled by the review in coverage_check. The web UI fills this from its own scan; API callers may omit it entirely and the review runs on the code alone. |
retry_note | string, optional | Only set by the app's automatic reformat retry when a first reply was not valid JSON. Leave it out. |
cat > ImageCache.swift <<'SWIFT'
// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}
SWIFT
jq -n --rawfile c ImageCache.swift \
'{code: $c, source_version: "swift5", target: "app",
notes: "Shared image cache used from view code; must stay a singleton."}' > input.json
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json | jq '.data.hold_credits'
CODE = """// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}"""
payload = {
"code": CODE,
"source_version": "swift5",
"target": "app",
"notes": "Shared image cache used from view code; must stay a singleton.",
}
est = api("POST", "/estimate", payload)
print("worst case:", est.get("hold_credits", est.get("credits")), "credits")
const code = `// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}`;
const payload = {
code,
source_version: "swift5",
target: "app",
notes: "Shared image cache used from view code; must stay a singleton.",
};
const est = await api("POST", "/estimate", payload);
console.log("worst case:", est.hold_credits ?? est.credits, "credits");
const code = `// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}`
payload := map[string]any{
"code": code,
"source_version": "swift5",
"target": "app",
"notes": "Shared image cache used from view code; must stay a singleton.",
}
var est struct{ HoldCredits int64 `json:"hold_credits"` }
err := call("POST", "/estimate", payload, &est)
String code = """
// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}""";
String jsonPayload = """
{"code": %s, "source_version": "swift5", "target": "app",
"notes": "Shared image cache used from view code; must stay a singleton."}
""".formatted(toJsonString(code));
String envelope = api("POST", "/estimate", jsonPayload);
// worst-case cost is at data.hold_credits
CODE_TEXT = <<~'SWIFT'
// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}
SWIFT
payload = { code: CODE_TEXT, source_version: "swift5", target: "app",
notes: "Shared image cache used from view code; must stay a singleton." }
est = api("POST", "/estimate", payload)
puts "worst case: #{est["hold_credits"] || est["credits"]} credits"
$code = <<<'SWIFT'
// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}
SWIFT;
$payload = [
"code" => $code,
"source_version" => "swift5",
"target" => "app",
"notes" => "Shared image cache used from view code; must stay a singleton.",
];
$est = api("POST", "/estimate", $payload);
echo "worst case: " . ($est["hold_credits"] ?? $est["credits"]) . " credits\n";
var code = """
// File: ImageCache.swift
import Foundation
final class ImageCache {
static let shared = ImageCache()
private var store: [String: Data] = [:]
private let queue = DispatchQueue(label: "cache")
func load(_ key: String, completion: @escaping (Data?) -> Void) {
queue.async {
completion(self.store[key])
}
}
}
""";
var payload = new {
code,
source_version = "swift5",
target = "app",
notes = "Shared image cache used from view code; must stay a singleton.",
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");
prescan is optional — omit it and the review runs on the code alone
— but it is how you make the review answer for things you already know about. Send
{"patterns": [{"id": "dq:dispatch-queue", "label": "DispatchQueue", "lines": [7, 10]},
{"id": "cb:completion-handler", "label": "completion handler", "lines": [9]},
{"id": "global:mutable-static", "label": "mutable static state", "lines": [5, 6]}],
"types": [{"id": "type:class:ImageCache", "label": "class ImageCache", "lines": [4]}],
"signals": {"legacyGcd": true, "callbackStyle": true, "sharedMutableGlobals": true}}
and every one of those patterns and types ids comes back in
coverage_check — addressed, or explained away as a false positive (a
DispatchQueue used only as a serial label for logging is not a data race, and
the review says so). Nothing you flag is silently dropped.
Step 4 — Run the review and wait for the result
/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 the migrated rewrite is written out in full). Always send an
Idempotency-Key header so a network retry can't start a second,
double-charged run. The review is in output — usually nested as
output.output, and as a JSON string, so parse defensively. The samples
below print the review name and verdict, the five health areas, the findings and the
migration plan, then write rewrite.code to Migrated.swift using
rewrite.filename.
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: review-$(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 review once, then read it
echo "$JOB" | jq -r '.data.output.output' > review.json
jq -r '
"\(.review_name) [\(.verdict_level)]: \(.verdict)",
"",
"HEALTH",
(.health[] | " [\(.status)] \(.area) - \(.note)"),
"",
"FINDINGS",
(.findings[] | " (\(.severity)) \(.category): \(.title)"),
"",
"CHECKLIST",
(.checklist[] | " [\(.status)] \(.item) - \(.note)"),
"",
"MIGRATION PLAN",
(.migration_plan[] | " - \(.)")' review.json
# and drop the migrated code straight into the repo
jq -r '.rewrite.code' review.json > "$(jq -r '.rewrite.filename' review.json)" # Migrated.swift
import time
job_id = api("POST", "/run", payload,
**{"Idempotency-Key": "review-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"]
review = json.loads(raw) if isinstance(raw, str) else raw
print(f'{review["review_name"]} [{review["verdict_level"]}]: {review["verdict"]}')
for area in review["health"]:
print(f' [{area["status"]:>4}] {area["area"]:<32} {area["note"]}')
for f in review["findings"]:
print(f' ({f["severity"]}) {f["category"]}: {f["title"]}')
if f["fix_code"]:
print(f' {f["fix_code"]}')
for item in review["checklist"]:
print(f' [{item["status"]:>4}] {item["item"]:<42} {item["note"]}')
for c in review["coverage_check"]:
print(f' {c["id"]}: {"ok" if c["addressed"] else "SET ASIDE"} - {c["note"]}')
for i, step in enumerate(review["migration_plan"], 1):
print(f' {i}. {step}')
with open(review["rewrite"]["filename"], "w", encoding="utf-8") as fh: # Migrated.swift
fh.write(review["rewrite"]["code"])
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 review = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(`${review.review_name} [${review.verdict_level}]: ${review.verdict}`);
for (const area of review.health) {
console.log(` [${area.status}] ${area.area}: ${area.note}`);
}
for (const f of review.findings) {
console.log(` (${f.severity}) ${f.category}: ${f.title}`);
if (f.fix_code) console.log(` ${f.fix_code}`);
}
for (const item of review.checklist) console.log(` [${item.status}] ${item.item}: ${item.note}`);
for (const c of review.coverage_check) {
console.log(` ${c.id}: ${c.addressed ? "ok" : "SET ASIDE"} - ${c.note}`);
}
review.migration_plan.forEach((step, i) => console.log(` ${i + 1}. ${step}`));
writeFileSync(review.rewrite.filename, review.rewrite.code); // Migrated.swift
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, unquote, then unmarshal:
type Review struct {
ReviewName string `json:"review_name"`
VerdictLevel string `json:"verdict_level"`
Verdict string `json:"verdict"`
Health []struct {
Area, Status, Note string
} `json:"health"`
Findings []struct {
Severity, Category, Title, Detail string
FixCode string `json:"fix_code"`
} `json:"findings"`
Checklist []struct {
Item, Status, Note string
} `json:"checklist"`
Rewrite struct {
Filename, Code string
} `json:"rewrite"`
MigrationPlan []string `json:"migration_plan"`
}
var wrapper struct{ Output string `json:"output"` }
json.Unmarshal(job.Output, &wrapper)
var review Review
json.Unmarshal([]byte(wrapper.Output), &review)
fmt.Printf("%s [%s]: %s\n", review.ReviewName, review.VerdictLevel, review.Verdict)
for _, a := range review.Health {
fmt.Printf(" [%s] %s: %s\n", a.Status, a.Area, a.Note)
}
for _, f := range review.Findings {
fmt.Printf(" (%s) %s: %s\n", f.Severity, f.Category, f.Title)
}
for _, c := range review.Checklist {
fmt.Printf(" [%s] %s: %s\n", c.Status, c.Item, c.Note)
}
for i, step := range review.MigrationPlan {
fmt.Printf(" %d. %s\n", i+1, step)
}
os.WriteFile(review.Rewrite.Filename, []byte(review.Rewrite.Code), 0o644) // Migrated.swift
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 review is at data.output.output as a JSON string — parse it again, then read
// review_name, verdict_level, verdict, overview, health[] (five areas with area/status/note),
// findings[] (severity/category/title/detail/fix_code), checklist[] (ten items: item/status/note),
// coverage_check[] (id/addressed/note), rewrite{filename, code}, migration_plan[], next_steps[]
// and summary.
// Finally write the migrated code to disk:
// Files.writeString(Path.of(rewriteFilename), rewriteCode); // Migrated.swift
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"]
review = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{review["review_name"]} [#{review["verdict_level"]}]: #{review["verdict"]}"
review["health"].each { |a| puts " [#{a["status"]}] #{a["area"]}: #{a["note"]}" }
review["findings"].each do |f|
puts " (#{f["severity"]}) #{f["category"]}: #{f["title"]}"
puts " #{f["fix_code"]}" unless f["fix_code"].to_s.empty?
end
review["checklist"].each { |c| puts " [#{c["status"]}] #{c["item"]}: #{c["note"]}" }
review["coverage_check"].each { |c| puts " #{c["id"]}: #{c["addressed"] ? "ok" : "SET ASIDE"}" }
review["migration_plan"].each_with_index { |s, i| puts " #{i + 1}. #{s}" }
File.write(review["rewrite"]["filename"], review["rewrite"]["code"]) # Migrated.swift
$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"];
$review = is_string($raw) ? json_decode($raw, true) : $raw;
echo "{$review['review_name']} [{$review['verdict_level']}]: {$review['verdict']}\n";
foreach ($review["health"] as $a) {
echo " [{$a['status']}] {$a['area']}: {$a['note']}\n";
}
foreach ($review["findings"] as $f) {
echo " ({$f['severity']}) {$f['category']}: {$f['title']}\n";
if ($f["fix_code"] !== "") { echo " {$f['fix_code']}\n"; }
}
foreach ($review["checklist"] as $item) {
echo " [{$item['status']}] {$item['item']}: {$item['note']}\n";
}
foreach ($review["coverage_check"] as $c) {
echo " {$c['id']}: " . ($c["addressed"] ? "ok" : "SET ASIDE") . "\n";
}
foreach ($review["migration_plan"] as $i => $step) {
echo " " . ($i + 1) . ". $step\n";
}
file_put_contents($review["rewrite"]["filename"], $review["rewrite"]["code"]); // Migrated.swift
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 review = doc.RootElement;
Console.WriteLine($"{review.GetProperty("review_name")} " +
$"[{review.GetProperty("verdict_level")}]: {review.GetProperty("verdict")}");
foreach (var a in review.GetProperty("health").EnumerateArray())
{
Console.WriteLine($" [{a.GetProperty("status")}] {a.GetProperty("area")}: {a.GetProperty("note")}");
}
foreach (var f in review.GetProperty("findings").EnumerateArray())
{
Console.WriteLine($" ({f.GetProperty("severity")}) {f.GetProperty("category")}: " +
$"{f.GetProperty("title")}");
}
foreach (var c in review.GetProperty("checklist").EnumerateArray())
{
Console.WriteLine($" [{c.GetProperty("status")}] {c.GetProperty("item")}: {c.GetProperty("note")}");
}
foreach (var step in review.GetProperty("migration_plan").EnumerateArray())
{
Console.WriteLine($" - {step}");
}
var rewrite = review.GetProperty("rewrite");
await File.WriteAllTextAsync(rewrite.GetProperty("filename").GetString()!, // Migrated.swift
rewrite.GetProperty("code").GetString()!);
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 review object — output schema
One JSON object, always the same shape. Every array is present (findings is
empty only if genuinely nothing applies); health always has exactly the five
areas, checklist always has exactly the ten items, and
rewrite.code is never empty. If the paste was too thin to review responsibly,
you still get this object: what is there gets reviewed, the verdict says the
paste is thin, and what you would need to show lands in next_steps. If the
paste is not Swift at all, you still get the object — one high-severity finding
explaining what arrived, every health area at risk, every checklist item at
na, and a rewrite.code block of // comments saying what
to paste instead. A paste spanning several files keeps its // File: ... file-name
comment headers, and each one is migrated in place.
| Field | Type | Meaning |
|---|---|---|
review_name | string | A short name for the review, taken from the code's own domain naming — its type or file names. |
verdict_level | string | ready (already Swift 6.2-safe), adopt (migration needed, but mechanical) or rework (real data races or unsound concurrency as pasted). |
verdict | string | One or two sentences: the overall state and the single most important change. |
overview | string | One or two paragraphs: what this code does, and the pattern behind what was found. |
health | array of 5 | {area, status, note} — the five areas listed below, each exactly once and in order. status is good (nothing material), risk (works, with caveats) or bad (a high-severity finding lives here). Each note references something concrete in the pasted code; an area the paste does not exercise at all is good with a note saying so, unless its absence is itself the risk. An area a high finding touches is never good. |
findings | array | {severity, category, title, detail, fix_code}. severity is high (a real data race, unsound isolation, @unchecked Sendable over genuinely shared mutable state, unprotected global mutable state) | medium (works today but will fight the 6.2 compiler or mislead — nonisolated used to silence errors, @concurrent on non-CPU-bound work, a legacy DispatchQueue where an actor belongs) | low (polish — completion handlers that could be async, a missing isolation rationale on an unstructured Task); category is isolation, data-race, offloading, globals, conformance, legacy-patterns, sendable or structure. detail quotes the pasted code verbatim; fix_code is corrected Swift in your own naming and style, or an empty string when the finding is a question or trade-off rather than a mechanical fix. |
checklist | array of 10 | {item, status, note} — the ten items listed below, each exactly once and in order. status is pass (the paste shows it handled), fail (the paste shows it mishandled — a finding backs this) or na (the paste gives no evidence either way — no unstructured tasks, no global state). The note says what was seen or what is missing. |
coverage_check | array | {id, addressed, note} — one entry per prescan patterns or types id you sent (dq:dispatch-queue, type:actor:JobStore, …), confirming where the review covers it or why it was set aside (a regex hit can be a false positive; the note says so). Nothing you flagged is silently dropped. |
rewrite | object | {filename, code} — the full migrated Swift 6.2 source. filename defaults to Migrated.swift (unless the paste's own // File: headers suggest a better name), and code is your own code migrated: same domain, same intent, findings fixed — shared mutable state moved into actors, completion handlers turned into async functions, legacy queues and locks retired, isolation made explicit and @concurrent reserved for CPU-heavy work. Your naming, domain vocabulary, // File: headers and comments are preserved, and it is a complete replacement for what you pasted, not a fragment. |
migration_plan | string[] | Ordered, concrete steps for this paste: first the Xcode / SwiftPM build-setting changes (Approachable Concurrency, SE-0466 MainActor default isolation, SE-0461 NonisolatedNonsendingByDefault), then the code changes, in the order they should be applied. |
next_steps | string[] | Ordered and concrete follow-up actions once the plan has been applied: what to test, what to measure, what to migrate next. |
summary | string | 3–5 sentences a code reviewer could paste into a PR review. |
The five health areas, in order, spelled exactly like this:
| area | What its note covers |
|---|---|
Actor isolation | Every type that holds mutable state has a stated isolation domain — an actor, @MainActor, or a documented nonisolated — and nonisolated is never used simply to make a diagnostic go away. Under SE-0466 an app or executable target can infer @MainActor by default; a library spells its public isolation out. |
Data-race safety | No mutable state is reachable from two isolation domains at once: no captured self mutated inside a detached closure, no dictionary or array shared across queues, and no @unchecked Sendable standing in for an argument nobody has made. |
Async & offloading | Async functions stay on the calling actor (SE-0461) unless there is a reason to leave it, @concurrent is reserved for genuinely CPU-heavy work, and unstructured Tasks carry a rationale for the isolation they run in. |
Global & static state | Global and static var state is actor-protected, immutable let, or explicitly isolated — a mutable singleton reachable from any thread is the classic Swift 6 error, and the fix is usually to make the type an actor or pin it to @MainActor. |
Protocol conformances | Conformances that need main-actor state use isolated conformance syntax rather than nonisolated shims or assumeIsolated escapes, and Sendable conformances are earned by the type's contents rather than asserted. |
The ten checklist items, in order, spelled exactly like this:
| item | What its note covers |
|---|---|
Async functions stay on the calling actor | Under SE-0461 a nonisolated async function runs on its caller's actor; the paste does not hop off it accidentally, and any deliberate hop is spelled out. |
Global/static mutable state is actor-protected | Every global or static var is an actor, isolated to @MainActor, or made an immutable let — nothing mutable is reachable unsynchronized. |
@concurrent used only for CPU-heavy work | @concurrent marks work that genuinely benefits from leaving the caller's actor; awaiting I/O does not qualify. |
No nonisolated used to silence isolation errors | Each nonisolated exists because the member truly touches no isolated state, not because it made a diagnostic disappear. |
No @unchecked Sendable without a safety argument | Any @unchecked Sendable is backed by a written argument for why the type is safe to share; otherwise the type becomes an actor or a value type. |
Legacy DispatchQueue/lock patterns replaced by actors | Serial queues, NSLock and os_unfair_lock used as ad-hoc mutual exclusion give way to an actor that owns the state. |
Completion handlers migrated to async/await | Callback-style APIs are expressed as async functions, so errors and cancellation flow through the language instead of around it. |
Unstructured Tasks carry an isolation rationale | Every Task { } or Task.detached { } says which isolation it runs in and why, and its lifetime is owned by something. |
MainActor conformances use isolated conformance syntax | A conformance needing main-actor state is declared as an isolated conformance rather than papered over with nonisolated shims or assumeIsolated. |
Build settings plan covers SE-0466 / SE-0461 | The migration plan names the actual Xcode / SwiftPM settings to flip — Approachable Concurrency, MainActor default isolation, NonisolatedNonsendingByDefault — before any code change. |
A small, realistic result for the ImageCache.swift paste above, trimmed for length:
{
"review_name": "ImageCache - shared image store",
"verdict_level": "rework",
"verdict": "'ImageCache' is a mutable singleton whose dictionary is read and written from a
private serial queue while 'shared' is reachable from any isolation domain; make
the type an actor before anything else.",
"overview": "A single final class caches Data blobs by String key. It keeps a static
'shared' instance, a mutable dictionary, and a DispatchQueue used as an ad-hoc
lock, then hands results back through a completion handler. The intent - one
cache, serialized access - is exactly what an actor expresses natively, but as
written the compiler cannot see the serialization: 'shared' is a mutable global
reachable from any thread, the closure passed to 'queue.async' captures 'self'
and mutates state the caller may also touch, and the completion handler runs on
whichever queue the work happened to finish on. The target is 'app', so the type
is also a candidate for MainActor default inference once SE-0466 is enabled.",
"health": [
{ "area": "Actor isolation", "status": "bad",
"note": "'final class ImageCache' declares no isolation at all, yet every member touches
the shared 'store' dictionary." },
{ "area": "Data-race safety", "status": "bad",
"note": "'queue.async { completion(self.store[key]) }' reads 'store' off the queue while
nothing stops another caller writing it from a different thread." },
{ "area": "Async & offloading", "status": "risk",
"note": "'load' is callback-based, so cancellation and errors cannot propagate, and the
completion handler runs on the private queue rather than the caller's actor." },
{ "area": "Global & static state", "status": "bad",
"note": "'static let shared = ImageCache()' exposes mutable instance state as a global;
this is the canonical Swift 6 concurrency error." },
{ "area": "Protocol conformances", "status": "good",
"note": "The paste declares no protocol conformances, so no isolated-conformance question
arises yet." }
],
"findings": [
{ "severity": "high", "category": "data-race",
"title": "store is mutated and read without isolation",
"detail": "'private var store: [String: Data] = [:]' is guarded only by convention:
'queue.async' serializes the read in 'load', but nothing in the type's
signature stops a caller writing 'store' from another domain.",
"fix_code": "actor ImageCache {\n static let shared = ImageCache()\n private var store: [String: Data] = [:]\n\n func load(_ key: String) -> Data? {\n store[key]\n }\n}" },
{ "severity": "high", "category": "globals",
"title": "shared is a mutable singleton with no isolation domain",
"detail": "'static let shared = ImageCache()' is a global handle onto mutable state; once
strict concurrency is on, every use of it is diagnosed.",
"fix_code": "actor ImageCache {\n static let shared = ImageCache()\n}\n// or, for view-facing caches on an app target:\n// @MainActor final class ImageCache { static let shared = ImageCache() }" },
{ "severity": "medium", "category": "legacy-patterns",
"title": "DispatchQueue used as an ad-hoc lock",
"detail": "'private let queue = DispatchQueue(label: \"cache\")' exists only to serialize
access to 'store' - which is precisely what actor isolation provides, checked
by the compiler rather than by discipline.",
"fix_code": "// delete the queue entirely; the actor serializes access\nactor ImageCache {\n private var store: [String: Data] = [:]\n}" },
{ "severity": "low", "category": "offloading",
"title": "Completion handler where async would do",
"detail": "'func load(_ key: String, completion: @escaping (Data?) -> Void)' forces every
caller into a closure and drops cancellation on the floor.",
"fix_code": "func load(_ key: String) async -> Data? {\n store[key]\n}" }
],
"checklist": [
{ "item": "Async functions stay on the calling actor", "status": "na",
"note": "No async functions in the paste; 'load' is callback-based." },
{ "item": "Global/static mutable state is actor-protected", "status": "fail",
"note": "'static let shared' vends an unisolated instance holding a mutable dictionary." },
{ "item": "@concurrent used only for CPU-heavy work", "status": "na",
"note": "No '@concurrent' attribute appears in the paste." },
{ "item": "No nonisolated used to silence isolation errors", "status": "na",
"note": "No 'nonisolated' members; the type has no isolation to opt out of yet." },
{ "item": "No @unchecked Sendable without a safety argument", "status": "pass",
"note": "The type asserts no Sendable conformance - it simply is not Sendable today." },
{ "item": "Legacy DispatchQueue/lock patterns replaced by actors", "status": "fail",
"note": "'DispatchQueue(label: \"cache\")' is the only synchronization in the paste." },
{ "item": "Completion handlers migrated to async/await", "status": "fail",
"note": "'load' takes an '@escaping (Data?) -> Void' completion instead of returning." },
{ "item": "Unstructured Tasks carry an isolation rationale", "status": "na",
"note": "No 'Task { }' or 'Task.detached { }' in the paste." },
{ "item": "MainActor conformances use isolated conformance syntax", "status": "na",
"note": "The type conforms to no protocols." },
{ "item": "Build settings plan covers SE-0466 / SE-0461", "status": "fail",
"note": "No package or target settings were shown; the plan below supplies them." }
],
"coverage_check": [
{ "id": "dq:dispatch-queue", "addressed": true,
"note": "Third finding - the queue is removed and the actor serializes access." },
{ "id": "cb:completion-handler", "addressed": true,
"note": "Fourth finding - 'load' becomes an async function returning 'Data?'." },
{ "id": "global:mutable-static", "addressed": true,
"note": "Second finding - 'shared' becomes a handle onto an actor." },
{ "id": "type:class:ImageCache", "addressed": true,
"note": "The type under review; migrated in full in rewrite.code." }
],
"rewrite": { "filename": "Migrated.swift",
"code": "// File: ImageCache.swift\nimport Foundation\n\nactor ImageCache {\n static let shared = ImageCache()\n private var store: [String: Data] = [:]\n\n func load(_ key: String) -> Data? {\n store[key]\n }\n\n func insert(_ data: Data, for key: String) {\n store[key] = data\n }\n}" },
"migration_plan": [
"Turn on Approachable Concurrency for the target (Xcode: Swift Compiler - Concurrency;
SwiftPM: .enableUpcomingFeature settings on the target).",
"Enable SE-0466 MainActor default isolation for this app target, so unannotated types
default to the main actor rather than to nothing.",
"Enable SE-0461 NonisolatedNonsendingByDefault so nonisolated async functions stay on the
caller's actor.",
"Change 'final class ImageCache' to 'actor ImageCache' and delete the DispatchQueue.",
"Replace 'load(_:completion:)' with 'func load(_ key: String) -> Data?' and update the
call sites to 'await ImageCache.shared.load(key)'.",
"Add the missing write path ('insert(_:for:)') so callers never reach 'store' directly."
],
"next_steps": [
"Audit the view code that calls 'load' - each call site becomes an 'await' inside a Task
owned by the view's lifetime.",
"Decide whether the cache should be an actor or '@MainActor', based on whether it is read
from background work at all.",
"Build once with strict concurrency on and fix the remaining diagnostics in call sites."
],
"summary": "'ImageCache' serializes access by convention rather than by isolation: a mutable
static singleton, a dictionary reachable from any thread, and a DispatchQueue
standing in for a lock. …"
}
The migrated rewrite is a starting point, not a sign-off: it is written to be complete and self-consistent with the findings, but it is AI-generated and it only sees what you pasted. Read it, build it with strict concurrency enabled, run your test suite, and keep the human review in the loop before it goes anywhere near production — changing a public type's isolation is a source-breaking change for every caller.
Step 5 — Stream the review as it is written
/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 the migrated rewrite 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.
| Event | Payload | Meaning |
|---|---|---|
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). |
done | {job_id, status, charged_credits, output} | The final, authoritative result — read the review from output.output rather than relying on 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: review-$(date +%s)" \
-d @input.json
# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"review_name\":\"ImageCache"}
# ...
# 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": "review-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"))
review = json.loads(result["output"]["output"]) # authoritative
print("charged:", result["charged_credits"], "-", review["review_name"])
for area in review["health"]:
print(f' [{area["status"]}] {area["area"]}')
open(review["rewrite"]["filename"], "w", encoding="utf-8").write(review["rewrite"]["code"])
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 review = JSON.parse(done.output.output);
console.log(`\n${done.charged_credits} credits - ${review.review_name}`);
for (const area of review.health) console.log(` [${area.status}] ${area.area}`);
writeFileSync(review.rewrite.filename, review.rewrite.code); // Migrated.swift
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", "review-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 review JSON —
// unmarshal it into the Review struct from step 4, then write review.Rewrite.Code to disk.
// 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", "review-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
// review_name, verdict_level, health[], findings[], checklist[], rewrite{filename, code} 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"] = "review-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
review = JSON.parse(done["output"]["output"])
puts "\n#{done["charged_credits"]} credits - #{review["review_name"]}"
review["health"].each { |a| puts " [#{a["status"]}] #{a["area"]}" }
File.write(review["rewrite"]["filename"], review["rewrite"]["code"]) # Migrated.swift
$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: review-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);
$review = json_decode($done["output"]["output"], true);
echo "\n{$done['charged_credits']} credits - {$review['review_name']}\n";
foreach ($review["health"] as $a) { echo " [{$a['status']}] {$a['area']}\n"; }
file_put_contents($review["rewrite"]["filename"], $review["rewrite"]["code"]); // Migrated.swift
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
Content = JsonContent.Create(payload),
};
req.Headers.Add("Idempotency-Key", "review-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 reviewDoc = JsonDocument.Parse(text!);
var review = reviewDoc.RootElement;
Console.WriteLine(review.GetProperty("review_name"));
foreach (var a in review.GetProperty("health").EnumerateArray())
Console.WriteLine($" [{a.GetProperty("status")}] {a.GetProperty("area")}");
var rewrite = review.GetProperty("rewrite");
await File.WriteAllTextAsync(rewrite.GetProperty("filename").GetString()!, // Migrated.swift
rewrite.GetProperty("code").GetString()!);
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.