Changelog¶
All notable changes to AAT are documented here. The format follows Keep a Changelog; versions follow semver with a 0.x caveat: the graph and plan formats may still change before 1.0.
Unreleased¶
Added¶
aat run show <batch-id>, or a path to a batch directory or itsbatch.json, shows the batch. Before, a batch reference was an error.- It shows the totals, and one row per permutation with its layers, outcome, and step counts.
- It shows what cleanup did across the runs: per cleanup node, the steps that ran and failed, and the pairings skipped
as released or by
when, plus each failed cleanup step. --jsonprints the same.- Without
--step, a part flag or--pathprints that part of every step that has it, cleanup steps included, as inaat run show latest --response --path error.code. It prints one line each, or a JSON array with--json. - A predicate assertion's message shows the expressions in its literals expanded, as in
predicate "amountReceived == 1000" is true. The result'sexprkeeps the predicate as written. - Under
--oas-validate, cleanup exchanges are checked against the spec and recorded inoasValidation. Understrict, an invalid one fails the cleanup step, and its cleanup chain still runs. - Form bodies can be written as
form:, a mapping of field names to values, in place of abody:query string. - The request is sent as
application/x-www-form-urlencoded, and every key and value is URL-encoded, keeping the brackets of a key. - A field whose whole value is one placeholder is left out when that input has no value (absent, null,
"", or an empty list or map), so optional fields need no conditional blocks. - Nested mappings write bracketed keys:
metadata: {source: web}sendsmetadata[source]=web. An input whose value is a map does the same, a list repeats its key as written (tags[]), and a map in a list writesitems[0][sku]. - Other text is rendered as one value. Iteration blocks, placeholders in field names, duplicate fields, and fields that nesting would send twice are errors when the template loads.
- A credential or overlay header that changes the Content-Type fails the request.
- The MCP server's template view shows the form.
aat validatematches an input that is the whole value of aform:field to that field, so a graph can name its inputs its own way:payment_method_types[]: "{{paymentMethodTypes}}"counts as the spec'spayment_method_types, in both the unknown-input and the required-field checks.aat validateandaat runreport a header orform:field whose whole value names no input of the node, which would never be sent.- Verification steps take
values, as main steps do, to read a particular step:values: {charge: {from: firstRefund.charge}}. A reference names a main step, andaat validatechecks the input names, the references, and the values' shapes. --resolutionsandaat run show --stepname the layer that set a value. A layer's literal value readslayer, and the record carrieslayer: currency-jpyfor any value a layer set. The MCP server'sinspect_archiveshows it too.settings.minRequestIntervalpaces requests for APIs with rate limits: the starts of any two requests are at least that far apart, such as250ms. One interval covers everything a command sends, including the plans of a parallel batch, retries, verification, and cleanup.aat promptand the MCP server'sexecute_planhonor it too. A value without a unit, such as250, is rejected when the environment file loads.- Step retries honor the server's
Retry-Afterheader, and on a 429 without one,RateLimit-Reset. A retry waits at least as long as the failed response asks, in seconds or until an HTTP date, instead of only the backoff. - Cleanup chains: a cleanup node can declare its own
cleanup. It runs right after that cleanup step succeeds and takes its outputs first, so a resource released in two calls (request, then confirm) is cleaned up completely. Chained outputs stay inside the chain. Archive cleanup records gaincleanupFor(cleanup_forin--json), naming the step each one cleans up after. - A cleanup pairing can say when it isn't needed:
cleanup: {node: voidPayment, when: 'status == "authorized"', releasedBy: [capturePayment]}. releasedBylists nodes whose success, after the creating step and with the same values for the inputs they share with the cleanup, releases the resource.whenis a predicate over the creating step's outputs, or the previous cleanup step's in a chain. The cleanup is skipped when it's false. Awhenthat can't be evaluated runs the cleanup and recordswhenError.- A skipped cleanup's chain doesn't run. Skips print as
skipped:lines undercleanup:, and are recorded in the archive'scleanupSkipped, incleanup_skippedin--json, in acleanup skipped:table inaat run show, and in the MCP server'sinspect_archive. - Loading a graph checks that
releasedBynames other nodes and thatwhenparses and names the node's outputs, soaat validate,aat run, and the MCP server all reject a bad pairing.aat docs generateadds When and Released By columns when a pairing uses them. - A
minormaxselection whose candidates tie prints a warning under its step, as inwarning: selection "cheapest": 3 of 8 elements from listProducts.products tie for min price at 19.99; picked index 0. - The warning is also in the step's
warningsin--json, inaat run show --step, and in the MCP server'sinspect_archive. - Archive selection records gain
field,sortField,sortValue,ties, andonTie. onTie: failon a selection fails the step on a tie instead, andonTie: firsttakes the first without a warning. Plan validation acceptsonTieonly onminandmax.aat run show --step ID --resolutionsprints how each of the step's inputs got its value, as JSON.- Each entry gives the source, the upstream step and output, the expression, pool pick, or constraint, and the selection that picked the value.
--steplists each input with its source, and--step --jsonincludesresolutions.- A step that fails before its request is sent keeps the resolutions made up to that point, with an
errorrecord for the input that failed. That covers an input that can't be resolved, a missing adapter, a request that can't be built, and a send error. Before, such a step recorded none. - The MCP server's
inspect_archiveshows each input's selection and error. aat run show --compactprints JSON on one line: a step part, with or without--path, and with--jsonthe step list, the step, or the shape.--compactwhere the output is text, such as--shapewithout--json, is an error.aat validateandaat runcheck that literal values fit their input's shape: a list for an array input, and no map for a single-value input. A list for a single-value input is still allowed, since a template can send it as repeated pairs.- It covers step values and pools, graph defaults, layers, and slot
injectvalues. - A step expected to fail, such as a mutation, isn't checked, since a negative test may send the wrong shape on purpose.
- A slot
injectvalue fails only when no input with its name takes it, since composition decides which steps it reaches. The composed plan's steps are then checked like any others. - A pool entry for an array input gets a hint: a bare list in a graph default or a layer is a pool, so write one list
as
{value: [...]}. - Expressions and custom types aren't checked.
- Expressions in assertions: a
fieldEqualsvalueand a quoted string in apredicateexprcan hold{{…}}expressions that name the step's inputs, such asvalue: "{{today + 3 days}}"orexpr: 'quantity == "{{quantity}}"'. - A quoted expression that yields a number or a boolean compares as one.
- An expression that can't be evaluated fails its assertion.
today,now, andunixtimeread the time the step's inputs were resolved, so a retry compares with what it resent.- Selection filters and cleanup
whenconditions stay literal. AUTOWIRE?in workflow templates marks an optional input that only some compositions feed. It is wired when a step produces the output, such as one an addon adds, and left unset otherwise.aat generatescaffolds HEAD, OPTIONS, and TRACE operations, form-encoded request bodies, and cookie parameters, which it sends as oneCookieheader. Body and response properties fromallOfbranches become inputs and outputs, and an OpenAPI 3.1 type list such as["null", integer]maps to its non-null type. The MCP server's operation search lists operations of every method too.aat run show <run>prints what an archive recorded, without a browser.- Without
--step, it lists the steps with their node, HTTP status, result, duration, and output names, then the verification and cleanup steps. --stepshows one step.--request,--response,--inputs, and--outputsprint that part as JSON.--pathnarrows the part with a gjson path.--shapeprints the part's structure: each path with its type, array sizes, how many objects hold a key, and a sample value.- The run is
latest(runs inside batches included), a run ID, a batch ID and run ID joined by a slash, or a path to a run directory, an archive file, or an.aarexport. - The MCP server's
get_sample_responsetakespathandshape, asaat run showdoes. - Its
run_idacceptslatestand a batch ID with a run ID, and finds runs inside saved batches. - The same goes for
inspect_archive,analyze_failure, anddiff_archives. - The AI assistant primer is published as raw Markdown, for tools that fetch pages.
llms-full.txtat the docs site's root holds the whole primer, andllms.txtindexes it and the reference pages.aat docs primerprints the same primer from the binary, in the version that matches it.- The primer links to docs pages by URL, so its links also work outside the site.
- OAS validation checks form-encoded request bodies (
application/x-www-form-urlencoded) against the operation's schema. Bracketed keys such asitems[0][sku]=…andtags[]=…are read as nested objects and arrays, and values take the types the schema allows. Before, only JSON request bodies were validated, and a form body was not checked. {{@index}}in an iteration block is the element's position, counting from 0, for keys such asitems[{{@index}}][sku].aat generate --operationand--pathscaffold only some of a spec's operations: the operationIds named, or the operations under a path, matched by whole segments. An operationId the spec doesn't have is an error that suggests similar ones, and so is a path that matches nothing.- Expressions for generated values and timestamps:
{{uuid}}(a random version 4 UUID),{{random N}}(Nrandom digits and lowercase letters),{{now}}(UTC, RFC 3339), and{{unixtime}}(Unix seconds, an integer).nowandunixtimetake offsets in seconds, minutes, hours, or days, such as{{unixtime - 1 hours}}. Each occurrence generates its own value, and a retried step resends the values its first attempt generated.
Changed¶
aat run show --step --jsonnames a step's assertion resultsvalidation({passed, results}), asarchive.jsondoes, where it saidassertions.aat validatenotes a workflows, layers, or plans directory that the manifest names but that doesn't exist yet. It reads as empty, where before it failed the manifest check.- Manifest messages print paths relative to the manifest.
- A missing layers directory loads no layers. A run that asks for a layer says the directory doesn't exist.
- A header whose whole value is one placeholder, such as
Idempotency-Key: "{{requestKey}}", isn't sent when that input has no value, instead of failing withunresolved placeholders. The{{?requestKey}}…{{/requestKey}}wrapper still works, and a placeholder inside other text still needs a value. aat generatescaffolds a form body asform:, one field per property with noContent-Typeheader, and writes an array property the spec encodes as adeepObjectasname[]. An object property no longer warns, since a map value is sent as bracketed keys. An optional header parameter is a plain placeholder.- A
from, afromInput, or a named selection'sfromadds the step it reads todependsOnwhen the plan is instantiated, as graph defaults and composition already did. Plan validation no longer reports a reference whose step is missing fromdependsOn, and a cycle message names the value or selection that implies a dependency. An isolated mutation checks its clone IDs against the dependencies references imply. - A graph default's
from: node.outputreads the nearest earlier step on that node that isn't expected to fail, instead of the node's first step. On a verification step it reads the last such step, chosen before mutations expand. - Before, a verification after a refused request on the node read the refused step and failed with
no outputs, and one after two successful steps read the first. - AUTOWIRE passes over steps expected to fail as well.
- A graph default's literal value reads
graph_defaultin--resolutions, where it readplan_default, which now means a value the plan sets. - Composition wires the AUTOWIRE markers that the slot and addon passes leave, once the plan is complete: a base or slot step can take an output that only an addon produces, and a base workflow without slots or addons resolves its markers. The final pass takes the nearest earlier producer that does not depend on the step. Markers the earlier passes already wired are unchanged.
- A plan that still holds an
AUTOWIREmarker after composition fails validation, naming the step and input, instead of sending the wordAUTOWIRE.aat promptasks the model for those inputs, andaat validate plancomposes standalone base workflows before validating them. - Workflow compatibility checks cover base workflows and slot options. A plain
AUTOWIREthat the base and its slots cannot feed is a warning that names any addon producing the output.AUTOWIRE?on a required input with no graph default is also a warning.AUTOWIRE?in an addon never warns as unfed. aat generateno longer replaces an existing graph file or template: it lists them, writes nothing, and exits 2 unless given--force. OperationIds that differ only in case are an error, since their templates would be one file on a case-insensitive file system.- A generated template sets
Content-Typeonly when it has a body, to that body's media type; before, every operation with a request body gotapplication/json.aat generatewarns about what a template leaves to write by hand: multipart and other bodies, a body schema withoneOf,anyOf, or no properties, and a parameter with a non-defaultstyleorexplode: false. OAS validation reads request body properties fromallOfbranches too. - A list value in a request URL or form body is no longer sent as JSON text. Right after
key=in a query or form body it repeats the pair (tags=a&tags=b); anywhere else its elements are encoded one by one and joined with commas (/items/1,2). - A step whose failed response asks for a wait longer than 60 seconds stops retrying (
failed_fast), and the error detail says how long the server asked for. Before, the retries went out after the backoff regardless. - Docs: the Homebrew cask is documented for Linux as well as macOS.
brew install gburgyan/tap/aatinstallsaatandaat-sandboxwith Homebrew on Linux. minandmaxselection compare numbers sent as strings by value: asortFieldof"99.10"sorts below"1000.00", so an API that returns prices or totals as decimal strings selects the cheapest element. A string that is not a number still fails the selection, naming the element.- Graph validation rejects cleanup pairings that loop back, such as a node cleaned up by
bwhose own cleanup is that node again. The error names the cycle once, asa → b → a. - Cleanup step IDs are unique within a run: a node's second cleanup step is
deleteCart_2in archives and--jsonoutput, as the web UI already named it. Cleanup responses are checked against the graph's error detection rules. A flagged response recordsresponseBodyErrorand ends its chain; the run outcome is unchanged. - Breaking:
aat run plan --dump-stateredacts credentials by default, as run archives do. - Credential headers such as
AuthorizationandX-API-Keyread[REDACTED], at the top level and in every step. - Known secrets are replaced wherever they appear, and the export gains
"redacted": true. --dump-state-secretskeeps live credentials, for a harness that sends requests as the run's session. It warns on stderr when the dump goes to stdout, and it is an error without--dump-state.- A harness that reads
auth.headersto send requests needs the new flag. - Docs: the AI assistant primer covers more ground:
- layers and batches
- step value forms: pools, literal lists, and
{} - expressions and assertion details
- retries and selection ties
- Lua transforms, and reading results with
aat run show - Docs: two pages disagreed with the code and are corrected:
plans.mdno longer says{}skips graph defaults for required inputs.value-flow.mdsays an inlineminormaxcan usefieldalone, and how ties break.--oas-validate strictstops before the first request, with exit code 2, when a spec the graph references fails to load. Before, the run printed a warning and continued without validating, and--quietand--jsonhid the warning. Inautomode the warning now goes to stderr, where--quietand--jsonkeep it visible.aat promptbehaves the same way.- In a form body, or in a path after its
?, an iteration block whose body writes a wholekey=valuepair joins its copies with&instead of commas, so{{#tags}}tags[]={{.}}{{/tags}}sendstags[]=a&tags[]=b. A block whose body starts or ends with&is repeated with nothing between the copies. Blocks elsewhere, including JSON bodies and blocks inside a pair, still join with commas. aat generatewrites each optional form body field as one conditional block that brings its own&, so a body of many optional fields grows with their number rather than its square. A request body the spec declares empty (no properties andadditionalProperties: false) gets no body and no warning. A form body property that takes an object, or an array of objects, gets a warning to write its keys by hand as bracketed pairs.- A run that stops on a step's error names the step, as in
step "addSocks" (addItem): executing HTTP request: …, so theaat:line and the--jsonerror say which step failed. - A retried step sends the same inputs on every attempt. Its values are resolved once, before the first attempt, so a
random pool pick, a
todaydate, and an overlay value no longer change between attempts. A plan-level--retriesrerun still resolves them again. uuid,now, andunixtimeare reserved words in expressions, so{{now}}no longer refers to an input namednow. An offset in hours or minutes ontoday, or on a reference, is an error that suggestsnoworunixtime.- Plan validation lists each distinct problem once,
aat validateprints an error repeated word for word once, and a step's values and selections are checked in name order, so the output is the same on every run. - A registered cleanup no longer runs when a later main step on the cleanup node already released the resource with
the same inputs, such as an explicit cancel of the order the pairing would cancel. Before, the cleanup ran again
and usually failed with a 4xx. The skip is recorded, as for a pairing's
releasedBy. - A slot option's
injectvalues decode like graph input defaults. {value: …},{pool: …},{from: …}, and expressions work. Any other key is an error, soinject: {ages: {default: [35]}}fails validation instead of sending a map. A bare list is still the literal list.- An injected value no longer replaces a step's pool, constraint, or
fromInput. injecton a base workflow or an addon, where it was ignored, is a validation error.- Validation reports an assertion of an unknown type, such as
fieldEqual, and bad expression syntax in step values, pools, and assertions. The checks run inaat validate, when a workflow template loads, and when a recipe's override assertions are applied. Before, an unknown type failed only at run time, a recipe override of one was dropped silently, and a bad expression failed only when its step ran. - An optional input that takes
from:an output the earlier step didn't return, directly or through a named selection, is left out, asAUTOWIRE?leaves one unset, instead of failing the step. Its resolution recordsoptional_skip. A required input still fails, andaat validate --strictwarns when a required input takesfrom:an optional output, in a graph default or in a plan or workflow file, or reads one through a named selection. The MCP server'svalidate_planandsave_planlist the warnings for a plan file. - Docs: the OAS validation pages no longer claim checks that don't run (the HTTP method and input types in
aat validate, and every request at run time). The AI assistant primer covers starting from an OpenAPI spec, form bodies and query strings, headers and idempotency keys, lists and pagination, the request timeout, and reaching an object after an expected failure.
Fixed¶
- OpenAPI specs with circular references load. A schema that refers back to itself, directly or through another
schema, used to fail with
infinite circular reference detected, soaat generate,aat validate, and the MCP server rejected the spec, andaat runskipped OAS validation. Large published specs have such cycles. Other errors in a spec still fail it, and libopenapi's log lines no longer reach stdout. - Runs with OAS validation start quickly on a spec with hundreds of operations. The validator is built for the operations the graph's nodes name, not for the whole spec, which could take most of a minute before the first request.
- OAS validation accepts
nullfor an OpenAPI 3.0 schema markednullable: truethat is built withanyOforoneOf, such as a field that holds either an ID or an expanded object. The validator addednullonly to a schema's owntype, so a response with such a field set tonullfailed withgot null, want object. - A request body that is neither JSON nor form-encoded, or a schema the validator can't compile, is reported as not
validated. The archive marks the payload
skippedwith a reason, the step line showsOAS: request not validatedorOAS: response not validated, and aschemaassertion is skipped with the reason. Before, the step line could readOAS: 0 warning(s), and aschemaassertion failed with an empty message. - A form-encoded body no longer sends the final newline of a
body: |block, which the server read as part of the last value. Whitespace around the body is removed for form bodies only. - The static OAS check no longer reports a required form field or query parameter as missing when the template writes
it. The keys of a form-encoded body count as supplied, and a bracketed key such as
metadata[source]suppliesmetadata. - The static OAS check no longer reports an input that the template sends only in request headers, such as an
idempotency key, as missing from the operation's parameters and request body. The template names the header, and
specs often leave such headers undeclared, so
aat validate --strictfailed on it. - Two inputs that pick from the same array with
minormaxby different fields no longer get the same element. The selection cache left out the compared field, so the second input got the element chosen for the first. Amatchselection'sfilteredSizeis now the number of matching elements, not the array's size. - A required input marked
{}falls back to its plain default with layers applied, and evaluates the expressions in it, sopostalCode: {}with a default of{{env.postalCode}}sends the variable's value, not the text. A layer's value for the input wins over the graph default, as it does without{}. - A request that times out says so, naming aat's 30-second request timeout, instead of giving only Go's
context deadline exceeded (Client.Timeout exceeded while awaiting headers). The limit is named only when the whole limit passed, so a shorter timeout or an interrupted run isn't blamed on it. aat generatewrites the graph'soas:reference relative to the graph file's directory, so a spec kept elsewhere resolves. It used to write only the spec's file name.aat generatetypes object body propertiesobjectand inserts them as JSON literals instead of quoted strings, and no longer writes a node-levelname:line.aat run planandaat run batchredact the access token they authenticated with, such as an OAuth2 token, wherever it appears in an archive. Before, it was masked only in credential headers.
0.1.0 - 2026-09-12¶
The first release with release archives, a Homebrew cask, and a Docker image. v0.0.1 through v0.0.4 are
retracted, so go install and go get skip them.
Added¶
aat-sandbox, a second binary that serves an offline e-commerce demo API (aat-sandbox serve: shop API on :8765 with OAuth2 tokens, payments API on :8766 with an API key,us/euregions with their own currency, tax, tiers, and coupons, an order state machine, simulated latency, and two chaos hooks for retry demos) and extracts theexamples/shopproject (aat-sandbox init). The contract lives inexamples/shop/openapi.yaml; the server tests validate every response against it.make buildbuilds both binaries;make sandboxbuilds only the demo server. The sandbox binds127.0.0.1unless--hostsays otherwise.examples/shop, the offline quick-start project foraat-sandbox: a 17-operation graph, Quick Purchase and Checkout workflows with slots and addons, 12 layers, 7 plans (full order lifecycle, retries, a negative state-machine walk,addItemmutations), a declined-card overlay, a receipt visualizer,us/euenvironments with payments routed to their own host and credential, and MCP configuration for AI coding tools. Its graph describes the API for the AI tools that read it through MCP: each operation lists its error codes in the order they are checked, and each input and output says what it must be, where it comes from, and what it means.- The shop example ships an integration kit, the part of an AAT project that an API's integrators get.
aat-kit.yamlnames the graph, templates, OpenAPI spec, domain file, workflows, sandbox environment, and three reference plans.sh package-kit.shcopies them into a directory and a tarball, with the kit manifest as itsaat-project.yaml. Theshop-apiserver in.mcp.jsonreads the kit manifest, so it shows what an integrator's AI tool would see.make example-shoppackages the kit, then validates and runs the unpacked copy. - Multi-environment files: one environment file holds named environments with shared settings, inheritance
(
extends),${var}substitution in every string, abstract environments (names starting with_) to inherit from, andinclude:to split out files such as secrets.--env NAME,AAT_ENV_NAME, an overlay'senvironment:, or the manifest'sdefaultEnvironmentselects one, in that order.defaultEnvironmentapplies only to the manifest's own environment file, and a single-environment file still loads, with--envan error for it.aat env listlists the environments, andaat validatechecks each one. --var KEY=VALUE(repeatable) onaat run plan,aat run batch,aat prompt,aat validate,aat env list, andaat mcp servesets a var of a multi-environment file for one invocation, for example to pointexamples/shopat a sandbox on other ports or in a container. A key the file never declares or references is an error.- An environment's
values:map holds per-environment data that plan values, graph input defaults, and layers read with{{env.KEY}}; an OS environment variableKEYwins over it. - Overlays (
--overlay FILE,.aat-overrides.yaml, and nowaat-overrides.yaml) can setauthandheadersfor every request, andenvironment:to choose the environment. Overlay headers win over the credential on every route.overrides:entries in environment files and overlays acceptvalues:andexpectFailure:, so a happy-path plan reruns as a negative test without edits; the archive records each override value as the input's resolution, with the sourceoverride_value. - OAuth2 auth accepts
grantTypeandextraParamsfor providers such as Auth0.--verbose-authlogs token requests and responses to stderr, with the access token masked and at most half of a short password or client secret shown. - Step
mutations:expand a step into independent sibling steps when the plan is instantiated, each withset:input overrides or arawBody:for a malformed payload.mutationScope: isolatedgives each sibling its own copy of the steps it depends on, and--no-mutationsdrops mutations for a quick run. Theschemaassertion type validates a response against the OpenAPI spec; it was a stub. - Checkpoints:
aat run plan --stop-after STEPstops after a step and skips cleanup, so what the run created stays alive, and--dump-state FILE(-for stdout) exports the live session for another tool: the environment's base URL and auth headers, each step's base URL and request headers, unredacted, and the step outputs. The file is written with mode0600. With--json, the summary nests the state understateand reportsstopped_at. - Ctrl+C ends
aat run planandaat run batchasaborted(exit code130), even during a request or a retry wait. Cleanup still runs, within a 30-second budget, and the archive is written. The web UI styles theabortedandstoppedoutcomes. aat mcp serve --httpserves MCP over Streamable HTTP, with a/healthzendpoint.--loglogs each tool call as JSON to stderr.--hostonaat web,aat web view,aat web viewtrace, andaat mcp servechooses the interface to bind (default127.0.0.1; theAAT_HOSTenvironment variable sets it when the flag is absent). The Docker image setsAAT_HOST=0.0.0.0.- Plan-level
execution.cleanupsteps execute after the main flow, honoringrunOn: always|success|failure. A step that names a node paired with a graphcleanup:does not run on its own: itsrunOndecides whether the pairings run, once per created resource, newest first, and never for a create step that failed. Other cleanup steps run first, in declaration order. - Plan
execution.verificationsteps execute after the main flow and before cleanup, with their assertions counting toward the run outcome. They appear in archives asverify_<node>steps. - Workflow slot options and addons can declare
verification:. Composition merges it into the base workflow's, and a node they verify replaces the base's verification of that node. - A step that succeeds after retrying shows
retried Nx: <category>in run output and on the web UI's timeline and step page. Archives record the category of each retried attempt inretriedOn, and the--jsonstep summary inretried_on. - Run output lists each failed assertion under its step (
status: expected status 200, got 201), and the--jsonstep summary includes them asfailed_assertions. statusassertions accept a status class such asexpect: 2xxorexpect: 4xx.retry.onandretry.failOnaccept HTTP status codes (on: [503]) alongside category names;aat validate planrejects unknown rules.--oas-validate strict(andsettings.oasValidation: strict) now fails a step whose request or response violates the OpenAPI spec; it previously behaved likeauto.expectFailuresteps are exempt, and skipped validations or schema compilation warnings never fail a step.aat validatechecks the layers directory: parse errors, duplicate layer names, and layer input keys that match no node input (which layers silently ignored).aat validatechecks the domain file andvisualizers.yaml(Domain and Visualizers sections), and names each error's file relative to the working directory.- Template conditional and iteration blocks accept keys with hyphens, such as
{{?X-Request-Id}}…{{/X-Request-Id}}for a header parameter. - The web UI's step request view copies the request as a
curlcommand. aat run cleandeletes auto-generated run and batch archives and keeps named ones.tools/aat-to-junit.pyconverts a run archive to JUnit XML for CI test reports.- Workflows accept a
selectionHintthataat promptshows the model when it chooses a workflow. The trace viewer's Copy Conversation button copies a planning call's model settings and conversation as JSON. aat webreports a clear error (exit code 2) when the frontend bundle is not embedded, such as a plaingo installbuild; the CLI, MCP server, and CI features still work in that build.aat --versionreports the module version forgo install …@vX.Y.Zbuilds.- Release pipeline: versionless archive names for stable
releases/latest/download/…URLs, a Homebrew cask in a tap (brew install gburgyan/tap/aat), and a multi-arch image atghcr.io/gburgyan/aat. Release archives and the Homebrew cask includeaat-sandboxnext toaat, and the release notes are this file's section for the version. make example-shoprunsexamples/shopagainst a local sandbox the way the CIexample-shopjob does, andmake clibuildsaatwithout rebuilding the web UI.- A documentation site built from
docs/userwith Material for MkDocs (mkdocs.yml), deployed to GitHub Pages by a new Docs workflow that fails on broken links, broken anchors, and pages missing from the navigation.make docsruns the same strict build locally. - Documentation pages for installing, checkpoints, archives,
aat generate,aat docs generate, the examples, and the airline case study; the Lua transforms page is written out. The quickstart (on the public Petstore API) and the tutorial (built by hand againstaat-sandbox) are rewritten and were run verbatim; the previous versions did not load. - Docs: Share Your API with Integrators describes an integration kit for any API: what to ship and what
to keep, a kit manifest beside the project manifest, internal environments that
include:the shipped file, packaging and checking the kit in CI, and what an integrator's AI tool can read through theapipersona. It publishes the prompt, the setup, and the results of the shop's single-prompt Python and Go client runs, so the claim that a kit lets an AI tool write a working client from one prompt can be checked. The docs home page, the examples index, Project Setup, and MCP Server link to it. make demosregenerates the docs site's recordings and screenshots against a freshaat-sandbox: VHS recordings ofaat run plan full-lifecycleand a parallel layer-group batch, Playwright screenshots of the run timeline, a step's request with Copy as cURL, and the batch matrix, plus an MP4 of the plan run and the repository's social preview (demos/). It checks that both recorded runs passed and that each GIF stays within its size budget before writing anything. The docs site's home, matrix-testing, and web UI pages show the results.- Repository scaffolding: issue and pull request templates,
SECURITY.md, andROADMAP.md.
Changed¶
- BREAKING: the environment file flag
--env FILEis now--env-config FILE, and--env NAMEselects a named environment.--env-overlayis now--overlay. - BREAKING: exit codes follow one rule on every command:
0passed.1a test or validation ran and found a failure.2AAT could not do what was asked. That covers an unknown flag, argument, or subcommand, and a project, environment, or--varerror. Most commands exited1for these.130aborted.
Command by command:
- aat validate exits 1 when it finds a problem, and 2 when there is no manifest to validate or
--var is bad.
- aat prompt exits with its run's outcome code, instead of 1 for any run that did not pass.
- aat import, aat generate, and aat docs generate exit 2 on an error.
- BREAKING: project YAML is decoded strictly. A key that no field accepts — in the manifest,
environment files and their includes, overlays, the graph, templates, the domain file, visualizers,
workflows, layers, plans, recipes, and plan YAML given to the MCP plan tools — is an error naming the
file, the line, and the likely intended key (plans/smoke.yaml: line 12: unknown key "fromSelecton"
in step value (did you mean "fromSelection"?)). Such keys were silently ignored, so a typo produced a
plan that loaded and did something else. A second YAML document in a file (--- followed by content)
is an error too. aat prompt model output (JSON) is unaffected. To migrate, run aat validate and fix
what it lists.
- BREAKING: a manifest that exists but fails to load is an error for every command that discovers
it; it was skipped, so commands fell back to a lower-priority project or to none. A missing manifest
is still skipped, and a higher-priority manifest that loads still wins.
- BREAKING: request templates escape each substituted value for where it lands. Values used to go in
raw.
- Path: a value is URL-encoded as one path segment before the first ?, and as a query component after
it. a/b stays one segment, and & or # in a value can no longer add a parameter or cut the URL.
- JSON body: a value inside quotes is JSON-escaped, so a quote, backslash, or newline in a field such
as notes no longer breaks the body. A value outside quotes is written as JSON: arrays and objects as
JSON, numbers in plain digits, and null.
- Form body: values are URL-encoded.
- Headers and other bodies: unchanged.
To send a malformed payload on purpose, use a step's rawBody.
- BREAKING: JSON keys follow the convention of the document they are in:
- The aat run batch --json summary's batchId is now batch_id. It is left out when the batch stopped
before it started.
- In run archives, a step's duration_ms is now durationMs. AAT and tools/aat-to-junit.py still read
archives that use the old key.
- A plan's auth in metadata.plan and metadata.instantiatedPlan uses camelCase keys (tokenUrl,
credentials) instead of Go field names (TokenURL, Credentials).
The state object that --json nests with --dump-state - keeps the camelCase keys of the --dump-state
file.
- BREAKING: a recipe's overrides must name steps of the composed plan. An override for any other step ID,
such as an addon step without its inc0_ prefix, used to be ignored; now the recipe fails to load, and the
error lists the plan's steps. A value override on an input that the workflow template wires with from,
fromSelection, or fromInput now replaces that wiring and sends the override. It used to have no effect.
- BREAKING: Lua transforms can no longer load code or reach the host process. The package library is gone,
and so are the base library's dofile, loadfile, load, loadstring, require, module, getfenv,
setfenv, collectgarbage, and newproxy. An integration kit's templates run on its users' machines, so a
transform must not read their files. Also, dofile() and loadfile() with no argument read stdin, which under
aat mcp serve is the MCP connection.
- BREAKING: aat run batch <filter> selects plans by whole path segments. orders selects orders.yaml
and every plan under orders/, and orders/refund selects one plan. The filter matched the start of each
plan's path, so smoke also ran smoke-eu.yaml.
- BREAKING: a batch that finds no plans exits 2, and the message names the filter and the plan
directories. It used to pass with nothing run, so a mistyped filter passed in CI. An absolute path that does
not exist gets the same error.
- BREAKING: aat run batch --json reports an error that stops the batch before any plan runs in a top-level
error field, with an empty runs array. It used a run entry with no plan name.
- BREAKING: only aat run plan and aat run batch take the execution flags: --env, --env-config,
--graph, --templates, --domain, --override, --overlay, --var, --retries, --layer,
--no-auto-overrides, --oas-validate, --verbose-auth, and --no-mutations. aat run clean and
aat run rebuild-summaries accepted and ignored them; they now reject them.
- Template headers no longer replace the auth credential, an override's own headers, or overlay headers. They
still replace environment and plan headers, such as a per-operation Content-Type. Header names compare
case-insensitively when headers merge.
- Run output names each step by its step ID, which --stop-after, dependsOn, the archive, and the web
UI use, with the node in parentheses when the two differ and the column has room
(addProduct (addItem)); it printed the node, so two steps on one node looked alike. Engine errors do
the same (step "addSocks" (addItem) returned status 409), and so do the parallel batch display and the
MCP execute_plan table.
- Durations are wall-clock: a retried step's duration runs from its first attempt to the end of its last,
so retry waits count, and a run's duration (the PASSED line, the --json summary.duration_ms,
batch.json run entries, the web UI, and MCP) is the time the run took, recorded in the archive as
result.durationMs. Both were sums of the last attempt of each step, so full-lifecycle printed 955ms
for a three-second run. Archives written before keep showing the sum. Step durations of a second or more
read 1.4s.
- A step that fails after retrying prints its error followed by the same retried Nx: <category> note as
a step that recovers; the note used to take one of two other forms depending on the terminal width.
- aat run progress output marks OpenAPI violations on each step (OAS: 1 warning(s)) and totals them
after the outcome, as documented; only an unused summary path printed them before.
- aat validate and aat validate workflow show OpenAPI and workflow-compatibility warnings as a WARN
section without --strict instead of reporting OK, and counts read "1 file" rather than "1 files".
- The sequential batch header no longer prints mode=strict, a leftover of the runtime modes removed in
0.0.2.
- aat web and aat mcp serve --http listen on 127.0.0.1 by default instead of every interface.
Pass --host 0.0.0.0 (or set AAT_HOST) to accept connections from other machines.
- Steps composed from workflow templates (recipes, aat prompt) get a default status: 2xx
assertion instead of status: 200, and none when they declare expectFailure. On a step with
expectFailure (including one added by an overlay), a status assertion that expects success is
reported as skipped, since the expected-failure status list is the status check; one that agrees
with it, such as 409 or 4xx, is evaluated.
- The --json step summary's name is the step ID, as documented, instead of the node name, so
mutation siblings and repeated nodes are distinguishable.
- aat generate --oas places optional query parameters, headers, and body properties in conditional
blocks, orders body properties as the spec does, and writes integer, number, boolean, and array body
values as JSON literals. With --output-graph - it writes no files unless --output-templates is
given. A template header that resolves to an empty conditional is not sent.
- A layer that sets a value source (value, pool, from, fromResolved) replaces the graph
default's source instead of merging with it, so a layer value is no longer shadowed by a default's
from.
- Requesting layers (--layer, --layer-group, or a recipe's selection.layers) without a
layers: directory in the manifest is an error; the layers were silently ignored before.
- An override that declares its own auth no longer sends the inherited credential header
(Authorization, or the top-level API key header) to its host.
- Override precedence: among glob (and among exact) overrides the last registered match now wins, so
.aat-overrides.yaml, --overlay, and --override take precedence over env.yaml overrides as
documented. Exact names still beat globs.
- Cleanup input matching scans earlier steps in execution order (it was map order).
- MCP operation details and explain_field label a graph input's default "Test default": it is data AAT
sends in tests, not a value the API fills in. Read as a plain default, the shop's quantity: 1 and
method: card made required fields look optional.
- CLI description and --help text describe AAT as graph-based API workflow testing; the LLM is
optional and authoring-time only, and execution never calls one.
- Documentation covers features that had none: the Ctrl+C aborted outcome, --oas-validate, the batch
matrix view, archive import and export, and aat run rebuild-summaries.
- libopenapi-validator v0.14.0 and libopenapi v0.38.7.
- Minimum Go version is 1.25.7 (the OpenAPI libraries require it).
Removed¶
- BREAKING: YAML keys that nothing read, which strict decoding now rejects: step
fallback,assertions.semantic, the environment settingsmaxRunDuration,defaultRetries, andarchiveFormat(retry with a step'sretry:or--retries), templateresponse.validate, thepromptfield of selections and graph defaultselect, and recipeoverrides.descriptions. - BREAKING: the
llmselection strategy, which plan validation already rejected butaat promptaccepted from the model, and thewarnOpenAPI validation mode, which behaved exactly likeauto. - The MCP
execute_plantool no longer accepts the obsoletemodeparameter (the runtime strict/lean/adaptive modes were removed in 0.0.2). - Repository leftovers from the private airline project (
setup.sh, a root-level plan, IDE run configurations, an empty case-study stub).
Fixed¶
- Building the web UI (
make frontend,make build) no longer rewrites a tracked file, andmake cleanno longer breaksgo buildby deleting one. Vite writes the bundle toserver/web/dist/app, which git ignores, and the embed is satisfied by the trackedserver/web/dist/placeholder.txt. - An
errorDetectionequalsrule with a number (value: 0) matches the JSON number. The YAML integer and the JSON number used to compare as different types, so the rule never matched. A map or listvalueis now a validation error; at run time it crashedaat runand the MCP server. - A request path value with an encoded
/(%2F) keeps it inside its segment; the executor used to decode it into a real/. The archive records the URL as the executor joins it, instead of concatenating the base URL and the path. - A number of a million or more fills a placeholder in plain digits instead of exponent form (
1.2e+06), and a map fills one as JSON instead of Go syntax (map[k:v]). - Archive references stay inside the archive directory:
aat import --namemust be a single directory name. A name that starts withrun-orbatch-gets the!prefix, as a name derived from the file does.--name ../xused to import outside the archive directory.- The web server answers
404for a run, batch, or trace ID that is not a single directory name. An ID of..read the parent directory'sarchive.json.PUT /api/runs/{id}/namerenamed any directory in the archive directory; it now renames only runs and batches. - The MCP archive tools reject a
run_idthat is not a directory name. The plan tools reject absolute plan names and names that climb out of the plans directory. - An unknown subcommand is an error (exit
2), with a suggestion when the name is close. This coversaat run bogusand unknown subcommands ofaat plan,aat env,aat mcp, andaat docs. They printed help and exited0, so a mistyped subcommand passed in CI. Commands that take no arguments, such asaat validateandaat web, now reject stray arguments instead of ignoring them. aat mcp servereports a manifest that fails to load with the load error; it said the manifest was not found.aat importfails on such a manifest instead of importing into_output/runs.aat run plan --jsonandaat run batch --jsonprint the error document for every error that stops them before a plan runs. Before, a manifest that failed to load, a bad--var, or an overlay environment that could not be resolved left stdout empty.- Graph-level cleanup deletes the resource each step created. Cleanup looked up the creating node's
outputs by node name, but outputs are stored by step ID, so a step with its own
idfell through to the first step with an output of that name: twocreateCartsteps with their own IDs deleted the first cart twice and left the second. Cleanup now reads the registering step's outputs, then the most recent step with a matching output. - A request that fails before any response reports
executing HTTP request: …once, notexecuting request: executing HTTP request: …. aat mcp servestarts when a relative--manifestnames anoas:spec. The spec path was joined onto the graph's directory a second time (examples/shop/examples/shop/openapi.yaml), so a project loaded from another directory failed with "no such file or directory".- The MCP
get_sample_responsetool returns the newest successful response for an operation, and a failed one, marked as such, only when no run succeeded. It took the newest response of any status, so a negative test's409could pass for the sample. It also searches the runs inside batch directories, and a manifest withoutarchivesgets the expected output shape instead of an error. The archive tools accept the ID of a run inside a batch. - A run archive that cannot be redacted is not written.
aat run, batch runs, and the MCPexecute_plantool report the error; before, the error was ignored and the archive was written with its secrets in place. Redaction fails only on a value JSON cannot hold, such as a NaN from a Lua transform. aat mcp servewithout--personaregistersget_data_flow,get_response_shape, andexplain_field, which only theapipersona had, so it has every tool: 39 with an OpenAPI spec, 32 without.- The web UI's run timeline shows a step's assertion count only when the step has assertions, not
0 / 0on every step. - Run archives redact known secrets from every string they hold: request URLs and query parameters,
request and response bodies, outputs and display outputs, error, assertion, and OpenAPI messages,
and plan step values, as well as headers, inputs, and resolved values;
batch.jsonentries too. In JSON bodies only string values change. Before, a credential used as an input was redacted ininputsbut kept in the body of the same request. - Archive redaction no longer mangles ordinary data: the oauth2
usernameandclientIdare not treated as secrets, and a secret shorter than eight characters is redacted only where a whole value equals it. The shop sandbox'sdemocredentials had turneddemo@example.cominto[REDACTED]@example.comin inputs. Overlapping secrets are redacted completely; map order could leave part of one visible. - Run archives redact an API key sent under a custom
auth.headerName, the credentials of host overrides and overlay overrides (they were never collected as secrets), and the literal credentials and credential headers of the plan stored inmetadata.planandmetadata.instantiatedPlan;aat promptarchives also collect the credentials of.aat-overrides.yaml. aat generate --oasmarks a response property the schema does not list asrequiredas an optional output with an optional extract rule, so a scaffolded step no longer fails when the API omits it.- A
selectwith afilterand nostrategyworks likematchinstead of failing at run time withunknown selection strategy. --manifestnaming a file that does not exist is an error instead of silently falling back to manifest discovery.aat run planno longer prints a failed or errored run's message a second time on stderr.- A project's manifest no longer inherits fields it leaves out (such as
domain,layers, ordefaultEnvironment) from a lower-priority project named byAAT_PROJECTor the user config'sdefault_project; the highest-priority manifest found describes the whole project. aat validate,aat validate plan, and the MCP plan tools resolve recipe layers, so a misspelled layer is reported and layer-supplied inputs no longer fail validation.- MCP
execute_planapplies override values,expectFailure, and recipe layers likeaat run plan. aat plan listsummarizes recipes instead of reporting a parse error for each.- Workflow compatibility checking (
aat validate) accounts for slots: an addonAUTOWIREinput that every option of a slot produces is no longer reported as unfed, an addon that attaches after a slot option's node is checked instead of skipped, and slot options are no longer checked as bases of their own. - The static OpenAPI output check looks each output up at its template extract path, through nested objects and array items, instead of requiring a top-level response property named after the output.
settings.oasValidationand--oas-validatereject unknown values instead of treating them asauto.- A workflow template whose
verification:names a node missing from the graph fails to load, as cleanup entries already did. aat run batch --parallel Nwith runtime OpenAPI validation no longer has a data race: parallel runs share one loaded spec, and libopenapi-validator v0.13.1 wrote into the schema model while rendering a response schema behind a$ref. The upgrade to v0.14.0 removes the race, so validations still run concurrently.- Workflow composition fills slots in declaration order, so merged cleanup, slot verification, and slot
injectvalues no longer vary between runs, and neither can batch dedup fingerprints. - The sequential batch display prints a run's
OAS: N warning(s)total, as the plan display does. - Cleanup steps carry a step ID and a start time in archives, so the web UI places them on the timeline; their start time was empty.
- The batch By Test matrix no longer clips its rotated permutation labels: the header grows to fit the longest, and a wide matrix uses the space beside the page column. The run timeline shows a step's node only when it differs from the step ID.
--override NODE=URLroutes keep the environment headers, plan headers, overlay headers, and the credential, like anoverrides:entry with thatmatchandbaseUrl; they used to send no headers.aat promptrejects layers when the manifest sets no layers directory instead of silently running without them.- Lua transforms:
print()writes to stderr instead of stdout (where it corrupted--jsonand--dump-state -output),return {}is a valid empty set of outputs, and a template with a transform but noextractrules runs its transform; outputs a transform computes no longer fail the adapter output check. aat generate --oasextracts an array property of an object response by its name instead of@this.- The static OpenAPI check accepts a required parameter or body property that the template sends itself
(such as a literal
"photoUrls": []), which madeexamples/petstorefailaat validate --strict. aat validatechecks workflow templates in subdirectories of the workflows directory (such asworkflows/slots/), which it skipped.- A layer's
fromResolvedapplies over an existing graph default instead of being dropped. - The plan summary resolves
intent.goalas a step ID;aat plan listtruncates long goals on character boundaries. - Visualizers receive
--color-text-secondary,--color-danger, and--color-warningas documented; the web UI sent variable names it does not define. aat validate,aat generate --oas, and the MCP OpenAPI operation details include parameters declared on an OpenAPI path item (such as a shared{cartId}), not only those on the operation.
0.0.4 - 2026-03-04¶
Changed¶
CONTRIBUTING.mddescribes what a pull request needs and the project's policy on AI-assisted contributions.
0.0.3 - 2026-03-03¶
Added¶
- MCP server personas:
--persona apifor integrating with an API and--persona testfor writing and running tests. New tools browse the OpenAPI spec, return a sample response from past runs, and show templates, and tool errors say what to try next. - Runtime OpenAPI validation of requests and responses (
--oas-validate), with schema compilation problems reported as warnings and request and response errors shown separately. - Layer-group batches detect plans that come out identical across layer permutations and run each once. The web UI's batch page hides the skipped runs, dims their representative outcome, and adds a By Test matrix with a filter for each dimension.
- Visualizer plugins (
visualizers.yaml): custom views of a step, rendered in a sandboxed iframe in the web UI. - Run and batch summaries and the web UI group issues by category.
fromInputfills an input from another step's input, and an output can be optional when a response may omit it..aat-overrides.yamlis discovered automatically, for local development overrides.- Terminal output uses color and fits the terminal width, batch runs show progress and accept
--shuffle, and progress output shows retries. - A
rawflag on mechanical assertions evaluates the whole response body. - Archives and the web UI record when an override routed a step to another URL.
- Archives and the MCP server report the build's version.
Changed¶
- The project is renamed from Adaptive API Testing to Adaptive API Toolkit.
- Plan composition has a single entry point, and
aat prompt's planning pipeline is simpler, with a plan summary written for the user.
Fixed¶
- Inputs with graph defaults are no longer classified as supplied by a layer.
- Layers embedded in a recipe load during layer-group batch runs.
- Batch runs no longer get duplicate run group IDs (#1).
- A batch run that errored no longer shows "run not found" in the web UI.
aat web view latestopens a batch when it is the most recent archive.- Duplicate cleanup step IDs no longer leave the web UI spinning.
- Dependencies from
requiresandsatisfiestokens stay within an addon or the base workflow, so composed plans no longer get dependency cycles across them.
0.0.2 - 2026-02-25¶
Added¶
aat web view <file>opens an archive file without a project.- GitHub Actions CI runs the tests with the race detector, golangci-lint, and a gofmt check;
make checkruns the same checks locally.
Fixed¶
make clean test buildworks.- Data races found by the race detector.
0.0.1 - 2026-02-24¶
The first tagged version.
Added¶
- API graphs in YAML: nodes with typed inputs and outputs, auto-wiring, constraints, and value pools from a domain file.
- Request templates with conditional (
{{?a|b}}) and iteration blocks, response extraction, and Lua transforms. - The execution engine: dependency-aware scheduling; values wired from earlier steps, with array selection
strategies and named selections; constraint-aware fallback; retries by error category;
expectFailurenegative tests; response-body error detection; mechanical assertions; and graph-level cleanup. - Workflow templates composed from slots and addons; plans, compact recipes, and data layers, with
--layer-grouppermutation matrices. aat run planandaat run batch(with--parallel), plan-level retries,--jsonand--quietoutput, and exit codes for CI.aat prompt: an LLM drafts a plan from a sentence, with opt-in planning traces.- OpenAPI support:
aat generate --oasscaffolds a graph and templates, andaat validatechecks them against the spec. aat validatefor the graph, plans, and workflows, with project manifest discovery (aat-project.yaml).- The MCP server (
aat mcp serve): tools for the graph, templates, domain, OpenAPI spec, plans, execution, and archives, plus resources and prompts. aat docs generate: Markdown and Mermaid documentation from the graph.- Run archives with the full decision trail and redacted secrets; export and import, saved names, and fast run listing.
- The web UI (
aat web): run and batch lists, a run timeline, step detail with audit tabs, and a planning trace viewer (aat web viewtrace). - Per-node routing to other base URLs (
--overrideand environmentoverrides:), plan-level auth and headers, and OAuth2 token caching. - The Petstore example, the user documentation, and the Apache 2.0 license.