Skip to content

Archives

Every plan execution writes a JSON archive: aat run plan, each plan in aat run batch, aat prompt when it executes the plan, and the MCP server's execution tool. An archive records each request and response, how every input got its value, every assertion result, cleanup, and the outcome, so you can inspect a run long after it finished. The web UI is the viewer; the files are plain JSON for scripts too.

Where Archives Go

Archives are written to the directory given by --output, else the manifest's archives: entry, else _output/runs in the working directory. aat web, aat web view, aat run show, aat import, aat run clean, and aat run rebuild-summaries look for them the same way.

Layout

A single run gets its own directory:

_output/runs/
  run-20260910-225958-d819f460/
    archive.json
    attempt-01.json
    summary.json

A batch gets a directory holding batch.json and one run directory per executed plan (duplicate permutations skipped by dedup appear in batch.json but get no directory):

_output/runs/
  batch-20260910-225919-0754c0ea/
    batch.json
    run-20260910-225919-5a04ca45/
      archive.json
      summary.json
    run-20260910-225921-80a38128/
      archive.json
      summary.json
    ...

Directory names are run- or batch-, the local date and time (YYYYMMDD-HHMMSS), and eight random hex characters.

File Contents
archive.json The full record of the run. With --retries, the final attempt
attempt-NN.json An earlier attempt that --retries retried (attempt-01.json, attempt-02.json, …), in the same format as archive.json. Present only when the run was retried
summary.json A small summary the web UI reads for its lists: run ID, timestamp, outcome, step counts, duration, plan name, attempt numbers, layers, issues counts, and oas: the OpenAPI validation mode, validatedRequests and validatedResponses, and violations, recorded whenever the graph references a spec, a clean run included. Derived from archive.json; see Rebuilding Summaries
batch.json Batch metadata (batch ID, source directory, layers, layer groups), one entry per run including skipped duplicates (plan name, run ID, outcome, counts, layers, permutation, duplicateOf), and the aggregate result

What an Archive Contains

archive.json has these top-level keys:

Key Contents
metadata Run ID, timestamp, environment name, graph version, AAT version, the plan as loaded (plan), the plan after graph defaults and layers were merged in (instantiatedPlan), the layers applied, attempt/totalAttempts for retried runs, and oasValidation, the OpenAPI validation mode, when the graph references a spec
steps One record per main and verification step (see below)
cleanup Cleanup step records in the same format; absent when no cleanup ran
cleanupSkipped Registered cleanups that did not run because they were no longer needed: node, cleanupFor, reason (released or when), and releasedBy or when. See API Graphs: Cleanup. Absent when none were skipped
knownIssues The knownIssue entries that kept a failure out of result.outcome: stepId, node, until, reason, url. When present, a passed run still holds a failed step. Absent when none applied
knownIssuesResolved Entries whose step passed anyway, so the entry has outlived the defect and should be deleted. Absent when none
result outcome (passed, failed, error, aborted, or stopped), error, durationMs, the run's wall-clock time (archives written before it was recorded omit it, and the web UI then sums the step durations), knownIssues, how many failures an entry covered, and endedEarly when a covered failure stopped the run short

Each step record holds:

