← Merge Desk / API
Get a token

Driving Merge Desk over HTTP

Everything the page does with a model, you can do from a script. One endpoint does the work; the rest is authentication and polling. The app takes the conflicted material as one string, a task field naming which of three lanes to run over it, and returns a single JSON envelope whose body shape depends on that lane.

Read one thing before anything else: Merge Desk never runs git. It does not clone, check out, stage, commit or push, and it has no access to your repository. It reads the text you send and answers about that text. The commands it emits are for you to read and run yourself.

Base URL

https://api.skillsafe.ai/v1/app-api

Every request carries Authorization: Bearer <token>. The token is scoped to this app when it is minted, so no slug header is needed on later calls - POST /guest is the one call that names the slug, in its JSON body. Get a token from the token page without opening a developer console.

The response envelope

Every endpoint returns the same wrapper. Success carries data; failure carries error. Nothing returns a bare value, so a client can branch on the presence of error alone. The model's own answer is a JSON string nested inside that wrapper, at data.output.output - two layers, and the inner one has to be parsed.

{"ok": true,  "data":  { ... }}
{"ok": false, "error": {"code": "validation_error", "message": "...", "details": { ... }}}

Error codes

CodeHTTPWhat it meansWhat to do
unauthorized401Missing, malformed or expired token.Mint a new one. Guest tokens expire; personal tokens outlive them. A guest token is also refused for a metered run.
payment_required402Balance below the run's minimum, or below what the hold needs.Call POST /estimate first and compare min_credits against GET /me. The page disables its own button rather than submitting into this.
not_found404Unknown job id, or a job that belongs to another subject.Check the id you polled. A 404 from GET /jobs/{id} is not a transient error; do not retry it in a loop.
validation_error422The input did not match the app's shape.Read error.details; it names the offending field. The commonest cause is an empty or absent conflicted.
rate_limited429Too many requests.Back off and retry; never tight-loop. Polling GET /jobs/{id} faster than once a second will get you here.
internal_error5xxThe platform failed, not your input.Retry once with the same Idempotency-Key. That is exactly what the key is for: a retry cannot double-bill.
sponsor_exhausted402Arrives as error.details.reason, not as error.code. The app's free sponsored allowance for the day is used up.Switch to a personal token with credits. Signing in is what the page does here; a script should treat it as terminal for the day.

A job that reaches a terminal failure does not arrive as an HTTP error. It arrives as a 200 with data.status set to failed, or - on POST /run-stream - as an SSE event: error frame. Branch on status, not only on the HTTP code.

The task field comes first

Merge Desk is a three-lane app over one paste. Every run must name its lane in task, because the lanes share one system prompt and one model and are routed by that field alone. If task is missing or is not one of the three lane ids, the model picks the closest lane, answers that lane's contract, and says so by setting lane_inferred to true in its reply. That is a fallback for a malformed request, not a feature to lean on: check the flag on every response and treat true as a bug in your client.

taskWhat it doesExtra inputartifact.kind
triageDecides, for every hunk and before anyone edits anything, what each side was trying to do and how the hunk should be settled.none
resolveProduces the merged text hunk by hunk, naming and justifying every line from either side that did not survive.text
reviewReviews a resolution you have not pushed yet: did this merge lose anything, and is the result coherent?resolutionmarkdown

artifact.kind above is what each lane normally emits, not a guarantee. kind is one of none, text or markdown, and it is forced back to none whenever content is empty - so artifact.kind != "none" is a reliable test for "there is a file here".

The lanes are a pipeline, and the reply tells you where it goes next: next_lane.lane is normally resolve after a triage, review after a resolve, and resolve again after a review that is rejecting the resolution. It is "" when there is nothing sensible to run next.

Input fields

The request body is the input object itself, flat, with no wrapper. The same object is accepted by POST /estimate, POST /run and POST /run-stream.

FieldTypeRequiredNotes
taskstringall lanes One of triage, resolve, review. See above - this is the routing field.
conflictedstringall lanes The conflicted material as git left it, markers intact. Two-way (<<<<<<< / ======= / >>>>>>>) and diff3 (|||||||) both parse. Several files may be concatenated with # file: path/to/file marker lines; a paste carrying diff --git or +++ b/path headers is split on those instead. The page refuses anything under 24 characters.
resolutionstringreview only Your proposed merged text, markers removed - the file as you intend to commit it. Ignored by the other two lanes; the page hides the box for them. A review with an empty resolution is answered with posture: "blocked", which is a wasted run.
strategy_hintstringall lanes One of prefer-neither (the default), prefer-ours, prefer-theirs. A tie-breaker for genuinely equivalent hunks only. It never licenses dropping a substantive change from the other side; when it would, the model ignores the hint and says so in body.strategy_note. Send prefer-neither rather than omitting the field - it is part of the idempotency key.
branch_contextstringno One or two sentences on what each branch was doing and which direction the codebase is going. Optional, and it changes the answer more than anything else here: without it the model can classify a hunk but cannot recover intent. The page truncates it to 2000 characters.
prescanobjectno, but send it The deterministic facts the model must reconcile: {style, counts, files, hunks, flags}. See the next section. Optional for an API caller because the parser that produces it runs only in the browser - but the model is instructed to return exactly one coverage_check entry per flag id it was sent, and that reconciliation is the main way you can check the answer rather than trust it.
resolution_checkobjectreview only, no The deterministic verdict on your resolution, so the review lane has to reconcile numbers rather than re-derive them by eye. Keys listed below.
clip_notestringno Tells the model the input was truncated so it works around the gap instead of inventing what was cut. The page sends this whenever it clips: it removes unconflicted context from conflicted above 56000 characters, cuts resolution from the middle above 30000, and says in the note how many characters went and whether any later hunks were dropped entirely.
retry_notestringno Sent only on an automatic re-ask after a reply that did not parse. It quotes the parse error and restates the contract; the model answers the same lane on the same input and returns only the JSON object. The page reuses a key derived from the same input with the attempt number appended, so the retry cannot double-bill.

The prescan contract

The browser app runs a deterministic conflict parser before every run and passes its findings in prescan. Five keys, and each has a job:

