Plan a parallel agent fan-out from your own scripts
Send a pile of work — a failing test run pasted straight out of the runner, a bug list, a task list, or the leftovers from a previous round of agents — and get back one JSON object: a posture saying whether fanning out is the right move at all, the independent problem domains with the reason each stands alone, one copy-ready agent brief per domain, the conflict watch list of files and resources two agents would both touch, the work held back for a later round, a reconciliation of the free client-side prescan, and the review-and-integrate checklist for when the agents come back. Everything this app does goes through the SkillSafe App API — plain JSON over HTTPS — so a red CI run can plan its own fan-out before anyone reads it. Wire it into whatever produces the work: a post-failure hook on your test job, a nightly triage of the open bug queue, a chat command that turns a paste into briefs. Pick a language once and the whole page follows.
Basics
Base URL: https://api.skillsafe.ai/v1/app-api, app slug
dispatch-desk. Every request sends
Authorization: Bearer <token> and JSON bodies with
Content-Type: application/json. Responses are wrapped in an envelope:
{"data": …} on success, {"error": {"code", "message"}} on
failure. The plan itself is produced by the gpt-terra model. Estimates are
free; runs are metered against your credit balance. There is a single run task — one
work list in, one plan out, no follow-up calls and no session state to carry. A re-plan is
just another run with round: "replan" and the agents' reports pasted into
context.
| Status | Meaning |
|---|---|
400 | Malformed body, or a field the endpoint does not accept — check the input object against the table in step 3. |
401 | Missing or expired token — create a new session. |
402 | Not enough credits — top up at skillsafe.ai/account/billing. The app surfaces this before the run by comparing min_credits from /estimate against the balance from /me; do the same and you never see this code. |
403 | The token isn't allowed to do this (e.g. a guest running a metered plan once the sponsored allowance is gone). |
404 | Unknown job id, record id, or a collection this release does not declare. |
409 | An Idempotency-Key replay whose body does not match the original request. |
429 | Rate limited. Data endpoints share 120 requests/min; /collections/{name}/similar is 30/min per IP. Back off and retry. |
5xx | Transient platform error — retry with backoff, reusing the same Idempotency-Key. |
Browsers enforce CORS for this API, so run these examples from a server, script or terminal — not from another website's frontend.
Step 1 — Get a token
A guest token lets you check balances and estimate costs for free. For metered plan runs
billed to your own account, use your personal token: open the
token page, sign in with SkillSafe, and press
Copy shell export — it puts export SKILLSAFE_TOKEN="…" on
your clipboard, which every example below reads. Treat the token like a password: it can
spend your credits. For fully headless scripts — a CI job that has no browser to open
— POST /guest mints a guest token with no human involved.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"dispatch-desk"}' | jq -r '.data.token'
import requests
API = "https://api.skillsafe.ai/v1/app-api"
res = requests.post(API + "/guest", json={"slug": "dispatch-desk"})
token = res.json()["data"]["token"]
const API = "https://api.skillsafe.ai/v1/app-api";
const res = await fetch(API + "/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "dispatch-desk" }),
});
const { data } = await res.json();
const token = data.token;
body, _ := json.Marshal(map[string]string{"slug": "dispatch-desk"})
res, _ := http.Post("https://api.skillsafe.ai/v1/app-api/guest",
"application/json", bytes.NewReader(body))
defer res.Body.Close()
var env struct {
Data struct {
Token string `json:"token"`
} `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
token := env.Data.Token
var req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"dispatch-desk\"}"))
.build();
var res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
// the guest token is at data.token in the returned envelope
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
res = Net::HTTP.post(URI(API + "/guest"),
{ slug: "dispatch-desk" }.to_json,
"Content-Type" => "application/json")
token = JSON.parse(res.body)["data"]["token"]
<?php
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/guest");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "dispatch-desk"]),
]);
$token = json_decode(curl_exec($ch), true)["data"]["token"];
curl_close($ch);
using var http = new HttpClient();
var res = await http.PostAsJsonAsync(
"https://api.skillsafe.ai/v1/app-api/guest", new { slug = "dispatch-desk" });
var env = await res.Content.ReadFromJsonAsync<JsonElement>();
var token = env.GetProperty("data").GetProperty("token").GetString();
The app stores this browser's token under the localStorage key
skillsafe_app_token:dispatch-desk, on the app's own origin. The
token page reads and manages it for you — you never need
to open developer tools.
A tiny client
Every task below is a single HTTP call, so start with a short helper that adds the auth
header, sends JSON and unwraps the data envelope. The later steps reuse it,
and it is where the optional Idempotency-Key header from steps 4 and 5 gets
threaded through.
export API="https://api.skillsafe.ai/v1/app-api"
export TOKEN="YOUR_TOKEN" # from the token page, or the /guest call above
# every call looks like:
# curl -s "$API/..." -H "Authorization: Bearer $TOKEN" [-d '{json}']
# jq is used below to pull fields out of the {"data": ...} envelope
import json, os, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # see above
def api(method, path, body=None, **headers):
res = requests.request(method, API + path, json=body,
headers={"Authorization": f"Bearer {TOKEN}", **headers})
payload = res.json()
if not res.ok:
raise RuntimeError(payload.get("error", {}).get("message", res.reason))
return payload["data"]
// Node 18+ (built-in fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // read it from your shell environment in real code
async function api(method, path, body, extraHeaders = {}) {
const res = await fetch(API + path, {
method,
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json", ...extraHeaders },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message ?? res.statusText);
return json.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
const API = "https://api.skillsafe.ai/v1/app-api"
var token = os.Getenv("SKILLSAFE_TOKEN") // see above
func call(method, path string, body, out any, headers map[string]string) error {
var buf bytes.Buffer
if body != nil {
json.NewEncoder(&buf).Encode(body)
}
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
for k, v := range headers {
req.Header.Set(k, v)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
var env struct {
Data json.RawMessage `json:"data"`
Error *struct{ Message string `json:"message"` } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if res.StatusCode >= 400 {
return fmt.Errorf("api %s %s: %s", method, path, env.Error.Message)
}
if out == nil {
return nil
}
return json.Unmarshal(env.Data, out)
}
// Java 17+, no dependencies. Pair with your JSON library (Jackson, Gson…)
// to read fields out of the returned envelope.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SkillSafe {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // see above
static final HttpClient HTTP = HttpClient.newHttpClient();
static String api(String method, String path, String jsonBody, String idemKey) throws Exception {
var b = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, jsonBody == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(jsonBody));
if (idemKey != null) b = b.header("Idempotency-Key", idemKey);
var res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // envelope: {"data": …}
}
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # see above
def api(method, path, body = nil, headers = {})
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
headers.each { |k, v| req[k] = v }
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise (payload.dig("error", "message") || res.message) unless res.is_a?(Net::HTTPSuccess)
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"; // see above
function api(string $method, string $path, ?array $body = null, array $extra = []): mixed {
global $TOKEN;
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge([
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
], $extra),
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body),
]);
$payload = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($payload["error"]["message"] ?? "HTTP $status");
}
return $payload["data"];
}
// .NET 8+
using System.Net.Http.Json;
using System.Text.Json;
static class SkillSafe
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new();
static SkillSafe() =>
Http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN")); // see above
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path,
object? body = null, string? idemKey = null)
{
var req = new HttpRequestMessage(method, Api + path);
if (body != null) req.Content = JsonContent.Create(body);
if (idemKey != null) req.Headers.Add("Idempotency-Key", idemKey);
var res = await Http.SendAsync(req);
var json = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!res.IsSuccessStatusCode)
throw new Exception(json.GetProperty("error").GetProperty("message").GetString());
return json.GetProperty("data");
}
}
Step 2 — Check who you are and your balance
Returns subject_type ("user" or "guest"),
subject_id and your credits balance, in credits (10,000 credits =
1 USD). A guest subject has no wallet worth speaking of: it exists so the free lanes
— /estimate and the read-only endpoints — work without an account,
and so a sponsored run can be attributed to somebody. Check this before planning a long
work list, and check it against min_credits from step 3 rather than waiting
for a 402.
curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq '.data'
# {
# "subject_type": "user",
# "subject_id": "usr_...",
# "credits": 48210
# }
me = api("GET", "/me")
print(me["subject_type"], me["credits"], "credits")
if me["subject_type"] != "user":
print("guest session — metered runs need a signed-in token")
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits, "credits");
if (me.subject_type !== "user") {
console.warn("guest session — metered runs need a signed-in token");
}
var me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int64 `json:"credits"`
}
if err := call("GET", "/me", nil, &me, nil); err != nil {
log.Fatal(err)
}
fmt.Printf("%s %s: %d credits\n", me.SubjectType, me.SubjectID, me.Credits)
String envelope = api("GET", "/me", null, null);
// data.subject_type, data.subject_id, data.credits
System.out.println(envelope);
me = api("GET", "/me")
puts "#{me["subject_type"]}: #{me["credits"]} credits"
warn "guest session — metered runs need a signed-in token" unless me["subject_type"] == "user"
$me = api("GET", "/me");
echo "{$me['subject_type']}: {$me['credits']} credits\n";
if ($me["subject_type"] !== "user") {
fwrite(STDERR, "guest session — metered runs need a signed-in token\n");
}
var me = await SkillSafe.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")}: {me.GetProperty("credits")} credits");
Step 3 — Estimate the cost
Send exactly the input you would send to /run; the response's
hold_credits is the worst-case cost. Nothing is charged and no job is created,
so estimating is free — useful when you are piping a whole runner log in and want a
ceiling before spending credits. The app calls this on a 500 ms debounce while you
type, which is also the sane cadence for a script that lets a human edit the work list
before pressing go.
| Response field | Type | Meaning |
|---|---|---|
model | string | The concrete model that will run the plan, as shown in the app's price meter. |
model_alias | string | The stable alias the app pins. Dispatch Desk is built and tuned for gpt-terra; assert on this and fail loudly if it ever comes back as something else, because the output schema in step 6 is what this model was prompted for. |
markup_bps | number | The app's markup over raw model cost, in basis points. It is already folded into hold_credits and min_credits — it is here so you can show a caller what they are paying for, not so you can add it again. |
hold_credits | number | The worst-case cost, in credits, and the amount reserved when the run starts. You are charged only what the run actually uses; the remainder of the hold is released. |
min_credits | number | The floor needed to start a run at all. Below this the app disables the run button and says how short you are. Between min_credits and hold_credits the run starts but may be truncated mid-plan — see the truncated flag in step 5. |
sponsor_enabled | boolean | True when the app is currently sponsoring runs for signed-out subjects. When it flips false mid-session the next run answers 403 with details.reason == "sponsor_exhausted", which is your cue to switch to a user token. |
The input object
One flat JSON object. Only work is required; every other field has a default
that matches the app's own form. This is the exact object the app builds in
readForm(), so anything you send here behaves identically to using the web UI.
| Input field | Type | Notes |
|---|---|---|
work | string, required | The pasted work, exactly as you have it: a Vitest/Jest FAIL block, a pytest report, a go test log, a TAP stream, a bulleted bug list, a numbered task list, or a mix. Stack traces are wanted, not noise — the file paths inside them are the strongest signal for telling independent work apart. Inputs longer than 100,000 characters are clipped middle-out on a line boundary, keeping the head and the tail, with a [... clipped N characters from the middle ...] marker showing where. The app refuses anything under 40 characters as too short to plan. |
shape | string | failures | tasks | mixed | unknown. What you say the paste is. The model is told to trust the text over this label when the two disagree, so an honest unknown costs nothing. |
agents | string | "auto" or "2"…"6" — a string, not a number, because it comes off a <select>. The hard cap on parallel briefs: the plan will never emit more briefs than this. "auto" means as many as there are genuinely independent domains. Work that does not fit under the cap is not dropped — it moves into sequential with after pointing at the round that frees it. |
round | string | first (plan the initial fan-out) or replan (the agents came back; plan the next wave). On replan the model treats what context says the agents reported as fact: it will not re-dispatch finished work, and exec_summary says what changed since the last wave. |
ownership | string | agents (may change production code) | tests-only (must not touch production code) | ask (must report findings and change nothing). Whichever you send appears in every brief's constraints, so it is the one field that reliably shapes what the agents are allowed to do. |
context | string, optional | Free text: repo layout, which subsystems are owned by whom, what changed just before this broke, whether the suite shares a database or a port, which files are off limits, how many agents you can really run — and, on a re-plan, what each agent reported and what is still red. Clipped at 20,000 characters, also middle-out. |
prescan_facts | object, optional | What a client-side scanner mechanically matched in work: {"resources": [], "flags": []}, each entry {id, label}. See the two tables below. Optional for API callers — omit it, or send the two empty arrays — but supplying it is the only thing that makes coverage_check meaningful. |
retry_note | string, optional | Only set by the app's automatic reformat retry when a first reply was not valid JSON. Leave it out; the model is instructed to obey it and re-answer, which is not what you want on a first attempt. |
prescan_facts — resources and flags
The app runs a free, offline prescan (DispatchScan.analyze) on every keystroke
and sends the result along with the work. It splits the paste into items, pulls out the
file paths, and raises a fixed checklist of mechanical independence risks. Nothing about it
is AI: it is regular expressions over the text you already have, so an API caller can
reproduce it, approximate it, or skip it.
Resources are the things the prescan found. Ids are
res:item/<n>-<slug> for each split-out work item — e.g.
res:item/3-should-track-pendingtoolcount — and
res:file/<slug> for each file path, e.g.
res:file/src-agents-abort-ts. Labels are Failure/…,
Task/… or File/…. Every work item you send here must
turn up somewhere in the plan — in inventory at minimum, and through it
in a domain, a brief or sequential. Silently dropping pasted work is the
failure mode this reconciliation exists to catch, because the user believes it was covered.
Flags are mechanical independence risks. Ids are
<family>:<slug>, and there are eleven families:
| Flag family | Example id | What it means for the plan |
|---|---|---|
shared-file | shared-file:src-batch-emitter-ts | One file appears under two or more items. Either those items are one domain, or the file gets exactly one owner and the other domain waits — the skill's rule is one agent per independent problem domain, and a shared file is not independence. |
same-error | same-error:typeerror-cannot-read-properties | The same error signature repeats across items. Identical failures in different files usually have one root cause; dispatching three agents at one bug burns three contexts and produces three conflicting fixes. |
shared-fixture | shared-fixture:tests-conftest-py | A conftest, setup file, factory or test helper is in play. Shared setup is shared state by definition: name the single owner in exactly one brief's constraints, or forbid all of them from touching it. |
ordering-dep | ordering-dep:blocked-by | The text says "after", "depends on", "blocked by", "once X lands". That is a sequence, not a set of independent tasks — the later item belongs in sequential. |
serialized-resource | serialized-resource:migrations-0042-orders-index-sql | A migration, schema file, lockfile, snapshot or generated artifact: single-writer by nature. Two agents both adding a migration or both regenerating a lockfile produce a conflict nobody wants to review. |
shared-env | shared-env:localhost-5432-app-test | A shared port, database or environment variable. Agents that run tests concurrently against the same one interfere at runtime, not just in the diff — and the failures look like flakes. |
thin-item | thin-item:fix-the-race-condition | An item with no file, no error text and no acceptance criterion. This is the canonical "no context" bad brief: the agent spends its whole context rediscovering what you already knew. |
too-broad | too-broad:fix-all-the-tests | An item phrased too broadly to scope. "Fix all the tests" is the skill's canonical bad brief; it comes back as a sprawling diff. |
timing-hint | timing-hint:race-condition | A timing, race, flake, timeout or deadlock hint. Any brief covering this item must carry the explicit constraint: do not just increase timeouts — find the real issue. |
single-domain | single-domain:floor | Everything is one item, or everything is in one file. Below the floor for fanning out: one agent with the whole picture beats several with partial ones. |
plain-secret | plain-secret:hunter2abcdef | A credential-looking literal is in the paste. It would otherwise be copied into a brief and into the saved plan. Redact it before you send. |
Every flag id you send comes back in coverage_check exactly once —
confirmed as accounted for, or set aside with a reason. Resource ids are not reconciled
there; they shape inventory instead. Omit prescan_facts entirely
and coverage_check comes back empty, with the rest of the plan unaffected.
cat > work.txt <<'WORK'
FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
WORK
jq -n --rawfile work work.txt \
'{work: $work,
shape: "failures",
agents: "auto",
round: "first",
ownership: "agents",
context: "Monorepo, Vitest, 28 test files. These went red after a refactor of the tool-execution pipeline that landed on main this morning; the three files share no imports beyond the types package.",
prescan_facts: {resources: [], flags: []}}' > input.json
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d @input.json | jq '.data | {model, model_alias, markup_bps, hold_credits, min_credits, sponsor_enabled}'
# {
# "model": "gpt-terra-2026-05",
# "model_alias": "gpt-terra",
# "markup_bps": 2000,
# "hold_credits": 5400,
# "min_credits": 900,
# "sponsor_enabled": false
# }
WORK = """FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
"""
payload = {
"work": WORK,
"shape": "failures",
"agents": "auto",
"round": "first",
"ownership": "agents",
"context": "Monorepo, Vitest, 28 test files. These went red after a refactor of the "
"tool-execution pipeline that landed on main this morning; the three files "
"share no imports beyond the types package.",
"prescan_facts": {"resources": [], "flags": []},
}
est = api("POST", "/estimate", payload)
assert est["model_alias"] == "gpt-terra", f'unexpected model: {est["model_alias"]}'
print("worst case:", est["hold_credits"], "credits; floor:", est["min_credits"])
me = api("GET", "/me")
if me["credits"] < est["min_credits"]:
raise SystemExit(f'short by {est["min_credits"] - me["credits"]} credits')
const work = `FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
`;
const payload = {
work,
shape: "failures",
agents: "auto",
round: "first",
ownership: "agents",
context:
"Monorepo, Vitest, 28 test files. These went red after a refactor of the " +
"tool-execution pipeline that landed on main this morning; the three files " +
"share no imports beyond the types package.",
prescan_facts: { resources: [], flags: [] },
};
const est = await api("POST", "/estimate", payload);
if (est.model_alias !== "gpt-terra") throw new Error(`unexpected model: ${est.model_alias}`);
console.log("worst case:", est.hold_credits, "credits; floor:", est.min_credits);
const work = `FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
`
payload := map[string]any{
"work": work,
"shape": "failures",
"agents": "auto",
"round": "first",
"ownership": "agents",
"context": "Monorepo, Vitest, 28 test files. These went red after a refactor of the " +
"tool-execution pipeline that landed on main this morning; the three files " +
"share no imports beyond the types package.",
"prescan_facts": map[string]any{
"resources": []any{}, "flags": []any{},
},
}
var est struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int64 `json:"markup_bps"`
HoldCredits int64 `json:"hold_credits"`
MinCredits int64 `json:"min_credits"`
SponsorEnabled bool `json:"sponsor_enabled"`
}
if err := call("POST", "/estimate", payload, &est, nil); err != nil {
log.Fatal(err)
}
if est.ModelAlias != "gpt-terra" {
log.Fatalf("unexpected model: %s", est.ModelAlias)
}
fmt.Printf("worst case %d credits, floor %d\n", est.HoldCredits, est.MinCredits)
String work = """
FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
""";
String jsonPayload = """
{"work": %s,
"shape": "failures",
"agents": "auto",
"round": "first",
"ownership": "agents",
"context": "Monorepo, Vitest, 28 test files. These went red after a refactor of the tool-execution pipeline that landed on main this morning.",
"prescan_facts": {"resources": [], "flags": []}}
""".formatted(toJsonString(work)); // toJsonString = your JSON library's string encoder
String envelope = api("POST", "/estimate", jsonPayload, null);
// assert data.model_alias equals "gpt-terra", then read
// data.hold_credits (worst case) and data.min_credits (floor to start at all)
WORK = <<~LOG
FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
LOG
payload = { work: WORK,
shape: "failures",
agents: "auto",
round: "first",
ownership: "agents",
context: "Monorepo, Vitest, 28 test files. These went red after a refactor of " \
"the tool-execution pipeline that landed on main this morning.",
prescan_facts: { resources: [], flags: [] } }
est = api("POST", "/estimate", payload)
raise "unexpected model: #{est["model_alias"]}" unless est["model_alias"] == "gpt-terra"
puts "worst case: #{est["hold_credits"]} credits, floor #{est["min_credits"]}"
$work = <<<'LOG'
FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
LOG;
$payload = [
"work" => $work,
"shape" => "failures",
"agents" => "auto",
"round" => "first",
"ownership" => "agents",
"context" => "Monorepo, Vitest, 28 test files. These went red after a refactor of "
. "the tool-execution pipeline that landed on main this morning.",
"prescan_facts" => ["resources" => [], "flags" => []],
];
$est = api("POST", "/estimate", $payload);
if ($est["model_alias"] !== "gpt-terra") {
throw new Exception("unexpected model: " . $est["model_alias"]);
}
echo "worst case: {$est['hold_credits']} credits, floor {$est['min_credits']}\n";
var work = """
FAIL src/agents/tool-abort.test.ts
x should abort a tool call and capture partial output (5023 ms)
AssertionError: expected message to contain 'interrupted at'
at src/agents/abort-controller.ts:118:9
FAIL src/batch/completion.test.ts
x emits batch-complete once every tool settles
TypeError: Cannot read properties of undefined (reading 'threadId')
at src/batch/emitter.ts:57:31
FAIL src/approval/race.test.ts
x runs an approved tool exactly once under concurrent approvals
AssertionError: execution count expected 1, received 0
at src/approval/queue.ts:88:5
""";
var payload = new {
work,
shape = "failures",
agents = "auto",
round = "first",
ownership = "agents",
context = "Monorepo, Vitest, 28 test files. These went red after a refactor of the "
+ "tool-execution pipeline that landed on main this morning.",
prescan_facts = new {
resources = Array.Empty<object>(), flags = Array.Empty<object>(),
},
};
var est = await SkillSafe.ApiAsync(HttpMethod.Post, "/estimate", payload);
if (est.GetProperty("model_alias").GetString() != "gpt-terra")
throw new Exception("unexpected model");
Console.WriteLine($"worst case: {est.GetProperty("hold_credits")} credits, " +
$"floor {est.GetProperty("min_credits")}");
prescan_facts.flags is how you make the plan answer for the collisions you
already know about. Send
{"resources": [{"id": "res:file/src-batch-emitter-ts", "label": "File/src/batch/emitter.ts"}],
"flags": [{"id": "shared-file:src-batch-emitter-ts", "label": "shared-file: src/batch/emitter.ts appears under 2 items"}]}
and that id comes back in coverage_check — accounted for by a conflict
row, a merged domain, a brief constraint or a held-back item, or explicitly set aside with
the reason. Nothing you flag is silently dropped, which makes it the field to assert on in
an automated gate.
Step 4 — Run the plan and wait for the result
/run takes the same input as /estimate, places a credit hold and
returns a job_id. Poll /jobs/{job_id} every 1–2 seconds
until status is succeeded or failed (a run typically
takes 40–120 s, because every brief is a complete, self-contained prompt rather
than a one-line summary). Always send an Idempotency-Key header so a network
retry can't start a second, double-charged run. The plan is in output —
usually nested as output.output, and as a JSON string, so parse
defensively. The samples below print the posture, the domains, the briefs and the held-back
work, then save the whole object to plan.json.
IDEM="dd-$(shasum -a 256 input.json | cut -c1-16)"
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d @input.json | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
sleep 2
done
# unwrap the plan once, then read it
echo "$JOB" | jq -r '.data.output.output' > plan.json
jq -r '
"\(.plan_name) [\(.posture)]: \(.verdict)",
"",
"DOMAINS",
(.domains[] | " \(.id) \(.name) [\(.risk)] - \(.rationale)"),
"",
"BRIEFS",
(.briefs[] | " \(.id) -> \(.domain_id) \(.title)"),
"",
"CONFLICTS",
(.conflicts[] | " \(.resource) claimed by \(.domain_ids | join(", ")) - \(.mitigation)"),
"",
"HELD BACK",
(.sequential[] | " \(.item)\(if .after != "" then " (after " + .after + ")" else "" end) - \(.why)"),
"",
"COVERAGE",
(.coverage_check[] | " \(.id): \(if .addressed then "accounted for" else "SET ASIDE" end) - \(.note)"),
"",
"INTEGRATION",
(.integration[] | " - \(.)")' plan.json
# refuse to fan out when the plan says not to
jq -e '.posture != "sequential-first"' plan.json > /dev/null \
|| { echo "plan says do not fan out yet"; exit 1; }
import hashlib, time
idem = "dd-" + hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()).hexdigest()[:16]
job_id = api("POST", "/run", payload, **{"Idempotency-Key": idem})["job_id"]
while True:
job = api("GET", f"/jobs/{job_id}")
if job["status"] in ("succeeded", "failed"):
break
time.sleep(1.5)
if job["status"] == "failed":
raise RuntimeError(job.get("error", "run failed"))
raw = job["output"]
if isinstance(raw, dict) and "output" in raw:
raw = raw["output"]
plan = json.loads(raw) if isinstance(raw, str) else raw
print(f'{plan["plan_name"]} [{plan["posture"]}]: {plan["verdict"]}')
for d in plan["domains"]:
print(f' {d["id"]:<4} {d["name"]:<34} risk={d["risk"]:<6} {len(d["items"])} item(s)')
print(f' {d["rationale"]}')
for b in plan["briefs"]:
print(f' {b["id"]} -> {b["domain_id"]} {b["title"]}')
print(f' goal: {b["goal"]}')
print(f' returns: {b["expected_output"]}')
for c in plan["conflicts"]:
print(f' conflict {c["resource"]} :: {", ".join(c["domain_ids"])} -> {c["mitigation"]}')
for s in plan["sequential"]:
print(f' held back: {s["item"]}' + (f' (after {s["after"]})' if s["after"] else ""))
for c in plan["coverage_check"]:
print(f' {c["id"]}: {"accounted for" if c["addressed"] else "SET ASIDE"} - {c["note"]}')
for step in plan["integration"]:
print(" integrate:", step)
with open("plan.json", "w", encoding="utf-8") as fh:
json.dump(plan, fh, indent=2)
if plan["posture"] == "sequential-first":
raise SystemExit("plan says do not fan out yet")
import { writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
const idem = "dd-" + createHash("sha256")
.update(JSON.stringify(payload)).digest("hex").slice(0, 16);
const { job_id } = await api("POST", "/run", payload, { "Idempotency-Key": idem });
let job;
do {
await new Promise((r) => setTimeout(r, 1500));
job = await api("GET", `/jobs/${job_id}`);
} while (job.status !== "succeeded" && job.status !== "failed");
if (job.status === "failed") throw new Error(job.error ?? "run failed");
const raw = job.output?.output ?? job.output;
const plan = typeof raw === "string" ? JSON.parse(raw) : raw;
console.log(`${plan.plan_name} [${plan.posture}]: ${plan.verdict}`);
for (const d of plan.domains) {
console.log(` ${d.id} ${d.name} [${d.risk}] - ${d.rationale}`);
for (const item of d.items) console.log(` * ${item}`);
}
for (const b of plan.briefs) {
console.log(` ${b.id} -> ${b.domain_id}: ${b.title}`);
}
for (const c of plan.conflicts) {
console.log(` conflict ${c.resource} (${c.domain_ids.join(", ")}): ${c.mitigation}`);
}
for (const s of plan.sequential) {
console.log(` held back: ${s.item}${s.after ? ` after ${s.after}` : ""} - ${s.why}`);
}
for (const c of plan.coverage_check) {
console.log(` ${c.id}: ${c.addressed ? "accounted for" : "SET ASIDE"} - ${c.note}`);
}
writeFileSync("plan.json", JSON.stringify(plan, null, 2));
if (plan.posture === "sequential-first") process.exitCode = 1;
var started struct{ JobID string `json:"job_id"` }
idem := fmt.Sprintf("dd-%x", sha256.Sum256([]byte(fmt.Sprint(payload))))[:19]
if err := call("POST", "/run", payload, &started,
map[string]string{"Idempotency-Key": idem}); err != nil {
log.Fatal(err)
}
var job struct {
Status string `json:"status"`
Error string `json:"error"`
Output json.RawMessage `json:"output"`
}
for {
if err := call("GET", "/jobs/"+started.JobID, nil, &job, nil); err != nil {
log.Fatal(err)
}
if job.Status == "succeeded" || job.Status == "failed" {
break
}
time.Sleep(1500 * time.Millisecond)
}
// job.Output is {"output": "<json string>"} — unwrap, then unmarshal:
type Plan struct {
PlanName string `json:"plan_name"`
Posture string `json:"posture"`
Verdict string `json:"verdict"`
ExecSummary string `json:"exec_summary"`
Assumptions []string `json:"assumptions"`
OpenQuestions []string `json:"open_questions"`
Inventory []struct {
Kind, Name, Scope, Role string
} `json:"inventory"`
Domains []struct {
ID, Name, Rationale, Risk string
Items []string `json:"items"`
} `json:"domains"`
Briefs []struct {
ID string `json:"id"`
DomainID string `json:"domain_id"`
Title string `json:"title"`
Scope string `json:"scope"`
Goal string `json:"goal"`
Context string `json:"context"`
Constraints []string `json:"constraints"`
ExpectedOutput string `json:"expected_output"`
Prompt string `json:"prompt"`
} `json:"briefs"`
Conflicts []struct {
Resource string `json:"resource"`
DomainIDs []string `json:"domain_ids"`
Mitigation string `json:"mitigation"`
} `json:"conflicts"`
Sequential []struct {
Item, After, Why string
} `json:"sequential"`
CoverageCheck []struct {
ID, Note string
Addressed bool
} `json:"coverage_check"`
Integration []string `json:"integration"`
Summary string `json:"summary"`
}
var wrapper struct{ Output string `json:"output"` }
json.Unmarshal(job.Output, &wrapper)
var plan Plan
json.Unmarshal([]byte(wrapper.Output), &plan)
fmt.Printf("%s [%s]: %s\n", plan.PlanName, plan.Posture, plan.Verdict)
for _, d := range plan.Domains {
fmt.Printf(" %s %s [%s]: %s\n", d.ID, d.Name, d.Risk, d.Rationale)
}
for _, b := range plan.Briefs {
fmt.Printf(" %s -> %s: %s\n", b.ID, b.DomainID, b.Title)
}
for _, c := range plan.Conflicts {
fmt.Printf(" conflict %s %v: %s\n", c.Resource, c.DomainIDs, c.Mitigation)
}
os.WriteFile("plan.json", []byte(wrapper.Output), 0o644)
String envelope = api("POST", "/run", jsonPayload, "dd-" + jsonPayload.hashCode());
String jobId = /* data.job_id via your JSON library */;
while (true) {
String job = api("GET", "/jobs/" + jobId, null, null);
String status = /* data.status */;
if (status.equals("succeeded") || status.equals("failed")) break;
Thread.sleep(1500);
}
// The plan is at data.output.output as a JSON string — parse it again, then read
// plan_name, posture, verdict, exec_summary, assumptions[], open_questions[],
// inventory[] (kind/name/scope/role),
// domains[] (id/name/rationale/risk/items[]),
// briefs[] (id/domain_id/title/scope/goal/context/constraints[]/expected_output/prompt),
// conflicts[] (resource/domain_ids[]/mitigation),
// sequential[] (item/after/why), coverage_check[] (id/addressed/note),
// integration[] and summary.
// Finally keep the plan on disk:
// Files.writeString(Path.of("plan.json"), planJson);
require "digest"
idem = "dd-" + Digest::SHA256.hexdigest(payload.to_json)[0, 16]
started = api("POST", "/run", payload, { "Idempotency-Key" => idem })
job = nil
loop do
job = api("GET", "/jobs/#{started["job_id"]}")
break if %w[succeeded failed].include?(job["status"])
sleep 1.5
end
raise (job["error"] || "run failed") if job["status"] == "failed"
raw = job["output"].is_a?(Hash) ? job["output"].fetch("output", job["output"]) : job["output"]
plan = raw.is_a?(String) ? JSON.parse(raw) : raw
puts "#{plan["plan_name"]} [#{plan["posture"]}]: #{plan["verdict"]}"
plan["domains"].each { |d| puts " #{d["id"]} #{d["name"]} [#{d["risk"]}] - #{d["rationale"]}" }
plan["briefs"].each { |b| puts " #{b["id"]} -> #{b["domain_id"]}: #{b["title"]}" }
plan["conflicts"].each do |c|
puts " conflict #{c["resource"]} (#{c["domain_ids"].join(", ")}): #{c["mitigation"]}"
end
plan["sequential"].each { |s| puts " held back: #{s["item"]} - #{s["why"]}" }
plan["coverage_check"].each do |c|
puts " #{c["id"]}: #{c["addressed"] ? "accounted for" : "SET ASIDE"} - #{c["note"]}"
end
plan["integration"].each { |s| puts " integrate: #{s}" }
File.write("plan.json", JSON.pretty_generate(plan))
exit 1 if plan["posture"] == "sequential-first"
$idem = "dd-" . substr(hash("sha256", json_encode($payload)), 0, 16);
$started = api("POST", "/run", $payload, ["Idempotency-Key: $idem"]);
do {
sleep(2);
$job = api("GET", "/jobs/" . $started["job_id"]);
} while (!in_array($job["status"], ["succeeded", "failed"]));
if ($job["status"] === "failed") {
throw new Exception($job["error"] ?? "run failed");
}
$raw = is_array($job["output"]) ? ($job["output"]["output"] ?? $job["output"]) : $job["output"];
$plan = is_string($raw) ? json_decode($raw, true) : $raw;
echo "{$plan['plan_name']} [{$plan['posture']}]: {$plan['verdict']}\n";
foreach ($plan["domains"] as $d) {
echo " {$d['id']} {$d['name']} [{$d['risk']}]: {$d['rationale']}\n";
}
foreach ($plan["briefs"] as $b) {
echo " {$b['id']} -> {$b['domain_id']}: {$b['title']}\n";
}
foreach ($plan["conflicts"] as $c) {
echo " conflict {$c['resource']} (" . implode(", ", $c["domain_ids"]) . "): {$c['mitigation']}\n";
}
foreach ($plan["sequential"] as $s) {
echo " held back: {$s['item']} - {$s['why']}\n";
}
foreach ($plan["coverage_check"] as $c) {
echo " {$c['id']}: " . ($c["addressed"] ? "accounted for" : "SET ASIDE") . "\n";
}
file_put_contents("plan.json", json_encode($plan, JSON_PRETTY_PRINT));
var idem = "dd-" + Convert.ToHexString(
System.Security.Cryptography.SHA256.HashData(
JsonSerializer.SerializeToUtf8Bytes(payload)))[..16];
var started = await SkillSafe.ApiAsync(HttpMethod.Post, "/run", payload, idem);
var jobId = started.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await SkillSafe.ApiAsync(HttpMethod.Get, $"/jobs/{jobId}");
var status = job.GetProperty("status").GetString();
if (status is "succeeded" or "failed") break;
await Task.Delay(1500);
}
var rawText = job.GetProperty("output").GetProperty("output").GetString();
using var doc = JsonDocument.Parse(rawText!);
var plan = doc.RootElement;
Console.WriteLine($"{plan.GetProperty("plan_name")} " +
$"[{plan.GetProperty("posture")}]: {plan.GetProperty("verdict")}");
foreach (var d in plan.GetProperty("domains").EnumerateArray())
{
Console.WriteLine($" {d.GetProperty("id")} {d.GetProperty("name")} " +
$"[{d.GetProperty("risk")}]: {d.GetProperty("rationale")}");
}
foreach (var b in plan.GetProperty("briefs").EnumerateArray())
{
Console.WriteLine($" {b.GetProperty("id")} -> {b.GetProperty("domain_id")}: " +
$"{b.GetProperty("title")}");
}
foreach (var c in plan.GetProperty("conflicts").EnumerateArray())
{
Console.WriteLine($" conflict {c.GetProperty("resource")}: {c.GetProperty("mitigation")}");
}
await File.WriteAllTextAsync("plan.json", rawText!);
The model is asked for one JSON object and nothing else, but a stray code fence or preamble
is always possible. Strip a leading ```json fence, take the text between the
first { and the last }, and only then parse — that is
exactly what the app's parseResult() does before it falls back to a
retry_note reformat run. Step 6 has the full recovery recipe.
Step 5 — Stream the plan as it is written
/run-stream takes exactly the same body as /run but answers with
server-sent events, so you can show progress instead of a spinner. This is the app's
default lane: a plan with four complete agent briefs is a long reply, and
the app advances its step list by watching for the section keys as they arrive rather than
making the user stare at a bar that means nothing. Events are separated by a blank line;
each has an event: line and a data: line carrying JSON.
| Event | Payload | Meaning |
|---|---|---|
job | {job_id, status} | Sent once, when the job is accepted — show "starting". |
delta | {text} | A chunk of the reply, in order. Append it; the accumulated length is your only progress signal (the total is not known in advance). The app watches the accumulated text for "plan_name", "posture", "domains", "briefs", "conflicts", "sequential", "coverage_check", "integration" and "summary" and lights up a six-step list as each key appears. |
done | {job_id, status, charged_credits, output, truncated} | The final, authoritative result — read the plan from output.output rather than trusting concatenated deltas, and the settled price from charged_credits. truncated: true means the balance ran out mid-plan: what arrived is real and worth keeping, but sections are missing. |
error | {code, message, details} | Replaces done when the run fails. details.reason == "sponsor_exhausted" means the free allowance for signed-out subjects is used up for today. |
Idempotency
Both /run and /run-stream accept an Idempotency-Key
request header; some clients prefer the equivalent idempotency_key field in
the JSON body, and the two are interchangeable. Derive the key from the input, not from a
clock: the app builds
dispatch-desk:<fnv1a-hash-of-the-input>:<length>:a<attempt>,
where the hash covers work, shape, agents,
round, ownership and context. Two consequences worth
internalising:
- A retry of the same input must reuse the key. A dropped connection, a
5xx, a timeout in your HTTP client — all of these can leave a run that is already in flight. Re-sending with the same key returns the original job instead of starting a second one, so a malformed or lost first reply can never double-bill you. - A deliberate second attempt gets a new key. When the first reply is
not parseable JSON, the app retries once with a
retry_noteappended and the attempt counter bumped, because that genuinely is a second run and is charged as one. Bumping a counter you control keeps the two cases distinguishable; a timestamp does not. - Replaying a key with a different body is a
409, not a silent overwrite. If you edited the work list, you have a new input, so let the derived key change with it.
# -N disables buffering so events print as they arrive
IDEM="dd-$(shasum -a 256 input.json | cut -c1-16):a1"
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" \
-d @input.json
# event: job
# data: {"job_id":"job_...","status":"running"}
#
# event: delta
# data: {"text":"{\"plan_name\":\"Tool-pipeline refactor"}
# ...
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":3180,"output":{"output":"{...}"}}
# A retry after a dropped connection reuses $IDEM and returns the SAME job:
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEM" -d @input.json
import hashlib, json, requests
idem = "dd-" + hashlib.sha256(
json.dumps(payload, sort_keys=True).encode()).hexdigest()[:16] + ":a1"
result = None
with requests.post(
API + "/run-stream",
headers={"Authorization": f"Bearer {TOKEN}",
"Idempotency-Key": idem},
json=payload,
stream=True,
) as r:
r.raise_for_status()
event = None
for line in r.iter_lines(decode_unicode=True):
if not line:
continue
if line.startswith("event:"):
event = line[len("event:"):].strip()
elif line.startswith("data:"):
data = json.loads(line[len("data:"):].strip())
if event == "delta":
print(".", end="", flush=True) # live progress
elif event == "done":
result = data
elif event == "error":
raise RuntimeError(data.get("message", "run failed"))
plan = json.loads(result["output"]["output"]) # authoritative
print("\ncharged:", result["charged_credits"], "-", plan["plan_name"])
print("posture:", plan["posture"])
if result.get("truncated"):
print("WARNING: cut short by the available balance — sections are missing")
for b in plan["briefs"]:
print(f' {b["id"]} -> {b["domain_id"]}: {b["title"]}')
with open("plan.json", "w", encoding="utf-8") as fh:
json.dump(plan, fh, indent=2)
const idem = "dd-" + createHash("sha256")
.update(JSON.stringify(payload)).digest("hex").slice(0, 16) + ":a1";
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": idem,
},
body: JSON.stringify(payload),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", done = null, acc = "";
for (;;) {
const chunk = await reader.read();
if (chunk.done) break;
buf += decoder.decode(chunk.value, { stream: true });
const frames = buf.split("\n\n");
buf = frames.pop();
for (const frame of frames) {
const name = /^event:\s*(.+)$/m.exec(frame)?.[1];
const body = /^data:\s*(.+)$/m.exec(frame)?.[1];
if (!name || !body) continue;
const data = JSON.parse(body);
if (name === "delta") {
acc += data.text; // section-key progress
if (acc.includes('"briefs"')) process.stdout.write("b");
else process.stdout.write(".");
}
if (name === "done") done = data;
if (name === "error") throw new Error(data.message ?? "run failed");
}
}
const plan = JSON.parse(done.output.output);
console.log(`\n${done.charged_credits} credits - ${plan.plan_name} [${plan.posture}]`);
if (done.truncated) console.warn("cut short by the available balance");
for (const b of plan.briefs) console.log(` ${b.id} -> ${b.domain_id}: ${b.title}`);
writeFileSync("plan.json", JSON.stringify(plan, null, 2));
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idem) // same key on every retry of this input
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var event string
var final map[string]any
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
case strings.HasPrefix(line, "data:"):
var data map[string]any
json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &data)
switch event {
case "delta":
fmt.Print(".") // live progress
case "done":
final = data
case "error":
log.Fatal(data["message"])
}
}
}
// final["output"].(map[string]any)["output"].(string) is the plan JSON —
// unmarshal it into the Plan struct from step 4, then write it to plan.json.
// Java 17+ — read the stream line by line instead of buffering the body.
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", idem) // unchanged across retries of this input
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();
var res = HTTP.send(req, HttpResponse.BodyHandlers.ofLines());
String event = null, done = null;
for (String line : (Iterable<String>) res.body()::iterator) {
if (line.startsWith("event:")) {
event = line.substring(6).trim();
} else if (line.startsWith("data:")) {
String data = line.substring(5).trim();
if ("delta".equals(event)) System.out.print("."); // live progress
else if ("done".equals(event)) done = data;
else if ("error".equals(event)) throw new RuntimeException(data);
}
}
// parse `done`, then parse data.output.output again — it is a JSON string holding
// plan_name, posture, verdict, domains[], briefs[], conflicts[], sequential[],
// coverage_check[], integration[] and the rest.
require "net/http"
require "json"
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = idem # same key on every retry of this input
req.body = payload.to_json
event = nil
done = nil
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.strip
if line.start_with?("event:")
event = line.delete_prefix("event:").strip
elsif line.start_with?("data:")
data = JSON.parse(line.delete_prefix("data:").strip)
case event
when "delta" then print "." # live progress
when "done" then done = data
when "error" then raise (data["message"] || "run failed")
end
end
end
end
end
end
plan = JSON.parse(done["output"]["output"])
puts "\n#{done["charged_credits"]} credits - #{plan["plan_name"]} [#{plan["posture"]}]"
warn "cut short by the available balance" if done["truncated"]
plan["briefs"].each { |b| puts " #{b["id"]} -> #{b["domain_id"]}: #{b["title"]}" }
File.write("plan.json", JSON.pretty_generate(plan))
$event = null;
$done = null;
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Idempotency-Key: $idem",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$done) {
foreach (explode("\n", $chunk) as $line) {
$line = trim($line);
if (str_starts_with($line, "event:")) {
$event = trim(substr($line, 6));
} elseif (str_starts_with($line, "data:")) {
$data = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") { echo "."; } // live progress
elseif ($event === "done") { $done = $data; }
elseif ($event === "error") { throw new Exception($data["message"] ?? "run failed"); }
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
$plan = json_decode($done["output"]["output"], true);
echo "\n{$done['charged_credits']} credits - {$plan['plan_name']} [{$plan['posture']}]\n";
foreach ($plan["briefs"] as $b) {
echo " {$b['id']} -> {$b['domain_id']}: {$b['title']}\n";
}
file_put_contents("plan.json", json_encode($plan, JSON_PRETTY_PRINT));
var req = new HttpRequestMessage(HttpMethod.Post, Api + "/run-stream") {
Content = JsonContent.Create(payload),
};
req.Headers.Add("Idempotency-Key", idem); // same key on every retry of this input
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? evt = null, done = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event:")) evt = line[6..].Trim();
else if (line.StartsWith("data:"))
{
var data = line[5..].Trim();
if (evt == "delta") Console.Write("."); // live progress
else if (evt == "done") done = data;
else if (evt == "error") throw new Exception(data);
}
}
using var final = JsonDocument.Parse(done!);
var text = final.RootElement.GetProperty("output").GetProperty("output").GetString();
using var planDoc = JsonDocument.Parse(text!);
var plan = planDoc.RootElement;
Console.WriteLine($"{plan.GetProperty("plan_name")} [{plan.GetProperty("posture")}]");
foreach (var b in plan.GetProperty("briefs").EnumerateArray())
Console.WriteLine($" {b.GetProperty("id")} -> {b.GetProperty("domain_id")}: {b.GetProperty("title")}");
await File.WriteAllTextAsync("plan.json", text!);
In a browser, the native EventSource only speaks GET, and this endpoint is a
POST — read the fetch response body incrementally, as the JavaScript
sample above does. On an idempotent replay the server may answer with a plain JSON envelope
instead of an event stream; check the Content-Type before you start parsing
frames. And keep the accumulated delta text even when the stream dies: the app recovers a
partial plan from it by closing the open JSON and re-parsing, which is far better than
losing a run you already paid for.
Step 6 — Parse the result
One JSON object, always the same shape, whichever lane produced it. The plan is grounded in
the paste alone: domains, briefs and conflicts cite only items, files, fixtures and
subsystems that actually appear in work or context, and anything
the model would need but was not given goes into open_questions rather than
being invented. Where the paste is silent on something that changes the verdict you get an
entry in assumptions. Refusing to parallelize is a successful answer,
not a failure: a sequential-first plan with zero briefs and a strong
sequential list is complete and valid.
| Field | Type | Meaning |
|---|---|---|
plan_name | string | A short name for this dispatch, taken from the work's own vocabulary — e.g. Tool-pipeline refactor fallout — 3-way split. The app falls back to Untitled dispatch plan if it is missing, and uses it for the export filename and the history list. |
posture | string | parallel-ready | partial-parallel | sequential-first. The headline verdict. Anything unrecognised is coerced to partial-parallel. See the table below. |
verdict | string | One sentence: fan out or not, and the single most important reason. This is the line to put in a CI comment. |
exec_summary | string | Two to four short paragraphs separated by blank lines: what the work is, how it decomposes (or why it does not), the biggest collision risk, and what to do first. The app splits it on \n\n and renders one paragraph per block. |
assumptions | string[] | What had to be assumed because the paste did not say. Read these first — a wrong assumption invalidates the split built on it. |
open_questions | string[] | What the planner would ask before dispatching. On a low-detail paste this is often the most valuable field. |
inventory | array | {kind, name, scope, role} — every Failure, Task, File, Fixture and Resource the plan parsed out of the paste, and where it ended up. role names the domain it belongs to and why, or says held back or shared - single owner. This is the field that proves nothing was dropped. Entries with neither kind nor name are discarded by the client. |
domains | array | {id, name, rationale, risk, items[]} — the independent problem domains, in dispatch order. Columns below. At least one entry is required: the app throws domains must contain at least one entry and triggers its reformat retry if this is empty. |
briefs | array | {id, domain_id, title, scope, goal, context, constraints[], expected_output, prompt} — one copy-ready agent brief per parallel domain. Columns below. Legitimately empty when posture is sequential-first. |
conflicts | array | {resource, domain_ids[], mitigation} — one row per file, fixture, migration, lockfile, port or database claimed by more than one domain, or by a domain and the held-back work. mitigation names the single owner or the way to serialize it. Rows with no resource are discarded. |
sequential | array | {item, after, why} — the work explicitly not dispatched this round. after is what it waits on, or an empty string when it is simply not parallelizable. Rows with no item are discarded. If briefs is empty this must not also be empty. |
coverage_check | array | {id, addressed, note} — one entry per prescan_facts.flags id you sent, each appearing exactly once. Semantics below. |
integration | string[] | The review-and-integrate steps for when the agents come back, made concrete for this split: read each summary, check whether two agents edited the same code, run the full suite, spot-check for systematic errors, plus whatever this particular fan-out makes risky. |
summary | string | Two or three sentences a reader can act on immediately. |
The three posture values:
| posture | What it means |
|---|---|
parallel-ready | Two or more genuinely independent domains and no unresolved collision. Dispatch every brief in one message and go. The app labels this Parallel ready. |
partial-parallel | Some of the work fans out now, some has to wait — a shared migration, an ordering dependency, an item too thin to brief. briefs covers the first group, sequential the second. This is also the fallback the client applies to an unrecognised value. |
sequential-first | Do not fan out yet: one root cause wearing several hats, one domain, an unresolved shared resource, or too little detail to brief anyone. Expect zero or very few briefs and a substantial sequential list. This is a correct, useful answer — the reason not to spend four agent contexts is the product. |
Each entry in domains:
| Column | Meaning |
|---|---|
id | D1, D2, … in dispatch order — the handle referenced by briefs[].domain_id and conflicts[].domain_ids. The client fills in D<n> if it is missing. |
name | A short domain name, e.g. Tool abort lifecycle. |
rationale | Why this stands alone — specifically what it does not share with the others. This is the sentence to disagree with if you think the split is wrong. |
risk | low | medium | high, the collision risk of handing this domain to its own agent. Anything else is coerced to medium. The app sorts briefs by this — highest risk first — on screen and in every export, so the brief most likely to need judgement is read while the reader is still paying attention. |
items | The work items this domain covers, quoted from the paste. |
Each entry in briefs:
| Column | Meaning |
|---|---|
id | Sequential AB-001, AB-002, … The client fills in the next id if it is missing. |
domain_id | The domains[].id this brief serves. If it names a domain that does not exist, the client re-points it at the domain in the same position rather than dropping the brief. |
title | What this agent is for, in a few words. |
scope | The opening line of the prompt: exactly what this agent owns. |
goal | The finish condition, stated so it can be tested. |
context | Everything the agent needs and cannot infer: the concrete error text, the test names, the file paths, what changed. Agents get isolated context — they never inherit your session — so a brief that assumes prior knowledge is a broken brief. |
constraints | string[]. Always carries the ownership rule you sent. Any brief covering an item that raised a timing-hint flag also carries the explicit "do NOT just increase timeouts — find the real issue" constraint. |
expected_output | What the agent must return, specifically. "Fix it" is the canonical bad brief; this field is the antidote. |
prompt | The complete, self-contained brief as one block of text, ready to paste as an agent's whole task. If the model leaves it empty the client rebuilds it from scope, goal, context, constraints and expected_output, in that order — do the same rather than dispatching a blank. |
coverage_check semantics:
| Case | What you get |
|---|---|
| Every flag id you sent | Each prescan_facts.flags id appears in coverage_check exactly once. Nothing you flagged is silently dropped, which makes this the field to assert on in an automated gate. Ids in prescan_facts.resources are not reconciled here — they must show up in inventory instead. |
addressed: true | The plan actually accounts for the flag: a conflict row, two items merged into one domain, a constraint in a brief, or an item held back. note says which. |
addressed: false | The flag was deliberately set aside; note gives the reason — a shared file that only one of the two items really writes to, a timing word inside a test name rather than a symptom. Setting a flag aside is legitimate; ignoring it is not. |
| An id you sent that is missing entirely | Treat it as an unaddressed collision, not as an implicit pass. The app says so in its reconciliation line, and a gate should fail on it. |
| Nothing sent | Omit prescan_facts, or send the two empty arrays, and coverage_check comes back empty. The rest of the plan is unaffected. |
Defensive parsing, the way the app does it
The model is asked for exactly one JSON object and nothing else, and it almost always complies. The app still assumes it might not, in three layers:
- Unwrap. Strip a leading
```json(or bare```) fence and a trailing one, then take the substring from the first{to the last}, thenJSON.parse. A preamble sentence or a trailing "Let me know if…" costs you nothing. - Normalize. Coerce every field to its declared type rather than
trusting it: missing arrays become empty arrays, non-string scalars become strings,
postureandriskfall back to their defaults, ids are filled in by position, and rows with nothing usable in them are dropped. The only hard failure is an emptydomainsarray — a plan with no domain is not a plan. - Retry once, with a note. If it still will not parse, re-run the same
input once with
retry_noteset to a message naming the parse error and restating the format requirements, and a freshIdempotency-Keyattempt counter. If the second attempt also fails, keep the raw text and show it rather than pretending the run did not happen — you paid for it.
A small, realistic result for the three-cluster paste above, trimmed for length:
{
"plan_name": "Tool-pipeline refactor fallout — 3-way split",
"posture": "parallel-ready",
"verdict": "Three failure clusters sit in three subsystems with no shared file or fixture —
fan out to three agents in one message.",
"exec_summary": "Six tests went red in three files after this morning's tool-execution
refactor. The abort-lifecycle failures all bottom out in
src/agents/abort-controller.ts, the batch-completion failures in
src/batch/emitter.ts, and the approval race in src/approval/queue.ts. No file
appears under more than one cluster and no fixture is shared, so these are
three problems, not one wearing three hats.
The one thing to watch is the shared types package: all three subsystems
import from it, and if the refactor changed a type there, three agents will
each try to patch it. Nobody owns it in this plan — see the conflict row.
Start all three at once. The approval race is the highest-risk brief and the
one most likely to come back with questions, so read that summary first.",
"assumptions": [
"The refactor is the cause: everything else in the suite is green, per the context.",
"The three subsystems have separate owners, so three concurrent edits are reviewable."
],
"open_questions": [
"Did the refactor change any exported type in the shared types package?",
"Is the 5-second duration on the abort tests a real timeout, or just a slow test?"
],
"inventory": [
{ "kind": "Failure", "name": "should abort a tool call and capture partial output",
"scope": "src/agents/tool-abort.test.ts", "role": "D1 — abort lifecycle" },
{ "kind": "Failure", "name": "emits batch-complete once every tool settles",
"scope": "src/batch/completion.test.ts", "role": "D2 — batch completion" },
{ "kind": "Failure", "name": "runs an approved tool exactly once under concurrent approvals",
"scope": "src/approval/race.test.ts", "role": "D3 — approval race" },
{ "kind": "File", "name": "src/agents/abort-controller.ts", "scope": "src/agents",
"role": "D1 only — no other cluster touches it" },
{ "kind": "Resource", "name": "shared types package", "scope": "workspace",
"role": "shared - single owner (see conflicts)" }
],
"domains": [
{ "id": "D1", "name": "Tool abort lifecycle",
"rationale": "Both failures trace into src/agents/abort-controller.ts and nothing else
imports it; the batch and approval clusters never appear in these traces.",
"risk": "medium",
"items": ["should abort a tool call and capture partial output",
"should track pendingToolCount across an abort"] },
{ "id": "D2", "name": "Batch completion events",
"rationale": "One TypeError in src/batch/emitter.ts explains both batch failures; the
emitter is not referenced by the abort or approval traces.",
"risk": "low",
"items": ["emits batch-complete once every tool settles"] },
{ "id": "D3", "name": "Concurrent approval execution",
"rationale": "Isolated to src/approval/queue.ts, and it is a concurrency bug rather than a
signature change, so it needs a different kind of attention.",
"risk": "high",
"items": ["runs an approved tool exactly once under concurrent approvals"] }
],
"briefs": [
{ "id": "AB-003", "domain_id": "D3",
"title": "Fix the double/zero execution under concurrent approvals",
"scope": "You own src/approval/queue.ts and src/approval/race.test.ts. Nothing else.",
"goal": "src/approval/race.test.ts passes, and an approved tool executes exactly once
when two approvals arrive concurrently.",
"context": "After a refactor of the tool-execution pipeline, this test fails with
'AssertionError: execution count expected 1, received 0' at
src/approval/race.test.ts:52:20, with the trace passing through
src/approval/queue.ts:88:5. The count is 0, not 2, so the likely shape is a
dropped execution rather than a duplicated one.",
"constraints": [
"Agents may change production code.",
"Do NOT touch src/agents/ or src/batch/ — other agents own those this round.",
"Do NOT just increase timeouts or add sleeps — find the real ordering issue.",
"Do not edit the shared types package; report it instead if you need a change there."
],
"expected_output": "A summary naming the root cause in one sentence, the diff you applied,
and the command you ran to prove the test passes.",
"prompt": "You own src/approval/queue.ts and src/approval/race.test.ts. Nothing else.\n\n
Goal: src/approval/race.test.ts passes, and an approved tool executes exactly
once when two approvals arrive concurrently.\n\nContext:\n…" },
{ "id": "AB-001", "domain_id": "D1", "title": "Restore abort lifecycle semantics",
"scope": "You own src/agents/abort-controller.ts and src/agents/tool-abort.test.ts.",
"goal": "Both abort tests pass; partial output is captured and pendingToolCount is
accurate across an abort.",
"context": "…", "constraints": ["Agents may change production code.", "…"],
"expected_output": "…", "prompt": "…" },
{ "id": "AB-002", "domain_id": "D2", "title": "Fix the undefined threadId in the emitter",
"scope": "You own src/batch/emitter.ts and src/batch/completion.test.ts.",
"goal": "…", "context": "…", "constraints": ["…"],
"expected_output": "…", "prompt": "…" }
],
"conflicts": [
{ "resource": "shared types package",
"domain_ids": ["D1", "D2", "D3"],
"mitigation": "No agent may edit it. If a type change is needed, the agent reports it and
you make the single edit after all three return." }
],
"sequential": [],
"coverage_check": [
{ "id": "timing-hint:timed-out", "addressed": true,
"note": "AB-003 carries the do-not-raise-the-timeout constraint." }
],
"integration": [
"Read all three summaries before merging anything.",
"Diff the three branches against each other for edits to the shared types package.",
"Run the full suite once, not three partial suites — the interaction is the risk.",
"If two agents independently changed the same type, treat both fixes as suspect."
],
"summary": "Dispatch AB-003, AB-001 and AB-002 in a single message. Nobody owns the shared
types package this round; collect type change requests and make them yourself
afterwards. Read the approval-race summary first — it is the only genuine
concurrency bug in the set."
}
The dispatch block — one message, not three
This is the whole point of the skill and the one thing to get right. Multiple dispatch calls in one message run in parallel; one call per message runs sequentially. So the plan is written to be pasted as a single block: every brief inline, the held-back work explicitly marked do-not-dispatch, and the integration steps at the bottom. The app derives this in the browser from the result already on screen, and so should you — it costs no run.
Two details that matter. Sort the briefs by their domain's risk — high,
then medium, then low — and break ties by id; use that same
order everywhere, because a brief list sorted by risk on screen and by id in the file you
paste is how an agent ends up on the wrong task. And fall back to building
prompt out of the other brief fields when the model left it empty.
# Build the dispatch block from plan.json, highest-risk domain first.
jq -r '
(reduce .domains[] as $d ({}; .[$d.id] = ({high:0, medium:1, low:2}[$d.risk] // 1))) as $rank
| (.briefs | sort_by($rank[.domain_id] // 1, .id)) as $bs
| "Dispatch these \($bs | length) agents in a single message so they run in parallel.",
"",
( $bs
| to_entries[]
| "--- Agent \(.key + 1) of \($bs | length): \(.value.title // .value.id) ---",
"",
( .value.prompt
// ([ .value.scope,
"",
"Goal: " + .value.goal,
"",
"Context:", .value.context,
"",
"Constraints:",
(.value.constraints | map("- " + .) | join("\n")),
"",
"Return: " + .value.expected_output ] | join("\n")) ),
"" ),
(if (.sequential | length) > 0 then
"--- Do NOT dispatch these in this round ---", "",
(.sequential[] | "- \(.item)\(if .after != "" then " (after: " + .after + ")" else "" end) - \(.why)"),
""
else empty end),
(if (.integration | length) > 0 then
"--- When they return ---", "", (.integration[] | "- \(.)"), ""
else empty end)' plan.json > dispatch.txt
# dispatch.txt is now ONE message. Paste it whole; do not send it brief by brief.
wc -l dispatch.txt
RISK_ORDER = {"high": 0, "medium": 1, "low": 2}
def brief_prompt(b):
"""Rebuild a complete prompt when the model left `prompt` empty."""
parts = []
if b.get("scope"):
parts.append(b["scope"])
if b.get("goal"):
parts += ["", "Goal: " + b["goal"]]
if b.get("context"):
parts += ["", "Context:", b["context"]]
if b.get("constraints"):
parts += ["", "Constraints:"] + ["- " + c for c in b["constraints"]]
if b.get("expected_output"):
parts += ["", "Return: " + b["expected_output"]]
return "\n".join(parts)
def sort_briefs(plan):
rank = {d["id"]: RISK_ORDER.get(d["risk"], 1) for d in plan["domains"]}
return sorted(plan["briefs"], key=lambda b: (rank.get(b["domain_id"], 1), b["id"]))
def dispatch_block(plan):
briefs = sort_briefs(plan)
out = [f"Dispatch these {len(briefs)} agents in a single message so they run in parallel.", ""]
for i, b in enumerate(briefs, 1):
out += [f"--- Agent {i} of {len(briefs)}: {b['title'] or b['id']} ---", "",
b.get("prompt") or brief_prompt(b), ""]
if plan["sequential"]:
out += ["--- Do NOT dispatch these in this round ---", ""]
for s in plan["sequential"]:
after = f" (after: {s['after']})" if s["after"] else ""
out.append(f"- {s['item']}{after} - {s['why']}")
out.append("")
if plan["integration"]:
out += ["--- When they return ---", ""] + [f"- {s}" for s in plan["integration"]] + [""]
return "\n".join(out)
block = dispatch_block(plan)
with open("dispatch.txt", "w", encoding="utf-8") as fh:
fh.write(block)
# ONE message = parallel. Send `block` as a single turn to your agent runner;
# sending one brief per turn silently serializes the whole plan.
print(block[:400], "...")
const RISK_ORDER = { high: 0, medium: 1, low: 2 };
function briefPrompt(b) {
const L = [];
if (b.scope) L.push(b.scope);
if (b.goal) L.push("", "Goal: " + b.goal);
if (b.context) L.push("", "Context:", b.context);
if (b.constraints?.length) L.push("", "Constraints:", ...b.constraints.map((c) => "- " + c));
if (b.expected_output) L.push("", "Return: " + b.expected_output);
return L.join("\n");
}
function sortBriefs(plan) {
const rank = Object.fromEntries(
plan.domains.map((d) => [d.id, RISK_ORDER[d.risk] ?? 1]));
return [...plan.briefs].sort((a, b) =>
(rank[a.domain_id] ?? 1) - (rank[b.domain_id] ?? 1) || a.id.localeCompare(b.id));
}
function dispatchBlock(plan) {
const briefs = sortBriefs(plan);
const L = [`Dispatch these ${briefs.length} agents in a single message so they run in parallel.`, ""];
briefs.forEach((b, i) => {
L.push(`--- Agent ${i + 1} of ${briefs.length}: ${b.title || b.id} ---`, "",
b.prompt || briefPrompt(b), "");
});
if (plan.sequential.length) {
L.push("--- Do NOT dispatch these in this round ---", "");
for (const s of plan.sequential) {
L.push(`- ${s.item}${s.after ? ` (after: ${s.after})` : ""} - ${s.why}`);
}
L.push("");
}
if (plan.integration.length) {
L.push("--- When they return ---", "", ...plan.integration.map((s) => "- " + s), "");
}
return L.join("\n");
}
const block = dispatchBlock(plan);
writeFileSync("dispatch.txt", block);
// ONE message = parallel. One brief per message = sequential. Send `block` whole.
var riskOrder = map[string]int{"high": 0, "medium": 1, "low": 2}
func briefPrompt(b Brief) string {
var L []string
if b.Scope != "" {
L = append(L, b.Scope)
}
if b.Goal != "" {
L = append(L, "", "Goal: "+b.Goal)
}
if b.Context != "" {
L = append(L, "", "Context:", b.Context)
}
if len(b.Constraints) > 0 {
L = append(L, "", "Constraints:")
for _, c := range b.Constraints {
L = append(L, "- "+c)
}
}
if b.ExpectedOutput != "" {
L = append(L, "", "Return: "+b.ExpectedOutput)
}
return strings.Join(L, "\n")
}
func dispatchBlock(p Plan) string {
rank := map[string]int{}
for _, d := range p.Domains {
r, ok := riskOrder[d.Risk]
if !ok {
r = 1
}
rank[d.ID] = r
}
briefs := append([]Brief(nil), p.Briefs...)
sort.SliceStable(briefs, func(i, j int) bool {
if rank[briefs[i].DomainID] != rank[briefs[j].DomainID] {
return rank[briefs[i].DomainID] < rank[briefs[j].DomainID]
}
return briefs[i].ID < briefs[j].ID
})
var L []string
L = append(L, fmt.Sprintf(
"Dispatch these %d agents in a single message so they run in parallel.", len(briefs)), "")
for i, b := range briefs {
title := b.Title
if title == "" {
title = b.ID
}
prompt := b.Prompt
if prompt == "" {
prompt = briefPrompt(b)
}
L = append(L, fmt.Sprintf("--- Agent %d of %d: %s ---", i+1, len(briefs), title), "", prompt, "")
}
if len(p.Sequential) > 0 {
L = append(L, "--- Do NOT dispatch these in this round ---", "")
for _, s := range p.Sequential {
line := "- " + s.Item
if s.After != "" {
line += " (after: " + s.After + ")"
}
L = append(L, line+" - "+s.Why)
}
L = append(L, "")
}
if len(p.Integration) > 0 {
L = append(L, "--- When they return ---", "")
for _, s := range p.Integration {
L = append(L, "- "+s)
}
L = append(L, "")
}
return strings.Join(L, "\n")
}
os.WriteFile("dispatch.txt", []byte(dispatchBlock(plan)), 0o644)
// ONE message = parallel; one brief per message = sequential.
// Sort briefs by their domain's risk (high, medium, low), tie-break on id, then
// emit ONE block. One message = parallel; one brief per message = sequential.
Map<String, Integer> riskOrder = Map.of("high", 0, "medium", 1, "low", 2);
Map<String, Integer> rank = new HashMap<>();
for (Domain d : plan.domains) rank.put(d.id, riskOrder.getOrDefault(d.risk, 1));
List<Brief> briefs = new ArrayList<>(plan.briefs);
briefs.sort(Comparator
.comparingInt((Brief b) -> rank.getOrDefault(b.domainId, 1))
.thenComparing(b -> b.id));
StringBuilder sb = new StringBuilder();
sb.append("Dispatch these ").append(briefs.size())
.append(" agents in a single message so they run in parallel.\n\n");
for (int i = 0; i < briefs.size(); i++) {
Brief b = briefs.get(i);
sb.append("--- Agent ").append(i + 1).append(" of ").append(briefs.size())
.append(": ").append(b.title.isEmpty() ? b.id : b.title).append(" ---\n\n")
.append(b.prompt.isEmpty() ? rebuildPrompt(b) : b.prompt).append("\n\n");
}
if (!plan.sequential.isEmpty()) {
sb.append("--- Do NOT dispatch these in this round ---\n\n");
for (Seq s : plan.sequential) {
sb.append("- ").append(s.item)
.append(s.after.isEmpty() ? "" : " (after: " + s.after + ")")
.append(" - ").append(s.why).append('\n');
}
sb.append('\n');
}
if (!plan.integration.isEmpty()) {
sb.append("--- When they return ---\n\n");
for (String s : plan.integration) sb.append("- ").append(s).append('\n');
}
Files.writeString(Path.of("dispatch.txt"), sb.toString());
// rebuildPrompt(b) = scope, blank line, "Goal: " + goal, blank line, "Context:" + context,
// blank line, "Constraints:" with one "- " line each, blank line, "Return: " + expectedOutput.
RISK_ORDER = { "high" => 0, "medium" => 1, "low" => 2 }.freeze
def brief_prompt(b)
l = []
l << b["scope"] unless b["scope"].to_s.empty?
l += ["", "Goal: #{b["goal"]}"] unless b["goal"].to_s.empty?
l += ["", "Context:", b["context"]] unless b["context"].to_s.empty?
unless (b["constraints"] || []).empty?
l += ["", "Constraints:"] + b["constraints"].map { |c| "- #{c}" }
end
l += ["", "Return: #{b["expected_output"]}"] unless b["expected_output"].to_s.empty?
l.join("\n")
end
def dispatch_block(plan)
rank = plan["domains"].to_h { |d| [d["id"], RISK_ORDER.fetch(d["risk"], 1)] }
briefs = plan["briefs"].sort_by { |b| [rank.fetch(b["domain_id"], 1), b["id"]] }
l = ["Dispatch these #{briefs.size} agents in a single message so they run in parallel.", ""]
briefs.each_with_index do |b, i|
title = b["title"].to_s.empty? ? b["id"] : b["title"]
l += ["--- Agent #{i + 1} of #{briefs.size}: #{title} ---", "",
b["prompt"].to_s.empty? ? brief_prompt(b) : b["prompt"], ""]
end
unless plan["sequential"].empty?
l += ["--- Do NOT dispatch these in this round ---", ""]
plan["sequential"].each do |s|
after = s["after"].to_s.empty? ? "" : " (after: #{s["after"]})"
l << "- #{s["item"]}#{after} - #{s["why"]}"
end
l << ""
end
unless plan["integration"].empty?
l += ["--- When they return ---", ""] + plan["integration"].map { |s| "- #{s}" } + [""]
end
l.join("\n")
end
File.write("dispatch.txt", dispatch_block(plan))
# ONE message = parallel; one brief per message = sequential.
const RISK_ORDER = ["high" => 0, "medium" => 1, "low" => 2];
function briefPrompt(array $b): string {
$l = [];
if ($b["scope"]) { $l[] = $b["scope"]; }
if ($b["goal"]) { $l[] = ""; $l[] = "Goal: " . $b["goal"]; }
if ($b["context"]) { $l[] = ""; $l[] = "Context:"; $l[] = $b["context"]; }
if (!empty($b["constraints"])) {
$l[] = ""; $l[] = "Constraints:";
foreach ($b["constraints"] as $c) { $l[] = "- $c"; }
}
if ($b["expected_output"]) { $l[] = ""; $l[] = "Return: " . $b["expected_output"]; }
return implode("\n", $l);
}
function dispatchBlock(array $plan): string {
$rank = [];
foreach ($plan["domains"] as $d) {
$rank[$d["id"]] = RISK_ORDER[$d["risk"]] ?? 1;
}
$briefs = $plan["briefs"];
usort($briefs, function ($a, $b) use ($rank) {
return [$rank[$a["domain_id"]] ?? 1, $a["id"]] <=> [$rank[$b["domain_id"]] ?? 1, $b["id"]];
});
$n = count($briefs);
$l = ["Dispatch these $n agents in a single message so they run in parallel.", ""];
foreach ($briefs as $i => $b) {
$title = $b["title"] ?: $b["id"];
$l[] = "--- Agent " . ($i + 1) . " of $n: $title ---";
$l[] = "";
$l[] = $b["prompt"] ?: briefPrompt($b);
$l[] = "";
}
if ($plan["sequential"]) {
$l[] = "--- Do NOT dispatch these in this round ---";
$l[] = "";
foreach ($plan["sequential"] as $s) {
$after = $s["after"] ? " (after: {$s['after']})" : "";
$l[] = "- {$s['item']}$after - {$s['why']}";
}
$l[] = "";
}
if ($plan["integration"]) {
$l[] = "--- When they return ---";
$l[] = "";
foreach ($plan["integration"] as $s) { $l[] = "- $s"; }
}
return implode("\n", $l);
}
file_put_contents("dispatch.txt", dispatchBlock($plan));
// ONE message = parallel; one brief per message = sequential.
var riskOrder = new Dictionary<string, int> { ["high"] = 0, ["medium"] = 1, ["low"] = 2 };
var rank = plan.GetProperty("domains").EnumerateArray().ToDictionary(
d => d.GetProperty("id").GetString()!,
d => riskOrder.GetValueOrDefault(d.GetProperty("risk").GetString() ?? "", 1));
var briefs = plan.GetProperty("briefs").EnumerateArray()
.OrderBy(b => rank.GetValueOrDefault(b.GetProperty("domain_id").GetString() ?? "", 1))
.ThenBy(b => b.GetProperty("id").GetString(), StringComparer.Ordinal)
.ToList();
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Dispatch these {briefs.Count} agents in a single message so they run in parallel.");
sb.AppendLine();
for (var i = 0; i < briefs.Count; i++)
{
var b = briefs[i];
var title = b.GetProperty("title").GetString();
if (string.IsNullOrEmpty(title)) title = b.GetProperty("id").GetString();
var prompt = b.GetProperty("prompt").GetString();
if (string.IsNullOrEmpty(prompt)) prompt = RebuildPrompt(b); // scope/goal/context/…
sb.AppendLine($"--- Agent {i + 1} of {briefs.Count}: {title} ---");
sb.AppendLine();
sb.AppendLine(prompt);
sb.AppendLine();
}
foreach (var s in plan.GetProperty("sequential").EnumerateArray())
{
var after = s.GetProperty("after").GetString();
sb.AppendLine($"- {s.GetProperty("item")}" +
(string.IsNullOrEmpty(after) ? "" : $" (after: {after})") +
$" - {s.GetProperty("why")}");
}
foreach (var s in plan.GetProperty("integration").EnumerateArray())
sb.AppendLine($"- {s.GetString()}");
await File.WriteAllTextAsync("dispatch.txt", sb.ToString());
// ONE message = parallel; one brief per message = sequential.
This is AI-generated planning from pasted text, not a project manager: it sees only what
you sent, never the repository, the real ownership map or how the last fan-out went. Read
assumptions and open_questions before you dispatch, check that
every brief's constraints really carry your ownership rule, and never hand an
agent a brief you have not read yourself.
Step 7 — Store and search past plans
The app declares one collection, plans, with acl_read: owner and
acl_write: user — records belong to the calling identity, so an API
caller using your personal token reads and writes the same history the web app shows.
A collection is used deliberately rather than a single data key:
GET /v1/app-api/data/{key} caches per subject and key for roughly 90 seconds,
so a one-document history silently stops updating after the first read, while
POST /query returns fresh rows immediately after a write.
| Field | Type | What the app puts there |
|---|---|---|
uid | string | The client-side identity of a run, used to union the account rows with this device's rows without duplicating. |
title | string, embedded | plan_name. |
posture | string | parallel-ready | partial-parallel | sequential-first — the field to filter on. |
summary | string, embedded | The plan's verdict, first 400 characters. |
domains_text | string, embedded | The domain names joined with commas, first 400 characters. This is what makes "the one where we split auth from billing" findable, which a title alone never would. |
input_hash | string | The hash of the input that produced this plan — the cheap way to spot that you are about to pay for a plan you already have. |
domains_count, briefs_count | number | Counts, for filtering and for the history line. |
ran_at | timestamp | ISO-8601. Sort on this, descending. |
entry | object | {plan, meta, input} — the whole plan object from step 6, the run metadata, and the input that produced it. It round-trips intact but is not filterable. |
The embed set is title, summary and
domains_text, chosen once and never widened, because the platform does not
backfill vectors: records written before an embed field existed are never vectorized.
Documents are capped at 64 KB, so the app trims before writing — the pasted work
list goes first, then the long-form brief prompts, and the plan itself is kept last because
it is what the user came back for.
# Exact filter: every plan that said do-not-fan-out, newest first. `where` values
# must be operator OBJECTS - a bare value is rejected. Ordering is the `sort`
# object; `order_by` is accepted and then silently ignored.
curl -s -X POST "$API/collections/plans/query" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"where":{"posture":{"eq":"sequential-first"}},
"sort":{"field":"ran_at","dir":"desc"},"limit":20}' \
| jq -r '.data.records[] | "\(.record_id) \(.doc.title) - \(.doc.summary)"'
# Create a record. Note the path: /records, not the collection root, and the
# document goes inside a {"doc": ...} wrapper.
curl -s -X POST "$API/collections/plans/records" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "$(jq -n --slurpfile plan plan.json \
'{doc: {uid: "cli-\(now|floor)",
title: $plan[0].plan_name,
posture: $plan[0].posture,
summary: ($plan[0].verdict[0:400]),
domains_text: ([$plan[0].domains[].name] | join(", ")),
input_hash: "cli",
domains_count: ($plan[0].domains | length),
briefs_count: ($plan[0].briefs | length),
ran_at: (now | todate),
entry: {plan: $plan[0], meta: {model: "gpt-terra"}, input: null}}}')" \
| jq -r '.data.record.record_id'
# Semantic search over title, summary and domains_text. 30 req/min per IP and
# about ten times the cost of the filter above - use `where` when an exact match
# would do.
curl -s -X POST "$API/collections/plans/similar" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"the one where we split auth from billing","limit":8}' \
| jq -r '.data.records[] | "\(.score) \(.doc.title)"'
# Exact filter - cheap, and the right tool whenever the question has an exact answer.
page = api("POST", "/collections/plans/query", {
"where": {"posture": {"eq": "sequential-first"}},
"sort": {"field": "ran_at", "dir": "desc"},
"limit": 20,
})
for rec in page["records"]:
doc = rec["doc"] # records nest under `doc` — never read fields flat
print(rec["record_id"], doc["title"], "|", doc["domains_text"])
# Write one. The path ends in /records, and the document goes inside {"doc": ...}.
created = api("POST", "/collections/plans/records", {"doc": {
"uid": f"py-{int(time.time())}",
"title": plan["plan_name"],
"posture": plan["posture"],
"summary": plan["verdict"][:400],
"domains_text": ", ".join(d["name"] for d in plan["domains"])[:400],
"input_hash": idem,
"domains_count": len(plan["domains"]),
"briefs_count": len(plan["briefs"]),
"ran_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"entry": {"plan": plan, "meta": {"model": "gpt-terra"}, "input": payload},
}})
print("stored as", created["record"]["record_id"])
# Semantic search. Debounce it: 30/min per IP, and vector indexing is async, so a
# `similar` call immediately after a write can lag by a few seconds.
hits = api("POST", "/collections/plans/similar",
{"text": "the one where we split auth from billing", "limit": 8})
for rec in (hits if isinstance(hits, list) else hits["records"]):
print(round(rec["score"], 3), rec["doc"]["title"])
const page = await api("POST", "/collections/plans/query", {
where: { posture: { eq: "sequential-first" } },
sort: { field: "ran_at", dir: "desc" },
limit: 20,
});
for (const rec of page.records) {
const doc = rec.doc; // records nest under `doc` — never read fields flat
console.log(rec.record_id, doc.title, "|", doc.domains_text);
}
// The document goes inside a { doc: … } wrapper; the reply carries `record`.
const created = await api("POST", "/collections/plans/records", {
doc: {
uid: `js-${Date.now()}`,
title: plan.plan_name,
posture: plan.posture,
summary: plan.verdict.slice(0, 400),
domains_text: plan.domains.map((d) => d.name).join(", ").slice(0, 400),
input_hash: idem,
domains_count: plan.domains.length,
briefs_count: plan.briefs.length,
ran_at: new Date().toISOString(),
entry: { plan, meta: { model: "gpt-terra" }, input: payload },
},
});
console.log("stored as", created.record.record_id);
// similar() over the SDK resolves to the record ARRAY; the REST call returns
// {records}. Accept either shape rather than trusting one.
const hits = await api("POST", "/collections/plans/similar",
{ text: "the one where we split auth from billing", limit: 8 });
for (const rec of Array.isArray(hits) ? hits : hits.records) {
console.log(rec.score.toFixed(3), rec.doc.title);
}
query := map[string]any{
"where": map[string]any{"posture": map[string]any{"eq": "sequential-first"}},
"sort": map[string]any{"field": "ran_at", "dir": "desc"},
"limit": 20,
}
var page struct {
Records []struct {
RecordID string `json:"record_id"`
Doc map[string]any `json:"doc"` // records nest under doc
} `json:"records"`
}
if err := call("POST", "/collections/plans/query", query, &page, nil); err != nil {
log.Fatal(err)
}
for _, r := range page.Records {
fmt.Println(r.RecordID, r.Doc["title"], "|", r.Doc["domains_text"])
}
// Create: the path ends in /records, not at the collection root, and the
// document is wrapped in {"doc": ...}. The reply is {"record": {...}}.
names := make([]string, 0, len(plan.Domains))
for _, d := range plan.Domains {
names = append(names, d.Name)
}
record := map[string]any{
"uid": fmt.Sprintf("go-%d", time.Now().Unix()),
"title": plan.PlanName,
"posture": plan.Posture,
"summary": plan.Verdict,
"domains_text": strings.Join(names, ", "),
"input_hash": idem,
"domains_count": len(plan.Domains),
"briefs_count": len(plan.Briefs),
"ran_at": time.Now().UTC().Format(time.RFC3339),
"entry": map[string]any{"plan": plan, "meta": map[string]any{"model": "gpt-terra"}},
}
var created struct {
Record struct {
RecordID string `json:"record_id"`
} `json:"record"`
}
call("POST", "/collections/plans/records",
map[string]any{"doc": record}, &created, nil)
fmt.Println("stored as", created.Record.RecordID)
// Semantic search - note the /similar path and the 30 req/min per-IP limit.
var hits json.RawMessage
call("POST", "/collections/plans/similar",
map[string]any{"text": "the one where we split auth from billing", "limit": 8}, &hits, nil)
fmt.Println(string(hits))
// Filter. Every `where` entry must be an operator object.
String q = "{\"where\":{\"posture\":{\"eq\":\"sequential-first\"}},"
+ "\"sort\":{\"field\":\"ran_at\",\"dir\":\"desc\"},\"limit\":20}";
String page = api("POST", "/collections/plans/query", q, null);
// data.records[] — each row is {record_id, doc:{title, posture, summary, domains_text, …}}
// Read fields off row.doc, never off the row itself.
System.out.println(page);
// Create: the path ends in /records, not at the collection root, and the
// document goes inside {"doc": …}. The reply is {"data":{"record":{"record_id":…}}}.
String record = "{\"doc\":{\"uid\":\"java-" + System.currentTimeMillis() + "\","
+ "\"title\":\"Tool-pipeline refactor fallout — 3-way split\","
+ "\"posture\":\"parallel-ready\","
+ "\"summary\":\"Three failure clusters in three subsystems — fan out to three agents.\","
+ "\"domains_text\":\"Tool abort lifecycle, Batch completion events, Concurrent approval execution\","
+ "\"input_hash\":\"" + idem + "\","
+ "\"domains_count\":3,\"briefs_count\":3,"
+ "\"ran_at\":\"2026-08-12T09:00:00Z\",\"entry\":{\"plan\":" + planJson + "}}}";
api("POST", "/collections/plans/records", record, null);
// Semantic search over title, summary and domains_text — 30/min per IP.
System.out.println(api("POST", "/collections/plans/similar",
"{\"text\":\"the one where we split auth from billing\",\"limit\":8}", null));
page = api("POST", "/collections/plans/query", {
"where" => { "posture" => { "eq" => "sequential-first" } },
"sort" => { "field" => "ran_at", "dir" => "desc" },
"limit" => 20
})
page["records"].each do |r|
doc = r["doc"] # records nest under `doc` — never read fields flat
puts "#{r["record_id"]} #{doc["title"]} | #{doc["domains_text"]}"
end
# The document goes inside {"doc" => ...}; the reply carries "record".
created = api("POST", "/collections/plans/records", { "doc" => {
"uid" => "rb-#{Time.now.to_i}",
"title" => plan["plan_name"],
"posture" => plan["posture"],
"summary" => plan["verdict"][0, 400],
"domains_text" => plan["domains"].map { |d| d["name"] }.join(", ")[0, 400],
"input_hash" => idem,
"domains_count" => plan["domains"].size,
"briefs_count" => plan["briefs"].size,
"ran_at" => Time.now.utc.iso8601,
"entry" => { "plan" => plan, "meta" => { "model" => "gpt-terra" }, "input" => payload }
} })
puts "stored as #{created["record"]["record_id"]}"
hits = api("POST", "/collections/plans/similar",
{ "text" => "the one where we split auth from billing", "limit" => 8 })
records = hits.is_a?(Array) ? hits : hits["records"]
records.each { |r| puts "#{r["score"].round(3)} #{r["doc"]["title"]}" }
$page = api("POST", "/collections/plans/query", [
"where" => ["posture" => ["eq" => "sequential-first"]],
"sort" => ["field" => "ran_at", "dir" => "desc"],
"limit" => 20,
]);
foreach ($page["records"] as $rec) {
$doc = $rec["doc"]; // records nest under `doc` — never read fields flat
echo $rec["record_id"], " ", $doc["title"], " | ", $doc["domains_text"], "\n";
}
// The document goes inside ["doc" => ...]; the reply carries "record".
$created = api("POST", "/collections/plans/records", ["doc" => [
"uid" => "php-" . time(),
"title" => $plan["plan_name"],
"posture" => $plan["posture"],
"summary" => substr($plan["verdict"], 0, 400),
"domains_text" => substr(implode(", ", array_column($plan["domains"], "name")), 0, 400),
"input_hash" => $idem,
"domains_count" => count($plan["domains"]),
"briefs_count" => count($plan["briefs"]),
"ran_at" => gmdate("c"),
"entry" => ["plan" => $plan, "meta" => ["model" => "gpt-terra"], "input" => $payload],
]]);
echo "stored as ", $created["record"]["record_id"], "\n";
$hits = api("POST", "/collections/plans/similar",
["text" => "the one where we split auth from billing", "limit" => 8]);
foreach ($hits["records"] ?? $hits as $rec) {
printf("%.3f %s\n", $rec["score"], $rec["doc"]["title"]);
}
var page = await SkillSafe.ApiAsync(HttpMethod.Post, "/collections/plans/query", new
{
where = new { posture = new { eq = "sequential-first" } },
sort = new { field = "ran_at", dir = "desc" },
limit = 20
});
foreach (var rec in page.GetProperty("records").EnumerateArray())
{
var doc = rec.GetProperty("doc"); // records nest under `doc`
Console.WriteLine($"{rec.GetProperty("record_id")} {doc.GetProperty("title")}");
}
// The document goes inside a { doc = … } wrapper; the reply carries `record`.
var created = await SkillSafe.ApiAsync(HttpMethod.Post, "/collections/plans/records", new
{
doc = new
{
uid = $"cs-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
title = plan.GetProperty("plan_name").GetString(),
posture = plan.GetProperty("posture").GetString(),
summary = plan.GetProperty("verdict").GetString(),
domains_text = string.Join(", ", plan.GetProperty("domains").EnumerateArray()
.Select(d => d.GetProperty("name").GetString())),
input_hash = idem,
domains_count = plan.GetProperty("domains").GetArrayLength(),
briefs_count = plan.GetProperty("briefs").GetArrayLength(),
ran_at = DateTime.UtcNow.ToString("o"),
entry = new { plan, meta = new { model = "gpt-terra" } }
}
});
Console.WriteLine($"stored as {created.GetProperty("record").GetProperty("record_id")}");
var hits = await SkillSafe.ApiAsync(HttpMethod.Post, "/collections/plans/similar",
new { text = "the one where we split auth from billing", limit = 8 });
Console.WriteLine(hits.ToString());
Four traps worth knowing before you write the integration. Records nest under
doc — query, get and similar all
return {record_id, doc: {…}}, so reading rec.title gets
you undefined and no error. Every where entry must be an
operator object: {"posture":"sequential-first"} is rejected,
{"posture":{"eq":"sequential-first"}} is right. Ordering is the
sort object; order_by is accepted and then silently ignored,
leaving you with created_at desc. And record creation posts to
/collections/plans/records, not to the collection root, with the document
inside a {"doc": …} wrapper — the reply is
{"data":{"record":{"record_id":"rec_…"}}}, so the new id is at
data.record.record_id and not one level up.
Data endpoints share 120 requests/min; /collections/{name}/similar is 30/min
per IP and costs roughly an order of magnitude more than a where filter —
use the filter whenever an exact match would do, and never fire a similarity query per
keystroke. Vector indexing is asynchronous, so a similar call immediately
after a write can lag by seconds. There is no backfill: records written before an
embed field existed are never vectorized. Storage quotas that matter here:
64 KB per document, 10,000 records per collection, 1,000 records per owner.
A plan you already paid for is the cheapest plan there is. Before running, hash the input
the way step 5 does and query input_hash for an exact match — if the
work list has not changed, restore the stored entry.plan instead of spending
credits on the same answer twice.
A closing word on prescan_facts
prescan_facts is optional for API callers. Omit it, or send
{"resources": [], "flags": []}, and the plan comes back just as well reasoned
— only coverage_check arrives empty. But that field is the one part of
the reply that is checkable rather than merely readable, and it exists solely
because you gave it something to reconcile. Supplying prescan_facts is what
turns "the plan looks sensible" into "the plan accounted for all eleven things my scanner
found, and said in writing why it set two of them aside". Every flag id you send comes back
in coverage_check exactly once, confirmed or explicitly set aside; every work
item you send in resources has to turn up in inventory, and
through it in a domain, a brief or sequential.
The full semantics, with an example id for each family, are in the two tables in step 3. The eleven flag families, for reference while you are writing the matcher:
shared-file— one file appears under two or more items.same-error— the same error signature repeats across items.shared-fixture— a conftest, setup file, factory or test helper is in play.ordering-dep— the text says "after", "depends on", "blocked by", "once X lands".serialized-resource— a migration, schema file, lockfile, snapshot or generated artifact.shared-env— a shared port, database or environment variable.thin-item— an item with no file, no error text and no acceptance criterion.too-broad— an item phrased too broadly to scope.timing-hint— a timing, race, flake, timeout or deadlock hint.single-domain— everything is one item, or everything is in one file.plain-secret— a credential-looking literal is in the paste.
Ids are <family>:<slug>, the slug being a lowercased, hyphenated
form of whatever the family matched. You do not have to reproduce the app's slugs exactly;
you have to be consistent, because the id you send is the id that comes back. A useful CI
gate is then three assertions: every id you sent appears in coverage_check, no
id you consider blocking came back addressed: false, and
posture is not sequential-first — a check that refuses to
fan out work the planner said should not be fanned out.