← DOTS Desk / API
Tokens

Drive DOTS Desk from your own code

The page reads the pasted C# of one Unity DOTS feature in the browser for free — one or more files joined into one paste, each introduced by a // file: <path> line — with its line numbers. It lexes strings (verbatim, interpolated and both), char literals, comments and preprocessor lines out first, rebuilds the brace structure (types, methods, foreach loops, lambdas, object initializers), classifies every type by what it implements (IComponentData, IBufferElementData, ISharedComponentData, ISystem, SystemBase, IJobEntity, IJob, IJobParallelFor, IJobFor, IJobChunk, IAspect, Baker<T>), reads every field with its attributes, decides which methods Burst compiles, and follows every job instance to its schedule call and what happens to its JobHandle, every native allocation to its Dispose, every hand-made EntityCommandBuffer to its Playback, and every structural change to where it happens. Then it runs 21 deterministic checks drawn from Entities 1.x practice (catalogue ids DK-01..DK-21, plus DK-22 for context): systems, jobs and aspects that are not partial, managed fields in unmanaged components and jobs, managed calls (UnityEngine.Time, UnityEngine.Random, GameObject, managed collections, strings) in Burst-compiled code, code that could be Burst compiled and is not, API removed in Entities 1.0, SystemAPI outside a system, dropped JobHandles, containers disposed while a job may still use them, EntityManager or a plain command buffer in a parallel job, constant ParallelWriter sort keys, lookups cached in OnCreate and never updated, missing [ReadOnly], Complete() right after scheduling, structural changes inside a SystemAPI.Query loop, command buffers never played back or disposed, toggled tag components, singletons read without RequireForUpdate, native allocations never disposed, Allocator.Temp memory passed to a job, Unity.Mathematics.Random seeded with zero, and legacy IAspect or Entities.ForEach code. Nothing is compiled or run, and it is not the Unity Editor, the Burst compiler or the safety system: every flag is a reading of the text. Its two paid steps are available over HTTP. You send one JSON object with task set to "audit" or "fix", the paste as code and the page’s check as a facts string, and the model replies with one JSON object: an audit that rates the four areas, answers every flag and gives exact before/after edits per finding, or the whole fixed file(s) with a list of every change tied to the page’s flags.

Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends Content-Type: application/json and, once you have a token, Authorization: Bearer <token>. The paths below (/guest, /me, /estimate, /run, /jobs/{job_id}, /run-stream) are relative to it, exactly as sdk.js calls them. A plain-text summary of the app for agents is at /llms.txt. Both lanes run on the gpt-terra model with a 10% publisher markup. A run needs a signed-in (user) token and is metered: the platform places a hold on your credits when the run starts and then charges only what the run used. /estimate is free.

The response envelope

Responses are JSON of the form {"ok": ..., "data": ..., "error": {...}}. The page’s SDK (sdk.js) decides success from the HTTP status, not from ok: on a 2xx it returns data; otherwise it raises an error whose message is error.message (or the HTTP status text when the body carries none), with status, code (error.code) and details (error.details) attached. A body that is not JSON is treated as {}.

