Run VSIX Ready from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page parses both manifests, scans the publish workflow and any plugin.xml as text (nothing is run or installed), runs every contract, architecture, versioning and listing check, diffs the two manifests into fixed change kinds, ranks the job, editor-family and auth ladders, reads the release gates and confirms that no verdict contradicts a failing check, then hands it all to the run as facts. The run writes the go/no-go plan or the release-gate plan; the page then holds it to its own facts (vxreply.js). VSIX Ready is not affiliated with or endorsed by Microsoft, the Eclipse Foundation or JetBrains.
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":"vsix-ready"}), 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, context, manifest, facts) is missing. The page always sends all nine of task, context, manifest, previous, workflow, jetbrains, diff, carry, facts; carry is an empty string for plan, previous and diff are empty strings when no previous package.json was pasted, and workflow and jetbrains are empty when none was pasted. |
| 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 decision, then each release:
plan- go/no-go and the plan: one verdict per selected job with the page's surface (json_schema, language_server, mcp_server, cli_command, thin_shell, shell_launch or rich_extension) and verdict (go, go_later or no_go), the four editor families in the page's order with their rungs, the auth rung, one fix per failing check by check id and subject, the first-release sequence owned by roles in workflow order, and the compatibility matrix's first row - every registry figure with its dagger.release- the release-gate plan: the page's version facts (current, release or pre_release by the minor's parity, ok), one row per manifest change by kind and subject with the page's action (keep, revert, deprecate or narrow), one row per gate with the page's status (pass, fail or unknown), the publish steps one per line for one packed file with federated credentials and the same hash, and the changelog one line per item. Needs the previous package.json inprevious.
A missing or unknown task is answered as plan. Both lanes send the same nine 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 plan's verdicts, auth rung and fixes into carry for the release lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or release. If it is missing or neither, do plan and set "task": "plan". |
context | yes | the interview answers: the jobs and the demand behind each, what exists already, editors by share, remote and browser use, how customers sign in, the owner, restricted fleets, the deadline and the effort ceiling, how the build is marked, the gate answers, the matrix fields and whether the extension is being deprecated. |
manifest | yes | the package.json as the page read it: the ID, version, engines.vscode, entry points, activation events, commands, settings (a credential setting's default is withheld), contributions, capabilities, extensionKind, dependencies and the listing fields. |
previous | send it (empty with no previous package.json; release needs it) | the previous version's package.json read the same way, or empty. |
workflow | send it (empty with no workflow) | the publish workflow's signal lines (line, kind, text, with any written token masked) and whether one packed file goes to both registries, or empty. |
jetbrains | send it (empty with no plugin.xml) | the plugin.xml as read (id, name, vendor, since-build, until-build) and the previous plugin id, or empty. |
diff | send it (empty with no previous package.json) | the page's manifest diff, one row per change (kind, subject, detail, action, consequence, the safe alternative), or empty. |
carry | send it (empty for plan) | release task only: the go/no-go plan's verdicts, auth rung and fixes when the plan ran first; may be empty. |
facts | yes | the page's checks, one line each with the rule, the id, the subject, the status and the evidence separated by vertical bars; then the job ladder, the editor families, the auth ladder, the gates, the version facts, the compatibility matrix, the skill's workflow numbered 1 to 9, the figure list and the footnote. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it and reply with only the JSON object. |
The rules the model applies
From the source skill, @samber/ide-extension-offering - the only standard the run applies:
- Scope: a vendor's official editor integrations - whether to ship an IDE extension, for which job and which editors, and running it as a product: published, trusted, in sync with the API and CLI, and retired cleanly. Not internal or personal extensions, not writing extension or language-server code, not choosing extensions to install.
- Rule 1: the extension must not hold business logic or hand-written API calls that the CLI, SDK or language server already owns - two implementations of one behaviour drift within a few releases. Wrap the CLI or the language server, or generate from the same SDK.
- Rule 2: every VS Code-API extension ships the same packed .vsix to both the VS Code Marketplace and Open VSX, because the Marketplace terms restrict use to Microsoft products; Cursor, VSCodium, Theia and Kiro users install from Open VSX or find a stranger's copy.
- Rule 3: credentials live in the host's secret store (VS Code SecretStorage, JetBrains PasswordSafe) or come from the CLI's own login, never from a settings file - settings sync, dotfile repos and support screenshots publish them.
- Rule 4: telemetry follows the host's telemetry level whatever the extension's own setting says; send events through env.createTelemetryLogger or @vscode/extension-telemetry; the extension's own setting can only narrow collection. JetBrains needs explicit consent and nothing sent while idle.
- Rule 5: a published extension ID, plugin ID or Open VSX namespace never changes and is never removed; deprecate it and point to its successor. A removed Marketplace name stays reserved forever, a JetBrains id change strands every upgrade, and allowlists key on the ID.
- Rule 6: publishing credentials are short-lived or federated, scoped below owner, and absent from the package. Package once (vsce package, then inspect and secret-scan the packed file), publish that file with vsce publish --packagePath and --azure-credential or --oidc, and ovsx publish with --trusted-publishing; global Azure DevOps personal access tokens stop working on 2026-12-01†.
- Step 1, per job: a JSON schema on SchemaStore for config files, a language server for a vendor file format, an MCP server for coding agents, a CLI command for a long-lived local process, an extension only for jobs that need editor UI (browse or edit resources, findings inline, one-click login). A thin shell (commands, tree views, diagnostics, a walkthrough over the CLI or language server) is the default; the rich extension with webviews is starved and needs a staffed owner. Go later when the only demand is a sales or checklist ask; no-go when no job has demand or no owner exists.
- Step 2: one VS Code-API extension on both registries is the default family; the JetBrains plugin is starved (a second codebase plus three platform majors† a year to verify) and is promoted only when JetBrains users are the second-largest group and a second owner who knows Kotlin or Java fits the effort ceiling. Other editors get the language server and the schema, documented. The JetBrains LSP API runs only in commercial JetBrains IDEs (and the free tier of unified PyCharm since 2025.1†).
- Architecture: activate on specific events, never on "*"; bundle with esbuild or webpack; set extensionKind deliberately for remote workspaces; an extension that spawns processes cannot run on vscode.dev - say "desktop only" on the listing.
- Step 4: reuse the CLI's login through a CLI command that prints the session or token as JSON (the default); the AuthenticationProvider over product OAuth is promoted when the product issues OAuth to desktop clients or remote work is common; a pasted key goes straight to the secret store. An extension that runs the CLI or project files declares untrusted workspaces as "limited" or false.
- Versions: major.minor.patch only on the VS Code Marketplace, an odd minor for pre-releases and an even minor for releases; the pre-release channel needs engines.vscode 1.63.0† or above; a version number never republishes - roll back by republishing the last good code as a higher patch. Raise the engines.vscode floor only in a minor with a changelog line, at or below the Code-OSS base of the oldest fork your users run. Keep JetBrains until-build open-ended.
- Identity: the verified-publisher badge needs an extension listed for 6+ months† and a domain registered 6+ months†, then about 5 business days† of review; claim the Open VSX namespace and file the ownership claim.
- Gates - a release ships only when every gate passes: the same .vsix hash on both registries with a clean secret scan of the packed file; integration tests on the minimum and newest host, the pre-release host, the main fork and one remote target; JetBrains Plugin Verifier clean across the declared range; the compatibility matrix updated with the minimum CLI check covered by a test; no usage event with host telemetry at error or off; untrusted-workspace behaviour tested.
- Deprecate when the host absorbs the job, when an MCP server or language server now does it, or when no owner remains: announce with a date, the replacement and the migration steps; ship a last release that shows the notice once; mark it deprecated on every registry; keep the listing installable through the date; never remove it.
- Every registry or platform figure carries † right after it. Close with the footnote: "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it."
House rules
- Use only the input. Never invent a command, a setting, an extension ID, a plugin id, a version, a fork's base version, a date, an install count or a URL; the only figures you may quote are the skill's, each with † as the facts mark it, and the input's own values. When the input does not say, write "not stated in the input".
- The page's verdicts are settled: every check status, every job's surface and verdict, every family rung, the auth rung, every diff action, every gate status and the version facts. Never move them; explain them.
- A fix or a change names the page's check id and its subject exactly as the facts write them - two settings can fail the same check, and one setting can change in two ways.
- Never tell anyone to remove or unpublish a published extension or listing: deprecate it and point to its successor.
- Never put a token, key, secret, password or credential in a setting: it belongs in SecretStorage, PasswordSafe or the CLI's own login.
- Every registry or platform figure carries † right after it; the
footnotefield is exactly "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it." - Never imply that Microsoft, the Eclipse Foundation or JetBrains endorses, recommends, vets or certifies the extension.
- No URL: never write a scheme such as https and never copy a link. Write "the listing", "the changelog" or "the docs" where a link belongs.
- Write in the language of the input; keep the JSON keys, check ids, setting keys, command ids and enum values exactly as given.
- 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
plan
Decide go/no-go per job and plan the first release: the verdict per job, the editor families, the auth rung, one fix per failing check, the first-release sequence and the compatibility matrix.
summary- one sentence, at most 40 words: which jobs the editor does, on which surface, and what stands between the extension and the contract today.verdicts- one object per job the facts list, in the facts' order:job(exactly the facts' job id:config,resources,api,local,login,findingsormcp),surface(exactly the facts' surface:json_schema,language_server,mcp_server,cli_command,thin_shell,shell_launchorrich_extension),verdict(exactly the facts' verdict:go,go_laterorno_go),why(one sentence naming the answer that moved it).families- the facts' four editor families, in the facts' order:family(vscode,jetbrains,other_editorsorvisual_studio),rung(exactly the facts' rung:default,promoted,starved,documented,separateornot_needed),why(one sentence).auth- an object:rung(exactly the facts' rung:authentication_provider,cli_login,secret_store_keyornone),why(one sentence naming the answer that moved it).fixes- one object per check the facts mark fail, and any warn worth fixing:check(the page's check id),subject(the facts' subject for that row, or "" when the row has none),change(one concrete sentence: what to change in the manifest, the workflow or the release process). Every fail appears, by check id and subject.sequence- the first-release sequence, one object per step, following the skill's workflow:step(one sentence),owner(a role, one ofengineering,product,security,release engineering,developer relations,supportorlegal- never a person),when(the number of the workflow step it belongs to, 1 to 9, never going down from one step to the next).matrix- the facts' compatibility-matrix row, one object per column in the facts' order:column(extension,min_cli,min_vscode,since_build,api_versionorstatus),value(exactly the facts' value, "unknown" included).footnote- exactly the footnote.caveats- at most 4 short strings: what the team must confirm, starting with any unanswered interview question;[]when there is nothing to say.
{
"task": "plan",
"summary": "one sentence",
"verdicts": [ { "job": "resources", "surface": "thin_shell", "verdict": "go", "why": "one sentence" } ],
"families": [ { "family": "vscode", "rung": "default", "why": "one sentence" } ],
"auth": { "rung": "cli_login", "why": "one sentence" },
"fixes": [ { "check": "r3_secret_setting", "subject": "acme.apiKey", "change": "one sentence" } ],
"sequence": [ { "step": "one sentence", "owner": "engineering", "when": 3 } ],
"matrix": [ { "column": "extension", "value": "1.4.x" } ],
"footnote": "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it.",
"caveats": [ ]
}
The page holds it to its own facts (check() in vxreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL with a scheme; no backticks; a summary of at most 40 words; every registry figure with its dagger, and the exact footnote; no implied endorsement by Microsoft, the Eclipse Foundation or JetBrains; no advice to remove or unpublish an extension; no credential put in a setting; one verdict per selected job and no invented job, each with the page's surface and verdict (a moved one is put back); the four editor families in the page's order with the page's rungs (put back); the page's auth rung (put back); a fix for every fail check matched by check id and subject, and only the page's checks; sequence owners that are roles and workflow steps from 1 to 9 that never go down; the page's matrix columns and values (put back).
release
Plan the release of this version: the version facts, what every manifest change does to users and its safe action, every gate with what to do about it, the publish steps for one packed file and the changelog.
summary- one sentence, at most 40 words: whether this version can ship as marked, and what must change first.version- an object:current(exactly the facts' current version),kind(exactly the facts' kind:releaseorpre_release),ok(exactly the facts' true or false).changes- one object per diff row, in the facts' order:kind(exactly the row's kind:id_changed,command_removed,command_renamed,setting_removed,setting_type_changed,setting_default_changed,activation_added_star,engine_floor_raised,dependency_added,trust_widened,telemetry_dep_addedorview_removed),subject(exactly the row's subject),consequence(one sentence: what it does to users),action(exactly the row's action:keep,revert,deprecateornarrow).[]when the diff has no row.gates- one object per gate the facts list, in the facts' order:gate(exactly the facts' gate id:same_vsix,secret_scan,host_tests,plugin_verifier,compat_matrix,telemetry_offoruntrusted_test),status(exactly the facts' status:pass,failorunknown),action(one sentence: what to do before shipping, or what keeps it passing).publish- the publish steps in order, plain text, each step on its own line. Separate the lines with a newline, written \n inside the JSON string - for example "Package once: vsce package --out acme-cloud-1.4.0.vsix.\nScan the packed file for secrets before it leaves CI.\nPublish that same file: vsce publish --packagePath acme-cloud-1.4.0.vsix --azure-credential, then ovsx publish acme-cloud-1.4.0.vsix --trusted-publishing.\nVerify the same hash on both registries." Package once, scan the packed file, publish that same file to the VS Code Marketplace and Open VSX with federated credentials, verify the same hash - never a personal access token.changelog- the user-facing changelog lines for this version, plain text, each line on its own line separated by \n, for example "Acme: Open Console has a new command ID; the old one keeps working and is deprecated.\nThe acme.defaultStack setting is deprecated." When the facts raise the engines.vscode floor, one line names the new floor.footnote- exactly the footnote.caveats- at most 4 short strings: what to confirm before publishing;[]when there is nothing to say.
{
"task": "release",
"summary": "one sentence",
"version": { "current": "1.4.0", "kind": "release", "ok": false },
"changes": [ { "kind": "command_renamed", "subject": "acme.openDashboard", "consequence": "one sentence", "action": "deprecate" } ],
"gates": [ { "gate": "same_vsix", "status": "fail", "action": "one sentence" } ],
"publish": "first step\nsecond step\nthird step\nfourth step",
"changelog": "first line\nsecond line",
"footnote": "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it.",
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; the page's version facts (put back if moved); one change per diff row matched by kind and subject, no invented or doubled change, each with the page's action (put back); one gate per page gate with the page's status (put back); publish steps one per line that package once, scan, publish to the VS Code Marketplace and Open VSX with federated credentials and verify the same hash, with no personal access token; a changelog one line per item that names a raised engines.vscode floor.
Worked examples
Three runs from the page's free samples (invented products, hosts on the reserved .example domain): the go/no-go plan for an official extension that fails a lot, its release-gate plan carrying that plan, and the release-gate plan for a clean thin shell over a language server. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the long fields, strings and lists 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.
plan - A first-party extension that fails a lot
The page read it as: not yet - fails to fix first; acme.acme-cloud 1.4.0 (release, not ok); jobs config (json_schema, go), resources (thin_shell, go), login (thin_shell, go); fail checks: r2_open_vsx, r3_secret_setting (acme.apiKey), r4_logger, r6_vsce_token, a_activation, a_untrusted, v_channel.
Input (context is 1876 characters; the first 1392 are shown; facts is 9368 characters; the first 1399 are shown):
{
"task": "plan",
"context": "Extension: Acme Cloud (acme.acme-cloud); build marked: a pre-release; a maintained surface; plan status: confirmed\nJobs (Q1): config (author a config or schema file): real demand (tickets, deals with a job, installs); resources (browse or edit product resources): real demand (tickets, deals with a job, installs); api (call the API from the editor): not asked; local (run a local emulator, tunnel or listener): not asked; login (log in): real demand (tickets, deals with a job, installs); findings (see product findings inline): not asked; mcp (set up the MCP server): not asked\nWhat exists (Q2): a CLI with a --json contract: yes; a CLI command that prints the session or token as JSON: yes; a language server: no; a JSON schema on SchemaStore: no; an MCP server: no; an official extension already: yes; community extension: none known\nEditors by share of paying users (Q3): VS Code: 70%; VS Code forks (Cursor, Windsurf, VSCodium): 20%; JetBrains IDEs: 10%; Visual Studio: not given; Neovim, Zed and other editors: not given; Java, Kotlin or Android-heavy: no; remote work: some; browser (vscode.dev): none\nAuth today (Q4): CLI login: yes; product OAuth for desktop clients: no; API keys: yes\nOwner (Q5): a half-time owner; an owner who knows Kotlin or Java: no; on-call: no; a job needs a view a tree view, quick pick or diagnostic cannot show: no\nRestricted fleets (Q6): allowlists: yes;\n\n[... 484 more characters cut from this display ...]",
"manifest": "The package.json as read: acme.acme-cloud \"Acme Cloud\" version 1.4.0; engines.vscode ^1.90.0; main ./out/extension.js; browser none\nactivationEvents: *\ncommands: acme.deploy (Acme: Deploy); acme.openConsole (Acme: Open Console); acme.listServices (Acme: List Services)\nsettings: acme.apiKey (string; default withheld; Your Acme API key.); acme.region (string; default \"us-east\"; Region for deployments.); acme.telemetry.enabled (boolean; default true; Send usage data to Acme.)\nviews: acmeServices; walkthroughs: none; authentication: none; jsonValidation: none; languages: none; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces not declared; virtualWorkspaces not declared; extensionKind not set\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, compile; bundler: none\ndependencies: axios, @segment/analytics-node; devDependencies: typescript, @types/vscode, vsce\nlisting: repository yes; bugs no; icon images/icon.png; keywords 0; pricing not set; preview no",
"previous": "The previous package.json as read: acme.acme-cloud \"Acme Cloud\" version 1.3.2; engines.vscode ^1.90.0; main ./out/extension.js; browser none\nactivationEvents: *\ncommands: acme.deploy (Acme: Deploy); acme.openDashboard (Acme: Open Console); acme.listServices (Acme: List Services)\nsettings: acme.apiKey (string; default withheld; Your Acme API key.); acme.region (string; default \"us-east\"; Region for deployments.); acme.defaultStack (string; default \"\"; Stack the deploy command targets when none is picked.); acme.telemetry.enabled (boolean; default true; Send usage data to Acme.)\nviews: acmeServices; walkthroughs: none; authentication: none; jsonValidation: none; languages: none; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces not declared; virtualWorkspaces not declared; extensionKind not set\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, compile; bundler: none\ndependencies: axios, @segment/analytics-node; devDependencies: typescript, @types/vscode, vsce\nlisting: repository yes; bugs no; icon images/icon.png; keywords 0; pricing not set; preview no",
"workflow": "The publish workflow (GitHub Actions), signal lines (line | kind | text):\n- 14 | vsce_publish | - run: npx vsce publish -p ${{ secrets.VSCE_PAT }}\nSame packed file to both registries: not published to both",
"jetbrains": "",
"diff": "The manifest diff (kind | subject | detail | action | consequence | the safe alternative):\n- command_renamed | acme.openDashboard | acme.openDashboard -> acme.openConsole | deprecate | keybindings, tasks and scripts that call acme.openDashboard stop working; the new command (acme.openConsole) reaches nobody who bound the old one | keep acme.openDashboard registered as an alias of acme.openConsole and deprecate it - never drop the old ID in the same release\n- setting_removed | acme.defaultStack | Stack the deploy command targets when none is picked. | deprecate | users who set acme.defaultStack lose that behaviour silently, and their settings file keeps a dead key | keep acme.defaultStack readable and mark it deprecated, pointing to its replacement",
"carry": "",
"facts": "The page's checks (7 fail, 7 warn, 9 pass, 5 info, 10 n/a); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:\n- r1 (rule 1) | r1_direct_api | - | warn | axios in dependencies while a CLI with --json exists (interview) - route through the CLI or language server; two implementations of one behaviour drift\n- r1 (rule 1) | r1_cli_detect | - | info | no CLI path setting - detect the CLI on PATH, offer a configurable path and a pinned install, never curl | sh\n- r2 (rule 2) | r2_open_vsx | - | fail | vsce publish runs, ovsx publish never does - Cursor, VSCodium, Theia and Kiro users install from Open VSX or find a stranger's copy\n- r2 (rule 2) | r2_same_file | - | na | not published to both registries\n- r3 (rule 3) | r3_secret_setting | acme.apiKey | fail | acme.apiKey (string) holds a credential in settings - settings sync, dotfile repos and support screenshots publish it; keep it in SecretStorage or take it from the CLI's own login\n- r4 (rule 4) | r4_own_setting | acme.telemetry.enabled | warn | acme.telemetry.enabled - the host's telemetry level wins; gate on it first, and this setting can only narrow collection further\n- r4 (rule 4) | r4_logger | - | fail | @segment/analytics-node in dependencies with no @vscode/extension-telemetry and no env.createTelemetryLogger (interview) - a hand-rolled gate fires against the user's telemetry level\n- r5 (rule 5)\n\n[... 7969 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "plan",
"summary": "Ship the schema plus a thin shell over the Acme CLI for resources and sign-in; first clear seven fails: Open VSX, the API key setting, Segment telemetry, the PAT, activation on star, workspace trust and the even-minor pre-release.",
"verdicts": [
{
"job": "config",
"surface": "json_schema",
"verdict": "go",
"why": "A schema on SchemaStore validates and completes the config file in VS Code, JetBrains and Neovim alike, so no extension code is needed for this job."
},
{
"job": "resources",
"surface": "thin_shell",
"verdict": "go",
"why": "Real demand for browsing Acme services needs editor UI, and the CLI with a --json contract already owns the calls, so the extension renders tree views over it; the near date keeps the default rung."
},
{
"job": "login",
"surface": "thin_shell",
"verdict": "go",
"why": "Real demand for one sign-in, served by a command over the CLI's token command rather than a second login flow; the near date keeps the default rung."
}
],
"families": [
{
"family": "vscode",
"rung": "default",
"why": "One VS Code-API extension reaches VS Code and its forks; with forks at 20% of users, Open VSX is part of the default, not an extra."
},
{
"family": "jetbrains",
"rung": "starved",
"why": "JetBrains users are 10% and the second group, but there is no second owner who knows Kotlin or Java, only a half-time owner, and a near date - a second codebase with three platform majors† a year to verify does not fit."
},
{
"family": "other_editors",
"rung": "not_needed",
"why": "No Neovim, Zed or other editor users were given; the schema still reaches them through the catalog."
},
{
"family": "visual_studio",
"rung": "not_needed",
"why": "No Visual Studio users were given."
}
],
"auth": {
"rung": "cli_login",
"why": "Customers already sign in with the Acme CLI and it has a command that prints the session as JSON, so the extension reuses that login, holds the token in memory only, and no OAuth for desktop clients exists to promote anything higher."
},
"fixes": [
{
"check": "r2_open_vsx",
"subject": "",
"change": "Add an ovsx publish step that ships the very file vsce packed, and claim the acme namespace on Open VSX with an ownership claim before the next release."
},
{
"check": "r3_secret_setting",
"subject": "acme.apiKey",
"change": "Drop acme.apiKey from contributes.configuration, read the key from SecretStorage or the CLI's token command, and tell users to delete the old value from their settings and rotate it."
},
{
"check": "r4_logger",
"subject": "",
"change": "Send every event through @vscode/extension-telemetry or env.createTelemetryLogger instead of calling Segment directly, so the host's telemetry level gates it first."
},
{
"check": "r6_vsce_token",
"subject": "",
"change": "Replace the VSCE_PAT publish with vsce publish --packagePath on the packed file and --azure-credential or --oidc, then revoke the token; global Azure DevOps PATs stop working on 2026-12-01†."
},
{
"check": "a_activation",
"subject": "",
"change": "Activate on the deploy and list commands and on the Acme Services view instead of on star, keeping the CLI check lazy."
}
],
"sequence": [
{
"step": "Publish the JSON schema for the config file to SchemaStore.",
"owner": "engineering",
"when": 1
},
{
"step": "Claim the acme namespace on Open VSX and file the ownership claim.",
"owner": "developer relations",
"when": 2
},
{
"step": "Replace the axios calls with CLI calls, bundle with esbuild, and set extensionKind for remote workspaces.",
"owner": "engineering",
"when": 3
},
{
"step": "Move the API key to SecretStorage, reuse the CLI login and declare limited workspace trust.",
"owner": "security",
"when": 4
},
{
"step": "Publish the compatibility matrix with CLI 3.2.0 as the minimum and check it at activation.",
"owner": "engineering",
"when": 5
}
],
"matrix": [
{
"column": "extension",
"value": "1.4.x"
},
{
"column": "min_cli",
"value": "3.2.0"
},
{
"column": "min_vscode",
"value": "1.90.0"
},
{
"column": "since_build",
"value": "unknown"
},
{
"column": "api_version",
"value": "unknown"
},
{
"column": "status",
"value": "supported"
}
],
"footnote": "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it.",
"caveats": [
"Confirm the oldest fork your users run and compare its Code-OSS base with engines.vscode 1.90.0.",
"The API version for the matrix is not stated in the input.",
"Telemetry-off and untrusted-workspace tests are not yet in place or not confirmed."
]
}
Cut from this display only (the saved reply and the check use the full text): fixes (9 items, 5 shown); sequence (8 items, 5 shown).
What step 5 prints for it: r2_open_vsx | r3_secret_setting acme.apiKey | r4_logger | r6_vsce_token | a_activation | a_untrusted | v_channel | r4_own_setting acme.telemetry.enabled | r1_direct_api
The page's check: kept - nothing to flag
release - A first-party extension that fails a lot
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the manifest diff: 2 changes; gates: same_vsix fail, secret_scan fail, host_tests fail, compat_matrix unknown, telemetry_off fail, untrusted_test unknown.
Input (context is 1876 characters; the first 1392 are shown; carry is 2153 characters; the first 1394 are shown; facts is 9142 characters; the first 1399 are shown):
{
"task": "release",
"context": "Extension: Acme Cloud (acme.acme-cloud); build marked: a pre-release; a maintained surface; plan status: confirmed\nJobs (Q1): config (author a config or schema file): real demand (tickets, deals with a job, installs); resources (browse or edit product resources): real demand (tickets, deals with a job, installs); api (call the API from the editor): not asked; local (run a local emulator, tunnel or listener): not asked; login (log in): real demand (tickets, deals with a job, installs); findings (see product findings inline): not asked; mcp (set up the MCP server): not asked\nWhat exists (Q2): a CLI with a --json contract: yes; a CLI command that prints the session or token as JSON: yes; a language server: no; a JSON schema on SchemaStore: no; an MCP server: no; an official extension already: yes; community extension: none known\nEditors by share of paying users (Q3): VS Code: 70%; VS Code forks (Cursor, Windsurf, VSCodium): 20%; JetBrains IDEs: 10%; Visual Studio: not given; Neovim, Zed and other editors: not given; Java, Kotlin or Android-heavy: no; remote work: some; browser (vscode.dev): none\nAuth today (Q4): CLI login: yes; product OAuth for desktop clients: no; API keys: yes\nOwner (Q5): a half-time owner; an owner who knows Kotlin or Java: no; on-call: no; a job needs a view a tree view, quick pick or diagnostic cannot show: no\nRestricted fleets (Q6): allowlists: yes;\n\n[... 484 more characters cut from this display ...]",
"manifest": "The package.json as read: acme.acme-cloud \"Acme Cloud\" version 1.4.0; engines.vscode ^1.90.0; main ./out/extension.js; browser none\nactivationEvents: *\ncommands: acme.deploy (Acme: Deploy); acme.openConsole (Acme: Open Console); acme.listServices (Acme: List Services)\nsettings: acme.apiKey (string; default withheld; Your Acme API key.); acme.region (string; default \"us-east\"; Region for deployments.); acme.telemetry.enabled (boolean; default true; Send usage data to Acme.)\nviews: acmeServices; walkthroughs: none; authentication: none; jsonValidation: none; languages: none; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces not declared; virtualWorkspaces not declared; extensionKind not set\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, compile; bundler: none\ndependencies: axios, @segment/analytics-node; devDependencies: typescript, @types/vscode, vsce\nlisting: repository yes; bugs no; icon images/icon.png; keywords 0; pricing not set; preview no",
"previous": "The previous package.json as read: acme.acme-cloud \"Acme Cloud\" version 1.3.2; engines.vscode ^1.90.0; main ./out/extension.js; browser none\nactivationEvents: *\ncommands: acme.deploy (Acme: Deploy); acme.openDashboard (Acme: Open Console); acme.listServices (Acme: List Services)\nsettings: acme.apiKey (string; default withheld; Your Acme API key.); acme.region (string; default \"us-east\"; Region for deployments.); acme.defaultStack (string; default \"\"; Stack the deploy command targets when none is picked.); acme.telemetry.enabled (boolean; default true; Send usage data to Acme.)\nviews: acmeServices; walkthroughs: none; authentication: none; jsonValidation: none; languages: none; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces not declared; virtualWorkspaces not declared; extensionKind not set\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, compile; bundler: none\ndependencies: axios, @segment/analytics-node; devDependencies: typescript, @types/vscode, vsce\nlisting: repository yes; bugs no; icon images/icon.png; keywords 0; pricing not set; preview no",
"workflow": "The publish workflow (GitHub Actions), signal lines (line | kind | text):\n- 14 | vsce_publish | - run: npx vsce publish -p ${{ secrets.VSCE_PAT }}\nSame packed file to both registries: not published to both",
"jetbrains": "",
"diff": "The manifest diff (kind | subject | detail | action | consequence | the safe alternative):\n- command_renamed | acme.openDashboard | acme.openDashboard -> acme.openConsole | deprecate | keybindings, tasks and scripts that call acme.openDashboard stop working; the new command (acme.openConsole) reaches nobody who bound the old one | keep acme.openDashboard registered as an alias of acme.openConsole and deprecate it - never drop the old ID in the same release\n- setting_removed | acme.defaultStack | Stack the deploy command targets when none is picked. | deprecate | users who set acme.defaultStack lose that behaviour silently, and their settings file keeps a dead key | keep acme.defaultStack readable and mark it deprecated, pointing to its replacement",
"carry": "The go/no-go plan's verdicts (job: surface, verdict):\n- config: json_schema, go - A schema on SchemaStore validates and completes the config file in VS Code, JetBrains and Neovim alike, so no extension code is needed for this job.\n- resources: thin_shell, go - Real demand for browsing Acme services needs editor UI, and the CLI with a --json contract already owns the calls, so the extension renders tree views over it; the near date keeps the default rung.\n- login: thin_shell, go - Real demand for one sign-in, served by a command over the CLI's token command rather than a second login flow; the near date keeps the default rung.\nThe plan's auth rung: cli_login\nThe plan's fixes:\n- r2_open_vsx: Add an ovsx publish step that ships the very file vsce packed, and claim the acme namespace on Open VSX with an ownership claim before the next release.\n- r3_secret_setting (acme.apiKey): Drop acme.apiKey from contributes.configuration, read the key from SecretStorage or the CLI's token command, and tell users to delete the old value from their settings and rotate it.\n- r4_logger: Send every event through @vscode/extension-telemetry or env.createTelemetryLogger instead of calling Segment directly, so the host's telemetry level gates it first.\n- r6_vsce_token: Replace the VSCE_PAT publish with vsce publish --packagePath on the packed file and --azure-credential or --oidc, then revoke the\n\n[... 759 more characters cut from this display ...]",
"facts": "The page's checks (7 fail, 7 warn, 9 pass, 5 info, 10 n/a); verdict: not yet - fails to fix first; rule | id | subject | status | evidence:\n- r1 (rule 1) | r1_direct_api | - | warn | axios in dependencies while a CLI with --json exists (interview) - route through the CLI or language server; two implementations of one behaviour drift\n- r1 (rule 1) | r1_cli_detect | - | info | no CLI path setting - detect the CLI on PATH, offer a configurable path and a pinned install, never curl | sh\n- r2 (rule 2) | r2_open_vsx | - | fail | vsce publish runs, ovsx publish never does - Cursor, VSCodium, Theia and Kiro users install from Open VSX or find a stranger's copy\n- r2 (rule 2) | r2_same_file | - | na | not published to both registries\n- r3 (rule 3) | r3_secret_setting | acme.apiKey | fail | acme.apiKey (string) holds a credential in settings - settings sync, dotfile repos and support screenshots publish it; keep it in SecretStorage or take it from the CLI's own login\n- r4 (rule 4) | r4_own_setting | acme.telemetry.enabled | warn | acme.telemetry.enabled - the host's telemetry level wins; gate on it first, and this setting can only narrow collection further\n- r4 (rule 4) | r4_logger | - | fail | @segment/analytics-node in dependencies with no @vscode/extension-telemetry and no env.createTelemetryLogger (interview) - a hand-rolled gate fires against the user's telemetry level\n- r5 (rule 5)\n\n[... 7743 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "release",
"summary": "1.4.0 cannot ship as a pre-release: an even minor reads as a release. Keep the old command and setting deprecated, publish one packed file to both registries with federated credentials, and clear four failing gates first.",
"version": {
"current": "1.4.0",
"kind": "release",
"ok": false
},
"changes": [
{
"kind": "command_renamed",
"subject": "acme.openDashboard",
"consequence": "Keybindings, tasks and scripts that call acme.openDashboard stop working, and nobody who bound it reaches acme.openConsole.",
"action": "deprecate"
},
{
"kind": "setting_removed",
"subject": "acme.defaultStack",
"consequence": "Users who set acme.defaultStack silently lose the stack their deploys targeted.",
"action": "deprecate"
}
],
"gates": [
{
"gate": "same_vsix",
"status": "fail",
"action": "Package once and publish that one file to both the VS Code Marketplace and Open VSX, then compare the two hashes."
},
{
"gate": "secret_scan",
"status": "fail",
"action": "Add a step that unpacks the packed file and scans it for secrets before any publish runs."
},
{
"gate": "host_tests",
"status": "fail",
"action": "Add integration tests with the VS Code test CLI on engines 1.90.0, stable and Insiders, plus the main fork and one remote target."
},
{
"gate": "compat_matrix",
"status": "unknown",
"action": "Confirm the matrix lists 1.4.x with CLI 3.2.0 and that the minimum CLI check at activation has a test."
},
{
"gate": "telemetry_off",
"status": "fail",
"action": "Test that no Segment event fires with the host telemetry level at error or off, once events go through the host logger."
},
{
"gate": "untrusted_test",
"status": "unknown",
"action": "Confirm a test opens an untrusted workspace and sees the CLI paths disabled."
}
],
"publish": "Package once: vsce package --out acme-cloud-1.4.0.vsix, then inspect its file list.\nScan the packed file for secrets before it leaves CI.\nPublish that same file to the VS Code Marketplace with vsce publish --packagePath acme-cloud-1.4.0.vsix [... 160 more characters cut from this display]",
"changelog": "Acme: Open Console has a new command ID, acme.openConsole; the old acme.openDashboard keeps working and is deprecated.\nThe acme.defaultStack setting is deprecated and still read in this version.\nAcme Cloud now publishes to Open VSX as well as the VS Code [... 13 more characters cut from this display]",
"footnote": "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it.",
"caveats": [
"Decide whether 1.4.0 ships as a release or moves to an odd minor before packaging.",
"Revoke VSCE_PAT once the federated publish works; global Azure DevOps PATs stop working on 2026-12-01†."
]
}
Cut from this display only (the saved reply and the check use the full text): publish (401 characters, 241 shown); changelog (267 characters, 254 shown).
What step 5 prints for it: command_renamed acme.openDashboard -> deprecate | setting_removed acme.defaultStack -> deprecate
The page's check: kept - nothing to flag
release - A clean thin shell over a language server
The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the manifest diff: 1 changes; gates: same_vsix pass, secret_scan pass, host_tests pass, compat_matrix pass, telemetry_off pass, untrusted_test unknown.
Input (context is 1821 characters; the first 1396 are shown; facts is 7755 characters; the first 1367 are shown):
{
"task": "release",
"context": "Extension: Lumen Lint (quillvane.lumen-lint); build marked: a release; a maintained surface; plan status: confirmed\nJobs (Q1): config (author a config or schema file): real demand (tickets, deals with a job, installs); resources (browse or edit product resources): not asked; api (call the API from the editor): not asked; local (run a local emulator, tunnel or listener): not asked; login (log in): not asked; findings (see product findings inline): real demand (tickets, deals with a job, installs); mcp (set up the MCP server): not asked\nWhat exists (Q2): a CLI with a --json contract: no; a CLI command that prints the session or token as JSON: no; a language server: yes; a JSON schema on SchemaStore: yes; an MCP server: no; an official extension already: yes; community extension: none known\nEditors by share of paying users (Q3): VS Code: 60%; VS Code forks (Cursor, Windsurf, VSCodium): 25%; JetBrains IDEs: 10%; Visual Studio: not given; Neovim, Zed and other editors: 5%; Java, Kotlin or Android-heavy: no; remote work: some; browser (vscode.dev): none\nAuth today (Q4): CLI login: no; product OAuth for desktop clients: no; API keys: no\nOwner (Q5): a staffed team; an owner who knows Kotlin or Java: no; on-call: yes; a job needs a view a tree view, quick pick or diagnostic cannot show: no\nRestricted fleets (Q6): allowlists: no; proxies with custom CAs: no; air-gapped machines: no;\n\n[... 425 more characters cut from this display ...]",
"manifest": "The package.json as read: quillvane.lumen-lint \"Lumen Lint\" version 2.4.0; engines.vscode ^1.95.0; main ./dist/extension.js; browser none\nactivationEvents: onLanguage:lumen, workspaceContains:**/.lumenrc.json\ncommands: lumen.restartServer (Lumen: Restart Server); lumen.showOutput (Lumen: Show Output); lumen.fixAll (Lumen: Fix All Problems)\nsettings: lumen.serverPath (string; default \"\"; Path to the lumen-ls binary; empty finds it on PATH.); lumen.trace.server (string; default \"off\"; Traces the communication with the language server.)\nviews: none; walkthroughs: lumen.start; authentication: none; jsonValidation: .lumenrc.json (remote schema); languages: lumen; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces \"limited\" (Lumen Lint starts the language server only in trusted workspaces.); restrictedConfigurations lumen.serverPath; virtualWorkspaces false; extensionKind workspace\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, bundle, test; bundler: esbuild\ndependencies: vscode-languageclient, @vscode/extension-telemetry; devDependencies: esbuild, @vscode/vsce, @vscode/test-cli, @vscode/test-electron, ovsx, typescript\nlisting: repository yes; bugs yes; icon media/icon.png; keywords 3; pricing not set; preview no",
"previous": "The previous package.json as read: quillvane.lumen-lint \"Lumen Lint\" version 2.2.1; engines.vscode ^1.90.0; main ./dist/extension.js; browser none\nactivationEvents: onLanguage:lumen, workspaceContains:**/.lumenrc.json\ncommands: lumen.restartServer (Lumen: Restart Server); lumen.showOutput (Lumen: Show Output)\nsettings: lumen.serverPath (string; default \"\"; Path to the lumen-ls binary; empty finds it on PATH.); lumen.trace.server (string; default \"off\"; Traces the communication with the language server.)\nviews: none; walkthroughs: lumen.start; authentication: none; jsonValidation: .lumenrc.json (remote schema); languages: lumen; debuggers: none; mcpServerDefinitionProviders: none\ncapabilities: untrustedWorkspaces \"limited\" (Lumen Lint starts the language server only in trusted workspaces.); restrictedConfigurations lumen.serverPath; virtualWorkspaces false; extensionKind workspace\nextensionDependencies: none; extensionPack: none\nscripts: vscode:prepublish, bundle, test; bundler: esbuild\ndependencies: vscode-languageclient, @vscode/extension-telemetry; devDependencies: esbuild, @vscode/vsce, @vscode/test-cli, @vscode/test-electron, ovsx, typescript\nlisting: repository yes; bugs yes; icon media/icon.png; keywords 3; pricing not set; preview no",
"workflow": "The publish workflow (GitHub Actions), signal lines (line | kind | text):\n- 7 | id_token_write | id-token: write\n- 12 | tests_insiders | vscode: [\"1.95.0\", \"stable\", \"insiders\"]\n- 12 | tests_stable | vscode: [\"1.95.0\", \"stable\", \"insiders\"]\n- 20 | tests | - run: xvfb-run -a npx vscode-test\n- 32 | vsce_package | - run: npx vsce package --out lumen-lint.vsix\n- 34 | secret_scan | run: unzip -q lumen-lint.vsix -d packed && npx secretlint \"packed/**/*\"\n- 40 | vsce_publish | - run: npx vsce publish --packagePath lumen-lint.vsix --azure-credential\n- 41 | ovsx_publish | - run: npx ovsx publish lumen-lint.vsix --trusted-publishing\nSame packed file to both registries: yes",
"jetbrains": "",
"diff": "The manifest diff (kind | subject | detail | action | consequence | the safe alternative):\n- engine_floor_raised | engines.vscode | 1.90.0 -> 1.95.0 | keep | users on a host or fork older than 1.95.0 stop receiving updates | keep it in this minor, with a changelog line naming the new floor",
"carry": "The go/no-go plan's verdicts (job: surface, verdict):\n- config: json_schema, go - The schema already on SchemaStore validates .lumenrc.json in every editor that reads the catalog, so no extension code is needed for it.\n- findings: thin_shell, go - Inline findings need diagnostics in the editor, and the language server already owns them, so the extension stays a thin client over it with a staffed team behind it.\nThe plan's auth rung: none",
"facts": "The page's checks (0 fail, 0 warn, 24 pass, 4 info, 9 n/a); verdict: ready; rule | id | subject | status | evidence:\n- r1 (rule 1) | r1_direct_api | - | pass | no HTTP client or SDK in dependencies\n- r1 (rule 1) | r1_cli_detect | - | na | no CLI with --json (interview)\n- r2 (rule 2) | r2_open_vsx | - | pass | vsce publish and ovsx publish both run\n- r2 (rule 2) | r2_same_file | - | pass | one vsce package, one file published to both (lumen-lint.vsix)\n- r3 (rule 3) | r3_secret_setting | - | pass | 2 settings, none named for a token, key, secret, password or credential\n- r4 (rule 4) | r4_own_setting | - | pass | no telemetry setting of its own\n- r4 (rule 4) | r4_logger | - | pass | @vscode/extension-telemetry honours every telemetry level\n- r5 (rule 5) | r5_ext_id | - | pass | quillvane.lumen-lint in both versions\n- r5 (rule 5) | r5_plugin_id | - | na | no plugin.xml pasted\n- r6 (rule 6) | r6_vsce_token | - | pass | vsce publish uses --azure-credential\n- r6 (rule 6) | r6_ovsx_token | - | pass | ovsx publish --trusted-publishing\n- r6 (rule 6) | r6_id_token | - | pass | permissions grant id-token: write\n- r6 (rule 6) | r6_literal_token | - | pass | no token literal; credentials come from CI secrets or federation\n- r6 (rule 6) | r6_secret_scan | - | pass | a secret scan step runs\n- arch | a_activation | - | pass | activationEvents: onLanguage:lumen,\n\n[... 6388 more characters cut from this display ...]"
}
Reply (shortened for display - see the cuts below):
{
"task": "release",
"summary": "2.4.0 ships as a release: an even minor, the engines floor raised in a minor with a changelog line, one packed file to both registries; only the untrusted-workspace test is unconfirmed.",
"version": {
"current": "2.4.0",
"kind": "release",
"ok": true
},
"changes": [
{
"kind": "engine_floor_raised",
"subject": "engines.vscode",
"consequence": "Users on a host or fork older than 1.95.0 stop receiving updates and stay on 2.2.1.",
"action": "keep"
}
],
"gates": [
{
"gate": "same_vsix",
"status": "pass",
"action": "Keep publishing the one lumen-lint.vsix to both registries and comparing its hash."
},
{
"gate": "secret_scan",
"status": "pass",
"action": "Keep scanning the unpacked file before every publish."
},
{
"gate": "host_tests",
"status": "pass",
"action": "Keep the 1.95.0, stable and Insiders matrix, plus the main fork and the remote target."
},
{
"gate": "compat_matrix",
"status": "pass",
"action": "Add the 2.4.x row with engines 1.95.0 and language server 2.4.0."
},
{
"gate": "telemetry_off",
"status": "pass",
"action": "Keep the test that no event fires with telemetry at error or off."
},
{
"gate": "untrusted_test",
"status": "unknown",
"action": "Confirm a test opens an untrusted workspace and sees the language server stay off."
}
],
"publish": "Package once: vsce package --out lumen-lint.vsix.\nScan the packed file for secrets with the secretlint step.\nPublish that same file to the VS Code Marketplace with vsce publish --packagePath lumen-lint.vsix --azure-credential, and to Open VSX with ovsx [... 111 more characters cut from this display]",
"changelog": "Lumen Lint now needs VS Code 1.95.0 or newer.\nNew command: Lumen: Fix All Problems.",
"footnote": "† verify against the current registry documentation (VS Code Marketplace, Open VSX, JetBrains Marketplace) before quoting it.",
"caveats": [
"Confirm the oldest fork users run is based on Code-OSS 1.95.0 or newer before the floor ships."
]
}
Cut from this display only (the saved reply and the check use the full text): publish (363 characters, 252 shown).
What step 5 prints for it: engine_floor_raised engines.vscode -> keep
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./vxkit.js"); require("./vxreply.js")), then:
const K = VxKit, R = VxReply;
const prev = { name: "acme-cloud", publisher: "acme", version: "1.3.2", engines: { vscode: "^1.90.0" }, activationEvents: ["onCommand:acme.deploy"],
contributes: { configuration: { properties: { "acme.region": { type: "string", default: "us-east", description: "Region." } } } } };
const next = JSON.parse(JSON.stringify(prev)); next.version = "1.4.0"; next.contributes.configuration.properties["acme.region"].default = "eu-west";
const f = K.form(Object.assign(K.blank(), { manifest: JSON.stringify(next), previous: JSON.stringify(prev), job_resources: "demand", has_cli_json: "yes", owner: "staffed" }));
// f.parsed and f.prevParsed (an error with its line and column), f.m and f.prevM (the manifests as read), f.wf (the workflow scan),
// f.rows (the checks), f.changes.rows (kind, subject, action), f.jobs, f.families, f.auth, f.gates, f.vf (version facts), f.matrix,
// f.contradictions (verdicts a failing check argues against - always empty for a settled board)
// K.checksCsv(f), K.diffCsv(f), K.matrixMd(f), K.board(f), K.applyPlan(f), K.fixedWorkflow(f), K.deprecationNotice(f) - the exports
const b = K.buildInput(f, "release"); // or "plan" (R.laneForm(form, "release", planReply) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "release");
// R.check(reply, b) // {status: kept or flagged, notes, fixed}
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://vsix-ready.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://vsix-ready.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"]
// vsix-ready.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://vsix-ready.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://vsix-ready.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
}
// VsixReady.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 VsixReady {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://vsix-ready.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://vsix-ready.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://vsix-ready.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://vsix-ready.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":"vsix-ready"}' | 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": "vsix-ready"})["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: "vsix-ready" })).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": "vsix-ready"}, 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", "vsix-ready"), 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: "vsix-ready" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "vsix-ready"])["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 = "vsix-ready" }))!["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": "plan", "context": "Extension: Acme Cloud (acme.acme-cloud); build marked: a pre-release ...", "manifest": "The package.json as read: acme.acme-cloud "Acme Cloud" version 1.4.0 ...", "previous": "The previous package.json as read: ...", "workflow": "The publish workflow (GitHub Actions), signal lines ...", "jetbrains": "", "diff": "The manifest diff (kind | subject | ...): ...", "carry": "", "facts": "The page's checks (7 fail, ...); rule | id | subject | status | evidence: ..."}
# - 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": "plan", "context": "Extension: Acme Cloud (acme.acme-cloud); build marked: a pre-release ...", "manifest": "The package.json as read: acme.acme-cloud "Acme Cloud" version 1.4.0 ...", "previous": "The previous package.json as read: ...", "workflow": "The publish workflow (GitHub Actions), signal lines ...", "jetbrains": "", "diff": "The manifest diff (kind | subject | ...): ...", "carry": "", "facts": "The page's checks (7 fail, ...); rule | id | subject | status | evidence: ..."}
# - 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: "plan" or "release", context, manifest, previous, workflow, jetbrains, 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 vsix-ready:<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 plan reply carries summary, verdicts, families, auth, fixes, sequence, matrix, footnote and caveats; a release-lane reply carries summary, version, changes, gates, publish, changelog, footnote and caveats. The code below prints the gist of either: each fix of the plan with its check, or each change with its action.
KEY="vsix-ready:$(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 == "release" then (.changes[] | "\(.kind) \(.subject) -> \(.action)")
else (.fixes[] | "\(.check) \(.subject) \(.change)") end' reply.json
import hashlib, time
key = "vsix-ready:" + 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"] == "release":
for x in reply["changes"]:
print(x["kind"], x["subject"], "->", x["action"])
print(reply["version"]["current"], reply["version"]["ok"])
else:
for x in reply["fixes"]:
print(x["check"], x["subject"], x["change"])
import { createHash } from "node:crypto";
const key = `vsix-ready:${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 === "release") { for (const x of reply.changes) console.log(x.kind, x.subject, "->", x.action); console.log(reply.version.current, reply.version.ok); }
else { for (const x of reply.fixes) console.log(x.check, x.subject, x.change); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "vsix-ready:" + 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, name, field := "fixes", "check", "change"
if reply["task"] == "release" {
key, name, field = "changes", "kind", "action"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx[field])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "vsix-ready:" + 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("release")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("kind").asText() + " " + x.path("subject").asText() + " -> " + x.path("action").asText());
} else {
for (JsonNode x : reply.path("fixes")) System.out.println(x.path("check").asText() + " " + x.path("change").asText());
}
require "digest"
key = "vsix-ready:#{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"] == "release"
reply["changes"].each { |x| puts "#{x["kind"]} #{x["subject"]} -> #{x["action"]}" }
else
reply["fixes"].each { |x| puts "#{x["check"]} #{x["change"]}" }
end
$key = "vsix-ready:" . $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"] === "release") { foreach ($reply["changes"] as $x) echo $x["kind"], " ", $x["subject"], " -> ", $x["action"], "\n"; }
else { foreach ($reply["fixes"] as $x) echo $x["check"], " ", $x["change"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"vsix-ready:{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 == "release") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["kind"]} {x["subject"]} -> {x["action"]}"); }
else { foreach (var x in reply["fixes"]!.AsArray()) Console.WriteLine($"{x!["check"]} {x["change"]}"); }
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: vsix-ready:$(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": "plan", "context": "Extension: Acme Cloud (acme.acme-cloud); build marked: a pre-release ...", "manifest": "The package.json as read: acme.acme-cloud "Acme Cloud" version 1.4.0 ...", "previous": "The previous package.json as read: ...", "workflow": "The publish workflow (GitHub Actions), signal lines ...", "jetbrains": "", "diff": "The manifest diff (kind | subject | ...): ...", "carry": "", "facts": "The page's checks (7 fail, ...); rule | id | subject | status | evidence: ..."}
# - 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": "vsix-ready:" + 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": `vsix-ready:${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", "vsix-ready:"+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", "vsix-ready:" + 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" => "vsix-ready:#{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: vsix-ready:" . 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", $"vsix-ready:{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);