KeyTypeWhat it holds
stylestringmerge, diff3 or mixed - which marker style the paste uses overall. This matters: the merge base only exists in diff3, and the model is forbidden from reasoning about a base it was not given.
countsobject{files, hunks, conflictLines, ourLines, theirLines, certain, truncated, flags, inputLines}. The reply's file_count and hunk_count must agree with counts.files and counts.hunks unless a finding explains the disagreement. certain is how many hunks the parser could settle on its own.
filesarrayOne entry per parsed file: {path, hunks, lockfile, generated, migration, test, regenerate_with}. hunks is the list of hunk ids in that file. regenerate_with is the ecosystem's regeneration command when the file is generated state (for example pnpm install --lockfile-only), and "" otherwise.
hunksarrayOne entry per hunk, and the largest part of the payload. Fields below.
flagsarrayRule hits with stable ids: {id, severity, label, file, hunk_id, line, occurrences}. These are the entries the model must account for one by one.

Each entry of prescan.hunks:

FieldTypeMeaning
hunk_idstringH1, H2, ... The model is instructed to refer to hunks by these ids and never to renumber them, so they are your join key between request and reply.
filestringThe path exactly as pasted.
linenumber1-based line of the opening marker in the paste.
stylestringmerge or diff3, per hunk.
ours_labelstringWhat followed the opening marker, usually HEAD.
theirs_labelstringWhat followed the closing marker, usually the incoming branch name.
classificationstringOne of whitespace-only, import-block, one-side-empty, version-bump, overlapping-edit, lockfile, generated, migration-order, test-file, semantic, one-side-unchanged, superset, binary, empty, truncated.
our_linesnumberLine count of the ours side.
their_linesnumberLine count of the theirs side.
base_linesnumberLine count of the merge base. Meaningless unless style is diff3.
ours_emptybooleanThe ours side is empty - an addition against a deletion, the expensive case.
theirs_emptybooleanThe theirs side is empty.
base_equals_oursbooleandiff3 only. When true, ours changed nothing here and theirs is the answer as a matter of fact, not judgement.
base_equals_theirsbooleanThe mirror image.
whitespace_onlybooleanThe two sides differ only in whitespace, or not at all.
truncatedbooleanThe paste stops inside this hunk.
mechanical_choicestringWhat the deterministic resolver would take: ours, theirs, both, regenerate, or "" when it declines.
mechanical_certainbooleanWhether that choice is provable from the markers alone. false means it is a hint, not a fact.
mechanical_reasonstringWhy, in one clause.
oursstringThe ours side verbatim, capped at 1400 characters in the triage lane and 2600 in the other two.
theirsstringThe theirs side verbatim, same cap.
basestring or nullThe merge base verbatim, or null when the hunk is two-way.

The model must return exactly one coverage_check entry per flag id sent, and none for ids that were not sent. That is what lets a free deterministic pass hold a paid model pass accountable - anything unaccounted for is a defect you can detect programmatically:

sent    = {f["id"] for f in payload["prescan"]["flags"]}
covered = {c["flag_id"] for c in result["coverage_check"]}
assert sent == covered, f"unreconciled: {sent - covered}"

Each coverage_check entry is {flag_id, status, finding_id, note}, where status is confirmed (the flag is real and here is the finding), set-aside (real but deliberately not acted on, with the reason in note) or superseded (a different finding subsumes it). Any other value is coerced to confirmed on the way in, so read note before believing a status.

You may send an empty prescan, or omit it. The lane still works; it simply has fewer facts to ground itself in and nothing to reconcile, and the reply's hunk_id values will be ids the model invented rather than ids you can join on.

The resolution_check contract (review lane)

For the review lane the page also compares your resolution against the conflicted original mechanically and sends the result. It travels as counts and a bounded sample, never the whole file a second time. Every key:

KeyTypeMeaning
compared_against_filestringWhich pasted file the resolution was compared against, or "". A model told "three lines were dropped" without being told the comparison covered one of three files would draw the wrong conclusion, so the scope travels with the numbers.
scope_confidentbooleanWhether that file was identified confidently rather than guessed.
hunks_in_scopenumberHow many hunks the comparison actually covered.
hunks_totalnumberHow many hunks exist in the paste. Lower hunks_in_scope than this is normal for a multi-file paste and is not a defect.
untouched_context_check_ranbooleanWhether the "only the conflicted regions changed" comparison was possible at all.
markers_leftnumberHow many conflict-marker lines are still present in the resolution. Anything but zero fails the first named check.
dropped_from_oursarrayUp to 25 entries of {hunk_id, text}, each text capped at 160 characters: substantive lines present on the ours side and absent from the resolution.
dropped_from_theirsarrayThe same for theirs.
dropped_from_ours_totalnumberThe real total, which may exceed the sampled 25.
dropped_from_theirs_totalnumberThe same for theirs.
untouched_context_lines_missingarrayUp to 15 lines from outside the conflicted regions that are missing from the resolution - how a resolution that quietly reformatted the file gets caught.
untouched_context_missing_totalnumberThe real total.
bracket_balanceobject{"{": 0, "[": 0, "(": 0} - net bracket delta between the conflicted original and the resolution. Non-zero is a strong signal the resolution will not parse.
json_parse_checkedbooleanWhether the file looked like JSON and was therefore parsed.
json_parse_okbooleanWhether that parse succeeded.
json_parse_errorstringThe parser's message, or "".
base_only_lines_absentnumberHow many absent lines came from the merge base only. These are not drops: a line the base had and neither side kept was deleted on purpose.

Send resolution_check only on the review lane, and only if you can compute it honestly. A fabricated one is worse than none: the model is instructed not to contradict a deterministic fact without saying it believes the scanner mis-parsed, so wrong numbers here buy you a confidently wrong review.

The output contract

The model replies with exactly one JSON object and nothing else: no prose before it, no code fence around it. That object arrives as a string at data.output.output, so parsing is two steps - the HTTP envelope, then the model's object. Every key below is always present; every array is present even when empty. A reply that cannot be parsed is re-asked once with a retry_note, which is one extra run.

