English · 日本語
CLI reference¶
Implementation:
bajutsu/cli/(Typer; one file per command undercli/commands/). The entry point isbajutsu = "bajutsu.cli:app"inpyproject.toml. Every command in this CLI (command-line interface) selects one app with--target <name>and points at config with--config(defaultbajutsu.config.yaml). App-specific differences live in config (configuration).
Related: run-loop · recording · codegen · configuration
Common¶
- Every command loads
.envfirst (_bootstrap, below). - Missing config / undefined app / no actuator → prints a message and exits with code 2.
--backendis a comma-separated list (e.g.idb). Empty uses config'sbackend. It checks availability in order and the first usable one is the actuator (drivers).
run¶
Runs a scenario deterministically; pass/fail is machine-only. The only AI component is the alert guard (on by default per scenario), which fires only to clear an OS prompt that blocked a step — see dismissAlerts.
bajutsu run --target <name> [--scenario <file.yaml>] [options]
By default run loads every *.yaml in the app's configured scenarios dir
(targets.<name>.scenarios, see configuration) — so the config alone is enough
to run. Pass --scenario <file> to run a single file instead.
| Option | Default | Description |
|---|---|---|
--target |
(required) | the target app (config's targets.<name>) |
--scenario |
config's scenarios dir |
run one *.yaml instead of the app's whole scenarios dir |
--backend |
config | actuator order (comma-separated; first usable wins) |
--tag |
"" | comma list; run only scenarios carrying any of these tags |
--exclude |
"" | comma list; skip scenarios carrying any of these tags |
--udid |
booted |
the target Simulator (comma list = a device pool for --workers) |
--erase / --no-erase |
scenario › config › off | override every scenario's preconditions.erase (wipe the simulator first); omit and it resolves each scenario's value, then the target's erase config, then off (BE-0177). The app is reinstalled fresh either way (config appPath + preconditions.reinstall) |
--dismiss-alerts / --no-dismiss-alerts |
scenario › config › on | override every scenario's dismissAlerts — the vision guard that dismisses system alerts idb cannot see; omit and it resolves each scenario's value, then the target's dismissAlerts config, then on (uses the configured AI provider — ANTHROPIC_API_KEY, or AWS credentials for Bedrock; recording) |
--alert-instruction |
"" | default button instruction, below a scenario's own dismissAlerts.instruction and above the target's dismissAlerts config |
--log-predicate |
"" | an NSPredicate narrowing the deviceLog stream (e.g. subsystem) |
--log-subsystem |
"" | the os_log subsystem for appTrace (defaults to the app's bundleId) |
--network / --no-network |
config › on | collect the app's network exchanges for request assertions; omit and it resolves the target's network config, then on (BE-0177). iOS needs BajutsuKit in the app; web observes natively via Playwright, and stubs scenario mocks in-process |
--workers |
1 | parallel scenarios over a device pool. On iOS, needs --udid u1,u2,… and is capped to that pool size. On web, --workers N alone is N parallel browser-context lanes — no --udid needed (BE-0054). Each lane carries its own network collector, interval recordings, and (iOS) device control, so network / video / setLocation / push work the same as a single-device run |
--baselines |
baselines/ beside the scenario |
directory of baseline images for visual assertions; baseline: home.png resolves inside it |
--schemas |
schemas/ beside the scenario |
directory of JSON Schema files for responseSchema assertions; schema: items.json resolves inside it (needs the schema extra) |
--headed / --no-headed |
app headless (headless) |
web backend: show the run in a visible, slow-motion Chromium window instead of headless, so you can watch each step (the window opens on the machine running the command). Omit to use the app's headless config; iOS ignores it |
--progress / --no-progress |
off | stream per-scenario / per-step progress lines to stderr (the serve UI consumes these) |
--zip |
off | after the run, also write runs/<id>.zip — one portable artifact (report + evidence) for CI upload or sharing. Runs after the verdict, so it can't affect pass/fail; see export |
--runs-dir |
runs |
directory to write the run tree into. Lets a caller run from one working directory but persist the run elsewhere — serve uses it when the active config is bound from a different tree (a Git checkout or an uploaded bundle) to run from that tree while keeping the run in serve's store (BE-0073) |
--evidence-store |
"" (also BAJUTSU_EVIDENCE_STORE) |
after the run, upload the whole run tree to object storage at this URI — s3://bucket/prefix (AWS / R2 / MinIO) or gs://bucket/prefix (Google Cloud Storage). The remote layout mirrors the local one under the prefix (<prefix><runId>/…), so the upload path selects the cloud lifecycle policy (retain main-branch evidence, expire feature-branch evidence). Runs after the verdict, so an upload failure is reported as a warning and can't affect pass/fail. Needs the s3 or gcs extra (BE-0110) |
--config |
bajutsu.config.yaml |
the config file |
--project |
"" | run a project registered with project add by name — resolves its stored config source back to a --config spec and runs it. The headless trigger a CI/cron step calls, the CLI mirror of POST /api/projects/<name>/run (BE-0225). Mutually exclusive with --config |
- Evidence is written to
FileSink(runs/<runId>, udid=..., log_predicate=...)(evidence). runIdisYYYYMMDD-HHMMSS.- Output:
PASS|FAIL runs/<runId>/manifest.json. Exits 0 if every scenario passes, 1 on failure. - When the alert guard fires (it is the run's only AI), an
AI usage:line with the token totals it consumed is printed to stderr after the result, leaving stdout the single machine-readable result line. A run that used no AI prints nothing.
bajutsu run --target showcase-swiftui --udid <UDID> --backend idb --no-erase # the app's whole scenarios dir
bajutsu run --scenario demos/showcase/scenarios/smoke.yaml --target showcase-swiftui --no-erase # one file
project¶
The config project hub as a headless CLI — the same store serve exposes over HTTP, so a
project registered here is visible in the web hub and vice versa (BE-0225).
A project is a named binding to a config source; run --project <name> (above) resolves and runs
it. The store is the DB when BAJUTSU_DATABASE_URL is set, else the on-disk JSON beside the runs
directory (--runs, default runs). Every command resolves to the single default org locally.
bajutsu project add <name> --config <source> # register (or rebind) a project; first one becomes active
bajutsu project ls # list projects; the active one is marked with a leading '*'
bajutsu project use <name> # make a project the active binding
bajutsu project rm <name> # deregister a project (its runs are retained on disk)
--config accepts a local path or a Git spec (github:owner/repo@ref:path), the same forms
run --config takes (BE-0063).
The cadence lives in the CI/cron system that calls run --project, not in Bajutsu.
doctor¶
A runnability gate + the convention score for the current screen (AI-independent; configuration).
bajutsu doctor --target <name> [--udid booted] [--backend ...] [--config ...]
- First the env gate (
preflight.py): the required CLIs for the actuator (xcrun;idb/idb_companionfor idb) and a booted Simulator, printed as a ✓/✗ checklist. A missing check exits 1 (fail fast with a fixable hint). - Then
query()s via the actuator and rendersscore(elements, idNamespaces). Exits 1 when the grade is Blocked, 0 otherwise.
audit¶
A static determinism score for a scenario — the device-free cousin of doctor's convention
score (AI-independent; selectors; BE-0049).
It reads a scenario file (expanding components / data, like trace --explain) and reports, per
scenario, how reproducible it is — without running it.
bajutsu audit <scenario.yaml> [--json]
- Grades each selector on the stability ladder (selectors): a unique
id/idMatchesis stable;label/labelMatches/traits/valueis moderate (auxiliary, no id); anindex(nth-of-many) is fragile. Plus it flags coordinate gestures (swipe {from,to}, which a stable id could replace) and over-loose waits (until: screenChanged/settled, which wait for no concrete condition). - Emits a per-scenario
grade(Stable/Moderate/Fragile), a stability fraction, and the located findings — as text, or--jsonfor tooling. - Advisory and read-only: it never runs the scenario, never edits it, and never gates CI — a successful audit exits 0 even with findings (only a missing / unreadable scenario file exits 2). A finding is something to harden, not a verdict — the opposite of retry-to-pass, which hides flakiness.
Two further modes prove determinism by observation rather than static grading:
bajutsu audit <scenario.yaml> --repeat K --target <name> # run K times, diff the outcomes
bajutsu audit --history <runs-dir> # mine past runs for flakiness
--repeat Kruns the scenarioKtimes under identical preconditions and reports anything whose outcome varied (deterministicvsflaky) — a divergence is a finding to fix, never a retry that turns red into green.--history <runs-dir>is the longitudinal view: it groups each scenario's accumulated runs by the run's scenario fingerprint (theprovenance.scenarioHasheachmanifest.jsoncarries) and classifies each scenario (flaky/deterministic/unproven). A verdict that flipped while the fingerprint stayed constant is true flakiness — not an edited scenario, since editing changes the hash and starts a fresh group. Keying by scenario name as well as fingerprint pins which scenario in a suite flaked. Runs with no fingerprint (pre-provenance) can't be grouped and are reported as skipped. Like the other modes it is read-only and exits 0 even when it finds flakiness (only a missing runs dir exits 2).
coverage¶
A static e2e coverage map for a suite — the read-only cousin of doctor's convention score
(AI-independent; BE-0050).
Where doctor grades the ids one screen exposes, this grades the ids a whole suite exercises:
it walks every *.yaml in the app's configured scenarios dir (expanding components / data), groups
the stable ids they reference by namespace, and measures them against the app's declared
idNamespaces (configuration) — without running anything.
bajutsu coverage --target <name> [--config ...] [--runs <dir>] [--crawl <screenmap>] [--json] [--html <path>]
- Reports the coverage fraction (declared namespaces the suite references / declared namespaces),
the per-namespace ids that touch each, the gap list (declared namespaces no scenario
references — what is untested), and off-namespace ids (referenced ids whose namespace was never
declared). As text, or
--jsonfor tooling. - A referenced id is any
id/idMatchesa scenario addresses — across steps, nested control flow,withinscopes, and assertions. --runs <dir>folds in two run-evidence dimensions:- Endpoint coverage: it reads every
network.jsonunder the runs dir (the union of observed exchanges) and measures how many observed endpoints (METHOD path) the suite's network assertions (request/event/requestSequence) cover. It reports the fraction asserted, the unasserted observed endpoints (traffic the suite never asserts on), and matchers declared but not observed in any run. - Observed-id coverage: it reads every per-step
elements.jsonunder the runs dir and collects the stable ids the runs actually rendered (each element'sidentifier; null and empty ids are dropped), grouping them by the declared namespaces — the run-evidence counterpart to the static id map. It reports the per-namespace observed ids, the namespaces observed in no run, and observed ids whose namespace was never declared (off-namespace).
Omit --runs for the static id-namespace map only.
- --crawl <screenmap> (with --runs) folds in a screens-visited dimension against an
autonomous crawl's discovered surface (BE-0038):
the denominator is the screens the crawl found (screenmap.json nodes; pass the file or its
run dir), the numerator is the screens the run set reached — each per-step elements.json is
fingerprinted with the same crawl.fingerprint, so a visited screen matches a discovered one.
It reports the fraction reached and the unvisited screens the crawl discovered but no run
touched. Needs --runs for the visited evidence; given --crawl without it, the dimension is
skipped with a warning.
- --html <path> also writes a self-contained HTML report of the same figures (inline CSS,
no JavaScript, no external asset — it opens straight from disk), with a coverage bar per dimension
and the gap / off-namespace / unvisited lists called out. The endpoint, observed-id, and
screens-visited sections render only when --runs (and, for screens, --crawl) supply them. The
text (or --json) output is unchanged; the path is confirmed on stderr.
- Advisory and read-only: it never runs a scenario, never edits anything, and never gates CI —
it exits 0 even with gaps (only a missing config / scenarios dir or an unreadable scenario exits
2). A gap is a namespace to cover, not a verdict.
stats¶
An aggregate run-stats dashboard — the read-only trend across many runs, complementing the
per-run report.html (AI-independent;
BE-0102). Where coverage
answers "what surface do we test?" and audit answers "is a scenario reproducible?", stats
answers "how is the whole suite doing over time?" It aggregates the manifest.json of every run
under a directory — no device, no AI, no verdict.
bajutsu stats --runs <dir> [--json] [--html <path>]
--runs <dir>is the directory of past runs to aggregate; it reads each run'smanifest.json(unreadable / malformed ones are skipped). A missing directory exits 2.- Reports, as text or
--jsonfor tooling: - Pass-rate over time — overall and per day (the day is parsed from the run id's timestamp
prefix; a custom-labelled run id has no day and buckets under
(no date)). - Duration / performance — total wall-clock across the set (each run's duration is summed from
its scenarios'
duration_s) and the slowest scenarios by average duration. - Failure hotspots — the scenarios, steps (
scenario > action), and assertion kinds that fail most, each with its most frequent failure reason. - Flakiness — each scenario's classification (
flaky/deterministic/unproven) reused from the BE-0049 longitudinal audit. - Volume — run count per backend, the denominator the rates are read against.
- Scenario series are keyed by the BE-0049
(scenarioHash, name)identity, so a verdict that flips at a constant fingerprint is true flakiness while an edited scenario starts a fresh series. A run without aprovenance.scenarioHashstill counts toward the run-level trend but can't join a scenario series (reported as skipped). --html <path>also writes a self-contained HTML dashboard of the same figures (inline CSS, a minimal inline-SVG trend line, no JavaScript, no external asset — it opens straight from disk). The text (or--json) output is unchanged; the path is confirmed on stderr.- Advisory and read-only: it never re-runs a scenario, never recomputes a verdict, and never gates CI — an older manifest missing a newer field renders as "not captured", never as a failure. Any threshold a team sets on its numbers is their own informational check.
flakiness¶
A ranked flaky-scenario surface over the run history — the actionable counterpart to audit
--history (AI-independent;
BE-0220).
Where audit --history labels each scenario flaky / deterministic / unproven, flakiness
sorts the whole suite flaky-first and scores how badly each one flips, so a team sees its worst
offenders at a glance — no device, no AI, no verdict.
bajutsu flakiness --history <runs-dir> [--json] [--window N] # mine a directory of run manifests
bajutsu flakiness [--org <org>] [--json] [--window N] # read the serve database (BAJUTSU_DATABASE_URL)
- Two sources, one ranking.
--history <runs-dir>mines a directory of past runs'manifest.json(the CI / scripting form); omitting it reads the serve database (BAJUTSU_DATABASE_URL), grouping straight from theprovenance.scenarioHashstamped on each run row. A missing runs directory or an unconfigured database exits 2. - Runs are grouped by
provenance.scenarioHashand each scenario is scored by its verdict flip rate —2·min(passed, failed)/runs(0 when consistent, 1 at a 50/50 split) — reusing the BE-0049 classification. The output is ordered flaky-first, then by descending flip rate, then by run count. - Each entry carries the newest passing and newest failing run ids, so
--jsonconsumers (and the serve panel) can link straight to the representative evidence on both sides. --window Nkeeps only each scenario's newestNruns before scoring (0, the default, uses the whole history);--org <org>picks the tenant whose runs the database read mines.- A run with no
provenance.scenarioHash(a pre-provenance run) or no recorded verdict can't be grouped and is reported as skipped, exactly as inaudit --history. - Advisory and read-only: it only reports flakiness already recorded in history — it re-runs nothing, recomputes no verdict, and never gates CI.
export¶
Bundles a finished run into a single portable .zip — report.html together with manifest.json,
junit.xml, the executed scenario.yaml, and all of its evidence (screenshots, video,
network.json, …) (BE-0060).
The whole runs/<id>/ tree is rooted under a single <id>/ folder, so report.html's relative
links resolve offline — the report works by double-click, no server.
bajutsu export <run-id | run-dir> [-o out.zip] [--force]
<run>is a run id (resolved under--runs, defaultruns/) or a path to a run directory.- Output defaults to
<id>.zipbeside the run dir;-o/--outputoverrides. It refuses to overwrite an existing file without--force(mirroring howrecordnever silently overwrites). - Pure packaging of what the run already wrote: no device, no AI, no effect on any verdict. It
archives strictly the run dir (never
.env, config, or anything above it) and inherits the run's secret-scrubbing.bajutsu run --zipis the same archiver, run inline after the verdict;bajutsu serveoffers it as a Download button beside the embedded report.
trace¶
Inspects a finished run as a text timeline: per scenario, steps and observed network
exchanges interleaved chronologically, followed by expectations, app-trace intervals, and an evidence
summary. Read-only (reads the saved manifest.json / network.json / appTrace.json, and
scenario.yaml for provenance).
- Each step shows the natural-language phrase it was recorded from — its
from:provenance (BE-0044) — inline as← "<phrase>"; a run of steps sharing one phrase is labeled once. A hand-authored scenario or an older run without provenance simply shows none.
bajutsu trace [<run-dir>] [--scenario <substr>] [--runs runs]
bajutsu trace --explain <scenario.yaml> # pre-run dry run (no device)
- With no
<run-dir>, uses the latest run underruns/.--scenariofilters by name substring. - Exits 2 if no run is found.
--explain <scenario.yaml>is the pre-run counterpart: instead of reading a finished run, it previews how the scenario'scapturePolicywould fire (BE-0028). Action-triggered rules are counted exactly and their matching steps listed;event/resultrules are reported as runtime-conditional; and a heavy capture (video/deviceLog/appTrace/network) on a broadly-matching rule is flagged with ⚠ so you can tighten the match before a run pays for it. Read-only and deterministic — no device, no LLM. (Components and data rows are expanded; configsetuppreludes are not included.) Exits 2 if the scenario file is missing.
report¶
Re-renders a finished run's report.html (and re-emits junit.xml) from its stored data, with
the current template — no device, no LLM, no re-run
(BE-0068). So a
template improvement or a rendering-bug fix reaches past runs without re-executing them; the
verdict is read from the stored model, never recomputed.
bajutsu report <run-id | run-dir> # re-render one run
bajutsu report --all [--runs runs] # re-render every run dir (with a manifest.json) under runs/
- The render model is
manifest.json(a versioned, lossless render input —schemaVersion) plus the executedscenario.yaml; the renderer reads only the run dir. An older run renders without error, with any newer-only section shown as "not captured" rather than invented. - It re-presents recorded outcomes — it never re-evaluates an assertion or alters a verdict — so it
sits inside the determinism contract.
serveuses the same renderer on view: it rendersreport.htmlfresh from each run's stored model on every request (falling back to the baked file if the model can't be loaded), so upgradingserverefreshes every report with no re-bake. - Exits 2 if the run (or, with
--all, the runs root) has no readablemanifest.json.
triage¶
Diagnoses the first failed scenario in a run and suggests a minimal fix. This is advisory — it
never judges pass/fail (the AI boundary). It assembles failure context (the failing step and its
reason, failed expectations, the element tree nearest the failure, the scenario) and runs a
TriageAgent. The default is rule-based (HeuristicTriageAgent, no API key): it categorizes the
failure (selector / timing / assertion) and, when a target id is absent but a similar id is on
screen, suggests "did you mean …?" (the classic renamed-id self-heal). --ai swaps in a
Claude-backed agent (needs ANTHROPIC_API_KEY) that reasons over the same context plus the
failure screenshot for richer diagnoses.
An agent may also return a structured fix the tool can apply: renameId, addIndex
(disambiguate an ambiguous match), or raiseTimeout. --apply <scenario-file> prints it as a
dry-run diff; --write applies it to the source; --rerun --target <name> then re-runs the
patched scenario (--no-erase) and reports whether it now passes. The boundary holds: a fix
is applied only when the user opts in after reviewing the diff, and a fragment that no longer matches the
source is a safe no-op.
bajutsu triage [<run-dir>] [--scenario <substr>] [--runs runs] [--ai]
bajutsu triage [<run-dir>] --ai --apply <scenario-file> [--write] \
[--rerun --target <name> [--backend idb] [--udid <udid>]]
- Defaults to the latest run under
runs/. Exits 0 when the run has no failed scenario. --rerunrequires--write(nothing to verify otherwise) and--target.- With
--ai, anAI usage:line of the tokens the diagnosis consumed is printed to stderr after the diagnosis. The rule-based default uses no AI, so it prints nothing. --json <path>also writes the diagnosis as JSON (category, summary, suggestions, and the structured fix); combined with--apply, it includes the fix's unified diff and patched text.--jsonitself only writes that JSON file and never touches the scenario source — patching the source still requires--write(as it always has). TheserveWeb UI uses--jsonto surface triage in the browser (BE-0147).
Cross-run flaky triage (--flaky)¶
Where the default triage explains one failed run, --flaky explains a pattern: why one
scenario intermittently passes and fails while its content fingerprint is unchanged
(BE-0220).
It is the fix-proposal counterpart to flakiness: flakiness ranks which
scenarios flip, and --flaky diagnoses why one of them does and proposes a fix. It reads the
evidence of both the passing and the failing runs of the scenario and reasons about the delta
between them (a selector that resolved in one run but was ambiguous in another, a wait that beat a
spinner most of the time, a network response that varied).
bajutsu triage --flaky --scenario <name> --history <runs-dir> --ai [--apply <file> [--write]] [--json <path>]
- Requires
--ai. Cross-run diagnosis has no rule-based agent — the delta reasoning is exactly the judgement-free investigator work the LLM does — so--flakywithout--aiexits 2. It stays advisory: the runs already decided pass/fail; this only explains and proposes, never on the verdict path (prime directive 1). --scenario <name>picks the flaky scenario (matched as a substring of the run's scenario name, resolved to one exact name so two scenarios sharing a substring never mix), and--history <runs-dir>is a directory of past runs (each with amanifest.json) to contrast — the same directory shapebajutsu flakiness --historyandaudit --historyread. Both are required; a missing one exits 2.- The report shows the passing / failing run counts, the fingerprint, the intermittency category
(
selector-ambiguity/timing/network-variance/state-leak/unknown), and the proposed fix as a reviewable diff against the current scenario — never applied on its own. - Reuses the same structured fixes and flags as single-run triage:
--apply/--writepatch the scenario,--jsonwrites the machine-readable result, and any edit that would weaken an assertion is flagged as laxer (BE-0023) inline in both surfaces. A full YAML rewrite is deferred to a follow-up; the current fixes are the targeted structured edits. - Exits 0 when the scenario's history has no failing run or no passing run to contrast (nothing to diagnose as flaky) — advisory, like the other read-only modes.
record¶
Explores toward a goal with AI and writes the recorded scenario (Tier 1; recording).
By default it auto-names a *.yaml under the app's configured scenarios dir
(targets.<name>.scenarios); pass --out to write a specific path instead.
bajutsu record --target <name> --goal "<natural-language goal>" [--out <file.yaml>] [options]
| Option | Default | Description |
|---|---|---|
--target |
(required) | the target app |
--goal |
(required) | the goal to author (natural language) |
--out |
auto-named in config's scenarios dir |
explicit output path (overrides the app's scenarios dir) |
--name |
(from the goal) | file name for the auto-named scenario (ignored when --out is given) |
--udid |
booted |
the target Simulator |
--backend |
config | actuator order |
--erase / --no-erase |
--erase |
erase before launch (the app must be installed) |
--dismiss-alerts |
off | clear prompts during authoring (needs an API key) |
--headed / --no-headed |
app headless |
web backend: author against a visible (headed, slow-motion) browser instead of headless; omit to use the app's headless config |
--alert-instruction |
"" | the press instruction for the above |
--language |
config ai.language (auto) |
AI output language for the authored prose (from: provenance, reasoning) — ja / en / auto; overrides ai.language, auto follows the goal (BE-0188) |
--config |
bajutsu.config.yaml |
config |
- Internally
launch_driver→record_loop(driver, goal, ClaudeAgent(), ...)→dump_scenarios. - Output:
recorded <N> steps -> <path>. NeedsANTHROPIC_API_KEY(ClaudeAgent). - A Git
--configis read-only input (BE-0063):recordreads the config from the fetched checkout, but the authored scenario goes local — with no--outit auto-names under the current directory (not the checkout'sscenariosdir, which is the read-only SHA-keyed cache), and an--outinside the checkout is refused. Review the file and commit it to the repository through normal git. - An
AI usage:line with the tokens the authoring (and any alert-guard) AI consumed follows on stderr.
crawl¶
Explores the app breadth-first and writes a screen map of the reachable screens and the
transitions between them (Tier 1; BE-0038). Unlike record, which is
goal-directed — AI explores toward one natural-language goal and writes one scenario — crawl
is systematic discovery: it visits the screens it can reach and reports what it found. The
exploration engine is deterministic (a screen's identity and the order candidate actions are
tried are pure functions of the element tree); no AI is involved, and it is never a
pass/fail gate.
bajutsu crawl --target <name> [--max-screens N] [--max-steps N] [--out <dir>] [options]
| Option | Default | Description |
|---|---|---|
--target |
(required) | the target app |
--max-screens |
50 |
stop after discovering this many distinct screens |
--max-steps |
200 |
stop after taking this many actions |
--udid |
booted |
the target Simulator — or a comma list (A,B,C) for a parallel pool (see --workers) |
--workers |
1 |
crawl with this many workers at once, sharing one screen map: across this many simulators on iOS (BE-0064, capped to the --udid devices) or this many browser processes on web (BE-0077). 1 = single-worker crawl |
--backend |
config | actuator order |
--erase / --no-erase |
--erase |
erase before launch (the app must be installed) |
--dismiss-alerts / --no-dismiss-alerts |
--dismiss-alerts |
dismiss unexpected OS prompts while crawling (so they aren't read as crashes; uses the configured AI provider — ANTHROPIC_API_KEY, or AWS credentials for Bedrock) |
--headed / --no-headed |
app headless |
web backend: crawl a visible (headed, slow-motion) browser instead of headless; omit to use the app's headless config |
--language |
config ai.language (auto) |
AI output language for the guide's streamed reasoning — ja / en / auto; overrides ai.language, auto stays English for crawl (BE-0188) |
--out |
runs/<timestamp> |
run dir the screen map is written into |
--continue |
off | continue a past run (point --out at it) — re-explore every screen it left with untried operations, not one branch; raise --max-screens/--max-steps to go deeper, and --workers/--udid run the continuation in parallel (BE-0181) |
--config |
bajutsu.config.yaml |
config |
- A Git
--configis read-only input (BE-0063):crawlreads the config from the fetched checkout, but the screen map / screenshots go to the local--outrun dir (defaultruns/<timestamp>), never into the read-only SHA-keyed cache; an--outinside the checkout is refused. - Traversal is by deterministic replay, not in-place backtracking: to revisit a known screen
the crawl relaunches the app to a clean start and replays the shortest recorded path to it,
then takes the next untried action — the same way
runreaches any state. - Continue a past crawl (BE-0181):
a crawl stops on
--max-screens/--max-stepsfar more often than oncompleted, sobajutsu crawl --out <existing run> --continuepicks the run's map back up and re-explores every screen it left with untried operations — the whole remaining frontier — instead of re-crawling from the entry screen with a bigger budget. It needs no extra saved state: each such screen's recorded path (paths) is replayed and its still-untried operations re-derived by the deterministic guide (candidate_actions) and matched against the savedplan. Because the reconstruction is deterministic, it reconstructs only the deterministically derivable operations; AI-only operations are intentionally not reconstructed — for an AI-guided run, the richer operations the model proposed (a realisticfill/type, a vision-locatedtap_point) are not re-seeded, only the deterministic taps/inputs are. A frontier that reconstructs to nothing is left untouched (the saved map keeps itsplanand stop reason) rather than reported as completed. Raise--max-screens/--max-stepsto go deeper, and--workers/--udidrun the continuation across the same parallel pool a fresh crawl uses. This is distinct from the single-branch resume the Web UI's pruned-branch "tap to resume" fires (which re-explores one named pruned operation);--continueand that resume are mutually exclusive. - Disabled controls (
notEnabled) are reported per screen asblockedrather than tapped. To enumerate transitions the crawl explores the combinations of control states: it tries each empty text field (and toggles each switch, and switches each tab of a tab bar) independently, and — when several fields are empty — also a compound fill of them at once. The compound matters because a control can stay disabled until several fields are valid, and an intermediate single fill is often invisible (a masked password exposes no value), so filling one at a time can't reach the all-filled state. Crawl is AI-driven: it first inspects the screen deterministically, then hands those operations to Claude to reason about and combine, proposing realistic inputs (a valid email, a password meeting the rules, all of a form's fields in one fill) to enable controls whose precondition isn't obvious, plus operations on id-less elements, narrating its reasoning into the run log. The AI also handles a tab bar whose individual tabs the accessibility tree can't address — idb surfaces a SwiftUI TabView as a single "Tab Bar" group with no per-tab identifiers, so the bar is visible but its tabs can't be tapped by selector. When that bar is present (and no tab is already addressable by id), it locates the tabs by vision — the same fallback the alert guard uses — and taps each by coordinate, still switching tabs before drilling in. (UIKit tab bars, whose tabs idb exposes as individual elements, are a planned refinement — for now they fall back to the same vision path.) The AI only chooses what to try — screen identity, transitions, and crashes stay deterministic, so the crawl is never a verdict (it never gates CI). - Output:
<out>/screenmap.json, a JSON graph ofnodes(screens — fingerprint, kind, ids, candidate actions, plusblockeddisabled controls),edges(transitions),crashes(action paths that collapsed the app UI),alerts(OS prompts the guard dismissed mid-crawl — the triggering path + the button tapped),plan(the live frontier: still-untried operations per screen, refreshed as the crawl advances so a reader can see what it will try next), andstop_reason(completed/max_screens/max_steps), plus<out>/screens/<fingerprint>.png— a screenshot captured for each discovered screen (while the crawl is on it). The map is rewritten as the crawl advances, so a reader (the Crawl tab inserve) can draw it live: each screen is a label+info node, and screens that are the same UI in different states (a form empty vs filled) collapse into one node you can expand in place. Stops at the first of--max-screens/--max-steps. - On completion it also writes
<out>/screenmap.html— a self-contained report (inline CSS, no JavaScript, no external asset) and the offline counterpart to the live Crawl tab: the screens laid out in BFS depth columns with their screenshots, the transitions drawn as a static inline SVG (amber, with a 🛡️ marker, where a step tapped through an OS alert), and the crash / dismissed-alert paths listed below. It sits besidescreenmap.jsonandscreens/, so it opens straight from the run dir — share or archive it without the web UI. Read-only and model-free, like the JSON. - On completion it also writes one
<out>/crashes/crash-NNN.yamlper faithfully reproducible crash — a repro scenario built from the crash's recorded action path, directly runnable byrun, so a discovered crash becomes a committed Tier 2 regression after human review. The conversion is pure, deterministic, and model-free (tap/type/fillmap to their steps). A path that taps a normalized coordinate (a vision-located control) has no selector to address, so it emits no scenario rather than a lossy one. - Parallel pool (BE-0064):
with
--workers Nover a--udid A,B,Cpool the crawl runs across N booted simulators at once, all sharing the one screen map and frontier — independent branches explore concurrently and the AI guide's round-trips overlap across devices, so wall-clock time falls roughly with the device count. Only scheduling becomes concurrent: which worker reaches a screen first is timing-dependent, so for an app with its own non-determinism the recorded paths and discovery order can vary run to run, but screen identity, transitions, and crashes stay the same deterministic functions of the element tree (the crawl is still never a verdict). A wedged device drops its work and the others carry on. Default--workers 1is the single-worker crawl, unchanged. On web the same model runs N browser processes instead of simulators (BE-0077): the worker count alone sizes the lane set (the web has no devices), each reset is a fresh browser context (the clean-stateerase), and a browser that wedges is torn down and relaunched so one bad browser can't sink the crawl.
How a screen is identified (the fingerprint)¶
Every screen is reduced to a fingerprint — a short, stable identity that lets the crawl tell a
revisit from a genuinely new screen. This reduction is a pure, deterministic function of the element tree (no
AI, no screenshot pixels), which is what keeps the screen map reproducible: the same screen always
hashes to the same value. Each node records both the fingerprint and its kind.
- Identifier fingerprint (
kind: "id"— the normal case). The sorted set of accessibility identifiers present on the screen, hashed. Keying on which identifiers are present — not their on-screen text — makes the identity stable across locales and data changes: a list showing different rows, or a label in another language, is still the same screen. Each identifier is then tagged with its interactive state, but only when that state departs from the default, so the same layout in a different state hashes differently and the crawl explores the combinations: !— a disabled control (notEnabled),=— a filled text input (a field that now holds a value),+— a selected / toggled control (a switch on, a tab selected).
An enabled, empty, unselected screen contributes just the bare identifiers, so a screen with no
such state hashes exactly as its plain identifier set would. This is why filling a form or
flipping a switch yields a new node: it is a distinct, separately-explorable state, which is how
the crawl discovers an action that only becomes available once a precondition is met (e.g. a
submit button enabled after every field is filled).
- Structural fingerprint (kind: "structural" — the fallback). When a screen carries fewer than
two accessibility identifiers, the id set is too thin to identify it reliably, so the crawl hashes
the actionable elements' traits bucketed by coarse on-screen position instead. This is less
stable — layout jitter can shift it, and unrelated screens with a similar control skeleton can
collide — so it is flagged as structural to signal the identity is approximate. Raising
accessibility-identifier coverage (see doctor) moves a screen back onto the stable id scheme.
- Display grouping is a separate, view-only step. The fingerprint above is the exact, per-state
identity stored in the map. The Crawl tab additionally groups nodes that are the same screen
in a different transient state — an empty vs filled form, a toggle, or an alert/overlay that adds a
few elements — into one expandable unit, so the graph stays readable. That grouping never changes
the fingerprints or what the crawl explores; it only collapses the drawing.
Web backend (--backend web)¶
The crawl runs against a web app the same way it runs against the Simulator — the exploration
engine is platform-neutral, so bajutsu crawl --target <web-app> --backend web produces the same
screenmap.json, screenshots, and crash list. The web app is identified by baseUrl (not
bundleId), and because a browser needs no Mac or emulator a web crawl runs on the Linux
make check / CI gate. Three things differ from iOS, and all stay deterministic
(BE-0066):
- Clean start = re-navigate. There is no app process to relaunch; returning to a clean state
is
page.goto(baseUrl)against a fresh browser context (theeraseequivalent, ~free), the same lifecycle seamrunuses. - Crash detection. The iOS signal (the accessibility tree collapsing) does not exist on the
web, so the crawl uses the browser's own deterministic signals instead: an uncaught JS exception
(
pageerror), a 4xx/5xx main-frame navigation, or a blank document. Each is a machine fact (an event, a status number, an empty element set) — no model judges whether the page "looks broken". - Dialogs instead of OS alerts. The web has no OS prompts; it has JS dialogs (
alert/confirm/beforeunload). They are auto-handled by a fixed, model-free policy (dismiss) and recorded inalerts, replacing the iOS vision alert guard.--dismiss-alertsand the vision path are iOS-only;--headedapplies (watch the crawl in a visible browser).
codegen¶
Generates a native test from a scenario (AI-independent · structural mapping · codegen): XCUITest (Swift, iOS) or Playwright (TypeScript, web).
bajutsu codegen <scenario.yaml> --target <name> [--emit xcuitest | playwright] [-o <out>] [--config ...]
| Option | Default | Description |
|---|---|---|
--emit |
xcuitest |
output format: xcuitest or playwright (others exit with code 2) |
-o, --out |
- |
output file. - for stdout |
- Config's
launchEnvgoes into the generated test:app.launchEnvironment(XCUITest) or seededlocalStorage(Playwright). --emit playwrightrequires the app to be a web target (targets.<name>.baseUrl); otherwise it exits with code 2.- On file output:
wrote <N> scenario(s) -> <out>.
approve¶
Promotes a run's captured screenshots into visual baselines — the second half of the
visual-regression loop (run → review → approve → re-run). Reads manifest.json, so it needs
no Simulator and runs headless in CI; it is the CLI twin of the WebUI's Approve button.
bajutsu approve [<run_dir>] --baselines <dir> [--scenario <id>] [--all] [--runs runs]
| Option | Default | Description |
|---|---|---|
<run_dir> |
latest under runs/ |
the run to approve from |
--baselines |
(required) | where to write the promoted baseline PNGs |
--scenario |
"" | only this scenario id (e.g. 00-home, as in the run dir) |
--all |
off | also refresh baselines whose comparison already passed (default: only failing / missing) |
--runs |
runs |
runs root, used when <run_dir> is omitted |
- Copies each visual check's captured
visual-actual.pngto<dir>/<baseline>. Exits 0 when ≥1 baseline was promoted, 1 when there was nothing to approve.
serve¶
A local web UI to author, run, and explore — a Tier-1 convenience, not part of the CI
gate. Four top-level tabs over the CLI: Record authors a scenario from a goal
(python -m bajutsu record ...), Replay runs a scenario and shows its report
(python -m bajutsu run ...), Crawl explores the app and draws its screen map live
(python -m bajutsu crawl ...), and Stats shows the aggregate run-stats dashboard across the
run history (bajutsu stats). All run against the active config, which you open from
the file browser, a Git repository, or an uploaded .zip bundle (see "Open config" below).
Each request spawns the CLI per request on a background thread,
streams its output, and serves the produced runs/<id>/ tree so the report's relative asset
links (and the crawl's screenmap.json) resolve. Stdlib only (no web framework); binds
127.0.0.1.
bajutsu serve [--port 8765] [--config bajutsu.config.yaml] [--root .] [--runs runs] [--baselines <dir>]
[--host 127.0.0.1] [--token <t>] [--max-concurrent-runs 4] [--evidence-store <uri>]
--token(or$BAJUTSU_SERVE_TOKEN) — authentication (BE-0051). With a token set, every request must authenticate: API clients sendAuthorization: Bearer <token>; the browser exchanges the token once viaPOST /api/login(the UI prompts on a 401), which sets an HttpOnly, SameSite session cookie — the token is never put in a URL. Binding a non-loopback--host(e.g.0.0.0.0) requires a token, so the server is never exposed unauthenticated (it exits otherwise). With no token the server is open, as before — only safe on loopback. Full multi-user auth (OAuth/RBAC) is out of scope here (BE-0015).- CSRF + security headers (BE-0051). When a token is set, a state-changing POST whose
Origindoesn't match theHostis rejected (defense-in-depth atop theSameSite=Strictsession cookie); non-browser clients (noOrigin) are unaffected. Every response carriesX-Content-Type-Options: nosniff,X-Frame-Options: DENY, andReferrer-Policy: no-referrer. --configis optional. Omit it and open aconfig.ymlfrom the UI's file browser (an "Open config" button); the browser is confined to--root(default: the current directory).--scenarios <dir>is available as an override of the selected app's configured dir.- From a Git repository (BE-0063).
--configalso accepts a Git source (github:owner/repo@ref:path), and the "Open config" dialog has a From a Git repository field for the same spec: serve materializes the repo subtree at the ref into its cache, binds that config, and serves from the checkout root — so the config's relativescenarios/appPath/buildresolve against the fetched tree. This Git binding is the self-hosted payoff (BE-0016 Tier A): point serve at the team's test repository instead of hand-syncing files, and switch branches in the UI rather than redeploying. The file browser stays confined to--root; the checkout is a managed content-addressed cache, and a Git-sourced run confines the config's path fields to the checkout root (BE-0063). --baselinessets the visual-regression baselines dir (default: abaselines/folder under the app's scenarios dir); runs launched from the UI use it, and the report's Approve button promotes the captured screenshot into it viaPOST /api/approve.--themes <dir>is a drop-in theme directory (BE-0191): each*.cssfile adds a selectable UI theme. A theme is declarative — a block of the design tokens documented inbajutsu/templates/serve.themes.css, with a leading/* bajutsu-theme name: … kind: dark|light */manifest comment; the theme's id is the filename stem and its[data-theme="<id>"]selector must match it. No JavaScript, so a dropped-in theme sits at the same trust level as your scenarios and config. The built-inmidnight/daylightpair is always offered; discovered themes extend it. Scanned once at startup (no live reload). An optionalui.default_themein the config sets the initial selection (otherwise the OS scheme is followed until the user picks one).- Pick an app (its scenarios populate the dropdown), set backend / udid / erase /
disable alert-dismiss, hit Run; the output streams live and thereport.htmlembeds on completion. - The Crawl tab picks an app, a pool of simulators (multi-select, like Replay — two or more
crawl in parallel, sharing one screen map; BE-0064),
and a budget (max screens / steps), then
POST /api/crawlspawns the crawl; the returned run id lets the UI pollruns/<id>/screenmap.jsonand draw the screen map as it grows (screens laid out in breadth-first layers, transitions as arrows). The Stop button aborts it, like Replay. - The Stats tab renders the aggregate run-stats dashboard
(BE-0102) — the same
read-only trend as
bajutsu stats, live over the server's run history: pass-rate over time, slowest and flakiest scenarios, failure hotspots, and run volume. It reuses the same deterministic aggregator, reading each run'smanifest.jsonfrom the artifact store (the run-id list comes from the system of record when a database is wired, org-scoped, else the artifact store) — no device, no AI, no verdict. It is served atGET /statsand refreshed with the tab's refresh button. - The Flaky tab renders the ranked flaky-scenario surface
(BE-0220) —
the same read-only ranking as
bajutsu flakiness, live over the server's run history: scenarios sorted flaky-first by verdict flip rate, each row linking to the representative passing and failing runs' evidence. When a database is wired it groups straight from the provenance stamp on each run row (org-scoped); otherwise it builds the same records from each run'smanifest.json. No device, no AI, no verdict. It is served atGET /flakinessand refreshed with the tab's refresh button. - Upload a bundle (BE-0073).
The "Open config" dialog has a third source, Upload a bundle, that lets a browser user bring
their own suite to a hosted
servewith no file-system access to the host. Drop a.zipwhose layout is just a working local checkout — abajutsu.config.yaml, itsscenariostree, and the built app binary the config'sappPathnames (a.appdir, a zipped.app, or an.ipa).serveextracts it into a confined sandbox (a sibling ofruns/, never--root) and binds it as the active config, exactly like the file-browser and Git sources — so the Replay / Record / Crawl tabs then run from the extracted tree (the config's relativeappPath/scenarios/baselinesresolve against the bundle). Runs land inserve's own store (--runs-dir), not the sandbox, so they survive in History; the upload's file name and the zip's sha256 are recorded into each run'smanifest.json(provenance), so "what did this run execute?" stays answerable. Only one bundle is bound at a time — opening another config (any source) removes the sandbox. The bundle carries no secrets:${secrets.*}resolve from theservehost's environment as on any run (BE-0032), and an uploaded config'sbuildcommand is never executed on the host (the bundle ships a prebuilt binary; DESIGN §1). Extraction is hardened against zip-slip (absolute /../ symlink entries are rejected) and zip-bombs (entry-count, total-uncompressed, and per-entry compression-ratio caps); every target's path fields are confined to the bundle at bind; and binding a config is an admin-role action behind the same token auth as every other request — bringing an arbitrary binary is only exposed on an authenticated, single-Macserve. - Compose from artifacts (BE-0268).
A combined bundle couples three pieces of very different change cadence — a large binary that
changes every build, a small scenario tree that changes every edit, and a config that almost never
changes — so re-shipping the whole zip for a one-line scenario fix pays for the unchanged binary
again. The "Open config" dialog's fourth source, Compose from artifacts, splits the bundle
into three independently uploadable, content-addressed parts — Config, Scenarios (a
.zipof the scenario tree or a single.yaml, told apart by content and, for a single file, written into the config'sscenariosdirectory with its extension normalized to.yaml), and Binary (a zipped.app, an.ipa, or an Android.apk, extracted or written raw by the config'sappPathsuffix) — each stored by the sha256 of its bytes (POST /api/artifacts/{config,scenarios,binary}). Because a part is addressed by its content, an unchanged part is skipped entirely: the browser hashes the file and asksGET /api/artifacts/existsbefore uploading, so the same binary never travels the wire twice. Compose & load (POST /api/compose) then assembles a chosen(config, scenarios, binary)triple into exactly the same confined tree a combined bundle produces — the config is the sole layout authority, naming via its own relativescenarios/appPathwhere each part lands — and binds it as the active config, so the Replay / Record / Crawl tabs run from it just as with a bundle. A triple whose config needs a part you did not supply is rejected before any run (never a half-built tree). Swapping one leg and composing again runs a new combination (binary A vs. B against the same scenarios, or several scenario sets against one binary) with no re-upload of the shared parts. Each run'smanifest.jsonrecords the triple's provenance as acompositionIdplus one<kind>Shaper part, so "what did this run execute?" is answerable to the exact bytes of each piece. Every artifact upload and the compose step are admin-role actions behind the same token auth, with the same zip-slip / zip-bomb hardening as the combined bundle. (The three-drop-zone UI and this compose endpoint are BE-0268's UI slice; the split-vs-combined internal unification of the combinedPOST /api/uploadis a separate follow-up.) - The AI provider for authoring (Record and Crawl) is one global choice in Settings → AI
provider: Anthropic API (
ANTHROPIC_API_KEY), Amazon Bedrock (AWS credentials +BAJUTSU_BEDROCK_MODEL), or the Anthropic CLI (ant, a browser-based OAuth/SSO sign-in on your Pro/Max/Console seat — BE-0163).serveapplies it to spawned jobs viaBAJUTSU_AI_PROVIDER, so there is no per-tab picker; every AI path (authoring, the alert guard, triage) uses the one provider. - Inline scenario validation in the editor (BE-0138).
The Author tab's YAML editor validates as you type, not only on Save: a debounced
POST /api/lintruns the samebajutsu lintchecks and returns line-anchored diagnostics, shown as a gutter marker on each offending line and a clickable problems list (click a finding to jump to its line). YAML parse errors carry the exact line and column; schema/validation errors resolve to their line best-effort. The scenario JSON Schema (GET /api/schema, the same outputbajutsu schemaprints) also drives lightweight key completion (Ctrl/⌘+Space) and hover descriptions. Deterministic and AI-free — no device or model runs; Save keeps its own validation guard, so the inline layer only surfaces failures earlier. - If the app's built binary (config
appPath) is missing, the app'sbuildcommand runs first (its output streams into the job log); a build failure aborts the run before it spawns. Settargets.<name>.buildto the shell command that producesappPath(e.g.make -C demos/showcase swiftui-build) to build on demand from the UI without a manual build first. - A History list under the controls shows past runs (newest first, with a pass/fail dot and
scenario summary); click one to reopen its report.
GET /api/runsbacks it. - The run subprocess inherits the launch environment (the venv
binis prepended toPATHso theidbclient resolves). Run it from the project root sobajutsu.config.yamlresolves. - Input validation on
/api/run. The scenario must be an existing*.yamlinside the selected app's scenarios dir (no arbitrary host paths or..traversal), andbackend/udidmust be known tokens, not free text — so a request can't run an arbitrary file or smuggle surprising argv. This hardening is the prerequisite for hostingservebeyond loopback (BE-0015 / BE-0016); it still binds127.0.0.1and has no auth, so don't expose it to an untrusted network yet. --max-concurrent-runs(default 4) caps how many run/record jobs may run at once so one caller can't monopolize the scarce device (BE-0051); dispatch over the cap returns 429. Set0for unlimited.--evidence-store <uri>(or$BAJUTSU_EVIDENCE_STORE) — upload each run's evidence (BE-0110). Point it ats3://bucket/prefixorgs://bucket/prefixand every completed run's tree is uploaded there, keyed<prefix><evidence_prefix><runId>/…so the path selects the cloud lifecycle policy. Unlike the standalonerun --evidence-store(which uploads directly with the runner's own credentials),serveholds the credentials and never hands them to a worker: the control plane signs a presigned PUT URL per file, and the worker uploads over plain HTTP with no cloud SDK or credentials of its own. A caller picks the per-run path by passingevidence_prefixin thePOST /api/runbody (validated as a safe relative segment); the server prepends its own bucket + base prefix, so the run id is always in the key and runs never collide. Upload runs after the verdict, so a failure only warns. Needs thes3orgcsextra on the server (the worker needs neither). See self-hosting for the topology.- Hosting flags (advanced).
--emit-launchagentprints a launchd plist to runserveas a token-authenticated LaunchAgent on a single Mac;--backend server(with--asgi) switches to the hosted FastAPI control plane. Both are covered in self-hosting.
mcp¶
Starts an MCP (Model Context Protocol) server so an agent (Claude Desktop / Code) can run
scenarios and read run evidence. Needs the optional bajutsu[mcp] extra (fastmcp).
bajutsu mcp [--config bajutsu.config.yaml] [--runs runs] [--transport stdio]
| Option | Default | Description |
|---|---|---|
--config |
bajutsu.config.yaml |
config the tools resolve targets against |
--runs |
runs |
the runs dir exposed as resources |
--transport |
stdio |
stdio (a local agent) or sse (HTTP) |
- Tools:
bajutsu_run(deterministic run) andbajutsu_doctor(convention score) — both mirror the CLI, and no AI enters the verdict. - Resources: a finished run's
manifest.json/report.html/junit.xmland any nested artifact (bajutsu://runs/<id>/…), plusruns/latest.
worker¶
Leases queued runs from Redis and executes them — the execution half of the hosted server backend
(BE-0015;
self-hosting). Needs the optional bajutsu[worker] extra (redis / rq); not
needed for local use.
bajutsu worker [--redis-url <url>] [--queue bajutsu]
lint¶
Validates a scenario file against the grammar without running it — the same strict validation
run applies at load. Exits 0 if valid, non-zero with the error otherwise.
bajutsu lint <scenario.yaml>
schema¶
Prints the scenario JSON Schema to stdout, for editor integration (autocomplete / inline validation). No options.
bajutsu schema > bajutsu.schema.json
Environment variables (.env)¶
_bootstrap (@app.callback) loads .env before every command (implementation: bajutsu/dotenv.py).
KEY=VALUEform. Handles#comments, blank lines, anexportprefix, and quotes.- Never overrides an existing environment variable (a real value always wins).
.envis a fallback. - The path defaults to
.env, changeable viaBAJUTSU_DOTENV. It is.gitignored. - Main use:
ANTHROPIC_API_KEY(forrecord,crawl, and the alert guard, which runs by default). - AI provider (BE-0053): set
BAJUTSU_AI_PROVIDER=bedrockto reach Claude through Amazon Bedrock instead of the Anthropic API — authenticated by the standard AWS credential chain (env vars / shared profile / instance or role), so noANTHROPIC_API_KEYis needed on that path. SetBAJUTSU_BEDROCK_MODELto a provider-prefixed model id (e.g.global.anthropic.claude-opus-4-6-v1; the bare Anthropic id is not a valid Bedrock id) andAWS_REGIONfor the region. Anthropic is the default. The same selection drivesrecord,crawl,triage, and the alert guard. - Anthropic CLI (BE-0163): set
BAJUTSU_AI_PROVIDER=ant(orai.provider: ant) to reach Claude through the officialantCLI — runant auth login(a browser-based OAuth/SSO sign-in) so a Claude Pro/Max/Console seat is billed instead of an API key;ANTHROPIC_PROFILEselects a named CLI profile. NoANTHROPIC_API_KEYis needed, and every AI path keeps full vision.
# .env — Anthropic (default)
ANTHROPIC_API_KEY=sk-ant-...
# .env — Amazon Bedrock instead (authenticated by your AWS credentials)
BAJUTSU_AI_PROVIDER=bedrock
BAJUTSU_BEDROCK_MODEL=global.anthropic.claude-opus-4-6-v1
AWS_REGION=us-east-1
# .env — Anthropic CLI instead (no API key; run `ant auth login` first)
BAJUTSU_AI_PROVIDER=ant
# ANTHROPIC_PROFILE=work # optional: select a named ant CLI profile