Drive Same Bones from your own code
Both metered lanes are available over HTTP: the X-ray (what makes the original work - its emotional journey, what is underneath, its central tension and peak moments - who holds power, every part of your notes sorted into transfer, translate or replace with the job it does and a proposed new form, the pressures the new setting adds, a logline, risks and questions) and the pitch verdict (a remake pitch read against the original: transformed, reskin, fidelity trap or genre collision, with four checks, bone-by-bone swaps marked rebuilt, surface or forced, fixes and a revised logline). A natural use is a writing tool, a fan-fiction or game-jam workflow, or a writers' room that wants a "is this a real remake?" pass before drafting.
The division of labour first: the page splits and sorts; the model reads; the page re-checks every bone and every swap. The numbered bones, their first keyword sort (which a person can change), the setting's pressures and tone check, and the pitch's word-for-word carry-over (names, one-for-one swaps, "but in" framing, period words kept) come from bonekit.js, which runs in node as well as in the browser. Its check function holds each reply to the person's own notes: one bone per part, swaps that point at words the pitch really uses, verdict rules that hold, no internal field names and no gendered pronoun the notes never used. The page's sorts are keyword guesses and say so.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"same-bones"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task / source_notes / setting is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Every run sends task: xray or verdict. Both share the case-file fields; the verdict adds the pitch. The model follows only the named task's contract, and decides each verdict by an explicit check order.
| Task | Verdicts (in check order) | Reply fields |
|---|---|---|
xray | clash (the setting or form makes the core moments land wrong), thin (the setting names a place but adds no pressure), rich (otherwise - at least two pressures) | task, verdict, title, summary, core {journey, appeal, tension, peaks}, power, bones [{n, part, kind, function, new_form}] (one per page part), pressures [{pressure, conflict}], logline, risks [{pattern, text}], questions |
verdict | genre_collision (key moments land in a tone the pitch does not own), fidelity_trap (something kept unchanged that the new world would not have), reskin (one-for-one swaps, same conflict), transformed (otherwise) | task, verdict, title, summary, checks (4: systemic consistency, emotional core, originality, cultural care - each {check, result: pass|mixed|fail, note}), swaps [{from, to, kind: rebuilt|surface|forced, note}], fixes (2-4), stands_alone, logline |
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (BoneKit.toWire does this). The schema requires task, source_notes and setting.
Input fields
| Field | Lane | Type | Notes |
|---|---|---|---|
task | both | string | xray or verdict. |
source | both | string | The original's name as typed. |
source_notes | both | string | The person's notes on the original - one line per character, turn or moment, and what they love. Past 3,000 characters the middle is cut with a marker. The model works only from these. |
setting, form, keep | both | string | The new setting; what the remake will be (film, novel, series...); must-keeps that the reply may rebuild but never cut. |
page_bones | both | string (JSON) | [{n, part, kind}] from BoneKit.bones: at most 10 numbered parts, each with the page's first sort or the person's re-sort. |
page_flags, page_verdict | both | string (JSON), string | [{level, text}] with level fix / warn / info; the page's own reading - rich / thin / clash for an X-ray, transformed / reskin / fidelity_trap / genre_collision for a verdict. |
pitch | verdict | string | The remake pitch; past 4,000 characters the middle is cut. |
page_pitch | verdict | string (JSON) | {carried_names, swap_lines, but_in, notes_overlap, setting_overlap, literal_keeps, tone_clash} from BoneKit.readPitch. |
retry_note | both | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with BoneKit.parseReply, then normalize(reply, task), then check(reply, state, task), which returns {notes}: the page's notes (info, warn) shown under "The page's check".
Worked examples
Replies produced against the app's prompt for the four sample films (public-domain originals, made-up pitches; lists shortened; the full replies ship in example-result.js). Together they cover every verdict of both lanes. The page replays each of them free from its sample buttons.
task: xray - Frankenstein into a deep-sea habitat - rich
Input:
{
"task": "xray",
"source": "Frankenstein (Mary Shelley, 1818)",
"source_notes": "Victor, a brilliant student, builds a living creature out of dead parts and runs from it the moment it wakes.\nThe creatu ... (674 characters)",
"setting": "A sealed research habitat on the floor of the Pacific in 2140, run by a mining company, with rationed air and power and ... (160 characters)",
"form": "limited TV series",
"keep": "The creature should still be the most sympathetic character.",
"page_bones": "[{\"n\":1,\"part\":\"Victor, a brilliant student, builds a living creature out of dead parts and runs from it the moment it w ... (923 characters)",
"page_flags": "[]",
"page_verdict": "rich"
}
Reply:
{
"task": "xray",
"verdict": "rich",
"title": "Born in the Sealed Habitat",
"summary": "The setting states real pressures - a company in charge, rationed air and power, and three months with no way out - and these push Victor's cover-up and the creature's revenge into higher stakes than the original had.",
"core": {
"journey": "You move from sympathy for a newly made being who wants connection to anger at Victor for abandoning it, ending in a chase neither can escape.",
"appeal": "Underneath is the fear of being made and then abandoned, and the desire to belong to someone.",
"tension": "Victor's refusal to take responsibility for what he built collides with the creature's one request for a companion, and once that request is destroyed there is no way back from the revenge that follows.",
"peaks": [
"the creature waking and being abandoned",
"the companion destroyed before it could live",
"the hunt ending in the Arctic ice"
]
},
"power": "Victor holds power as the one who made the creature and who later controls the truth, choosing silence while an innocent servant hangs. The creature's only agency comes from what it teaches itself in secret and, once refused a companion, from the revenge it chooses to take.",
"bones": [
{
"n": 1,
"part": "Victor builds a living creature out of dead parts and runs from it when it wakes.",
"kind": "translate",
"function": "introduces the maker creating new life, then fleeing the instant it is real",
"new_form": "Victor, the habitat's bio-engineer, grows the creature from salvaged tissue in secret, then locks the hatch and retreats to another section when it wakes."
},
{
"n": 2,
"part": "The creature teaches itself to speak by watching a family through a crack in a cottage wall.",
"kind": "translate",
"function": "shows the creature secretly learning language and connection by watching others",
"new_form": "The creature learns to speak by watching the habitat crew through a gap in a bulkhead panel, picking up their words and routines unseen."
},
"..."
],
"pressures": [
{
"pressure": "air and power in the habitat are rationed",
"conflict": "Every decision to hide or chase the creature now costs air and power the whole crew needs, turning Victor's secrecy into a danger for everyone, not just him."
},
"..."
],
"logline": "Sealed three months from rescue in a mining company's ocean-floor habitat, the scientist who grew new life from salvaged tissue must face what he abandoned before the rationed air runs out for everyone aboard.",
"risks": [
{
"pattern": "reskin",
"text": "If the habitat is only scenery and the deaths, blame, and chase play out exactly as in the original, the remake would reduce to a costume change."
},
"..."
],
"questions": [
"Who in the mining company has the power to investigate or cover up a death in the habitat, and does Victor report to them directly?",
"..."
]
}
task: xray - Pride and Prejudice at a food truck festival - thin
Input:
{
"task": "xray",
"source": "Pride and Prejudice (Jane Austen, 1813)",
"source_notes": "Five sisters with no fortune must marry well because the family home will pass to a male cousin.\nElizabeth, the second s ... (530 characters)",
"setting": "A food truck festival.",
"form": "romantic comedy film",
"keep": "",
"page_bones": "[{\"n\":1,\"part\":\"Five sisters with no fortune must marry well because the family home will pass to a male cousin.\",\"kind\" ... (747 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"A short setting (4 words) - say who runs it, what is scarce and how news travels, or the remake ... (304 characters)",
"page_verdict": "thin"
}
Reply:
{
"task": "xray",
"verdict": "thin",
"title": "Pride and Prejudice, Food Truck Style",
"summary": "As given, your setting only names a place: it doesn't say who's in charge, what's scarce, how news travels, or how rules are enforced. Without that, swapping chefs for gentry would likely just reskin the original rather than transform it.",
"core": {
"journey": "The audience watches two sharp, prideful people hold onto their first impressions of each other until those impressions crack and they finally see each other clearly.",
"appeal": "The underlying desire is to be truly known and loved once pride and misjudgment are stripped away.",
"tension": "The central tension is that both are too proud to admit they got the other wrong, so neither will take the first step toward the truth.",
"peaks": [
"the dance where she judges him as proud",
"her refusal of his first proposal",
"him quietly paying to fix the scandal"
]
},
"power": "Power in the original sits with whoever controls money and property: the cousin set to inherit the family home, and Darcy, whose money quietly fixes the scandal later on, which is why the sisters must marry well to stay secure. Elizabeth's agency comes from her sharp judgment and her refusal to accept a proposal, even a secure one, until it is offered honestly.",
"bones": [
{
"n": 1,
"part": "Five sisters with no fortune must marry well because the family home will pass to a male cousin.",
"kind": "translate",
"function": "shows money and property pressure forcing marriage for security",
"new_form": "The family truck's best festival spot and recipe only pass to a male relative, so the sisters must each land a strong business partnership to keep working."
},
{
"n": 2,
"part": "Elizabeth, the second sister, judges proud Mr Darcy at a dance and refuses his first proposal.",
"kind": "translate",
"function": "sets up her misjudging him and rejecting his first advance",
"new_form": "At the festival's opening cook-off, she sizes him up as arrogant and turns down his offer to team their trucks for the weekend."
},
"..."
],
"pressures": [],
"logline": "At a food truck festival, two proud, sharp-tongued chefs let first impressions and secondhand gossip keep them apart until they're forced to admit they misjudged each other.",
"risks": [
{
"pattern": "reskin",
"text": "Without deciding who runs the festival, what's scarce, and how gossip spreads, the remake risks just swapping the dance for a food truck lineup while the story plays out exactly as before."
},
"..."
],
"questions": [
"Who runs your food truck festival, and what say do they have over which trucks get the best spots?",
"..."
]
}
task: xray - Romeo and Juliet as a kids' cooking show - clash
Input:
{
"task": "xray",
"source": "Romeo and Juliet (William Shakespeare, c. 1595)",
"source_notes": "Two teenagers from feuding families fall in love at a party and marry in secret the next day.\nA street fight leaves Rome ... (520 characters)",
"setting": "A breezy children's cooking-competition show, played for laughs, for ages 6 to 10, where two rival cooking-school teams ... (139 characters)",
"form": "animated kids' series",
"keep": "",
"page_bones": "[{\"n\":1,\"part\":\"Two teenagers from feuding families fall in love at a party and marry in secret the next day.\",\"kind\":\"t ... (736 characters)",
"page_flags": "[{\"level\":\"warn\",\"text\":\"The setting names only one kind of pressure (communication) - add another: authority, resources ... (333 characters)",
"page_verdict": "clash"
}
Reply:
{
"task": "xray",
"verdict": "clash",
"title": "Rival Cooking Teams, Secret Partnership",
"summary": "Played straight, your notes end with Romeo poisoning himself over Juliet's apparent death and Juliet then stabbing herself, a double death that would land as frightening rather than funny in a breezy cooking show made for six to ten year olds. This xray deliberately swaps that ending for a loss that costs both teams everything without a death, so the tragedy survives as heartbreak instead of horror.",
"core": {
"journey": "The audience falls for two rivals who fall for each other too fast, then watches that romance collide with an old grudge that was never theirs to inherit.",
"appeal": "The pull of wanting a connection so strong it outruns every warning and every old rule standing against it.",
"tension": "The harder they hold onto each other, the more they prove their families' hatred right by defying it, and a single missed message decides which way it goes.",
"peaks": [
"marrying in secret the day after they meet",
"the message that never reaches Romeo",
"Romeo's poisoning and Juliet's stabbing at the end"
]
},
"power": "Power in the original lies with the two feuding families, whose old hatred sets the rules Romeo and Juliet live under, and with whoever has arranged Juliet's marriage. The underdogs' only agency is secrecy: marrying without their families knowing, and Juliet faking her own death to escape the marriage set for her.",
"bones": [
{
"n": 1,
"part": "Two teenagers from feuding families fall in love at a party and marry in secret the next day.",
"kind": "transfer",
"function": "Love forms across an inherited rivalry and is acted on in secret.",
"new_form": "At the season-opening mixer, a player from each rival cooking school hits it off and secretly agrees to cook as an unofficial pair the next day."
},
{
"n": 2,
"part": "A street fight leaves Romeo's friend dead, and Romeo kills the killer and is banished.",
"kind": "translate",
"function": "A clash costs Romeo a friend; retaliating gets him removed from the competition.",
"new_form": "During the semifinal cook-off, a rival team's sabotage gets Romeo's teammate disqualified; Romeo retaliates by wrecking their dish and is benched for the rest of the season."
},
"..."
],
"pressures": [
{
"pressure": "two cooking-school teams are rivals",
"conflict": "Loving someone on the other team means working against your own team's win every week, since the two schools are rivals by design."
},
"..."
],
"logline": "Two rival cooking-school kids fall fast for each other, and when a missed text convinces each that the other has quit, their secret partnership becomes the thing that finally costs both teams the season.",
"risks": [
{
"pattern": "genre_collision",
"text": "If the climax keeps anything like a real death or a stabbing to match the original beat for beat, it will read as frightening rather than funny in a show built for six to ten year olds."
},
"..."
],
"questions": [
"Are you willing to end the season with a real, lasting loss, such as getting cut or breaking up as teammates, even with no deaths, so the tragedy still lands for your audience?",
"..."
]
}
task: xray - A Christmas Carol on a Mars colony - rich
Input:
{
"task": "xray",
"source": "A Christmas Carol (Charles Dickens, 1843)",
"source_notes": "Scrooge, a rich miser, refuses to give anything to the poor and makes his clerk work in the cold on Christmas Eve.\nThe g ... (531 characters)",
"setting": "A mining colony on Mars where the company rations air, water and heat, and every colonist's contract decides who gets th ... (123 characters)",
"form": "audio drama",
"keep": "The three visits and the change at the end.",
"page_bones": "[{\"n\":1,\"part\":\"Scrooge, a rich miser, refuses to give anything to the poor and makes his clerk work in the cold on Chri ... (743 characters)",
"page_flags": "[]",
"page_verdict": "rich"
}
Reply:
{
"task": "xray",
"verdict": "rich",
"title": "The Miser Who Rations Air",
"summary": "The colony's rationed air, water and heat, allotted by contract, turn Scrooge's old refusal of comfort into a direct threat to survival and a conflict with company rules that the original never had. That is enough new pressure to make this setting rich rather than thin or clashing with the core.",
"core": {
"journey": "The audience moves from watching a hoarder refuse comfort to those beneath him, through being shown exactly what his choices cost, to watching him act on that knowledge while there is still time.",
"appeal": "The underlying draw is the fear of causing harm that cannot be undone, matched by the wish for a real chance to see that cost in time to undo it.",
"tension": "The central tension is whether a man who holds all the power to help will choose to give before the harm his choices cause becomes permanent, and only confronting that harm directly can change his mind.",
"peaks": [
"the ghost's warning that a life of greed will be punished",
"the clerk's son sick and going to die unless something changes",
"waking changed and spending the day giving"
]
},
"power": "Scrooge holds power because his wealth lets him decide whether the poor and the clerk get any relief, while the clerk's family has no way to make him listen directly. Their agency only arrives once the visions force Scrooge to see, firsthand, what his refusals cost them.",
"bones": [
{
"n": 1,
"part": "Scrooge, a rich miser, refuses to give anything to the poor and makes his clerk work in the cold on Christmas Eve.",
"kind": "translate",
"function": "shows the power holder hoarding resources and denying relief to those beneath him",
"new_form": "Scrooge, holding a senior contract, denies extra air and heat to lower-tier colonists and keeps the clerk on an unheated shift through the scheduled rest period."
},
{
"n": 2,
"part": "The ghost of his dead business partner warns him that a life of greed will be punished.",
"kind": "transfer",
"function": "delivers a warning that greed will bring suffering, raising the stakes",
"new_form": "The business partner's ghost still appears to Scrooge, now reaching him through the colony's own channels, warning that hoarding will cost him everything in the end."
},
"..."
],
"pressures": [
{
"pressure": "the company rations air, water and heat",
"conflict": "A ration cut here is not mere stinginess but a direct threat to survival, raising what Scrooge's hoarding actually costs."
},
"..."
],
"logline": "On a Martian mining colony where a contract decides who breathes, Scrooge, who hoards air and heat for himself, is shown in one night what his choices cost the people who depend on him, in time to change them.",
"risks": [
{
"pattern": "surface_diversity",
"text": "Replacing Christmas Eve and Christmas morning with a renamed 'Mars holiday' in costume only, instead of an occasion built from the colony's own rules, would treat the original's holiday as decoration."
},
"..."
],
"questions": [
"When Scrooge finally gives, is he giving up his own surplus allocation or changing the contract rules that govern everyone else?",
"..."
]
}
task: verdict - the deep-sea pitch - transformed
Input:
{
"task": "verdict",
"source": "Frankenstein (Mary Shelley, 1818)",
"source_notes": "Victor, a brilliant student, builds a living creature out of dead parts and runs from it the moment it wakes.\nThe creatu ... (674 characters)",
"setting": "A sealed research habitat on the floor of the Pacific in 2140, run by a mining company, with rationed air and power and ... (160 characters)",
"form": "limited TV series",
"keep": "The creature should still be the most sympathetic character.",
"page_bones": "[{\"n\":1,\"part\":\"Victor, a brilliant student, builds a living creature out of dead parts and runs from it the moment it w ... (923 characters)",
"pitch": "Deep Culture. On a sealed company habitat three kilometres under the Pacific, a junior biologist grows a thinking organi ... (888 characters)",
"page_pitch": "{\"carried_names\":[],\"swap_lines\":0,\"but_in\":false,\"notes_overlap\":12,\"setting_overlap\":69,\"literal_keeps\":[],\"tone_clash\":false}",
"page_flags": "[]",
"page_verdict": "transformed"
}
Reply:
{
"task": "verdict",
"verdict": "transformed",
"title": "Deep Culture: a maker's refusal",
"summary": "The pitch keeps the maker's refusal to take responsibility and the creature's loneliness, but rebuilds every beat from the habitat's scarce air, company control, and the wait for supply. That produces conflicts your notes never had, such as a vote over who gets to breathe.",
"checks": [
{
"check": "systemic consistency",
"result": "pass",
"note": "The grown organism, the cold-storage sealing, the air vents, the trench, and air rationed by vote all follow from a sealed, company-run habitat with scarce air and a three-month wait for the supply ship."
},
{
"check": "emotional core",
"result": "pass",
"note": "The refusal to take responsibility, the pain of a rejected companion, and the mutual hunt at the end carry the same weight as in your notes."
},
"..."
],
"swaps": [
{
"from": "Victor builds a creature from dead parts and runs from it the moment it wakes",
"to": "a biologist grows an organism from the station's discarded tissue cultures and seals it in cold storage the hour it opens an eye",
"kind": "rebuilt",
"note": "The making and the instant rejection survive; cutting up corpses becomes growing from the station's own waste, and running away becomes sealing the organism in storage."
},
{
"from": "the creature teaches itself to speak by watching a family through a crack in a cottage wall",
"to": "it learns language by listening to the crew through the air vents",
"kind": "rebuilt",
"note": "The secret self-teaching survives; a cottage wall's crack becomes the habitat's own air vents."
},
"..."
],
"fixes": [
"Bring the air-rationed-by-vote idea back into the finale, so the last hunt also forces a vote on who gets to breathe.",
"State the company's stake once in plain terms, so its threat to end the project reads as real institutional power rather than a passing mention.",
"..."
],
"stands_alone": "Yes: a biologist's grown creation, a company's cover-up, and a vote on who gets to breathe make a sealed-habitat thriller that needs no knowledge of the original to land.",
"logline": "With air rationed by vote and the supply ship still three months out, a biologist must survive the oxygen-starved life she grew from the station's own discarded cultures, up to a last vote on who gets to breathe."
}
task: verdict - the food truck pitch - reskin
Input:
{
"task": "verdict",
"source": "Pride and Prejudice (Jane Austen, 1813)",
"source_notes": "Five sisters with no fortune must marry well because the family home will pass to a male cousin.\nElizabeth, the second s ... (530 characters)",
"setting": "A food truck festival.",
"form": "romantic comedy film",
"keep": "",
"page_bones": "[{\"n\":1,\"part\":\"Five sisters with no fortune must marry well because the family home will pass to a male cousin.\",\"kind\" ... (747 characters)",
"pitch": "It's Pride and Prejudice but with food trucks. Liz Bennet runs a taco truck with her four sisters. Mr Darcy becomes Dan ... (519 characters)",
"page_pitch": "{\"carried_names\":[\"Darcy\",\"Wickham\"],\"swap_lines\":3,\"but_in\":true,\"notes_overlap\":39,\"setting_overlap\":100,\"literal_keeps\":[],\"tone_clash\":false}",
"page_flags": "[{\"level\":\"info\",\"text\":\"Names carried over from the original: Darcy, Wickham.\"},{\"level\":\"warn\",\"text\":\"3 one-for-one s ... (355 characters)",
"page_verdict": "reskin"
}
Reply:
{
"task": "verdict",
"verdict": "reskin",
"title": "Taco Truck Pride and Prejudice",
"summary": "This mostly swaps names and props onto the original's exact plot - the dance, the lie, the elopement and the quiet rescue all work exactly as they did in your notes, just relabeled with trucks. The food truck setting does not yet add any pressure of its own, so anyone who knows the book would spot it right away.",
"checks": [
{
"check": "systemic consistency",
"result": "mixed",
"note": "The truck details are plausible together, but the money-and-inheritance stakes behind your notes are dropped rather than rebuilt into any festival rule, like who gets a permit or a prime spot."
},
{
"check": "emotional core",
"result": "pass",
"note": "The pride-driven misjudgment and the quiet reconciliation are both present and should land the way they did in your notes."
},
"..."
],
"swaps": [
{
"from": "Five sisters with no fortune who must marry well before the family home passes to a male cousin",
"to": "Liz Bennet running a taco truck with her four sisters",
"kind": "surface",
"note": "The sister count is kept, but the money pressure behind why they need to succeed is gone, not rebuilt as a festival stake."
},
{
"from": "Elizabeth judging proud Mr Darcy at a dance and refusing his first proposal",
"to": "Dan Darcy's burger truck looking down on tacos and insulting Liz's cooking at opening night",
"kind": "surface",
"note": "The dance becomes opening night and status pride becomes food snobbery, but the judgment plays out exactly as before, and the proposal and refusal are dropped."
},
"..."
],
"fixes": [
"Give the sisters a stake that comes from the festival itself, such as losing their truck's spot or license if the season goes badly, instead of dropping the money pressure behind your notes.",
"Root Dan's judgment of Liz in a specific food-truck authority, such as being a festival judge or an influential reviewer, rather than generic burger-versus-taco snobbery, so his pride works differently than Darcy's did.",
"..."
],
"stands_alone": "Yes, on its own it would still work as a serviceable rivals-to-lovers romance, but without any detail unique to food trucks it would feel like a generic version of a story audiences already know.",
"logline": "When the Bennet sisters' taco truck risks losing its festival spot, prickly Liz and snobbish burger-truck owner Dan Darcy clash over pride and cooking, until a mechanic's lie and a quiet rescue force them to see past their first impressions."
}
task: verdict - the cooking-show pitch - genre collision
Input:
{
"task": "verdict",
"source": "Romeo and Juliet (William Shakespeare, c. 1595)",
"source_notes": "Two teenagers from feuding families fall in love at a party and marry in secret the next day.\nA street fight leaves Rome ... (520 characters)",
"setting": "A breezy children's cooking-competition show, played for laughs, for ages 6 to 10, where two rival cooking-school teams ... (139 characters)",
"form": "animated kids' series",
"keep": "",
"page_bones": "[{\"n\":1,\"part\":\"Two teenagers from feuding families fall in love at a party and marry in secret the next day.\",\"kind\":\"t ... (736 characters)",
"pitch": "Kitchen Chaos! Romeo and Juliet are junior chefs on rival cooking-school teams who fall in love at the season-opening ba ... (639 characters)",
"page_pitch": "{\"carried_names\":[\"Romeo\",\"Juliet\"],\"swap_lines\":0,\"but_in\":false,\"notes_overlap\":22,\"setting_overlap\":38,\"literal_keeps\":[],\"tone_clash\":true}",
"page_flags": "[{\"level\":\"info\",\"text\":\"Names carried over from the original: Romeo, Juliet.\"},{\"level\":\"warn\",\"text\":\"Tragic notes, li ... (184 characters)",
"page_verdict": "genre_collision"
}
Reply:
{
"task": "verdict",
"verdict": "genre_collision",
"title": "Kitchen Chaos turns heartbreak into slapstick",
"summary": "The pitch keeps the same beats as your notes - a friend's death, a banishment, a faked death, a lost note - but plays the climactic loss as a faint into custard and a laughing pie fight, so the tragedy lands as a joke instead of heartbreak. That shift comes from tone, not from anything the cooking-show setting itself requires.",
"checks": [
{
"check": "systemic consistency",
"result": "pass",
"note": "Each new beat - the bake-off, the food fight, the blender, the custard vat - follows from a cooking-competition show, so the elements hang together inside that premise."
},
{
"check": "emotional core",
"result": "fail",
"note": "Your notes call out love moving too fast against old hatred and a tragedy turned on a lost message, but the finale resolves into a faint and a pie fight where the cast laughs, so the loss your notes care about never lands."
},
"..."
],
"swaps": [
{
"from": "feuding families fall in love at a party and marry in secret the next day",
"to": "rival cooking-school teams fall in love at the season-opening bake-off and secretly agree to swap recipes",
"kind": "rebuilt",
"note": "Swapping recipes in secret is a real cooking-show stand-in for marrying in secret, the same forbidden-commitment function rebuilt from the setting."
},
{
"from": "a street fight leaves Romeo's friend dead, and Romeo kills the killer and is banished",
"to": "a food fight wipes out Romeo's best friend's souffle, and Romeo gets sent off the show for pie-ing the other team's captain",
"kind": "surface",
"note": "A ruined souffle sits in the same plot slot as a friend's death, so the beat still happens, but trading a life for a dessert drains the grief meant to drive Romeo's banishment."
},
"..."
],
"fixes": [
"Give the beat that stands in for the friend's death real cost, something that can't be laughed off, like a permanent elimination from the finals or a broken friendship, so Romeo's banishment-equivalent feels earned.",
"Let the ending stay quiet for a beat before any comedy returns: have Romeo's shock at finding her play straight, even briefly, so the loss your notes describe has a moment to land.",
"..."
],
"stands_alone": "On its own it reads as a lively cooking-show romp, but the sudden swing from a near-death scare into a laughing pie fight would still feel like a tonal lurch even to someone who has never heard of the story it comes from.",
"logline": "A food fight costs Romeo's best friend their last shot at the finals, and his revenge gets him suspended just as he and Juliet, rival-team chefs secretly swapping recipes, risk losing each other to a lost note and a staged collapse."
}
task: verdict - the Mars colony pitch - fidelity trap
Input:
{
"task": "verdict",
"source": "A Christmas Carol (Charles Dickens, 1843)",
"source_notes": "Scrooge, a rich miser, refuses to give anything to the poor and makes his clerk work in the cold on Christmas Eve.\nThe g ... (531 characters)",
"setting": "A mining colony on Mars where the company rations air, water and heat, and every colonist's contract decides who gets th ... (123 characters)",
"form": "audio drama",
"keep": "The three visits and the change at the end.",
"page_bones": "[{\"n\":1,\"part\":\"Scrooge, a rich miser, refuses to give anything to the poor and makes his clerk work in the cold on Chri ... (743 characters)",
"pitch": "Red Ledger. On a company mining colony on Mars, quartermaster Ebenezer Scrooge controls the air, water and heat ration f ... (683 characters)",
"page_pitch": "{\"carried_names\":[\"Scrooge\"],\"swap_lines\":0,\"but_in\":false,\"notes_overlap\":31,\"setting_overlap\":82,\"literal_keeps\":[\"victorian\",\"lamps\",\"workhouse\",\"christmas\",\"poulterer\"],\"tone_clash\":false}",
"page_flags": "[{\"level\":\"info\",\"text\":\"Names carried over from the original: Scrooge.\"},{\"level\":\"warn\",\"text\":\"Period or culture word ... (222 characters)",
"page_verdict": "fidelity_trap"
}
Reply:
{
"task": "verdict",
"verdict": "fidelity_trap",
"title": "Red Ledger Keeps Victorian London",
"summary": "The ration system is a genuine new pressure, but the pitch keeps Victorian London's streets, gas lamps, a workhouse, a poulterer's goose, and the closing line unchanged, props from 1843 London that a Mars mining colony would not produce.",
"checks": [
{
"check": "systemic consistency",
"result": "fail",
"note": "The ration system controlled by contracts fits the setting, but Victorian London streets, gas lamps, a workhouse, and a poulterer's shop do not follow from anything a Mars mining colony would have."
},
{
"check": "emotional core",
"result": "pass",
"note": "The three visits, the sick son, and Scrooge's turn into giving still carry the same weight the notes describe."
},
"..."
],
"swaps": [
{
"from": "a rich miser refuses to give anything to the poor",
"to": "a quartermaster who won't raise the air, water and heat ration for contract workers in the coldest week",
"kind": "rebuilt",
"note": "Withholding from the poor becomes withholding the ration the setting's contracts actually control."
},
{
"from": "makes his clerk work in the cold on Christmas Eve",
"to": "the colony's corridors rebuilt as Victorian London streets with gas lamps and a workhouse, \"because that is where the story happens\"",
"kind": "forced",
"note": "Victorian streets, lamps and a workhouse would not exist on a Mars mining colony and are kept only to match the original."
},
"..."
],
"fixes": [
"Cut the Victorian London streets, gas lamps and workhouse, and build the colony's poverty entirely from who controls the ration and who does not.",
"Replace the poulterer's shop and the goose with something the colony's economy would produce, such as Scrooge releasing hoarded ration credits to the clerk's family.",
"..."
],
"stands_alone": "Someone who never knew the original would find the Victorian streets, the workhouse, the poulterer's shop and the closing line hard to place inside a Mars mining colony, since nothing about the setting explains why they are there.",
"logline": "On a Mars mining colony where a contract decides who gets air, water and heat, a quartermaster who starves his workers of warmth is shown by three visitors exactly what his choices cost, in time to raise every ration before the clerk's son dies."
}
Build the input yourself
Load the script in node (global.window = {}; require("./bonekit.js")), then:
const K = BoneKit;
const form = { source: "Frankenstein (Mary Shelley, 1818)", source_notes: "Victor builds a creature...\nThe creature asks for a companion...",
setting: "A sealed research habitat on the floor of the Pacific...", form: "limited TV series", keep: "", kinds: "" };
const st = K.state(form); // the bones, the setting reading, the pitch reading
console.log(st.B.list.map(b => b.n + " " + b.kind), st.S.verdict);
const wire = K.toWire(K.buildInput(st, "xray").input); // POST this to /estimate and /run
// re-sort a bone: form.kinds = "3:replace"; then rebuild the state
// the verdict lane: form.pitch = "your pitch"; K.toWire(K.buildInput(K.state(form), "verdict").input)
// later: const reply = K.normalize(K.parseReply(job.output.output), "xray");
// const { notes } = K.check(reply, K.state(form), "xray");
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://same-bones.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://same-bones.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// same-bones.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://same-bones.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://same-bones.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// SameBones.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class SameBones {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://same-bones.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://same-bones.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://same-bones.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://same-bones.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"same-bones"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "same-bones"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "same-bones" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "same-bones"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "same-bones"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "same-bones" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "same-bones"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "same-bones" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "xray", "source": "Frankenstein (Mary Shelley, 1818)", "source_notes": "Victor, a brilliant student, builds a living creature...", "setting": "A sealed research habitat on the floor of the Pacific in 2140...", "form": "limited TV series", "keep": "...", "page_bones": "[...]", "page_flags": "[]", "page_verdict": "rich"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "xray", "source": "Frankenstein (Mary Shelley, 1818)", "source_notes": "Victor, a brilliant student, builds a living creature...", "setting": "A sealed research habitat on the floor of the Pacific in 2140...", "form": "limited TV series", "keep": "...", "page_bones": "[...]", "page_flags": "[]", "page_verdict": "rich"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "xray" | "verdict", source, source_notes, setting, form, keep, page_bones, page_flags, page_verdict, ... (see the field table)}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses same-bones:<task>:<inputHash>:a<attempt> (the examples below use the X-ray lane), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="same-bones:xray:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "same-bones:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `same-bones:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "same-bones:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "same-bones:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "same-bones:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "same-bones:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"same-bones:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: same-bones:xray:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "xray", "source": "Frankenstein (Mary Shelley, 1818)", "source_notes": "Victor, a brilliant student, builds a living creature...", "setting": "A sealed research habitat on the floor of the Pacific in 2140...", "form": "limited TV series", "keep": "...", "page_bones": "[...]", "page_flags": "[]", "page_verdict": "rich"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "same-bones:xray:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `same-bones:xray:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "same-bones:xray:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "same-bones:xray:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "same-bones:xray:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: same-bones:xray:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"same-bones:xray:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);