{
  "lane": "triage" | "resolve" | "review",
  "lane_inferred": false,
  "title": "short specific title naming the file or the change, not the lane",
  "posture": "safe-to-resolve" | "needs-author" | "blocked",
  "verdict": "one sentence a reviewer could paste into a pull request",
  "summary": "3-6 sentences: what the conflict is about, and what was done about it",
  "file_count": 3,
  "hunk_count": 5,
  "assumptions": ["..."],
  "open_questions": ["..."],
  "findings": [
    {
      "id": "MD-001",
      "title": "short imperative title",
      "severity": "critical" | "high" | "medium" | "low",
      "hunk_id": "H3",
      "file": "src/checkout/total.ts",
      "line": 24,
      "evidence": "verbatim lines from the input",
      "why": "what goes wrong if this is merged as-is",
      "fix": "what to do, in prose",
      "fix_code": "optional replacement text, no conflict markers"
    }
  ],
  "coverage_check": [
    {"flag_id": "MD-LOCKFILE", "status": "confirmed", "finding_id": "MD-002", "note": "..."}
  ],
  "artifact": {"kind": "none" | "text" | "markdown", "filename": "", "content": ""},
  "next_lane": {"lane": "", "reason": ""},
  "body": { }
}
KeyTypeNotes
lanestringMust equal the task you sent. A reply naming no known lane is treated as unparseable and re-asked.
lane_inferredbooleantrue means your task did not arrive or was not recognised and the model chose for you. Assert it is false.
titlestringNames the file or the change, not the lane. Empty is replaced with "Untitled run".
posturestringAbout the merge, not code quality. safe-to-resolve = every hunk can be settled from what was pasted. needs-author = at least one hunk needs a person who knows the intent behind one side. blocked = the paste is unusable (no markers, truncated mid-hunk, binary). Anything else is coerced to needs-author.
verdictstringOne sentence, pasteable into a pull request.
summarystringThree to six sentences.
file_countnumberMust agree with prescan.counts.files unless a finding explains why not.
hunk_countnumberMust agree with prescan.counts.hunks on the same terms.
assumptionsstring[]What was taken as true to answer at all. Empty strings are dropped.
open_questionsstring[]Anything the model needed and was not given goes here rather than being invented.
findingsobject[]{id, title, severity, hunk_id, file, line, evidence, why, fix, fix_code}. Ids run sequentially from MD-001; the list is re-sorted most severe first on arrival, so do not rely on the wire order matching the id order. An unrecognised severity becomes medium. evidence is verbatim input, never a paraphrase.
coverage_checkobject[]One entry per prescan.flags id sent. Entries with no flag_id are discarded, which is why a missing id shows up as an unreconciled flag rather than a silent pass.
artifactobject{kind, filename, content}. content is a whole file, never a diff and never a fragment, and contains no conflict markers. kind is forced to none if content is empty.
next_laneobject{lane, reason}. lane is one of the three ids or "".
bodyobjectThe lane's own payload. The three shapes are below, and they have nothing in common - a client that reads body must branch on lane first.

body for task: "triage"

{
  "hunks": [
    {
      "hunk_id": "H1",
      "file": "src/checkout/total.ts",
      "line": 3,
      "classification": "whitespace-only" | "import-block" | "one-side-empty" |
                        "version-bump" | "overlapping-edit" | "lockfile" | "generated" |
                        "migration-order" | "test-file" | "semantic",
      "recommendation": "take-ours" | "take-theirs" | "take-both" |
                        "hand-merge" | "regenerate" | "ask-author",
      "confidence": "high" | "medium" | "low",
      "risk": "critical" | "high" | "medium" | "low",
      "reason": "why this recommendation, citing the hunk",
      "intent_ours": "one clause: what the ours side was trying to do",
      "intent_theirs": "one clause: what the theirs side was trying to do",
      "needs_author": false
    }
  ],
  "order": ["H2", "H1", "H5", "H4", "H3"],
  "auto_resolvable": 2,
  "requires_judgement": ["H3"],
  "regenerate_instead": [{"file": "pnpm-lock.yaml", "command": "pnpm install --lockfile-only"}],
  "abort_advice": "",
  "commands": ["git checkout --conflict=diff3 src/checkout/total.ts"]
}

body for task: "resolve"

{
  "strategy_note": "one paragraph: the through-line of how these hunks were merged",
  "resolutions": [
    {
      "hunk_id": "H1",
      "file": "src/checkout/total.ts",
      "line": 3,
      "resolved": "the exact replacement text for this hunk, no markers",
      "kept_from_ours": ["verbatim line or short clause"],
      "kept_from_theirs": ["verbatim line or short clause"],
      "dropped_from_ours": ["verbatim line -- and the reason it did not survive"],
      "dropped_from_theirs": [],
      "rationale": "why this merged text is what both authors would accept",
      "confidence": "high" | "medium" | "low",
      "unresolved": false,
      "unresolved_reason": ""
    }
  ],
  "retest": [{"what": "the behaviour to re-check", "why": "which side's change makes it necessary", "how": "a command or a manual step"}],
  "followups": ["work this merge revealed but deliberately did not do"],
  "commit_message": "a merge commit message that names what was reconciled"
}

body for task: "review"

{
  "approval": "approve" | "approve-with-nits" | "request-changes" | "needs-author",
  "checks": [
    {
      "name": "No conflict markers remain",
      "status": "pass" | "fail" | "partial" | "unknown",
      "evidence": "verbatim or a precise statement",
      "requirement": "what a passing state looks like"
    }
  ],
  "dropped_changes": [
    {
      "side": "ours" | "theirs",
      "hunk_id": "H3",
      "content": "verbatim line that is gone",
      "severity": "critical" | "high" | "medium" | "low",
      "why_it_matters": "..."
    }
  ],
  "semantic_risks": [
    {"statement": "...", "hunk_id": "H2", "likelihood": "high" | "medium" | "low", "check": "how to disprove it"}
  ],
  "required_tests": [{"what": "...", "why": "...", "how": "..."}],
  "review_comment": "markdown a reviewer could paste into the pull request verbatim"
}

checks covers nine named checks, by name and in this order:

  1. No conflict markers remain
  2. Every hunk was addressed
  3. Nothing from ours was dropped silently
  4. Nothing from theirs was dropped silently
  5. Syntax and structure still plausible
  6. Only the conflicted regions changed
  7. Imports and symbols still resolve
  8. Behaviour of both changes is preserved
  9. Lockfiles and generated files were regenerated, not merged

1. Get a token

A guest token is minted on demand and is enough for GET /me and POST /estimate. Running a lane is metered, so it needs a personal token: sign in at the token page and copy it from there - that page reads the token this browser already holds for merge-desk, so you never open a developer console. Every POST /guest mints a new guest identity, so reuse one token across a session rather than minting per request.

2. A tiny client

A few lines of setup that every later step reuses: the base URL, the bearer token, and a JSON post that raises on the error branch of the envelope. Keep the token in an environment variable rather than a literal in a file you might commit - and note that the token is scoped to this app, so it is useless for anything else and still worth not leaking.

3. Check who you are and what you can spend

GET /me is free and tells you the subject type and the credit balance. Compare that balance against the estimate in the next step before you run anything - a 402 after submitting is a failure of the client, not of the user. The page disables its own run button with the shortfall named rather than letting a run bounce.

4. Price the run before you make it