{ "ok": true,  "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "details": { ... } } }

Error codes

Branch on the HTTP status and error.code, show error.message. The page handles 401 and 402 by name; the rest reach it as a generic failure (“The run failed: ...”).

HTTPerror.codeWhat it means / what to do
400VALIDATION_ERRORThe body is not a JSON object, a required field (task, code, facts) is missing, or a declared field has the wrong type. Every field is a string; sending facts or audit_handoff as an object is the usual cause.
401UNAUTHORIZEDNo token, or one that expired or was revoked. The page forgets the signed-in user and says “Your session expired - sign in again.” Get a new token.
402INSUFFICIENT_CREDITSThe balance cannot cover the run. The page says “Not enough credits for this run.” with a top-up link, and tries never to submit into a 402: it disables the run button when the /me balance is below the estimate’s min_credits.
404NOT_FOUNDA wrong path, or a job id that does not belong to this token.
409CONFLICTAn Idempotency-Key replayed with a different body. Send the original body, or use a new key (the page bumps its :a<attempt> suffix for a retry).
429RATE_LIMITEDToo many requests. Back off with a delay; do not tight-loop, and never resend a metered run without its original Idempotency-Key.

Failures that do not arrive as an HTTP status:

WhatWhat happens
event: error on /run-streamThe SDK raises an error carrying the event’s message, code and job_id.
Job status: "failed"Polling stops; the job’s error field says why.
Polling past 180 sThe SDK’s own client-side limit (waitForJob, 1 s interval, 180 000 ms default): it throws “job timed out”. The job itself is not cancelled.

The task field: two lanes

task routes the run, and it takes exactly two values: "audit" and "fix". Each lane has its own reply contract, and a reply never mixes them. The skill’s instructions tell the model to do the audit lane when task is missing or anything else (and the page’s own Dotskit.buildInput turns any lane that is not "fix" into "audit"), so a typo silently gets you an audit: always send one of the two values.

taskWhat it doesverdictUse it when
"audit"Reviews the code the way a senior Unity DOTS engineer reviews a feature before it goes into a build with Burst and the safety checks on: says what the feature does, its systems and jobs and what runs in parallel, rates the four areas (burst, jobs, structural, memory) as sound, fragile or broken, confirms or dismisses every page flag, adds what text rules cannot see, and gives the verbatim evidence and exact before/after edits for each finding, listed in the order to apply them — what stops the build first (partial, removed API, managed fields, SystemAPI outside a system), then what fails Burst or throws in the safety checks, then leaks and stalls, then hygiene — with a plan that ends in compiling with Burst and Jobs > Burst > Safety Checks on, entering Play mode, and opening the Burst Inspector and the Profiler.blocked | needs_work | readyBefore a build: does it compile and does Burst accept it, do its jobs keep their handles and read their data safely, does it change structure while iterating, does it free its native memory, and what should change first.
"fix"Writes the fixed version of the pasted code: the whole of every file it changes (never a new file), with structural changes moved into an entity command buffer, every JobHandle kept (normally in state.Dependency), containers disposed on the job’s handle, [ReadOnly], partial, [BurstCompile] and RequireForUpdate added where they belong, managed fields made unmanaged and managed calls moved out of Burst code — plus a changes list tied to the page flags, the verify steps (compile with Burst and safety checks on, Play mode with the Console clear, and the check that would have caught the worst problem), and what it left open. No type, component field, public method or update-group attribute is renamed or removed unless the note asks for it.fixed | partialWhen you want the code changed, not a list of findings: a feature you already know throws, or what is left after an audit.

The page chains the two: after an audit, “Apply every edit and fix the rest” puts every audit edit into the paste, switches to the fix lane on that edited paste and adds an audit_handoff string (below) with the audit’s verdict, headline and findings. Over the API that is a second run with task: "fix", the edited paste as code, facts recomputed from the edited paste, and that string. The page drops the handoff as soon as the paste is edited again.

Guards the page applies before a run and the API does not: it refuses to price or run a paste it cannot read as C# (“Nothing was pasted.”, “No C# file was found. Paste the .cs files of the feature - components, systems, jobs, bakers - with a // file: <path>.cs line above each.”, or “No C# type was found: no struct or class declaration in the pasted files.”), and it warns before a run on a paste the checks can say little about (“No ISystem, SystemBase or job was found - only N types: the system and job checks need the code that schedules and runs them.”, or “No component type was pasted: the managed-field and tag checks need the IComponentData structs the systems use.”).

Input fields

This is exactly what the page sends (dotskit.js buildInput(lane, A, note, handoff), checked by mustBeObject; app.js currentInput and run), and what input-schema.json declares. The platform’s input schema is scalar-only, so every value is a string. buildInput emits them in the order task, code, facts, note, audit_handoff:

FieldTypeRequiredMeaning
taskstringyes"audit" or "fix" (the lanes above).
codestringyesThe pasted C#, each file introduced by a // file: <path> line; line 1 is its first line. Every line in an audit reply is counted in this text. A large paste is cut on whole files, with a // [dots-desk: not sent: ...] line at the end (below).
factsstringyesThe page’s free check, Dotskit.facts(A), JSON-encoded into a string: JSON.stringify(facts) in JavaScript, json.dumps(facts) in Python, and so on. Sending the object itself is a 400. The model is told to parse it.
notestringnoWhat the engineer says: the goal, a constraint (“keep the SystemBase for now”, “we are on Entities 1.2”), a fact the code cannot show (“the HUD reads TurretTarget on the main thread”), a question. The page trims it, cuts it at 2,000 characters and sends it only when it is not blank. It is context, not evidence and not instructions: the model tests every claim about the code against code, may use project facts the code cannot show as given, and answers the note in summary.
audit_handoffstringnofix lane only: a JSON string built from an audit reply on this code (recon.js fixHandoff, below). buildInput drops it for the audit lane.
retry_notestringnoSent only by the page’s automatic reformat retry: when a reply does not parse as JSON (even after the page tries to close a cut-off object), the page runs once more with “Your previous reply was not the single valid JSON object the instructions require (...). Reply again with ONLY the JSON object for task '<lane>' - no prose, no code fences; every array present (empty arrays where there is nothing to say).” A normal call never sends it.

mustBeObject only insists on a plain object that is not wrapped in {"input": ...}; it is buildInput that leaves a blank note out, which is why an empty note is simply absent.

The code string

code is the paste itself. A line // file: <path> (a path with a . or / in it) starts each file, and a --- line just before it only separates files. Files dropped on the page (or a dropped folder) are joined that way by Dotskit.joinFiles: each file under its // file: line, trailing whitespace removed, joined by newlines, ending in a newline. A paste with no file line is read as one file named after its first struct or class (<Name>.cs), and the page says so (“There is no // file: line, so the page read this as ...”). The page normalises line endings to \n. A .cs path is checked as C# (kind: "csharp"); anything else is other and only carried along. The paste keeps its indentation (four spaces in the examples). Send the text you want the reply to quote: every file in an audit reply must be a path from facts.files, every line must fall inside that file, every evidence must be copied from it, and every edit’s before must occur in it exactly once.

The page reads up to 400,000 characters (MAX_TEXT, cut at a line) and 40 files (MAX_FILES), and sends at most 60,000 characters (MAX_SEND): past that buildInput drops whole files from the end — the last file that is not C# first, then the last file — until the rest fits, cuts a single remaining file at a line (“the end of <path>”) if it still does not, and ends the string with one line naming what it left out:

// [dots-desk: not sent: Assets/Scripts/Turrets/TurretHud.cs, Assets/Scripts/Turrets/TurretBaker.cs]

The model is told never to quote or rewrite what was not sent, and facts.notes gets the sentence “Not sent (the paste is over 60,000 characters): ... The page checked them; the run did not see them.” facts is still computed from the whole paste. The worked examples below are small, so their code is the paste unchanged.

The facts string

facts is optional to compute but required to send. It is produced by the page’s free check: Dotskit.analyze splits the paste into files, lexes them, rebuilds their brace structure, inventories types, systems, jobs, allocations, command buffers and structural changes and runs the rules, and Dotskit.facts(A) turns that analysis into the object below (it does not depend on the lane: an audit and a fix run of the same paste carry the same facts). It is how the model knows which flags to answer, which file paths and line ranges to name, and which types and methods must survive. Its top-level keys, in the order built:

KeyContents
summaryCounts: csharp_files, types, systems, jobs, burst_methods, allocations.
filesEvery file: path (use it verbatim), kind (csharp, other), line and end_line (the paste lines of the file’s content, without its // file: line).
typesEvery struct and class (up to 60): name, kind, role (component, buffer, shared, managed_component, isystem, systembase, job, aspect, baker, authoring, other), file, line, partial, burst, and when they apply interface and scheduled (a job: parallel, single, run or null when the paste never schedules it), tag (a component with no fields), enableable, nested_in, and for components, jobs and systems their fields (up to 24: name, type, attrs).
systemsEvery ISystem and SystemBase (up to 30): name, kind, file, line, partial, burst_update (OnUpdate carries [BurstCompile]), methods, requires (the RequireForUpdate<T> in OnCreate), singletons (the singletons it reads) and schedules (each job it creates, with each schedule call and where its JobHandle goes: -> state.Dependency, -> <local>, -> dropped, or (implicit dependency) for an IJobEntity scheduled without arguments).
jobsEvery job struct (up to 30): name, interface, file, line, burst, partial, scheduled.
allocationsEvery native container allocated inside a method (up to 40): name, container, allocator (Temp, TempJob, Persistent, World for state.WorldUpdateAllocator, or (unknown)), file, line, where, disposed, passed_to (job fields).
command_buffersEvery new EntityCommandBuffer(...) (up to 20): name, file, line, where, played_back, disposed.
structural_changesEvery create, destroy, instantiate, add, remove or shared-component set (up to 60): call, via (EntityManager or command buffer), file, line, where, in_query_loop.
functionsEvery method with a body (up to 120): name (Type.Method), file, line, burst.
unreadableFiles whose braces did not balance (up to 20): path, line, error; their structure may be misread.
flagsEvery flag the page raised: uid (F1, F2, ... in severity, file, then line order), id (DK-01..DK-22), sev (high | medium | low | info), file, line, text, detail, and lines (every line an aggregated flag covers, only when it covers more than one). An audit must answer every non-info flag by its uid; a fix must close every high and medium flag or name it in open.
verdict_candidateThe audit verdict the flags point at (the page’s own reading): blocked with any high flag, needs_work with any medium, otherwise ready.
notesPlain sentences about what was not read or not sent (empty for a paste that fits).

The simplest source is dotskit.js itself: the page loads it as /dotskit.js, and it also runs in Node (it sets module.exports). This script prints what the page would send as code and facts (with the 60,000-character cut applied), with the facts decoded so you can read them; the step 4 samples compact them back into the page’s string byte for byte. Save the paste as swarm-paste.txt and redirect the output into swarm-check.json:

// build-check.js -- run: node build-check.js swarm-paste.txt > swarm-check.json   (dotskit.js saved next to it)
const fs = require("fs");
const Dotskit = require("./dotskit.js");
const A = Dotskit.analyze(fs.readFileSync(process.argv[2], "utf8"));
if (!A.ok) { console.error(A.errors.join("\n")); process.exit(1); }
const inp = Dotskit.buildInput("audit", A, "", null);        // code and facts do not depend on the lane
console.log(JSON.stringify({ code: inp.code, facts: JSON.parse(inp.facts) }, null, 2));
console.error(Dotskit.summaryLine(A) + "; " + A.flags.filter((f) => f.sev !== "info").length + " flags (" +
  A.counts.high + " high, " + A.counts.medium + " medium, " + A.counts.low + " low); verdict_candidate " + A.candidate);

For the swarm example it reports 3 C# files · 2 systems (2 ISystem, 0 SystemBase) · 1 job · 3 component types · 4 Burst-compiled methods; 5 flags (5 high, 0 medium, 0 low); verdict_candidate blocked; for the bullets example, 2 C# files · 1 system (1 ISystem, 0 SystemBase) · 1 job · 3 component types · 3 Burst-compiled methods; 4 flags (3 high, 1 medium, 0 low); verdict_candidate blocked. For both worked examples its code and facts are exactly what the page sends.

Without the page’s check. The platform only checks that facts is a string, so a minimal object works — "{}", or a hand-built one such as {"flags": [], "notes": ["No page check was run; read the code directly."]} — and the model still reads code. But you lose what the check adds: with no flags there is nothing to confirm, dismiss or close, with no files there are no paths and line ranges to name, with no systems and jobs the model does not get the schedule calls and where each JobHandle goes, and with no types and functions nothing says what must survive a fix. Build it with dotskit.js as above when you can.

The audit_handoff string

Only for task: "fix", and only for the code the audit was about with every audit edit applied. The page builds both from the normalised audit reply res (recon.js normalize, below) and the analysis A of the audited paste, in recon.js fixHandoff:

const run = Recon.applyFindings(A, res.findings, Dotskit);   // every finding's edits, in order, each on top of the last
JSON.stringify({ verdict: res.verdict, headline: res.headline,
  findings: res.findings.map(f => ({ id: f.id, severity: f.severity, file: f.file, line: f.line, title: f.title, why: f.why, edits: f.edits.length })),
  applied: /* edits that applied */, failed: /* edits that did not */ })
// run.A.text is the edited paste: it becomes code, and facts is recomputed from it

That is {"verdict", "headline", "findings": [{id, severity, file, line, title, why, edits}], "applied", "failed"}, where each finding’s edits is the number of edits it carried. applyFindings applies every finding’s edits with Dotskit.applyEdits, in the order listed: each before must occur exactly once in its file as the earlier edits left it (leading and trailing blank lines ignored), or that edit is reported as failed and skipped — it is never guessed. The handoff is a previous reply, not evidence: the model checks it against code and fixes what is left. This script does the whole step — the edited paste, its recomputed facts and the handoff string, in the same shape as build-check.js prints, so the samples read it the same way:

// handoff.js -- run: node handoff.js swarm-paste.txt swarm-reply.txt > swarm-fix-check.json   (dotskit.js, recon.js next to it)
const fs = require("fs");
const Dotskit = require("./dotskit.js");
const Recon = require("./recon.js");
const A = Dotskit.analyze(fs.readFileSync(process.argv[2], "utf8"));           // the audited paste
const res = Recon.normalize(Recon.parseResult(fs.readFileSync(process.argv[3], "utf8")), "audit");
const ho = Recon.fixHandoff(A, res);                                            // every edit, in order
const B = Dotskit.analyze(ho.text);                                             // facts recomputed from the edited paste
const inp = Dotskit.buildInput("fix", B, "", ho.handoff);
console.log(JSON.stringify({ code: inp.code, facts: JSON.parse(inp.facts), audit_handoff: inp.audit_handoff }, null, 2));
console.error(ho.applied + " edit(s) applied, " + ho.failed + " did not apply");

Save the audit’s output.output as swarm-reply.txt; for the swarm audit below it reports 11 edit(s) applied, 0 did not apply, and the page sends that fix run with the audit’s note and the key dots-desk:fix:e06f3253-26e3:a1.

The output contracts

The model is instructed (SKILL.md) to reply with one JSON object and nothing else — no prose, no code fences — with every array of its lane present ([] when empty). The run’s output text is that object. In both lanes it must ground everything in code: other systems, the subscene and its bakers, the system groups and update order, the package versions and the Player settings are outside the paste and are never stated as facts unless the note says so — they go into questions; whether a type that is used but not pasted is a component, a tag or enableable is unknown unless the code shows it. It never invents a measurement (frame times, entity counts and speedups are facts only when the note gives them). It must never change what the code offers the rest of the project: every type’s name, kind and namespace, every component’s fields (a field’s type may change only to make it unmanaged, such as string to FixedString64Bytes), every public method’s name and parameters, and every system’s update group and ordering attributes, unless the note asks for it. It uses an API only when it is sure the API exists with that name and signature in Entities 1.x (and the version the note gives), and never adds a package the paste does not use. Every page flag whose sev is not info is answered exactly once (audit lane: in flag_answers), and an info flag is never reported as a finding.

task: "audit"

task: "fix"

How the page reads a reply (recon.js parseResult and normalize): it strips a code fence and parses from the first { to the last }. A reply is read as the lane that was asked for, unless it names the other lane and carries that lane’s body (a files array for fix, a findings array for audit). An unknown verdict shows as needs_work or partial and counts as a disagreement; an unknown severity becomes medium; ids like DK7 or dk 7 become DK-07; flags keeps only F<n> values; findings with no file, evidence and title are dropped; feature may also arrive as service or component, file as path, a change’s function as func, method or member and its change as what, and an open item as path; questions is capped at 10, plan at 30 and verify at 20. An audit reply carries 9 sections (verdict, headline, feature, areas, findings, flag_answers, plan, questions, summary) and a fix reply 8 (verdict, headline, files, changes, verify, open, questions, summary), which is what the page counts when it recovers a partial reply.

Worked examples

Both are examples the page ships (the example buttons on the app page, ids swarm and bullets), built exactly as the page builds them: Dotskit.buildInput(lane, Dotskit.analyze(text), note, null), with the real model reply the page replays for each. The pastes are shown whole; the request bodies cut the long code, facts and audit_handoff strings with a visible ..., and the replies are pretty-printed excerpts, with ... where entries or the end of a string were left out. Every value shown is copied from the input or the reply.

Audit: a drone swarm whose move system is not partial, whose component holds a string, which instantiates inside a SystemAPI.Query loop, seeds Random with zero and reads UnityEngine.Time in a Burst job

Three files under Assets/Scripts/Swarm/: SwarmComponents.cs (the Drone component with a string Callsign, the SwarmSpawner component and the SwarmSpawned tag), SwarmSpawnSystem.cs (a Burst-compiled ISystem that walks every spawner without SwarmSpawned in a SystemAPI.Query foreach, seeds new Random(0), calls state.EntityManager.Instantiate and SetComponentData for each drone and state.EntityManager.AddComponent<SwarmSpawned> for the spawner, all inside the loop) and DroneMoveSystem.cs (an ISystem declared without partial that schedules the Burst-compiled DroneMoveJob, an IJobEntity whose Execute reads Time.time). The page’s free check raises 5 flags (5 high, 0 medium, 0 low) (plus the DK-22 context flag); its own reading of the verdict is blocked. The engineer’s note: “First DOTS feature on the team. The spawner and the drone movement are going into Friday's playtest build - what breaks with Burst and the safety checks on, and what do we fix first?”

The code string, whole: 2,369 characters, 89 lines ending in a newline. Save it as swarm-paste.txt and run node build-check.js swarm-paste.txt > swarm-check.json:

// file: Assets/Scripts/Swarm/SwarmComponents.cs
using Unity.Entities;
using Unity.Mathematics;

public struct Drone : IComponentData
{
    public float Speed;
    public float BobHeight;
    public string Callsign;
}

public struct SwarmSpawner : IComponentData
{
    public Entity Prefab;
    public int Count;
    public float Radius;
}

// Added to a spawner once it has spawned its swarm.
public struct SwarmSpawned : IComponentData { }
---
// file: Assets/Scripts/Swarm/SwarmSpawnSystem.cs
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

[BurstCompile]
public partial struct SwarmSpawnSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<SwarmSpawner>();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        foreach (var (spawner, origin, entity) in
            SystemAPI.Query<RefRO<SwarmSpawner>, RefRO<LocalTransform>>()
                .WithNone<SwarmSpawned>()
                .WithEntityAccess())
        {
            var random = new Random(0);
            for (int i = 0; i < spawner.ValueRO.Count; i++)
            {
                var drone = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);
                var offset = random.NextFloat3Direction() * random.NextFloat(0f, spawner.ValueRO.Radius);
                state.EntityManager.SetComponentData(drone,
                    LocalTransform.FromPosition(origin.ValueRO.Position + offset));
            }

            state.EntityManager.AddComponent<SwarmSpawned>(entity);
        }
    }
}
---
// file: Assets/Scripts/Swarm/DroneMoveSystem.cs
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