Field Contents
stepId, node, startTime, durationMs Which step ran, and when: a retried step's start and duration cover every attempt and the waits between them. Archives written before 0.1.0 name the duration duration_ms; AAT reads both
inputs The resolved input values
request Method, full URL, headers, and body. A body that isn't JSON, such as a form-encoded one, is archived as the text it was sent as, and its Content-Type header says which it is. When an override routed the step elsewhere, originalUrl holds the URL it would have used
response Status, headers, and body
outputs, displayOutputs Extracted outputs (after any Lua transform, whose script is in transformScript) and the plan's display outputs
resolutions How each input got its value: source (such as plan_default, graph_default, layer, expression, plan_from, select_edge, or fallback_pool), the layer that set the value, the raw and final value, the step and output it came from, and whether a constraint passed
selections For inputs picked from an array: source step and field, array size, filter and how many elements passed it, strategy, and the selected index
validation Each assertion's type, pass or fail, and message
expectFailure For negative steps: expected statuses, actual status, pass or fail
oasValidation Request and response checks against the OpenAPI spec
errorClassification, error, retryCount, retriedOn Error category and detail for a failed step, and the category of each step-level retry
iterations, repeatStop On a step with a repeat block: each request it sent, with its request, response, outputs, OpenAPI validation, and whether untilMet, plus the inputs it sent on a step that pages with next. repeatStop says why the step stopped: until, exhausted (the listing's last page), loop (a cursor came back), max, timeout, or error. The step's own request and response are its last request's, and OpenAPI counts in summary.json cover every request
cleanupFor On a cleanup step: the step whose resource it releases, or the cleanup step before it in a cleanup chain. Cleanup step IDs are unique within a run (deleteCart, then deleteCart_2)
whenError On a cleanup step: why its pairing's when condition couldn't be evaluated, such as an output the step didn't return. The cleanup ran anyway

A gRPC step

A step that made a gRPC call records what the server sent as well as the HTTP status AAT maps it to, so a reader comparing numbers and a reader looking for a code both find what they need:

{
  "request":  {"method": "GRPC", "protocol": "grpc",
               "url": "grpc://localhost:8767/shop.v1.Payments/Charge"},
  "response": {"status": 400,
               "grpcCode": "INVALID_ARGUMENT",
               "grpcMessage": "CARD_DECLINED: card ending in 0002 was declined by the issuer",
               "trailers": {"content-type": "application/grpc"},
               "body": {"code": "INVALID_ARGUMENT", "message": "..."}}
}

trailers is kept apart from headers because a gRPC server chooses which to send a value in, and both are redacted the same way. An HTTP step carries none of these fields. See gRPC.

What Is Redacted, and What Is Not

Archives redact the credentials AAT knows about, not every piece of sensitive data, so an archive, an exported .aar/.aab, or a CI artifact is not automatically safe to commit or share. Before you attach one to a ticket, check what it holds.

Redacted:

  • Credential headers, by name. In requests and responses, the values of Authorization, Proxy-Authorization, X-API-Key, X-Auth-Token, Cookie, and Set-Cookie (any capitalization) are replaced with [REDACTED]. The header name stays.
  • Known secrets, everywhere. AAT collects the resolved secret credentials of every auth block that can apply to the run — the environment's, its host overrides' (such as the shop's payments key), the plan's, and those of overlays and their overrides — plus the LLM API key and, in aat run plan and aat run batch, the access token the run authenticated with. A secret credential is any credential except the oauth2 username and clientId, which identify an account rather than prove it. Each secret is redacted from every string in the archive: URLs and query parameters, headers under any name, request and response bodies, inputs and resolved values, outputs and display outputs, error and assertion messages, and the archived plan. In a JSON body only string values change; keys, numbers, and layout are kept. batch.json is redacted the same way.
  • How a secret matches. A string that equals a secret is always replaced. A secret of at least eight characters is also replaced inside longer strings, including its URL-escaped forms. A shorter secret is not, because it is an ordinary word in data too: with the shop sandbox's demo password, "password": "demo" is redacted but demo@example.com is kept.
  • Plan credentials. In metadata.plan and metadata.instantiatedPlan, the value of every source: literal credential in the plan's auth block and the plan's credential headers are replaced with [REDACTED], whatever their length. source: env references keep their variable names.

Stored as-is:

  • Tokens AAT does not know as secrets anywhere other than a credential header, such as a session token a login step returns, or the access token of a run started by aat prompt or the MCP server's execute_plan, echoed in a body.
  • Other sensitive data an API returns or a plan sends, such as personal data, and a secret written as a literal step value, which is not a credential AAT knows about.
  • A short secret inside a longer string, as described above.

Terminal output and the --json summary are not redacted: they are the live view of the run, like the requests themselves.

summary.json holds no request data. The live-state dump written by --dump-state redacts nothing at all; see Checkpoints: Security.

Viewing Archives

aat web serves the archive directory; aat web view opens one run or batch in the browser:

aat web view                                    # the run list
aat web view latest                             # the newest run or batch
aat web view run-20260910-225958-d819f460       # a run
aat web view batch-20260910-225919-0754c0ea     # a batch

A reference is a directory name in the archive directory, saved names included; AAT treats it as a batch when the directory holds batch.json. latest opens whichever run or batch in the archive directory has the newest timestamp. If a server is already listening on the port (--port, default 9119), aat web view opens the page there; otherwise it starts a temporary server that runs until you press Ctrl+C.

Viewing a File

The reference can also be a file: an exported .aar or .aab, or a bare archive.json or batch.json, for example from a downloaded CI artifact. AAT loads it into memory and serves it from a temporary server, with no project, manifest, or archive directory needed, and writes nothing to disk:

aat web view exported-run.aar
aat web view nightly.aab
aat web view downloaded-artifacts/run-20260910-225958-d819f460/archive.json

Next to an archive.json, sibling attempt-NN.json files are loaded so the attempt selector works; next to a batch.json, the member run directories are loaded. A file is read-only: save, rename, export, and import are unavailable.

Known issue: aat web view <file> always starts its own server, so it fails with bind: address already in use while aat web (or another aat web view) holds the port. Pass a different --port.

Inspecting a Run from the CLI

aat run show prints what an archive recorded, for a terminal, a script, or an AI coding assistant that cannot open the web UI. It only reads: it writes nothing, not even summary.json.

aat run show latest                                     # the steps
aat run show latest --step checkout                     # one step
aat run show latest --step checkout --response --shape  # the structure of its response
aat run show latest --step checkout --response --path orderId

The run is one of:

  • latest: the newest run, runs inside batches included, even those of a batch that is still running
  • a run ID, such as run-20260910-225958-d819f460, looked up at the top of the archive directory and then inside each batch
  • a batch ID and a run ID joined by a slash, such as batch-20260910-225919-0754c0ea/run-20260910-225919-5a04ca45
  • a batch ID and a plan name joined by a slash, as the batch view's PLAN column shows the plan, such as batch-20260910-225919-0754c0ea/negative/state-machine: the plan's run in that batch. A run ID of the batch wins over a plan of the same name, and a permutation skipped as a duplicate has no run. A plan that ran as several runs, such as its layer permutations, exits with code 2 and lists them (see A Batch)
  • a path to a run directory, an archive.json or attempt-NN.json, or an exported .aar file
  • a batch ID, such as batch-20260910-225919-0754c0ea, or a path to a batch directory or its batch.json, which shows the batch instead of a run (see A Batch)

IDs are looked up in the archive directory, as Where Archives Go describes.

Without --step, it lists the steps: ID, node, HTTP status, pass or fail, duration, and output names, then the verification steps and the cleanup steps, each with the step it releases. Above them, the header names the plan and the archive, and, when the graph references an OpenAPI spec, what validation checked. After the shop's smoke recipe:

run-20260914-112509-419e1d5f  PASSED  2ms
plan: Buy one in-stock product and pay by card
archive: /path/to/shop/_output/runs/run-20260914-112509-419e1d5f/archive.json
oas: auto, 4 requests and 7 responses validated, 0 violations

  #  STEP           NODE           STATUS  RESULT     TIME  OUTPUTS
  1  listProducts   listProducts      200  pass        0ms  currency, products
  2  createCart     createCart        201  pass        0ms  cartId, status
  3  addItem        addItem           201  pass        0ms  cartId, lineCount, subtotal
  4  checkout       checkoutCart      201  pass        0ms  currency, discount, orderId, receiptNumber, shipping, status, subtotal, tax, ta…
  5  paymentCharge  paymentCharge     201  pass        0ms  amountDisplay, orderStatus, paymentId, status

cleanup:
  #  STEP         NODE         STATUS  RESULT     TIME  FOR
  1  deleteOrder  deleteOrder     204  pass        0ms  checkout
  2  deleteCart   deleteCart      204  pass        0ms  createCart

A run that a known issue kept green prints a known issues: table after the steps, naming each covered step, the date its cover runs out, and why — which is what explains a FAIL row inside a PASSED run. Entries whose step has started passing again are listed separately under known issues no longer needed:.

--step takes a step ID, or a node name when that node ran only once. On its own, it prints: - the step's method and URL, status, result, duration and retries, and error - the inputs, each with where its value came from - the outputs, with arrays and objects by their size - assertion counts, with each failure - its knownIssue, when it has one: the reason, the date it lapses, and where it is tracked - warnings, such as a selection tie - a repeated step's requests, one line each: status, time, and whether until held, the inputs each sent that differ from the step's when it pages with next, and its outputs - the sizes of the request and response bodies, which on a repeated step are its last request's

A step that failed before its request was sent still shows the inputs resolved up to the failure, and the one that failed.

step checkout (node checkoutCart)
POST http://localhost:8765/us/v1/carts/cart_0001/checkout
status 201  pass  0ms
inputs:
  cartId         "cart_0001"  plan_from createCart.cartId
  customerEmail  -            optional_skip
  deliveryDate   -            optional_skip
  notes          -            optional_skip
  postalCode     "78701"      expression {{env.postalCode}}
  shippingTier   "standard"   graph_default
outputs:
  currency       "USD"
  orderId        "ord_0001"
  status         "created"
  total          10340
  ...
assertions: 1 passed, 0 failed
request body: 48 bytes
response body: 518 bytes

A repeated step lists its requests after its assertions, and the body sizes it prints are its last request's. The shop's state-machine plan reads its customer's orders a page at a time in a verification step:

$ aat run show latest --step verify_listOrders
step verify_listOrders (node listOrders)
GET http://localhost:8765/us/v1/orders?limit=1&customerEmail=audit-j6ojn7id%40example.com&after=ord_0001
status 200  pass  0ms  2 requests (stopped: exhausted)
...
requests:
  #  STATUS     TIME  UNTIL  SENT              OUTPUTS
  1     200      0ms         -                 liveOrderCount 0, nextCursor "ord_0001", orderCount 1, orders [1 item]
  2     200      0ms         after "ord_0001"  liveOrderCount 0, nextCursor "", orderCount 0, orders [0 items]
request body: none
response body: 45 bytes

These flags print one part of the step instead:

Flag Prints
--request, --response The request or response body, as indented JSON
--inputs, --outputs The resolved inputs or the extracted outputs, as JSON
--resolutions How each input got its value, as JSON. Each entry is the input's resolution record (source, fromStep and fromOutput, expression, the pool pick, a constraint) with the selection that picked the value, and an error for an input that couldn't be resolved
--path PATH Only what a gjson path selects: lines.0.sku, or lines.#.sku for every element. The $.lines[0].sku form works too. Without a part flag, it reads the response body
--shape The part's structure instead of its values. Without a part flag, the response body's
--max-bytes N Cut a printed part after N bytes, 65536 by default, with a note on stderr; 0 prints everything
--json The step list or the step as JSON with snake_case keys, or the shape as a JSON array. A step's JSON includes resolutions, warnings, and validation, its assertion results as archive.json names them (archive.json itself uses camelCase keys). A repeated step's also includes iterations: each request's status, duration_ms, until_met, retries, error, the sent inputs that differ from the step's, and its scalar outputs
--compact JSON on one line, for a script or jq: a part, with or without --path, and with --json the step list, the step, or the shape. With --shape, or without a part, it needs --json

--iteration N, with --step, reads one request of a repeated step instead, counting from 1. On its own it prints that request's URL, status, and time, whether until held, why the step stopped when it is the last, the inputs it sent when the step pages, its outputs, its OpenAPI validation, and the sizes of its bodies. With --request, --response, --inputs, --outputs, --path, or --shape, it prints that request's part; --json works as for a step. --resolutions needs the step alone, since a repeated step resolves its inputs once for every request.

A form-encoded body prints as its decoded fields, one name=value per line, since a single escaped string is not what the step sent. --compact prints the body as it went over the wire, and --path and --shape read the value its bracketed keys describe, so --path metadata.source selects from metadata[source]=aat-stripe and a key ending in [], or one sent more than once, reads as an array.

Without --step, a part flag or --path prints that part of every step that has it, cleanup steps included, one line each. One command then answers a question about the whole run:

$ aat run show latest --response --path error.code
STEP                  NODE           VALUE
unknownCoupon         applyCoupon    "COUPON_INVALID"
addAfterCheckout      addItem        "CART_NOT_OPEN"
shipBeforePay         shipOrder      "INVALID_TRANSITION"
returnBeforeDelivery  createReturn   "INVALID_TRANSITION"
refundBeforePay       paymentRefund  "PAYMENT_NOT_REFUNDABLE"
declinedCard          paymentCharge  "CARD_DECLINED"
payTwice              paymentCharge  "INVALID_TRANSITION"
shipCancelled         shipOrder      "INVALID_TRANSITION"
refundTwice           paymentRefund  "PAYMENT_NOT_REFUNDABLE"

With --json or --compact, it prints a JSON array of step_id, node, and value. --shape still needs --step.

--shape is the way to learn a large response. It prints one line per path: the path's type, an array's item count, how many objects hold a key when not all of them do, and a sample value. The elements of an array are merged, so a key that only some elements hold, or a value that is sometimes null (string|null), shows up. Each path works as an extract rule in a template and as --path:

$ aat run show latest --step checkout --response --shape
orderId            string  "ord_0001"
receiptNumber      string  "RCPT-US-0001"
status             string  "created"
...
total              number  10340
totalDisplay       string  "$103.40"
lines              array   1 item
lines.#            object
lines.#.sku        string  "SKU-1001"
lines.#.quantity   number  1
lines.#.lineTotal  number  8999
createdAt          string  "2026-09-13T03:42:35Z"

Archives several hundred megabytes in size still list their steps in under a second, and so does the shape of a response tens of megabytes long.

A path that matches nothing, an unknown step, or an unknown run exits with code 2, and the message says what does exist: the top-level keys, the step IDs, or the forms a run reference takes. Archives are redacted when they are written, so aat run show prints only what the archive holds; see What Is Redacted, and What Is Not. The MCP server's get_sample_response takes the same path and shape; see MCP Server.

A Batch

A batch ID, or a path to a batch directory or its batch.json, shows the batch: its totals from batch.json, OpenAPI validation totals across its runs, one row per permutation, and what cleanup did across its runs.

$ aat run show batch-20260914-112520-9e406b57
batch-20260914-112520-9e406b57  PASSED  27.4s
batch: _output/runs/batch-20260914-112520-9e406b57/batch.json
runs: 63, 27 passed, 0 failed, 0 errors, 36 skipped as duplicates
oas: auto, 161 requests and 330 responses validated, 2 violations

  #  RUN                           PLAN                         LAYERS                                             RESULT  STEPS     TIME
  1  run-20260914-112520-74bbc499  full-lifecycle               -                                                  pass    15/15     2.5s
  2  run-20260914-112523-9369a809  full-lifecycle               basket-apparel                                     pass    15/15     2.2s
  ...
 14  run-20260914-112546-ff31eb17  negative/state-machine       -                                                  pass    16/16      3ms
  ...
 28  -                             full-lifecycle               shipping-standard,basket-apparel                   skip        -        -  duplicate of full-lifecycle [basket-apparel]
  ...

cleanup:
  NODE          RAN  FAILED  RELEASED  WHEN
  deleteCart     27       0         0     0
  deleteOrder    26       0         0     0
  • RAN and FAILED count cleanup steps across the runs.
  • RELEASED and WHEN count the pairings skipped: because a step released the resource, or because the pairing's when was false.
  • Cleanup failures: a cleanup failures: list names each failed cleanup step by run, with its status or error.
  • --json prints the same as a document with snake_case keys.
  • OAS: the oas: line sums the oas counts in the runs' summary.json files: the mode, the request and response bodies validated, and the violations. It appears when a run recorded them, and ends recorded by N of M runs when not every run did, such as a run from before AAT recorded them. --json prints it as oas, with runs.
  • Violations include those of steps expected to fail, which never fail a step. The 2 above are the shop's add-item mutations, which send a zero quantity and a malformed body on purpose.
  • Steps: a batch has no steps of its own, so --step and the part flags need a run: batch-ID/run-ID, or batch-ID/PLAN for a plan with one run in the batch.

A plan name from the PLAN column names the plan's run:

$ aat run show batch-20260914-112520-9e406b57/negative/state-machine
batch-20260914-112520-9e406b57/run-20260914-112546-ff31eb17  PASSED  3ms
plan: The order state machine refuses out-of-order transitions
archive: /path/to/shop/_output/runs/batch-20260914-112520-9e406b57/run-20260914-112546-ff31eb17/archive.json
oas: auto, 12 requests and 18 responses validated, 0 violations
...

A plan that ran as several runs, such as the permutations of a layered batch, exits with code 2 and lists them:

$ aat run show batch-20260914-112520-9e406b57/full-lifecycle --step checkout
aat: plan "full-lifecycle" ran as 6 runs in batch batch-20260914-112520-9e406b57; name one of them:
  batch-20260914-112520-9e406b57/run-20260914-112520-74bbc499  no layers
  batch-20260914-112520-9e406b57/run-20260914-112523-9369a809  layers basket-apparel
  batch-20260914-112520-9e406b57/run-20260914-112525-46a3dd76  layers shipping-express,basket-apparel
  batch-20260914-112520-9e406b57/run-20260914-112527-60ccacd7  layers basket-gear
  batch-20260914-112520-9e406b57/run-20260914-112529-32fa0acc  layers shipping-express,basket-gear
  batch-20260914-112520-9e406b57/run-20260914-112531-f9449320  layers shipping-express

Exporting and Importing

A run or batch can travel as a single zip file.

Export

Export is a web UI feature; there is no CLI command for it. In the run list, each row has a download arrow (Export run or Export batch), and run and batch detail pages have an Export button.

  • A run exports as .aar, a zip of its directory's JSON files: archive.json, summary.json, and any attempt-NN.json.
  • A batch exports as .aab: batch.json plus every member run's directory.

The file is named after the run or batch ID, or its saved name (run-20260910-225958-d819f460.aar, checkout-baseline.aar). The content is the archive as written, so the redaction limits above apply to exports too. The same downloads are available at GET /api/runs/{id}/export and GET /api/batches/{id}/export.

Import

  • Web UI: the Import button on the run list accepts .aar and .aab files, adds them to the served archive directory, and opens the imported run or batch.
  • CLI: aat import FILE extracts into the archive directory.
  • API: POST /api/import with a multipart form whose file field holds the archive (100 MB limit). The response is {"ref": …, "name": …, "type": "run"|"batch"}; a name that already exists returns 409.
$ aat import "nightly run #3.aar"
Imported run "nightly-run-3" → /path/to/shop/_output/runs/nightly-run-3
Flag Type Default Description
--name string derived from the file name Directory name for the imported archive
--output path manifest archives, else _output/runs Archive directory to import into
--manifest path auto-discovered Explicit path to aat-project.yaml

Without --name, the web UI, the API, and the CLI derive the directory name from the file name: the .aar/.aab extension is dropped, every character other than letters, digits, ., _, and - becomes -, repeated dashes collapse, and leading and trailing dashes go. A result that starts with run- or batch- gets a ! prefix, so run-20260910-225958-d819f460.aar imports as !run-20260910-225958-d819f460. An archive imported without --name is therefore always a named run that aat run clean never deletes.

--name must be a single directory name, without path separators, and a name that starts with run- or batch- gets the same ! prefix.

An import never overwrites or merges. If the target directory exists, it fails (exit code 2 from the CLI):

aat: import: directory "nightly-run-3" already exists

The file must be a zip with archive.json (a run) or batch.json (a batch) at its root. Symlinks, absolute paths, and .. entries are rejected, as are zips with more than 10,000 entries or more than 500 MB uncompressed.

Naming and Saving Runs

A directory whose name starts with run- or batch- is auto-generated and a candidate for aat run clean. Any other name, or a name starting with !, marks a named (saved) run or batch: it is never deleted by aat run clean and shows under the Saved filter in the web UI's run list.

On a run or batch detail page in the web UI:

  • Save opens a name field, prefilled with the plan name (for a batch, its source). Save in that form renames the directory to the name you typed; Save as-is keeps the ID and adds the ! prefix (!run-20260910-225829-a4c05ee5). A typed name that starts with run- or batch- also gets the ! prefix.
  • A saved run shows its name, a pencil (Edit name) to rename it again, and Unsave, which restores the original ID from the archive's metadata.

A name cannot contain / or \ or be . or .., and renaming fails if a directory with the new name exists. Naming works only when serving a directory, not a file.

The same operations are PUT /api/runs/{id}/name with a body such as {"name": "run-keep"} (an empty name saves as-is) and DELETE /api/runs/{id}/name to unsave; batches use /api/batches/{id}/name. The response carries the new directory name to use in URLs:

{"ref":"!run-keep","name":"run-keep"}

Because only the directory name matters, renaming a directory by hand works too. Quote a ! name in the shell, for example '!run-20260910-225829-a4c05ee5'.

Pruning Old Runs (aat run clean)

The archive directory grows by one directory per run or batch. aat run clean deletes auto-generated run and batch directories older than a cutoff:

aat run clean                # delete runs older than 7 days
aat run clean --days 30      # keep a month
aat run clean --dry-run      # list what would be deleted, remove nothing
Flag Type Default Description
--days int 7 Delete runs older than this many days
--dry-run bool false Preview without deleting
--output path manifest archives, else _output/runs Archive directory to clean
--manifest path auto-discovered Explicit path to aat-project.yaml

Only top-level directories whose names start with run- or batch- are candidates, and their age comes from the timestamp in the name, not from file times. Deleting a batch directory deletes its member runs with it. Named directories are skipped and counted:

$ aat run clean --dry-run --days 0
aat: dry run — previewing cleanup in _output/runs (older than 0 days)...
  would delete: batch-20260910-225858-2e1dcfc5 (age: < 1 day)
  would delete: batch-20260910-225919-0754c0ea (age: < 1 day)
  would delete: run-20260910-225742-06f1a24d (age: < 1 day)
  ...
  would delete: run-20260910-230834-1a340655 (age: < 1 day)
  warning: run-garbage: cannot parse timestamp: unrecognized format: run-garbage
aat: would delete 16 run(s), skipped 2 saved, 1 errors

Known issue: a run- or batch- directory whose name carries no parseable timestamp, like run-garbage above, is reported as an error and left alone, but the command still exits 0. In scripts, check the summary line for errors.

Rebuilding Summaries (aat run rebuild-summaries)

The web UI builds its lists from each run's summary.json. After an AAT upgrade adds summary fields, older summaries lack them. Rebuild every summary from the full archives without re-running anything:

$ aat run rebuild-summaries
aat: rebuilding summaries in /path/to/shop/_output/runs...
aat: rebuilt 3 summaries

It rewrites summary.json for every top-level run directory with an archive.json and for every run inside a batch directory (auto-generated or named), using --output or the manifest's archives like the other commands. The next web UI listing picks the new summaries up. A run with no summary.json at all does not need this: the web UI computes and writes the summary the first time it lists the run.

Known issue: batch.json is not recomputed, so batch-level counts stay as they were written.