POST /estimate is free, creates no job and charges nothing. It takes the same input object as a run and returns the model, the markup and - the number that matters - hold_credits, the amount reserved while the run is in flight. It also returns min_credits, below which the run is refused outright, and sponsor_enabled, which says whether the app's free allowance would cover this run without an account.

The hold differs per lane. The three lanes have different prompts and different output caps: triage sends the two sides of each hunk trimmed to 1400 characters and answers with one compact record per hunk, while resolve sends 2600 and has to write the whole merged file back, and review carries a second paste - the resolution - on top of the conflict. So an estimate for one lane is not an estimate for another: re-estimate whenever task changes, which is exactly what the page does when you switch lane, discarding the previous number rather than showing lane A's hold for lane B. Present it as reserved, never as the price - the actual charge is usually far lower.

5. Run a lane and poll for the result

POST /run returns a job_id immediately; poll GET /jobs/{id} until status is terminal - succeeded, failed or cancelled. Poll no faster than once a second or you will meet rate_limited. Always send an Idempotency-Key derived from the lane plus the input: two lanes over the same paste are two distinct runs and must not collide on one key, and a retried request with the same key will never bill twice. Step 8 gives the exact derivation the app uses.

On success the model's object is the string at data.output.output. Parse it, then check three things before you use it: lane_inferred is false, every prescan flag id appears in coverage_check, and hunk_count matches what you sent.

6. Stream it instead, for anything interactive

POST /run-stream is the same call over Server-Sent Events. Frames are named: event: job once the job exists, event: delta with {"text": "..."} as the answer generates, event: done with {job_id, status, charged_credits, truncated, output}, and event: error with {code, message, job_id} when the run fails. The same Idempotency-Key rule applies - and on an idempotent replay the response comes back as plain JSON rather than a stream, so check the Content-Type before assuming you have an event stream.

Accumulate the deltas and parse once the stream closes: the envelope is only valid complete. The page uses the partial text for progress only, advancing named stages when "findings", then the lane's own key - "hunks", "resolutions" or "checks" - then "coverage_check" appear in the accumulating string. That is a display trick, not parsing; never act on a fragment.

7. One worked example per lane

All three lanes take the same conflicted paste and differ only in task - plus resolution and resolution_check for review. The envelope is identical across all three; only body and artifact change. Fields shown as ... follow the shapes documented above.

The paste every example below works from

# file: src/checkout/total.ts
import { Item } from "./types";
<<<<<<< HEAD
import { logger } from "../log";
||||||| merged common ancestors
=======
import { taxRateFor } from "../tax";
>>>>>>> feature/eu-vat

export interface TotalOptions {
  currency: string;
<<<<<<< HEAD
  region?: string;
||||||| merged common ancestors
  region?: string;
=======
  region: string;
  vatInclusive: boolean;
>>>>>>> feature/eu-vat
}

export function total(items: Item[], opts: TotalOptions): number {
  const subtotal = items.reduce((acc, i) => acc + i.price * i.qty, 0);
<<<<<<< HEAD
  logger.debug("subtotal", { subtotal, currency: opts.currency });
  if (subtotal > 100000) {
    throw new Error("subtotal exceeds the single-order ceiling");
  }
  return Math.round(subtotal);
||||||| merged common ancestors
  return Math.round(subtotal);
=======
  const rate = taxRateFor(opts.region);
  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);
  return Math.round(gross);
>>>>>>> feature/eu-vat
}
# file: src/checkout/total.test.ts
import { total } from "./total";

<<<<<<< HEAD
test("rejects an order over the ceiling", () => {
  expect(() => total([{ price: 200000, qty: 1 }], { currency: "GBP" })).toThrow();
});
=======
test("adds VAT for a German order", () => {
  expect(total([{ price: 1000, qty: 1 }], { currency: "EUR", region: "DE", vatInclusive: false }))
    .toBe(1190);
});
>>>>>>> feature/eu-vat
# file: pnpm-lock.yaml
  /intl-messageformat@10.5.0:
<<<<<<< HEAD
    resolution: {integrity: sha512-aaaaOURSaaaa}
=======
    resolution: {integrity: sha512-bbbbTHEIRSbbbb}
>>>>>>> feature/eu-vat

Three files, five hunks: H1 and H2 and H3 in total.ts, H4 in the test file, H5 in the lockfile. Two of the five are provable without a model - H2, where the diff3 base is byte-identical to ours, and H5, which is a lockfile and must be regenerated. Run the whole thing through a lane anyway: the two that are provable cost the model nothing to confirm, and the point of the run is H3.

Running all three lanes over that one paste

task: "triage" — Triage every hunk

One record per hunk with both sides' intent recovered, a recommendation carrying its own confidence and risk, and the order to work them in. This is the only lane that emits no file.

Request

{
  "task": "triage",
  "conflicted": "<the paste above, as one JSON string>",
  "strategy_hint": "prefer-neither",
  "branch_context": "ours (HEAD) is a hotfix on release/4.2: it added a single-order ceiling and debug logging after an incident where a fat-fingered quantity produced a six-figure order. theirs (feature/eu-vat) is the EU VAT feature: it makes region required and computes gross. Both are shipping this week. The VAT work is the direction the codebase is going.",
  "prescan": {
    "style": "mixed",
    "counts": {
      "files": 3, "hunks": 5, "conflictLines": 42, "ourLines": 11, "theirLines": 11,
      "certain": 2, "truncated": 0, "flags": 5, "inputLines": 62
    },
    "files": [
      {"path": "src/checkout/total.ts", "hunks": ["H1", "H2", "H3"], "lockfile": false,
       "generated": false, "migration": false, "test": false, "regenerate_with": ""},
      {"path": "pnpm-lock.yaml", "hunks": ["H5"], "lockfile": true,
       "generated": false, "migration": false, "test": false,
       "regenerate_with": "pnpm install --lockfile-only"}
    ],
    "hunks": [
      {"hunk_id": "H2", "file": "src/checkout/total.ts", "line": 12, "style": "diff3",
       "ours_label": "HEAD", "theirs_label": "feature/eu-vat",
       "classification": "one-side-unchanged",
       "our_lines": 1, "their_lines": 2, "base_lines": 1,
       "ours_empty": false, "theirs_empty": false,
       "base_equals_ours": true, "base_equals_theirs": false,
       "whitespace_only": false, "truncated": false,
       "mechanical_choice": "theirs", "mechanical_certain": true,
       "mechanical_reason": "the base is identical to ours, so only theirs changed",
       "ours": "  region?: string;",
       "theirs": "  region: string;\n  vatInclusive: boolean;",
       "base": "  region?: string;"},
      {"hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24, "style": "diff3",
       "ours_label": "HEAD", "theirs_label": "feature/eu-vat",
       "classification": "overlapping-edit",
       "our_lines": 5, "their_lines": 3, "base_lines": 1,
       "ours_empty": false, "theirs_empty": false,
       "base_equals_ours": false, "base_equals_theirs": false,
       "whitespace_only": false, "truncated": false,
       "mechanical_choice": "", "mechanical_certain": false,
       "mechanical_reason": "both sides rewrote the same region; no provable choice",
       "ours": "  logger.debug(\"subtotal\", { subtotal, currency: opts.currency });\n  if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }\n  return Math.round(subtotal);",
       "theirs": "  const rate = taxRateFor(opts.region);\n  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);\n  return Math.round(gross);",
       "base": "  return Math.round(subtotal);"}
    ],
    "flags": [
      {"id": "MD-OVERLAP-EDIT", "severity": "high",
       "label": "both sides changed the same region in different ways",
       "file": "src/checkout/total.ts", "hunk_id": "H3", "line": 24, "occurrences": 1},
      {"id": "MD-LOCKFILE", "severity": "high",
       "label": "the conflict is in a dependency lockfile",
       "file": "pnpm-lock.yaml", "hunk_id": "H5", "line": 57, "occurrences": 1},
      {"id": "MD-BASE-EQ-OURS", "severity": "low",
       "label": "ours is identical to the merge base - only theirs changed",
       "file": "src/checkout/total.ts", "hunk_id": "H2", "line": 12, "occurrences": 1}
    ]
  }
}

