Run Contract Gate from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page diffs the two specs, gives every change a check id, a level and a fingerprint, applies the team's levels file, audits the acknowledgement rows, judges the gate and ranks the rungs, then hands it all to the run as facts; the specs themselves are never sent. The run writes the pipeline design or the changelog entries; the page then holds it to its own facts (cgreply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"contract-gate"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, api, diff, facts) is missing. The page always sends all five of task, api, diff, carry, facts; carry is an empty string for pipeline. |
| 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 sign-in. |
| 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
Two lanes, chosen by task, in the order the work runs - the machinery, then this release:
pipeline- the changelog pipeline design: the six rungs as ranked, the three baselines, one severity row per fired check at the page's level, the gate, the declared-change channel and the gaps.entries- the changelog entries: one traced entry per breaking or potentially breaking change, its classification, operations and fingerprints, the action callers take, and the unapproved breaks.
A missing or unknown task is answered as pipeline. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the pipeline's decisions into carry for the entries lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | pipeline or entries. If it is missing or neither, do pipeline and set "task": "pipeline". |
api | yes | the API and the interview: what it is, the breaking-change definition, validation, generation, versioning, usage data, who reads the changelog, timing, the declared-change channel, the policy answers on auth and enums, the audience filter and the as-of date; "not given" means not given. |
diff | yes | the two specs' versions and operation counts, every finding as fingerprint | check | level | operation | location | text with whether an err finding is acknowledged, the levels file and the acknowledgement rows. |
carry | send it (empty for pipeline) | entries task only: the pipeline's decisions, when the pipeline task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, the gate verdict and the ranked rungs. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/api-changelog-automation - the only standard the run applies:
- Never trust the diff tool twice: its defaults are not the team's policy, and it sees only declared structure. Behaviour, defaults, validation, error codes or bodies, auth enforced at a gateway, rate limits, pagination and ordering need a declared-change channel: a PR question, and a change fragment per change carrying the same fields as a diff-derived entry, approved like any break.
- Preconditions: a spec not validated against the running service (contract tests, traffic validation) describes a document, not the API - that is the first gap. A generated spec needs a pinned generator, stable operationIds, $ref'd schemas and stable enum order, or phantom changes teach the team to turn the gate off. Commit the spec of every release; the first release has no baseline.
- The severity map comes from the written breaking-change definition, one row per check: the policy line, the level, the reason, and provisional while the policy is unsigned. With no written definition every level is the tool author's opinion. Tools disagree on response enum growth, on auth changes and on removing deprecated elements - never adopt a default unread. Pinning a level disables the tool's own downgrades for that check. With semver in info.version, raise the major-version-not-bumped check to err; date, header and URI versions use per-version baselines instead. Mark lifecycle (deprecated, x-sunset, x-stability-level) and open enums (x-extensible-enum). Pin the diff tool's version.
- One baseline per job: the PR gate diffs against the merge base; the published changelog against the last released spec; the drift alarm against what production actually serves. Date- or header-versioned APIs diff per version.
- The PR gate fails on err, shows warn as potentially breaking and never drops it, filters by audience first (an operation that becomes internal leaves the filtered diff as a removal and stays breaking), and requires a named approver through a code-owners rule on the spec, the map and the acknowledgement file.
- Every accepted break has an acknowledgement row: fingerprint, check, operation, approver, date, ticket, reason class (intended, correction, security, historic) and the notice link. The ignore file is generated from the rows, never edited by hand; a row matching nothing is deleted. Spec corrections still get an entry.
- Entries: built only from structured findings, fragments and acknowledgement rows; one per contract change, grouped by API version, breaking first; a shared-schema change is one entry listing every affected operation and fingerprint; each carries a version-scoped id, a classification (breaking, potentially-breaking, non-breaking), its source (diff:<check-id> or declared:<fragment-id>), fingerprints, operations, the action required and a sunset date when one applies. Version-bump findings are excluded. A rewrite may shorten or reword an entry, never add a path, field, status code, date or consequence its source lacks.
- Rungs, by value per unit of effort: PR gate, release diff plus declared channel, approval record, feed, usage-aware classification, version-derived changelog. A fixed date ships the PR gate alone first; a standing mandate promotes the approval record and the feed; machine consumers promote the feed above the approval record; no usage data deletes usage-aware classification; date-based per-account versions with response transformers promote the version-derived changelog.
- Measurement: escaped breaks (zero), trace coverage (100% before publishing), acknowledgement hygiene (zero incomplete or stale rows), override rate per release.
House rules
- Use only the input. Never invent an endpoint, a field, a caller, a date, an incident or a usage figure.
- The page's findings and checks are settled: never change a level, call a failed check fine or a passed one broken.
- No URL at all.
- Write in the language of the input; keep the JSON keys, check ids, paths, fingerprints and classifications in English.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
pipeline
Write the changelog pipeline design for this API - the target setup, prescriptive, fixing what the page flags. Choosing an approver group, a file location or a filter is a design decision; say what changes.
summary- one sentence, at most 40 words: what the gate would let through today and the first fix.rungs- all six, in the facts' order:rung(pr-gate,release-diff,approval,feed,usageorversion-derived),build(now,laterorskip, as the facts rank it),why(one sentence).baselines- three objects:job(pr-gate,changelogordrift, each once),against(one sentence: what the candidate is compared with).severity_map- one row per check that fired in this diff:check(the check id),level(exactly the level the page applied),policy_line(one sentence: the policy line it enforces when the definition is written; when it is not, what the policy must decide),provisional(true when there is no written definition, false otherwise).gate-fail_on(err),warn(one sentence: how warn findings are shown),audience(one sentence: the filter),approver(one sentence: the code-owners rule and who approves - naming the approving group is a design decision).declared-question(the PR template question),fragment_fields(short strings: the fragment's fields - the same fields as an entry: id, classification, operations, summary, action required, the affected versions).gaps- ordered short strings: what to fix before the gate can be trusted, most important first; [] when none.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "pipeline",
"summary": "one sentence",
"rungs": [ { "rung": "pr-gate", "build": "now", "why": "one sentence" } ],
"baselines": [ { "job": "pr-gate", "against": "one sentence" } ],
"severity_map": [ { "check": "response-property-removed", "level": "err", "policy_line": "one sentence", "provisional": true } ],
"gate": { "fail_on": "err", "warn": "one sentence", "audience": "one sentence", "approver": "one sentence" },
"declared": { "question": "one sentence", "fragment_fields": [ "id" ] },
"gaps": [ "a gap" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in cgreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no percentage; a summary of at most 40 words; the six rungs in the ranked order with the ranked build values, each with a reason; the three baselines once each with what they compare against; one severity row per fired check at exactly the page's level with a policy line, nothing that did not fire, provisional exactly when no definition is written; the gate failing on err with its warn, audience and approver sentences; a declared-change question and at least three fragment fields; the validation gap first when the spec is not checked against the service.
entries
Write the changelog entries for this release from the findings - traced, never embellished.
summary- one sentence, at most 40 words: what callers must do for this release.api_version- the candidate's version as the facts give it.entries- breaking first, then potentially breaking; non-breaking (info) entries are optional:id(the api version, a hyphen and a number),classification(breaking,potentially-breakingornon-breaking, from the finding's level),source(diff:<check-id>),fingerprints(the findings it covers - one check per entry; one entry covers the same check at the same property on several operations, as a shared schema reports it),operations(copied from those findings),summary(one sentence a caller understands),action_required(one sentence; "" only for non-breaking),sunset(YYYY-MM-DD when the findings carry one, else "").unapproved- the fingerprints of err findings with no acknowledgement, exactly as the facts list them.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "entries",
"summary": "one sentence",
"api_version": "1.5.0",
"entries": [ { "id": "1.5.0-1", "classification": "breaking", "source": "diff:response-property-removed", "fingerprints": [ "4737a336" ], "operations": [ "GET /charges" ], "summary": "one sentence", "action_required": "one sentence", "sunset": "" } ],
"unapproved": [ "4737a336" ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; the candidate's api version; every err and warn finding covered exactly once, never the version-bump finding; one check per entry, its source diff:
Worked examples
Three runs from the page's free samples (invented APIs): the pipeline for a semver payments API with unapproved breaks, the changelog entries for the same release carrying that pipeline, and the entries for a date-versioned orders API whose breaks are acknowledged. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
pipeline - A semver payments API with unapproved breaks
The page read it as: the gate fails; 6 err, 2 warn, 0 info; 5 unapproved; 0 of 4 gates; rungs: pr-gate now, release-diff now, approval now, feed now, usage skip, version-derived skip; failing checks: validated, gate, version, hygiene.
Input (diff is 1688 characters; the first 1394 are shown; facts is 2774 characters; the first 1396 are shown):
{
"task": "pipeline",
"api": "API: Tallowby Payments API\nWhat it is and who calls it: A public payments API called by merchants' servers and three official SDKs; partners read the changelog page.\nBreaking-change definition: No written definition; spec validated against the service: no; generated from code: no\nVersioning: Semver in info.version; per-account date versions with response transformers: no; per-operation usage data: no\nChangelog readers: People read the changelog; timing: A standing pipeline; declared-change channel today: no\nPolicy says any auth change is breaking: no; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
"diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- c8c54bb9 | new-required-request-parameter | err | GET /charges | parameter query:account | added the required query parameter account | acknowledged\n- 4737a336 | response-property-removed | err | GET /charges | response 200 data[].receipt_url | removed the property data[].receipt_url from the 200 response | NOT acknowledged\n- 3bdf3d16 | response-property-removed | err | POST /charges | response 201 receipt_url | removed the property receipt_url from the 201 response | NOT acknowledged\n- 3b3b713e | api-security-scope-added | err | POST /charges | security oauth | added the scope payments:write to oauth | NOT acknowledged\n- fdca31b3 | api-removed-before-sunset | err | POST /charges/{id}/capture | - | removed the deprecated POST /charges/{id}/capture before its sunset date 2027-03-01 | NOT acknowledged\n- ae6b25c8 | api-major-version-not-bumped | err (raised: semver) | info.version | info.version | info.version went from 1.4.0 to 1.5.0 with breaking changes | NOT acknowledged\n- fd05a761 | response-property-enum-value-added | warn | GET /charges | response 200 data[].status | added the value disputed to the response property data[].status in 200\n- bed7de27 | response-property-enum-value-added | warn | POST /charges | response 201\n\n[... 294 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (4 fail, 4 warn, 3 pass, 2 n/a); gate verdict: the gate fails:\n- The two specs | baseline | pass | baseline OpenAPI 3.0.3, version 1.4.0, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.0.3, version 1.5.0, 2 operations\n- Preconditions | validated | fail | the diff describes a document, not the API - the first gap to close\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | warn | every severity row is provisional pending policy sign-off\n- The severity map | levels | pass | no overrides - the defaults are not your policy\n- The severity map | disputed | warn | response-property-enum-value-added, api-security-scope-added fired on their default levels\n- The gate | gate | fail | 5 unapproved breaks: response-property-removed on GET /charges [4737a336]; response-property-removed on POST /charges [3bdf3d16]; api-security-scope-added on POST /charges [3b3b713e]; api-removed-before-sunset on POST /charges/{id}/capture [fdca31b3]; api-major-version-not-bumped on info.version [ae6b25c8]\n- The gate | warn | warn | response-property-enum-value-added on GET /charges; response-property-enum-value-added on POST /charges\n- The gate | version | fail | info.version 1.4.0 -> 1.5.0 with breaking changes\n- The approval record | hygiene | fail | c8c54bb9: no notice or migration link; 0badc0de: matches no change in this diff - delete\n\n[... 1378 more characters cut from this display ...]"
}
Reply:
{
"task": "pipeline",
"summary": "Today nothing is trustworthy: four unapproved breaks plus an unbumped major version would fail the gate, but the spec is unchecked against the live service, so validating it there comes first.",
"rungs": [
{
"rung": "pr-gate",
"build": "now",
"why": "Blocking unapproved breaks before merge is the cheapest protection for merchants and the three SDKs."
},
{
"rung": "release-diff",
"build": "now",
"why": "A per-release diff plus a declared channel records what shipped, including behaviour changes the spec cannot show."
},
{
"rung": "approval",
"build": "now",
"why": "An accepted break already exists and its row lacks a notice link, so the record has to start now."
},
{
"rung": "feed",
"build": "now",
"why": "A standing pipeline justifies publishing the traced entries as a feed for partners."
},
{
"rung": "usage",
"build": "skip",
"why": "Without per-operation usage data there is nothing to classify by, so this rung is dropped."
},
{
"rung": "version-derived",
"build": "skip",
"why": "Semver releases without per-version response transformers make this weeks of work for little return."
}
],
"baselines": [
{
"job": "pr-gate",
"against": "The spec at the merge base of the pull request."
},
{
"job": "changelog",
"against": "The spec committed with the last released version, 1.4.0 for this release."
},
{
"job": "drift",
"against": "What the production service actually serves, captured by contract tests or traffic validation."
}
],
"severity_map": [
{
"check": "new-required-request-parameter",
"level": "err",
"policy_line": "Adding a required request parameter breaks existing calls that omit it.",
"provisional": true
},
{
"check": "response-property-removed",
"level": "err",
"policy_line": "Removing a documented response property breaks clients that read it.",
"provisional": true
},
{
"check": "response-property-enum-value-added",
"level": "warn",
"policy_line": "The policy must decide whether response enums may grow; until it does, a new value is shown as potentially breaking.",
"provisional": true
},
{
"check": "api-security-scope-added",
"level": "err",
"policy_line": "The policy must state whether a new required scope is breaking; it stays err until signed.",
"provisional": true
},
{
"check": "api-removed-before-sunset",
"level": "err",
"policy_line": "A deprecated operation may only be removed on or after its announced sunset date.",
"provisional": true
},
{
"check": "api-major-version-not-bumped",
"level": "err",
"policy_line": "Under semver, any breaking change requires a new major version.",
"provisional": true
}
],
"gate": {
"fail_on": "err",
"warn": "Warn findings appear as a potentially breaking section on the PR and in the changelog, never dropped.",
"audience": "Diff only beta and stable operations, excluding draft and alpha, and treat an operation moved out of that set as a removal.",
"approver": "A code-owners rule on the spec, the severity map and the acknowledgement file requires a named API owner to approve every accepted break."
},
"declared": {
"question": "Does this change alter behaviour, defaults, validation, error codes or bodies, gateway auth, rate limits, pagination or ordering without a spec change?",
"fragment_fields": [
"id",
"classification",
"operations",
"summary",
"action required",
"sunset date",
"approver",
"ticket"
]
},
"gaps": [
"Validate the spec against the running service with contract tests or traffic validation",
"Write and sign a breaking-change definition so severity rows stop being provisional",
"Decide response enum growth and added auth scopes explicitly instead of keeping tool defaults",
"Revert or acknowledge the four unapproved breaks and bump the major version",
"Add the notice link to the c8c54bb9 row and delete the 0badc0de row",
"Open a declared-change channel: the PR question plus change fragments",
"Pin the diff tool version"
],
"caveats": [
"Removing POST /charges/{id}/capture before 2027-03-01 breaks the sunset promise even if approved."
]
}
What step 5 prints for it: new-required-request-parameter err | response-property-removed err | response-property-enum-value-added warn | api-security-scope-added err | api-removed-before-sunset err | api-major-version-not-bumped err
The page's check: kept - nothing to flag
entries - A semver payments API with unapproved breaks
The input carries the saved pipeline's decisions (R.laneForm fills carry from the pipeline reply), as the page's handoff does. The diff: 6 err, 2 warn, 0 info; 5 unapproved.
Input (diff is 1688 characters; the first 1394 are shown; facts is 2699 characters; the first 1396 are shown):
{
"task": "entries",
"api": "API: Tallowby Payments API\nWhat it is and who calls it: A public payments API called by merchants' servers and three official SDKs; partners read the changelog page.\nBreaking-change definition: No written definition; spec validated against the service: no; generated from code: no\nVersioning: Semver in info.version; per-account date versions with response transformers: no; per-operation usage data: no\nChangelog readers: People read the changelog; timing: A standing pipeline; declared-change channel today: no\nPolicy says any auth change is breaking: no; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
"diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- c8c54bb9 | new-required-request-parameter | err | GET /charges | parameter query:account | added the required query parameter account | acknowledged\n- 4737a336 | response-property-removed | err | GET /charges | response 200 data[].receipt_url | removed the property data[].receipt_url from the 200 response | NOT acknowledged\n- 3bdf3d16 | response-property-removed | err | POST /charges | response 201 receipt_url | removed the property receipt_url from the 201 response | NOT acknowledged\n- 3b3b713e | api-security-scope-added | err | POST /charges | security oauth | added the scope payments:write to oauth | NOT acknowledged\n- fdca31b3 | api-removed-before-sunset | err | POST /charges/{id}/capture | - | removed the deprecated POST /charges/{id}/capture before its sunset date 2027-03-01 | NOT acknowledged\n- ae6b25c8 | api-major-version-not-bumped | err (raised: semver) | info.version | info.version | info.version went from 1.4.0 to 1.5.0 with breaking changes | NOT acknowledged\n- fd05a761 | response-property-enum-value-added | warn | GET /charges | response 200 data[].status | added the value disputed to the response property data[].status in 200\n- bed7de27 | response-property-enum-value-added | warn | POST /charges | response 201\n\n[... 294 more characters cut from this display ...]",
"carry": "Rungs now: pr-gate, release-diff, approval, feed\nSeverity map: new-required-request-parameter err (provisional); response-property-removed err (provisional); response-property-enum-value-added warn (provisional); api-security-scope-added err (provisional); api-removed-before-sunset err (provisional); api-major-version-not-bumped err (provisional)\nGate: fail on err; approver: A code-owners rule on the spec, the severity map and the acknowledgement file requires a named API owner to approve every accepted break.\nDeclared fragments carry: id, classification, operations, summary, action required, sunset date, approver, ticket",
"facts": "The page's checks (4 fail, 4 warn, 3 pass, 2 n/a); gate verdict: the gate fails:\n- The two specs | baseline | pass | baseline OpenAPI 3.0.3, version 1.4.0, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.0.3, version 1.5.0, 2 operations\n- Preconditions | validated | fail | the diff describes a document, not the API - the first gap to close\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | warn | every severity row is provisional pending policy sign-off\n- The severity map | levels | pass | no overrides - the defaults are not your policy\n- The severity map | disputed | warn | response-property-enum-value-added, api-security-scope-added fired on their default levels\n- The gate | gate | fail | 5 unapproved breaks: response-property-removed on GET /charges [4737a336]; response-property-removed on POST /charges [3bdf3d16]; api-security-scope-added on POST /charges [3b3b713e]; api-removed-before-sunset on POST /charges/{id}/capture [fdca31b3]; api-major-version-not-bumped on info.version [ae6b25c8]\n- The gate | warn | warn | response-property-enum-value-added on GET /charges; response-property-enum-value-added on POST /charges\n- The gate | version | fail | info.version 1.4.0 -> 1.5.0 with breaking changes\n- The approval record | hygiene | fail | c8c54bb9: no notice or migration link; 0badc0de: matches no change in this diff - delete\n\n[... 1303 more characters cut from this display ...]"
}
Reply:
{
"task": "entries",
"summary": "Send the new required account parameter on GET /charges, stop reading receipt_url, request the payments:write scope, move off the capture operation, and handle the disputed status.",
"api_version": "1.5.0",
"entries": [
{
"id": "1.5.0-1",
"classification": "breaking",
"source": "diff:new-required-request-parameter",
"fingerprints": [
"c8c54bb9"
],
"operations": [
"GET /charges"
],
"summary": "GET /charges now requires the query parameter account.",
"action_required": "Send the account query parameter on every GET /charges call.",
"sunset": ""
},
{
"id": "1.5.0-2",
"classification": "breaking",
"source": "diff:response-property-removed",
"fingerprints": [
"4737a336",
"3bdf3d16"
],
"operations": [
"GET /charges",
"POST /charges"
],
"summary": "The receipt_url property is gone from the GET /charges 200 response and the POST /charges 201 response.",
"action_required": "Remove any reliance on receipt_url in both responses.",
"sunset": ""
},
{
"id": "1.5.0-3",
"classification": "breaking",
"source": "diff:api-security-scope-added",
"fingerprints": [
"3b3b713e"
],
"operations": [
"POST /charges"
],
"summary": "POST /charges now requires the oauth scope payments:write.",
"action_required": "Request the payments:write scope in the tokens used for POST /charges.",
"sunset": ""
},
{
"id": "1.5.0-4",
"classification": "breaking",
"source": "diff:api-removed-before-sunset",
"fingerprints": [
"fdca31b3"
],
"operations": [
"POST /charges/{id}/capture"
],
"summary": "The deprecated POST /charges/{id}/capture was removed before its announced sunset date.",
"action_required": "Stop calling POST /charges/{id}/capture.",
"sunset": "2027-03-01"
},
{
"id": "1.5.0-5",
"classification": "potentially-breaking",
"source": "diff:response-property-enum-value-added",
"fingerprints": [
"fd05a761",
"bed7de27"
],
"operations": [
"GET /charges",
"POST /charges"
],
"summary": "The status property in the GET /charges 200 and POST /charges 201 responses can now return disputed.",
"action_required": "Make status handling accept disputed or tolerate unknown values.",
"sunset": ""
}
],
"unapproved": [
"4737a336",
"3bdf3d16",
"3b3b713e",
"fdca31b3",
"ae6b25c8"
],
"caveats": [
"Four breaks lack approval and the major version was not bumped, so this release should not publish as is.",
"No migration notice is recorded for the account parameter change."
]
}
What step 5 prints for it: 1.5.0-1 breaking | 1.5.0-2 breaking | 1.5.0-3 breaking | 1.5.0-4 breaking | 1.5.0-5 potentially-breaking
The page's check: kept - nothing to flag
entries - A date-versioned orders API, breaks acknowledged
The input carries the saved pipeline's decisions (R.laneForm fills carry from the pipeline reply), as the page's handoff does. The diff: 2 err, 2 warn, 2 info; 0 unapproved.
Input (facts is 2082 characters; the first 1399 are shown):
{
"task": "entries",
"api": "API: Parquorn Orders API\nWhat it is and who calls it: A date-versioned orders API; marketplaces pin a version per account and their monitoring pipelines poll the change feed.\nBreaking-change definition: A written breaking-change definition; spec validated against the service: yes; generated from code: no\nVersioning: Date versions (header or account); per-account date versions with response transformers: yes; per-operation usage data: yes\nChangelog readers: Monitoring pipelines and coding agents too; timing: A standing pipeline; declared-change channel today: yes\nPolicy says any auth change is breaking: yes; policy says every response enum may grow: no; audience filter: Gate beta and stable (exclude draft, alpha)\nAs-of date: 2026-11-01",
"diff": "Baseline: version 2026-07-01, 3 operations; candidate: version 2026-10-01, 3 operations\nFindings (fingerprint | check | level | operation | location | text):\n- dc3e7442 | request-property-became-required | err | POST /orders | request shipping_address | the request property shipping_address became required | acknowledged\n- 7499a65e | api-made-internal | err | POST /orders/{id}/reprice | - | POST /orders/{id}/reprice is now marked internal or pre-beta, so it leaves the public contract | acknowledged\n- d3263ad4 | request-parameter-removed | warn (pinned) | GET /orders | parameter query:status | removed the query parameter status\n- c489ea32 | response-property-enum-value-added | warn (pinned) | GET /orders | response 200 [].state | added the value returned to the response property [].state in 200\n- d19b3cdb | new-optional-request-parameter | info | GET /orders | parameter query:page_size | added the optional query parameter page_size\n- 254b2639 | response-property-added | info | GET /orders | response 200 [].carrier | added the property [].carrier to the 200 response\nLevels file: request-parameter-removed warn, response-property-enum-value-added warn\nAcknowledgements: dc3e7442 by Ilse Marrowfield, intended; 7499a65e by Ilse Marrowfield, intended",
"carry": "Rungs now: pr-gate, release-diff, version-derived, feed, approval\nSeverity map: request-parameter-removed warn; new-optional-request-parameter info; response-property-added info; response-property-enum-value-added warn; request-property-became-required err; api-made-internal err\nGate: fail on err; approver: A code-owners rule on the spec, the levels file and the acknowledgement file requires a named API owner such as Ilse Marrowfield to approve each accepted break.\nDeclared fragments carry: id, date version, classification, operations, summary, action required, sunset date, approver",
"facts": "The page's checks (0 fail, 1 warn, 8 pass, 3 n/a); gate verdict: passes, with potentially breaking changes:\n- The two specs | baseline | pass | baseline OpenAPI 3.1.0, version 2026-07-01, 3 operations\n- The two specs | candidate | pass | candidate OpenAPI 3.1.0, version 2026-10-01, 3 operations\n- Preconditions | validated | pass | contract tests or traffic validation\n- Preconditions | determinism | na | hand-written\n- Preconditions | policy | pass | the map encodes it\n- The severity map | levels | pass | 2 pinned checks\n- The gate | gate | pass | 2 breaks, all acknowledged\n- The gate | warn | warn | request-parameter-removed on GET /orders; response-property-enum-value-added on GET /orders\n- The gate | version | na | Date versions (header or account) - per-version baselines do this job\n- The approval record | hygiene | pass | 2 rows complete\n- The approval record | override | na | 2 accepted err findings of 2\n- The approval record | channel | pass | PR question plus change fragments\nRungs by the skill's ranking, in order: pr-gate (now: stops accidental breaks before merge for an hour or two of work); release-diff (now: the default pairing: a complete record of what shipped, behaviour changes included); version-derived (now: date-based per-account versions with response transformers make it a field on records already written); feed (now: machine consumers promote the feed above\n\n[... 683 more characters cut from this display ...]"
}
Reply:
{
"task": "entries",
"summary": "On version 2026-10-01, send shipping_address on POST /orders, stop calling POST /orders/{id}/reprice, drop the status filter on GET /orders, and accept the new returned state.",
"api_version": "2026-10-01",
"entries": [
{
"id": "2026-10-01-1",
"classification": "breaking",
"source": "diff:request-property-became-required",
"fingerprints": [
"dc3e7442"
],
"operations": [
"POST /orders"
],
"summary": "POST /orders now requires shipping_address in the request.",
"action_required": "Include shipping_address in every POST /orders request.",
"sunset": ""
},
{
"id": "2026-10-01-2",
"classification": "breaking",
"source": "diff:api-made-internal",
"fingerprints": [
"7499a65e"
],
"operations": [
"POST /orders/{id}/reprice"
],
"summary": "POST /orders/{id}/reprice is no longer part of the public contract.",
"action_required": "Stop calling POST /orders/{id}/reprice.",
"sunset": ""
},
{
"id": "2026-10-01-3",
"classification": "potentially-breaking",
"source": "diff:request-parameter-removed",
"fingerprints": [
"d3263ad4"
],
"operations": [
"GET /orders"
],
"summary": "GET /orders no longer accepts the status query parameter.",
"action_required": "Remove the status query parameter from GET /orders calls.",
"sunset": ""
},
{
"id": "2026-10-01-4",
"classification": "potentially-breaking",
"source": "diff:response-property-enum-value-added",
"fingerprints": [
"c489ea32"
],
"operations": [
"GET /orders"
],
"summary": "The state property in the GET /orders 200 response can now have the value returned.",
"action_required": "Handle the returned state value or tolerate unknown values.",
"sunset": ""
},
{
"id": "2026-10-01-5",
"classification": "non-breaking",
"source": "diff:new-optional-request-parameter",
"fingerprints": [
"d19b3cdb"
],
"operations": [
"GET /orders"
],
"summary": "GET /orders accepts an optional page_size query parameter.",
"action_required": "",
"sunset": ""
},
{
"id": "2026-10-01-6",
"classification": "non-breaking",
"source": "diff:response-property-added",
"fingerprints": [
"254b2639"
],
"operations": [
"GET /orders"
],
"summary": "The GET /orders 200 response now includes carrier.",
"action_required": "",
"sunset": ""
}
],
"unapproved": [],
"caveats": []
}
What step 5 prints for it: 2026-10-01-1 breaking | 2026-10-01-2 breaking | 2026-10-01-3 potentially-breaking | 2026-10-01-4 potentially-breaking | 2026-10-01-5 non-breaking | 2026-10-01-6 non-breaking
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./cgyaml.js"); require("./cgkit.js"); require("./cgreply.js")), then:
const K = GateKit, R = GateReply;
const f = K.form(Object.assign(K.blank(), { name: "acme", asof: "2026-11-01",
oldspec: "openapi: 3.0.3\ninfo: {title: a, version: 1.0.0}\npaths:\n /pets:\n get:\n responses: {200: {description: ok}}\n /owners:\n get:\n responses: {200: {description: ok}}",
newspec: "openapi: 3.0.3\ninfo: {title: a, version: 1.1.0}\npaths:\n /pets:\n get:\n responses: {200: {description: ok}}" }));
// f.findings (each change: check, level, op, loc, text, fp), f.unapproved, f.ackRows, f.gate (the five gates), f.ladder (the rungs)
// K.severityMd(f), K.levelsFile(f), K.ignoreFile(f), K.entriesJson(f) - the generated files
const b = K.buildInput(f, "pipeline"); // or "entries" (R.laneForm(form, "entries", pipeline) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "pipeline");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. 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="YOUR_TOKEN" # from https://contract-gate.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 = "YOUR_TOKEN" # from https://contract-gate.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"]
// contract-gate.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://contract-gate.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 = "YOUR_TOKEN" // from https://contract-gate.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
}
// ContractGate.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 ContractGate {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://contract-gate.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 = "YOUR_TOKEN" # from https://contract-gate.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 = "YOUR_TOKEN"; // from https://contract-gate.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 = "YOUR_TOKEN"; // from https://contract-gate.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":"contract-gate"}' | 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": "contract-gate"})["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: "contract-gate" })).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": "contract-gate"}, 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", "contract-gate"), 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: "contract-gate" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "contract-gate"])["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 = "contract-gate" }))!["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": "pipeline", "api": "API: Tallowby Payments API\nWhat it is and who calls it: ...", "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- 4737a336 | response-property-removed | err | GET /charges | ...", "carry": "", "facts": "The page's checks (6 fail, ...); gate verdict: the gate fails:\n- The gate | gate | fail | ..."}
# - see the input table
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": "pipeline", "api": "API: Tallowby Payments API\nWhat it is and who calls it: ...", "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- 4737a336 | response-property-removed | err | GET /charges | ...", "carry": "", "facts": "The page's checks (6 fail, ...); gate verdict: the gate fails:\n- The gate | gate | fail | ..."}
# - see the input table
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: "pipeline" or "entries", api, diff, carry, facts} (see the input 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 contract-gate:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A pipeline reply carries summary, rungs, baselines, severity_map, gate, declared, gaps and caveats; an entries reply carries summary, api_version, entries, unapproved and caveats. The code below prints the gist of either: each fired check and its level, or each entry and its classification.
KEY="contract-gate:$(jq -r .task input.json):$(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}'
jq -r 'if .task == "entries" then (.entries[] | "\(.id) \(.classification)")
else (.severity_map[] | "\(.check) \(.level)") end' reply.json
import hashlib, time
key = "contract-gate:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "entries":
for x in reply["entries"]:
print(x["id"], x["classification"])
else:
for x in reply["severity_map"]:
print(x["check"], x["level"])
import { createHash } from "node:crypto";
const key = `contract-gate:${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.task);
if (reply.task === "entries") { for (const x of reply.entries) console.log(x.id, x.classification); }
else { for (const x of reply.severity_map) console.log(x.check, x.level); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "contract-gate:" + 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 }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key := "severity_map"
if reply["task"] == "entries" {
key = "entries"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["check"], xx["level"], xx["id"], xx["classification"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "contract-gate:" + 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.path("task").asText());
if (reply.path("task").asText().equals("entries")) {
for (JsonNode x : reply.path("entries")) System.out.println(x.path("id").asText() + " " + x.path("classification").asText());
} else {
for (JsonNode x : reply.path("severity_map")) System.out.println(x.path("check").asText() + " " + x.path("level").asText());
}
require "digest"
key = "contract-gate:#{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["task"]].inspect
if reply["task"] == "entries"
reply["entries"].each { |x| puts "#{x["id"]} #{x["classification"]}" }
else
reply["severity_map"].each { |x| puts "#{x["check"]} #{x["level"]}" }
end
$key = "contract-gate:" . $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["task"], "\n";
if ($reply["task"] === "entries") { foreach ($reply["entries"] as $x) echo $x["id"], " ", $x["classification"], "\n"; }
else { foreach ($reply["severity_map"] as $x) echo $x["check"], " ", $x["level"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"contract-gate:{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>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "entries") { foreach (var x in reply["entries"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["classification"]}"); }
else { foreach (var x in reply["severity_map"]!.AsArray()) Console.WriteLine($"{x!["check"]} {x["level"]}"); }
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: contract-gate:$(jq -r .task input.json):$(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": "pipeline", "api": "API: Tallowby Payments API\nWhat it is and who calls it: ...", "diff": "Baseline: version 1.4.0, 3 operations; candidate: version 1.5.0, 2 operations\nFindings (fingerprint | check | level | operation | location | text):\n- 4737a336 | response-property-removed | err | GET /charges | ...", "carry": "", "facts": "The page's checks (6 fail, ...); gate verdict: the gate fails:\n- The gate | gate | fail | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "contract-gate:" + rev["task"] + ":" + 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": `contract-gate:${rev.task}:${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", "contract-gate:"+input["task"].(string)+":"+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", "contract-gate:" + input.get("task") + ":" + 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" => "contract-gate:#{JSON.parse(rev)["task"]}:#{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: contract-gate:" . json_decode($rev, true)["task"] . ":" . 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", $"contract-gate:{input["task"]}:{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);