PR Tour — API & tutorial Open the app

Build walkthroughs from your own code

Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS with optional streaming. Send a unified diff (plus the PR title, description and audience) and get back the presenter-ready walkthrough in the app's tagged plain-text shape, with examples in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#.

Basics

Base URL: https://api.skillsafe.ai/v1/app-api. 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. Runs execute the app's agent (model gpt-terra) and are billed in SkillSafe credits to the calling token, with a worst-case hold up front and the actual cost settled when the job finishes.

StatusMeaning
401Missing or expired token — create a new session.
402Not enough credits — top up at skillsafe.ai/account/billing.
403The token isn't allowed to do this.
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. The in-browser diff scan on the app's front page is purely local convenience; the API run does its own reading of the diff.

Step 0 — A tiny client

Every step below is one or two HTTP calls, so start with a small helper that adds the auth header, sends JSON and unwraps the data envelope. The later steps reuse this helper.

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 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 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

For scripted use, the simplest reliable path is your personal token: open the token page, sign in, and hit "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 (below) mints a guest token with no browser involved; guests can always call /me and /estimate, but whether a guest can afford an actual run depends on the app's daily sponsorship budget, so don't build on it.

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

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 an expensive run — the app itself does exactly that before starting one.

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 the same input you would send to a run; the response's hold_credits is the worst-case cost and min_credits the floor. Nothing is charged and no job is created. The response also reports the resolved model and whether sponsorship is active.

curl -s -X POST "$API/estimate" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"title":"Add retry to dispatcher","diff":"…the unified diff…","audience":"Teammates"}' | jq '.data'
est = api("POST", "/estimate", {"title": title, "diff": diff, "audience": "Teammates"})
print("worst case:", est["hold_credits"], "credits on", est["model"])
const est = await api("POST", "/estimate", { title, diff, audience: "Teammates" });
console.log("worst case:", est.hold_credits, "credits on", est.model);
var est struct {
	HoldCredits int64  `json:"hold_credits"`
	Model       string `json:"model"`
}
err := call("POST", "/estimate", map[string]string{
	"title": title, "diff": diff, "audience": "Teammates",
}, &est)
String envelope = api("POST", "/estimate", """
    {"title": %s, "diff": %s, "audience": "Teammates"}
    """.formatted(toJsonString(title), toJsonString(diff)));
// worst-case cost is at data.hold_credits
est = api("POST", "/estimate", { title: title, diff: diff, audience: "Teammates" })
puts "worst case: #{est["hold_credits"]} credits on #{est["model"]}"
$est = api("POST", "/estimate", [
    "title" => $title,
    "diff" => $diff,
    "audience" => "Teammates",
]);
echo "worst case: {$est['hold_credits']} credits on {$est['model']}\n";
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", new {
    title, diff, audience = "Teammates" });
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits");

Step 4 — Build the walkthrough: run it and wait

POST /run
GET /jobs/{job_id}

/run places a credit hold and returns a job_id; poll /jobs/{job_id} every 1–2 seconds until status is succeeded or failed. Always send an Idempotency-Key header so a network retry can't start a second, double-charged run. The input fields are exactly what the app's form sends:

Input fieldTypeNotes
titlestring, optionalThe PR title, up to 300 chars.
descriptionstring, optionalThe PR description or commit messages — where the "why" lives. Over 12,000 chars the app drops whole lines from the MIDDLE, keeps the opening and closing lines, and inserts a [description truncated] marker line saying how many lines went.
diffstring, required in practiceThe unified diff (git diff output or the GitHub .diff view). Over 60,000 chars the app drops whole files from the middle on diff --git boundaries, keeps the first and last files intact, and inserts a [diff truncated] marker line naming what was dropped. Do the same, and keep the marker — it is how the agent knows the paste is partial, and it forces COVERAGE below Complete walkthrough.
notesstring, optionalPresenter context: audience familiarity, what to emphasize, review comments already made. Up to 6,000 chars, middle-clipped the same way past that.
audiencestring, optionalOne of "Teammates", "New contributor", "Reviewer in a hurry", "Non-engineer stakeholder". Defaults to Teammates.
factsstring, optionalA mechanical scan summary of the diff (file/hunk/line counts). The app generates this in the browser; from your own code, omit it — the agent reads the diff itself.
retry_notestring, optionalPresent only on a reformat retry. An authoritative restatement of the output format after a malformed reply; the agent obeys it exactly, does not change its reading of the diff, and never mentions it in the output. Omit it on a first attempt.
$modelstring, optionalPer-run model override (allowlisted models only).
# input.json: {"title":"…","description":"…","diff":"…","audience":"Teammates"}
JOB_ID=$(curl -s -X POST "$API/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: tour-$(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

# the walkthrough is plain tagged text at data.output.output
echo "$JOB" | jq -r '.data.output.output'
import time

job_id = api("POST", "/run", {
    "title": title,
    "description": description,
    "diff": diff,
    "audience": "Teammates",
}, **{"Idempotency-Key": "tour-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"]
text = raw["output"] if isinstance(raw, dict) and "output" in raw else raw
print(text)  # plain tagged text - COVERAGE:/CHANGE TYPE:/CONFIDENCE:/SUMMARY: + five sections
coverage = text.splitlines()[0].split(":", 1)[1].strip()
const { job_id } = await api("POST", "/run", {
  title,
  description,
  diff,
  audience: "Teammates",
}, { "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 text = job.output?.output ?? job.output; // plain tagged text, not JSON
const coverage = text.split("\n")[0].split(":").slice(1).join(":").trim();
console.log(coverage);
console.log(text);
var started struct{ JobID string `json:"job_id"` }
err := call("POST", "/run", map[string]string{
	"title": title, "diff": diff, "audience": "Teammates",
}, &started)
if err != nil {
	log.Fatal(err)
}

var job struct {
	Status string `json:"status"`
	Error  string `json:"error"`
	Output struct {
		Output string `json:"output"`
	} `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.Output is plain tagged text - print it or parse the header lines
fmt.Println(job.Output.Output)
String envelope = api("POST", "/run", """
    {"title": %s, "diff": %s, "audience": "Teammates"}
    """.formatted(toJsonString(title), toJsonString(diff)));
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 walkthrough is the plain-text *string* at data.output.output -
// no second JSON parse needed, just read the tagged lines and sections
started = api("POST", "/run", { title: title, diff: diff, audience: "Teammates" })

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"

text = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
puts text  # plain tagged text
coverage = text.lines.first.split(":", 2).last.strip
$started = api("POST", "/run", [
    "title" => $title,
    "diff" => $diff,
    "audience" => "Teammates",
]);

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");
}

$text = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
echo $text; // plain tagged text - COVERAGE:/CHANGE TYPE:/… + five sections
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", new {
    title, diff, audience = "Teammates" });
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);
}
// plain tagged text - print it or split on the header lines
var text = job.GetProperty("output").GetProperty("output").GetString();
Console.WriteLine(text);

The reply is plain text, not JSON, in exactly this shape:

COVERAGE: Complete walkthrough | Partial walkthrough | Not walkable
CHANGE TYPE: Feature | Bug fix | Refactor | Performance | Docs or chore | Mixed | Unknown
CONFIDENCE: <integer 0-100>
SUMMARY: <2-4 sentences, may wrap; ends at the first blank line>

## Context
- <bullets: what motivated the change>

## Segments
- <title> | <Intro|Context|Code|Summary> | <files, comma separated, or -> | <what is on screen>

## Narration
- <exactly one bullet per segment, same order: the words to speak, author's voice>

## Not shown
- <generated files left out, with reasons, or - None.>

## Open questions
- <what the paste does not settle, or - None.>

Parsing rules the app itself enforces: all four header lines are required; all five ## sections appear in that order; every ## Segments bullet has exactly four |-separated fields; ## Narration has one bullet per segment; unless COVERAGE is Not walkable, there are at least 3 segments, the first is an Intro and the last a Summary. A section with nothing to report is the single bullet - None.

If a reply ever breaks that shape, retry once with the same input plus a retry_note field describing the problem — the agent is instructed to obey it. That's exactly what the app itself does (and the retry is billed as its own run, so keep it to one).

Step 5 — The same run, streamed

POST /run-stream

Identical input to /run, but the response is text/event-stream, so you can show the walkthrough as it generates — the app's own progress panel does exactly this. Events:

EventData
job{job_id} — the run was accepted.
delta{text} — the next chunk of agent output.
done / pendingFinal payload: {job_id, status, charged_credits, output}. Authoritative — deltas can drop the tail, so always read the result from here.
error{code, message, job_id}.
curl -sN -X POST "$API/run-stream" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d @input.json
# event: job    data: {"job_id":"job_…"}
# event: delta  data: {"text":"COVERAGE: Complete walkthrough\n"}
# …
# event: done   data: {"job_id":"…","status":"succeeded","charged_credits":412,
#                      "output":{"output":"…the full tagged text…"}}
res = requests.post(API + "/run-stream", json=payload, stream=True,
                    headers={"Authorization": f"Bearer {TOKEN}"})
event, done = None, None
for line in res.iter_lines(decode_unicode=True):
    if line.startswith("event:"):
        event = line[6:].strip()
    elif line.startswith("data:"):
        data = json.loads(line[5:])
        if event == "delta":
            print(data.get("text", ""), end="", flush=True)
        elif event in ("done", "pending"):
            done = data
        elif event == "error":
            raise RuntimeError(data.get("message"))

text = done["output"]["output"]  # authoritative full walkthrough
const res = await fetch(API + "/run-stream", {
  method: "POST",
  headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = "message", done, out = "";
for (;;) {
  const chunk = await reader.read();
  if (chunk.done) break;
  buf += decoder.decode(chunk.value, { stream: true });
  let i;
  while ((i = buf.indexOf("\n")) >= 0) {
    const line = buf.slice(0, i); buf = buf.slice(i + 1);
    if (line.startsWith("event:")) event = line.slice(6).trim();
    else if (line.startsWith("data:")) {
      const data = JSON.parse(line.slice(5));
      if (event === "delta") out += data.text ?? "";
      else if (event === "done" || event === "pending") done = data;
      else if (event === "error") throw new Error(data.message);
    }
  }
}
const text = done.output.output; // authoritative - deltas can drop the tail
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
	log.Fatal(err)
}
defer res.Body.Close()

sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
event, done := "", []byte(nil)
for sc.Scan() {
	line := sc.Text()
	if strings.HasPrefix(line, "event:") {
		event = strings.TrimSpace(line[6:])
	} else if strings.HasPrefix(line, "data:") {
		data := strings.TrimSpace(line[5:])
		if event == "delta" {
			// unmarshal {"text": …} and print it as it arrives
		} else if event == "done" || event == "pending" {
			done = []byte(data)
		}
	}
}
// unmarshal done -> .output.output (the plain tagged text)
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
    .header("Authorization", "Bearer " + TOKEN)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(payloadJson))
    .build();
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();

final String[] event = {""};
StringBuilder doneData = new StringBuilder();
lines.forEach(line -> {
    if (line.startsWith("event:")) event[0] = line.substring(6).trim();
    else if (line.startsWith("data:")) {
        if (event[0].equals("delta")) { /* parse {"text"} and print */ }
        else if (event[0].equals("done")) doneData.append(line.substring(5).trim());
    }
});
// parse doneData -> output.output (the plain tagged text)
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = payload.to_json

event, done, buf = nil, nil, ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  http.request(req) do |res|
    res.read_body do |chunk|
      buf << chunk
      while (i = buf.index("\n"))
        line = buf.slice!(0..i).chomp
        if line.start_with?("event:") then event = line[6..].strip
        elsif line.start_with?("data:")
          data = JSON.parse(line[5..])
          print data["text"] if event == "delta"
          done = data if %w[done pending].include?(event)
        end
      end
    end
  end
end
text = done["output"]["output"]  # the plain tagged text
$event = ""; $done = null; $buf = "";
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $TOKEN", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done, &$buf) {
        $buf .= $chunk;
        while (($i = strpos($buf, "\n")) !== false) {
            $line = rtrim(substr($buf, 0, $i)); $buf = substr($buf, $i + 1);
            if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
            elseif (str_starts_with($line, "data:")) {
                $data = json_decode(substr($line, 5), true);
                if ($event === "delta") echo $data["text"] ?? "";
                if ($event === "done" || $event === "pending") $done = $data;
            }
        }
        return strlen($chunk);
    },
]);
curl_exec($ch);
curl_close($ch);
$text = $done["output"]["output"]; // the plain tagged text
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream")
    { Content = JsonContent.Create(payload) };
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());

string? line; string ev = ""; JsonElement doneEl = default;
while ((line = await reader.ReadLineAsync()) != null)
{
    if (line.StartsWith("event:")) ev = line[6..].Trim();
    else if (line.StartsWith("data:"))
    {
        var data = JsonDocument.Parse(line[5..]).RootElement.Clone();
        if (ev == "delta") Console.Write(
            data.TryGetProperty("text", out var t) ? t.GetString() : "");
        else if (ev is "done" or "pending") doneEl = data;
    }
}
var text = doneEl.GetProperty("output").GetProperty("output").GetString();

The grounding contract applies over the API exactly as in the app: every file, function and hunk the walkthrough names must appear in the diff you sent, inferred motivation is phrased as inference, and generated files (lockfiles, API reports, snapshots) land under ## Not shown rather than getting segments. A truncated diff can never come back as a Complete walkthrough — that honesty is the product.