[BurstCompile]
public struct DroneMoveSystem : ISystem
{
    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        new DroneMoveJob
        {
            DeltaTime = SystemAPI.Time.DeltaTime
        }.ScheduleParallel();
    }
}

[BurstCompile]
public partial struct DroneMoveJob : IJobEntity
{
    public float DeltaTime;

    void Execute(ref LocalTransform transform, in Drone drone)
    {
        transform.Position += transform.Forward() * drone.Speed * DeltaTime;
        transform.Position.y += math.sin(Time.time * 2f) * drone.BobHeight * DeltaTime;
    }
}

Its facts, decoded and cut to summary, files, systems, structural_changes, flags (each detail shortened) and verdict_candidate — the full object also carries the 6 types with their fields, the one job, the 4 functions, and empty allocations, command_buffers and unreadable. The request carries it as one compact JSON string of 6,843 characters:

{
  "summary": {
    "csharp_files": 3,
    "types": 6,
    "systems": 2,
    "jobs": 1,
    "burst_methods": 4,
    "allocations": 0
  },
  "files": [
    {
      "path": "Assets/Scripts/Swarm/SwarmComponents.cs",
      "kind": "csharp",
      "line": 2,
      "end_line": 20
    },
    {
      "path": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "kind": "csharp",
      "line": 23,
      "end_line": 57
    },
    {
      "path": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "kind": "csharp",
      "line": 60,
      "end_line": 90
    }
  ],
  "systems": [
    {
      "name": "SwarmSpawnSystem",
      "kind": "ISystem",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 29,
      "burst_update": true,
      "requires": [
        "SwarmSpawner"
      ],
      "singletons": [],
      "schedules": []
    },
    {
      "name": "DroneMoveSystem",
      "kind": "ISystem",
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 67,
      "burst_update": true,
      "requires": [],
      "singletons": [],
      "schedules": [
        {
          "job": "DroneMoveJob",
          "line": 72,
          "calls": [
            "ScheduleParallel() (implicit dependency)"
          ]
        }
      ]
    }
  ],
  "structural_changes": [
    {
      "call": "Instantiate",
      "via": "EntityManager",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 48,
      "where": "SwarmSpawnSystem.OnUpdate",
      "in_query_loop": true
    },
    {
      "call": "AddComponent",
      "via": "EntityManager",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 54,
      "where": "SwarmSpawnSystem.OnUpdate",
      "in_query_loop": true
    }
  ],
  "flags": [
    {
      "uid": "F1",
      "id": "DK-02",
      "sev": "high",
      "file": "Assets/Scripts/Swarm/SwarmComponents.cs",
      "line": 9,
      "text": "component Drone has a managed field: string Callsign",
      "detail": "An unmanaged component (a struct IComponentData or..."
    },
    {
      "uid": "F2",
      "id": "DK-20",
      "sev": "high",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 45,
      "text": "new Random(0) seeds Unity.Mathematics.Random with zero",
      "detail": "Unity.Mathematics.Random is an xorshift generator: state..."
    },
    {
      "uid": "F3",
      "id": "DK-14",
      "sev": "high",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 48,
      "text": "EntityManager.Instantiate, EntityManager.AddComponent inside the SystemAPI.Query foreach in SwarmSpawnSystem.OnUpdate",
      "detail": "A structural change (create, destroy, add or remove a...",
      "lines": [
        48,
        54
      ]
    },
    {
      "uid": "F4",
      "id": "DK-01",
      "sev": "high",
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 67,
      "text": "DroneMoveSystem (ISystem) is not partial",
      "detail": "The build fails: the system uses SystemAPI or schedules an..."
    },
    {
      "uid": "F5",
      "id": "DK-03",
      "sev": "high",
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 87,
      "text": "Time.time in DroneMoveJob.Execute, which Burst compiles",
      "detail": "Burst compiles only unmanaged code: UnityEngine.Time is..."
    },
    {
      "uid": "F6",
      "id": "DK-22",
      "sev": "info",
      "file": "Assets/Scripts/Swarm/SwarmComponents.cs",
      "line": 2,
      "text": "context: 3 C# files, 6 types (3 component, 2 ISystem, 1 job), 4 Burst-compiled methods, 1 job instance, 0 native allocations, 0 hand-made command buffers",
      "detail": "Only what is pasted is checked: other systems, the..."
    }
  ],
  "verdict_candidate": "blocked",
  "notes": []
}

