- Status: stable, versioned session contract
- Contract:
wright-agent/v1 - Schema:
schemas/wright-agent-v1.schema.json
wright-agent/v1 is the transport-neutral request and response contract of
wright_driver::service::ToolService. Its Rust ToolRequest and ToolResponse
types are the executable definition. capabilities reports the contract
identity and supported operations before a client uses them.
The separate wright-result/v1 contract describes command result envelopes.
Capabilities.contract continues to identify that result envelope;
Capabilities.agent_contract identifies this service contract. Results from
compile, check, analyze, and inspect carry a wright-result/v1
envelope inside the agent response.
Start a session with wright serve [INPUT]. INPUT is a source file or
project directory; omission uses the current directory. Standard input is
reserved for requests, so - is not a valid source input. The server binds
the configured project and processes requests until standard input closes.
Starting does not require the project to load successfully (#512): a project
that is missing, unreadable, or malformed is retried by the next
program-reading request, while capabilities, targetMetadata, lookup,
listEntries, and the provider* operations answer regardless. The default transport is
one JSON
request per line on stdio:
{"op":"capabilities"}
{"op":"findings"}
{"op":"check"}Startup options configure the session's input and lint policy before the first request is served; an unknown or unreadable value is a startup usage error (exit 2), never a silently remapped default:
--transport <stdio|jsonrpc|mcp>: request transport (defaultstdio);--kind <auto|opy|ostw|workshop|protocol>: input frontend (defaultauto, with the workflow aliasesws,hir,json);--profile <off|compat>: WIR transformation policy (defaultoff); applies to Workshop input only — a non-Workshop--kindcombination is a startup usage error, and a request whose input resolves to OPY or OSTW is refused the same way (ADR-0028, #725);--locale <LOCALE>: Workshop client locale override;--lint-config <PATH>,--rule-file <PATH>,--disable-rule <ID>,--rule-severity <ID>:<SEVERITY>: explicit lint policy. The session applies the same file, local rule paths, and overrideswright lintaccepts to every lint-consuming operation it serves (#594).
A configured session example: wright serve --lint-config policy.yaml --disable-rule repeated-value project.ws serves lint/lintRules
results under policy.yaml plus the disabled rule, reporting the same
effective policy wright lint prints.
Stdio returns one ToolResponse per request. A successful response has a
result member; an application-level refusal has an error member containing
code and message. The schema defines supported request fields; current
deserialization ignores unknown fields for forward compatibility — except
removed vocabulary, which refuses instead: a rules request still carrying
the removed name field is a malformed-request error on every transport
(ADR-0035). Clients should send only fields defined by the negotiated schema.
Use wright serve --transport jsonrpc [INPUT] for JSON-RPC 2.0. The canonical
agent method is request, with a ToolRequest in params:
{"jsonrpc":"2.0","id":1,"method":"request","params":{"op":"findings"}}Its JSON-RPC result contains the same successful service result as the stdio
ToolResponse.result. A service refusal is returned in that result as
{"error":{"code":"...","message":"..."}}. JSON-RPC framing errors use
the top-level error member and standard JSON-RPC error codes. The direct
JSON-RPC methods compile, check, analyze, and inspect remain aliases for
the corresponding operations and return the same wright-result/v1 envelope
they returned before this contract was introduced.
One-shot CLI workflows such as wright check --format json continue to return
their wright-result/v1 envelope. They use the same CompilerSession
workflows as the session service. The semantic query commands wright inspect symbols, wright inspect refs, wright inspect cfg, wright inspect callgraph, and wright inspect cost likewise run through ToolService
operations (symbols, references + usage, cfg, callGraph,
costEstimate) and report the same result payloads (#429); the CLI adds no
divergent semantics. The in-process embedding API can call
ToolService::handle directly.
Use wright serve --transport mcp [INPUT] for MCP over stdio (#473,
ADR-0020). The adapter speaks newline-delimited JSON-RPC 2.0 and implements
initialize (with protocol-version negotiation), ping, tools/list, and
tools/call. Each operation in the set below is one tool named wright_
plus the operation in snake case:
| Operation | Tool |
|---|---|
project |
wright_project |
rules |
wright_rules |
symbols |
wright_symbols |
references |
wright_references |
usage |
wright_usage |
cfg |
wright_cfg |
findings |
wright_findings |
persistentObjects |
wright_persistent_objects |
callGraph |
wright_call_graph |
check |
wright_check |
analyze |
wright_analyze |
inspect |
wright_inspect |
lint |
wright_lint |
lintRules |
wright_lint_rules |
costEstimate |
wright_cost_estimate |
targetMetadata |
wright_target_metadata |
validateEditTransaction |
wright_validate_edit_transaction |
providerValidateEdit |
wright_provider_validate_edit |
editStatements |
wright_edit_statements |
lookup |
wright_lookup |
simulate |
wright_simulate |
tools/list contains a tool only when its operation is in this set and
advertised by capabilities.operations. initialize, tools/list, and the
tool set itself are independent of whether the configured project currently
loads. A broken project never hides tools (#512). Each tool's inputSchema
is derived from the operation's request schema with op removed (the tool
name carries it); the Workshop edit tools also omit sources, which then
defaults to the on-disk text (#472), and the provider tools likewise omit
documents and sources, which then default to the session's loaded
project (#548). tools/call
arguments are the request fields.
A successful service result is returned unchanged as the tool result's JSON
text content. A service refusal is a tool result with isError: true whose
content carries the same {code, message}. Edit operations report their
outcome inside the result payload (ok plus diagnostics), which passes
through as ordinary content. The adapter does not reinterpret it. MCP
protocol errors (unknown tool or method, malformed arguments) are JSON-RPC
error responses and never become tool results.
The released wright binary includes serve, so each supported installation
channel can use the session contract without a separate runtime.
wright agent tools emits every operation capabilities advertises as
client tool definitions for code-executing agents. The output is versioned
with wright-agent/v1 and deterministic: identical input produces identical
bytes.
--format messages (the default) emits
{"contract": "wright-agent/v1", "tools": [...]} in the Anthropic Messages
API client tool shape: each tool carries name (wright_ plus the
snake-case operation), a description that also describes the result in
text, an input_schema derived from the operation's request schema with
op removed, and allowed_callers: ["code_execution_20260120"] so the
tools are callable from the code execution tool. Emitted schemas keep every
caller-meaningful field, including sources, which the MCP surface drops,
and ship their $ref targets under a local $defs with no recursive
reference, which the Messages API rejects.
--format json-schema emits
{"contract": "wright-agent/v1", "schemas": {...}}: operation name to the
same standalone request schema with its description inside, for harnesses
that consume plain JSON Schema.
The emitted set is generated from one catalog shared with capabilities
and the MCP adapter, so the definitions cannot drift from the advertised
operation set; a catalog change that drops a description, a request schema,
or introduces a recursive $ref fails the tests.
All requests are JSON objects with a required op string. Request fields and
the common response/error shapes are defined by the committed JSON Schema. The
table lists every operation advertised by capabilities.operations and names
the successful result payload.
| Operation | Request fields | Successful result |
|---|---|---|
capabilities |
none | Service name/version, wright-agent/v1, result contract, operation names, result_schemas (operation to committed-schema definition), languages, profiles, and athena_version (the athena release simulate links, ADR-0033) |
compile |
optional output (file path), optional force, optional to (output locale) |
wright-result/v1 compile envelope: verdict, diagnostics, output.written_to and output.sha256; the program text is written to the file and never returned. See Compile output |
check |
optional all |
wright-result/v1 check envelope |
analyze |
optional selection, optional all |
wright-result/v1 essential analysis envelope; the envelope's snapshot scopes the ids result issues (#595) |
inspect |
optional all |
wright-result/v1 overview and rule names; the envelope's snapshot scopes the ids result issues |
project |
none | Loaded program origin, sourceFiles (entry file first), counts, findings summary, and settings — the settings block as a read-only tree or null (#836); snapshot scopes the ids it reports |
rules |
optional selection, optional content, snapshot when rule is numeric |
{"rules": [...], "snapshot": n} — canonical Workshop rules. Each entry carries id, name, address, event (subroutine:<name> for subroutine rules), and span; selection joins when a selection is applied. See Rule content |
symbols |
optional selection, optional all |
{"symbols": [...], "snapshot": n} — symbols; selection joins when file/max is applied |
references |
required symbol (id or NAME/KIND:NAME; NAME matches non-rule symbols only), snapshot when an address is numeric, optional selection and all |
Usage header and references, snapshot scoping ids; {"references": [...], "selection": {...}} on selection. Reference span is the identifier occurrence, else the statement's (#892); owningRule {id, name, address, span} locates the rule without a rules lookup (#792) |
usage |
required symbol (id or NAME/KIND:NAME), snapshot when symbol is numeric |
Usage counts plus the resolved id, kind, and snapshot scoping them |
cfg |
required rule (index or NAME/NAME#N), snapshot when rule is numeric, optional selection and all |
Control-flow graph for the rule, plus selection when a selection is applied |
findings |
optional selection, optional all |
{"findings": [...], "snapshot": n} — Wright static-analysis findings; selection joins when applied |
persistentObjects |
optional all |
{"persistentObjects": [...], "snapshot": n} — persistent Workshop object facts |
lint |
optional selection, optional all |
Finding counts by severity and actionable findings, ok/exit matching wright lint, snapshot on rules; policy from lintRules |
lintRules |
none | Registered lint rules with full metadata and effective configuration; snapshot scopes the rule indexes skipped reports |
callGraph |
optional selection, optional all |
Subroutine call graph; edges carry caller, callerAddress, callee; {"edges": [...], "selection": {...}} when a selection is applied |
costEstimate |
optional selection, optional rule, optional all |
Exact generated-resource counts (programRules counts every declared rule, disabled included; exact.emittedBytes reports null plus emittedBytesUnavailable when emission fails, never 0), elementCount (the Workshop structure counted against the client element budget; rules ranked largest first. index is the canonical rule position and each entry carries address and a resolved span; the 5 largest by default, all: true for every rule; when the count cannot run elementCount carries only budget — the known limit still reports — and elementCountUnavailable names the reason, never a zero (ADR-0030 decision 3)), findings (the configured set lint serves, with resolved span), skipped rule evaluations (the lint disclosure), and selection when applied. With rule (NAME/NAME#N) the result is the rule drill-down (#724): rule (identity address, count, span), statements[] (direct children ranked by recursive count, each with name, count, span) and byNode[] (each node's own base_count + adjustment cost summed per canonical node name with occurrences; the entries sum to the rule's count) |
targetMetadata |
none | Canonical target/catalog metadata |
validateEditTransaction |
transaction, optional sources |
Atomic validation status, diagnostics, and previews when valid |
providerValidateEdit |
language_id, transaction, optional documents, sources, project_root |
Provider-validated transaction or structured refusal |
editStatements |
inputIdentity, edits, optional sources, snapshot when a rule is numeric |
The assembled EditTransaction plus the edit-validation payload — see statement edits |
lookup |
required language; optional query, kind, within, locale, limit |
Owner vocabulary entries with accepted spellings and rendered signatures, or an explicit unavailable payload naming the owner |
listEntries |
required language, kind; optional within, locale, pageSize, cursor |
One page of the owner's vocabulary of a kind, in owner order, with total and nextCursor (see vocabulary operations) |
simulate |
inputIdentity; seconds or scenario (exactly one); optional sources |
wright-result/v1 envelope carrying the unchanged athena trace — see simulate (ADR-0033) |
compile writes the program to a file and answers with the same content as
wright compile --format json: the verdict, diagnostics, output.written_to
and output.sha256, without the program text.
outputnames the file; without it the program goes to<entry stem>.owin the service process's working directory. Unlike the CLI,-is not special: the service never streams the program.- An existing file is replaced only when the request sets
force: true; otherwise the envelope carriesoutput-existswith exit 2. The service never prompts, so every repeat compile to the same file needsforce: true. - The input source file is never overwritten, even with
force(output-is-input, exit 2). - A failed compile writes no file. The JSON-RPC
compilealias takes no parameters and uses the defaults (noforce); callers needingoutputorforceuse therequestmethod. toselects the Workshop output locale independently of the input locale, matchingwright compile --to <LOCALE>: the emitted text uses the target locale's spellings while the loaded input is unchanged. An undeclared locale fails withinvalid-locale(exit 2); a declared locale missing mappings for the program fails withmissing-mappingand writes nothing — the workshop-rs explicit-fallback contract is the only fallback.
The service may exist without a program snapshot: construction over a
project that cannot load succeeds, and capabilities, targetMetadata
(the static catalog), lookup and listEntries (the language vocabulary),
and provider*
operations carrying caller-supplied documents answer without consulting the
project at all. They neither require nor trigger its load. A provider*
request that omits documents derives the set from the loaded project
(#548) and so loads like any other program-reading request.
A request that consults the program needs a valid current snapshot at
request time. When none exists, the request performs the deferred initial
load; when one exists, the service fingerprints the input's observable file
set: a file input's own content, a directory input's resolved members, or
a source-language project's declared compile inputs (LPP 1.7
compileInputs), or every file under its entry directory. It reloads
when the set changes, so an edit, an added file, or a removed file is reflected
in the next symbols, references, check, lint, or other
program-reading request without restarting the session. An unchanged input
is never reloaded.
A load or reload that fails — a missing or unreadable entry, or a source
that does not parse — refuses the request with the loader's structured
diagnostic code (input-io, parse-error, input-kind-ambiguous, a
provider diagnostic). The service keeps refusing until the project loads
again, retrying on every program-reading request; the previously loaded
program is never served silently and is never replaced by an empty or
placeholder program. The failed attempt invalidates the served snapshot:
even restoring byte-identical input loads a fresh program rather than
resurrecting the dropped one; repair needs no restart or explicit reload
request.
check and compile are workflows rather than snapshot queries (#591):
they run the session's own operation-aware load instead of requiring a
canonical program snapshot first. A check request over a source-language
provider invokes the provider's check operation and, when the provider can
compile, its compile operation too. check then runs the same canonical
validation compile surfaces, so an agent gating on check sees every
error compile reports, and a successful check supplies the canonical
snapshot the later symbols, references, or findings requests read
(#617). A provider whose compile returns Unsupported keeps the
check-only path: it answers check from its own diagnostics alone, the
one deliberate difference from compile. Any other compile failure,
including an LPP refusal, fails check the way it fails compile. The
freshness gate above still applies: an
observed disk change invalidates the served snapshot, and the next
workflow re-resolves the input rather than serving it. A failed load
surfaces inside the wright-result/v1 envelope's diagnostics (the same
diagnostic a snapshot query refuses with), matching the embedding API's
service.check()/service.compile() outcomes.
Numeric symbol ids and rule indexes are scoped to the program generation
that issued them, identified by snapshot (#595). snapshot is an opaque
integer the service increments each time it adopts a program: the initial
load, every content-changing reload, and every recovery after a failed
load — a failed attempt invalidates the served snapshot, so even restoring
the input to byte-identical content adopts a fresh generation and ids
issued under the dropped one stay refused. It is a generation counter, not
a content digest.
Every response that issues numeric symbol/rule addresses reports the
generation they belong to in a snapshot member: symbols, rules,
references, usage, findings, lint, lintRules,
persistentObjects, inspect, analyze, project, and costEstimate
all carry it (on the wright-result/v1 envelope for inspect/analyze,
in the result object elsewhere). A request that addresses a symbol or rule
numerically must echo that value: references and usage require
snapshot when symbol is an integer, cfg and rules when rule is
an integer, and references when its rule filter is an integer too —
every numeric address is checked (#592). A numeric address without
snapshot, or with another generation, is refused stale-id; the
client re-issues the id from a current response's snapshot and id
members, or addresses by declared name. Listing symbols or rules
has no side effect on id validity — a symbols {max: 0} response still
reports snapshot, and it is the value, not the act of listing, that
scopes the ids it reports.
snapshot is ignored when every address in the request is a declared
name: name addressing resolves against the current program in both states
and is never stale, so a name-addressed request may carry a stale or
absent snapshot without error.
inputIdentity labels the input, not its freshness: it is the SHA-256
digest of the input's primary entry source (the provider's
sourceIdentity per LPP ADR-0003). For a single-file input that covers
the whole input; for a provider directory project it hashes only the
entry file, so editing a member file leaves inputIdentity unchanged.
The stale-id refusals above, not this value, report freshness.
The responses that issue numeric ids or indexes into the loaded program are
symbols (symbol ids), rules (rule indexes), usage (the resolved id),
references, findings, lint, persistentObjects, inspect, analyze,
project, costEstimate, and the ambiguous-* refusal candidates, plus
skipped[].rule in lint and lintRules. cfg block ids are local to
that response, not program addresses; callGraph, targetMetadata,
capabilities, the compile/check envelopes, and the edit/provider
payloads issue none. Positional fields such as rule, action, value,
and span.file inside findings and references are coordinates into the
current program, always computed from the program live at request time.
rule on rules/references/cfg/costEstimate/editStatements
edits[], callGraph's caller, and CLI --rule/--caller/cfg <RULE> share one grammar resolved by the shared semantic index: NAME
selects a uniquely-named rule; NAME#N the Nth declaration of NAME
(1-based, no leading zeroes; unnamed rules take #N). A literal name may
contain #, so both readings compete — one resolves, none is
unknown-rule, more is ambiguous-rule listing canonical addresses
(lone a#2 resolves literally; a, a, a#2 makes a#2 ambiguous,
a#2#1 selects the literal). Outputs carry the
canonical address — rules[].address, owningRule.address, finding
ruleAddress, callGraph callerAddress, costEstimate
elementCount.rules[]/rule address, lint skipped[].ruleAddress,
condition-tree ruleAddress — NAME when unambiguous, else NAME#N;
reused it selects that rule or fails ambiguous-rule.
references/usage symbol takes NAME (non-rule symbols) or
KIND:NAME (globalVariable/playerVariable/subroutine); a
cross-kind collision is ambiguous-symbol naming each candidate.
Symbol ids and rule indexes stay different spaces; snapshot on a name
address is ignored (#595). Unmatched is unknown-symbol/unknown-rule with the #429
nearest-name message (or symbols/rules, #567); usage echoes id/kind.
findings, lint, and costEstimate accept optional selection fields:
severity: a threshold.errorreports errors only,warningerrors and warnings,infoeverything.rule: one lint rule id (the findingcode). An unknown id is a structuredinvalid-selectionerror, never a silent empty result.file: one source file. The reportedspan.pathspelling differs per surface, so the argument resolves to the same canonical file. The path as passed, root-relative, or absolute spellings all select it. A spelling naming no loaded source file isinvalid-selection(ADR-0035); a match selecting nothing stays an empty success.costEstimatefindings carry no span, sofileselects nothing there.max: a bound on the reported count, applied after filtering.
The CLI options --severity, --rule-id, --file, and --max on lint,
check, and analyze drive the same wright-driver selection, so both
surfaces return the same selected set for the same input and selection.
When a request applies any selection field, the result reports
selection: {"total": <set before selection>, "withheld": <dropped by max>, "byCode": {<code>: <count>}}:
findings becomes {"findings": [...], "selection": {...}}, while lint
and costEstimate add a selection member to their existing result objects.
byCode counts the filtered set per code before max truncates it, so a
capped result still reports every code's real count (#791). Requests without
selection fields receive the previous shapes unchanged.
lint and wright lint share one driver pipeline: an error finding
yields ok: false, exit: 1 (confirmed defects only — runtime-validated
rules; cli/lint.md). --fail-on <LEVEL> configures
SessionConfig.fail_on; no per-request failOn.
rules with "content": true adds the owner's
workshop-rs/rule-content-v1 document as each rule's content. Actions,
values, and operators use {"call": id, "args": [...]} with catalog ids
and arguments in catalog parameter order (lookup names each position).
Other nodes are literals, {"enum", "member"}, {"variable"},
{"subroutine"}, {"string"}, and {"localizedString"}. The flat actions
array shares its indices with cfg, references, and findings.
Wright adds available statement spans: a condition spans its whole expression
(left-operand start through right-operand end, or the whole Compare(...)
call); an action spans its call name through ), or its last token without
parentheses. Both exclude the terminating ;. Source-language content is the
lowered canonical program, independent of spelling and locale.
Without max, content returns at most 20 rules in the selection wrapper;
rule selects one. Selected content is never truncated. CLI:
wright inspect rules --content (--rule, --file, --max). OverPy canonical-id
spellings (names) await a provider capability.
rules, symbols, references, cfg, and callGraph accept optional
selection fields with the same semantics as finding selection: filters narrow
first, max bounds after, and a selection member reports {"total": <set before selection>, "withheld": <dropped by max>}. An unknown filter
value, such as a kind outside the listed domain or a rule/caller/callee
that matches nothing in the loaded program, is a structured
invalid-selection error, never a silent empty result.
rules:rule(one rule by index orNAME/NAME#N),file,max. The removednamefield is amalformed-requesterror, never silently ignored (ADR-0035).symbols:file,max, plus the pre-existingkind(globalVariable,playerVariable,subroutine,rule).kindpredates this selection contract: a request carrying onlykindkeeps the previous filtered bare array;file/maxwrap the result, and akindsent alongside them counts inside the reported selection.references:kind(declaration,definition,read,write,call),rule(references inside this rule, by index orNAME/NAME#N),file,max.cfg:kind(entry,exit,block,if,while,for),max. Selection filtersblocks, which keep their originalids, sosuccessorsand theentry/exitfields still address the full graph's block numbering.callGraph:caller(one rule byNAME/NAME#N; a duplicate isambiguous-rule, never merged),callee(a declared subroutine name),max.
The file field resolves like finding selection: any spelling that
resolves to the same source file selects it. A capped list reports a
truncation member with its total, shown and withheld counts, and an
expand hint. Unselected arrays become {items, truncation} only when
capped. Explicit selection wrappers retain their own totals. The CLI
options --only, --rule, --file, --caller, --callee, and --max
on inspect subcommands drive the same wright-driver selection.
CLI text, CLI JSON, stdio, JSON-RPC, and MCP expose the same actionable
content. JSON is compact and deterministic. check reports its verdict and
diagnostics; lint reports severity counts and actionable findings;
analyze reports overview, cost, hotspots, complexity, risks, and top
variables; bare inspect reports the overview, rule names, and detail commands.
Lint and risk findings carry code, severity, message, evidence, and a compact
location (path:line:column) when mapped; a repeated-value finding also
carries repeated — {expression, count, spanKind} where spanKind is
expression or enclosing-loop — stating as data what the message states in
prose (#791). Full spans and fix transactions
are available from findings; lint policy is available from lintRules.
Persistent-object details are queried through persistentObjects.
Two ADR-0030 prerequisites add shared members (#792): check's result now
carries program, the same summary members analyze's program reports —
origin, files, and declaration counts — and check, lint, analyze, and
inspect carry next, a list of complete wright command lines a renderer
prints as next: records or a summary footer. A references/inspect
reference also carries owningRule — {id, name, address, span} like a
rules list entry, or null when nothing owns it — so a consumer renders the
owning rule's location without a second lookup.
Full fact dumps are available only through the in-process CompilerSession
API; external callers use the narrower semantic operations.
General lists default to at most 20 items; lint also bounds its compact
result to 5500 bytes before the envelope so long finding messages cannot
consume the default token budget; the ranked analyze report keeps its
top-five hotspots, risks, and variables and top-three condition trees.
Capped lists report their total, truncation, and how to see the rest.
all: true lifts the default cap (CLI --all); filters and an explicit
max continue to apply. Verdicts and exit codes are computed on the
complete set before truncation. Static catalogs are not capped. brief
and --brief are removed.
ADR-0025 records this breaking
pre-1.0 shape change without a contract-id bump.
Edit transactions use source identities and half-open, 1-based line/column ranges. Provider positions use 0-based line/character coordinates. Wright proposes and validates edits; a caller remains responsible for applying them. Stale, overlapping, unsupported, or semantically invalid edits return an explicit refusal without a partial edit set.
On raw Workshop input the edit operation is supported directly. workshop-rs
owns the source semantics and Wright orchestrates the transaction:
validateEditTransactionapplies the caller's transaction to the currentsourcesand reparses/revalidates the edited project through the session's ownworkshop-rspath.sourcesis optional (#472): when omitted or null, the service reads the current text of the files the transaction names (eachedits[].source) from disk; when supplied, it remains the precondition text, so an embedder holding unsaved buffers keeps that ability. A transaction that produces malformed or invalid Workshop refuses with the real parse/validation diagnostics and no partial preview; a valid one returnsok: truewith apreviewof each edited source (edited text plus the post-edit identity). There is no Wright-side rename operation: a rename is a caller-proposed transaction over the identifier spansreferences/symbolsreport.- Non-Workshop input stays at the provider boundary: OPY refuses with
edit-requires-providernamingproviderValidateEdit, and kinds without a shipped provider keep theirsource-provider-unavailablerefusal. - Findings on
lintandfindingscarry afixmember when a rule offers a mechanically determined correction (#556):fix.kindnames the remediation,fix.summarydescribes it, andfix.transactionis theEditTransactionshapevalidateEditTransactionvalidates. Preview it throughvalidateEditTransactionand apply it through the caller's own write path; there is no separate apply operation. Fixes exist only on raw Workshop input forexact-evidence findings whose correction is unambiguous. The current fixable rules areduplicate-condition(remove-dead-branch: delete the unreachableElse Ifbranch) andrepeated-value(evaluate-once: mark each duplicated occurrence withEvaluate Once). Shapes the correction cannot safely cover, such as a condition that can move between evaluations or an occurrence the engine re-evaluates (the loop'sWhilecondition,Wait Until/Loop Ifconditions,Update Every Framesubtrees, parameters an enabled reevaluation mode keeps live), withhold thefixentirely rather than offer a risky edit.evaluate-onceis an acknowledgment idiom, not a cost reduction: at once-per-execution positions it freezes nothing, it marks the repeated evaluation as intentional so the finding retires.
editStatements (#855) is the structured form of the raw Workshop edit:
statement-level insert/replace/delete on a rule's conditions/actions
lists carrying rule-content-v1 documents instead of composed source,
returning the assembled EditTransaction plus its
validateEditTransaction validation. Its request and result rules are in
agent-contract-edits.md.
providerValidateEdit runs the same
validated-edit pipeline through the configured source-language provider
(opy today): Wright checks the caller's preconditions and translates
coordinates, the provider validates each edited document, and the edited
document set is rechecked before ok: true. The document set is
caller-supplied or session-derived:
language_idnames the configured provider:opyfor the shipped provider.documentsis optional (#548). When supplied, it is a map from URI to{uri, languageId, version, text}describing the project as the provider sees it. When omitted, the session's loaded project supplies the set: every member file read from disk atversion0, keyed byfile://URI, the same project view the session serves, so an agent never reconstructs the document set the session already holds. The omitted-set request reads the program like any other program-reading operation (#471): it triggers the deferred load and refuses with the loader's diagnostic on an unloadable project. Anedits[].sourceoutside the set refusesedit-unknown-source.sourcesis optional (#548) and defaults to each document's text. It is a map from URI to current text covering every source the transaction may edit: eachedits[].sourceonproviderValidateEdit. It is the precondition eachsource_identityverifies against (SHA-256 hex of the text; for the entry document this equalsproject.inputIdentity) and the text the edits apply to.providerValidateEdit's post-edit check blocks on errors in any supplied document, so the set should cover the project documents whose diagnostics may break.transactionranges are theEditRange1-based half-open columns every edit operation shares. The returned transaction'ssourcefields are document URIs and eachsource_identityis the identity of that document's current text, so the caller can replay or apply the edits itself.
Operations carrying caller-supplied documents answer without consulting the session's loaded project (#471); the operation refuses cleanly when no provider is configured.
lookup (#529, ADR-0021) resolves a free-text name guess to the language owner's accepted spelling and signature, and listEntries (ADR-0031) lists a kind's vocabulary page by page for editors. Both read language vocabulary only and work over a session whose project cannot load. Their request and result rules are in agent-contract-vocabulary.md.
The service response is either { "result": value } or
{ "error": { "code": string, "message": string } }. Error codes are the
machine-readable discriminator; error message wording is for people and is not
stable. Transport framing errors are separate from service refusals.
Workflow envelopes retain wright-result/v1 fields, including structured
diagnostics. Provider-owned diagnostics remain distinguishable through their
provider status and origin metadata. Wright findings are returned by the
findings/lint operations rather than converted into owner diagnostics. Source
spans retain their source path when mapped; missing or invalid source mapping
is reported as unmapped. Unsupported provider operations remain explicit
diagnostics or mutation refusals, not guessed source locations or textual
fallbacks.
capabilities is the version negotiation step. Clients should check
agent_contract and the advertised operation list before sending
requests, ignore unknown response fields, and not assume an operation
exists unless it is advertised.
wright-agent/v1 permits additive optional response fields and additional
operations whose requests remain valid for existing clients. Removing or
renaming an operation or field, changing a field's type or meaning, or changing
the response/error model requires a new major contract such as
wright-agent/v2; the v1 schema and its compatibility tests remain in place.
The CLI result envelope has its independent wright-result/v1 version; its
evolution policy is defined in docs/cli/machine-contract.md.
ADR-0025 records the essential-default
shape change across both contracts, including the removal of brief and full
external fact dumps.
One recorded exception applies to the same rule in both contracts, decided
before the 1.0 contract freeze (#134): the lint result's rules member
lists only each rule's id and effectiveSeverity (#431). Full rule
metadata, meaning summary, rationale, documentation, known limits, evidence,
and tags, is served once by lintRules.
ADR-0020 records a second pre-freeze exception (#472): sources is optional
on validateEditTransaction rather than required, so
agent callers stop resending whole files. The same exception extends to the
provider operation (#548): documents and sources are optional on
providerValidateEdit, defaulting to the
session's loaded project and each document's text.
The optional guide installed by wright agent install (distributed by
wrightkit/skills) teaches clients these capabilities; it is not required
to expose, execute, or validate any semantic operation.