Trimmed for the page: the real payload carries all three files, all five hunks and all five flags. Send every hunk and every flag you have - the reply is required to have one entry per hunk in body.hunks and one coverage_check entry per flag id, and a partial prescan buys you a partial answer that still looks complete.

Response data.output.output, parsed

{
  "lane": "triage",
  "lane_inferred": false,
  "title": "VAT rewrite against a release hotfix in total.ts",
  "posture": "needs-author",
  "verdict": "Four of five hunks settle mechanically; H3 drops either the order ceiling or VAT and needs the release owner.",
  "summary": "The paste holds one merge of a release hotfix against the EU VAT feature. ...",
  "file_count": 3,
  "hunk_count": 5,
  "assumptions": ["taxRateFor returns a fraction, not a percentage - nothing in the paste proves it."],
  "open_questions": ["Is the single-order ceiling a permanent control or an incident stopgap?"],
  "findings": [
    {"id": "MD-001", "title": "Do not let the VAT rewrite delete the order ceiling",
     "severity": "critical", "hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24,
     "evidence": "throw new Error(\"subtotal exceeds the single-order ceiling\");",
     "why": "Taking theirs whole removes the guard that was added after an incident, and the file still compiles.",
     "fix": "Keep the ceiling check, then compute gross from the checked subtotal.",
     "fix_code": ""},
    {"id": "MD-002", "title": "Regenerate pnpm-lock.yaml instead of merging it",
     "severity": "high", "hunk_id": "H5", "file": "pnpm-lock.yaml", "line": 57,
     "evidence": "resolution: {integrity: sha512-aaaaOURSaaaa}",
     "why": "A hand-picked integrity hash can be valid YAML and still not match the tarball.",
     "fix": "Take either side, then run the regeneration command and commit the result.",
     "fix_code": ""}
  ],
  "coverage_check": [
    {"flag_id": "MD-OVERLAP-EDIT", "status": "confirmed", "finding_id": "MD-001", "note": ""},
    {"flag_id": "MD-LOCKFILE", "status": "confirmed", "finding_id": "MD-002", "note": ""},
    {"flag_id": "MD-BASE-EQ-OURS", "status": "set-aside", "finding_id": "",
     "note": "H2 is provable from the base; recorded as take-theirs, no finding needed."}
  ],
  "artifact": {"kind": "none", "filename": "", "content": ""},
  "next_lane": {"lane": "resolve", "reason": "Four hunks are ready to merge once H3 is decided."},
  "body": {
    "hunks": [
      {"hunk_id": "H2", "file": "src/checkout/total.ts", "line": 12,
       "classification": "one-side-unchanged", "recommendation": "take-theirs",
       "confidence": "high", "risk": "low",
       "reason": "The diff3 base is byte-identical to ours, so ours changed nothing here. That is a fact, not a judgement.",
       "intent_ours": "leave the optional region field as it was",
       "intent_theirs": "make region required and add the vatInclusive flag",
       "needs_author": false},
      {"hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24,
       "classification": "overlapping-edit", "recommendation": "hand-merge",
       "confidence": "medium", "risk": "critical",
       "reason": "Both sides rewrote the return path. Neither side's text contains the other's change, so any single-side choice drops a shipped behaviour.",
       "intent_ours": "cap a single order and log the subtotal after an incident",
       "intent_theirs": "compute gross from the regional VAT rate",
       "needs_author": true}
    ],
    "order": ["H2", "H1", "H5", "H4", "H3"],
    "auto_resolvable": 2,
    "requires_judgement": ["H3"],
    "regenerate_instead": [{"file": "pnpm-lock.yaml", "command": "pnpm install --lockfile-only"}],
    "abort_advice": "",
    "commands": [
      "git checkout --conflict=diff3 src/checkout/total.ts",
      "git checkout --theirs pnpm-lock.yaml"
    ]
  }
}

task: "resolve" — Merge the hunks

The merged text, hunk by hunk, with every line from either side that did not survive named and given a reason. artifact.content is the whole resolved file, ready to write over the original - never a diff, never a fragment.

Request

{
  "task": "resolve",
  "conflicted": "<the same paste, as one JSON string>",
  "strategy_hint": "prefer-theirs",
  "branch_context": "The VAT work is the direction the codebase is going, but the order ceiling is an incident control and must survive.",
  "prescan": {
    "style": "mixed",
    "counts": {"files": 3, "hunks": 5, "certain": 2, "truncated": 0, "flags": 5, "inputLines": 62},
    "files": [
      {"path": "src/checkout/total.ts", "hunks": ["H1", "H2", "H3"], "lockfile": false,
       "generated": false, "migration": false, "test": false, "regenerate_with": ""}
    ],
    "hunks": [
      {"hunk_id": "H1", "file": "src/checkout/total.ts", "line": 3, "style": "diff3",
       "ours_label": "HEAD", "theirs_label": "feature/eu-vat",
       "classification": "import-block",
       "our_lines": 1, "their_lines": 1, "base_lines": 0,
       "ours_empty": false, "theirs_empty": false,
       "base_equals_ours": false, "base_equals_theirs": false,
       "whitespace_only": false, "truncated": false,
       "mechanical_choice": "both", "mechanical_certain": false,
       "mechanical_reason": "both sides are imports; take both and deduplicate",
       "ours": "import { logger } from \"../log\";",
       "theirs": "import { taxRateFor } from \"../tax\";",
       "base": ""}
    ],
    "flags": [
      {"id": "MD-IMPORT-BLOCK", "severity": "low",
       "label": "both sides are only import or use lines",
       "file": "src/checkout/total.ts", "hunk_id": "H1", "line": 3, "occurrences": 1},
      {"id": "MD-OVERLAP-EDIT", "severity": "high",
       "label": "both sides changed the same region in different ways",
       "file": "src/checkout/total.ts", "hunk_id": "H3", "line": 24, "occurrences": 1}
    ]
  }
}