The request body (POST /estimate, then POST /run or /run-stream with Idempotency-Key: dots-desk:audit:ebdd3062-27b2:a1 as the page sends it):

{
  "task": "audit",
  "code": "// file: Assets/Scripts/Swarm/SwarmComponents.cs\nusing Unity.Entities;\n...",
  "facts": "{\"summary\":{\"csharp_files\":3,\"types\":6,\"systems\":2,\"jobs\":1,\"burst_methods\":4,\"allocations\":0},...",
  "note": "First DOTS feature on the team. The spawner and the drone movement are going into Friday's playtest build - what breaks with Burst and the safety checks on, and what do we fix first?"
}

The reply (output.output), an excerpt: the verdict, the headline, the four areas, every finding’s id, flags, file, line, severity and title, the structural-change finding with its first two edits, the Random finding with its one edit, one flag answer and the last plan step:

{
  "task": "audit",
  "verdict": "blocked",
  "headline": "Blocked: DroneMoveSystem won't compile, DroneMoveJob fails Burst, and SwarmSpawnSystem throws the first time it spawns a swarm.",
  "feature": "SwarmSpawnSystem (ISystem) instantiates SwarmSpawner.Count Drone prefabs in a random radius around each un-spawned...",
  "areas": [
    {
      "area": "burst",
      "status": "broken",
      "note": "DroneMoveSystem is missing partial, Drone.Callsign is a managed string field, and DroneMoveJob.Execute reads UnityEngine.Time.time - each on its own stops Burst or the build."
    },
    {
      "area": "jobs",
      "status": "sound",
      "note": "DroneMoveJob is scheduled with the no-argument ScheduleParallel() so state.Dependency is handled for it, and its ref/in query parameters need no lookups or [ReadOnly]."
    },
    {
      "area": "structural",
      "status": "broken",
      "note": "SwarmSpawnSystem calls EntityManager.Instantiate and EntityManager.AddComponent inside the SystemAPI.Query foreach it is iterating."
    },
    {
      "area": "memory",
      "status": "broken",
      "note": "The spawn offset's Random is seeded with new Random(0)."
    }
  ],
  "findings": [
    {
      "id": "DK-01",
      "flags": [
        "F4"
      ],
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 67,
      "severity": "high",
      "area": "burst",
      "title": "DroneMoveSystem is not partial",
      ...
    },
    {
      "id": "DK-02",
      "flags": [
        "F1"
      ],
      "file": "Assets/Scripts/Swarm/SwarmComponents.cs",
      "line": 9,
      "severity": "high",
      "area": "burst",
      "title": "Drone.Callsign is a managed string field",
      ...
    },
    {
      "id": "DK-03",
      "flags": [
        "F5"
      ],
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 87,
      "severity": "high",
      "area": "burst",
      "title": "DroneMoveJob.Execute reads UnityEngine.Time.time",
      ...
    },
    {
      "id": "DK-14",
      "flags": [
        "F3"
      ],
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 48,
      "severity": "high",
      "area": "structural",
      "title": "Structural changes inside the spawner's query foreach",
      "evidence": "                var drone = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);",
      "why": "The foreach walks the chunks SystemAPI.Query is iterating; EntityManager.Instantiate on line 48 and EntityManager.AddComponent<SwarmSpawned> on line...",
      "edits": [
        {
          "before": "        foreach (var (spawner, origin, entity) in",
          "after": "        var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);\n\n        foreach (var (spawner, origin, entity) in"
        },
        {
          "before": "                var drone = state.EntityManager.Instantiate(spawner.ValueRO.Prefab);",
          "after": "                var drone = ecb.Instantiate(spawner.ValueRO.Prefab);"
        },
        ...
      ]
    },
    {
      "id": "DK-20",
      "flags": [
        "F2"
      ],
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 45,
      "severity": "high",
      "area": "memory",
      "title": "Random seeded with zero",
      "evidence": "            var random = new Random(0);",
      "why": "Unity.Mathematics.Random treats state zero as invalid: with safety checks on it throws \"Seed must be...",
      "edits": [
        {
          "before": "            var random = new Random(0);",
          "after": "            var random = Random.CreateFromIndex((uint)entity.Index);"
        }
      ]
    }
  ],
  "flag_answers": [
    {
      "flag": "F3",
      "status": "confirmed",
      "reason": "Both the Instantiate on line 48 and the AddComponent on line 54 run inside the SystemAPI.Query foreach with no WithStructuralChanges() and no command buffer."
    },
    ...
  ],
  "plan": [
    ...,
    "Open the Burst Inspector on DroneMoveJob.Execute and both systems' OnUpdate to confirm they compiled, and the Profiler Timeline for a frame with an active spawner to confirm DroneMoveJob still runs in parallel."
  ],
  "questions": [
    "FixedString32Bytes holds about 29 UTF-8 bytes - is that enough for every Callsign this...",
    ...
  ],
  "summary": "Five high-severity problems stand between this and Friday's build, in the order to fix them: DroneMoveSystem..."
}

