CLI reference
| Command | Purpose |
|---|---|
saffron run [paths] [--base-url u] [--api-url u | name=u] [--env name] [--filter expr] [--rerecord] [--headed] [--no-agent] [--plan [--json]] [--max-cost usd] [--max-agent-minutes m] [--max-session-minutes m] [--stability-proofs n] [--strict] [--no-verify] [--no-reuse] [--no-hooks] [--browser b[,b...]] [--device name] [--viewport WxH] [--workers n] [--agent-workers n] [--shard i/n] [--shard-by scenario|file] [--assertion-policy p] [--provider p] [--model m] [--storage-state f] [--trace mode] [--video mode] |
Run features: cached → replay, missing/failed → agent (unless --no-agent). A path with a line, features/login.saffron:12, runs only the scenario that line is in, Cucumber-style: its Scenario line, a tag above it or any line inside it; an Examples row runs that row alone, and :12:30 names two. A path with a name, features/login.saffron:Successful login, runs the scenario of that name, which stays true while the file is edited; :Roles (example 2) runs one Examples row, as saffron trace lists it. A line no scenario owns (the feature's header, a Background, a StepSet, a Rule's header) or a name no scenario has exits 2, saying what is there; so does a path that does not exist. A run of named scenarios is not a suite run: its results replace those scenarios' own in the last report (one imported from CI, interrupted, for another base URL or billed another way is replaced, and the run says so), every row kept from an earlier run shows the time that run started, the page's headline counts and cost are this run's own with the number of rows kept from earlier runs beside them, and its history line is marked partial, so the dashboard counts it for those scenarios while trends and the last run's figures count suite runs only. Two scenarios of one name in one file exit 2, as any two sharing a recording do. --provider claude|codex|antigravity|cursor picks whose agent records and heals for this run (AI providers and sign-in); the first line printed names it, the models asked for and the vendor package version (§4). Ctrl-C writes the report of what finished, marked interrupted, and exits 130 (§4); SIGTERM does the same and exits 143. --workers and --agent-workers take a whole number, 1 or more: 0 or a typo exits 2, as the config's workers and agentWorkers do. Exit 1 on red (and yellow with --strict), exit 2 when setup or teardown fails (§17). --no-hooks skips them. --no-verify skips proof replays; --no-reuse disables step seeding. --shard 2/4 runs the second quarter of the suite; --shard-by file keeps each feature file on one machine (§15). --browser chromium,firefox runs the suite in each browser into one report, --device "iPhone 15" and --viewport 1280x720 set the profile, --agent-workers 3 lets three agent sessions run at once (§15). --trace off|on|retain-on-failure overrides the trace setting for this run (§18), --video off|on|retain-on-failure the video setting. --api-url https://staging-api.test points the default API elsewhere for this run, --api-url orders=https://staging-orders.test a named one (§23); repeat it for several. --plan prints what the run would do instead, and stops: each scenario as replay, replay proposal or check again (no AI), record or record again (an agent session, with the reason), refused or unreadable (red, no AI), the agent sessions and proof replays that needs, and new steps several scenarios to record share, to record and accept first; with run history, it also estimates the browser time of the replays and of the recordings with their proofs, one after another, from each scenario's past times; --json gives it as data. --max-cost 5 and --max-agent-minutes 30 start no new agent session once this run's AI cost or time reaches the limit (maxCostUsd, maxAgentMinutes); a session already running finishes, and what is left is red, counted as blocked by the run's budget. --max-session-minutes 10 ends one agent session that has gone on that long (maxSessionMinutes): its tools refuse, and it ends unfinished, with the agent's account of where it got to. While an agent records, the run prints each step it records and each proof as it ends on stderr (· Checkout: recording step 41 of 92: ...), so a long recording can be followed as it goes. |
saffron trace [scenario] [--port n] [--no-open] [--json] [--run startedAt | --run <id> [--provider p] [--allow-fork] [--max-download MB]] |
Open the execution replay of a scenario from the last run (§18). Name it by scenario name, feature path or feature:scenario, and in a browser-matrix run feature:scenario@browser; with no name, or a name several scenarios share, it opens all of them in one replay, starting on a red one, else a yellow one. --port listens on a fixed port, --no-open only prints the address, --json prints the parsed trace instead of serving it. --run <startedAt> opens it only while the last run is that one (its startedAt, as status --json gives it): an editor's row names the run it lists. --run <id> with a CI run's id (a number, from saffron runs) replays that run's traces from .saffron/ci/ instead, downloading the run as report --run does when it is not there yet (--provider, --allow-fork as for import; --max-download as for report --run, the same too-large error and --json shape). That replay is view only: it has no Accept or Reject, because nothing was filed (§19). |
saffron accept [file[#revision] | --all] [--include-unverified] [--include-imported] [--propagate] [--with-feature-edit] [--step-set name] |
Promote proposals; propagate locator and request fixes; apply feature edits. No args: list pending. --step-set "Sign in" files only that StepSet's proved steps, for reuse, and leaves the proposal pending (§5). With #revision (from saffron status --json), refuses a proposal replaced since it was listed (§5). --all takes the verified proposals made here: it skips ones that are unverified or not checked (no proof replay ran) unless --include-unverified, and ones imported from a CI run (§19) unless --include-imported, and ones with a note held for review (§5: a behavior change, a requirement not kept, an insensitive check, a volatile locator, an unstable recording, a row not proved, a date not following the clock, data not followed, a check weakened, a write not finished, a check that changed the page), and an Outline proposal that fails a row, and says how many it skipped. A file you name is accepted as before, with a warning for one that fails a row. |
saffron reject [file[#revision] | --all] |
Discard proposals. #revision as for accept. |
saffron steps [search] [--json] [--snippets] |
List/search the step vocabulary (files + caches) with recorded/divergent/unrecorded badges and usage; --snippets writes .vscode/saffron.code-snippets for native VS Code completion. |
saffron mcp |
Serve the vocabulary, the project overview, the recording plan and the pinned API contract to AI assistants over stdio MCP (search_steps, list_step_sets, project_status, run_plan, api_contract, verify), e.g. claude mcp add saffron -- npx saffron mcp. |
saffron author <prose-file> [-o out] [--provider p] [--model m] |
Draft a .saffron feature file from plain-paragraph requirements, reusing the project's step vocabulary (AI; reports how many lines are seedable). |
saffron login [provider] |
Check that a provider (default: the configured one) can record and heal on this machine, and list what is missing. saffron login cursor also runs Cursor's one-time browser sign-in; saffron login antigravity also allows Saffron's tools in agy's settings. See AI providers and sign-in. |
saffron lsp |
Run the Saffron language server over stdio for JetBrains (LSP4IJ/Ultimate), Neovim, any LSP editor: badge completion, StepSet go-to-definition, hover, diagnostics. A client that adds --stdio is accepted: stdio is the only transport. |
saffron init [--agents list] [--examples] [--no-instructions] [--no-scaffold] [--no-mcp] |
Make the project agent-ready (--examples installs the Saucedemo example suite for a guided first run): install the bundled Saffron skill into agent skill directories, register saffron mcp in .mcp.json / .cursor/mcp.json / .vscode/mcp.json, add a managed block to AGENTS.md/CLAUDE.md, scaffold config. |
saffron report [--no-open] [--merge [paths...]] | --run <id> [--provider p] [--allow-fork] [--max-download MB] [--json | --panel] |
Open the latest HTML report (--no-open only prints its path). --merge combines the shard reports of one sharded run into latest.json, latest.html and one history line (§15). --run views a CI run's results without importing them (§19): downloads its bundles once into .saffron/ci/ (the newest 10 runs and 1 GB by default, ci.cacheRuns and ci.cacheMB), checks them as import does, judges each proposal against this checkout without filing it, and opens the run's report page. It never writes .saffron/reports/, .saffron/proposals/ or .saffron/artifacts/. A run with no bundle says why (running, expired or none) and exits 0. --max-download refuses a download over that many megabytes (exit 2, kind: "too-large" with sizeBytes in --json). --json prints the run, its totals, scenarios and proposals for the editors; --panel prints the run's results as one HTML page instead, the one the editors' CI results panel shows (a failure is {"error": ...}, as with --json). |
saffron prune [--yes] [--check] [--json] |
List recordings no scenario owns any more (a deleted scenario or feature file leaves its cache behind); --yes deletes them, and names any it could not delete (no permission) and exits 1; --check exits 1 without deleting, for CI. While a feature file fails to parse it deletes nothing and exits 2. --json always has orphanScan, as saffron status --json does: complete, the parse errors, and ownershipUnknown, the recordings the broken file may own: those in its folders, whatever they name inside, and any naming it or no feature (a broken step set hides every file, so all of them). Recordings of other files stay in orphans, in saffron status --json too. |
saffron diff [files...] [--json] [--html [file]] |
Show what a pending proposal changes in the committed recording (name a proposal file, or a feature file for its pending proposals; --json prints a list, one entry per proposal): the action list behind the narrative, and the feature file edits accept --with-feature-edit would apply (featureEdits in --json), each with the file and line it rewrites (file, line): for a StepSet's step, its definition, which every scenario using the set follows. An edit to a Background step is marked skipped, as accept leaves it to you. The run report shows the same file and line beside each suggested edit. A note about one step (a behavior change, a check that passes either way, a locator a later day will not find) is shown under that step (notes in --json). --html writes a review page instead (default .saffron/reports/review.html): each step beside its action today and proposed, its target, the values the proof captured and the notes about it, with the last run's screenshot of the page before the agent took over. |
saffron export [-o file] [--branch name [--base branch]] [--report-only] |
Pack the pending proposals, the last run's report and the screenshots and traces it names into one bundle, for review on another checkout (§19). Default .saffron/reports/saffron-bundle.zip (saffron-bundle-shard-<i>-of-<n>.zip on a shard). --report-only writes a summary instead, the run's report and screenshots with no proposals and no traces (default .saffron/reports/saffron-summary.zip), small enough to upload on every run so any run's results can be viewed; a run that left only summaries has nothing to import (§19). --branch also commits the proposals onto the commit the run tested as a new branch, pushes it, and on GitHub or Azure DevOps opens a draft pull request for the branch the run tested (--base picks another; §21). A branch name with a space, a quote or any of $ ` ; | & < > ( ) \ is refused, as the pull request gives it to a shell. Exit 2 when the export failed or no branch could be pushed, in one line; 1 only when the branch is pushed but the pull request could not be opened. |
saffron eject [files...] [-o dir] [--force] [--json] |
Write the committed recordings as Playwright Test specs you own from then on, never looser than replay (§22). Default folder playwright/; playwright.config.ts only when there is none. What has no faithful form is written as test.fixme with the reason. Exit 1 when nothing could be exported. |
saffron runs [--branch b] [--limit n] [--provider github|azure] [--json] |
The CI runs of the checkout's branch, through gh (GitHub Actions) or az (Azure Pipelines), with how many of each run's proposals are pending here (§19; in the IDEs, §20). A run a fork's pull request started is listed like any other; saffron import --run refuses it unless --allow-fork. Exit 2 when the CI tool is missing or signed out, saying what to do. With --json, a missing, signed-out or failing gh or az is {provider, branch, runs: [], problem: {kind, message}, error}, error holding the same message; any other failure is {"error": ...} alone. |
saffron import <bundles...> | --run <id> [--provider p] [--allow-fork] [--overwrite] [--allow-unknown-repository] [--json] |
Check bundles (files, folders holding saffron-bundle*.zip or saffron-summary*.zip, or http(s) URLs) against this checkout and file their proposals, report and evidence here (§19). --run downloads a CI run's bundles through gh or az instead, after asking the service who started the run: a run a fork's pull request started is refused unless --allow-fork, and a downloaded bundle that another run made is refused. Of bundles from several attempts of one run, the latest attempt's are taken, per shard, browser and base URL; two shards that filed different proposals for one scenario are a conflict, and neither proposal is imported. Exit 1 when a proposal was not imported, 2 when the bundle was refused and nothing was written, or the import failed (a full disk, say), in one line. --overwrite replaces a different proposal pending here for the same scenario. --allow-unknown-repository imports a bundle that does not say which repository it comes from (made outside a git checkout); its proposals are still checked by their bindings. With --json, every failure (an invalid config included) is {"error": ...} on stdout and exit 2. |
saffron dashboard [--stdout] [--no-open] |
The suite's quality dashboard (§6), in the run report's design, from .saffron/history.jsonl: tests, the share passing (each scenario's latest result), failing scenarios and for how many runs in a row, flaky ones (passed and failed by turns over the last 20 runs on one base URL), ones the agent healed again and again, never run and left out of the last run, the slowest, a grid of each of those scenarios' result per run, and results per tag. Writes .saffron/reports/dashboard.html and opens it; --stdout prints the page instead (what the IDE panels show). |
saffron coverage [--json] [--write] [--base ref [--markdown] [--fail-on-loss]] |
The coverage map: requirement IDs (tags matching requirementTag) to scenarios, their recorded checks and the last run's verdict. --write saves it as .saffron/coverage.json; --base origin/main compares with that commit's map and lists scenarios added and removed, requirements no longer covered and checks lost (§9). |
saffron status [--json] |
Project overview: feature files and scenarios with cache state, tags, pending proposals, last run, history, vocabulary health, effective config. --json is what the IDE panels and dashboards read; it adds quality, the numbers saffron dashboard shows (§6), importedFrom on a pending proposal imported from a CI run (§19), and featureEdits on one that suggests feature edits, as saffron diff --json lists them (each with its file and line), which both IDE panels show in a proposal's details. A last run Ctrl-C stopped says so, and its lastRun has interrupted and notRun (§4). |
saffron api pin <source> [--out file] [--service name] |
Pin the API contract (§23): fetch an OpenAPI 3 document from a Swagger UI page (the document it loads is pinned), a document URL or a file, and write it as JSON to api/openapi.json (--out names another file, inside the project; --service orders pins a named API's contract, as api/orders.json by default). Prints the api.contract line to add to saffron.config.json. Exit 2 when the source is not an OpenAPI 3 document (a Swagger 2.0 or YAML document says so). |
Global: -p, --project-root <dir> |
Project root (default: cwd). |
An expected failure in any command (an invalid saffron.config.json, a folder where a file was expected, no permission) is one line, saffron: <message>, and exit 2. Set SAFFRON_DEBUG=1 to see the stack instead.