Run Shoehorn Desk from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page masks comments, strings, template literals and regex literals, finds every type assertion with the exact span of its expression and type, classes each one, and migrates the safe ones itself, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans the migrated file with the same scanner (shoereply.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":"shoehorn-desk"}), 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, unit, code, facts) is missing. The page always sends all five of task, unit, code, carry, facts; carry is an empty string for review. |
| 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 - inspect, then produce:
review- each of the page's findings confirmed (with the issue, the fix and a short code fix) or cleared (with why), up to five issues a scan cannot see on real lines, and the fixes in the order to make them, each naming its ids.rewrite- the whole test file migrated - fromPartial and fromAny (with the type as the generic where nothing else gives it), the import merged, DOM narrowing kept as a check, the value casts decided - with what changed and what was kept, every test title unchanged.
A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.
The input
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite. If it is missing or neither, do review and set "task": "review". |
unit | yes | the file's path and its test runner. |
code | yes | the test file as pasted (before the page's migration). A line starting "// [... N lines cut by the page" marks code the page scanned but did not send. |
carry | send it (empty for review) | rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty. |
facts | yes | the page's scan: one finding per line as id | area | severity | line | where | evidence | source, and every assertion with its line, expression, type and what the page did with it. 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, @mattpocock/migrate-to-shoehorn - the only standard the run applies:
- Test code only: shoehorn is never used in production code.
- Why: as in tests is a habit people are trained out of, it makes you spell the target type by hand, and deliberately wrong data needs the double "as unknown as Type".
- A large object where the test needs only a few properties: pass just those through fromPartial instead of faking the whole shape. Drop a placeholder only when nothing can call it: fromPartial changes the type, not the value, so a method the code under test may still call (a logger's info) stays, and the reply says so.
- "as Type" becomes fromPartial(...): partial data that still type-checks.
- "as unknown as Type" becomes fromAny(...): intentionally wrong data, which keeps autocomplete.
- fromExact(...) forces the full object (it can be swapped for fromPartial later).
- Add the imports from @total-typescript/shoehorn (install with npm i @total-typescript/shoehorn), then run the type check to verify.
- The page also applies what TypeScript requires: a call argument or a JSX attribute gives the value its type (fromPartial(...), fromAny(...)); anywhere else (a const with no annotation) the type goes in the generic - fromPartial<Request>(...) - or the result is unknown. A cast that narrows a DOM query result is not test data, and shoehorn does not belong there; nor does a cast that only reaches a mock's methods (vi.mocked(fn) or jest.mocked(fn) types it). At runtime fromPartial, fromAny and fromExact return their argument unchanged - they only change the type.
House rules
- Use only the input. Never invent a type, a property, a helper or a value the file does not hold. When deciding a cast on a value needs code you cannot see (what loadSession returns, which properties a function reads), say so in a caveat and pick the reading the test's own expects support.
- The page's ids and lines are measured facts: never renumber an id.
- No web links (http or https) in prose.
- Write in English. Keep the JSON keys and the ids exactly as given.
- Never use backticks. Name calls plainly (fromPartial<Request>(...), screen.getByRole). Words from the rules above are free to use; what you never do is paste a whole sentence from this prompt.
- Never write a gendered pronoun for a user; write they, the user or the person.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
review
Confirm or clear each of the page's findings, decide the casts the page left, add what a scan cannot see, and put the steps in order.
summary- one sentence, at most 40 words: how much of the file the page migrated, what is left to decide, and the first step.findings- one object per page finding you confirm, in id order:id(the page's id),widget(the test it sits in, e.g. "it - returns the user for an id"),issue(one sentence: what the assertion hides today),fix(one sentence: the decision - fromPartial, fromPartial<T>, fromAny, fromExact, keep a narrowing check, or fix the type),code(the migrated line or lines, at most 6; "" when fix says it all).cleared- one object per page finding that is not a problem here, withidandwhy(one sentence tied to the code). Every page id appears exactly once, in findings or in cleared.extra- at most 5 issues the scan cannot see, each tied to a line of the code:id(X1, X2, ...),area(partial, wrong, decide, scope or imports),line(a number),widget,issue,fix. Examples: placeholder properties a fromPartial no longer needs, a test whose expects read a property the partial data never sets, fromAny where fromPartial would still type-check (or the reverse), a helper that builds test data with as inside it, a spot where fromExact would catch a shape change.[]when there are none.plan- 2 to 10 short strings, the steps in order (what stops the file compiling first, then the decisions, then the page's mechanical rewrites to accept, then the type check), each naming the ids it covers (e.g. "Decide loadSession() as Session (S4)"). Every confirmed id appears in a step.[]only when nothing is confirmed.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "review",
"summary": "one sentence",
"findings": [ { "id": "S1", "widget": "it - returns the user for an id", "issue": "one sentence", "fix": "one sentence", "code": "await getUser(fromPartial({ params: { id: \"123\" } }), res);" } ],
"cleared": [ { "id": "S2", "why": "one sentence" } ],
"extra": [ { "id": "X1", "area": "partial", "line": 20, "widget": "it - reads the tenant header", "issue": "one sentence", "fix": "one sentence" } ],
"plan": [ "Add the shoehorn import (S3)" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in shoereply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link in the prose.
rewrite
Rewrite the whole test file, migrated.
summary- one sentence, at most 40 words: what the migration changed and what it left for the team.code- the complete migrated file. Keep every import, describe, it and test with its title word for word, and every expect; change only the assertions the findings name (and any carried extra), plus the import from @total-typescript/shoehorn. Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. A literal asserted "as Type" - fromPartial(the literal) as a call argument or JSX attribute; fromPartial<Type>(the literal) anywhere else. "as unknown as Type" or "as any" on data - fromAny(...) as a call argument or JSX attribute, fromAny<Type>(...) elsewhere; when the data is only incomplete rather than wrong (the double cast was there to get past a missing-property error), fromPartial<Type>(...) keeps the type check, and the review says which it chose; an "as any" with no type to keep needs the type the test means - name it from the code, or say in a caveat what is missing. A cast on a value - the decision from the carry; when it is test data, fromPartial or fromAny as above; when it narrows a DOM query, keep a check (expect(el).toBeInstanceOf(HTMLInputElement) and a narrowing, or the typed query) rather than shoehorn. A function called but not imported - add it to one import from @total-typescript/shoehorn (merge with an existing one). A file that is not test code - change nothing and say so; shoehorn never goes into production code.changes- one object per page finding you acted on, in id order:id,change(one sentence: what changed, naming the test). Extra ids (X1, ...) from the carried review may appear too.kept- one object per page finding you did not change:id,why(one sentence - the review cleared it, it is DOM narrowing kept as a check, or it needs code the file does not hold). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rewrite",
"summary": "one sentence",
"code": "the complete migrated file",
"changes": [ { "id": "S1", "change": "one sentence" } ],
"kept": [ { "id": "S2", "why": "one sentence" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every test title survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the rewritten code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A test title the rewrite drops or renames is flagged.
Worked examples
Three runs from the page's free samples (invented apps): the review of a React form test with casts to decide, the rewrite of the same file carrying that review, and the review of an Express handler suite the page migrates on its own. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, 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.
review - A React form test with casts to decide
The page read it as: Needs decisions - 0 blocker, 4 major, 1 minor; findings S1 literal-assertion, S2 dom-narrowing, S3 missing-import, S4 untyped-shoehorn, S5 value-assertion.
Input (facts is 1961 characters; the first 1394 are shown):
{
"task": "review",
"unit": "File: src/SignupForm.test.tsx\nRunner: Vitest",
"code": "import { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { vi, it, expect } from \"vitest\";\nimport { SignupForm, type SignupFormProps } from \"./SignupForm\";\nimport { api } from \"./api\";\nimport { loadSession, type Session } from \"./session\";\n\nit(\"submits the email\", async () => {\n const props = { onSubmit: vi.fn() } as SignupFormProps;\n render(<SignupForm {...props} />);\n const email = screen.getByLabelText(\"Email\") as HTMLInputElement;\n await userEvent.type(email, \"a@b.co\");\n await userEvent.click(screen.getByRole(\"button\", { name: \"Sign up\" }));\n expect(props.onSubmit).toHaveBeenCalledWith({ email: \"a@b.co\" });\n expect(email.value).toBe(\"a@b.co\");\n});\n\nit(\"shows the server error\", async () => {\n vi.spyOn(api, \"signup\").mockRejectedValue(new Error(\"taken\"));\n const user = fromPartial({ id: \"u1\" });\n const session = loadSession() as Session;\n render(<SignupForm onSubmit={api.signup} user={user} session={session} />);\n await userEvent.click(screen.getByRole(\"button\", { name: \"Sign up\" }));\n expect(await screen.findByText(\"taken\")).toBeTruthy();\n});\n",
"carry": "",
"facts": "The page's scan of 26 lines - 3 type assertions: 1 migrated by the page (1 fromPartial, 0 fromAny), 1 to decide, 1 DOM narrowing; 2 describe/it/test titles; verdict: Needs decisions.\nFindings (id | area | severity | line | where | evidence | source):\n- S1 | partial | minor | line 9 | inside a test | An object literal asserted \"as SignupFormProps\" - partial test data, but the assertion would also accept a misspelt or wrongly typed property. | source: const props = { onSubmit: vi.fn() } as SignupFormProps;\n- S2 | decide | major | line 11 | inside a test | \"screen.getByLabelText(\"Email\") as HTMLInputElement\" narrows a DOM query result - that is not partial data, and fromPartial would hide a missing element. | source: const email = screen.getByLabelText(\"Email\") as HTMLInputElement;\n- S3 | imports | major | line 20 | fromPartial | fromPartial is called but not imported from @total-typescript/shoehorn, so the file does not compile. | source: const user = fromPartial({ id: \"u1\" });\n- S4 | partial | major | line 20 | fromPartial | fromPartial(...) sits where nothing gives it a type and has no generic, so its result is unknown - every use of it is untyped. | source: const user = fromPartial({ id: \"u1\" });\n- S5 | decide | major | line 21 | inside a test | \"loadSession() as Session\" asserts a call result, not a literal - it may be partial data (fromPartial), deliberately wrong data\n\n[... 567 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "The page migrated one of three assertions to fromPartial; the loadSession cast, the email DOM narrowing and the untyped user fromPartial are left to decide, and the first step is importing fromPartial from @total-typescript/shoehorn.",
"findings": [
{
"id": "S1",
"widget": "it - submits the email",
"issue": "The as SignupFormProps assertion on the props literal would accept a misspelt or wrongly typed property in the partial props.",
"fix": "Use fromPartial<SignupFormProps>, since the const has no annotation to give the value a type.",
"code": "const props = fromPartial<SignupFormProps>({ onSubmit: vi.fn() });"
},
{
"id": "S2",
"widget": "it - submits the email",
"issue": "The as HTMLInputElement assertion trusts that the Email label points at an input, so a label wired to another element would only fail later on email.value.",
"fix": "Keep a narrowing check instead of shoehorn: assert the element is an HTMLInputElement and narrow with instanceof before reading value.",
"code": "const email = screen.getByLabelText(\"Email\");\nexpect(email).toBeInstanceOf(HTMLInputElement);\nif (!(email instanceof HTMLInputElement)) throw new Error(\"Email is not an input\");"
},
{
"id": "S3",
"widget": "it - shows the server error",
"issue": "fromPartial is called on line 20 without an import, so the file does not compile at all.",
"fix": "Add one import of fromPartial from @total-typescript/shoehorn at the top of the file.",
"code": "import { fromPartial } from \"@total-typescript/shoehorn\";"
},
{
"id": "S4",
"widget": "it - shows the server error",
"issue": "The user fromPartial sits in a const with no annotation and no generic, so its result is unknown and the user prop rejects it.",
"fix": "Move fromPartial({ id: \"u1\" }) into the user attribute of SignupForm, where the prop supplies the type, instead of naming a user type the file does not import.",
"code": "render(<SignupForm onSubmit={api.signup} user={fromPartial({ id: \"u1\" })} session={session} />);"
},
{
"id": "S5",
"widget": "it - shows the server error",
"issue": "The as Session assertion on the loadSession() result hides whether that call really returns a Session or something the test is papering over.",
"fix": "Use fromPartial<Session> on the call result, since the test never reads the session and only needs it to type-check as partial data.",
"code": "const session = fromPartial<Session>(loadSession());"
}
],
"cleared": [],
"extra": [],
"plan": [
"Add import { fromPartial } from \"@total-typescript/shoehorn\" (S3)",
"Give the user fromPartial a type by passing it straight to the user prop (S4)",
"Decide loadSession() as Session and use fromPartial<Session> (S5)",
"Replace the email cast with an instanceof check (S2)",
"Accept the page's fromPartial<SignupFormProps> for the props (S1)",
"Run the type check and the Vitest suite (S1, S2, S3, S4, S5)"
],
"caveats": [
"What loadSession returns is not visible in this file; if it already returns Session, drop the cast instead of wrapping it.",
"No expect reads the session, so the test cannot tell whether it needs more than partial data."
]
}
What step 5 prints for it: S1: Use fromPartial<SignupFormProps>, since the const has no annotation to give the value a type. | S2: Keep a narrowing check instead of shoehorn: assert the element is an HTMLInputElement and narrow with instanceof before reading value. | S3: Add one import of fromPartial from @total-typescript/shoehorn at the top of the file. | S4: Move fromPartial({ id: "u1" }) into the user attribute of SignupForm, where the prop supplies the type, instead of naming a user type the file does not import. | S5: Use fromPartial<Session> on the call result, since the test never reads the session and only needs it to type-check as partial data.
The page's check: kept - nothing to flag
rewrite - A React form test with casts to decide
The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the rewritten code: 5 findings before, 0 after.
Input (facts is 1953 characters; the first 1394 are shown):
{
"task": "rewrite",
"unit": "File: src/SignupForm.test.tsx\nRunner: Vitest",
"code": "import { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { vi, it, expect } from \"vitest\";\nimport { SignupForm, type SignupFormProps } from \"./SignupForm\";\nimport { api } from \"./api\";\nimport { loadSession, type Session } from \"./session\";\n\nit(\"submits the email\", async () => {\n const props = { onSubmit: vi.fn() } as SignupFormProps;\n render(<SignupForm {...props} />);\n const email = screen.getByLabelText(\"Email\") as HTMLInputElement;\n await userEvent.type(email, \"a@b.co\");\n await userEvent.click(screen.getByRole(\"button\", { name: \"Sign up\" }));\n expect(props.onSubmit).toHaveBeenCalledWith({ email: \"a@b.co\" });\n expect(email.value).toBe(\"a@b.co\");\n});\n\nit(\"shows the server error\", async () => {\n vi.spyOn(api, \"signup\").mockRejectedValue(new Error(\"taken\"));\n const user = fromPartial({ id: \"u1\" });\n const session = loadSession() as Session;\n render(<SignupForm onSubmit={api.signup} user={user} session={session} />);\n await userEvent.click(screen.getByRole(\"button\", { name: \"Sign up\" }));\n expect(await screen.findByText(\"taken\")).toBeTruthy();\n});\n",
"carry": "S1 | confirmed | it - submits the email | Use fromPartial<SignupFormProps>, since the const has no annotation to give the value a type.\nS2 | confirmed | it - submits the email | Keep a narrowing check instead of shoehorn: assert the element is an HTMLInputElement and narrow with instanceof before reading value.\nS3 | confirmed | it - shows the server error | Add one import of fromPartial from @total-typescript/shoehorn at the top of the file.\nS4 | confirmed | it - shows the server error | Move fromPartial({ id: \"u1\" }) into the user attribute of SignupForm, where the prop supplies the type, instead of naming a user type the file does not import.\nS5 | confirmed | it - shows the server error | Use fromPartial<Session> on the call result, since the test never reads the session and only needs it to type-check as partial data.",
"facts": "The page's scan of 26 lines - 3 type assertions: 1 migrated by the page (1 fromPartial, 0 fromAny), 1 to decide, 1 DOM narrowing; 2 describe/it/test titles; verdict: Needs decisions.\nFindings (id | area | severity | line | where | evidence | source):\n- S1 | partial | minor | line 9 | inside a test | An object literal asserted \"as SignupFormProps\" - partial test data, but the assertion would also accept a misspelt or wrongly typed property. | source: const props = { onSubmit: vi.fn() } as SignupFormProps;\n- S2 | decide | major | line 11 | inside a test | \"screen.getByLabelText(\"Email\") as HTMLInputElement\" narrows a DOM query result - that is not partial data, and fromPartial would hide a missing element. | source: const email = screen.getByLabelText(\"Email\") as HTMLInputElement;\n- S3 | imports | major | line 20 | fromPartial | fromPartial is called but not imported from @total-typescript/shoehorn, so the file does not compile. | source: const user = fromPartial({ id: \"u1\" });\n- S4 | partial | major | line 20 | fromPartial | fromPartial(...) sits where nothing gives it a type and has no generic, so its result is unknown - every use of it is untyped. | source: const user = fromPartial({ id: \"u1\" });\n- S5 | decide | major | line 21 | inside a test | \"loadSession() as Session\" asserts a call result, not a literal - it may be partial data (fromPartial), deliberately wrong data\n\n[... 559 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"summary": "The migration imports fromPartial, types the props and session with fromPartial generics, moves the user data into its prop, and swaps the email cast for an instanceof check; the team should confirm what loadSession returns.",
"code": "import { render, screen } from \"@testing-library/react\";\nimport userEvent from \"@testing-library/user-event\";\nimport { vi, it, expect } from \"vitest\";\nimport { fromPartial } from \"@total-typescript/shoehorn\";\nimport { SignupForm, type SignupFormProps } from \"./SignupForm\";\nimport { api } from \"./api\";\nimport { [... 999 more characters cut from this display]",
"changes": [
{
"id": "S1",
"change": "In submits the email, the props literal now goes through fromPartial<SignupFormProps> instead of the as SignupFormProps assertion."
},
{
"id": "S2",
"change": "In submits the email, the HTMLInputElement cast became an expect toBeInstanceOf check plus an instanceof narrowing before email.value is read."
},
{
"id": "S3",
"change": "The file now imports fromPartial from @total-typescript/shoehorn, so the calls in both tests compile."
},
{
"id": "S4",
"change": "In shows the server error, fromPartial({ id: \"u1\" }) moved into the user attribute so the prop gives it a type, and the untyped user const is gone."
},
{
"id": "S5",
"change": "In shows the server error, the loadSession() result now goes through fromPartial<Session> instead of the as Session assertion."
}
],
"kept": [],
"caveats": [
"What loadSession returns is not visible here; if it already returns Session, drop the fromPartial wrapper on the session."
]
}
Cut from this display only (the saved reply and the check use the full text): code (1310 characters, 311 shown).
What step 5 prints for it: S1: In submits the email, the props literal now goes through fromPartial<SignupFormProps> instead of the as SignupFormProps assertion. | S2: In submits the email, the HTMLInputElement cast became an expect toBeInstanceOf check plus an instanceof narrowing before email.value is read. | S3: The file now imports fromPartial from @total-typescript/shoehorn, so the calls in both tests compile. | S4: In shows the server error, fromPartial({ id: "u1" }) moved into the user attribute so the prop gives it a type, and the untyped user const is gone. | S5: In shows the server error, the loadSession() result now goes through fromPartial<Session> instead of the as Session assertion.
The page's check: kept - nothing to flag
review - An Express handler suite that migrates on its own
The page read it as: Migrates automatically - 0 blocker, 0 major, 6 minor; findings S1 double-assertion, S2 literal-assertion, S3 double-assertion, S4 double-assertion, S5 literal-assertion, S6 double-assertion.
Input (facts is 2580 characters; the first 1391 are shown):
{
"task": "review",
"unit": "File: src/get-user.test.ts\nRunner: Vitest",
"code": "import { describe, it, expect, vi } from \"vitest\";\nimport type { Request, Response } from \"express\";\nimport { getUser } from \"./get-user\";\n\ndescribe(\"getUser\", () => {\n it(\"returns the user for an id\", async () => {\n const res = { json: vi.fn(), status: vi.fn() } as unknown as Response;\n await getUser({ params: { id: \"123\" } } as Request, res);\n expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ id: \"123\" }));\n });\n\n it(\"rejects a numeric id\", async () => {\n const res = { json: vi.fn(), status: vi.fn() } as unknown as Response;\n await getUser({ params: { id: 123 } } as unknown as Request, res);\n expect(res.status).toHaveBeenCalledWith(400);\n });\n\n it(\"reads the tenant header\", async () => {\n const req = {\n params: { id: \"7\" },\n headers: { \"x-tenant\": \"acme\" },\n cookies: {},\n query: {},\n body: {},\n signedCookies: {},\n ip: \"\",\n } as Request;\n const res = { json: vi.fn(), status: vi.fn() } as unknown as Response;\n await getUser(req, res);\n expect(res.json).toHaveBeenCalled();\n });\n});\n",
"carry": "",
"facts": "The page's scan of 33 lines - 6 type assertions: 6 migrated by the page (2 fromPartial, 4 fromAny), 0 to decide, 0 DOM narrowing; 4 describe/it/test titles; verdict: Migrates automatically.\nFindings (id | area | severity | line | where | evidence | source):\n- S1 | wrong | minor | line 7 | inside a test | \"as unknown as Response\" on { json: vi.fn(), status: vi.fn() } - deliberately wrong data, which loses autocomplete and hides the target type. | source: const res = { json: vi.fn(), status: vi.fn() } as unknown as Response;\n- S2 | partial | minor | line 8 | inside a test | An object literal asserted \"as Request\" - partial test data, but the assertion would also accept a misspelt or wrongly typed property. | source: await getUser({ params: { id: \"123\" } } as Request, res);\n- S3 | wrong | minor | line 13 | inside a test | \"as unknown as Response\" on { json: vi.fn(), status: vi.fn() } - deliberately wrong data, which loses autocomplete and hides the target type. | source: const res = { json: vi.fn(), status: vi.fn() } as unknown as Response;\n- S4 | wrong | minor | line 14 | inside a test | \"as unknown as Request\" on { params: { id: 123 } } - deliberately wrong data, which loses autocomplete and hides the target type. | source: await getUser({ params: { id: 123 } } as unknown as Request, res);\n- S5 | partial | minor | line 27 | inside a test | An object literal asserted \"as\n\n[... 1189 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "The page migrated all six assertions in this Vitest file (two fromPartial, four fromAny) with nothing left to decide; first add the fromAny and fromPartial import from @total-typescript/shoehorn.",
"findings": [
{
"id": "S1",
"widget": "it - returns the user for an id",
"issue": "The double assertion to Response hides the target type and drops autocomplete on the mock response.",
"fix": "Use fromAny<Response>, since the const has no annotation to give the value a type.",
"code": "const res = fromAny<Response>({ json: vi.fn(), status: vi.fn() });"
},
{
"id": "S2",
"widget": "it - returns the user for an id",
"issue": "The as Request assertion would accept a misspelt or wrongly typed property in the partial request.",
"fix": "Use fromPartial, since the getUser argument already supplies the Request type.",
"code": "await getUser(fromPartial({ params: { id: \"123\" } }), res);"
},
{
"id": "S3",
"widget": "it - rejects a numeric id",
"issue": "The double assertion to Response hides the target type and drops autocomplete on the mock response.",
"fix": "Use fromAny<Response>, since the const has no annotation to give the value a type.",
"code": "const res = fromAny<Response>({ json: vi.fn(), status: vi.fn() });"
},
{
"id": "S4",
"widget": "it - rejects a numeric id",
"issue": "The numeric id is deliberately wrong data, and the double assertion hides that intent behind a manual Request type.",
"fix": "Use fromAny, which keeps the wrong data on purpose while the getUser argument supplies the type.",
"code": "await getUser(fromAny({ params: { id: 123 } }), res);"
},
{
"id": "S5",
"widget": "it - reads the tenant header",
"issue": "The as Request assertion on the large request literal would accept a misspelt or wrongly typed property.",
"fix": "Use fromPartial<Request>, since the const has no annotation, and pass only the properties the test needs.",
"code": "const req = fromPartial<Request>({\n params: { id: \"7\" },\n headers: { \"x-tenant\": \"acme\" },\n});"
},
{
"id": "S6",
"widget": "it - reads the tenant header",
"issue": "The double assertion to Response hides the target type and drops autocomplete on the mock response.",
"fix": "Use fromAny<Response>, since the const has no annotation to give the value a type.",
"code": "const res = fromAny<Response>({ json: vi.fn(), status: vi.fn() });"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "partial",
"line": 22,
"widget": "it - reads the tenant header",
"issue": "The cookies, query, body, signedCookies and ip placeholders only existed to fake the full Request shape and the test never reads them.",
"fix": "Drop those placeholder properties once the literal goes through fromPartial<Request>, keeping params and headers."
},
{
"id": "X2",
"area": "decide",
"line": 7,
"widget": "it - returns the user for an id",
"issue": "The mock response with json and status as vi.fn() is partial data rather than wrong data, so fromAny may give up more checking than needed.",
"fix": "Try fromPartial<Response> on the three mock responses and keep fromAny only if the type check rejects it."
}
],
"plan": [
"Add import { fromAny, fromPartial } from \"@total-typescript/shoehorn\" (S1, S2, S3, S4, S5, S6)",
"Accept fromPartial on the two request literals and drop the unused placeholders (S2, S5, X1)",
"Accept fromAny for the numeric id request (S4)",
"Accept fromAny<Response> on the mock responses, or try fromPartial<Response> first (S1, S3, S6, X2)",
"Run the type check and the Vitest suite (S1, S2, S3, S4, S5, S6)"
],
"caveats": [
"Whether getUser reads any request property beyond params and headers is not visible in this file; the expects only need those two."
]
}
What step 5 prints for it: S1: Use fromAny<Response>, since the const has no annotation to give the value a type. | S2: Use fromPartial, since the getUser argument already supplies the Request type. | S3: Use fromAny<Response>, since the const has no annotation to give the value a type. | S4: Use fromAny, which keeps the wrong data on purpose while the getUser argument supplies the type. | S5: Use fromPartial<Request>, since the const has no annotation, and pass only the properties the test needs. | S6: Use fromAny<Response>, since the const has no annotation to give the value a type.
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./shoekit.js"); require("./shoereply.js")), then:
const K = ShoeKit, R = ShoeReply;
const f = K.form({ name: "src/u.test.ts", targets: "vitest", carry: "",
code: "it(\"gets\", () => {\n getUser({ id: \"1\" } as Request);\n const el = screen.getByRole(\"textbox\") as HTMLInputElement;\n});" });
// f.findings, f.scan.asserts (every assertion and what the page did), f.scan.migrated (the migrated file), f.checklist
// K.findingsCsv(f), K.assertsCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review"); // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b) // {status: kept or flags, 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://shoehorn-desk.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://shoehorn-desk.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"]
// shoehorn-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://shoehorn-desk.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://shoehorn-desk.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
}
// ShoehornDesk.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 ShoehornDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://shoehorn-desk.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://shoehorn-desk.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://shoehorn-desk.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://shoehorn-desk.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":"shoehorn-desk"}' | 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": "shoehorn-desk"})["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: "shoehorn-desk" })).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": "shoehorn-desk"}, 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", "shoehorn-desk"), 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: "shoehorn-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "shoehorn-desk"])["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 = "shoehorn-desk" }))!["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": "review", "unit": "File: src/SignupForm.test.tsx
Runner: Vitest", "code": "import { render, screen } from \"@testing-library/react\";
...", "carry": "", "facts": "The page's scan of 25 lines - 3 type assertions ...
Findings (id | area | severity | line | where | evidence | source):
- S1 | partial | minor | line 8 | ..."}
# - 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": "review", "unit": "File: src/SignupForm.test.tsx
Runner: Vitest", "code": "import { render, screen } from \"@testing-library/react\";
...", "carry": "", "facts": "The page's scan of 25 lines - 3 type assertions ...
Findings (id | area | severity | line | where | evidence | source):
- S1 | partial | minor | line 8 | ..."}
# - 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: "review" or "rewrite", unit, code, 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 shoehorn-desk:<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 review reply carries summary, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.
KEY="shoehorn-desk:$(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 == "rewrite" then (.changes[] | "\(.id): \(.change)")
else (.findings[] | "\(.id): \(.fix)") end' reply.json
import hashlib, time
key = "shoehorn-desk:" + 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"] == "rewrite":
for x in reply["changes"]:
print(x["id"], x["change"])
else:
for x in reply["findings"]:
print(x["id"], x["fix"])
import { createHash } from "node:crypto";
const key = `shoehorn-desk:${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 === "rewrite") { for (const x of reply.changes) console.log(x.id, x.change); }
else { for (const x of reply.findings) console.log(x.id, x.fix); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "shoehorn-desk:" + 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, b := "findings", "fix"
if reply["task"] == "rewrite" {
key, b = "changes", "change"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx["id"], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "shoehorn-desk:" + 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("rewrite")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("fix").asText());
}
require "digest"
key = "shoehorn-desk:#{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"] == "rewrite"
reply["changes"].each { |x| puts "#{x["id"]} #{x["change"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["fix"]}" }
end
$key = "shoehorn-desk:" . $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"] === "rewrite") { foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["change"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["fix"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"shoehorn-desk:{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 == "rewrite") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["change"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["fix"]}"); }
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: shoehorn-desk:$(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": "review", "unit": "File: src/SignupForm.test.tsx
Runner: Vitest", "code": "import { render, screen } from \"@testing-library/react\";
...", "carry": "", "facts": "The page's scan of 25 lines - 3 type assertions ...
Findings (id | area | severity | line | where | evidence | source):
- S1 | partial | minor | line 8 | ..."}
# - 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": "shoehorn-desk:" + 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": `shoehorn-desk:${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", "shoehorn-desk:"+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", "shoehorn-desk:" + 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" => "shoehorn-desk:#{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: shoehorn-desk:" . 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", $"shoehorn-desk:{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);