strategy_hint: "prefer-theirs" here is a tie-breaker for equivalent hunks, and the model is instructed to ignore it - and say so in strategy_note - the moment it would drop a substantive change. That is exactly what happens to H3 below: the hint says lean theirs, and the answer keeps both.

Response data.output.output, parsed

{
  "lane": "resolve",
  "lane_inferred": false,
  "title": "VAT gross calculation with the order ceiling kept, total.ts",
  "posture": "safe-to-resolve",
  "verdict": "Both branches survive: the ceiling still guards the subtotal, and gross is computed from the checked value.",
  "summary": "H1 takes both imports. H2 takes theirs on the base-equality proof. H3 is hand-merged: ...",
  "file_count": 3,
  "hunk_count": 5,
  "assumptions": ["taxRateFor(region) returns a fraction such as 0.19."],
  "open_questions": ["Should the ceiling apply to the gross total rather than the subtotal?"],
  "findings": [
    {"id": "MD-001", "title": "The ceiling now guards the subtotal, not the gross",
     "severity": "medium", "hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24,
     "evidence": "if (subtotal > 100000) {",
     "why": "With VAT added afterwards, an order can now exceed the old ceiling after tax.",
     "fix": "Decide deliberately which figure the control applies to; this merge preserves the original behaviour.",
     "fix_code": ""}
  ],
  "coverage_check": [
    {"flag_id": "MD-IMPORT-BLOCK", "status": "confirmed", "finding_id": "", "note": "H1 takes both imports; both are used in the merged file."},
    {"flag_id": "MD-OVERLAP-EDIT", "status": "confirmed", "finding_id": "MD-001", "note": ""}
  ],
  "artifact": {
    "kind": "text",
    "filename": "src/checkout/total.ts",
    "content": "import { Item } from \"./types\";\nimport { logger } from \"../log\";\nimport { taxRateFor } from \"../tax\";\n\nexport interface TotalOptions {\n  currency: string;\n  region: string;\n  vatInclusive: boolean;\n}\n\nexport function total(items: Item[], opts: TotalOptions): number {\n  const subtotal = items.reduce((acc, i) => acc + i.price * i.qty, 0);\n  logger.debug(\"subtotal\", { subtotal, currency: opts.currency });\n  if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }\n  const rate = taxRateFor(opts.region);\n  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);\n  return Math.round(gross);\n}\n\nexport function formatTotal(cents: number, currency: string): string {\n  return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).format(cents / 100);\n}\n"
  },
  "next_lane": {"lane": "review", "reason": "H3 was hand-merged; have the merge reviewed before pushing."},
  "body": {
    "strategy_note": "Nothing shipped on either branch is discarded. The hint asked to lean theirs, and it is ignored at H3 because taking theirs whole would delete the order ceiling. total.ts is the file in artifact; the lockfile is regenerated rather than merged and the test file keeps both tests.",
    "resolutions": [
      {"hunk_id": "H1", "file": "src/checkout/total.ts", "line": 3,
       "resolved": "import { logger } from \"../log\";\nimport { taxRateFor } from \"../tax\";",
       "kept_from_ours": ["import { logger } from \"../log\";"],
       "kept_from_theirs": ["import { taxRateFor } from \"../tax\";"],
       "dropped_from_ours": [],
       "dropped_from_theirs": [],
       "rationale": "Two independent imports; both symbols are used in the merged file.",
       "confidence": "high", "unresolved": false, "unresolved_reason": ""},
      {"hunk_id": "H2", "file": "src/checkout/total.ts", "line": 12,
       "resolved": "  region: string;\n  vatInclusive: boolean;",
       "kept_from_ours": [],
       "kept_from_theirs": ["region: string;", "vatInclusive: boolean;"],
       "dropped_from_ours": ["region?: string; -- byte-identical to the merge base, so ours changed nothing here"],
       "dropped_from_theirs": [],
       "rationale": "The diff3 base equals ours, so theirs is the only change in this hunk.",
       "confidence": "high", "unresolved": false, "unresolved_reason": ""},
      {"hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24,
       "resolved": "  logger.debug(\"subtotal\", { subtotal, currency: opts.currency });\n  if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }\n  const rate = taxRateFor(opts.region);\n  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);\n  return Math.round(gross);",
       "kept_from_ours": [
         "logger.debug(\"subtotal\", { subtotal, currency: opts.currency });",
         "if (subtotal > 100000) {",
         "throw new Error(\"subtotal exceeds the single-order ceiling\");"
       ],
       "kept_from_theirs": [
         "const rate = taxRateFor(opts.region);",
         "const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);"
       ],
       "dropped_from_ours": ["return Math.round(subtotal); -- superseded by the gross return, which both sides need"],
       "dropped_from_theirs": [],
       "rationale": "The guard runs on the subtotal before tax, exactly as ours intended, and the return is theirs. Neither branch loses its change.",
       "confidence": "medium", "unresolved": false, "unresolved_reason": ""},
      {"hunk_id": "H5", "file": "pnpm-lock.yaml", "line": 57,
       "resolved": "",
       "kept_from_ours": [], "kept_from_theirs": [],
       "dropped_from_ours": [], "dropped_from_theirs": [],
       "rationale": "",
       "confidence": "high", "unresolved": true,
       "unresolved_reason": "A lockfile is generated state. Take either side and run pnpm install --lockfile-only; a hand-merged integrity hash can be valid YAML and still wrong."}
    ],
    "retest": [
      {"what": "A GBP order over 100000 still throws",
       "why": "The ceiling moved below the VAT calculation and must still fire",
       "how": "pnpm test src/checkout/total.test.ts"},
      {"what": "A German order returns gross, not net",
       "why": "theirs added the rate multiplication that this merge preserved",
       "how": "pnpm test src/checkout/total.test.ts -t VAT"}
    ],
    "followups": [
      "Decide whether the order ceiling should apply to the gross figure now that VAT exists.",
      "The two tests in total.test.ts were both kept; neither was updated for the required region field."
    ],
    "commit_message": "Merge feature/eu-vat into release/4.2\n\nKeeps the single-order ceiling and the debug log from the hotfix and computes gross from the checked subtotal using the regional VAT rate. region becomes required. pnpm-lock.yaml is regenerated rather than merged."
  }
}