The reply has 5 findings, all high, and confirms all 5 flags; its verdict is blocked, as the page read it. The findings carry 11 edits in all: partial added to DroneMoveSystem (DK-01, F4, 1 edit); using Unity.Collections; added and Callsign made a FixedString32Bytes (DK-02, F1, 2 edits); an ElapsedTime field on DroneMoveJob, set from (float)SystemAPI.Time.ElapsedTime in OnUpdate and read in place of Time.time (DK-03, F5, 3 edits); an ecb from the EndSimulationEntityCommandBufferSystem singleton created before the loop, with Instantiate, SetComponentData and AddComponent<SwarmSpawned> recorded into it (DK-14, F3 — the aggregated flag on lines 48 and 54, 4 edits); and Random.CreateFromIndex((uint)entity.Index) in place of new Random(0) (DK-20, F2, 1 edit). The jobs area is sound: DroneMoveJob is scheduled with the no-argument ScheduleParallel(), so state.Dependency is handled for it. Its two questions ask whether FixedString32Bytes is long enough for every callsign and whether a swarm may start moving the frame after it spawns, which the paste cannot answer.

Handing the audit to the fix lane

On the page, “Apply every edit and fix the rest” on this audit runs Recon.fixHandoff: all 11 edits apply in order (11 edit(s) applied, 0 did not apply from handoff.js), the edited paste replaces the original, the free check runs again on it and the fix lane is selected. The edited paste is 2,625 characters and 94 lines; its facts raise no flag but the DK-22 context flag, so its verdict_candidate is ready. The audit_handoff string (2,367 characters), decoded:

{
  "verdict": "blocked",
  "headline": "Blocked: DroneMoveSystem won't compile, DroneMoveJob fails Burst, and SwarmSpawnSystem throws the first time it spawns a swarm.",
  "findings": [
    {
      "id": "DK-01",
      "severity": "high",
      "file": "Assets/Scripts/Swarm/DroneMoveSystem.cs",
      "line": 67,
      "title": "DroneMoveSystem is not partial",
      "why": "OnUpdate uses SystemAPI.Time and schedules DroneMoveJob (an...",
      "edits": 1
    },
    ...,
    {
      "id": "DK-14",
      "severity": "high",
      "file": "Assets/Scripts/Swarm/SwarmSpawnSystem.cs",
      "line": 48,
      "title": "Structural changes inside the spawner's query foreach",
      "why": "The foreach walks the chunks SystemAPI.Query is iterating...",
      "edits": 4
    },
    ...
  ],
  "applied": 11,
  "failed": 0
}

The fix request the page then sends (Idempotency-Key: dots-desk:fix:e06f3253-26e3:a1), with the audit’s note carried over; code is the edited paste and facts is recomputed from it:

{
  "task": "fix",
  "code": "// file: Assets/Scripts/Swarm/SwarmComponents.cs\nusing Unity.Entities;\n...",
  "facts": "{\"summary\":{\"csharp_files\":3,\"types\":6,\"systems\":2,\"jobs\":1,\"burst_methods\":4,\"allocations\":0},...",
  "note": "First DOTS feature on the team. The spawner and the drone movement are going into Friday's playtest build - what breaks with Burst and the safety checks on, and what do we fix first?",
  "audit_handoff": "{\"verdict\":\"blocked\",\"headline\":\"Blocked: DroneMoveSystem won't compile, DroneMoveJob fails Burst, and SwarmSpawnSystem throws the first time it spawns a swarm.\",..."
}

Over the API you build the same body with handoff.js and send it with makeInput("fix", "swarm-fix-check.json", note) from step 4. Because every high and medium flag is already closed in code, the fix lane’s instructions say to change nothing unless the model finds a problem of that kind itself: then files, changes and open are [], the verdict is fixed, and summary says the code needs no fix. The page's saved swarm fix run (the example one click after the audit) came back exactly that way: verdict fixed, no file, no change, nothing open.

Fix: a bullet system that destroys entities inside its SystemAPI.Query loop, disposes an array its job still reads and reads a singleton it never requires

