DVT MARTIN
dvt martinputs Martin — the DVT data agent — in your terminal. It's the same agent that answers in the app suite's Chat (dvt serve → /chat), with the same tools and the same APPLY gates, just REPL-faced: ask in English, watch his work stream line by line as each tool finishes, read the answer, and apply his proposals with a keypress.
RUN IT
No flags needed. Martin loads the project in the current directory, says hello, and waits:
$ dvt martin MARTIN — the DVT data agent, in your terminal. Ask about your data, your models, your last run. /help for commands. ⏵ you > which orders models do we have, and are they fresh? martin is working... … thinking with the model ✓ list_relations — 14 relation(s) ✓ run_sql · select max(ordered_at) as latest from fct_orders — 1 row(s) … reading the reply Three orders models: stg_orders, int_orders_enriched and fct_orders. The newest row in fct_orders landed today at 06:14, so this morning's build has already run. ⏵ you >
Every line under martin is working... is live — a trace entry prints the moment each tool call finishes, with a ✓ or ✗, what the tool touched, and a one-line result. You never stare at a silent spinner wondering what the agent is reading. ctrl-c while he works interrupts the question and returns your prompt; ctrl-dat the prompt leaves. Martin remembers your recent exchanges, so "now break that down by region" works as a follow-up.
FROM FAILURE TO FIX, WITHOUT LEAVING THE PROMPT
The everyday use case: a run failed, and you want to know why and be done with it. /errors asks Martin to diagnose the last run — he reads the run ledger, opens the failing model, explains, and proposes the fix:
⏵ you > /errors
martin is working...
… thinking with the model
✓ diagnose_last_run — 1 failure(s)
✓ read_project_file — file models/marts/fct_sales.sql
✓ propose_file_edit — edit to models/marts/fct_sales.sql proposed — awaiting APPLY
… reading the reply
fct_sales failed: its join reads o.order_ts, but the orders source
renamed that column to ordered_at. One targeted edit fixes it —
proposal below.
▮ PROPOSAL targeted edit models/marts/fct_sales.sql
--- remove
on o.order_ts::date = d.date_day
+++ insert
on o.ordered_at::date = d.date_day
[a]pply / [d]ismiss > a
applied models/marts/fct_sales.sql (backup: models/marts/fct_sales.sql.20260803091412.bak)Nothing was written until you pressed a — and the previous version is sitting beside the file as a timestamped .bak (dvt clean-bak-files sweeps those up once you trust the change).
IN PLAIN DBT
There is no agent. You'd tail the log yourself, open target/run_results.json, grep for the failing model, and fix it by hand — or paste your SQL and schema into a browser chatbot that has never seen your project, can't run a query to check its own theory, and edits nothing. Martin investigates with real project tools and still can't touch a file until you press APPLY.
EVERY CHANGE WAITS FOR YOUR KEYPRESS
Martin's tools split cleanly in two. Reads — project files, schemas, live SELECTs, the last run's ledger, connection health — run freely, and that's what the trace shows. Writes don't exist as direct tools at all: anything that would change the world (a model file, a targeted edit, a sources.yml, a scheduler job, a dvt command, a shell line) arrives as a proposal card — label, target, a preview of the content — and sits at the [a]pply / [d]ismiss prompt until you decide. Dismiss discards it; nothing ran.
Command proposals carry a second guard: a proposed command must be a dvt verb from a fixed whitelist (run, build, test, seed, compile, sync, docs, generate-sources, flip-target-to, config-checkup, config-fix, clean-bak-files, metadata, generate-base-views, retract, and kin) — never an arbitrary shell line. Apply one and it runs in your project directory with the output printed right there. File reads, greps and edits are jailed to the project directory, and files that hold secrets are refused outright.
PLAN MODE — INVESTIGATE, NEVER WRITE
Start with --plan (or switch any time with /plan) and Martin keeps only his read-only tools: queries, file reads, greps, schema lookups, run diagnosis, connection health, web search. Nothing that proposes, nothing that writes — the proposal machinery itself is out of reach, not merely discouraged. The prompt shows the mode so you always know which Martin you're talking to:
$ dvt martin --plan MARTIN — the DVT data agent, in your terminal. Ask about your data, your models, your last run. /help for commands. ◔ plan you > /execute execute mode ⏵ you >
Plan mode is the right gear for exploring an unfamiliar project or auditing what a fix would involve before letting anyone — human or agent — draft it.
ONE GATEWAY, NO PER-PROVIDER KEYS
Martin's AI outlet is AI-Proxy — one OpenAI-compatible gateway in front of your existing model subscriptions and local Ollama. One base URL, one key, added once in the hub under Settings → AI CREDENTIALS (dvt serve). There are no per-provider API keys anywhere in DVT, and no per-token billing relationship with DVT — the gateway fronts whatever access you already have. Without it configured, dvt martin refuses at startup rather than half-working:
$ dvt martin No AI gateway configured — add your AI-Proxy credential in hub → Settings → AI CREDENTIALS (dvt serve).
The honest note that comes with one gateway: it must be reachablefrom your machine when you ask. If it's down or unreachable, Martin's requests fail with a plain "provider unreachable" — there is no offline fallback, because there is no second outlet. On an on-prem floor that's a feature: one machine runs the gateway, everyone reaches models over the internal network, and air-gapped setups route to local Ollama through the same door.
Which model answers is the gateway's call unless you pin one — --model at launch, or /model mid-session to pick from the catalog the gateway reports right now:
⏵ you > /model
0 claude-haiku-4-5
1 claude-sonnet-4-6
2 gemini-2.5-pro
3 gpt-5.2
4 qwen3:8b
number (empty = gateway default) > 1
model: claude-sonnet-4-6That catalog is live and can shift as the gateway's upstream sessions come and go. If your pinned model vanishes mid-conversation, Martin retries once on the gateway's default route and says so in the trace, rather than stranding the conversation.
AI_ENABLED — THE KILL SWITCH
Some projects must never talk to an AI model, full stop. Set ai_enabled: false in the project's dvt_project.yml and dvt martin refuses before anything else happens:
$ dvt martin AI is disabled for this project (dvt_project.yml: ai_enabled: false)
Disabled, not muted: with the switch off, credentials are never even resolved — zero bytes leave the project for AI, because there is no request left to make. Every AI surface in DVT honors the same switch and says plainly that it's off. It's a local setting in a version-controlled file: no server turns it back on, and no license overrides it.
THE SLASH COMMANDS
/help prints the full card — this is its real output, and the tour of everything Martin can do from the CLI:
⏵ you > /help MARTIN — commands /model pick the AI model your gateway serves /plan /execute switch modes (plan = investigate only, never write) /errors diagnose the last run /generate-sources [<connection>|all] kick-start sources.yml /flip-target-to <target> dry-run + propose the default-target flip /config-checkup TELL what is wrong with the model configs (no AI) /config-fix REPAIR them — the same issues, actually fixed /clean-bak-files remove DVT backup files from the model folders /sql <query> raw SQL through the guard — no AI in the loop /help this /quit leave (ctrl-d works too) Mention project files with @path/to/file — Martin reads them himself.
/generate-sources points Martin at one connection (or all) and has him draft your sources.yml as a proposal. /flip-target-to <target> runs the deterministic dry-run first — no AI in the preview — prints the ledger, then has Martin propose the real flip and explain, model by model, which convert cleanly and which carry a named translation boundary. /config-fix works the same way: the deterministic checkup report prints first, and only then does Martin propose the repair — the repair itself stays behind APPLY like every mutation.
Three of them use no AI at all. /config-checkupis the plain report — it only tells, so there's nothing to propose. /clean-bak-files previews the exact files a cleanup would remove before anything is proposed. And /sqlsends your query straight through Martin's SQL guard — exactly one statement, a bounded read-only SELECT, with model and source names resolving to live data — no model in the loop, rows straight back:
⏵ you > /sql select status, count(*) as n from fct_orders group by 1 status n paid 8214 pending 312 refunded 77
Result tables print up to 20 rows and then say how many more there are. And anywhere in a question, @path/to/file hands Martin a file to read himself — same project jail, same secret-refusal as every other read.
IN PLAIN DBT
Each of these is a separate manual workflow: hand-writing sources.ymltable by table, porting model SQL to a new warehouse dialect yourself, eyeballing configs for drift, and switching to a separate SQL client (with real write credentials) just to check a count. Here they're one prompt away, and the read-only ones are guaranteed read-only by construction.
REFERENCE — EVERY FLAG
| FLAG | TYPE | DEFAULT | WHAT IT DOES |
|---|---|---|---|
| --model | string | (gateway default) | Pin an AI model id your gateway serves. Empty means the gateway's default model answers. Change it mid-session with /model, which lists the live catalog. |
| --plan | flag | off | Start in plan mode: investigate only, never write. Martin keeps read-only tools exclusively — the proposal machinery is out of reach. Switch modes any time with /plan and /execute. |
| --project-dir | path | . | The DVT project to load. Martin's file reads, greps, edits and proposals are all jailed to this directory. |