task: "review" — Review the resolution

The only lane that takes a second paste. Send resolution alongside conflicted - and if you can compute it honestly, resolution_check, so the model reconciles deterministic numbers instead of counting lines by eye. The example below reviews a resolution that took theirs wholesale, which is the failure this lane exists to catch.

Request

{
  "task": "review",
  "conflicted": "<the same paste, as one JSON string>",
  "resolution": "import { Item } from \"./types\";\nimport { taxRateFor } from \"../tax\";\n\nexport interface TotalOptions {\n  currency: string;\n  region: string;\n  vatInclusive: boolean;\n}\n\nexport function total(items: Item[], opts: TotalOptions): number {\n  const subtotal = items.reduce((acc, i) => acc + i.price * i.qty, 0);\n  const rate = taxRateFor(opts.region);\n  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);\n  return Math.round(gross);\n}\n\nexport function formatTotal(cents: number, currency: string): string {\n  return new Intl.NumberFormat(\"en\", { style: \"currency\", currency }).format(cents / 100);\n}\n",
  "strategy_hint": "prefer-neither",
  "branch_context": "ours is the release hotfix; theirs is EU VAT. Both shipped this week.",
  "prescan": {
    "style": "mixed",
    "counts": {"files": 3, "hunks": 5, "certain": 2, "truncated": 0, "flags": 5, "inputLines": 62},
    "files": [{"path": "src/checkout/total.ts", "hunks": ["H1", "H2", "H3"], "lockfile": false,
               "generated": false, "migration": false, "test": false, "regenerate_with": ""}],
    "hunks": [
      {"hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24, "style": "diff3",
       "ours_label": "HEAD", "theirs_label": "feature/eu-vat",
       "classification": "overlapping-edit",
       "our_lines": 5, "their_lines": 3, "base_lines": 1,
       "ours_empty": false, "theirs_empty": false,
       "base_equals_ours": false, "base_equals_theirs": false,
       "whitespace_only": false, "truncated": false,
       "mechanical_choice": "", "mechanical_certain": false,
       "mechanical_reason": "both sides rewrote the same region; no provable choice",
       "ours": "  logger.debug(\"subtotal\", { subtotal, currency: opts.currency });\n  if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }\n  return Math.round(subtotal);",
       "theirs": "  const rate = taxRateFor(opts.region);\n  const gross = opts.vatInclusive ? subtotal : subtotal * (1 + rate);\n  return Math.round(gross);",
       "base": "  return Math.round(subtotal);"}
    ],
    "flags": [
      {"id": "MD-OVERLAP-EDIT", "severity": "high",
       "label": "both sides changed the same region in different ways",
       "file": "src/checkout/total.ts", "hunk_id": "H3", "line": 24, "occurrences": 1}
    ]
  },
  "resolution_check": {
    "compared_against_file": "src/checkout/total.ts",
    "scope_confident": true,
    "hunks_in_scope": 3,
    "hunks_total": 5,
    "untouched_context_check_ran": true,
    "markers_left": 0,
    "dropped_from_ours": [
      {"hunk_id": "H1", "text": "import { logger } from \"../log\";"},
      {"hunk_id": "H3", "text": "logger.debug(\"subtotal\", { subtotal, currency: opts.currency });"},
      {"hunk_id": "H3", "text": "if (subtotal > 100000) {"},
      {"hunk_id": "H3", "text": "throw new Error(\"subtotal exceeds the single-order ceiling\");"}
    ],
    "dropped_from_theirs": [],
    "dropped_from_ours_total": 4,
    "dropped_from_theirs_total": 0,
    "untouched_context_lines_missing": [],
    "untouched_context_missing_total": 0,
    "bracket_balance": {"{": 0, "[": 0, "(": 0},
    "json_parse_checked": false,
    "json_parse_ok": true,
    "json_parse_error": "",
    "base_only_lines_absent": 2
  }
}

Response data.output.output, parsed

