Drive Annalward from your own code
The page reads pasted Temporal Python (temporalio) in the browser for free — one or
more files joined into one paste, each introduced by a # file: <path> line, plus
pyproject.toml for the temporalio and Python versions — with its line
numbers. It lexes strings and comments out first, rebuilds every block from indentation, inventories every
@workflow.defn class and its run, signal, query and update handlers, every
@activity.defn function and whether it heartbeats, every activity call made from workflow code
with its timeouts and retry policy, every compensation list and the order it is replayed in, and every
Worker and what it registers, and runs 21 deterministic checks drawn from durable-execution
practice (catalogue ids TW-01..TW-21, plus TW-22 for context): the system clock, randomness, network, file
or database I/O, time.sleep, threads and executors in workflow code, module state changed from
a workflow, the stdlib logger in workflow code, an activity call with no timeout, with a numeric timeout or
with more than one positional argument, retries with no bound, a heartbeat_timeout nothing
heartbeats for, a blocking call in an async def activity, a long activity loop with no
heartbeat, compensations replayed in the order they were added or registered after their step, a swallowed
activity failure or cancellation, a workflow loop with no continue-as-new, a query that changes state, a
workflow or activity no Worker registers, and sync activities on a Worker with no
activity_executor. Nothing is imported or run, no Temporal server is contacted, and it is not
the replayer: 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: ...”).
| HTTP | error.code | What it means / what to do |
|---|---|---|
400 | VALIDATION_ERROR | The 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. |
401 | UNAUTHORIZED | No 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. |
402 | INSUFFICIENT_CREDITS | The 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. |
404 | NOT_FOUND | A wrong path, or a job id that does not belong to this token. |
409 | CONFLICT | An 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). |
429 | RATE_LIMITED | Too 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:
| What | What happens |
|---|---|
event: error on /run-stream | The 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 s | The 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 Tempkit.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.
task | What it does | verdict | Use it when |
|---|---|---|---|
"audit" | Reviews the code the way a senior engineer reviews Temporal workflows before the first execution starts: says what the workflows orchestrate and which activities touch the world, rates the four areas (determinism, activities, compensation, operations) as sound, fragile or broken, confirms or dismisses every page flag, adds what text rules cannot see (a side effect that is not idempotent under retries, a permanent failure that is retried, a workflow that reports success after compensating), and gives the verbatim evidence and exact before/after edits for each finding, listed in the order to apply them — what cannot run or replay first, then lost failures, compensations and cancellations, then history and registration, then hygiene — with a plan that ends in a replay test (temporalio.worker.Replayer) and the failure paths run in WorkflowEnvironment.start_time_skipping(). | blocked | needs_work | ready | Before shipping: will it replay, will the SDK accept every activity call, are retries bounded and side effects idempotent, what does a failure or a cancel leave behind, and what should change first. |
"fix" | Writes the fixed version of the pasted code: the whole of every Python file it changes (and at most one new file), with workflow code made deterministic (workflow.now(), workflow.random(), await asyncio.sleep(...), I/O moved into an activity), every activity call given a timedelta timeout, args=[...] and a bounded retry policy, compensations registered before their step and run in reverse, failures and cancellation re-raised and the Worker able to start — plus a changes list tied to the page flags, the verify steps (a replay with temporalio.worker.Replayer and the failure test to write), and what it left open. No public name or signature changes, no def turned into async def or back, and no signal, query, update or task queue renamed, unless the note asks for it. | fixed | partial | When you want the code changed, not a list of findings: a worker 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 Python (“Nothing was pasted.”, “No Python file was found. Paste .py source, with a # file: <path>.py line above each file.”, or “This does not read as Python source: no def, class, import or call was found.”), and it warns before a run on a paste with no Temporal code (“No Temporal workflow or activity was found: there may be little here to audit.”, or “... to fix.”) or with no workflow class (“No @workflow.defn class was found; the run will see activities and the worker only.”).
Input fields
This is exactly what the page sends (tempkit.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:
| Field | Type | Required | Meaning |
|---|---|---|---|
task | string | yes | "audit" or "fix" (the lanes above). |
code | string | yes | The pasted Python, 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 # [annalward: not sent: ...] line at the end (below). |
facts | string | yes | The page’s free check, Tempkit.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_handoff | string | no | fix lane only: a JSON string built from an audit reply on this code (recon.js fixHandoff, below). buildInput drops it for the audit lane. |
note | string | no | What the engineer says: the goal, a constraint (“no new dependencies”, “these workflows are already running in production”), 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_note | string | no | Sent 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 package folder) are joined that way by
Tempkit.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, workflows.py. The page normalises line endings to \n. A
.py (or .pyi) path is checked as Python; pyproject.toml,
requirements*.txt, setup.cfg, Pipfile and
poetry.lock are deps (they give facts.service.temporalio and
facts.service.python_version, the version specs as declared); anything else is
other. Workflow code is the methods of @workflow.defn classes; helpers they
call in modules that are not pasted are outside the check. Python 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 non-Python file 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:
# [annalward: not sent: orders/worker.py, pyproject.toml]
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: Tempkit.analyze splits the paste into files, lexes them, rebuilds their
blocks, inventories them and runs the rules, and Tempkit.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, workflows and handlers must survive. Its top-level keys, in the order built:
| Key | Contents |
|---|---|
service | python_version and temporalio (the version specs a pasted pyproject.toml declares, or null), and counts: python_files, workflows, activities, activity_calls, workers. |
files | Every file: path (use it verbatim), kind (python, deps, other), line and end_line (the paste lines of the file’s content, without its # file: line). |
functions | Every def (up to 200): name (Class.method for methods), file, line, async, and role (run, signal, query, update, validator, activity) when it has one. |
workflows | Every @workflow.defn class (up to 40): name, file, line, run, signals, queries, updates, sandboxed. |
activities | Every @activity.defn function (up to 120): name, activity_name, file, line, async, heartbeats. |
activity_calls | Every workflow.execute_activity / start_activity (and the _method and local variants; up to 160): file, line, in, kind, activity, start_to_close, schedule_to_close, heartbeat (the text given, or null), retry_policy, max_attempts, awaited. |
workers | Every Worker(...) (up to 20): file, line, task_queue, workflows, activities (null when not a literal list), activity_executor. |
compensations | Every compensation list in workflow code (up to 40): file, line, workflow, list, added, order (reverse, forward, never, unknown), replayed_at. |
workflow_excepts | Every except in workflow code (up to 60): file, line, type, raises, in. |
flags | Every flag the page raised: uid (F1, F2, ... in severity, file, then line order), id (TW-01..TW-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_candidate | The audit verdict the flags point at (the page’s own reading): blocked with any high flag, needs_work with any medium, otherwise ready. |
notes | Plain sentences about what was not read or not sent (empty for a paste that fits). |
The simplest source is tempkit.js itself: the page loads it from
/tempkit.js (as window.Tempkit), 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
renewals-paste.txt and redirect the output into renewals-check.json:
// build-check.js -- run: node build-check.js renewals-paste.txt > renewals-check.json (tempkit.js saved next to it)
const fs = require("fs");
const Tempkit = require("./tempkit.js");
const A = Tempkit.analyze(fs.readFileSync(process.argv[2], "utf8"));
if (!A.ok) { console.error(A.errors.join("\n")); process.exit(1); }
const inp = Tempkit.mustBeObject(Tempkit.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.length + " file(s), " + A.workflows.length + " workflow(s), " + A.activities.length + " activity function(s), " +
A.activity_calls.length + " activity call(s), " + A.workers.length + " Worker(s); " +
real.length + " flags (" + A.counts.high + " high, " + A.counts.medium + " medium, " + A.counts.low + " low); verdict_candidate " + A.candidate);
For the renewals example it reports 4 file(s), 1 workflow(s), 2 activity function(s), 2 activity call(s), 1 Worker(s); 4 flags (0 high, 3 medium, 1 low); verdict_candidate needs_work; for the payouts example,
4 file(s), 1 workflow(s), 2 activity function(s), 2 activity call(s), 1 Worker(s); 4 flags (4 high, 0 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, and with no functions and workflows nothing
says what must survive a fix. Build it with tempkit.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, Tempkit); // 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
Tempkit.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 renewals-paste.txt renewals-reply.txt > renewals-fix-check.json (tempkit.js, recon.js next to it)
const fs = require("fs");
const Tempkit = require("./tempkit.js");
const Recon = require("./recon.js");
const A = Tempkit.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 = Tempkit.analyze(ho.text); // facts recomputed from the edited paste
const inp = Tempkit.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 renewals-reply.txt; for the renewals
audit below it reports 8 edit(s) applied, 0 did not apply, the edited paste raises no flag beyond the TW-22
context line, and the page sends that fix run with the audit’s note and the key
annalward:fix:6bf3d7d4-2d57: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 client call that starts the workflow, a schedule,
a search attribute, a namespace setting, a worker option or a running execution that is not in the paste
is never claimed to exist — it goes into questions; whether an imported helper is
deterministic, or an external API is idempotent, 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 does for its callers: same workflow and activity classes and functions, names,
parameters and return values on the paths that worked, same signal, query and update names, same
sync-or-async nature of every function, same task queues, unless the note asks for it. An edit that adds,
removes or reorders activity calls or timers breaks executions already in flight, so when the paste gives
no sign the workflow is not yet deployed the reply says it needs workflow.patched(...) (or a
new workflow type, or draining) and asks whether executions are running — and it never writes
workflow.patched into an edit unless the note says they are. 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:"audit".verdict:blocked(any finding is high) |needs_work(the worst is medium) |ready(nothing above low; low findings and questions do not stop a release).headline: one sentence.system: one or two sentences — what the workflows orchestrate and which activities touch the world.areas[]: exactly four entries, in the order determinism, activities, compensation, operations:{area, status, note}withstatussound|fragile|broken(brokenwhen a high finding sits in that area,fragilefor a medium one). Determinism covers time, randomness, I/O, threads, sleep, global state and logging in workflow code; activities covers timeouts, arguments, retry policies, idempotency, heartbeats and blocking in activities; compensation covers saga registration and order, and failures and cancellation handled and re-raised; operations covers history growth and continue-as-new, queries, signals and updates, worker registration and executors, and versioning of running executions.findings[]:{id, flags[], file, line, severity, area, title, evidence, why, edits[]}.idis a catalogue id (TW-01..TW-21) orother;flagslists the pageuids it confirms ([]for something the page did not flag);fileis apathfromfacts.files, copied exactly;lineis the line ofcodewith the offending text, inside that file’sline..end_line;severityishigh(the workflow cannot run or cannot replay: non-determinism in workflow code, an activity call the SDK rejects, a Worker that cannot start) |medium(failures, compensations, cancellations or history handled wrongly: a blocking call in an async activity, a heartbeat timeout nothing heartbeats for, compensations in forward order or registered after their step, a swallowed failure or cancellation, an unbounded loop with no continue-as-new, a query that changes state, module state changed from workflow code, a workflow or activity no Worker registers, a non-idempotent activity that retries) |low(bounded risk or hygiene: retries with no bound, a permanent failure retried by a bounded policy, a long activity that never heartbeats, the stdlib logger in workflow code);areaisdeterminism|activities|compensation|operations;evidenceis one to three whole lines copied verbatim fromcodeas pasted.edits[]:{before, after}on that one file. Eachbeforeis a contiguous, exact slice of the file — whole lines with their indentation, as pasted — that occurs exactly once in the file as it stands when the edit is applied;afteris its replacement, indented with four spaces. Findings are applied in the order listed and their edits in order, each on top of the ones before, so a later finding that touches the same lines writes itsbeforeas the earlier one left it. A new import goes into the edit of the first finding that needs it. When I/O leaves a workflow, the change adds an activity in the pasted activities file, calls it with a timeout and registers it on the pasted Worker — one finding per file, in that order, each carrying the same flag and id.editsmay be[]only for a low finding or a change outside the paste.flag_answers[]:{flag, status, reason}, exactly one per non-info page flag,statusconfirmed(a finding lists that uid) |dismissed(with the reason).plan[]: the findings in the order to apply them, ending with a replay test: record a history, replay it withtemporalio.worker.Replayerafter the change, and run the failure paths intemporalio.testing.WorkflowEnvironment.start_time_skipping().questions[](only what the paste cannot answer) andsummary(two to four sentences, answeringnoteif one was sent).
task: "fix"
task:"fix".verdict:fixed(openis empty) |partial.headline: one sentence.files[]:{path, content}— every pasted Python file it changes, whole (every line, without the# file:line, indented with four spaces), under itspathfromfacts.files; at most one new file next to them. Only the standard library and what the paste already imports or its pyproject declares.changes[]:{file, function, flags[], change, why}, one per change.functionis the function or method it is in, asfacts.functionsnames it (Class.method), or(module);flagsare the page flags the change closes. Every high and medium flag appears in theflagsof a change or of anopenitem.verify[]: always a replay of a recorded history withtemporalio.worker.Replayeragainst the fixed workflow code, plus the test that would have caught the worst problem (what it runs inWorkflowEnvironment.start_time_skipping(), what it makes fail, what it asserts).open[]:{item, flags[], reason}for what it left unfixed — a decision for the engineer, or code outside the paste.questions[]andsummary, as in the audit lane; with anaudit_handoff, it fixes what the audit left.
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 TW7 or tw 7
become TW-07; flags keeps only F<n> values; findings
with no file, evidence and title are dropped; system may also arrive as
service, 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, system,
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 renewals and
payouts), built exactly as the page builds them:
Tempkit.buildInput(lane, Tempkit.analyze(text), note, null), with the saved 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 billing and ledger hosts are invented
.test names.
Audit: a subscription workflow that loops for the account’s lifetime, heartbeats nothing its heartbeat timeout waits for and counts reads in a query
Four files: pyproject.toml (requires-python = ">=3.10",
temporalio>=1.6), renewals/activities.py, renewals/workflows.py
and renewals/worker.py. SubscriptionWorkflow.run waits 30 days per cycle on
workflow.wait_condition (catching asyncio.TimeoutError), charges the renewal and exports usage, in a
while not self.cancelled: loop that never continues as new; the export_usage
call sets heartbeat_timeout, but the activity pages through usage without ever calling
activity.heartbeat; the status query increments a counter; and
run logs through the stdlib logger. The page’s free check raises 4 flags (0 high,
3 medium, 1 low) (plus the TW-22 context flag); its own reading of the verdict is
needs_work. The engineer’s note: “Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?”
The code string, whole: 3,600 characters, 118 lines ending in a newline. Save it as
renewals-paste.txt and run
node build-check.js renewals-paste.txt > renewals-check.json:
# file: pyproject.toml
[project]
name = "subscription-renewals"
version = "2.1.0"
requires-python = ">=3.10"
dependencies = ["temporalio>=1.6", "httpx>=0.27"]
---
# file: renewals/activities.py
import httpx
from temporalio import activity
BILLING_API = "https://billing.example.test/v3"
@activity.defn
async def charge_renewal(account_id: str, plan: str) -> str:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(f"{BILLING_API}/renewals", json={"account": account_id, "plan": plan}, headers={"Idempotency-Key": f"renew-{account_id}"})
resp.raise_for_status()
return resp.json()["invoice_id"]
@activity.defn
async def export_usage(account_id: str) -> int:
rows = 0
async with httpx.AsyncClient(timeout=30) as client:
page = 1
while True:
resp = await client.get(f"{BILLING_API}/usage", params={"account": account_id, "page": page})
resp.raise_for_status()
batch = resp.json()["rows"]
if not batch:
return rows
rows += len(batch)
page += 1
---
# file: renewals/workflows.py
import asyncio
import logging
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
with workflow.unsafe.imports_passed_through():
from renewals.activities import charge_renewal, export_usage
log = logging.getLogger(__name__)
BILLING_RETRY = RetryPolicy(initial_interval=timedelta(seconds=5), maximum_attempts=5)
@workflow.defn
class SubscriptionWorkflow:
def __init__(self) -> None:
self.plan = "basic"
self.cancelled = False
self.renewals = 0
self.status_reads = 0
@workflow.run
async def run(self, account_id: str, plan: str) -> int:
self.plan = plan
while not self.cancelled:
try:
await workflow.wait_condition(lambda: self.cancelled, timeout=timedelta(days=30))
except asyncio.TimeoutError:
pass
if self.cancelled:
break
invoice = await workflow.execute_activity(
charge_renewal,
args=[account_id, self.plan],
start_to_close_timeout=timedelta(minutes=2),
retry_policy=BILLING_RETRY,
)
self.renewals += 1
log.info("renewed %s: invoice %s", account_id, invoice)
await workflow.execute_activity(
export_usage,
account_id,
start_to_close_timeout=timedelta(hours=1),
heartbeat_timeout=timedelta(minutes=2),
retry_policy=BILLING_RETRY,
)
return self.renewals
@workflow.signal
def change_plan(self, plan: str) -> None:
self.plan = plan
@workflow.signal
def cancel(self) -> None:
self.cancelled = True
@workflow.query
def status(self) -> dict:
self.status_reads += 1
return {"plan": self.plan, "renewals": self.renewals, "cancelled": self.cancelled}
---
# file: renewals/worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from renewals.activities import charge_renewal, export_usage
from renewals.workflows import SubscriptionWorkflow
async def main() -> None:
client = await Client.connect("localhost:7233")
worker = Worker(client, task_queue="renewals", workflows=[SubscriptionWorkflow], activities=[charge_renewal, export_usage])
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
Its facts, decoded (the request carries this object as one compact JSON string):
{
"service": {
"python_version": ">=3.10",
"temporalio": ">=1.6",
"python_files": 3,
"workflows": 1,
"activities": 2,
"activity_calls": 2,
"workers": 1
},
"files": [
{
"path": "pyproject.toml",
"kind": "deps",
"line": 2,
"end_line": 6
},
{
"path": "renewals/activities.py",
"kind": "python",
"line": 9,
"end_line": 35
},
{
"path": "renewals/workflows.py",
"kind": "python",
"line": 38,
"end_line": 99
},
{
"path": "renewals/worker.py",
"kind": "python",
"line": 102,
"end_line": 118
}
],
"functions": [
{
"name": "charge_renewal",
"file": "renewals/activities.py",
"line": 16,
"async": true,
"role": "activity"
},
{
"name": "export_usage",
"file": "renewals/activities.py",
"line": 24,
"async": true,
"role": "activity"
},
{
"name": "SubscriptionWorkflow.__init__",
"file": "renewals/workflows.py",
"line": 55,
"async": false
},
{
"name": "SubscriptionWorkflow.run",
"file": "renewals/workflows.py",
"line": 62,
"async": true,
"role": "run"
},
{
"name": "SubscriptionWorkflow.change_plan",
"file": "renewals/workflows.py",
"line": 89,
"async": false,
"role": "signal"
},
{
"name": "SubscriptionWorkflow.cancel",
"file": "renewals/workflows.py",
"line": 93,
"async": false,
"role": "signal"
},
{
"name": "SubscriptionWorkflow.status",
"file": "renewals/workflows.py",
"line": 97,
"async": false,
"role": "query"
},
{
"name": "main",
"file": "renewals/worker.py",
"line": 111,
"async": true
}
],
"workflows": [
{
"name": "SubscriptionWorkflow",
"file": "renewals/workflows.py",
"line": 54,
"run": "run",
"signals": [
"change_plan",
"cancel"
],
"queries": [
"status"
],
"updates": [],
"sandboxed": true
}
],
"activities": [
{
"name": "charge_renewal",
"activity_name": "charge_renewal",
"file": "renewals/activities.py",
"line": 16,
"async": true,
"heartbeats": false
},
{
"name": "export_usage",
"activity_name": "export_usage",
"file": "renewals/activities.py",
"line": 24,
"async": true,
"heartbeats": false
}
],
"activity_calls": [
{
"file": "renewals/workflows.py",
"line": 71,
"in": "SubscriptionWorkflow.run",
"kind": "execute_activity",
"activity": "charge_renewal",
"start_to_close": "timedelta(minutes=2)",
"schedule_to_close": null,
"heartbeat": null,
"retry_policy": true,
"max_attempts": 5,
"awaited": true
},
{
"file": "renewals/workflows.py",
"line": 79,
"in": "SubscriptionWorkflow.run",
"kind": "execute_activity",
"activity": "export_usage",
"start_to_close": "timedelta(hours=1)",
"schedule_to_close": null,
"heartbeat": "timedelta(minutes=2)",
"retry_policy": true,
"max_attempts": 5,
"awaited": true
}
],
"workers": [
{
"file": "renewals/worker.py",
"line": 113,
"task_queue": "renewals",
"workflows": [
"SubscriptionWorkflow"
],
"activities": [
"charge_renewal",
"export_usage"
],
"activity_executor": false
}
],
"compensations": [],
"workflow_excepts": [
{
"file": "renewals/workflows.py",
"line": 67,
"type": "asyncio.TimeoutError",
"raises": false,
"in": "SubscriptionWorkflow.run"
}
],
"flags": [
{
"uid": "F1",
"id": "TW-18",
"sev": "medium",
"file": "renewals/workflows.py",
"line": 64,
"text": "while not self.cancelled: in SubscriptionWorkflow.run loops for the workflow's whole life and never continues as new",
"detail": "Every activity, timer and signal in the loop appends events to one history; after enough iterations the history passes Temporal's limits (about 51,200 events or 50 MB) and the workflow is terminated, and long before that every replay gets slower. Call workflow.continue_as_new(state) when workflow.info().is_continue_as_new_suggested() is true, or after a fixed number of iterations."
},
{
"uid": "F2",
"id": "TW-12",
"sev": "medium",
"file": "renewals/workflows.py",
"line": 79,
"text": "workflow.execute_activity(export_usage) sets heartbeat_timeout, but export_usage() never calls activity.heartbeat()",
"detail": "With a heartbeat_timeout the server expects a heartbeat at least that often; an activity that never heartbeats is timed out as soon as one attempt runs longer than the heartbeat timeout, and every retry fails the same way. Call activity.heartbeat(progress) inside its loop, or drop heartbeat_timeout for a short activity."
},
{
"uid": "F3",
"id": "TW-19",
"sev": "medium",
"file": "renewals/workflows.py",
"line": 98,
"text": "query SubscriptionWorkflow.status() changes workflow state",
"detail": "A query is answered by replaying the workflow and must be read-only: a change it makes is not recorded in history, is lost on the next replay, and differs between workers, so the workflow's state depends on who asked and when. Keep the query a pure read; change state in a signal or an update handler."
},
{
"uid": "F4",
"id": "TW-07",
"sev": "low",
"file": "renewals/workflows.py",
"line": 78,
"text": "workflow SubscriptionWorkflow logs with the stdlib logger or print in workflow code (log.info())",
"detail": "Workflow code is replayed, so a stdlib logger or print writes every line again on every replay and after every worker restart. workflow.logger is replay-aware: it skips records during replay and tags each record with the workflow id and run id."
},
{
"uid": "F5",
"id": "TW-22",
"sev": "info",
"file": "renewals/activities.py",
"line": 9,
"text": "context: 3 Python files, 1 workflow, 2 activities (0 sync), 2 activity call(s), 0 compensation list(s), 1 Worker(s), temporalio >=1.6, Python >=3.10",
"detail": "Also read: pyproject.toml (deps). Only what is pasted is checked: workflow code is the methods of @workflow.defn classes (helpers they call in other modules are outside the check), and whether an activity is idempotent, and how a change replays against running workflows, is for the audit and for the replayer."
}
],
"verdict_candidate": "needs_work",
"notes": []
}
The request body (POST /estimate, then POST /run or
/run-stream with Idempotency-Key: annalward:audit:ede4d6c7-25e5:a1 as the page sends it):
{
"task": "audit",
"code": "# file: pyproject.toml\n[project]\nname = \"subscription-renewals\"\nversion = \"2.1.0\"\nrequires-python = \">=3.10\"\ndependencies = [\"temporalio>=1.6\", \"httpx>=0.27\"]\n---\n# file: renewals/activities.py\nimport httpx\nfrom temporalio import activity\n\nBILLING_API = \"https://billing.exampl...",
"facts": "{\"service\":{\"python_version\":\">=3.10\",\"temporalio\":\">=1.6\",\"python_files\":3,\"workflows\":1,\"activities\":2,\"activity_calls\":2,\"workers\":1},\"files\":[{\"path\":\"pyproject.toml\",\"...",
"note": "Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?"
}
The reply (output.output), an excerpt: the verdict, the headline, the system, each
area’s status, and the finding that answers “anything that will bite us in a year?” most
directly — the loop that never continues as new — with its edit, its flag answer, the
last step of the plan and the first question:
{
"task": "audit",
"verdict": "needs_work",
"headline": "SubscriptionWorkflow's renewal charge reuses the same Idempotency-Key forever and its loop never calls continue_as_new, so long-lived accounts risk silent duplicate-charge dedup and unbounded history growth.",
"system": "SubscriptionWorkflow runs one long-lived workflow per account that periodically calls charge_renewal to bill a renewal and export_usage to pull usage rows; both activities call the billing API over HTTP with httpx.",
"areas": [
{
"area": "determinism",
"status": "sound"
},
{
"area": "activities",
"status": "fragile"
},
{
"area": "compensation",
"status": "sound"
},
{
"area": "operations",
"status": "fragile"
}
],
"findings": [
...,
{
"id": "TW-18",
"flags": [
"F1"
],
"file": "renewals/workflows.py",
"line": 64,
"severity": "medium",
"area": "operations",
"title": "Renewal loop never continues-as-new",
"evidence": "async def run(self, account_id: str, plan: str) -> int:\nself.plan = plan\nwhile not self.cancelled:",
"why": "Each iteration appends a timer and two activity results to this one run's history for as long as the account keeps renewing, which the note says is the account's whole life. Left unbounded the history approaches Temporal's ~51,200-event/50MB limit, so call workflow.continue_as_new when workflow.info().is_continue_as_new_suggested() and carry plan and renewals forward.",
"edits": [
{
"before": " @workflow.run\n async def run(self, account_id: str, plan: str) -> int:\n self.plan = plan\n while not self.cancelled:\n try:\n await workflow.wait_condition(lambda: self.cancelled, timeout=timedelta(days=30))\n except asyncio.TimeoutError:\n pass\n if self.cancelled:\n break\n invoice = await workflow.execute_activity(\n charge_renewal,\n args=[account_id, self.plan, self.renewals],\n start_to_close_timeout=timedelta(minutes=2),\n retry_policy=BILLING_RETRY,\n )\n self.renewals += 1\n log.info(\"renewed %s: invoice %s\", account_id, invoice)\n await workflow.execute_activity(\n export_usage,\n account_id,\n start_to_close_timeout=timedelta(hours=1),\n heartbeat_timeout=timedelta(minutes=2),\n retry_policy=BILLING_RETRY,\n )\n return self.renewals",
"after": " @workflow.run\n async def run(self, account_id: str, plan: str, renewals: int = 0) -> int:\n self.plan = plan\n self.renewals = renewals\n while not self.cancelled:\n try:\n await workflow.wait_condition(lambda: self.cancelled, timeout=timedelta(days=30))\n except asyncio.TimeoutError:\n pass\n if self.cancelled:\n break\n invoice = await workflow.execute_activity(\n charge_renewal,\n args=[account_id, self.plan, self.renewals],\n start_to_close_timeout=timedelta(minutes=2),\n retry_policy=BILLING_RETRY,\n )\n self.renewals += 1\n log.info(\"renewed %s: invoice %s\", account_id, invoice)\n await workflow.execute_activity(\n export_usage,\n account_id,\n start_to_close_timeout=timedelta(hours=1),\n heartbeat_timeout=timedelta(minutes=2),\n retry_policy=BILLING_RETRY,\n )\n if workflow.info().is_continue_as_new_suggested():\n workflow.continue_as_new(args=[account_id, self.plan, self.renewals])\n return self.renewals"
}
]
},
...
],
"flag_answers": [
{
"flag": "F1",
"status": "confirmed",
"reason": "run() loops for the account's whole life with no continue_as_new; fixed by calling it when is_continue_as_new_suggested() is true."
},
...
],
"plan": [
...,
"Record a history of SubscriptionWorkflow and replay it with temporalio.worker.Replayer after these changes, and exercise the renewal/cancel/continue-as-new paths with temporalio.testing.WorkflowEnvironment.start_time_skipping()."
],
"questions": [
"Are any SubscriptionWorkflow executions already running in production? The continue_as_new change adds a new decision point mid-loop, which needs workflow.patched(...), a new workflow type, or draining in-flight executions to replay safely if so - we did not add patched since the note does not confirm executions are running.",
...
],
"summary": ...
}
The reply has 7 findings carrying 8 edits and confirms all 4 flags. Two findings the page did not flag
(other, medium, flags: []) come first: charge_renewal builds its
Idempotency-Key from account_id alone, so every renewal after the first reuses
the first renewal’s key; the fix gives the activity a cycle parameter
(renewals/activities.py) and passes self.renewals as it
(renewals/workflows.py). Then activity.heartbeat(rows) after each page of
export_usage (F2), continue_as_new when
is_continue_as_new_suggested(), carrying renewals forward (F1), the counter
removed from the status query (F3), and workflow.logger in place of the stdlib
logger (F4). A third unflagged finding (low, two edits) closes the list: a permanent 4xx from billing is
retried like a transient failure, so charge_renewal raises
ApplicationError(..., non_retryable=True) for it. The TW-18 edit’s before
already carries the self.renewals argument the second finding wrote, as the in-order contract
requires. Because continue-as-new adds a decision mid-loop, the reply asks whether executions are
already running rather than writing workflow.patched into an edit, and asks whether accounts
renewed more than once have already been under-billed.
Fix: a payout batch whose worker cannot start, whose timeout is a number, whose activity call passes three positional arguments and whose workflow calls time.sleep
Four files: pyproject.toml (requires-python = ">=3.11",
temporalio>=1.7), payouts/activities.py with two plain def
activities on requests, payouts/workflows.py and
payouts/worker.py. PayoutBatchWorkflow.run gives
fetch_balance start_to_close_timeout=30, passes send_payout three
positional arguments and pauses with time.sleep(1); the Worker registers the two sync
activities with no activity_executor. The free check raises 4 flags (4 high, 0 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: “The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.”
The code string, whole: 2,453 characters, 86 lines ending in a newline. Save it as
payouts-paste.txt and run
node build-check.js payouts-paste.txt > payouts-check.json:
# file: pyproject.toml
[project]
name = "creator-payouts"
version = "0.9.1"
requires-python = ">=3.11"
dependencies = ["temporalio>=1.7", "requests>=2.31"]
---
# file: payouts/activities.py
import requests
from temporalio import activity
LEDGER_API = "https://ledger.example.test/v1"
@activity.defn
def fetch_balance(creator_id: str) -> int:
resp = requests.get(f"{LEDGER_API}/balances/{creator_id}", timeout=10)
resp.raise_for_status()
return resp.json()["cents"]
@activity.defn
def send_payout(creator_id: str, cents: int, batch_id: str) -> str:
resp = requests.post(
f"{LEDGER_API}/payouts",
json={"creator": creator_id, "cents": cents},
headers={"Idempotency-Key": f"{batch_id}-{creator_id}"},
timeout=20,
)
resp.raise_for_status()
return resp.json()["payout_id"]
---
# file: payouts/workflows.py
import time
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
with workflow.unsafe.imports_passed_through():
from payouts.activities import fetch_balance, send_payout
MIN_PAYOUT_CENTS = 1000
@workflow.defn
class PayoutBatchWorkflow:
@workflow.run
async def run(self, batch_id: str, creator_ids: list[str]) -> list[str]:
paid = []
for creator_id in creator_ids:
cents = await workflow.execute_activity(
fetch_balance, creator_id, start_to_close_timeout=30, retry_policy=RetryPolicy(maximum_attempts=3)
)
if cents < MIN_PAYOUT_CENTS:
continue
payout_id = await workflow.execute_activity(
send_payout,
creator_id,
cents,
batch_id,
start_to_close_timeout=timedelta(minutes=1),
retry_policy=RetryPolicy(maximum_attempts=5),
)
paid.append(payout_id)
time.sleep(1)
return paid
---
# file: payouts/worker.py
import asyncio
from temporalio.client import Client
from temporalio.worker import Worker
from payouts.activities import fetch_balance, send_payout
from payouts.workflows import PayoutBatchWorkflow
async def main() -> None:
client = await Client.connect("localhost:7233")
worker = Worker(client, task_queue="payouts", workflows=[PayoutBatchWorkflow], activities=[fetch_balance, send_payout])
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
Its facts, decoded, cut to files, workers, flags and
verdict_candidate (the full object also has the service counts, the 4
functions with their roles, the one workflow, the 2 sync activities, the 2 activity calls with their
timeouts and retry bounds, and empty compensations and workflow_excepts):
{
"files": [
{
"path": "pyproject.toml",
"kind": "deps",
"line": 2,
"end_line": 6
},
{
"path": "payouts/activities.py",
"kind": "python",
"line": 9,
"end_line": 31
},
{
"path": "payouts/workflows.py",
"kind": "python",
"line": 34,
"end_line": 67
},
{
"path": "payouts/worker.py",
"kind": "python",
"line": 70,
"end_line": 86
}
],
"workers": [
{
"file": "payouts/worker.py",
"line": 81,
"task_queue": "payouts",
"workflows": [
"PayoutBatchWorkflow"
],
"activities": [
"fetch_balance",
"send_payout"
],
"activity_executor": false
}
],
"flags": [
{
"uid": "F1",
"id": "TW-09",
"sev": "high",
"file": "payouts/workflows.py",
"line": 52,
"text": "workflow.execute_activity(fetch_balance) in PayoutBatchWorkflow.run gives start_to_close_timeout as a number",
"detail": "The SDK takes a datetime.timedelta for every activity timeout; a bare number does not convert, the call raises when the activity is scheduled, and the workflow task fails on every retry. Write timedelta(seconds=30) - and say the unit, since seconds and minutes are easy to confuse."
},
{
"uid": "F2",
"id": "TW-10",
"sev": "high",
"file": "payouts/workflows.py",
"line": 57,
"text": "workflow.execute_activity(send_payout, ...) in PayoutBatchWorkflow.run passes 3 positional arguments to the activity",
"detail": "execute_activity takes the activity, then at most one positional argument; everything after it is keyword-only, so extra positional arguments raise TypeError when the workflow reaches this line. Pass several arguments as args=[a, b, c] (or one dataclass), matching the activity's parameters in order."
},
{
"uid": "F3",
"id": "TW-04",
"sev": "high",
"file": "payouts/workflows.py",
"line": 66,
"text": "PayoutBatchWorkflow.run calls time.sleep in workflow code (time.sleep())",
"detail": "time.sleep blocks the workflow's event loop and the worker thread instead of creating a durable timer: nothing is recorded in history, the worker cannot run other workflow tasks, and the deadlock detector fails the task after two seconds. Use await asyncio.sleep(...), which Temporal turns into a durable timer."
},
{
"uid": "F4",
"id": "TW-21",
"sev": "high",
"file": "payouts/worker.py",
"line": 81,
"text": "the Worker registers the sync activities fetch_balance, send_payout but has no activity_executor",
"detail": "A plain def activity runs in a thread or process pool that the Worker must be given; without activity_executor=... the Worker refuses to start, so no workflow or activity on this task queue runs. Pass activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=...) - or make the activities async def with async clients."
},
{
"uid": "F5",
"id": "TW-22",
"sev": "info",
"file": "payouts/activities.py",
"line": 9,
"text": "context: 3 Python files, 1 workflow, 2 activities (2 sync), 2 activity call(s), 0 compensation list(s), 1 Worker(s), temporalio >=1.7, Python >=3.11",
"detail": "Also read: pyproject.toml (deps). Only what is pasted is checked: workflow code is the methods of @workflow.defn classes (helpers they call in other modules are outside the check), and whether an activity is idempotent, and how a change replays against running workflows, is for the audit and for the replayer."
}
],
"verdict_candidate": "blocked"
}
The request body (Idempotency-Key: annalward:fix:a9ad0833-1e49:a1):
{
"task": "fix",
"code": "# file: pyproject.toml\n[project]\nname = \"creator-payouts\"\nversion = \"0.9.1\"\nrequires-python = \">=3.11\"\ndependencies = [\"temporalio>=1.7\", \"requests>=2.31\"]\n---\n# file: payouts/activities.py\nimport requests\nfrom temporalio import activity\n\nLEDGER_API = \"https://ledger.example.t...",
"facts": "{\"service\":{\"python_version\":\">=3.11\",\"temporalio\":\">=1.7\",\"python_files\":3,\"workflows\":1,\"activities\":2,\"activity_calls\":2,\"workers\":1},\"files\":[{\"path\":\"pyproject.toml\",\"...",
"note": "The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync."
}
The reply (output.output), an excerpt: the verdict, the headline, the two returned files
(the workflow file cut, the worker file whole), the change that lets the Worker start, the last
verification step, and the empty open and questions lists:
{
"task": "fix",
"verdict": "fixed",
"headline": "Fixed the two workflow non-determinism bugs, the illegal activity call, and the missing activity executor so the payout worker starts and the batch replays cleanly.",
"files": [
{
"path": "payouts/workflows.py",
"content": "import asyncio\nfrom datetime import timedelta\n\nfrom temporalio import workflow\nfrom temporalio.common import RetryPolicy\n\nwith workflow.unsafe.imports_passed_through():\n from payouts.activities import fetch_balance, send_payout\n\nMIN_PAYOUT_CENTS = 1000\n\n\n..."
},
{
"path": "payouts/worker.py",
"content": "import asyncio\nimport concurrent.futures\n\nfrom temporalio.client import Client\nfrom temporalio.worker import Worker\n\nfrom payouts.activities import fetch_balance, send_payout\nfrom payouts.workflows import PayoutBatchWorkflow\n\n\nasync def main() -> None:\n client = await Client.connect(\"localhost:7233\")\n worker = Worker(\n client,\n task_queue=\"payouts\",\n workflows=[PayoutBatchWorkflow],\n activities=[fetch_balance, send_payout],\n activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=10),\n )\n await worker.run()\n\n\nif __name__ == \"__main__\":\n asyncio.run(main())\n"
}
],
"changes": [
...,
{
"file": "payouts/worker.py",
"function": "main",
"flags": [
"F4"
],
"change": "Added activity_executor=concurrent.futures.ThreadPoolExecutor(max_workers=10) to the Worker construction and imported concurrent.futures.",
"why": "A Worker registering plain def activities (fetch_balance, send_payout) refuses to start unless it is given an executor to run them in."
}
],
"verify": [
...,
"In WorkflowEnvironment.start_time_skipping(), start a worker built exactly as the fixed payouts/worker.py does (task_queue=\"payouts\", the same activity_executor), run PayoutBatchWorkflow with send_payout mocked to raise once and then succeed, and assert the worker starts and the run still returns the payout_id - the test that would have caught F4 (worker refusing to start) and the send_payout retry behavior."
],
"open": [],
"questions": [],
"summary": ...
}
The other three changes are all in PayoutBatchWorkflow.run:
start_to_close_timeout=timedelta(seconds=30) for fetch_balance (F1),
args=[creator_id, cents, batch_id] for send_payout (F2), and
await asyncio.sleep(1) in place of time.sleep(1), with import time
replaced by import asyncio (F3). Both activities stay plain def functions, as
the note asked, so the verdict is fixed with nothing open;
payouts/activities.py is not returned because nothing in it changed.
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.
- Both lanes: the reply is the lane you asked for (one written for the other task is a disagreement), it carries a valid verdict for that lane, and no credential from the paste is repeated anywhere in it (every credential value the free check found in the paste).
- Audit: the areas table. Exactly four rows, covering determinism, activities, compensation and operations once each, each with a valid status.
- Audit: every finding is anchored. Its
idis in the catalogue (TW-01..TW-21, orother; TW-22 is context and never a finding); itsfileis a pasted file; itslineis inside that file; itsevidenceis found line by line in that file ofcode(indentation trimmed) and on or within three lines ofline; every citeduidis a non-info flag the page raised. A finding that carries a high flag at a lower severity, a finding with no valid area, or a medium or high finding with no edit, is pointed out. - Audit: every flag is answered once, consistently. Every non-info flag answered exactly once and no other; a
confirmedanswer needs a finding carrying that uid; adismissedone must not be carried by a finding and should give a reason. The verdict must fit the worst severity (blockedwith a high finding,needs_workwith a medium, otherwiseready). - Audit: every edit is put in place and re-checked. The findings’ edits are applied in the listed order, each on top of the ones before (
Tempkit.applyEdits): abeforethat is not found, is found more than once, or changes nothing is a disagreement. After each finding the free check runs again: the code must still read and no new high flag may appear. With every edit applied, every confirmed flag a finding with edits carries must be gone, every function of the paste must still exist, and a new medium flag is pointed out. - Fix: the files. At least one file, at most one new one; none empty, none with a
# file:line, every path a.pyfile; a file indented with tabs is pointed out. - Fix: put into the paste and re-checked. The files replace the pasted files of the same path (or are added) (
Tempkit.replaceFiles) and the free check runs again: the code must read, no high or medium flag of the paste may remain unless anopenitem names it, no new high or medium flag may appear, every function must still exist and keep itsdeforasync defand its Temporal role (run, signal, query, update, validator or activity decorator), and every@workflow.defnclass must still be one. - Fix: the changes list matches the code. Every change names a returned file, a function in the fixed code and real page flags; a high or medium flag no change and no open item names is pointed out. The verdict must fit the
openlist; an emptyverify, or one with no step that replays a recorded history, is pointed out.
recon.js also runs in Node, so you can hold a reply to exactly the page’s standard.
Save /tempkit.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 renewals-check.json renewals-reply.txt audit > renewals-edited.txt (tempkit.js, recon.js next to it)
const fs = require("fs");
const Recon = require("./recon.js"); // loads ./tempkit.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 renewals audit reply it reports:
ok: The areas table rates determinism, activities, compensation and operations once each.
ok: Every one of the 7 findings names a pasted file, a line inside it and evidence copied from it.
ok: The verdict (needs_work) fits the worst finding.
ok: Every one of the page's 4 flags is answered once.
ok: All 8 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
and for the payouts fix reply (node recheck.js payouts-check.json payouts-reply.txt fix):
ok: 2 files 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 4 functions is still there.
ok: Every one of the 4 changes names a returned file, a function in the fixed code and real page flags.
ok: The verdict (fixed) fits the open list.
ok: The reply repeats no credential from the paste.
0 disagreement(s) in 6 statements checked
Nothing is left raised in either case. The page does not run the code and is not the replayer. After
that, record a history of the workflow and replay it with temporalio.worker.Replayer against
the changed code, run the failure paths in
temporalio.testing.WorkflowEnvironment.start_time_skipping(), and run the tests the
reply’s plan or verify names, before you ship anything — and if
executions are already running, put a changed workflow behind workflow.patched(...) first.
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). The token comes from SKILLSAFE_TOKEN, or the
YOUR_TOKEN placeholder: copy yours from /tokens.html (step 2).
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}'
import hashlib, json, os, time, urllib.parse
import requests # pip install requests
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # step 2
class ApiError(Exception):
def __init__(self, status, code, message, details=None):
super().__init__(f"{status} {code}: {message}")
self.status, self.code, self.details = status, code, details
def api(method, path, body=None, headers=None):
h = {"Content-Type": "application/json"}
if TOKEN:
h["Authorization"] = "Bearer " + TOKEN
h.update(headers or {})
r = requests.request(method, BASE + path, headers=h,
data=None if body is None else json.dumps(body))
try:
env = r.json()
except ValueError:
env = {}
if not r.ok:
err = env.get("error") or {}
raise ApiError(r.status_code, err.get("code"), err.get("message") or r.reason, err.get("details"))
return env.get("data")
// annalward.mjs -- Node 18+ (global fetch), run with: node annalward.mjs
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // step 2
async function api(method, path, body, headers = {}) {
const h = { "Content-Type": "application/json", ...headers };
if (TOKEN) h.Authorization = "Bearer " + TOKEN;
const res = await fetch(BASE + path, {
method,
headers: h,
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error((json.error && json.error.message) || res.statusText);
err.status = res.status;
err.code = json.error && json.error.code;
err.details = json.error && json.error.details;
throw err;
}
return json.data;
}
package main
import (
"bufio"
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"time"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // step 2
type apiError struct {
Status int
Code string
Message string
}
func (e *apiError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func newRequest(method, path string, body any, headers map[string]string) (*http.Request, error) {
var buf []byte
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
buf = b
}
req, err := http.NewRequest(method, base+path, bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
for k, v := range headers {
req.Header.Set(k, v)
}
return req, nil
}
func errorFrom(res *http.Response, env envelope) error {
e := &apiError{Status: res.StatusCode, Message: http.StatusText(res.StatusCode)}
if env.Error != nil {
e.Code = env.Error.Code
if env.Error.Message != "" {
e.Message = env.Error.Message
}
}
return e
}
// api sends body as JSON and decodes the envelope's data into out (out may be nil).
func api(method, path string, body any, headers map[string]string, out any) error {
req, err := newRequest(method, path, body, headers)
if err != nil {
return err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env envelope
_ = json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode < 200 || res.StatusCode > 299 {
return errorFrom(res, env)
}
if out != nil && len(env.Data) > 0 {
return json.Unmarshal(env.Data, out)
}
return nil
}
func main() {
// steps 2-6 go here; remove any import a step you skip no longer uses
_, _, _, _, _ = bufio.NewScanner, sha256.Sum256, hex.EncodeToString, url.PathEscape, strings.Index
_ = time.Second
}
// Annalward.java -- Java 17+, needs com.fasterxml.jackson.core:jackson-databind on the classpath
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Annalward {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN"); // step 2
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static class ApiException extends Exception {
final int status;
final String code;
ApiException(int status, String code, String message) {
super(status + " " + code + ": " + message);
this.status = status;
this.code = code;
}
}
static HttpRequest.Builder request(String method, String path, Object body, Map<String, String> headers) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Content-Type", "application/json")
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body)));
if (token != null && !token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
return b;
}
static ApiException errorFrom(int status, JsonNode env) {
JsonNode err = env.path("error");
return new ApiException(status, err.path("code").asText(null), err.path("message").asText("HTTP " + status));
}
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
HttpResponse<String> res = HTTP.send(request(method, path, body, headers).build(), HttpResponse.BodyHandlers.ofString());
JsonNode env;
try { env = JSON.readTree(res.body()); } catch (Exception e) { env = JSON.createObjectNode(); }
if (env == null) env = JSON.createObjectNode();
if (res.statusCode() / 100 != 2) throw errorFrom(res.statusCode(), env);
return env.path("data");
}
public static void main(String[] args) throws Exception {
// steps 2-6 go here (makeInput from step 4 goes next to api())
}
}
require "digest"
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
$token = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # step 2
class ApiError < StandardError
attr_reader :status, :code
def initialize(status, code, message)
super("#{status} #{code}: #{message}")
@status = status
@code = code
end
end
def build_request(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{$token}" if $token && !$token.empty?
headers.each { |k, v| req[k] = v }
req.body = JSON.generate(body) unless body.nil?
[uri, req]
end
def api(method, path, body = nil, headers = {})
uri, req = build_request(method, path, body, headers)
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
env = (JSON.parse(res.body) rescue {})
env = {} unless env.is_a?(Hash)
unless res.is_a?(Net::HTTPSuccess)
err = env["error"] || {}
raise ApiError.new(res.code.to_i, err["code"], err["message"] || res.message)
end
env["data"]
end
<?php
// needs the curl extension
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"; // step 2
class ApiError extends Exception {
public $status;
public $apiCode;
public function __construct($status, $code, $message) {
parent::__construct("$status $code: $message");
$this->status = $status;
$this->apiCode = $code;
}
}
function headers_for(array $extra = []): array {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($extra as $k => $v) $h[] = "$k: $v";
return $h;
}
function api(string $method, string $path, $body = null, array $headers = []) {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => headers_for($headers),
CURLOPT_RETURNTRANSFER => true,
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
$raw = curl_exec($ch);
if ($raw === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$env = json_decode($raw);
if (!is_object($env)) $env = new stdClass();
if ($status < 200 || $status > 299) {
$e = $env->error ?? new stdClass();
throw new ApiError($status, $e->code ?? null, $e->message ?? "HTTP $status");
}
return $env->data ?? null;
}
// Program.cs -- .NET 6+ console app (implicit usings on) with top-level statements; steps 2-6 (with the MakeInput local function) go ABOVE this class
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
static class Api
{
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN"; // step 2
public static readonly HttpClient Http = new HttpClient();
public class ApiException : Exception
{
public int Status;
public string? Code;
public ApiException(int status, string? code, string message) : base($"{status} {code}: {message}")
{
Status = status;
Code = code;
}
}
public static HttpRequestMessage Request(string method, string path, object? body = null, IDictionary<string, string>? headers = null)
{
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (body != null) req.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(Token)) req.Headers.TryAddWithoutValidation("Authorization", "Bearer " + Token);
if (headers != null) foreach (var kv in headers) req.Headers.TryAddWithoutValidation(kv.Key, kv.Value);
return req;
}
public static ApiException ErrorFrom(HttpResponseMessage res, JsonNode? env)
{
var err = env?["error"];
return new ApiException((int)res.StatusCode, err?["code"]?.GetValue<string>(),
err?["message"]?.GetValue<string>() ?? res.ReasonPhrase ?? "error");
}
public static async Task<JsonNode?> Call(string method, string path, object? body = null, IDictionary<string, string>? headers = null)
{
using var req = Request(method, path, body, headers);
using var res = await Http.SendAsync(req);
var text = await res.Content.ReadAsStringAsync();
JsonNode? env = null;
try { env = JsonNode.Parse(text); } catch (JsonException) { }
if (!res.IsSuccessStatusCode) throw ErrorFrom(res, env);
return env?["data"];
}
}
Step 2 — a token
Two kinds work:
-
Personal — needed for a run. Both lanes are metered in credits on
gpt-terra, so a run needs a signed-in (user) token (the page says “The audit run is metered, so it needs an account.”, or the same of the fix run) unless the estimate sayssponsor_enabled. Sign in and copy your token from /tokens.html, then export it asSKILLSAFE_TOKEN(the samples fall back to the placeholderYOUR_TOKEN). -
Guest — enough for
/meand/estimate.POST /guestwith{"slug": "annalward"}and noAuthorizationheader, exactly assdk.jsguest()does. The response’sdatacarriestokenandguest_id. Reuse a token you already hold rather than minting a new one each time.
# 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":"annalward"}' | jq -r '.data.token')
echo "guest token: ${TOKEN:0:12}..."
# personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if TOKEN in ("", "YOUR_TOKEN"):
TOKEN = "" # no Authorization header on /guest
guest = api("POST", "/guest", {"slug": "annalward"})
TOKEN = guest["token"]
print("guest", guest.get("guest_id"))
// personal token (needed for a run): paste it from /tokens.html into TOKEN and skip this
if (!TOKEN || TOKEN === "YOUR_TOKEN") {
TOKEN = ""; // no Authorization header on /guest
const guest = await api("POST", "/guest", { slug: "annalward" });
TOKEN = guest.token;
console.log("guest", guest.guest_id);
}
// inside main -- personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if token == "" {
var guest struct {
Token string `json:"token"`
GuestID string `json:"guest_id"`
}
if err := api("POST", "/guest", map[string]string{"slug": "annalward"}, nil, &guest); err != nil {
panic(err)
}
token = guest.Token
fmt.Println("guest", guest.GuestID)
}
// inside main -- personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if (token.isEmpty() || token.equals("YOUR_TOKEN")) {
token = ""; // no Authorization header on /guest
JsonNode guest = api("POST", "/guest", Map.of("slug", "annalward"), null);
token = guest.path("token").asText();
System.out.println("guest " + guest.path("guest_id").asText());
}
# personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if $token.empty? || $token == "YOUR_TOKEN"
$token = "" # no Authorization header on /guest
guest = api("POST", "/guest", { slug: "annalward" })
$token = guest["token"]
puts "guest #{guest["guest_id"]}"
end
// personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if ($TOKEN === "" || $TOKEN === "YOUR_TOKEN") {
$TOKEN = ""; // no Authorization header on /guest
$guest = api("POST", "/guest", ["slug" => "annalward"]);
$TOKEN = $guest->token;
echo "guest {$guest->guest_id}\n";
}
// top-level statements (above the Api class) -- personal token (needed for a run): set SKILLSAFE_TOKEN (from /tokens.html) and skip this
if (Api.Token == "" || Api.Token == "YOUR_TOKEN")
{
Api.Token = ""; // no Authorization header on /guest
var guest = await Api.Call("POST", "/guest", new { slug = "annalward" });
Api.Token = guest!["token"]!.GetValue<string>();
Console.WriteLine($"guest {guest["guest_id"]}");
}
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'
me = api("GET", "/me")
print(me["subject_type"], me.get("subject_id"), me.get("credits"))
signed_in = me["subject_type"] == "user"
const me = await api("GET", "/me");
console.log(me.subject_type, me.subject_id, me.credits);
const signedIn = me.subject_type === "user";
var me struct {
SubjectType string `json:"subject_type"`
SubjectID any `json:"subject_id"`
Credits any `json:"credits"`
}
if err := api("GET", "/me", nil, nil, &me); err != nil {
panic(err)
}
fmt.Println(me.SubjectType, me.SubjectID, me.Credits)
signedIn := me.SubjectType == "user"
_ = signedIn
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.path("subject_type").asText() + " " + me.path("subject_id").asText() + " " + me.path("credits"));
boolean signedIn = me.path("subject_type").asText().equals("user");
me = api("GET", "/me")
puts [me["subject_type"], me["subject_id"], me["credits"]].join(" ")
signed_in = me["subject_type"] == "user"
$me = api("GET", "/me");
echo $me->subject_type, " ", $me->subject_id ?? "", " ", $me->credits ?? "", "\n";
$signedIn = $me->subject_type === "user";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me?["subject_type"]} {me?["subject_id"]} {me?["credits"]}");
var signedIn = me?["subject_type"]?.GetValue<string>() == "user";
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 renewals example from renewals-check.json; step 6 reuses
makeInput for the payouts (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:
| Field | Meaning |
|---|---|
hold_credits | A 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_credits | The 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_enabled | When true, a caller who is not signed in may run; otherwise the page asks them to sign in. |
warnings | Input 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.
# renewals-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 "Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?" \
'{task: "audit", code, facts: (.facts | tojson), note: $note}' renewals-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})'
def make_input(task, check_path, note=None):
"""The body the page sends: task, code, facts as a JSON STRING, an optional note, and
(fix lane only) the audit_handoff string -- every value a string."""
with open(check_path, encoding="utf-8") as f:
check = json.load(f) # build-check.js (or handoff.js) output
inp = {
"task": task,
"code": check["code"], # the paste as the page sends it
"facts": json.dumps(check["facts"], separators=(",", ":"), ensure_ascii=False), # a STRING, like JSON.stringify
}
n = (note or "").strip()
if n:
inp["note"] = n[:2000] # optional; the page caps it at 2000 chars
if task == "fix" and check.get("audit_handoff"):
inp["audit_handoff"] = check["audit_handoff"] # optional; already a JSON string
return inp
inp = make_input("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?")
est = api("POST", "/estimate", inp)
print("hold", est.get("hold_credits"), "min", est.get("min_credits"),
"sponsored", est.get("sponsor_enabled"), "warnings", est.get("warnings") or [])
// The body the page sends: task, code, facts as a JSON STRING, an optional note,
// and (fix lane only) the audit_handoff string -- every value a string.
function makeInput(task, checkPath, note) {
const check = JSON.parse(readFileSync(checkPath, "utf8")); // build-check.js (or handoff.js) output
const input = {
task,
code: check.code, // the paste as the page sends it
facts: JSON.stringify(check.facts), // a STRING: the page's free check
};
const n = (note || "").trim();
if (n) input.note = n.length > 2000 ? n.slice(0, 2000) : n; // optional; max 2000 chars
if (task === "fix" && check.audit_handoff) input.audit_handoff = check.audit_handoff; // optional
return input;
}
const input = makeInput("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?");
const est = await api("POST", "/estimate", input);
console.log("hold", est.hold_credits, "min", est.min_credits,
"sponsored", est.sponsor_enabled, "warnings", est.warnings || []);
// makeInput builds the body the page sends: task, code, facts as a JSON STRING,
// an optional note and (fix lane only) the audit_handoff string.
makeInput := func(task, checkPath, note string) map[string]any {
raw, err := os.ReadFile(checkPath) // build-check.js (or handoff.js) output
if err != nil {
panic(err)
}
var check struct {
Code string `json:"code"`
Facts json.RawMessage `json:"facts"`
AuditHandoff string `json:"audit_handoff"`
}
if err := json.Unmarshal(raw, &check); err != nil {
panic(err)
}
var facts bytes.Buffer
if err := json.Compact(&facts, check.Facts); err != nil { // keeps key order; travels as a STRING
panic(err)
}
in := map[string]any{"task": task, "code": check.Code, "facts": facts.String()}
if n := []rune(strings.TrimSpace(note)); len(n) > 0 {
if len(n) > 2000 {
n = n[:2000] // optional; the page caps it at 2000 chars
}
in["note"] = string(n)
}
if task == "fix" && check.AuditHandoff != "" {
in["audit_handoff"] = check.AuditHandoff // optional; already a JSON string
}
return in
}
input := makeInput("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?")
var est struct {
HoldCredits any `json:"hold_credits"`
MinCredits any `json:"min_credits"`
SponsorEnabled bool `json:"sponsor_enabled"`
Warnings []any `json:"warnings"`
}
if err := api("POST", "/estimate", input, nil, &est); err != nil {
panic(err)
}
fmt.Printf("hold %v min %v sponsored %v warnings %v\n", est.HoldCredits, est.MinCredits, est.SponsorEnabled, est.Warnings)
// ---- add to the Annalward class, next to api(): the body the page sends
static Map<String, Object> makeInput(String task, String checkPath, String note) throws Exception {
JsonNode check = JSON.readTree(Files.readString(Path.of(checkPath))); // build-check.js (or handoff.js) output
Map<String, Object> in = new LinkedHashMap<>();
in.put("task", task);
in.put("code", check.path("code").asText()); // the paste as the page sends it
in.put("facts", JSON.writeValueAsString(check.path("facts"))); // a STRING, compact
String n = note == null ? "" : note.strip();
if (!n.isEmpty()) in.put("note", n.length() > 2000 ? n.substring(0, 2000) : n); // optional; max 2000 chars
if (task.equals("fix") && check.hasNonNull("audit_handoff")) in.put("audit_handoff", check.get("audit_handoff").asText());
return in;
}
// ---- inside main
Map<String, Object> input = makeInput("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?");
JsonNode est = api("POST", "/estimate", input, null);
System.out.println("hold " + est.path("hold_credits") + " min " + est.path("min_credits")
+ " sponsored " + est.path("sponsor_enabled").asBoolean(false) + " warnings " + est.path("warnings"));
# The body the page sends: task, code, facts as a JSON STRING, an optional note,
# and (fix lane only) the audit_handoff string -- every value a string.
def make_input(task, check_path, note = nil)
check = JSON.parse(File.read(check_path, encoding: "UTF-8")) # build-check.js (or handoff.js) output
input = {
task: task,
code: check["code"], # the paste as the page sends it
facts: JSON.generate(check["facts"]) # a STRING, compact
}
n = note.to_s.strip
input[:note] = n[0, 2000] unless n.empty? # optional; max 2000 chars
input[:audit_handoff] = check["audit_handoff"] if task == "fix" && !check["audit_handoff"].to_s.empty?
input
end
input = make_input("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?")
est = api("POST", "/estimate", input)
puts "hold #{est["hold_credits"]} min #{est["min_credits"]} " \
"sponsored #{est["sponsor_enabled"]} warnings #{(est["warnings"] || []).inspect}"
// The body the page sends: task, code, facts as a JSON STRING, an optional note,
// and (fix lane only) the audit_handoff string -- every value a string.
function make_input(string $task, string $checkPath, string $note = ""): array {
// decode to objects (not arrays) so an empty {} stays {} when re-encoded
$check = json_decode(file_get_contents($checkPath)); // build-check.js (or handoff.js) output
$input = [
"task" => $task,
"code" => $check->code, // the paste as the page sends it
"facts" => json_encode($check->facts, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), // a STRING
];
$n = trim($note);
if ($n !== "") $input["note"] = mb_substr($n, 0, 2000); // optional; max 2000 chars
if ($task === "fix" && !empty($check->audit_handoff)) $input["audit_handoff"] = $check->audit_handoff;
return $input;
}
$input = make_input("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?");
$est = api("POST", "/estimate", $input);
echo "hold ", $est->hold_credits ?? "-", " min ", $est->min_credits ?? "-",
" sponsored ", json_encode($est->sponsor_enabled ?? false),
" warnings ", json_encode($est->warnings ?? []), "\n";
// The body the page sends: task, code, facts as a JSON STRING, an optional note,
// and (fix lane only) the audit_handoff string -- every value a string.
static Dictionary<string, string> MakeInput(string task, string checkPath, string? note = null)
{
var compact = new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };
var check = JsonNode.Parse(File.ReadAllText(checkPath))!; // build-check.js (or handoff.js) output
var input = new Dictionary<string, string>
{
["task"] = task,
["code"] = check["code"]!.GetValue<string>(), // the paste as the page sends it
["facts"] = check["facts"]!.ToJsonString(compact), // a STRING, compact
};
var n = (note ?? "").Trim();
if (n.Length > 0) input["note"] = n.Length > 2000 ? n[..2000] : n; // optional; max 2000 chars
var handoff = check["audit_handoff"]?.GetValue<string>();
if (task == "fix" && !string.IsNullOrEmpty(handoff)) input["audit_handoff"] = handoff;
return input;
}
var input = MakeInput("audit", "renewals-check.json",
"Each subscription runs as one workflow for as long as the account exists. Anything that will bite us in a year?");
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"hold {est?["hold_credits"]} min {est?["min_credits"]} " +
$"sponsored {est?["sponsor_enabled"]} warnings {est?["warnings"]?.ToJsonString() ?? "[]"}");
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:
output.output— the model’s reply as a string: the JSON object from the output contract of the lane you asked for. Parse it yourself (from the first{to the last}). The samples print the verdict and headline, each area’s status, each finding’sid,file,line,severityand number of edits, then each flag answer. Keep the text:recheck.jsreads it.truncated—truewhen the reply was cut short because the balance ran out. The page then closes the cut-off JSON, shows what arrived as a partial result (“N of 9 sections recovered” for an audit, of 8 for a fix run), and says it was cut short by the available balance.charged_credits— the real cost of the run, as opposed to the estimate’shold_creditsreservation. The page re-reads/meafterwards for the new balance.error— on afailedjob.
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
annalward:<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
annalward:audit:ede4d6c7-25e5:a1 (renewals audit) and annalward:fix:a9ad0833-1e49:a1
(payouts fix); the fix run of the renewals audit handed on (above) is annalward:fix:6bf3d7d4-2d57: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="annalward: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}]}'
body = json.dumps(inp)
key = "annalward:audit:" + hashlib.sha256(body.encode()).hexdigest()[:16] + ":a1"
job_id = api("POST", "/run", inp, {"Idempotency-Key": key})["job_id"]
deadline = time.time() + 180 # 1 s interval, 180 s client-side limit
while True:
job = api("GET", "/jobs/" + urllib.parse.quote(job_id, safe=""))
if job["status"] in ("succeeded", "failed"):
break
if time.time() > deadline:
raise TimeoutError("job timed out")
time.sleep(1)
if job["status"] == "failed":
raise SystemExit(f"run failed: {job.get('error')}")
print("truncated:", job.get("truncated") is True, "charged:", job.get("charged_credits"))
text = (job.get("output") or {}).get("output") or ""
reply = json.loads(text[text.index("{"): text.rindex("}") + 1])
print(reply["verdict"], "-", reply["headline"])
for s in reply.get("areas", []):
print("area", s["area"], s["status"])
for f in reply.get("findings", []):
print(f["id"], f["file"], f.get("line"), f["severity"], len(f.get("edits") or []), "edit(s)")
for a in reply.get("flag_answers", []):
print("answer", a["flag"], a["status"])
const key = "annalward:audit:" +
createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16) + ":a1";
const { job_id } = await api("POST", "/run", input, { "Idempotency-Key": key });
let job;
const deadline = Date.now() + 180000; // 1 s interval, 180 s client-side limit
for (;;) {
job = await api("GET", "/jobs/" + encodeURIComponent(job_id));
if (job.status === "succeeded" || job.status === "failed") break;
if (Date.now() > deadline) throw new Error("job timed out");
await new Promise((r) => setTimeout(r, 1000));
}
if (job.status === "failed") throw new Error("run failed: " + JSON.stringify(job.error));
console.log("truncated:", job.truncated === true, "charged:", job.charged_credits);
const text = (job.output && job.output.output) || "";
const reply = JSON.parse(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1));
console.log(reply.verdict, "-", reply.headline);
for (const s of reply.areas || []) console.log("area", s.area, s.status);
for (const f of reply.findings || []) console.log(f.id, f.file, f.line, f.severity, (f.edits || []).length, "edit(s)");
for (const a of reply.flag_answers || []) console.log("answer", a.flag, a.status);
inputJSON, _ := json.Marshal(input)
sum := sha256.Sum256(inputJSON)
key := "annalward:audit:" + hex.EncodeToString(sum[:8]) + ":a1"
var started struct {
JobID string `json:"job_id"`
}
if err := api("POST", "/run", input, map[string]string{"Idempotency-Key": key}, &started); err != nil {
panic(err)
}
type jobT struct {
Status string `json:"status"`
Output *struct {
Output string `json:"output"`
} `json:"output"`
Truncated bool `json:"truncated"`
ChargedCredits any `json:"charged_credits"`
Error any `json:"error"`
}
var job jobT
deadline := time.Now().Add(180 * time.Second) // 1 s interval, 180 s client-side limit
for {
job = jobT{}
if err := api("GET", "/jobs/"+url.PathEscape(started.JobID), nil, nil, &job); err != nil {
panic(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
if time.Now().After(deadline) {
panic("job timed out")
}
time.Sleep(time.Second)
}
if job.Status == "failed" {
panic(fmt.Sprintf("run failed: %v", job.Error))
}
fmt.Println("truncated:", job.Truncated, "charged:", job.ChargedCredits)
text := ""
if job.Output != nil {
text = job.Output.Output
}
var reply map[string]any
if a, b := strings.Index(text, "{"), strings.LastIndex(text, "}"); a >= 0 && b > a {
if err := json.Unmarshal([]byte(text[a:b+1]), &reply); err != nil {
panic(err)
}
}
fmt.Println(reply["verdict"], "-", reply["headline"])
areas, _ := reply["areas"].([]any)
for _, e := range areas {
s, _ := e.(map[string]any)
fmt.Println("area", s["area"], s["status"])
}
findings, _ := reply["findings"].([]any)
for _, e := range findings {
f, _ := e.(map[string]any)
edits, _ := f["edits"].([]any)
fmt.Println(f["id"], f["file"], f["line"], f["severity"], len(edits), "edit(s)")
}
answers, _ := reply["flag_answers"].([]any)
for _, e := range answers {
a, _ := e.(map[string]any)
fmt.Println("answer", a["flag"], a["status"])
}
byte[] digest = MessageDigest.getInstance("SHA-256").digest(JSON.writeValueAsBytes(input));
String key = "annalward:audit:" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
String jobId = api("POST", "/run", input, Map.of("Idempotency-Key", key)).path("job_id").asText();
JsonNode job;
long deadline = System.currentTimeMillis() + 180_000; // 1 s interval, 180 s client-side limit
while (true) {
job = api("GET", "/jobs/" + URLEncoder.encode(jobId, StandardCharsets.UTF_8), null, null);
String s = job.path("status").asText();
if (s.equals("succeeded") || s.equals("failed")) break;
if (System.currentTimeMillis() > deadline) throw new RuntimeException("job timed out");
Thread.sleep(1000);
}
if (job.path("status").asText().equals("failed")) throw new RuntimeException("run failed: " + job.path("error"));
System.out.println("truncated: " + job.path("truncated").asBoolean(false) + ", charged: " + job.path("charged_credits"));
String text = job.path("output").path("output").asText("");
JsonNode reply = JSON.readTree(text.substring(text.indexOf('{'), text.lastIndexOf('}') + 1));
System.out.println(reply.path("verdict").asText() + " - " + reply.path("headline").asText());
for (JsonNode s : reply.path("areas")) {
System.out.println("area " + s.path("area").asText() + " " + s.path("status").asText());
}
for (JsonNode f : reply.path("findings")) {
System.out.println(f.path("id").asText() + " " + f.path("file").asText() + " " + f.path("line") + " "
+ f.path("severity").asText() + " " + f.path("edits").size() + " edit(s)");
}
for (JsonNode a : reply.path("flag_answers")) {
System.out.println("answer " + a.path("flag").asText() + " " + a.path("status").asText());
}
key = "annalward:audit:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job_id = api("POST", "/run", input, { "Idempotency-Key" => key })["job_id"]
deadline = Time.now + 180 # 1 s interval, 180 s client-side limit
job = nil
loop do
job = api("GET", "/jobs/#{URI.encode_www_form_component(job_id)}")
break if %w[succeeded failed].include?(job["status"])
raise "job timed out" if Time.now > deadline
sleep 1
end
raise "run failed: #{job["error"].inspect}" if job["status"] == "failed"
puts "truncated: #{job["truncated"] == true}, charged: #{job["charged_credits"]}"
text = (job["output"] || {})["output"].to_s
reply = JSON.parse(text[text.index("{")..text.rindex("}")])
puts "#{reply["verdict"]} - #{reply["headline"]}"
(reply["areas"] || []).each { |s| puts "area #{s["area"]} #{s["status"]}" }
(reply["findings"] || []).each { |f| puts "#{f["id"]} #{f["file"]} #{f["line"].inspect} #{f["severity"]} #{(f["edits"] || []).size} edit(s)" }
(reply["flag_answers"] || []).each { |a| puts "answer #{a["flag"]} #{a["status"]}" }
$key = "annalward:audit:" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$jobId = api("POST", "/run", $input, ["Idempotency-Key" => $key])->job_id;
$deadline = time() + 180; // 1 s interval, 180 s client-side limit
while (true) {
$job = api("GET", "/jobs/" . rawurlencode($jobId));
if (in_array($job->status, ["succeeded", "failed"], true)) break;
if (time() > $deadline) throw new RuntimeException("job timed out");
sleep(1);
}
if ($job->status === "failed") throw new RuntimeException("run failed: " . json_encode($job->error ?? null));
echo "truncated: ", json_encode(($job->truncated ?? false) === true), ", charged: ", $job->charged_credits ?? "-", "\n";
$text = $job->output->output ?? "";
$reply = json_decode(substr($text, strpos($text, "{"), strrpos($text, "}") - strpos($text, "{") + 1));
echo $reply->verdict, " - ", $reply->headline, "\n";
foreach ($reply->areas ?? [] as $s) echo "area ", $s->area, " ", $s->status, "\n";
foreach ($reply->findings ?? [] as $f) echo $f->id, " ", $f->file, " ", json_encode($f->line ?? null), " ", $f->severity, " ", count($f->edits ?? []), " edit(s)\n";
foreach ($reply->flag_answers ?? [] as $a) echo "answer ", $a->flag, " ", $a->status, "\n";
var digest = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(input)));
var key = "annalward:audit:" + Convert.ToHexString(digest, 0, 8).ToLowerInvariant() + ":a1";
var started = await Api.Call("POST", "/run", input, new Dictionary<string, string> { ["Idempotency-Key"] = key });
var jobId = started!["job_id"]!.GetValue<string>();
JsonNode job;
var deadline = DateTime.UtcNow.AddSeconds(180); // 1 s interval, 180 s client-side limit
while (true)
{
job = (await Api.Call("GET", "/jobs/" + Uri.EscapeDataString(jobId)))!;
var s = job["status"]?.GetValue<string>();
if (s == "succeeded" || s == "failed") break;
if (DateTime.UtcNow > deadline) throw new TimeoutException("job timed out");
await Task.Delay(1000);
}
if (job["status"]?.GetValue<string>() == "failed") throw new Exception($"run failed: {job["error"]?.ToJsonString()}");
var truncated = job["truncated"] is JsonValue t && t.TryGetValue(out bool tb) && tb;
Console.WriteLine($"truncated: {truncated}, charged: {job["charged_credits"]}");
var text = job["output"]?["output"]?.GetValue<string>() ?? "";
var reply = JsonNode.Parse(text[text.IndexOf('{')..(text.LastIndexOf('}') + 1)])!;
Console.WriteLine($"{reply["verdict"]} - {reply["headline"]}");
foreach (var s in reply["areas"]?.AsArray() ?? new JsonArray())
Console.WriteLine($"area {s?["area"]} {s?["status"]}");
foreach (var f in reply["findings"]?.AsArray() ?? new JsonArray())
Console.WriteLine($"{f?["id"]} {f?["file"]} {f?["line"]?.ToJsonString() ?? "null"} {f?["severity"]} {f?["edits"]?.AsArray().Count ?? 0} edit(s)");
foreach (var a in reply["flag_answers"]?.AsArray() ?? new JsonArray())
Console.WriteLine($"answer {a?["flag"]} {a?["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
payouts 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:
| Event | Data | What the SDK does |
|---|---|---|
delta | {text} | Passes text to onDelta. |
job | job info | Passes it to onJob (the page uses it to advance its progress display). |
done | the final payload: {job_id, status, charged_credits, output} | Keeps it as the result. |
pending | a payload | Treated 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
payouts-check.json: makeInput then carries its audit_handoff.
# the fix worked example: payouts-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 "The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync." \
'{task: "fix", code, facts: (.facts | tojson), note: $note} + (if .audit_handoff then {audit_handoff} else {} end)' \
payouts-check.json)
FKEY="annalward: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}'
# the fix worked example: payouts-check.json from build-check.js
# (for audited code, pass handoff.js's output instead: make_input adds its audit_handoff)
fix_inp = make_input("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.")
fix_key = "annalward:fix:" + hashlib.sha256(json.dumps(fix_inp).encode()).hexdigest()[:16] + ":a1"
h = {"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN, "Idempotency-Key": fix_key}
done, failure, streamed = None, None, ""
with requests.post(BASE + "/run-stream", headers=h, data=json.dumps(fix_inp), stream=True) as r:
if "text/event-stream" not in r.headers.get("content-type", ""):
try:
env = r.json()
except ValueError:
env = {}
if not r.ok:
err = env.get("error") or {}
raise ApiError(r.status_code, err.get("code"), err.get("message") or r.reason)
done = env.get("data")
else:
event, data = "message", ""
for line in r.iter_lines(decode_unicode=True):
if line == "": # a blank line ends a frame
if data:
try:
payload = json.loads(data)
except ValueError:
payload = None
if event == "delta" and payload is not None:
streamed += payload.get("text") or ""
elif event == "job":
print("job", payload)
elif event in ("done", "pending"):
done = payload
elif event == "error":
failure = payload
event, data = "message", ""
elif line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data += line[5:].strip()
if failure:
raise SystemExit(f"{failure.get('code')}: {failure.get('message')} (job {failure.get('job_id')})")
done = done or {}
full = (done.get("output") or {}).get("output") or streamed
print("truncated:", done.get("truncated") is True, "charged:", done.get("charged_credits"))
result = json.loads(full[full.index("{"): full.rindex("}") + 1])
print(result["verdict"], "-", result["headline"])
for f in result.get("files", []):
print("file", f["path"], len(f.get("content") or ""), "characters")
for c in result.get("changes", []):
print("change", c.get("function"), ",".join(c.get("flags") or []) or "-", "-", c.get("change"))
for step in result.get("verify", []):
print("verify", step)
for o in result.get("open", []):
print("open", o["item"], "-", o.get("reason"))
for q in result.get("questions", []):
print("question", q)
// the fix worked example: payouts-check.json from build-check.js
// (for audited code, pass handoff.js's output instead: makeInput adds its audit_handoff)
const fixInput = makeInput("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.");
const fixKey = "annalward:fix:" +
createHash("sha256").update(JSON.stringify(fixInput)).digest("hex").slice(0, 16) + ":a1";
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + TOKEN, "Idempotency-Key": fixKey },
body: JSON.stringify(fixInput),
});
let done = null, failure = null, streamed = "";
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) {
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((json.error && json.error.message) || res.statusText);
done = json.data;
} else {
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
let idx;
while ((idx = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
let event = "message", dataStr = "";
for (const line of frame.split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) dataStr += line.slice(5).trim();
}
if (!dataStr) continue;
let data;
try { data = JSON.parse(dataStr); } catch { continue; }
if (event === "delta") streamed += data.text || "";
else if (event === "job") console.log("job", data);
else if (event === "done" || event === "pending") done = data;
else if (event === "error") failure = data;
}
}
}
if (failure) throw new Error(`${failure.code}: ${failure.message} (job ${failure.job_id})`);
const full = (done && done.output && done.output.output) || streamed;
console.log("truncated:", !!(done && done.truncated), "charged:", done && done.charged_credits);
const result = JSON.parse(full.slice(full.indexOf("{"), full.lastIndexOf("}") + 1));
console.log(result.verdict, "-", result.headline);
for (const f of result.files || []) console.log("file", f.path, (f.content || "").length, "characters");
for (const c of result.changes || []) console.log("change", c.function, (c.flags || []).join(",") || "-", "-", c.change);
for (const step of result.verify || []) console.log("verify", step);
for (const o of result.open || []) console.log("open", o.item, "-", o.reason);
for (const q of result.questions || []) console.log("question", q);
// the fix worked example: payouts-check.json from build-check.js
// (for audited code, pass handoff.js's output instead: makeInput adds its audit_handoff)
fixInput := makeInput("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.")
fixJSON, _ := json.Marshal(fixInput)
fixSum := sha256.Sum256(fixJSON)
fixKey := "annalward:fix:" + hex.EncodeToString(fixSum[:8]) + ":a1"
sreq, err := newRequest("POST", "/run-stream", fixInput, map[string]string{"Idempotency-Key": fixKey})
if err != nil {
panic(err)
}
sres, err := http.DefaultClient.Do(sreq)
if err != nil {
panic(err)
}
defer sres.Body.Close()
var done, failure map[string]any
var streamed strings.Builder
if !strings.Contains(sres.Header.Get("Content-Type"), "text/event-stream") {
var env envelope
_ = json.NewDecoder(sres.Body).Decode(&env)
if sres.StatusCode < 200 || sres.StatusCode > 299 {
panic(errorFrom(sres, env))
}
_ = json.Unmarshal(env.Data, &done)
} else {
sc := bufio.NewScanner(sres.Body)
sc.Buffer(make([]byte, 64*1024), 16*1024*1024) // the done frame can be long
event, data := "message", ""
for sc.Scan() {
line := sc.Text()
switch {
case line == "": // a blank line ends a frame
if data != "" {
var p map[string]any
if json.Unmarshal([]byte(data), &p) == nil {
switch event {
case "delta":
if t, ok := p["text"].(string); ok {
streamed.WriteString(t)
}
case "job":
fmt.Println("job", p)
case "done", "pending":
done = p
case "error":
failure = p
}
}
}
event, data = "message", ""
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
data += strings.TrimSpace(line[5:])
}
}
}
if failure != nil {
panic(fmt.Sprintf("%v: %v (job %v)", failure["code"], failure["message"], failure["job_id"]))
}
full := streamed.String()
if out, ok := done["output"].(map[string]any); ok {
if s, ok := out["output"].(string); ok && s != "" {
full = s
}
}
fmt.Println("truncated:", done["truncated"] == true, "charged:", done["charged_credits"])
var result map[string]any
if a, b := strings.Index(full, "{"), strings.LastIndex(full, "}"); a >= 0 && b > a {
if err := json.Unmarshal([]byte(full[a:b+1]), &result); err != nil {
panic(err)
}
}
fmt.Println(result["verdict"], "-", result["headline"])
files, _ := result["files"].([]any)
for _, e := range files {
f, _ := e.(map[string]any)
c, _ := f["content"].(string)
fmt.Println("file", f["path"], len([]rune(c)), "characters")
}
changes, _ := result["changes"].([]any)
for _, e := range changes {
c, _ := e.(map[string]any)
var flags []string
if fs, ok := c["flags"].([]any); ok {
for _, fl := range fs {
flags = append(flags, fmt.Sprint(fl))
}
}
fl := strings.Join(flags, ",")
if fl == "" {
fl = "-"
}
fmt.Println("change", c["function"], fl, "-", c["change"])
}
verify, _ := result["verify"].([]any)
for _, st := range verify {
fmt.Println("verify", st)
}
open, _ := result["open"].([]any)
for _, e := range open {
o, _ := e.(map[string]any)
fmt.Println("open", o["item"], "-", o["reason"])
}
questions, _ := result["questions"].([]any)
for _, q := range questions {
fmt.Println("question", q)
}
// the fix worked example: payouts-check.json from build-check.js
// (for audited code, pass handoff.js's output instead: makeInput adds its audit_handoff)
Map<String, Object> fixInput = makeInput("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.");
byte[] fixDigest = MessageDigest.getInstance("SHA-256").digest(JSON.writeValueAsBytes(fixInput));
String fixKey = "annalward:fix:" + HexFormat.of().formatHex(fixDigest, 0, 8) + ":a1";
HttpRequest sreq = request("POST", "/run-stream", fixInput, Map.of("Idempotency-Key", fixKey)).build();
HttpResponse<Stream<String>> sres = HTTP.send(sreq, HttpResponse.BodyHandlers.ofLines());
JsonNode done = null, failure = null;
StringBuilder streamed = new StringBuilder();
if (!sres.headers().firstValue("content-type").orElse("").contains("text/event-stream")) {
JsonNode env = JSON.readTree(sres.body().collect(Collectors.joining("\n")));
if (env == null) env = JSON.createObjectNode();
if (sres.statusCode() / 100 != 2) throw errorFrom(sres.statusCode(), env);
done = env.path("data");
} else {
String event = "message";
StringBuilder data = new StringBuilder();
Iterator<String> it = sres.body().iterator();
while (it.hasNext()) {
String line = it.next();
if (line.isEmpty()) { // a blank line ends a frame
if (data.length() > 0) {
JsonNode p = null;
try { p = JSON.readTree(data.toString()); } catch (Exception ignored) { }
if (p != null) {
switch (event) {
case "delta": streamed.append(p.path("text").asText("")); break;
case "job": System.out.println("job " + p); break;
case "done": case "pending": done = p; break;
case "error": failure = p; break;
default: break;
}
}
}
event = "message";
data.setLength(0);
} else if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:")) data.append(line.substring(5).trim());
}
}
if (failure != null) {
throw new RuntimeException(failure.path("code").asText() + ": " + failure.path("message").asText()
+ " (job " + failure.path("job_id").asText() + ")");
}
String full = done == null ? "" : done.path("output").path("output").asText("");
if (full.isEmpty()) full = streamed.toString();
System.out.println("truncated: " + (done != null && done.path("truncated").asBoolean(false))
+ ", charged: " + (done == null ? "-" : done.path("charged_credits").toString()));
JsonNode result = JSON.readTree(full.substring(full.indexOf('{'), full.lastIndexOf('}') + 1));
System.out.println(result.path("verdict").asText() + " - " + result.path("headline").asText());
for (JsonNode f : result.path("files")) {
System.out.println("file " + f.path("path").asText() + " " + f.path("content").asText("").codePointCount(0, f.path("content").asText("").length()) + " characters");
}
for (JsonNode c : result.path("changes")) {
List<String> flags = new ArrayList<>();
c.path("flags").forEach(fl -> flags.add(fl.asText()));
System.out.println("change " + c.path("function").asText() + " " + (flags.isEmpty() ? "-" : String.join(",", flags))
+ " - " + c.path("change").asText());
}
for (JsonNode step : result.path("verify")) System.out.println("verify " + step.asText());
for (JsonNode o : result.path("open")) System.out.println("open " + o.path("item").asText() + " - " + o.path("reason").asText());
for (JsonNode q : result.path("questions")) System.out.println("question " + q.asText());
# the fix worked example: payouts-check.json from build-check.js
# (for audited code, pass handoff.js's output instead: make_input adds its audit_handoff)
fix_input = make_input("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.")
fix_key = "annalward:fix:#{Digest::SHA256.hexdigest(JSON.generate(fix_input))[0, 16]}:a1"
uri, req = build_request("POST", "/run-stream", fix_input, { "Idempotency-Key" => fix_key })
done = failure = nil
streamed = +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) do |http|
http.request(req) do |res|
unless res["content-type"].to_s.include?("text/event-stream")
env = (JSON.parse(res.body) rescue {})
unless res.is_a?(Net::HTTPSuccess)
raise ApiError.new(res.code.to_i, env.dig("error", "code"), env.dig("error", "message") || res.message)
end
done = env["data"]
next
end
buf = +""
res.read_body do |chunk|
buf << chunk
while (i = buf.index("\n\n"))
frame = buf.slice!(0, i + 2)
event = "message"
data = +""
frame.split("\n").each do |line|
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:") then data << line[5..].strip
end
end
next if data.empty?
payload = (JSON.parse(data) rescue nil)
next if payload.nil?
case event
when "delta" then streamed << payload["text"].to_s
when "job" then puts "job #{payload.inspect}"
when "done", "pending" then done = payload
when "error" then failure = payload
end
end
end
end
end
raise "#{failure["code"]}: #{failure["message"]} (job #{failure["job_id"]})" if failure
done ||= {}
full = (done["output"] || {})["output"].to_s
full = streamed if full.empty?
puts "truncated: #{done["truncated"] == true}, charged: #{done["charged_credits"]}"
result = JSON.parse(full[full.index("{")..full.rindex("}")])
puts "#{result["verdict"]} - #{result["headline"]}"
(result["files"] || []).each { |f| puts "file #{f["path"]} #{f["content"].to_s.length} characters" }
(result["changes"] || []).each do |c|
flags = (c["flags"] || []).join(",")
puts "change #{c["function"]} #{flags.empty? ? "-" : flags} - #{c["change"]}"
end
(result["verify"] || []).each { |step| puts "verify #{step}" }
(result["open"] || []).each { |o| puts "open #{o["item"]} - #{o["reason"]}" }
(result["questions"] || []).each { |q| puts "question #{q}" }
// the fix worked example: payouts-check.json from build-check.js
// (for audited code, pass handoff.js's output instead: make_input adds its audit_handoff)
$fixInput = make_input("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.");
$fixKey = "annalward:fix:" . substr(hash("sha256", json_encode($fixInput)), 0, 16) . ":a1";
$done = null; $failure = null; $streamed = ""; $buf = ""; $ctype = "";
$handle = function (string $event, string $data) use (&$done, &$failure, &$streamed) {
$p = json_decode($data);
if ($p === null) return; // not JSON: skipped, as sdk.js does
switch ($event) {
case "delta": $streamed .= $p->text ?? ""; break;
case "job": echo "job ", json_encode($p), "\n"; break;
case "done": case "pending": $done = $p; break;
case "error": $failure = $p; break;
}
};
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => headers_for(["Idempotency-Key" => $fixKey]),
CURLOPT_POSTFIELDS => json_encode($fixInput, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$ctype) {
if (stripos($line, "content-type:") === 0) $ctype = strtolower(trim(substr($line, 13)));
return strlen($line);
},
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$buf, &$ctype, $handle) {
$buf .= $chunk; // collects the response in memory
if (strpos($ctype, "text/event-stream") === false) return strlen($chunk); // plain JSON: parse at the end
while (($i = strpos($buf, "\n\n")) !== false) {
$frame = substr($buf, 0, $i);
$buf = substr($buf, $i + 2);
$event = "message"; $data = "";
foreach (explode("\n", $frame) as $line) {
if (strpos($line, "event:") === 0) $event = trim(substr($line, 6));
elseif (strpos($line, "data:") === 0) $data .= trim(substr($line, 5));
}
if ($data !== "") $handle($event, $data);
}
return strlen($chunk);
},
]);
if (curl_exec($ch) === false) throw new RuntimeException(curl_error($ch));
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if (strpos($ctype, "text/event-stream") === false) {
$env = json_decode($buf) ?? new stdClass();
if ($status < 200 || $status > 299) {
throw new ApiError($status, $env->error->code ?? null, $env->error->message ?? "HTTP $status");
}
$done = $env->data ?? null;
}
if ($failure) throw new RuntimeException(($failure->code ?? "") . ": " . ($failure->message ?? "") . " (job " . ($failure->job_id ?? "") . ")");
$full = $done->output->output ?? "";
if ($full === "") $full = $streamed;
echo "truncated: ", json_encode(($done->truncated ?? false) === true), ", charged: ", $done->charged_credits ?? "-", "\n";
$result = json_decode(substr($full, strpos($full, "{"), strrpos($full, "}") - strpos($full, "{") + 1));
echo $result->verdict, " - ", $result->headline, "\n";
foreach ($result->files ?? [] as $f) echo "file ", $f->path, " ", mb_strlen($f->content ?? ""), " characters\n";
foreach ($result->changes ?? [] as $c) {
$flags = implode(",", $c->flags ?? []);
echo "change ", $c->function ?? "", " ", $flags === "" ? "-" : $flags, " - ", $c->change ?? "", "\n";
}
foreach ($result->verify ?? [] as $step) echo "verify ", $step, "\n";
foreach ($result->open ?? [] as $o) echo "open ", $o->item, " - ", $o->reason ?? "", "\n";
foreach ($result->questions ?? [] as $q) echo "question ", $q, "\n";
// the fix worked example: payouts-check.json from build-check.js
// (for audited code, pass handoff.js's output instead: MakeInput adds its audit_handoff)
var fixInput = MakeInput("fix", "payouts-check.json",
"The payout worker will not even start. Fix it. Keep the two activities as plain def functions - the ledger client is sync.");
var fixDigest = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fixInput)));
var fixKey = "annalward:fix:" + Convert.ToHexString(fixDigest, 0, 8).ToLowerInvariant() + ":a1";
using var sreq = Api.Request("POST", "/run-stream", fixInput, new Dictionary<string, string> { ["Idempotency-Key"] = fixKey });
using var sres = await Api.Http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
JsonNode? done = null, failure = null;
var streamed = new StringBuilder();
if (sres.Content.Headers.ContentType?.MediaType != "text/event-stream")
{
JsonNode? env = null;
try { env = JsonNode.Parse(await sres.Content.ReadAsStringAsync()); } catch (JsonException) { }
if (!sres.IsSuccessStatusCode) throw Api.ErrorFrom(sres, env);
done = env?["data"];
}
else
{
using var reader = new StreamReader(await sres.Content.ReadAsStreamAsync());
var ev = "message";
var data = new StringBuilder();
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.Length == 0) // a blank line ends a frame
{
if (data.Length > 0)
{
JsonNode? p = null;
try { p = JsonNode.Parse(data.ToString()); } catch (JsonException) { }
if (p != null)
{
switch (ev)
{
case "delta": streamed.Append(p["text"]?.GetValue<string>() ?? ""); break;
case "job": Console.WriteLine($"job {p.ToJsonString()}"); break;
case "done": case "pending": done = p; break;
case "error": failure = p; break;
}
}
}
ev = "message";
data.Clear();
}
else if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:")) data.Append(line[5..].Trim());
}
}
if (failure != null) throw new Exception($"{failure["code"]}: {failure["message"]} (job {failure["job_id"]})");
var full = done?["output"]?["output"]?.GetValue<string>();
if (string.IsNullOrEmpty(full)) full = streamed.ToString();
var cut = done?["truncated"] is JsonValue tv && tv.TryGetValue(out bool cutShort) && cutShort;
Console.WriteLine($"truncated: {cut}, charged: {done?["charged_credits"]}");
var result = JsonNode.Parse(full[full.IndexOf('{')..(full.LastIndexOf('}') + 1)])!;
Console.WriteLine($"{result["verdict"]} - {result["headline"]}");
foreach (var f in result["files"]?.AsArray() ?? new JsonArray())
Console.WriteLine($"file {f?["path"]} {(f?["content"]?.GetValue<string>() ?? "").Length} characters");
foreach (var c in result["changes"]?.AsArray() ?? new JsonArray())
{
var flags = string.Join(",", (c?["flags"]?.AsArray() ?? new JsonArray()).Select(fl => fl?.GetValue<string>()));
Console.WriteLine($"change {c?["function"]} {(flags == "" ? "-" : flags)} - {c?["change"]}");
}
foreach (var step in result["verify"]?.AsArray() ?? new JsonArray()) Console.WriteLine($"verify {step}");
foreach (var o in result["open"]?.AsArray() ?? new JsonArray()) Console.WriteLine($"open {o?["item"]} - {o?["reason"]}");
foreach (var q in result["questions"]?.AsArray() ?? new JsonArray()) Console.WriteLine($"question {q}");
What the page does that you now do
- Reads the paste in the browser and builds
codeandfactsfrom it (tempkit.js), joining dropped files under# file:lines and cutting a large paste on whole files with a# [annalward: not sent: ...]line; you send the same two strings. - Prices every input with
/estimateand refuses to run belowmin_credits, or on a paste with no Python it can read. - Sends each attempt with an input- and lane-derived
Idempotency-Key, and retries once withretry_notewhen a reply does not parse. - Closes a cut-off JSON reply and shows what arrived when
truncatedis true or the stream ends early. - Hands an audit to the fix lane as
audit_handoff, with every edit applied to the paste first, and only while that edited paste is unchanged. - Checks the reply against the code (
recon.js), in the browser: files, lines, evidence, every edit applied in order and the free check re-run after each finding, flag answers and verdict; the fixed files put back, re-checked and matched against the changes list and the open items, with every function, Temporal role and workflow class kept; and repeated credentials.recheck.jsabove runs the same checks for you.
Or skip all of this and use Annalward in the browser: the check, the flags, the workflow, activity-call, activity, compensation and Worker inventories, putting an edit or the fixed files back into the paste, and the Markdown and CSV exports are free.