Two files under Assets/Scripts/Bullets/: BulletComponents.cs (Bullet, BulletConfig and Target) and BulletSystem.cs: BulletSystem reads the BulletConfig singleton without requiring it, destroys expired bullets with state.EntityManager.DestroyEntity inside its SystemAPI.Query foreach, copies the targets into a TempJob array, schedules BulletMoveJob in parallel on state.Dependency and then calls targets.Dispose() at once; the job’s Targets field is not [ReadOnly]. The free check raises 4 flags (3 high, 1 medium, 0 low); its reading of the verdict is blocked. This example has no audit before it, so it carries no audit_handoff. The engineer’s note: “Bullets throw a safety exception every frame and the Console says the array was deallocated. Fix it - nothing to keep, we are still prototyping.”

The code string, whole: 2,114 characters, 82 lines ending in a newline. Save it as bullets-paste.txt and run node build-check.js bullets-paste.txt > bullets-check.json:

// file: Assets/Scripts/Bullets/BulletComponents.cs
using Unity.Entities;
using Unity.Mathematics;

public struct Bullet : IComponentData
{
    public float3 Velocity;
    public float Lifetime;
}

public struct BulletConfig : IComponentData
{
    public float HitRadius;
}

public struct Target : IComponentData
{
    public float Health;
}
---
// file: Assets/Scripts/Bullets/BulletSystem.cs
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

[BurstCompile]
public partial struct BulletSystem : ISystem
{
    EntityQuery _targets;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _targets = SystemAPI.QueryBuilder().WithAll<Target, LocalTransform>().Build();
    }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var config = SystemAPI.GetSingleton<BulletConfig>();
        float dt = SystemAPI.Time.DeltaTime;

        foreach (var (bullet, entity) in SystemAPI.Query<RefRW<Bullet>>().WithEntityAccess())
        {
            bullet.ValueRW.Lifetime -= dt;
            if (bullet.ValueRO.Lifetime <= 0f)
                state.EntityManager.DestroyEntity(entity);
        }

        var targets = _targets.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
        state.Dependency = new BulletMoveJob
        {
            Targets = targets,
            HitRadiusSq = config.HitRadius * config.HitRadius,
            DeltaTime = dt
        }.ScheduleParallel(state.Dependency);
        targets.Dispose();
    }
}

[BurstCompile]
public partial struct BulletMoveJob : IJobEntity
{
    public NativeArray<LocalTransform> Targets;
    public float HitRadiusSq;
    public float DeltaTime;

    void Execute(ref LocalTransform transform, ref Bullet bullet)
    {
        transform.Position += bullet.Velocity * DeltaTime;
        for (int i = 0; i < Targets.Length; i++)
        {
            if (math.distancesq(Targets[i].Position, transform.Position) < HitRadiusSq)
            {
                bullet.Lifetime = 0f;
                break;
            }
        }
    }
}

Its facts, decoded and cut to summary, files, flags and verdict_candidate (the full object also carries the 5 types, BulletSystem with its one ScheduleParallel(state.Dependency) -> state.Dependency, the job, the one TempJob allocation targets passed to BulletMoveJob.Targets, the one structural change and the 3 functions), 5,909 characters as a string:

{
  "summary": {
    "csharp_files": 2,
    "types": 5,
    "systems": 1,
    "jobs": 1,
    "burst_methods": 3,
    "allocations": 1
  },
  "files": [
    {
      "path": "Assets/Scripts/Bullets/BulletComponents.cs",
      "kind": "csharp",
      "line": 2,
      "end_line": 19
    },
    {
      "path": "Assets/Scripts/Bullets/BulletSystem.cs",
      "kind": "csharp",
      "line": 22,
      "end_line": 83
    }
  ],
  "flags": [
    {
      "uid": "F1",
      "id": "DK-14",
      "sev": "high",
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "line": 49,
      "text": "EntityManager.DestroyEntity inside the SystemAPI.Query foreach in BulletSystem.OnUpdate",
      "detail": "A structural change (create, destroy, add or remove a..."
    },
    {
      "uid": "F2",
      "id": "DK-08",
      "sev": "high",
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "line": 59,
      "text": "targets.Dispose() right after BulletMoveJob is scheduled with it",
      "detail": "The job has only been scheduled; it runs later on a worker..."
    },
    {
      "uid": "F3",
      "id": "DK-12",
      "sev": "high",
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "line": 66,
      "text": "BulletMoveJob.Targets (NativeArray) is only read but not [ReadOnly] (scheduled in parallel)",
      "detail": "In a job scheduled in parallel a writable container is..."
    },
    {
      "uid": "F4",
      "id": "DK-17",
      "sev": "medium",
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "line": 42,
      "text": "BulletSystem.OnUpdate calls GetSingleton<BulletConfig>() but BulletSystem never requires BulletConfig",
      "detail": "GetSingleton throws when there is not exactly one entity..."
    },
    {
      "uid": "F5",
      "id": "DK-22",
      "sev": "info",
      "file": "Assets/Scripts/Bullets/BulletComponents.cs",
      "line": 2,
      "text": "context: 2 C# files, 5 types (3 component, 1 ISystem, 1 job), 3 Burst-compiled methods, 1 job instance, 1 native allocation, 0 hand-made command buffers",
      "detail": "Only what is pasted is checked: other systems, the..."
    }
  ],
  "verdict_candidate": "blocked",
  "notes": []
}

The request body (Idempotency-Key: dots-desk:fix:3babff4d-228c:a1):

{
  "task": "fix",
  "code": "// file: Assets/Scripts/Bullets/BulletComponents.cs\nusing Unity.Entities;\n...",
  "facts": "{\"summary\":{\"csharp_files\":2,\"types\":5,\"systems\":1,\"jobs\":1,\"burst_methods\":3,\"allocations\":1},...",
  "note": "Bullets throw a safety exception every frame and the Console says the array was deallocated. Fix it - nothing to keep, we are still prototyping."
}

The reply (output.output), an excerpt: the verdict, the headline, the middle of the one returned file (1,916 characters, 65 lines), all four changes, the first and last verification steps, and the empty open list:

{
  "task": "fix",
  "verdict": "fixed",
  "headline": "BulletSystem now retires bullets through a command buffer, disposes the parallel job's array on its handle, marks that array [ReadOnly], and requires the BulletConfig singleton before updating.",
  "files": [
    {
      "path": "Assets/Scripts/Bullets/BulletSystem.cs",
      "content": "...\n        var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);\n\n        foreach (var (bullet, entity) in SystemAPI.Query<RefRW<Bullet>>().WithEntityAccess())\n        {\n            bullet.ValueRW.Lifetime -= dt;\n            if (bullet.ValueRO.Lifetime <= 0f)\n                ecb.DestroyEntity(entity);\n        }\n\n        var targets = _targets.ToComponentDataArray<LocalTransform>(Allocator.TempJob);\n        state.Dependency = new BulletMoveJob\n        {\n            Targets = targets,\n            HitRadiusSq = config.HitRadius * config.HitRadius,\n            DeltaTime = dt\n        }.ScheduleParallel(state.Dependency);\n        targets.Dispose(state.Dependency);\n..."
    }
  ],
  "changes": [
    {
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "function": "BulletSystem.OnUpdate",
      "flags": [
        "F1"
      ],
      "change": "Replaced the EntityManager.DestroyEntity call inside the query foreach with ecb.DestroyEntity, recording it through an EntityCommandBuffer taken from the EndSimulationEntityCommandBufferSystem singleton.",
      "why": "Prevents the 'structural changes are not allowed while iterating' exception thrown when an entity is destroyed mid-iteration."
    },
    {
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "function": "BulletSystem.OnUpdate",
      "flags": [
        "F2"
      ],
      "change": "Changed targets.Dispose() to targets.Dispose(state.Dependency).",
      "why": "Frees the native array only after BulletMoveJob (which reads it on a worker thread) has completed, instead of while the scheduled job may still be using it."
    },
    {
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "function": "BulletMoveJob",
      "flags": [
        "F3"
      ],
      "change": "Marked the job's Targets field [ReadOnly].",
      "why": "The job only reads Targets; unmarked in a job scheduled in parallel, reading any element outside the job's own index range throws under the safety checks."
    },
    {
      "file": "Assets/Scripts/Bullets/BulletSystem.cs",
      "function": "BulletSystem.OnCreate",
      "flags": [
        "F4"
      ],
      "change": "Added state.RequireForUpdate<BulletConfig>(); in OnCreate.",
      "why": "Stops OnUpdate's GetSingleton<BulletConfig>() from throwing on frames where the BulletConfig singleton entity does not yet exist."
    }
  ],
  "verify": [
    "Compile in the Editor with Burst enabled and Jobs > Burst > Safety Checks on.",
    ...,
    "Open the Burst Inspector and confirm BulletSystem.OnUpdate, BulletSystem.OnCreate and BulletMoveJob.Execute still compile under Burst."
  ],
  "open": [],
  "questions": [],
  "summary": "Fixed all four flagged problems in BulletSystem.cs: destroying a bullet now goes through an EntityCommandBuffer from..."
}

The four changes close the four flags one each: ecb.DestroyEntity through the EndSimulationEntityCommandBufferSystem singleton’s buffer (F1), targets.Dispose(state.Dependency) (F2), [ReadOnly] on BulletMoveJob.Targets (F3) and state.RequireForUpdate<BulletConfig>() in OnCreate (F4). The verdict is fixed because open is empty; BulletComponents.cs needed no change, so it is not returned.

The page checks the reply against the code; the API does not

What comes back from the API is the model’s raw text in output.output. The page holds it to account in the browser before showing it (recon.js reconcile, with the facts, the code and the handoff it sent): it applies every edit or returned file to the paste and re-runs its own checks on the result. None of that happens in the API. A direct API caller gets the reply unchecked and must do the applying and re-checking itself.

recon.js also runs in Node, so you can hold a reply to exactly the page’s standard. Save the page’s /dotskit.js and /recon.js next to this script, pass the file you built the body from (build-check.js or handoff.js output), the reply text and the lane; it prints every statement, the count, any flag still raised, and the paste with every edit (or file) put back:

// recheck.js -- run: node recheck.js swarm-check.json swarm-reply.txt audit > swarm-edited.txt   (dotskit.js, recon.js next to it)
const fs = require("fs");
const Recon = require("./recon.js");                                            // loads ./dotskit.js itself
const sent = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));             // code, facts (and audit_handoff) you sent
const lane = process.argv[4] || "audit";
const res = Recon.normalize(Recon.parseResult(fs.readFileSync(process.argv[3], "utf8")), lane);  // output.output
const r = Recon.reconcile(res, { facts: sent.facts, code: sent.code, handoff: sent.audit_handoff || null });
r.notes.forEach((n) => console.error(n.kind + ": " + n.text));
console.error(r.disagreements + " disagreement(s) in " + r.statements + " statements checked");
if (r.after) (r.after.flags || []).forEach((f) => console.error("still raised: " + f.id + " " + f.sev + " " + f.file + ":" + f.line + " " + f.text));
if (r.after && r.after.text) console.log(r.after.text.replace(/\n$/, ""));      // the paste with every edit (or file) put back

For the swarm audit reply it reports:

ok: The areas table rates Burst, jobs, structural changes and memory once each.
ok: Every one of the 5 findings names a pasted file, a line inside it and evidence copied from it.
ok: The verdict (blocked) fits the worst finding.
ok: Every one of the page's 5 flags is answered once.
ok: All 11 edits applied in order, each before text found exactly once.
ok: Every confirmed flag a finding carries is gone once all edits are applied.
ok: Every type and method of the paste is still there after the edits.
0 disagreement(s) in 7 statements checked

and for the bullets fix reply (node recheck.js bullets-check.json bullets-reply.txt fix):

ok: 1 file returned whole.
ok: With the fixed files in place the free check raises no high or medium flag (0 low).
ok: Every one of the 5 types and 3 methods is still there.
ok: Every one of the 4 changes names a returned file, a method or type of the fixed code and real page flags.
ok: The verdict (fixed) fits the open list.
0 disagreement(s) in 5 statements checked

Neither run prints a still raised: line: with every audit edit (or the fixed file) in place, the free check raises no flag above info. That is the page’s own local reading — the same text rules, run again on the edited paste, in your browser or in Node, with no network call. The page does not compile or run the code. After that, compile in the Editor with Burst enabled and Jobs > Burst > Safety Checks on, enter Play mode with the Console clear, and open the Burst Inspector and the Profiler (Timeline) for the systems that changed, plus the checks the reply’s plan or verify names, before anything goes into a build.

Step 1 — a tiny client helper

