Base URL and authentication
Unlike the rest of the Data API, the fix routes are tenant-scoped and live under the control-plane base:x-api-key header as the Data API (a dashboard session also works). Reads require workspace membership; every mutation (POST) requires the admin role. Responses use camelCase field names — except the fix packet, which is snake_case by design (it is an agent-facing document).
Endpoints
The Fix object
Every fix endpoint (except the packet) returns or embeds this shape:Candidate refs
candidateRef carries the proposed candidate, discriminated by kind:
Statuses
DRAFT, SCOPING, NO_EVIDENCE, BLOCKED_COVERAGE, UNROUTED, PROPOSING, NO_HEADROOM, PROPOSED, GATING, GATE_FAILED, GATE_INCONCLUSIVE, VERIFIED, PR_OPEN, APPLIED_LOCALLY, HANDED_OFF, SHIPPED, CONFIRMING, HELD, REGRESSED, REJECTED, SUPERSEDED, ABANDONED. The dashboard lifecycle table explains each stage.
Gate result
gateResult is set once a gate finishes:
completedAt is when the verdict was written. Verdicts written before this field existed don’t have it.
The verdict reads holdout cases only. pass requires wins − losses ≥ max(2, noiseFloor + 1), losses ≤ noiseFloor, and abstained ≤ 30% of the holdout; more than 30% abstained is inconclusive (never a pass); anything else is fail. verdict values are exactly pass / fail / inconclusive.
While the gate is enqueued (GATING), gateResult may instead hold a pending marker — { "pending": true, "baselineFidelity": { ... } } — recorded at enqueue time, before any verdict exists. Treat it as “no verdict yet”; the display surfaces render no gate view for it. The pending marker may also carry enqueue-time counterfactual context (for example the anchors selected) with no counterfactual.verdict — the surfaces render no counterfactual copy for that either.
Counterfactual gate (gateResult.counterfactual)
gateResult.counterfactual is the counterfactual-anchored gate’s verdict: the fix replayed against recorded production failures from holdout traces, two arms per anchor — a control arm without the fix and a candidate arm with it. The block is optional: comparison-only verdicts omit it entirely, and every comparison field above is unchanged whether or not it is present.
The verdict math:
pass iff wins − losses ≥ max(2, ceil(0.2 × informative)) and informative ≥ 3 and abstained ≤ 30% of anchors; inconclusive when informative < 3 or abstained exceeds 30% (no informative anchors is inconclusive, never a pass); anything else is fail.
A counterfactual pass can upgrade a comparison GATE_FAILED/GATE_INCONCLUSIVE fix to VERIFIED — but only when the comparison gate did not fail with losses beyond its noise floor. Overall: pass if either gate passes and neither gate has losses above noise; otherwise the stricter of the two verdicts stands. The top-level reason always names which gate produced the verdict. Display surfaces render the block as prevented {wins} of {informative} replayed production failures (control still failed in {informative}; regressions {losses}) — alongside, never instead of, the comparison verdict. In the fix packet the block rides under the snake_case key counterfactual.
Verdict fidelity
baselineFidelity labels what the baseline arm was made of, so a verdict never claims more than the gate actually compared. It is optional: verdicts produced before fidelity labeling omit it.
baselineFidelity.prompt — the prompt pinned in both arms:
baselineFidelity.skills — where the baseline arm’s skill content came from:
Fidelity is always shown next to Verified. Every surface that renders the verdict — the dashboard badges, the CLI summaries, and the packet — qualifies it: Verified — tenant prompt baseline or Verified — standard-agent baseline, with · skill recovered from production receipts appended when
skills is observed. A generic-prompt or recovered-skill verdict never renders as plain “Verified”.
POST /tenants/:tenantId/problems/:problemId/fixes
Drafts a fix. Creation is idempotent while a fix is active: drafting again for the same problem returns the existing active fix instead of forking the pipeline. The new fix rests atDRAFT — drive it with advance.
string
default:"auto"
auto routes from evidence; prompt considers only the prompt route.400; an unknown problem returns 404. A merged-away problem ID is folded to its survivor, like the problem endpoints.
POST /tenants/:tenantId/fixes/draft-batch
Campaign mode: batch-drafts fixes for the tenant’s top-ranked fixable problems in one call — what the dashboard’s Draft fixes for top problems button calls. Problems are scanned in rank order; each is drafted through the same guarded creation path as the single-problem route above, or skipped with a reason when it already has an active fix, its latest lifecycle is notopen/reopened, or it aliases a problem already handled in the batch. Creation only — nothing advances server-side; drive the drafted fixes with advance.
integer
default:"10"
How many fixes to draft, 1–25 (
400 beyond the cap). The 25 cap is an LLM spend guard, not a paging nicety: every drafted fix burns distillation + propose budget once driven, so batches are deliberately small — run another after the first lands.Output (drafted entries trimmed)
drafted entries are full Fix objects decorated with the rank the batch ranked on. Per-problem failures never abort the batch — they land in skipped with the error message as the reason.
GET /tenants/:tenantId/fixes
The ranked queue: fixes ordered by their problem’s rank (descending), then recency. Also available per problem atGET /tenants/:tenantId/problems/:problemId/fixes (returns { "fixes": [...] } without pagination).
string
Filter to one status. An unknown status returns
400.integer
default:"25"
1–100.
string
Opaque cursor from a previous page’s
pagination.next_cursor.Reading a fix reconciles its confirmation state lazily:
SHIPPED/CONFIRMING fixes are checked against the problem’s lifecycle on every read, so a reopen or 14 clean monitoring days surface as REGRESSED/HELD without a background job.GET /tenants/:tenantId/fixes/:fixId/packet
The compact agent packet —moda.fix_packet.v1. Every fix has one, including diagnosis-only fixes without a candidate. Quoted evidence in the packet is neutralized as untrusted data before it reaches an agent.
target keys, candidate, and evidence entries are omitted when unknown (for example an UNROUTED fix has only fix_type in target and no candidate).
Typed candidates ride the packet under their own keys (injection-screened at propose time and scrubbed again on the way out):
candidate_tool(TOOL_SCHEMAfixes) —{ tool_name, current_description, current_description_source, proposed_description, param_notes, rationale }: the tool-description diff to apply in your own tool definition.candidate_skill(SKILLfixes) —{ skill_name, skill_md }: the complete drafted SKILL.md to install in your agent.candidate_skill_edit(SKILLfixes amending an existing synced skill) —{ skill_key, skill_name, current_skill_md, proposed_skill_md, change_summary }: the current-vs-proposed SKILL.md edit to apply to the skill already in your repo.
mark-applied — no GitHub integration required.
POST /tenants/:tenantId/fixes/:fixId/advance
Performs exactly one idempotent pipeline step and returns the updated fix. The backend runs no background jobs — clients drive the pipeline by callingadvance in a loop (this is what the dashboard polling loop does). Steps are claimed optimistically, so concurrent advances never double-run a stage; the loser returns the current row untouched.
Routing
Scoping picks the target from the problem’s evidence conversations, in order:PROMPTwhen at least 60% of evidence conversations carry the same registry prompt version. WithfixType: "prompt"and no dominant version the fix landsUNROUTED; when no conversation carries a prompt version at all, the reason explains how to stamp one (prompt attribution).SKILLwhen the laziness or emotion signal family owns at least half the evidence anchors.TOOL_SCHEMAwhen one tool’s call-side failures (bad arguments, unmet preconditions) appear in at least 40% of evidence conversations, and in at least 3 of them. Conversations are counted, not anchors, so one noisy trace cannot route a problem.routeConfidenceis that conversation share.UNROUTED(harness code) when runtime failures (timeouts, connection resets, upstream 5xx, 401/403, rate limits) appear in at least 60% of the conversations with tool failures, in at least 40% of evidence conversations, and in at least 3 of them. No prompt, tool-description, or skill edit fixes these; thestatusReasonstarts withharness code:and names the tools.SKILLotherwise, as a behavior-level fix. ThestatusReasonsays which tool was considered and why it was not routed.
POST /tenants/:tenantId/fixes/:fixId/verify
Re-gates the fix: enqueues fresh gate and control runs and returns the pinned gate run ID. WithcandidateContent this gates uploaded local content instead of the stored candidate. Then drive advance until the fix leaves GATING.
For TOOL_SCHEMA and SKILL fixes, both gate arms pin the same prompt so the candidate artifact is the only delta between arms: the tenant’s managed prompt when one exists (baselineFidelity.prompt: "tenant"), or Moda’s fixed standard-agent baseline prompt in both arms otherwise ("generic"). Skill candidates — net-new drafts and edits alike — are registered as content-addressed gate skills automatically: the candidate body rides the proposed arm, and a skill_edit pins the current body (synced or receipt-recovered) in the baseline arm. Only a candidate without gateable content rests at PROPOSED with the gate pending. The resulting verdict always carries its fidelity label.
string
Optional candidate content, 40–200,000 characters,
PROMPT fixes only (400 for other fix types). Omitted = re-gate the stored candidate version. Uploaded content is recorded by hash (candidateRef.contentSha256), not registered as a prompt version.PROPOSED, VERIFIED, GATE_FAILED, and GATE_INCONCLUSIVE. A fix already GATING returns 409 (“advance to poll it”), as does any other status.
POST /tenants/:tenantId/fixes/:fixId/submit
Delivers aVERIFIED fix (any other status returns 409).
string
required
pr — Moda’s GitHub App opens a draft PR on branch moda/fix/<shortref> with the composed body and magic word. local_ref — records that you applied the change yourself on a branch (status → APPLIED_LOCALLY).string
The branch name. Required when
channel is local_ref.409), one open fix PR per problem and per target file (409 names the sibling), and older drafts for the same problem or file are superseded. The pr channel requires a routed target file and a registered candidate version (400 otherwise — content verified via upload must be handed back as a PR by the agent itself); an unconfigured GitHub integration returns 503.
POST /tenants/:tenantId/fixes/:fixId/mark-applied
The no-GitHub confirmation: you applied the candidate in your own infrastructure — updated the tool description in your agent’s tool definition, installed the drafted skill, or hand-applied a prompt edit — so there is no PR merge to confirm the fix. The fix flips straight toSHIPPED with deliveryChannel: "local", records fixRef: { appliedNote, appliedAt, promptVersionId }, and posts the same deterministic mark_fixed problem feedback the merge webhook would, with actor: "fix-mark-applied" and a fix_ref of { fix_id, short_ref, applied_note? } (no pr_url / merge_commit).
string
Optional operator note, ≤ 600 characters, recorded on
fix_ref.applied_note.duplicate boolean. Allowed from any resting or delivered status (UNROUTED, PROPOSED, VERIFIED, GATE_FAILED, GATE_INCONCLUSIVE, PR_OPEN, APPLIED_LOCALLY, HANDED_OFF); pipeline-live (SCOPING, PROPOSING, GATING), pre-candidate, and terminal statuses return 409. Idempotent: marking an already-confirmed fix (SHIPPED, CONFIRMING, HELD, REGRESSED) is a duplicate no-op that still re-posts the deterministic feedback. Confirmation monitoring then behaves exactly as after a merge — CONFIRMING while the problem is in fixed-monitoring, HELD after 14 clean days from appliedAt, REGRESSED on reopen.
POST /tenants/:tenantId/fixes/:fixId/dismiss
Rejects the fix (status →REJECTED) and records the reason as problem feedback. Already-terminal fixes (REJECTED, SUPERSEDED, ABANDONED, HELD) return 409.
string
required
1–2,000 characters. Feeds the problem discovery loop.
Fix provenance on problem feedback (fix_ref)
When a PR whose body or head branch carriesMODA-FIX-<SHORTREF> is merged, Moda’s GitHub App notifies the backend, which flips the fix to SHIPPED and posts a mark_fixed problem feedback with actor: "moda-github-app" and a structured fix_ref:
mark-applied posts the same feedback shape with actor: "fix-mark-applied" and a fix_ref of { fix_id, short_ref, applied_note? } — no pr_url or merge_commit, because no PR exists.
POST /problems/:id/feedback accepts the same optional fix_ref object on any mark_fixed you submit yourself. Notes:
fix_refis stored on the feedback row and threaded into the problem’s lifecycle change reason for provenance; it is not part of the deterministicfeedback_id, so re-asserting the same action with an amended ref converges on one row instead of minting duplicates.- If a problem that was marked fixed with a
fix_reflater reopens automatically, the associatedSHIPPED/CONFIRMINGfixes flip toREGRESSED. - The merge notification is idempotent — redelivered webhook events converge on the same
SHIPPEDfix and the same pending feedback row.
Errors
Next steps
- Dashboard: Fixes — the same pipeline with the stepper cards and gate scoreboard.
- Problems — the evidence and feedback endpoints fixes are built on.