Driving Change Desk from your own code
Everything the web page does is one HTTP API. The base URL is
https://api.skillsafe.ai/v1/app-api, every call carries
Authorization: Bearer <token>, and every response is the same envelope.
The task field comes first
Change Desk is one app with three lanes over the same change. Every request must carry a
task field; it is what routes the run. The three values are:
| task | What you get | Derived from |
|---|---|---|
impact | The change request: blast radius, risk assessment, the rollback plan, the approvals to request. | @anthropics/change-request |
checklist | The pre-deploy verification checklist: gates in time order, each with an owner and an observable pass criterion. | @anthropics/deploy-checklist |
runbook | The operational runbook: preconditions, the numbered procedure with commands, verification, rollback, escalation. | @anthropics/runbook |
If task is missing or unrecognised the model picks the closest lane and names its
choice in lane and in the first sentence of summary - it never blends two
lanes into one answer. Read lane off the reply rather than assuming it echoes what you
sent.
The input fields
| Field | Type | Required | Meaning |
|---|---|---|---|
task | string | yes | The lane: impact, checklist or runbook. |
change | string | yes | The change itself. The web page sends a parsed digest (see below); the API accepts either that or a raw diff. |
intent | string | no | Why the change exists, in the author's words. Its absence is itself a finding. |
environment | string | no | production, staging-then-production, multi-region or customer-managed. |
window | string | no | business-hours, low-traffic, maintenance-window, freeze or emergency. A freeze or an emergency changes the answer. |
audience | string | no | approver, author or oncall. |
context | string | no | Table sizes, traffic shape, deploy duration, ownership - anything the diff cannot show. |
prescan_facts | object | no | {risk_tier, flags[], resources[], stats}. Facts from a parser. Every flags[].id you send must come back exactly once in coverage_check. |
upstream | string | no | A previous lane's result, when you are chaining lanes. The next lane builds on it instead of starting again. |
retry_note | string | no | Only when a previous reply was malformed. The web page sets this on its single automatic reformat retry. |
What change should contain
The browser parses the paste and sends a digest rather than the raw text: file headers with their classification, per-statement lock and reversibility lines, and the diff bodies. You can send a raw unified diff instead and it will work, but you lose the statement classification the model is asked to reason against - so if you are automating this, run your own parse and send the header lines too. This is the shape the web page sends:
# change: 1 file, +3 / -0, 1 area
# areas: db
# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking
# rollback block in the migration: ABSENT
# prescan risk tier: severe
# --- stated intent (the author's own words)
Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.
# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0
# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders
# statement 2 [create-index] lock=SHARE reversible=reversible table=orders
+ALTER TABLE orders DROP COLUMN legacy_ref;
+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);
The envelope
Every response is {"ok":true,"data":{...}} or
{"ok":false,"error":{"code":"...","message":"..."}}. Check ok before
reading data; the HTTP status mirrors it but the code is what you branch on.
| HTTP | code | What it means here |
|---|---|---|
| 400 | VALIDATION_ERROR | The body was not the shape the endpoint expects. Usually a missing task or a non-string change. |
| 401 | UNAUTHORIZED | No token, or a token that has expired. Mint a new one on the token page. |
| 402 | INSUFFICIENT_CREDITS | The balance is below min_credits. Call /estimate first and compare against /me - the web page disables its run button rather than letting this happen. |
| 403 | FORBIDDEN | A guest token on a metered run. Sign in for a personal token. |
| 404 | NOT_FOUND | A job id that does not exist, or a path that is not part of this API. |
| 429 | RATE_LIMITED | Back off. Do not tight-loop; the budget is shared across every caller from your IP. |
| 503 | UNAVAILABLE | The model tier is briefly unavailable. Retry with the same idempotency key. |
Step 1 — get a token
Open the token page, sign in, and press Copy shell
export. That gives you CD_TOKEN in your shell. A guest token is
enough for /me and /estimate; running a lane is metered and needs a
personal token. Never paste a token into source control - read it from your own secret store and
keep the placeholder "YOUR_TOKEN" in the samples below.
There is one endpoint that names the app rather than inferring it from the token:
POST /guest, whose body is {"slug":"change-desk"}. Every other call in
this document takes its app identity from the token, so there is no slug in the path.
Step 2 — check the token with /me
GET /me is free. It tells you whether the token is a guest or a person, and what the
balance is - which is what the web page uses to disable the run button before a 402 can happen.
Every later sample reuses the call() helper defined here.
# Every call carries the token. Keep it in your shell, never in source control.
export CD_TOKEN="YOUR_TOKEN" # from https://change-desk.skillsafe.ai/tokens.html
curl -s "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $CD_TOKEN"
import json, urllib.request
TOKEN = "YOUR_TOKEN" # from https://change-desk.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(BASE + path, data=data,
method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + TOKEN)
if data:
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
env = json.load(r)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
print(call("/me"))
const TOKEN = "YOUR_TOKEN"; // from https://change-desk.skillsafe.ai/tokens.html
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
"Authorization": `Bearer ${TOKEN}`,
...(payload ? { "Content-Type": "application/json" } : {})
},
body: payload ? JSON.stringify(payload) : undefined
});
const env = await res.json();
if (!env.ok) throw new Error(`${env.error.code}: ${env.error.message}`);
return env.data;
}
console.log(await call("/me"));
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
const token = "YOUR_TOKEN" // from https://change-desk.skillsafe.ai/tokens.html
const base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, payload []byte) (json.RawMessage, error) {
method := "GET"
var body io.Reader
if payload != nil {
method, body = "POST", bytes.NewReader(payload)
}
req, _ := http.NewRequest(method, base+path, body)
req.Header.Set("Authorization", "Bearer "+token)
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, errors.New(env.Error.Code + ": " + env.Error.Message)
}
return env.Data, nil
}
func main() {
me, err := call("/me", nil)
if err != nil {
panic(err)
}
fmt.Println(string(me))
}
import java.net.URI;
import java.net.http.*;
public class ChangeDesk {
static final String TOKEN = "YOUR_TOKEN"; // from https://change-desk.skillsafe.ai/tokens.html
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b.GET();
} else {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.body());
return res.body(); // {"ok":true,"data":{...}}
}
public static void main(String[] args) throws Exception {
System.out.println(call("/me", null));
}
}
require "json"
require "net/http"
require "uri"
TOKEN = "YOUR_TOKEN" # from https://change-desk.skillsafe.ai/tokens.html
BASE = "https://api.skillsafe.ai/v1/app-api"
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
puts call("/me")
<?php
$TOKEN = "YOUR_TOKEN"; // from https://change-desk.skillsafe.ai/tokens.html
$BASE = "https://api.skillsafe.ai/v1/app-api";
function call($path, $payload = null) {
global $TOKEN, $BASE;
$ch = curl_init($BASE . $path);
$headers = ["Authorization: Bearer " . $TOKEN];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$env = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($env["ok"])) {
throw new Exception($env["error"]["code"] . ": " . $env["error"]["message"]);
}
return $env["data"];
}
print_r(call("/me"));
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ChangeDesk {
const string Token = "YOUR_TOKEN"; // from https://change-desk.skillsafe.ai/tokens.html
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly HttpClient Http = new HttpClient();
static async Task<string> Call(string path, string payload = null) {
var req = new HttpRequestMessage(payload == null ? HttpMethod.Get : HttpMethod.Post, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (payload != null) req.Content = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
return body; // {"ok":true,"data":{...}}
}
static async Task Main() {
Console.WriteLine(await Call("/me"));
}
}
Step 3 — price the run with /estimate
POST /estimate is free, starts no job and charges nothing. It returns
model, model_alias, markup_bps, hold_credits,
min_credits and sponsor_enabled. Estimate the lane you are about
to run: the three lanes have different prompts and output caps, so
hold_credits differs between them and lane A's price is not lane B's.
hold_credits is a reservation, not a price. It reserves the full output cap;
the settled charged_credits is usually far lower.
# free: no job is started and nothing is charged
curl -s "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "impact", "change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);", "intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.", "environment": "production", "window": "low-traffic", "audience": "approver", "context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.", "prescan_facts": {"risk_tier": "severe", "flags": [{"id": "CD-DESTRUCTIVE", "severity": "critical", "detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"}, {"id": "CD-INDEX-BLOCKING", "severity": "high", "detail": "1 CREATE INDEX without CONCURRENTLY"}, {"id": "CD-LOCK", "severity": "high", "detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"}, {"id": "CD-NO-ROLLBACK", "severity": "high", "detail": "No down-migration or rollback block found in 1 migration file"}], "resources": [], "stats": {"files": 1, "adds": 3, "dels": 0, "areas": 1, "statements": 2, "destructive": 1, "blocking": 2, "migrations": 1, "flags": 4, "blockers": 4}}}'
# free: no job is started and nothing is charged
payload = json.loads(r"""
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""")
print(call("/estimate", payload))
# free: no job is started and nothing is charged
const payload = {
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
};
console.log(await call("/estimate", payload));
# free: no job is started and nothing is charged
payload := []byte(`{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}`)
out, err := call("/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(out))
# free: no job is started and nothing is charged
String payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
System.out.println(call("/estimate", payload));
# free: no job is started and nothing is charged
payload = JSON.parse(<<~JSON)
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON
puts call("/estimate", payload)
# free: no job is started and nothing is charged
$payload = json_decode(<<<'JSON'
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON, true);
print_r(call("/estimate", $payload));
# free: no job is started and nothing is charged
var payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
Console.WriteLine(await Call("/estimate", payload));
Step 4 — run a lane and poll the job
POST /run returns {"job_id":"job_..."} immediately. Poll
GET /jobs/{job_id} until status is succeeded or
failed. The reply text is at data.output.output and is the JSON object the
next section describes.
Send an idempotency key. Put it in the Idempotency-Key header and
derive it from the lane plus a hash of the input, exactly as the web page does
(change-desk:impact:<hash>:a1). Two lanes over the same change are two distinct
runs and must not share a key; a retry after a network blip must reuse the same key, or you pay
twice for one answer.
# metered: this one charges credits
JOB=$(curl -s "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: change-desk:impact:$(echo -n "$CHANGE" | shasum | cut -c1-16):a1" \
-d @request.json | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['job_id'])")
# poll until it reaches a terminal state
until [ "$(curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $CD_TOKEN" \
| python3 -c "import json,sys;print(json.load(sys.stdin)['data']['status'])")" != "running" ]; do
sleep 2
done
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H "Authorization: Bearer $CD_TOKEN" \
| python3 -c "import json,sys;print(json.load(sys.stdin)['data']['output']['output'])"
import time
payload = json.loads(r"""
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""")
job = call("/run", payload)["job_id"]
while True:
state = call("/jobs/" + job)
if state["status"] in ("succeeded", "failed"):
break
time.sleep(2)
reply = json.loads(state["output"]["output"]) # the object documented below
print(reply["verdict"], reply["risk_tier"], len(reply["steps"]), "steps")
# metered: this one charges credits
const payload = {
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
};
console.log(await call("/run", payload));
# metered: this one charges credits
payload := []byte(`{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}`)
out, err := call("/run", payload)
if err != nil {
panic(err)
}
fmt.Println(string(out))
# metered: this one charges credits
String payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
System.out.println(call("/run", payload));
# metered: this one charges credits
payload = JSON.parse(<<~JSON)
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON
puts call("/run", payload)
# metered: this one charges credits
$payload = json_decode(<<<'JSON'
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON, true);
print_r(call("/run", $payload));
# metered: this one charges credits
var payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
Console.WriteLine(await Call("/run", payload));
Step 5 — stream it instead
POST /run-stream is the same request with an SSE response, which is what the web page
uses so the progress card can advance on real signal. Events arrive as
data: {"delta":"..."} lines, then a terminal event carrying the job. Accumulate the
deltas: the concatenation is the same JSON object /run would have returned. If the
stream breaks midway, the accumulated text is still worth parsing - the web page closes the JSON
at the last structurally sound point and renders whatever sections arrived.
curl -N -s "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: change-desk:runbook:$(echo -n "$CHANGE" | shasum | cut -c1-16):a1" \
-d @request.json
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(payload).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "text/event-stream")
req.add_header("Idempotency-Key", "change-desk:runbook:" + input_hash + ":a1")
raw = ""
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if not line.startswith("data:"):
continue
event = json.loads(line[5:].strip())
if "delta" in event:
raw += event["delta"]
reply = json.loads(raw)
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": `change-desk:runbook:${inputHash}:a1`
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", raw = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const ev = JSON.parse(line.slice(5).trim());
if (ev.delta) raw += ev.delta;
}
}
const reply = JSON.parse(raw);
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", "change-desk:runbook:"+inputHash+":a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var raw strings.Builder
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var ev struct{ Delta string `json:"delta"` }
if json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &ev) == nil {
raw.WriteString(ev.Delta)
}
}
fmt.Println(raw.String())
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", "change-desk:runbook:" + inputHash + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
StringBuilder raw = new StringBuilder();
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (!line.startsWith("data:")) return;
String json = line.substring(5).trim();
int i = json.indexOf("\"delta\":\"");
if (i >= 0) raw.append(json.substring(i + 9, json.lastIndexOf('"')));
});
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Idempotency-Key"] = "change-desk:runbook:#{input_hash}:a1"
req.body = JSON.dump(payload)
raw = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
ev = JSON.parse(line[5..].strip) rescue next
raw << ev["delta"].to_s
end
end
end
end
reply = JSON.parse(raw)
$raw = "";
$ch = curl_init($BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $TOKEN,
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: change-desk:runbook:" . $inputHash . ":a1",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$raw) {
foreach (explode("\n", $chunk) as $line) {
if (strpos($line, "data:") !== 0) continue;
$ev = json_decode(trim(substr($line, 5)), true);
if (isset($ev["delta"])) $raw .= $ev["delta"];
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$reply = json_decode($raw, true);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", $"change-desk:runbook:{inputHash}:a1");
req.Content = new StringContent(payload, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new System.IO.StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string line;
while ((line = await reader.ReadLineAsync()) != null) {
if (!line.StartsWith("data:")) continue;
using var doc = System.Text.Json.JsonDocument.Parse(line.Substring(5).Trim());
if (doc.RootElement.TryGetProperty("delta", out var d)) raw.Append(d.GetString());
}
The output contract
The reply is one JSON object. The shape is identical in all three lanes - only
steps, detail_table and artifact carry lane-specific
content. These are the fields the web page's renderer actually reads; anything else in the object
is ignored.
| Field | Type | Meaning |
|---|---|---|
lane | string | The lane that was answered. Read this rather than assuming it echoes task. |
title | string | A short name for the change. |
headline | string | One sentence an approver could read alone. |
verdict | string | ready, needs-work or blocked. |
risk_tier | string | low, moderate, high or severe. The model's own judgement, not a copy of the prescan's. |
summary | string | Two to five sentences. |
checks[] | array | {id, name, status, evidence, requirement}. status is pass, warn, fail or unknown. |
findings[] | array | {id, severity, area, problem, evidence, fix}. severity is critical, high, medium or low. |
steps[] | array | {n, phase, title, detail, owner, command, expected, abort}. Lane-specific meaning - see below. |
detail_table | object | {title, columns[], rows[][]}. Rows are arrays of strings in column order. |
artifact | object | {format, filename, title, content}. content is the complete document in Markdown. |
coverage_check[] | array | {flag_id, status, note}, one per prescan flag you sent. status is confirmed, cleared or not-applicable. |
assumptions[] | array | Strings. |
open_questions[] | array | Strings. |
What the lane-specific parts mean:
| Lane | <code>steps[]</code> are | <code>steps[].phase</code> | <code>detail_table</code> is | <code>artifact.filename</code> |
|---|---|---|---|---|
impact | the rollback plan | detect → decide → revert → verify | the blast radius, columns [Area, What changes, Who notices first, Evidence] | CHANGE-REQUEST.md |
checklist | the gates, in time order | T-24h, T-1h, T-15m, T-0, T+15m, T+1h, T+24h | the sign-offs, columns [Role, Signing off on, Before which gate] | DEPLOY-CHECKLIST.md |
runbook | the procedure, numbered | precondition → execute → verify → rollback | the escalation path, columns [If this happens, Escalate to, How, Within] | RUNBOOK.md |
Two contract details worth automating against. In the checklist lane every gate
carries an expected that is observable - a gate without one is a defect, not a style
choice. In the runbook lane exactly one step's abort marks the point of
no return; if the change contains nothing irreversible, the prompt says so explicitly instead.
Worked example: each lane over the same change
The three requests below differ only in task and audience. That is the
whole point of the lane router: one work object, three documents.
Lane impact — the change request
curl -s "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "impact", "change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);", "intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.", "environment": "production", "window": "low-traffic", "audience": "approver", "context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.", "prescan_facts": {"risk_tier": "severe", "flags": [{"id": "CD-DESTRUCTIVE", "severity": "critical", "detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"}, {"id": "CD-INDEX-BLOCKING", "severity": "high", "detail": "1 CREATE INDEX without CONCURRENTLY"}, {"id": "CD-LOCK", "severity": "high", "detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"}, {"id": "CD-NO-ROLLBACK", "severity": "high", "detail": "No down-migration or rollback block found in 1 migration file"}], "resources": [], "stats": {"files": 1, "adds": 3, "dels": 0, "areas": 1, "statements": 2, "destructive": 1, "blocking": 2, "migrations": 1, "flags": 4, "blockers": 4}}}'
payload = json.loads(r"""
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""")
print(call("/run", payload))
const payload = {
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
};
console.log(await call("/run", payload));
payload := []byte(`{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}`)
out, err := call("/run", payload)
if err != nil {
panic(err)
}
fmt.Println(string(out))
String payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
System.out.println(call("/run", payload));
payload = JSON.parse(<<~JSON)
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON
puts call("/run", payload)
$payload = json_decode(<<<'JSON'
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON, true);
print_r(call("/run", $payload));
var payload = """
{
"task": "impact",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "approver",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
Console.WriteLine(await Call("/run", payload));
The reply's shape, abbreviated:
{
"lane": "impact",
"title": "Drop orders.legacy_ref and index status_v2",
"headline": "An irreversible column drop and a blocking index build on a 41-million-row table, with no down-migration.",
"verdict": "needs-work",
"risk_tier": "severe",
"summary": "...",
"checks": [
{
"id": "C1",
"name": "Rollback path exists",
"status": "fail",
"evidence": "db/migrations/0042_drop_legacy_ref.sql carries no down block",
"requirement": "A restore procedure, since DROP COLUMN cannot be reverted"
}
],
"findings": [
{
"id": "F1",
"severity": "critical",
"area": "db",
"problem": "DROP COLUMN legacy_ref destroys the column's data",
"evidence": "statement 1 [drop-column] reversible=irreversible DESTRUCTIVE",
"fix": "Confirm the restore point before running, and deploy the code that stops reading it first"
}
],
"steps": [
{
"n": 1,
"phase": "detect",
"title": "Watch the orders read path",
"owner": "the on-call for orders",
"expected": "error rate returns to baseline within 5 minutes",
"abort": "..."
}
],
"detail_table": {
"title": "Blast radius",
"columns": [
"Area",
"What changes",
"Who notices first",
"Evidence"
],
"rows": [
[
"db",
"orders loses legacy_ref",
"any reader still selecting it",
"statement 1"
]
]
},
"artifact": {
"format": "markdown",
"filename": "CHANGE-REQUEST.md",
"title": "The change request",
"content": "# Change request\\n..."
},
"coverage_check": [
{
"flag_id": "CD-DESTRUCTIVE",
"status": "confirmed",
"note": "drives F1"
}
],
"assumptions": [
"..."
],
"open_questions": [
"..."
]
}
Lane checklist — the pre-deploy checklist
curl -s "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "checklist", "change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);", "intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.", "environment": "production", "window": "low-traffic", "audience": "author", "context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.", "prescan_facts": {"risk_tier": "severe", "flags": [{"id": "CD-DESTRUCTIVE", "severity": "critical", "detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"}, {"id": "CD-INDEX-BLOCKING", "severity": "high", "detail": "1 CREATE INDEX without CONCURRENTLY"}, {"id": "CD-LOCK", "severity": "high", "detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"}, {"id": "CD-NO-ROLLBACK", "severity": "high", "detail": "No down-migration or rollback block found in 1 migration file"}], "resources": [], "stats": {"files": 1, "adds": 3, "dels": 0, "areas": 1, "statements": 2, "destructive": 1, "blocking": 2, "migrations": 1, "flags": 4, "blockers": 4}}}'
payload = json.loads(r"""
{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""")
print(call("/run", payload))
const payload = {
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
};
console.log(await call("/run", payload));
payload := []byte(`{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}`)
out, err := call("/run", payload)
if err != nil {
panic(err)
}
fmt.Println(string(out))
String payload = """
{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
System.out.println(call("/run", payload));
payload = JSON.parse(<<~JSON)
{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON
puts call("/run", payload)
$payload = json_decode(<<<'JSON'
{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON, true);
print_r(call("/run", $payload));
var payload = """
{
"task": "checklist",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "author",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
Console.WriteLine(await Call("/run", payload));
Lane runbook — the runbook
curl -s "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $CD_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "runbook", "change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);", "intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.", "environment": "production", "window": "low-traffic", "audience": "oncall", "context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.", "prescan_facts": {"risk_tier": "severe", "flags": [{"id": "CD-DESTRUCTIVE", "severity": "critical", "detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"}, {"id": "CD-INDEX-BLOCKING", "severity": "high", "detail": "1 CREATE INDEX without CONCURRENTLY"}, {"id": "CD-LOCK", "severity": "high", "detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"}, {"id": "CD-NO-ROLLBACK", "severity": "high", "detail": "No down-migration or rollback block found in 1 migration file"}], "resources": [], "stats": {"files": 1, "adds": 3, "dels": 0, "areas": 1, "statements": 2, "destructive": 1, "blocking": 2, "migrations": 1, "flags": 4, "blockers": 4}}}'
payload = json.loads(r"""
{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""")
print(call("/run", payload))
const payload = {
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
};
console.log(await call("/run", payload));
payload := []byte(`{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}`)
out, err := call("/run", payload)
if err != nil {
panic(err)
}
fmt.Println(string(out))
String payload = """
{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
System.out.println(call("/run", payload));
payload = JSON.parse(<<~JSON)
{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON
puts call("/run", payload)
$payload = json_decode(<<<'JSON'
{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
JSON, true);
print_r(call("/run", $payload));
var payload = """
{
"task": "runbook",
"change": "# change: 1 file, +3 / -0, 1 area\n# areas: db\n# migrations: 1 file, 2 statements, 1 destructive, 2 table-blocking\n# rollback block in the migration: ABSENT\n# prescan risk tier: severe\n\n# --- stated intent (the author's own words)\nRetire orders.legacy_ref now that status_v2 has been dual-written for six weeks.\n\n# --- file 1: db/migrations/0042_drop_legacy_ref.sql [database migration, new file] +3/-0\n# statement 1 [drop-column] lock=ACCESS EXCLUSIVE reversible=irreversible DESTRUCTIVE table=orders\n# statement 2 [create-index] lock=SHARE reversible=reversible table=orders\n+ALTER TABLE orders DROP COLUMN legacy_ref;\n+CREATE INDEX idx_orders_status_v2 ON orders (status_v2);",
"intent": "Retire orders.legacy_ref now that status_v2 has been dual-written for six weeks.",
"environment": "production",
"window": "low-traffic",
"audience": "oncall",
"context": "orders is 41 million rows. A deploy takes 6 minutes to roll all pods.",
"prescan_facts": {
"risk_tier": "severe",
"flags": [
{
"id": "CD-DESTRUCTIVE",
"severity": "critical",
"detail": "1 statement destroys data and cannot be undone by re-running the migration (ALTER TABLE ... DROP COLUMN)"
},
{
"id": "CD-INDEX-BLOCKING",
"severity": "high",
"detail": "1 CREATE INDEX without CONCURRENTLY"
},
{
"id": "CD-LOCK",
"severity": "high",
"detail": "2 statements take a table-blocking lock (ACCESS EXCLUSIVE, SHARE)"
},
{
"id": "CD-NO-ROLLBACK",
"severity": "high",
"detail": "No down-migration or rollback block found in 1 migration file"
}
],
"resources": [],
"stats": {
"files": 1,
"adds": 3,
"dels": 0,
"areas": 1,
"statements": 2,
"destructive": 1,
"blocking": 2,
"migrations": 1,
"flags": 4,
"blockers": 4
}
}
}
""";
Console.WriteLine(await Call("/run", payload));
Chaining the lanes
The web page's handoff buttons do one thing: they put a compact digest of the finished lane into
the next request's upstream field, keeping change identical. Do the same
and the checklist builds on the change request rather than re-deriving it. Its
verdict constrains the next lane's - a lane will not report ready over an
upstream blocked without saying in summary what changed.
# upstream lane: impact
# verdict: needs-work ยท risk_tier: severe
# title: Drop orders.legacy_ref and index status_v2
## findings
- [critical] F1 DROP COLUMN legacy_ref destroys the column's data -> confirm the restore point first
## rollback plan
- 1. [detect] Watch the orders read path (passes when: error rate at baseline)
Rate limits and cost
The data endpoints share a 120 requests-per-minute budget per IP;
/collections/{name}/similar is tighter at 30 a minute and costs roughly an order of
magnitude more than a filtered query, so debounce it. /me, /estimate and
/guest are free. Only /run, /run-stream and session turns
are metered.
Reading the app's own contract
llms.txt is the machine-readable summary of what this app does, its lanes, its input fields and its output contract. If you are pointing an agent at Change Desk, that is the file to give it.