One function per language that does what sdk.js does for every call: JSON in, bearer token if you have one, data out on a 2xx, an error carrying status, error.code and error.message otherwise. The later steps assume this helper is in the same file (for Go, Java and C#, the later snippets go inside main / the top-level statements; Java’s makeInput from step 4 goes in the class).

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="${SKILLSAFE_TOKEN:-YOUR_TOKEN}"   # step 2

# api METHOD PATH [extra curl args...] -- prints the whole envelope; pipe to jq
api() {
  method="$1"; path="$2"; shift 2
  curl -sS -X "$method" "$BASE$path" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" "$@"
}
# success: jq '.data'      failure: jq '.error | {code, message}'

Step 2 — a token

Two kinds work:

# personal token (needed for a run): copy it from /tokens.html, then
#   export SKILLSAFE_TOKEN=...        and skip the rest of this block
# guest token (enough for /me and /estimate): no Authorization header on this one call
TOKEN=$(curl -sS -X POST "$BASE/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"dots-desk"}' | jq -r '.data.token')
echo "guest token: ${TOKEN:0:12}..."

Step 3 — GET /me

Who the token belongs to and what it can spend. sdk.js documents the shape as {subject_type, subject_id, credits, profile?}. The page treats the caller as signed in only when subject_type is "user", shows credits as the balance (1 credit = $0.0001, so it divides by 10 000 for the dollar figure), and compares it with the estimate in step 4 before it lets you run.

api GET /me | jq '.data // .error'

Step 4 — build the input, then POST /estimate

First a small makeInput that does what buildInput does: task, then code as the paste the page would send, facts as a compact JSON string, the note when it is not blank, and — for the fix lane only — the audit_handoff string when the file carries one. It reads the file build-check.js (or handoff.js) printed. The samples build the audit input of the swarm example from swarm-check.json; step 6 reuses makeInput for the bullets (fix) example. Then price it. The body of /estimate is the same input you will send to /run; no job is created and nothing is charged — /estimate is free. From the response the page reads:

FieldMeaning
hold_creditsA reservation, not a price: the worst case the platform holds while the run is in flight. The page shows it as “reserves N credits ($...) for the audit run” and adds that you are charged only for what the run actually uses, usually far less. What you pay is the job’s charged_credits after settlement (step 5). A balance below the hold is allowed, but the page warns that the reply may be cut short (truncated: true).
min_creditsThe smallest balance that can start a run. Below it the page disables the run button and names the shortfall, because the call would be a 402.
sponsor_enabledWhen true, a caller who is not signed in may run; otherwise the page asks them to sign in.
warningsInput warnings. They do not refuse the call; the page shows them next to the price (“input warnings: ...”), so check them before you run.

The page re-prices whenever the paste, the note or the lane change. The amounts grow with the size of code and facts, so read them from the call rather than hard-coding them.

# swarm-check.json: printed by build-check.js (see "The facts string").
# (.facts | tojson) turns the facts into ONE compact string, as JSON.stringify does.
BODY=$(jq -c --arg note "First DOTS feature on the team. The spawner and the drone movement are going into Friday's playtest build - what breaks with Burst and the safety checks on, and what do we fix first?" \
  '{task: "audit", code, facts: (.facts | tojson), note: $note}' swarm-check.json)
# No page check at hand? facts: "{}" is accepted as a string, but the reply then has no page flags to answer.
# Fix lane after an audit: read handoff.js's output instead and add its string:
#   '{task: "fix", code, facts: (.facts | tojson), note: $note} + (if .audit_handoff then {audit_handoff} else {} end)'

api POST /estimate -d "$BODY" \
  | jq '.error // (.data | {hold_credits, min_credits, sponsor_enabled, warnings})'

Step 5 — POST /run, then poll the job (audit)

Send the same input to /run with an Idempotency-Key header, using a personal token. Its data carries a job_id. Poll GET /jobs/{job_id} the way sdk.js waitForJob does: every second until status is succeeded or failed, giving up on the client side after 180 seconds. On the finished job, read:

Idempotency key. Resending a request with the same key must not start a second paid run, so derive the key from the input, not from the clock — and include the lane, so an audit and a fix run of the same paste never share a key. The page uses dots-desk:<lane>:<inputHash>:a<attempt> (app.js run), where <lane> is audit or fix, <inputHash> is a 32-bit djb2 hash of JSON.stringify(input) in hex, a dash, and the string’s length in hex (app.js inputHash, over the input without retry_note), and <attempt> is 1 for a normal run and 2 for the automatic reformat retry (which also adds retry_note). For the two worked examples that is dots-desk:audit:ebdd3062-27b2:a1 (swarm audit) and dots-desk:fix:3babff4d-228c:a1 (bullets fix); the fix run of the swarm audit handed on (above) is dots-desk:fix:e06f3253-26e3:a1. The format is the page’s convention, not a requirement: any stable, unique string does. The samples below use the lane and a SHA-256 prefix.

The page itself uses /run-stream (step 6), not /run; both take the same body and header.

KEY="dots-desk:audit:$(printf '%s' "$BODY" | shasum -a 256 | cut -c1-16):a1"

JOB=$(api POST /run -H "Idempotency-Key: $KEY" -d "$BODY" | jq -r '.data.job_id')

for i in $(seq 180); do                       # 1 s interval, 180 s client-side limit
  J=$(api GET "/jobs/$JOB")
  S=$(printf '%s' "$J" | jq -r '.data.status')
  if [ "$S" = "succeeded" ] || [ "$S" = "failed" ]; then break; fi
  sleep 1
done

printf '%s' "$J" | jq '.data | {status, truncated, charged_credits, error}'
printf '%s' "$J" | jq -r '.data.output.output' \
  | jq '{verdict, headline, areas: [(.areas // [])[] | {area, status}],
         findings: [(.findings // [])[] | {id, file, line, severity, edits: ((.edits // []) | length)}],
         flag_answers: [(.flag_answers // [])[] | {flag, status}]}'

Step 6 — POST /run-stream (Server-Sent Events, fix)

The alternative to step 5, and what the page uses: same kind of body, same Idempotency-Key header, one request. Run one or the other for a given input, not both. To show the second lane, the samples build the fix input of the bullets example (with its own key) and stream it; to stream the audit input instead, pass the step 4 input and key. The response is text/event-stream. sdk.js splits it into frames on a blank line, takes the event name from an event: line (default message) and concatenates the data: lines, skips frames with no data or with data that is not JSON, and handles these events:

EventDataWhat the SDK does
delta{text}Passes text to onDelta.
jobjob infoPasses it to onJob (the page uses it to advance its progress display).
donethe final payload: {job_id, status, charged_credits, output}Keeps it as the result.
pendinga payloadTreated exactly like done. If it carries a job_id and no output, poll that job as in step 5.
error{message, code, job_id}After the stream ends, raises an error with those fields.

If the response is not text/event-stream, it is an ordinary JSON envelope: the SDK returns its data (an idempotent replay returns this way) or raises the error on a non-2xx status. Handle both shapes.

From the final payload the page reads output.output (falling back to the concatenated delta text), truncated and charged_credits. Browsers receive progress ticks rather than deltas from this endpoint, so the page relies on the final payload; if the stream fails part-way, it tries to recover a partial reply from whatever delta text arrived. The samples print the verdict and headline, each returned file’s path and size, each change (function, flags and change), each verification step, each open item and each question. To put the files into the paste and re-check them the way the page’s “Put every file into the paste” does, keep the reply text and run recheck.js with the lane fix. To fix code you audited, read handoff.js’s output instead of bullets-check.json (for the swarm, swarm-fix-check.json): makeInput then carries its audit_handoff.

# the fix worked example: bullets-check.json from build-check.js
# (for audited code, read handoff.js's output instead; the + (...) adds its audit_handoff)
FBODY=$(jq -c --arg note "Bullets throw a safety exception every frame and the Console says the array was deallocated. Fix it - nothing to keep, we are still prototyping." \
  '{task: "fix", code, facts: (.facts | tojson), note: $note} + (if .audit_handoff then {audit_handoff} else {} end)' \
  bullets-check.json)
FKEY="dots-desk:fix:$(printf '%s' "$FBODY" | shasum -a 256 | cut -c1-16):a1"

# -N turns off buffering; the whole stream lands in $STREAM
STREAM=$(api POST /run-stream -N -H "Idempotency-Key: $FKEY" -d "$FBODY")

# frames look like:
#   event: job
#   data: {...}
#
#   event: done
#   data: {"job_id":"...","status":"...","charged_credits":...,"output":{"output":"{\"task\":\"fix\",...}"}}

# the final reply text, from the done (or pending) frame:
REPLY=$(printf '%s\n' "$STREAM" \
  | awk '/^event:/{ev=$2} /^data:/ && (ev=="done" || ev=="pending"){sub(/^data: */,""); print}' \
  | jq -r '.output.output')
# an idempotent replay answers with a plain JSON envelope instead: REPLY=$(printf '%s' "$STREAM" | jq -r '.data.output.output')
printf '%s' "$REPLY" | jq '{verdict, headline,
  files: [(.files // [])[] | {path, characters: (.content | length)}],
  changes: [(.changes // [])[] | {function, flags, change}], verify, open, questions}'

What the page does that you now do

Or skip all of this and use DOTS Desk in the browser: the check, the flags, the systems, jobs, allocations, command buffers and structural changes it inventories, putting an edit or the fixed files back into the paste, and the Markdown and CSV exports are free.