{
  "lane": "review",
  "lane_inferred": false,
  "title": "Resolution of total.ts drops the single-order ceiling",
  "posture": "needs-author",
  "verdict": "Do not push this: the VAT calculation is correct but the order ceiling and its log are gone from the merged file.",
  "summary": "The resolution takes theirs wholesale at H1 and H3. ...",
  "file_count": 3,
  "hunk_count": 5,
  "assumptions": ["The comparison covered src/checkout/total.ts only, as the scanner reported."],
  "open_questions": ["Was the ceiling removed deliberately, or lost while resolving H3?"],
  "findings": [
    {"id": "MD-001", "title": "Restore the single-order ceiling before pushing",
     "severity": "critical", "hunk_id": "H3", "file": "src/checkout/total.ts", "line": 24,
     "evidence": "throw new Error(\"subtotal exceeds the single-order ceiling\");",
     "why": "The guard was added after an incident and is absent from the resolution while the file still compiles - the classic silent drop.",
     "fix": "Re-insert the debug log and the ceiling check before computing the rate.",
     "fix_code": "  logger.debug(\"subtotal\", { subtotal, currency: opts.currency });\n  if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }"}
  ],
  "coverage_check": [
    {"flag_id": "MD-OVERLAP-EDIT", "status": "confirmed", "finding_id": "MD-001", "note": "The overlap was resolved by discarding one side."}
  ],
  "artifact": {
    "kind": "markdown",
    "filename": "review-total-ts.md",
    "content": "## Request changes\n\nThe VAT calculation is right. The order ceiling from release/4.2 is missing.\n\n| Side | Hunk | Gone | Severity |\n| --- | --- | --- | --- |\n| ours | H3 | the subtotal ceiling and its throw | critical |\n| ours | H1 | the logger import | high |\n\nRe-test: a GBP order over 100000 must still throw.\n"
  },
  "next_lane": {"lane": "resolve", "reason": "H1 and H3 need merging again, keeping both sides."},
  "body": {
    "approval": "request-changes",
    "checks": [
      {"name": "No conflict markers remain", "status": "pass",
       "evidence": "The scanner reports markers_left: 0 and none is visible in the resolution.",
       "requirement": "No line begins with a seven-character conflict marker."},
      {"name": "Every hunk was addressed", "status": "partial",
       "evidence": "H1, H2 and H3 are reflected in the resolution; H4 and H5 are in other files not included in this paste of the merged text.",
       "requirement": "Each of the five hunks is either merged or explicitly deferred."},
      {"name": "Nothing from ours was dropped silently", "status": "fail",
       "evidence": "if (subtotal > 100000) { ... throw new Error(\"subtotal exceeds the single-order ceiling\");",
       "requirement": "Every substantive ours line is present, or its removal is stated as intended."},
      {"name": "Nothing from theirs was dropped silently", "status": "pass",
       "evidence": "All three theirs lines at H3 and the taxRateFor import are present.",
       "requirement": "The same test, for the incoming side."},
      {"name": "Syntax and structure still plausible", "status": "pass",
       "evidence": "Bracket balance is unchanged in all three kinds: {, [ and (.",
       "requirement": "The merged file parses as TypeScript."},
      {"name": "Only the conflicted regions changed", "status": "pass",
       "evidence": "untouched_context_missing_total is 0; formatTotal is byte-identical.",
       "requirement": "Nothing outside the hunks is reformatted, moved or deleted."},
      {"name": "Imports and symbols still resolve", "status": "fail",
       "evidence": "The logger import is gone, and so is its only use - consistent, but the debug log ours added is lost with it.",
       "requirement": "Every symbol used is imported, and every import is used."},
      {"name": "Behaviour of both changes is preserved", "status": "fail",
       "evidence": "VAT is computed; the order ceiling is not enforced.",
       "requirement": "Both branches' observable behaviour survives the merge."},
      {"name": "Lockfiles and generated files were regenerated, not merged", "status": "unknown",
       "evidence": "pnpm-lock.yaml is in the conflicted paste but not in the resolution, so nothing can be said about how it was settled.",
       "requirement": "Every lockfile hunk is regenerated with the ecosystem's command."}
    ],
    "dropped_changes": [
      {"side": "ours", "hunk_id": "H3",
       "content": "if (subtotal > 100000) {\n    throw new Error(\"subtotal exceeds the single-order ceiling\");\n  }",
       "severity": "critical",
       "why_it_matters": "This is the incident control from release/4.2. Its absence is invisible at compile time and only shows up as a six-figure order going through."},
      {"side": "ours", "hunk_id": "H1",
       "content": "import { logger } from \"../log\";",
       "severity": "high",
       "why_it_matters": "Dropped together with the debug line that used it, so the subtotal is no longer observable in logs."},
      {"side": "ours", "hunk_id": "H3",
       "content": "logger.debug(\"subtotal\", { subtotal, currency: opts.currency });",
       "severity": "high",
       "why_it_matters": "The diagnostic added during the incident. Cheap to keep, expensive to miss."}
    ],
    "semantic_risks": [
      {"statement": "region became required, and the existing GBP test calls total() without it.",
       "hunk_id": "H2", "likelihood": "high",
       "check": "pnpm tsc --noEmit and read the first error in total.test.ts"},
      {"statement": "taxRateFor may return a percentage rather than a fraction, which would multiply the total by 20.",
       "hunk_id": "H3", "likelihood": "low",
       "check": "Open ../tax and read the return value for DE"}
    ],
    "required_tests": [
      {"what": "A GBP order over the ceiling throws",
       "why": "It is the ours behaviour this resolution drops",
       "how": "pnpm test src/checkout/total.test.ts -t ceiling"},
      {"what": "A German net order returns 1190 for a 1000 subtotal",
       "why": "It is the theirs behaviour this resolution keeps, and it must stay green after the ceiling is restored",
       "how": "pnpm test src/checkout/total.test.ts -t VAT"}
    ],
    "review_comment": "## Request changes\n\nThe VAT calculation is right and I would take it as-is. The problem is what left with it.\n\n**Dropped from ours (H3):** the single-order ceiling and its throw, plus the debug log. ...\n\n**Before pushing:** restore the guard above the rate calculation, then re-run both tests.\n"
  }
}

Note what the deterministic resolution_check bought here: the model was told four ours lines were absent and that two more absent lines came from the merge base only. Those two are not drops - the base had them and neither side kept them - and the review does not report them. Without base_only_lines_absent, a careful reviewer counting by eye would have raised them as findings.

8. Idempotency: how the key is derived

The app builds its Idempotency-Key from the run, not from a random value, so that the same request retried is the same run and a different request cannot collide with it:

merge-desk:<lane>:<hash>:a<attempt>

hash = base36( djb2( JSON.stringify([task, conflicted, resolution || "",
                                     strategy_hint, branch_context || ""]) ) )

Exactly, field by field:

A worked pair. Over the step 7 paste, with strategy_hint: "prefer-neither" and that branch_context, the triage run keys as merge-desk:triage:hxra48:a1 and the resolve run over the same paste keys as merge-desk:resolve:bfenr0:a1 - one character of input difference, an entirely different key, which is the point. Every implementation below agrees on those two strings.

Those two values are computed over the real payload, newlines and all. The excerpt printed in step 7 is reflowed for the page, so retyping it will not reproduce them byte-for-byte - check your implementation against the JavaScript one below instead, which is the app's own.

Two consequences worth stating plainly, because both have bitten someone:

What this API does not do

Merge Desk reads text. That is the whole of it.

It never runs git: no clone, no fetch, no checkout, no merge --abort, no staging, no commit, no push. It never touches a repository - it has no credentials for one and never asks for any. It never executes code: it does not run your tests, import your module, type-check the file or evaluate the merged result. When the answer says a resolution "keeps X", that is a claim about the text; when it says a test should be re-run, that is a command for you to run and read.

Every command in body.commands, retest[].how and required_tests[].how is a string for a human to read first. Nothing on this API executes anything.

The second limit is the one that actually changes how you write a client. The deterministic conflict parser and the mechanical resolver run only in the browser. They are what produce prescan - the hunk ids, the classifications, the base-equality proofs, the lockfile detection, the rule flags - and what produce resolution_check for the review lane. They are page code, not an endpoint: there is no POST /prescan, and the API will not compute either object for you.

So an API caller has two honest options:

What the API does not lose by omitting prescan: the model still reads the markers itself. It parses two-way and diff3 material, it will tell you when a paste has no markers at all (posture: "blocked"), when it stops mid-hunk, and when one enormous hunk means the merge should be redone rather than finished. It simply has no second opinion to be held against.

Notes that will save you a support round trip