← Blocking Call Audit / API
Tokens

Drive Blocking Call Audit from your own code

The page reads pasted async Rust in the browser for free — one or more files joined into one paste, each introduced by a // file: <path> line, plus Cargo.toml (introduced by # file: Cargo.toml) for the tokio version and features — with its line numbers. It lexes strings, raw strings, char literals and nested comments out first, rebuilds the brace structure (fns, impl blocks, async blocks, loops, select! arms), resolves the use declarations, and inventories every async fn and whether it awaits, every call async code makes that blocks a runtime worker thread, every future built and never awaited, every spawned task and what happens to its JoinHandle, every std or parking_lot lock guard and whether it is still alive at an .await, every channel, every select! and whether its branches are cancel safe, and the tokio features Cargo.toml turns on. Then it runs 17 deterministic checks drawn from Tokio practice (catalogue ids TK-01..TK-17, plus TK-18 for context): std::thread::sleep and blocking network calls (reqwest::blocking, ureq, std::net) in async code, blocking process, database, channel and join calls, blocking std::fs I/O, block_on or a new Runtime inside async code, loops that never await, futures never awaited, dropped JoinHandles, discarded Results of awaited calls, unbounded fan-out, std lock guards held across an .await, unbounded channels fed in a loop, select! in a loop over a future that is not cancel safe, spawned loops with no shutdown path, tokio features that miss what the code uses, credential literals, and async fns that never await. Nothing is compiled or run, and it is not cargo clippy or tokio-console: 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 Rustkit.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 Rust engineer reviews a Tokio service before it goes to production: says what the code does and what runs concurrently in it, rates the four areas (runtime, tasks, cancellation, resources) 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, then what stalls the runtime or silently never runs (blocking calls, unawaited futures, guards across awaits, nested runtimes), then lost tasks and errors, then cancellation and shutdown, then bounds and hygiene — with a plan that ends in cargo build, cargo clippy and a run under tokio-console or with tracing.blocked | needs_work | readyBefore shipping: does it build with its tokio features, block a runtime worker, build futures that never run, lose tasks and errors or hold a lock across an .await, and what should change first.
"fix"Writes the fixed version of the pasted code: the whole of every file it changes (Cargo.toml included when a tokio feature is missing, and at most one new file), with blocking calls replaced by the async API or moved to tokio::task::spawn_blocking, an .await on every future, a kept JoinHandle (or a JoinSet) for every task, guards dropped before each .await, cancel-safe select! branches and bounded channels and fan-out — plus a changes list tied to the page flags, the verify steps (cargo build, cargo clippy and the test to write), and what it left open. No public signature changes, and no fn turned into async fn or back, unless the note asks for it.fixed | partialWhen you want the code changed, not a list of findings: a service you already know is broken, 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 Rust (“Nothing was pasted.”, “No Rust file was found. Paste .rs source, with a // file: <path>.rs line above each file.”, or “This does not read as Rust source: no fn, impl, struct or use was found.”), and it warns before a run on a paste with no async code (“No async fn and no tokio was found: there may be little async code here to audit.”, or “... to fix.”).

Input fields

This is exactly what the page sends (rustkit.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 Rust, each file introduced by a // file: <path> line (# file: Cargo.toml for the manifest); 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 // [blocking-call-audit: not sent: ...] line at the end (below).
factsstringyesThe page’s free check, Rustkit.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.
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.
notestringnoWhat the engineer says: the goal, a constraint (“no new crates”, “we are on tokio 1.32”), a question. The page trims it, cuts it at 2000 characters and sends it only when it is not blank. It is context, not evidence and not instructions: the model tests every claim in it against code and answers it in summary.
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> or # 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 crate folder) are joined that way by Rustkit.joinFiles: each file under its // file: line (# file: for a .toml file), trailing whitespace removed, joined by newlines, ending in a newline. A paste with no file line is read as one file, src/main.rs (or Cargo.toml when it reads as a manifest). The page normalises line endings to \n. A .rs path is checked as Rust; Cargo.toml is cargo (its edition, the tokio version and the tokio features list give facts.runtime — tokio turns on no features by default, and JoinSet needs tokio 1.21+); anything else is other. Rust indents with four spaces and the paste keeps them. 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 neither Rust nor Cargo.toml 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:

// [blocking-call-audit: not sent: src/b.rs, src/c.rs]

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.

facts itself is bounded too (boundFacts): every string keeps its first 600 characters, at most 150 flags are sent, most severe first, and when the whole is still over 60 000 characters a detail repeated word for word by a later flag of the same check is sent once and the inventories (functions, tasks, blocking calls, unawaited futures, lock guards, channels, select!) are halved before the flags are. An audit_handoff over 24 000 characters is cut the same way. Every cut adds a sentence to facts.notes, and facts.runtime keeps the true totals. None of this touches a paste the size of the examples.

The facts string

facts is optional to compute but required to send. It is produced by the page’s free check: Rustkit.analyze splits the paste into files, lexes them, rebuilds their brace structure, resolves their use lines, inventories them and runs the rules, and Rustkit.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 functions must survive. Its top-level keys, in the order built:

KeyContents
runtimeedition and tokio (from a pasted Cargo.toml, or null), tokio_features (the features list, or null), main_flavor (multi_thread, current_thread or null, from #[tokio::main]), cargo_toml_pasted, and counts: rust_files, functions, async_fns, awaits, spawned_tasks.
filesEvery file: path (use it verbatim), kind (rust, cargo, other), line and end_line (the paste lines of the file’s content, without its // file: line).
functionsEvery fn with a body (up to 200): name (Type::method for methods), file, line, async.
tasksEvery tokio::spawn, spawn_blocking, spawn_local and JoinSet::spawn (up to 120): file, line, func (where it is spawned), kind, target, kept (the JoinHandle is awaited, stored, returned or owned by a JoinSet), in_loop. A task whose async block reports its own errors is kept: false without a flag.
blocking_callsEvery call async code makes that blocks the worker thread (up to 120): file, line, func, call, id (the catalogue id it falls under).
unawaited_futuresEvery future built and dropped without being awaited (up to 60): file, line, func, call.
lock_guardsEvery .lock(), .read().unwrap() or .write().unwrap() in async code that is not awaited — a std or parking_lot lock (up to 80): expr, bound_to, file, line, func, across_await, await_line.
channelsEvery channel created (up to 60): name, kind, capacity, file, line, func.
selectsEvery select! (up to 40): file, line, func, in_loop, branches, not_cancel_safe.
flagsEvery flag the page raised: uid (F1, F2, ... in severity, file, then line order), id (TK-01..TK-18), 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 rustkit.js itself: the page loads it from /rustkit.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 ingest-paste.txt and redirect the output into ingest-check.json:

// build-check.js -- run: node build-check.js ingest-paste.txt > ingest-check.json   (rustkit.js saved next to it)
const fs = require("fs");
const Rustkit = require("./rustkit.js");
const A = Rustkit.analyze(fs.readFileSync(process.argv[2], "utf8"));
if (!A.ok) { console.error(A.errors.join("\n")); process.exit(1); }
const inp = Rustkit.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));
const real = A.flags.filter((f) => f.sev !== "info");
console.error(A.files.filter((f) => f.kind === "rust").length + " Rust file(s), " + A.async_fns + " async fn(s) of " + A.functions.length + " fn(s), " + A.tasks.length + " spawned task(s), " + A.selects.length + " select!; " +
  real.length + " flags (" + A.counts.high + " high, " + A.counts.medium + " medium, " + A.counts.low + " low); verdict_candidate " + A.candidate);

For the ingest example it reports 2 Rust file(s), 4 async fn(s) of 4 fn(s), 2 spawned task(s), 0 select!; 9 flags (5 high, 1 medium, 3 low); verdict_candidate blocked; for the thumbs example, 1 Rust file(s), 3 async fn(s) of 5 fn(s), 1 spawned task(s), 0 select!; 5 flags (3 high, 0 medium, 2 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 runtime the model does not know the tokio version and features, and with no functions nothing says what must survive a fix. Build it with rustkit.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, Rustkit);   // 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 Rustkit.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 ingest-paste.txt ingest-reply.txt > ingest-fix-check.json   (rustkit.js, recon.js next to it)
const fs = require("fs");
const Rustkit = require("./rustkit.js");
const Recon = require("./recon.js");
const A = Rustkit.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 = Rustkit.analyze(ho.text);                                             // facts recomputed from the edited paste
const inp = Rustkit.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 ingest-reply.txt; for the ingest audit below it reports 6 edit(s) applied, 0 did not apply, and the page sends that fix run with the audit’s note and the key blocking-call-audit:fix:a17c90ef-319c: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: a caller, a module, a trait impl, a runtime setup, a shutdown path, a timeout or a crate’s behaviour that is not in the paste is never claimed to exist — it goes into questions; whether a function from another crate or module is async, or blocks, is unknown unless the paste shows it. It must never write a secret value: a line that holds a literal credential is never copied into the reply (not in evidence, not in an edit, not in a file), and no credential, real hostname or endpoint is invented. It must never change what the code offers the rest of the crate: the same pub items with the same names, parameters, return types and generic bounds, and the same sync-or-async nature of every fn, unless the note asks for it. It uses an API only when it is sure the API exists in that crate and version (tokio 1.x unless the paste says otherwise; JoinSet needs 1.21+; CancellationToken lives in tokio-util), and never adds a crate the paste does not already depend on unless the note allows it. 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 TK7 or tk 7 become TK-07; flags keeps only F<n> values; findings with no file, evidence and title are dropped; service may also arrive as component or runtime, file as path, a change’s function as func 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, service, 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; the full pastes are in /example.js, ids ingest and thumbs), built exactly as the page builds them: Rustkit.buildInput(lane, Rustkit.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 and facts strings with a visible ..., and the replies are pretty-printed excerpts, with ... where entries were left out. Every value shown is copied from the input or the reply. Neither paste holds a credential; the collector host is an invented .invalid name.

Audit: a meter-reading ingest that sleeps and calls reqwest::blocking on a runtime worker, holds a std Mutex guard across an .await and is missing tokio features

Three files: Cargo.toml (tokio 1.38 with only rt-multi-thread, net, sync and time, and reqwest with its blocking feature), src/main.rs and src/forward.rs: a TCP ingest for electricity-meter readings. main listens on port 7400, spawns forward::flush_loop and drops its JoinHandle, and spawns one handle task per connection; handle adds each reading to a shared std::sync::Mutex map and keeps the guard alive across forward::audit_log(&reading).await; flush_loop is a loop that calls std::thread::sleep for 30 seconds and posts the batch with a reqwest::blocking::Client; audit_log is an async fn that only calls std::fs. The page’s free check raises 9 flags (5 high, 1 medium, 3 low) (plus the TK-18 context flag); its own reading of the verdict is blocked. The engineer’s note: “Readings stop arriving every 30 seconds and under load the whole service freezes. What do we fix before it takes the second substation’s feed? Changing the signatures in forward.rs is fine - main is their only caller.”

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

# file: Cargo.toml
[package]
name = "meter-ingest"
version = "0.3.1"
edition = "2021"

[dependencies]
tokio = { version = "1.38", features = ["rt-multi-thread", "net", "sync", "time"] }
reqwest = { version = "0.12", features = ["blocking", "json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
---
// file: src/main.rs
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use anyhow::Result;
use serde::Deserialize;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::{TcpListener, TcpStream};

mod forward;

#[derive(Debug, Deserialize)]
pub struct Reading {
    pub meter_id: String,
    pub kwh: f64,
}

pub type Totals = Arc<Mutex<HashMap<String, f64>>>;

#[tokio::main]
async fn main() -> Result<()> {
    let totals: Totals = Arc::new(Mutex::new(HashMap::new()));
    let listener = TcpListener::bind("0.0.0.0:7400").await?;
    tokio::spawn(forward::flush_loop(totals.clone()));
    loop {
        let (socket, peer) = listener.accept().await?;
        let totals = totals.clone();
        tokio::spawn(async move {
            if let Err(e) = handle(socket, totals).await {
                tracing::warn!("connection {peer} failed: {e}");
            }
        });
    }
}

async fn handle(socket: TcpStream, totals: Totals) -> Result<()> {
    let mut lines = BufReader::new(socket).lines();
    while let Some(line) = lines.next_line().await? {
        let reading: Reading = serde_json::from_str(&line)?;
        let mut map = totals.lock().unwrap();
        *map.entry(reading.meter_id.clone()).or_insert(0.0) += reading.kwh;
        forward::audit_log(&reading).await?;
    }
    Ok(())
}
---
// file: src/forward.rs
use std::io::Write;
use std::time::Duration;

use anyhow::Result;

use crate::{Reading, Totals};

const COLLECTOR_URL: &str = "https://collector.invalid/v1/usage";

pub async fn flush_loop(totals: Totals) {
    loop {
        std::thread::sleep(Duration::from_secs(30));
        let batch: Vec<(String, f64)> = {
            let mut map = totals.lock().unwrap();
            map.drain().collect()
        };
        if batch.is_empty() {
            continue;
        }
        let client = reqwest::blocking::Client::new();
        match client.post(COLLECTOR_URL).json(&batch).send() {
            Ok(resp) => tracing::info!("flushed {} meters: {}", batch.len(), resp.status()),
            Err(e) => tracing::error!("flush failed, {} meters lost: {e}", batch.len()),
        }
    }
}

pub async fn audit_log(reading: &Reading) -> Result<()> {
    let mut file = std::fs::OpenOptions::new().append(true).create(true).open("audit.csv")?;
    writeln!(file, "{},{}", reading.meter_id, reading.kwh)?;
    Ok(())
}

Its facts, decoded (the request carries this object as one compact JSON string):

{
  "runtime": {
    "edition": "2021",
    "tokio": "1.38",
    "tokio_features": [
      "rt-multi-thread",
      "net",
      "sync",
      "time"
    ],
    "main_flavor": "multi_thread",
    "cargo_toml_pasted": true,
    "rust_files": 2,
    "functions": 4,
    "async_fns": 4,
    "awaits": 5,
    "spawned_tasks": 2
  },
  "files": [
    {
      "path": "Cargo.toml",
      "kind": "cargo",
      "line": 2,
      "end_line": 13
    },
    {
      "path": "src/main.rs",
      "kind": "rust",
      "line": 16,
      "end_line": 59
    },
    {
      "path": "src/forward.rs",
      "kind": "rust",
      "line": 62,
      "end_line": 94
    }
  ],
  "functions": [
    {
      "name": "main",
      "file": "src/main.rs",
      "line": 35,
      "async": true
    },
    {
      "name": "handle",
      "file": "src/main.rs",
      "line": 50,
      "async": true
    },
    {
      "name": "flush_loop",
      "file": "src/forward.rs",
      "line": 71,
      "async": true
    },
    {
      "name": "audit_log",
      "file": "src/forward.rs",
      "line": 89,
      "async": true
    }
  ],
  "tasks": [
    {
      "file": "src/main.rs",
      "line": 38,
      "func": "main",
      "kind": "tokio::spawn",
      "target": "forward::flush_loop(totals.clone())",
      "kept": false,
      "in_loop": false
    },
    {
      "file": "src/main.rs",
      "line": 42,
      "func": "main",
      "kind": "tokio::spawn",
      "target": "async move { if let Err(e) = handle(socket, totals).await...",
      "kept": false,
      "in_loop": true
    }
  ],
  "blocking_calls": [
    {
      "file": "src/forward.rs",
      "line": 73,
      "func": "flush_loop",
      "call": "std::thread::sleep",
      "id": "TK-01"
    },
    {
      "file": "src/forward.rs",
      "line": 81,
      "func": "flush_loop",
      "call": "reqwest::blocking::Client::new",
      "id": "TK-02"
    },
    {
      "file": "src/forward.rs",
      "line": 82,
      "func": "flush_loop",
      "call": "client.post",
      "id": "TK-02"
    },
    {
      "file": "src/forward.rs",
      "line": 90,
      "func": "audit_log",
      "call": "std::fs::OpenOptions::new",
      "id": "TK-04"
    }
  ],
  "unawaited_futures": [],
  "lock_guards": [
    {
      "expr": "totals.lock()",
      "bound_to": "map",
      "file": "src/main.rs",
      "line": 54,
      "func": "handle",
      "across_await": true,
      "await_line": 56
    },
    {
      "expr": "totals.lock()",
      "bound_to": "map",
      "file": "src/forward.rs",
      "line": 75,
      "func": "flush_loop",
      "across_await": false,
      "await_line": null
    }
  ],
  "channels": [],
  "selects": [],
  "flags": [
    {
      "uid": "F1",
      "id": "TK-15",
      "sev": "high",
      "file": "Cargo.toml",
      "line": 8,
      "text": "tokio 1.38 in Cargo.toml is missing the features io-util, macros the code uses",
      "detail": "tokio turns on no features by default, so each module the code uses must be enabled: without them the build fails (\"could not find `main` in `tokio`\", \"could not find `time` in `tokio`\"). Add \"io-util\", \"macros\" to its features list - or \"full\" for an application."
    },
    {
      "uid": "F2",
      "id": "TK-11",
      "sev": "high",
      "file": "src/main.rs",
      "line": 54,
      "text": "the guard from totals.lock() in handle is still held at the .await on line 56",
      "detail": "A std::sync (or parking_lot) guard blocks the thread, not the task: while this task is suspended at the .await, any other task that calls lock() on the same lock blocks its worker thread - and if the guard's owner is waiting for that thread, the runtime deadlocks. The guard also makes the future !Send, so tokio::spawn rejects it. Drop the guard before the .await (copy what you need out in a small block), or switch to tokio::sync::Mutex if the lock really must be held across it."
    },
    {
      "uid": "F3",
      "id": "TK-06",
      "sev": "high",
      "file": "src/forward.rs",
      "line": 72,
      "text": "loop { ... } in flush_loop never awaits and never breaks, so it holds its worker thread forever",
      "detail": "Tokio switches tasks only at an .await. This loop has none and no way out: once it starts, its worker thread never runs another task again, and on a current_thread runtime nothing else ever runs. Await something on every iteration (a channel recv, an interval tick, tokio::task::yield_now) or move the work to spawn_blocking."
    },
    {
      "uid": "F4",
      "id": "TK-01",
      "sev": "high",
      "file": "src/forward.rs",
      "line": 73,
      "text": "flush_loop calls std::thread::sleep(): the worker thread sleeps and every task scheduled on it stops",
      "detail": "std::thread::sleep parks the runtime's worker thread itself: every other task on that thread - requests, timers, the shutdown signal - waits until it wakes (on a current_thread runtime, the whole program). Use tokio::time::sleep(d).await, which yields the thread while it waits."
    },
    {
      "uid": "F5",
      "id": "TK-02",
      "sev": "high",
      "file": "src/forward.rs",
      "line": 81,
      "text": "flush_loop makes 2 blocking calls (reqwest::blocking::Client::new(), client.post()): a blocking network call parks a runtime worker thread",
      "detail": "A synchronous HTTP or socket call waits for the network on the worker thread, so no other task scheduled there runs until the reply arrives; a few slow requests stall the whole runtime, and reqwest's blocking client panics when it is created or dropped inside the runtime. Use the async client (reqwest::Client, tokio::net) with .await, or move the call into tokio::task::spawn_blocking.",
      "lines": [
        81,
        82
      ]
    },
    {
      "uid": "F6",
      "id": "TK-08",
      "sev": "medium",
      "file": "src/main.rs",
      "line": 38,
      "text": "tokio::spawn(...) in main drops its JoinHandle",
      "detail": "Dropping the JoinHandle detaches the task: nothing ever learns that it panicked or returned an error, nothing can cancel it (abort) or wait for it at shutdown, and the runtime simply kills it mid-work when main returns. Keep the handle and await it, or spawn into a tokio::task::JoinSet and drain it with join_next().await."
    },
    {
      "uid": "F7",
      "id": "TK-14",
      "sev": "low",
      "file": "src/forward.rs",
      "line": 72,
      "text": "the task spawned at line 38 runs loop { ... } with no way to stop it",
      "detail": "Nothing can end this task: the loop has no break or return, it does not select! on a shutdown signal, and nothing aborts its JoinHandle. At shutdown it is killed mid-iteration (possibly mid-write), and in tests it outlives the test. Pass it a tokio_util::sync::CancellationToken (or a watch/broadcast shutdown receiver) and select! on token.cancelled() next to its work."
    },
    {
      "uid": "F8",
      "id": "TK-17",
      "sev": "low",
      "file": "src/forward.rs",
      "line": 89,
      "text": "async fn audit_log() never awaits",
      "detail": "An async fn with no .await runs start to finish in one poll, without ever yielding: it adds a future and a state machine for nothing, and if it does slow work it blocks like sync code while every caller must still .await it. Await the async API it should be using; if nothing in it ever waits, a plain fn is simpler - but that changes how every caller calls it."
    },
    {
      "uid": "F9",
      "id": "TK-04",
      "sev": "low",
      "file": "src/forward.rs",
      "line": 90,
      "text": "audit_log calls std::fs::OpenOptions::new(): blocking std::fs file I/O runs on a runtime worker thread",
      "detail": "std::fs reads and writes block the worker thread for as long as the disk takes - usually short, but on a network filesystem or a large file it stalls every task on that thread. Use tokio::fs (which runs the call on the blocking pool) or tokio::task::spawn_blocking for a batch of file work."
    },
    {
      "uid": "F10",
      "id": "TK-18",
      "sev": "info",
      "file": "src/main.rs",
      "line": 16,
      "text": "context: 2 Rust files, 4 async fn(s) of 4 fn(s), 5 .await(s), 2 spawned task(s), 0 select!, tokio 1.38 [rt-multi-thread, net, sync, time]",
      "detail": "Also read: Cargo.toml (cargo). Only what is pasted is checked: whether a function from another crate or module blocks or is async, and how the runtime is built and shut down outside the paste, is outside the check."
    }
  ],
  "verdict_candidate": "blocked",
  "notes": []
}

The request body (POST /estimate, then POST /run or /run-stream with Idempotency-Key: blocking-call-audit:audit:4b43fe9a-2996:a1 as the page sends it):

{
  "task": "audit",
  "code": "# file: Cargo.toml\n[package]\nname = \"meter-ingest\"\nversion = \"0.3.1\"\nedition = \"2021\"\n\n[dependencies]\ntokio = { version = \"1.38\", features = [\"rt-multi-thread\", \"net\", \"sync\", \"time\"] }\nreqwest = { version = \"0.12\", features = [\"blocking\", \"json\"] }\n...",
  "facts": "{\"runtime\":{\"edition\":\"2021\",\"tokio\":\"1.38\",\"tokio_features\":[\"rt-multi-thread\",\"net\",\"sync\",\"time\"],\"main_flavor\":\"multi_thread\",\"cargo_toml_pasted\":true,\"rust_files\":2,\"functions\":4,\"asyn...",
  "note": "Readings stop arriving every 30 seconds and under load the whole service freezes. What do we fix before it takes the second substation's feed? Changing the signatures in forward.rs is fine - main is their only caller."
}

The reply (output.output), an excerpt: the verdict, the headline, each area’s status, and the finding that answers “under load the whole service freezes”, with its edit and its flag answer:

{
  "task": "audit",
  "verdict": "blocked",
  "headline": "Blocked: the code as pasted doesn't build, a lock guard spans an await, and flush_loop never yields its worker thread - which is exactly the periodic stall and under-load freeze you're seeing.",
  "areas": [
    {
      "area": "runtime",
      "status": "broken"
    },
    {
      "area": "tasks",
      "status": "fragile"
    },
    {
      "area": "cancellation",
      "status": "sound"
    },
    {
      "area": "resources",
      "status": "broken"
    }
  ],
  "findings": [
    ...,
    {
      "id": "TK-11",
      "flags": [
        "F2"
      ],
      "file": "src/main.rs",
      "line": 54,
      "severity": "high",
      "area": "resources",
      "title": "totals guard held across audit_log's .await in handle",
      "evidence": "let mut map = totals.lock().unwrap();\n*map.entry(reading.meter_id.clone()).or_insert(0.0) += reading.kwh;\nforward::audit_log(&reading).await?;",
      "why": "map is a std MutexGuard bound with let, so it lives to the end of the while-loop body - across the audit_log(...).await on the next line. That makes handle's generated future hold a !Send guard across a suspension point, which poisons the Send bound of the async move block that awaits handle() in main (line 42-46) - as pasted this is a compile error at the tokio::spawn call, not just a deadlock risk. Even if it built, every connection would serialize on this lock while audit_log runs.",
      "edits": [
        {
          "before": "        let mut map = totals.lock().unwrap();\n        *map.entry(reading.meter_id.clone()).or_insert(0.0) += reading.kwh;",
          "after": "        {\n            let mut map = totals.lock().unwrap();\n            *map.entry(reading.meter_id.clone()).or_insert(0.0) += reading.kwh;\n        }"
        }
      ]
    },
    ...
  ],
  "flag_answers": [
    ...,
    {
      "flag": "F2",
      "status": "confirmed",
      "reason": "verified - map is bound with let in handle and the next statement awaits audit_log before map goes out of scope."
    },
    ...
  ],
  "plan": [
    ...,
    "Run cargo build and cargo clippy (await_holding_lock, unused_must_use), then watch a loaded run under tokio-console to confirm no task stalls the runtime again before this takes the second substation's feed."
  ],
  "questions": [
    ...
  ],
  "summary": ...
}

The reply has 8 findings and confirms all 9 flags. Six findings carry one edit each, 6 edits in all: "io-util" and "macros" added to the tokio line in Cargo.toml (F1), the totals guard moved into its own block so it drops before the .await (F2), std::thread::sleep replaced by tokio::time::sleep(...).await (one TK-06 finding carrying F3 and F4), the reqwest::blocking client replaced by the async reqwest::Client with .send().await (F5), the flush task’s JoinHandle kept and raced against listener.accept() in a tokio::select! in main, which logs and returns when flush_loop ends or panics (F6), and one medium finding the page did not flag (other, flags: []): a failed flush now merges its batch back into totals instead of losing it. The two low findings carry no edit. TK-14 (F7, flush_loop has no way to stop) needs a CancellationToken in flush_loop’s signature, which the note allows, but also a shutdown trigger that the paste does not have. The TK-04 finding (F8 and F9, audit_log) needs the fs feature added to Cargo.toml as well as tokio::fs. That is why the re-check below still raises those three low flags.

Fix: a thumbnail worker whose acknowledgements are never sent, whose retry delay never waits and which calls block_on inside the runtime

Two files: Cargo.toml (tokio 1.39 with features = ["full"], and futures) and src/worker.rs. Worker::run calls self.done.send(job.id); on a bounded tokio::sync::mpsc sender and tokio::time::sleep(Duration::from_secs(5)); without .await, so neither future ever runs; Worker::process drives render_thumbnail with futures::executor::block_on and reads and writes with std::fs. The free check raises 5 flags (3 high, 0 medium, 2 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: “Jobs are never acknowledged, the retry delay does nothing and the worker sometimes hangs. Fix those. Leave the std::fs read and write exactly as they are for now - the storage team replaces them with an object-store client next sprint.”

The code string, whole: 1,649 characters, 66 lines ending in a newline. Save it as thumbs-paste.txt and run node build-check.js thumbs-paste.txt > thumbs-check.json:

# file: Cargo.toml
[package]
name = "thumb-worker"
version = "0.2.0"
edition = "2021"

[dependencies]
tokio = { version = "1.39", features = ["full"] }
anyhow = "1"
futures = "0.3"
tracing = "0.1"
---
// file: src/worker.rs
use std::path::PathBuf;
use std::time::Duration;

use anyhow::Result;
use tokio::sync::mpsc;

pub struct Job {
    pub id: u64,
    pub source: PathBuf,
}

pub struct Worker {
    jobs: mpsc::Receiver<Job>,
    done: mpsc::Sender<u64>,
    out_dir: PathBuf,
}

impl Worker {
    pub fn new(jobs: mpsc::Receiver<Job>, done: mpsc::Sender<u64>, out_dir: PathBuf) -> Self {
        Self { jobs, done, out_dir }
    }

    pub async fn run(mut self) -> Result<()> {
        while let Some(job) = self.jobs.recv().await {
            match self.process(&job).await {
                Ok(()) => {
                    self.done.send(job.id);
                }
                Err(e) => {
                    tracing::warn!("job {} failed: {e}; retrying in 5s", job.id);
                    tokio::time::sleep(Duration::from_secs(5));
                }
            }
        }
        Ok(())
    }

    async fn process(&self, job: &Job) -> Result<()> {
        let bytes = std::fs::read(&job.source)?;
        let thumb = futures::executor::block_on(render_thumbnail(bytes));
        let target = self.out_dir.join(format!("{}.png", job.id));
        std::fs::write(&target, thumb)?;
        Ok(())
    }
}

async fn render_thumbnail(bytes: Vec<u8>) -> Vec<u8> {
    tokio::task::spawn_blocking(move || shrink(&bytes)).await.unwrap_or_default()
}

fn shrink(bytes: &[u8]) -> Vec<u8> {
    bytes.iter().step_by(4).copied().collect()
}

Its facts, decoded, cut to files, flags and verdict_candidate (the full object also carries the runtime block — tokio 1.39, features ["full"], no #[tokio::main] in the paste — the 5 functions, the one spawn_blocking task, the 2 blocking std::fs calls and the 2 unawaited futures):

{
  "files": [
    {
      "path": "Cargo.toml",
      "kind": "cargo",
      "line": 2,
      "end_line": 11
    },
    {
      "path": "src/worker.rs",
      "kind": "rust",
      "line": 14,
      "end_line": 67
    }
  ],
  "flags": [
    {
      "uid": "F1",
      "id": "TK-07",
      "sev": "high",
      "file": "src/worker.rs",
      "line": 40,
      "text": "self.done.send(...) in Worker::run builds a future that is never awaited, so it never runs",
      "detail": "Rust futures are lazy: calling self.done.send on a bounded tokio channel only creates a future, and the statement drops it without polling it once - the message is never sent. The compiler only warns (unused_must_use). Add .await, or tokio::spawn it and keep the JoinHandle."
    },
    {
      "uid": "F2",
      "id": "TK-07",
      "sev": "high",
      "file": "src/worker.rs",
      "line": 44,
      "text": "tokio::time::sleep(...) in Worker::run builds a future that is never awaited, so it never runs",
      "detail": "Rust futures are lazy: calling tokio::time::sleep only creates a future, and the statement drops it without polling it once - there is no delay at all. The compiler only warns (unused_must_use). Add .await, or tokio::spawn it and keep the JoinHandle."
    },
    {
      "uid": "F3",
      "id": "TK-05",
      "sev": "high",
      "file": "src/worker.rs",
      "line": 53,
      "text": "futures::executor::block_on() is called inside Worker::process",
      "detail": "Starting or driving a runtime from code that already runs on one panics (\"Cannot start a runtime from within a runtime\"), and futures::executor::block_on parks the worker thread until the future finishes - which deadlocks if that future needs the same thread. Await the future directly, or tokio::spawn it and await the JoinHandle."
    },
    {
      "uid": "F4",
      "id": "TK-17",
      "sev": "low",
      "file": "src/worker.rs",
      "line": 51,
      "text": "async fn Worker::process() never awaits",
      "detail": "An async fn with no .await runs start to finish in one poll, without ever yielding: it adds a future and a state machine for nothing, and if it does slow work it blocks like sync code while every caller must still .await it. Await the async API it should be using; if nothing in it ever waits, a plain fn is simpler - but that changes how every caller calls it."
    },
    {
      "uid": "F5",
      "id": "TK-04",
      "sev": "low",
      "file": "src/worker.rs",
      "line": 52,
      "text": "Worker::process makes 2 blocking calls (std::fs::read(), std::fs::write()): blocking std::fs file I/O runs on a runtime worker thread",
      "detail": "std::fs reads and writes block the worker thread for as long as the disk takes - usually short, but on a network filesystem or a large file it stalls every task on that thread. Use tokio::fs (which runs the call on the blocking pool) or tokio::task::spawn_blocking for a batch of file work.",
      "lines": [
        52,
        55
      ]
    },
    {
      "uid": "F6",
      "id": "TK-18",
      "sev": "info",
      "file": "src/worker.rs",
      "line": 14,
      "text": "context: 1 Rust file, 3 async fn(s) of 5 fn(s), 3 .await(s), 1 spawned task(s), 0 select!, tokio 1.39 [full]",
      "detail": "Also read: Cargo.toml (cargo). Only what is pasted is checked: whether a function from another crate or module blocks or is async, and how the runtime is built and shut down outside the paste, is outside the check."
    }
  ],
  "verdict_candidate": "blocked"
}

The request body (Idempotency-Key: blocking-call-audit:fix:efc72c13-19a0:a1):

{
  "task": "fix",
  "code": "# file: Cargo.toml\n[package]\nname = \"thumb-worker\"\nversion = \"0.2.0\"\nedition = \"2021\"\n\n[dependencies]\ntokio = { version = \"1.39\", features = [\"full\"] }\nanyhow = \"1\"\nfutures = \"0.3\"\ntracing = \"0.1\"\n---\n// file: src/worker.rs\nuse std::path::PathBuf;\nuse std::time::Duration;\n\nuse anyhow::Result;\n...",
  "facts": "{\"runtime\":{\"edition\":\"2021\",\"tokio\":\"1.39\",\"tokio_features\":[\"full\"],\"main_flavor\":null,\"cargo_toml_pasted\":true,\"rust_files\":1,\"functions\":5,\"async_fns\":3,\"awaits\":3,\"spawned_tasks\":1},\"fil...",
  "note": "Jobs are never acknowledged, the retry delay does nothing and the worker sometimes hangs. Fix those. Leave the std::fs read and write exactly as they are for now - the storage team replaces them with an object-store client next sprint."
}

The reply (output.output), an excerpt: the verdict, the headline, the middle of the one returned file, the change that stops the hangs, the last verification step and the open item:

{
  "task": "fix",
  "verdict": "partial",
  "headline": "Awaited the dropped ack-send and retry-sleep futures and replaced the nested block_on with a direct await, so jobs are acknowledged, retries actually wait 5s, and process() can no longer park a runtime worker thread.",
  "files": [
    {
      "path": "src/worker.rs",
      "content": "use std::path::PathBuf;\n...\n    pub async fn run(mut self) -> Result<()> {\n        while let Some(job) = self.jobs.recv().await {\n            match self.process(&job).await {\n                Ok(()) => {\n                    if let Err(err) = self.done.send(job.id).await {\n                        tracing::warn!(\"job {} finished but the ack channel is closed: {err}\", job.id);\n                    }\n                }\n                Err(e) => {\n                    tracing::warn!(\"job {} failed: {e}; retrying in 5s\", job.id);\n                    tokio::time::sleep(Duration::from_secs(5)).await;\n                }\n            }\n        }\n        Ok(())\n    }\n\n    async fn process(&self, job: &Job) -> Result<()> {\n        let bytes = std::fs::read(&job.source)?;\n        let thumb = render_thumbnail(bytes).await;\n        let target = self.out_dir.join(format!(\"{}.png\", job.id));\n        std::fs::write(&target, thumb)?;\n        Ok(())\n    }\n}\n..."
    }
  ],
  "changes": [
    ...,
    {
      "file": "src/worker.rs",
      "function": "Worker::process",
      "flags": [
        "F3",
        "F4"
      ],
      "change": "Replace futures::executor::block_on(render_thumbnail(bytes)) with render_thumbnail(bytes).await, since process is already an async fn.",
      "why": "process no longer parks a Tokio worker thread to drive a second, unrelated executor - which could stall or deadlock the runtime, especially a current_thread one - and it now has a real await point."
    }
  ],
  "verify": [
    ...,
    "Add a #[tokio::test] with tokio::time::pause(): drive Worker::run over an in-memory job/done channel pair, assert the done receiver gets the job id right after a successful process(), then feed a failing job and assert no retry happens until tokio::time::advance(Duration::from_secs(5)) is called."
  ],
  "open": [
    {
      "item": "Worker::process still calls std::fs::read and std::fs::write directly, which run synchronously on the runtime worker thread",
      "flags": [
        "F5"
      ],
      "reason": "The note asks to leave these exactly as they are because the storage team is replacing them with an object-store client next sprint; wrapping them in tokio::task::spawn_blocking or switching to tokio::fs is the fix once that lands."
    }
  ],
  "questions": [
    ...
  ],
  "summary": ...
}

The other two changes, both in Worker::run, await self.done.send(job.id) and log when the ack channel is closed (F1), and add .await to the retry’s tokio::time::sleep (F2). The verdict is partial because the note asked to leave the std::fs calls as they are, so the low F5 is named in open rather than fixed; Cargo.toml 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 /rustkit.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, and the paste with every edit (or file) put back:

// recheck.js -- run: node recheck.js ingest-check.json ingest-reply.txt audit > ingest-edited.txt   (rustkit.js, recon.js next to it)
const fs = require("fs");
const Recon = require("./recon.js");                                            // loads ./rustkit.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 ingest audit reply it reports:

ok: The areas table rates the runtime, tasks, cancellation and resources once each.
ok: Every one of the 8 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 9 flags is answered once.
ok: All 6 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 function of the paste is still there after the edits.
ok: The reply repeats no credential from the paste.
0 disagreement(s) in 8 statements checked
still raised: TK-14 low src/forward.rs:85 the task spawned at line 38 runs loop { ... } with no way to stop it
still raised: TK-17 low src/forward.rs:108 async fn audit_log() never awaits
still raised: TK-04 low src/forward.rs:109 audit_log calls std::fs::OpenOptions::new(): blocking std::fs file I/O runs on a runtime worker thread

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

ok: 1 file returned whole.
ok: With the fixed files in place the free check raises no high or medium flag beyond the ones left open (1 low).
ok: Every one of the 5 functions is still there.
ok: Every one of the 3 changes names a returned file, a function in the fixed code and real page flags.
ok: The verdict (partial) fits the open list.
ok: The reply repeats no credential from the paste.
0 disagreement(s) in 6 statements checked
still raised: TK-04 low src/worker.rs:54 Worker::process makes 2 blocking calls (std::fs::read(), std::fs::write()): blocking std::fs file I/O runs on a runtime worker thread

The page does not compile or run the code. After that, run cargo build and cargo clippy (its await_holding_lock lint and the compiler’s unused_must_use warning catch two of these classes), plus the tests the reply’s plan or verify names, and watch a run under tokio-console or with tracing for tasks that stall or never finish, before you ship anything.

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":"blocking-call-audit"}' | 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 ingest example from ingest-check.json; step 6 reuses makeInput for the thumbs (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.

# ingest-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 "Readings stop arriving every 30 seconds and under load the whole service freezes. What do we fix before it takes the second substation's feed? Changing the signatures in forward.rs is fine - main is their only caller." \
  '{task: "audit", code, facts: (.facts | tojson), note: $note}' ingest-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 blocking-call-audit:<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 blocking-call-audit:audit:4b43fe9a-2996:a1 (ingest audit) and blocking-call-audit:fix:efc72c13-19a0:a1 (thumbs fix); the fix run of the ingest audit handed on (below) is blocking-call-audit:fix:a17c90ef-319c: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="blocking-call-audit: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 thumbs 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 thumbs-check.json: makeInput then carries its audit_handoff.

# the fix worked example: thumbs-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 "Jobs are never acknowledged, the retry delay does nothing and the worker sometimes hangs. Fix those. Leave the std::fs read and write exactly as they are for now - the storage team replaces them with an object-store client next sprint." \
  '{task: "fix", code, facts: (.facts | tojson), note: $note} + (if .audit_handoff then {audit_handoff} else {} end)' \
  thumbs-check.json)
FKEY="blocking-call-audit: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 Blocking Call Audit in the browser: the check, the flags, the blocking-call, unawaited-future, task, lock-guard, channel and select! inventories, putting an edit or the fixed files back into the paste, and the Markdown and CSV exports are free.