Skip to main content

CLI Commands

Global Flags​

These flags apply to all commands:

FlagDescription
--config <file>Additional configuration file(s) to load and merge (repeatable)
--before-config <file>Configuration file(s) to load before auto-discovery (repeatable; for wrappers/libraries)
--no-auto-configDisable auto-discovery of datamitsu.config.{js,mjs,ts} at git root
--binary-command <name>Override the binary command name. Also settable via DATAMITSU_BINARY_COMMAND
--log-format <format>Status output format: console or newline-delimited jsonl. Also settable via DATAMITSU_LOG_FORMAT
--no-ociDisable OCI bundle store seeding. Also settable via DATAMITSU_NO_OCI
--no-parseShow raw tool output instead of parsed findings; parsing still runs. Also via DATAMITSU_NO_PARSE
-v, --verboseEnable debug-level logging for this invocation

With --log-format=jsonl, status and progress go to stderr as typed JSON events, one per line, and log lines arrive the same way: each is a log event whose level is debug, info, warn or error, with the message and its fields in msg. Which levels appear follows DATAMITSU_LOG_LEVEL (warn by default) and --verbose (debug), exactly as in console mode.

Exit codes​

CodeMeaning
0Success
1A tool failed — it exited non-zero, or its parsed output held a finding at or above its operation's failOn (see Failing on findings) — or any error without a code of its own
2Usage: an unknown flag, a flag or DATAMITSU_* value the command does not accept (--widen-to=Repo, DATAMITSU_FAIL_FAST=yes), a missing required flag, flags that cannot be combined, the wrong number of arguments, or a combination refused before anything runs (--require-coverage with --tools, a report of a narrowed run without --allow-partial)
3llms: an unknown or ambiguous page
4The run did not cover what it was asked to: --require-coverage (see Narrowed runs) or --fail-on-skip (see Skipped tools); when both fail, both messages are printed
5A report asked for with --report or DATAMITSU_REPORT was not written (see Reports)
130, 143fix, lint or check interrupted by SIGINT or SIGTERM (see Keep-going runs)

When several apply, a tool failure (1) wins over an incomplete run (4), which wins over a report that was not written (5); the messages of the ones that lost are still printed. A usage error is found before anything runs and combines with nothing. A script that tells "you called it wrong" from "the code is bad" checks for 2 and 1 apart. An unknown command (datamitsu bogus) and a few checks that other commands make on their own arguments still exit 1.

exec​

Execute a managed binary with all environment variables passed through.

datamitsu exec <appName> [args...]

When called without arguments, lists all available tools grouped by type (binary, uv, node, jvm, go, shell).

exec hands the app the environment it was started with, unchanged: in a GitHub Actions job a tool run through exec sees GITHUB_ACTIONS and prints its own annotations. fix, lint and check do not — they strip the variables that switch a tool's output format and set NO_COLOR=1 (see Tool Environment).

An app is installed on demand the first time you exec it. If that app declares links, its .datamitsu/ symlinks are created at the same time — this is how a lazy: true link-app that init deferred gets its managed-config links on first use.

Examples:

# List all available tools
datamitsu exec

# Run golangci-lint
datamitsu exec golangci-lint run ./...

# Run eslint via the managed Node.js runtime
datamitsu exec eslint --fix src/

install​

Install one or more managed apps (and optionally runtimes) into the store and leave them ready. Where exec installs, prepares and then runs an app, install materializes its files, runs its declared prepare step (see prepare) and exits — the building block for the per-app stages of a generated Dockerfile (see devtools dockerfile).

datamitsu install [app...]
FlagDescription
--runtime <name>Install the named runtime(s) only, without any app (repeatable)
--no-verifySkip the post-install version check (verification runs by default)

The prepare step always runs before the version check; --no-verify skips only the check. By default, each installed app's version command (--version, or its configured versionCheck.args) is run after installation, so a broken install fails the command instead of producing a broken artifact. Apps whose version check is disabled, and shell apps, are skipped.

Examples:

# Install a single tool
datamitsu install shellcheck

# Install only the node runtime (no app)
datamitsu install --runtime node

# Install and verify each tool actually runs (the default)
datamitsu install eslint prettier

# Install without running the post-install version checks
datamitsu install eslint prettier --no-verify

init​

Download binaries, install runtime-managed apps, and run initialization commands.

datamitsu init

Must be run from the git repository root.

FlagDescription
--allDownload all binaries (both required and optional)
--skip-downloadSkip binary downloads
--fail-on-download-errorStop if any binary download fails
--dry-runShow what would be done without making changes

Download concurrency is controlled via the DATAMITSU_CONCURRENCY env var (default: 3).

What init does:

  1. Detects project types in the repository
  2. Downloads required binaries and runtimes
  3. Installs runtime-managed apps (Bun/Node/UV/JVM/Go) that are referenced by tools
  4. Creates .datamitsu/ symlinks for the config files of installed link-apps — an app marked lazy: true is deferred and gets its links on first datamitsu exec instead
  5. Writes .datamitsu/configs/: every ejectable managed config the project has not ejected, removing any that no longer belong there
  6. Runs configured init commands (e.g., lefthook install)

Examples:

# Standard initialization
datamitsu init

# Download all tools including optional ones
datamitsu init --all

# Preview what would happen
datamitsu init --dry-run

check​

Run fix followed by lint in a single process with shared context. If fix fails, lint is skipped — unless --fail-fast=false runs it anyway (see Keep-going runs).

datamitsu check [files...]
FlagDescription
--explain [mode]Show execution plan without running. Modes: summary (default), detailed, json
--file-scopedOnly process git staged files
--tools <list>Comma-separated list of tools to run
--fail-on-skipExit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools)
--widen-to <level>Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs)
--require-coverage <level>Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs)
--fail-fast[=false]Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs)
--fail-on <level>Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings)
--report <format>=<path>Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports)
--events <what>Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events)
--allow-partialWrite a report that lists findings for a narrowed run instead of refusing the run (see Reports)
--annotations <mode>Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity)
--output <mode>How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output)
--baseline <file>Gate only on findings the baseline does not hold: a report baseline document or a run's own JSON; a tool that exits non-zero still fails (see Baselines)

Examples:

# Check entire project
datamitsu check

# Check specific files
datamitsu check src/main.go src/handler.go

# Check only staged files (useful in git hooks)
datamitsu check --file-scoped

# Preview what would run
datamitsu check --explain

# In CI: fail if a tool you rely on has no binary for the runner's platform
datamitsu lint --fail-on-skip

# See every failure at once instead of stopping at the first
datamitsu check --fail-fast=false

check, fix and lint start every tool without GITHUB_ACTIONS, the AI agent markers and FORCE_COLOR, and with NO_COLOR=1, so a tool prints the format its parser reads whether the run is in CI, under an agent or in a terminal. CI passes through. An operation gets a stripped variable back with inheritEnv; the Tool Environment page lists them all.

check closes with the wall clock of the whole command:

$ datamitsu check
┏━ fix ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ golang · typescript
┃
┃ ✓ gofmt 120ms
┃ ✓ prettier 1.10s
┗━ 2 tools · 2 runs · done in 1.10s · cache 80% ━━━━━━━━━━━

┏━ lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ golang · typescript
┃
┃ ✓ golangci-lint 7.90s
┃ ✓ eslint 3.20s
┗━ 2 tools · 2 runs · done in 7.90s ━━━━━━━━━━━━━━━━━━━━━━━

┗━ check · done in 9.40s · fix 1.10s · lint 7.90s · setup 400ms ━━━

done in runs from the start of the command to the end of the last operation. fix and lint repeat their own footers' times, and setup is the rest: loading the configuration, walking the repository, planning, installing tools. An operation that did not run is named — lint not run after a failed fix under fail-fast, both when a setup step such as the .datamitsuignore check failed. The line is printed for check only, never under --explain, and on failure too; fix and lint end with their footer.

Keep-going runs​

By default a run stops at the first failing tool: nothing at a later priority starts, a parallel sibling that is still running is stopped, a tool that runs once per file stops at the first failing file, and check does not run lint after a failed fix. That is the right answer for a person at a terminal, who fixes one thing and runs again.

--fail-fast=false (or DATAMITSU_FAIL_FAST=false) runs everything to the end:

LevelDefault (fail-fast)--fail-fast=false
Priority groupsStop after the first failing groupEvery group runs, in order
Overlapping toolsThe ones after the failure do not startAll run, one after another
Parallel toolsWaiting ones are cancelled, running ones are stoppedAll finish
Files of a per-file toolStop at the first failing fileEvery file runs; when more than one fails, the frame names each with its exit code
check's fix, then lintLint is not run after a failed fixLint runs, and its footer says lint ran after a failed fix
Exit code1 when anything failed1 when anything failed

The flag wins over the variable; --fail-fast restores the default when DATAMITSU_FAIL_FAST=false is set. datamitsu config runtime reports the effective value as failFast, and where it came from as failFastSource (default or env). A value of DATAMITSU_FAIL_FAST other than true, false, 1 or 0 is refused.

In CI, run datamitsu lint --fail-fast=false: one run then reports every failing tool. A report that lists findings turns fail-fast off by itself. When a later operation fails for another reason than an earlier one — lint cannot install its tools after fix failed — both reasons are reported. A tool that runs once per file and is interrupted after one of its files failed stays a failure; the files it did not reach make the run incomplete. CI runs lint, not fix or check, which change the working tree.

A formatter that fails at a low priority leaves its files unformatted for the linters that run after it, so a keep-going run can report findings that disappear once the formatter passes.

Failing on findings (--fail-on)​

A tool fails a run when it exits non-zero, or when its parsed output holds a finding at or above its operation's failOn — error unless the configuration sets another. --fail-on <level> (or DATAMITSU_FAIL_ON) raises that threshold for every operation of the run, to warning, info or hint; an operation whose own failOn is already stricter keeps it, and nothing lowers a threshold or turns a non-zero exit into a pass:

# CI: a warning fails the run too, whatever the tool's exit code
datamitsu lint --fail-on warning

The flag wins over the variable. datamitsu config runtime reports the variable as failOn (empty when it is not set), and --explain shows each task's threshold: failOn in the JSON, a Fail on: line when it is not error. A value of either that is not error, warning, info or hint exits 2 before anything runs — a mistyped gate is refused, not ignored.

The threshold gates only output a parser module with the severity contract (descriptor schema 2) read: its levels are what the tool printed. Under an older module the exit code alone decides, and a run that asked for a threshold other than error says so once, after its last operation:

WARN failOn ignored for hadolint: parser module predates the severity contract

With the default threshold, a tool that exits 0 on a finding its parser reads as an error fails the run — semgrep without --error, trivy without --exit-code. A tool that failed because of the threshold shows exit code 0 in its frame, with a Failed on: line naming the threshold.

Findings in the terminal​

What the terminal shows is what gates: for every tool it prints the findings at or above the operation's threshold, sorted by level, file, line, column and rule, and counts the rest.

  • A failed tool prints them in its red frame. A tool that failed although none of its findings reaches the threshold — one that fails on warnings, such as yamllint --strict or eslint --max-warnings 0 — prints every finding, so a failure always explains itself; one that failed without findings prints its output.
  • The findings below the threshold are never dropped: the tool line counts them per level, the frame ends with one line saying what it left out, and the footer sums them.
  • A passed tool prints no frame, except when a threshold other than error was asked for and its parser module predates the severity contract: its findings at or above that threshold then print in a yellow frame, since nothing enforced it.

An abridged run, the frame without its directory, command and duration lines:

┃ ✗ eslint 1.20s (1 failed) · 5 warnings
┌─ eslint [per-project] (run #1) ────────────────────
│ Exit code: 1
│
│ src/a.ts:3:7 error 'x' is assigned a value but never used. [no-unused-vars]
│ src/b.ts:9:1 error Unexpected var, use let or const instead. [no-var]
│ + 5 warnings hidden (failOn=error)
└─────────────────────────────────────────────────────────
┃ ✓ yamllint 310ms · 2 warnings
┗━ 2 tools · 2 runs · done in 1.20s · 1 failed · 7 warnings hidden

--fail-on hint shows every finding and fails on any of them. --no-parse prints each framed tool's output instead of findings and counts nothing for it, and so does a tool run once over many files whose parser names no file for its findings: a finding that cannot say which file it is about reads worse than the output it came from. A tool that prints no frame keeps its counters either way.

A tool the run stopped is neither a pass nor a failure, and it is listed rather than left out:

┃ ✗ eslint 1.2s (1 failed)
…
┃ ⊘ tsc [packages/api] cancelled (fail-fast)
┃ ⊘ custom-check not started (fail-fast)
┗━ 1 tools · 1 runs · done in 1.2s · 1 failed · 2 cancelled

cancelled means the tool had started and was stopped; not started means it never ran. (interrupted) replaces (fail-fast) when the run was stopped by Ctrl-C or SIGTERM: datamitsu then stops every running tool, prints what it did not finish and exits 130 (SIGINT) or 143 (SIGTERM). A second Ctrl-C ends the process at once. A signal that arrives while the configuration is still loading, before the run has started, ends the process at once too, with no report. The (N failed) counts never include stopped tools, and a tool stopped in several places of one kind shares one line with a ×N count.

A tool that runs once per file stops at its first failing file under fail-fast, and the files it never reached are named under its failure:

┃ ✗ alpha 12ms (1 failed)
…
┃ ⊘ alpha 2 files not run (fail-fast): src/b.txt, src/c.txt

The line names up to three files, relative to the repository root, and counts the rest.

Agent output​

--output agent (or DATAMITSU_OUTPUT=agent) prints a run for a program that reads it, such as a coding agent: no banner, frame, colour or progress, and one line per record on stdout. What the configuration prints while it loads (console.log) goes to stderr, with the warnings.

$ datamitsu check --fail-fast=false --output agent
fix changed 2 files: src/a.ts, src/b.ts
fix: 3 tools · 4 runs · 0 failed · 0 errors 0 warnings · 0 hidden
src/a.ts:3:7: error eslint(no-unused-vars): 'x' is assigned a value but never used.
src/b.ts:9:1: error eslint(no-var): Unexpected var, use let or const instead.
tsc [packages/api] exited 2 without parsable findings
│ error TS5083: Cannot read file 'tsconfig.base.json'.
knip: skipped (opt-in)
lint: 4 tools · 5 runs · 2 failed · 2 errors 0 warnings · 5 hidden
check · done in 9.40s · fix 1.10s · lint 7.90s · setup 400ms
  • A finding is path:row:col: <level> <source>(<code>): <message>, the path relative to the repository root, the column as the tool counted it and left out when there is none. A line break in anything a record names — a message, a path, a skip reason — is written as the two characters \n, so a record is always one line. The findings are the ones the terminal would show, chosen by the same rule (see Findings in the terminal), and --no-parse changes only the human frames.
  • A tool that failed without findings is one line — <tool> [<dir>] exited <code> without parsable findings, after the file for a tool that runs once per file — followed by the last 20 lines of what it printed, each behind │ , masked as a report is. A tool whose parser module puts it in the security category gets failed (exit <code>); output withheld for a security tool and no output.
  • What did not run — a task stopped or never started, the files a tool that runs once per file left unchecked, a tool the planner skipped — is one line each, as the human block words it.
  • The files a fix changed are one line before its summary, fix changed <N> files: <path>, <path> — an agent that holds any of them in memory reads them again. When they could not be observed (see Reports) the line is fix changes not observed: <reason>, followed by the files seen before the observation stopped.
  • Each operation ends with <op>: <tools> tools · <runs> runs · <failed> failed · <E> errors <W> warnings · <H> hidden, <H> counting the findings below the threshold, and · <N> baselined when a baseline held some, and check with its closing wall-clock line. The exit code is the run's.

Fail-fast stays the default: add --fail-fast=false to see every failure at once. --output agent prints on stdout, so it cannot be combined with --log-format jsonl, which keeps stdout clean, or with a report written to stdout (-); either exits 2, as does any other value. In a GitHub Actions job the records print inside the stop-commands region like any other tool text; a record of a finding in a .go file is in the form actions/setup-go's problem matcher reads, so a job that registers it annotates such a finding twice.

Run events​

With --log-format=jsonl, fix, lint and check write their progress to stderr as typed events, one JSON object per line:

typeWhen
helloOpens the stream: op_id stream, schema (datamitsu.report/1), events, the comma-separated types the stream may carry, and for fix, lint and check annotations
phaseAn operation (op: fix or lint) starts: status: "start"
tool_runA tool starts in a directory (status: "start") and ends: done or fail with its findings per level, or skip for a stopped tool
chunkA tool finished a unit of its work: index of total
errorA tool failed: tool, dir, msg
doneThe operation ended, with its summary
reportA report was written or not: format, path, status and, when it was not, msg (see Reports); github-annotations for the annotations
diagnosticOne finding of a tool, once the tool finished: by default those at or above the operation's failOn

Every task — one tool in one directory, or one file of a tool that runs once per file — has an op_id of its own: the operation's op_id followed by :<tool>:<dir>:<seq>, where dir is relative to the repository root (empty for the root) and seq numbers the operation's planned tasks from 1 in plan order, for example run-1:eslint:packages/web:3. A task's tool_run start, its chunk events, its error and its closing tool_run share it, and no other task's events do.

A --log-format jsonl stream opens with a hello event, whatever the command, so a reader can tell a stream without diagnostic events from a run that found nothing. For fix, lint and check it also says whether stdout carries GitHub annotations beside the stream: annotations is github or off.

{
"type": "hello",
"op_id": "stream",
"schema": "datamitsu.report/1",
"events": "hello,phase,download,install,chunk,tool_run,error,done,log,report,diagnostic",
"annotations": "off"
}

A tool_run that ends with done or fail carries the task's findings per level — findings_error, findings_warning, findings_info, findings_hint, zero included — and cached, true when a cache answered for the task. No other event carries them.

A diagnostic event is one finding, emitted once its tool has finished in the operation — every task of the tool, since a finding's fingerprint depends on all of the tool's findings — and before the operation's done. Its op_id is the task that reported it:

{
"type": "diagnostic",
"op_id": "run-1:eslint:packages/web:3",
"tool": "eslint",
"dir": "packages/web",
"file": "packages/web/src/a.ts",
"row": 3,
"col": 7,
"end_row": 3,
"end_col": 8,
"severity": "error",
"code": "no-unused-vars",
"source": "eslint",
"msg": "'x' is assigned a value but never used.",
"fingerprint": "8d08ad593059cec87a6c97ce43655d04b1bee8102810cd99952dd606b26bb618",
"provenance": "parser",
"reported": true,
"gates": true
}

file is relative to the repository root, rows and columns are 1-based and end_col is exclusive; fingerprint is the finding's report fingerprint. Every event of a JSON-L stream is masked as a report is, op_id included, from its first line with the values the environment names, and once a run has loaded its configuration with its apps' and operations' values and those its tools were started with; a task's events still share one op_id. By default only the findings at or above the operation's failOn are emitted — what the terminal shows. --events diagnostics=all (or DATAMITSU_EVENTS=diagnostics=all) emits every finding, with reported and gates saying which are at the threshold and which failed their tool, and baselined: true on one a baseline held; diagnostics=reported is the default. Any other value exits 2. The synthetic finding a report lists for a tool that failed without findings is never an event.

A stream that could not be written — a closed or full stderr — fails the command, whichever it is and however it ended otherwise: it exits 1 and says so on stdout, the one stream left, unless stdout carries the command's own data (a report written to -, the language server's JSON-RPC, source's shell code).

--verbose adds datamitsu's debug log lines to the stream as log events, with what a tool printed and the arguments it ran with withheld ("<withheld>"): a stream is what a pipeline publishes. The console keeps them.

A tool_run with status: "skip" ends the chain of a tool the run stopped, and is never a failure. Its msg says what happened and why: cancelled: fail-fast or cancelled: interrupted closes the start of a tool that was stopped while it ran; not started: fail-fast or not started: interrupted is the only event of a tool that never started.

An operation's done carries op, status (done or fail), duration_ms, tools (tools that ran), runs (tasks that ran), failed (tools with a failed task), skipped (tools the planner left out, see Skipped tools) and cancelled (tasks the run stopped, present when there were any). A counter or duration_ms that is zero is left out. An operation whose tools could not be installed ends with a done of status fail and no counters.

The stream ends with one more done, for the whole command: its op_id starts with cmd- where an operation's starts with run-, and its op is fix, lint or check. It carries status, success (whether the command succeeded), duration_ms (the wall clock of the whole command), the sums of the operations' tools, runs, failed and skipped, cancelled (always present, zero included) and complete: true when every planned operation ran, no task was cancelled or left unstarted, and no tool that runs once per file stopped at a failing file with files left to check. check whose fix failed under fail-fast reports complete: false. It is emitted for every execution of fix, lint and check — also when the run could not start, for example because the configuration does not load — never under --explain, for a usage error, or for a run a signal ended while its configuration was loading, and not for the fix that config reconcile runs after writing its files.

{
"type": "done",
"op_id": "cmd-7",
"op": "check",
"status": "fail",
"success": false,
"duration_ms": 9400,
"tools": 3,
"runs": 4,
"failed": 1,
"cancelled": 1,
"complete": false
}

Reports​

--report <format>=<path> writes a report of the run once its last operation has ended, whether or not its tools failed — the run that fails is the one a pipeline needs to read. The flag is repeatable, one per format: json, the run's own document; markdown, the same run for a person; sarif, the run for code scanning; junit, the run as test results; codequality, GitLab's Code Quality report; checkstyle, Checkstyle XML; rdjsonl, reviewdog's diagnostics; history, one line of counts appended to a trend file; and patch, the diffs a fix's formatters applied. The Reports guide explains what a report holds and how far to trust it; report render writes one again, offline, from a run's own JSON.

# CI: the whole run as one document, uploaded whatever the outcome
datamitsu lint --report json=out/run.json
  • The path is required, and relative to the working directory. Missing directories are created, and the file is written to a temporary name beside it and renamed, so a reader never sees half a report. It gets the permissions a file created in its place would — the umask decides — and a report it replaces keeps its own.
  • - writes the report to stdout, for one format per run. stdout then carries the report alone: the human output is left out, and stderr carries the run events as it does under --log-format jsonl.
  • A path that ends in / names a directory, for a format that splits a run over several files (sarif); any other format given one exits 2. Options follow the path after ?: sarif=out/results.sarif?category=linux.
  • DATAMITSU_REPORT is the variable twin, comma-separated: DATAMITSU_REPORT=json=out/run.json. A --report naming the same format wins over its entry. datamitsu config runtime reports the variable as report.
  • A report that lists findings — every format but history and patch — turns fail-fast off: a run that stopped at the first failing tool could not list every finding. Such a report together with --fail-fast=true, or with DATAMITSU_FAIL_FAST=true and no --fail-fast flag, exits 2. history and patch leave fail-fast as it is.
  • A format named twice, an unknown format or option, a missing path, two reports that would write one file — one path twice, a report where another's completeness companion goes, or a file inside a split format's directory — and a report combined with --explain (which runs nothing) exit 2 before anything runs.
  • A report that cannot be written prints error: report <format>: <path>: <cause> and makes the run exit 5 — unless a tool failed (exit 1) or the run was incomplete (exit 4), which keep their code; the line is printed either way. Under --log-format jsonl the same fact is a report event.
  • The time a report is stamped with comes from SOURCE_DATE_EPOCH when it is set, the reproducible-builds convention; durations are always measured.

A report says how much of the repository its findings stand for. Every tool is judged on three facts, and each one that fails adds a reason to the tool's incomplete list:

FactComplete whenReasons
scopethe run covered the whole repository — no files named, no subdirectory, no --file-scoped — and every task its unitnarrowed-selection, partial-unit
executionevery planned task ran to the endcancelled, not-started, setup-failed, platform-skip
extractionevery output was read into findings by a parser, or a cache replayed a pass its parser read as cleanno-extraction, parser-unavailable, parse-failed, truncated, unparsed-cache-hit, failed-without-findings

A tool whose output no parser read — its own, a format parser, or the fallback built into datamitsu — is never complete (no-extraction, or parse-failed when a parser was declared): its exit code says whether it passed, not what it found. Nor is a tool whose process exited non-zero while the parser that recognized its output found nothing in it (failed-without-findings). The run is complete when every tool is, every operation ran, and nothing was left out at run level: narrowed-selection, tools-filter (with selection.excludedTools naming the tools --tools left out — the selected tools' own runs can still be complete), not-narrowable (a tool skipped because it could not be narrowed) and operation-skipped. A tool disabled with skip: true is neither.

A report that lists findings is refused for a run narrowed before it starts — files named, a subdirectory, --file-scoped or --tools — because it would read as the findings of the repository: the run exits 2 before anything runs, and the report is recorded as refused. --allow-partial (or DATAMITSU_ALLOW_PARTIAL=true) writes it anyway, with every reason in the document; it never makes a run complete. A report left on its path by an earlier run is not deleted, so a pipeline that uploads it with if: always() checks the exit code as well.

json writes datamitsu's own document, datamitsu.report/1: what the run was asked to cover (selection), whether fail-fast was on (failFast), the CI job it ran in (ci: vendor, empty outside CI, and the sha, ref, baseRef and prNumber the vendor names — see facts().ci), and for each operation (fix, lint; check writes one document holding both) whether it ran and succeeded, the tools the planner skipped and the tasks the run stopped. Each tool lists its app as configured, its parser module and its threshold, and one invocation per process: its task, state, exit code, extraction outcome, the files it answered for and the findings it reported, with paths relative to the repository root. The files a cache answered appear as one cached or verdict-hit invocation of their task. A finding says whether it is at or above the threshold (reported), whether it failed its tool (gates) and whether the terminal shows it (shown, see Findings in the terminal), which is what the annotations, markdown and --output agent list.

Every finding carries a fingerprint that identifies it across runs: SHA-256 over the tool, the rule, the path and the text of the finding's first line, and its position among the findings the tool reported with the same rule on the same line. A line inserted above a finding moves its row, not its fingerprint, and rewording a message changes neither. fingerprintBasis says what the fingerprint rests on: line, row when the file could not be read, none for a finding without a file. A finding's location gives its columns as the tool counted them, in the unit its parser declares, and converted while the file was on disk into code points (chars), UTF-8 bytes (bytes) and UTF-16 units (utf16); precision is exact, ascii (no unit declared, but an ASCII line), or unknown, when there was nothing to convert from. A report never holds a command line or an environment variable, and nothing caches it: every report is written from the run that produced it.

sarif writes SARIF 2.1.0 for GitHub code scanning and the other services that keep alerts across uploads. It holds one run per tool of the lint operation (check writes its lint, a fix run its fix), in the category datamitsu — automationDetails.id datamitsu/ — or the one ?category=<name> names, and each result carries the finding's fingerprint as partialFingerprints.primaryLocationLineHash. A tool whose completeness is not established is left out rather than listed, and so is a tool with more than 25 000 results, which GitHub would cut: an upload without a tool leaves that tool's alerts as they are. Each tool left out gets one WARN line on stderr naming why and is recorded in the omitted list of the report's entry in exports. Because it leaves out what is incomplete, sarif is written for a narrowed run instead of being refused — named files, a subdirectory and --file-scoped leave every tool incomplete, --tools writes the selected tools that are complete. Code scanning refuses a SARIF file without a run, so a report that would hold none is not written: a file an earlier run left at its path, or the datamitsu-<n>.sarif files of its directory, are removed, the export is omitted (a report event with that status under --log-format jsonl), and a WARN line says so; nothing is written to stdout either. GitHub reads at most twenty runs from one file: sarif=<dir>/ writes datamitsu-1.sarif, datamitsu-2.sarif and so on, twenty tools each, sorted by name, and removes the datamitsu-<n>.sarif files an earlier run left there that this one did not write; a file or - for a run that plans more than twenty tools exits 2 before anything runs. See Code scanning for the workflow.

junit writes JUnit XML: one suite per operation and tool (<operation>/<tool>, its completeness in properties), one case per file the tool answered for, and a <failure> only for a file with a finding at or above the operation's failOn that failed its tool — the failure count follows the gate, not the findings. Findings below the threshold are a case's <system-out>. An invocation that failed without such a finding is one extra case named after its directory (with the invocation's ID when several share one): a <failure type="exit"> listing its findings, or an <error type="exit"> with the masked tail of its output when it has none (never for a security tool). Stopped tasks, skipped tools and an operation that did not run are <skipped> cases. See Test results.

codequality writes GitLab's Code Quality array: one issue per finding of the lint operation (fix for a fix run), with check_name <source>/<rule>, the finding's fingerprint, severity major for an error, minor for a warning and info below, categories Security for a security tool and Style otherwise, and a repository-relative location.path with positions in characters, or lines where the columns could not be converted. A finding without a file or outside the repository cannot be placed and is left out and counted. A tool that is not complete is kept. See GitLab Code Quality.

checkstyle writes one <file> per file and one <error> per finding of the lint operation (fix for a fix run), with line, column in characters where the report could convert it, severity error, warning or info, message and source <source>/<rule>; a finding without a file is under <file name="">. rdjsonl writes one reviewdog diagnostic per line: the message, the path with a range in UTF-8 byte columns (exclusive end, left out where they could not be converted), severity ERROR, WARNING or INFO, the source with its app's official URL and the rule with its documentation; a finding without a file has no location. See Checkstyle and reviewdog.

A format that lists findings but has no place to say how complete it is — junit, codequality, checkstyle, rdjsonl — gets a completeness companion beside its file, <path>.completeness.json (datamitsu.completeness/1): whether the report is complete, the run-level reasons, each tool with its own, the findings the format could not carry (omitted), and the run's exports. A job checks it before it publishes the report. Each incomplete tool also gets one WARN line on stderr, and the report's entry in exports names the companion (companion). A report on stdout has none.

markdown writes the same run for a person, as GitHub renders Markdown: what the run covered and whether it is complete, a table of each operation's tools (status, runs, files a cache answered, findings per level), the findings the terminal would show — grouped by level, then by file, as path:row:col — source(code): message, columns in characters and only where the report could convert them — with a count of those below the threshold, the tools skipped and the tasks stopped, and each incomplete tool with its reasons. It carries no tool output: a tool that failed without findings is its structured message. It lists findings, so it is refused for a narrowed run like json.

history appends one line, datamitsu.history/1, to the file it names — creating it and its directories the first time, never replacing it: when the run started, the build and configuration, the CI job's vendor, commit and ref, what the run covered (selection: mode, fileScoped, toolsFiltered), whether it was complete and ran fail-fast, and per operation and tool the processes that ran, the files a cache answered, the invocations that failed, completeness with its reasons and the findings per level. It holds no finding, no path and none of the paths the run was given, so a narrowed run writes it without --allow-partial, and it leaves fail-fast alone. The line is appended in one write. See History.

A fix operation records the files its tools changed. A snapshot of the working tree — git status --porcelain=v2 over tracked and untracked files, without optional locks, plus a hash of each file it lists — is taken before the first group of tools that run together and after every group, a failed or cancelled one included. Each file whose state moved between two snapshots is a change: created, modified, deleted, or reverted for a file that differed from the index before and matches it after — a formatter that restored what the commit holds. A change is listed on the invocation of that group whose files hold it, and on the operation (changes) when none does. changesObserved says whether the changes were observed and changesScope what that covers: ignored files and the contents of submodules and nested repositories are not observed, and a rename is a deletion and a creation. When they were not observed, changesReason says why — no-fix-task for lint and a fix that planned nothing, not-run, not-executed when the tools could not be set up, snapshot-failed (with changesDetail) when git is missing, the root is not a repository, or a status failed — and no list stands for "nothing changed"; changes seen before a snapshot failed are kept. Snapshots are taken only when the run records anything: a report, a JSON-L stream, annotations or agent output.

patch writes the unified diffs the formatters that write their result on stdout applied, captured while both versions existed, in the order they were applied: a file two formatters changed one after the other has two hunks, the first formatter's first, and git apply or patch -p1 takes the file — a name that holds a control character, a quote or a backslash is quoted as git quotes it. A tool that writes its files itself leaves no patch; its changes carry patch: false. Patches hold source text, so they are captured only when --report patch= is asked for, and the run's json then holds them too, masked like everything else, which lets report render --format patch write them again. Like history, patch lists no finding: a narrowed run writes it, and it leaves fail-fast alone.

A tool that exits non-zero without a finding its parser could read is listed with one synthetic finding of level error and no location, whose message says so — tsc exited 2 without parsable findings — rather than as a clean tool. The message carries none of the tool's output. The document keeps the last 4 KiB of a failed invocation's output as outputTail, without its colour codes. For a tool whose parser module puts it in the security category, the output is never kept: its synthetic finding reads gitleaks failed (exit 1); output withheld for a security tool, and there is no outputTail.

Before a report is written, the value of every variable of the environment, and of every app's and operation's env, whose name contains TOKEN, SECRET, PASSWORD or CREDENTIAL, or ends in _KEY (in any letter case), and that is at least 8 characters long, is replaced by *** wherever it occurs in the document. This catches what the environment names; a secret a tool prints that no variable holds is not caught, which is why a security tool's output is withheld altogether.

Baselines​

lint --baseline <file> and check --baseline <file> gate only on findings the baseline does not hold. The file is a report baseline document or a run's own JSON, whose findings are taken as one; it is read before anything runs, and one that cannot be read, of another schema, or of fingerprints of another version exits 2.

datamitsu lint --baseline .datamitsu-baseline.json

A finding whose fingerprint the baseline holds is marked while its process is judged, before the threshold decides — at most as many findings of a rule on a line as the baseline holds fingerprints of, whichever processes report them: it is neither reported nor gates, so it fails nothing, and under fail-fast cancels nothing. The terminal counts it — · 2 baselined on the tool's line and in the footer — instead of printing it; the own JSON marks it baselined, SARIF puts it in the result's properties.baselined, JUnit lists it in <system-out> marked (baselined), and Markdown and --output agent count it.

The exit code of a tool is untouched: a tool that exits non-zero still fails the run, and its frame shows the baselined findings it failed on, marked (baselined). Most linters exit 1 on the findings they print, so a baseline makes a run fail only on new findings when the tool exits 0 and the gate decides: give the tool its exit-zero flag — --exit-zero, --exit-code 0 — and let failOn gate. See Baselines.

GitHub annotations​

In a GitHub Actions job fix, lint and check print the run's findings as workflow commands once the last operation has ended — ::error file=…,line=…:: — which GitHub shows as annotations in the Checks tab and on the lines a pull request changed, with no token and no extra step:

::error file=src/a.ts,line=3,col=7,endColumn=8,title=eslint(no-unused-vars)::'x' is assigned a value but never used.
::error title=tsc::tsc exited 2 without parsable findings

--annotations (or DATAMITSU_ANNOTATIONS) chooses when:

ModePrints
autothe annotations of the CI the run is in — GitHub's in a GitHub Actions job (GITHUB_ACTIONS=true, not Gitea or Forgejo), Azure Pipelines' under TF_BUILD, TeamCity's under TEAMCITY_VERSION — unless stdout carries a document or --log-format jsonl is on; the default
githubGitHub's, wherever the run is, under --log-format jsonl too: stdout is not the event stream's channel
azureAzure Pipelines' logging commands, wherever the run is (see Azure Pipelines and TeamCity)
teamcityTeamCity's service messages, wherever the run is
offnever

An explicit mode together with a report written to stdout (-) or with --explain=json exits 2: the commands would land in the document. Any other value, from the flag or the variable, exits 2 as well. Nothing is printed when no task ran — a plan that matched nothing, a run refused before it started. The annotations are recorded as an export of the report, github-annotations, azure-annotations or teamcity-annotations, written or omitted with the reason, and under --log-format jsonl as a report event; the stream's hello says the mode the run settled on.

The annotations are what the terminal shows (see Findings in the terminal): each tool's findings at or above its failOn, or all of them for a tool that failed on findings below it, plus one error without a file for a tool that failed without a finding its parser could read. A finding reported twice — by fix and by lint, or by two processes of one tool — is annotated once. The path is relative to the repository root, whatever directory the workflow checked the repository out into; a finding outside the repository names no file. Columns are given in characters, and only on a one-line finding whose columns could be converted (see the report's precision). A message is escaped and cut at 4096 bytes.

GitHub keeps the first ten annotations of each type — error, warning, notice — a step prints, so datamitsu chooses which ten: findings in the files the change touched come first, then one finding of every other file before a second of any, then findings without a file. When anything did not fit, a notice takes the first notice slot and says how many were left out and where they are: datamitsu: 15 more findings in the step summary and in out/run.json names the step summary when it was written and every report written to a file that lists findings. The files a change touched come from git: on a pull_request, the merge commit the checkout holds against its first parent, the base branch's tip (with actions/checkout's default depth of 1 that parent is not fetched: set fetch-depth: 2); on a push, the commit the push started from, named by the event (fetched only with fetch-depth: 0). When the change cannot be read, one info line says why, and the order is the same without the priority. Several datamitsu commands in one step share one budget of ten.

In a GitHub Actions job the run also appends its markdown report to the step summary, the Markdown page of the job's run that GITHUB_STEP_SUMMARY names, unless --annotations off — whatever stdout carries, since the summary is a file, and under --log-format jsonl too. It lists every finding the annotations could have shown. GitHub takes 1 MiB of summary from a step, whoever wrote it, so the run writes what fits in what the file has left and ends with a line saying how many findings it cut and, when the run wrote its json report to a file, that every finding is there. A summary that cannot be written — the variable is unset, the file cannot be opened, or it is full — is one warning (a log event under --log-format jsonl), never a failure; nothing is written when no task ran. Gitea and Forgejo have no step summary.

Everything the run prints between its first results block and the annotations is one ::stop-commands:: region with a random token, so no line a tool printed — raw output, a parsed message — can be read as a workflow command; the annotations follow its end. Every line of tool text in a frame is indented behind its border. The problem matchers actions/setup-go and actions/setup-node register read every line whatever the region, and the indentation keeps tsc's and eslint-stylish's from matching; two remain: go's pattern accepts any text before a path, so raw output in Go's file:line:col: message form still matches — with the border in the file name, which GitHub drops, keeping an error annotation without a location — and so does eslint-compact's, for output in ESLint's compact format. Such a line appears only when a tool's output is shown raw: it has no parser, --no-parse is on, or it failed without findings.

Azure Pipelines and TeamCity​

Under TF_BUILD the run logs its findings as the issues of the Azure Pipelines task, once the last operation has ended:

##vso[task.logissue type=error;sourcepath=src/a.ts;linenumber=3;columnnumber=7;code=eslint(no-unused-vars)]'x' is assigned a value but never used.
##vso[task.logissue type=error;code=tsc]tsc exited 2 without parsable findings

The findings are those GitHub would get — what the terminal shows, each once, and the failures without a finding — with the path relative to the repository root and the column in characters on a one-line finding whose columns could be converted. The agent has two issue types, error and warning, so info and hint findings are not logged, and keeps ten of each type per task: datamitsu orders each type as it does for GitHub — the files the pull request touched first, then one finding of every other file before a second of any — and when anything did not fit, ends with one plain line, datamitsu: 4 more findings in out/run.json. The touched files are those that differ between HEAD and its merge base with origin/<target branch> (SYSTEM_PULLREQUEST_TARGETBRANCH); when that ref is not fetched, or the build is not a pull request, one info line says so. A property value and the message are escaped as the agent unescapes them: % as %AZP25, ; as %3B, a carriage return as %0D, a line feed as %0A, ] as %5D.

The agent runs a ##vso[ command wherever it appears in a line, not only at its start, on stdout and stderr, and has no way to suspend that. Under TF_BUILD — whatever --annotations says — and in azure mode, every line the run prints that holds tool output or a repository path — raw output, a parsed message, a command line, a directory, a file name, a progress label, an agent record or tail, a JSON-L event, a --verbose log line, an --explain plan, a GitHub annotation printed there — has ##vso[ rewritten to ##vso [, so no tool and no file name can set a variable, upload a file or fail the task through datamitsu's output. That alters a line of raw output by one space. A report written to stdout — by the run or by report render — is a document that has to keep its meaning: a JSON format spells the bracket \u005b and an XML format &#91;, which read back as the same text, and Markdown and patch get the space.

Under TEAMCITY_VERSION the run reports its findings as TeamCity inspections: an inspectionType once per tool and rule (id <tool>/<rule>, the rule's documentation as its description when the tool names one), an inspection per finding with the file, the line and a SEVERITY of ERROR, WARNING, INFO or WEAK WARNING for the four levels, and a buildProblem for a tool that failed without a finding, whose identity is the tool's name in Java identifier characters, at most 60. TeamCity keeps every inspection, so nothing is left out, except a finding without a file, which an inspection cannot hold: one plain line counts those. Values are escaped as TeamCity reads them: | as ||, ' as |', [ and ] as |[ and |], line breaks as |n and |r, and other control characters and the Unicode line separators as |0xNNNN.

TeamCity reads a service message anywhere in a line too. Everything the run prints between its first results block and the inspections is wrapped in ##teamcity[disableServiceMessages] … ##teamcity[enableServiceMessages], and the inspections follow the second line; under TEAMCITY_VERSION and in teamcity mode every line that holds tool output or a repository path — inside the region and out, the event stream included — has ##teamcity[ rewritten to ##teamcity [, so neither a tool nor a file name can turn the reading back on. A run killed before it prints its inspections — a second interrupt — leaves the reading suspended for the rest of the build step.

Tools still see TF_BUILD and TEAMCITY_VERSION (see Tool Environment); a tool that prints logging commands or service messages of its own when it sees them has them broken like any other line. The recipes are in the Reports guide.

Skipped tools​

info

A tool is reported as skipped (not run, not failed) for one of three reasons:

  • Disabled in config — the tool sets skip: true (optionally with a skipReason).
  • No binary for this platform — the tool's binary has no build for the current OS/architecture/libc. This is a soft skip: the run still succeeds.
  • Cannot narrow — a narrowed invocation selected less than the operation's declared granularity and the widening policy does not permit expanding it.

Skipped tools appear as ⊘ <tool> skipped (<reason>) lines and a · N skipped count in the summary footer, and as a skipped array in --explain=json.

--fail-on-skip makes the run exit 4 only for platform skips (a tool you expected to run had no binary), the code of a run that did not look at everything. Intentional skip: true and narrowing skips never fail that flag; use --require-coverage to enforce complete narrowed runs.

Narrowed runs​

Naming files, or running from a subdirectory, asks for less than the whole repository. Most tools follow: a formatter given one file formats that file. Some cannot. syncpack compares versions across package.json files, so a one-file answer is not a smaller answer, it is a wrong one.

datamitsu decides this per operation, from the tool's granularity. A narrowable tool runs on what you asked for. One whose verdict covers the whole repository is reported as skipped:

$ cd packages/api
$ datamitsu fix ./swagger.json
✓ oxfmt packages/api/swagger.json
⊘ syncpack skipped (whole-repository verdict — cannot narrow)

Before this behaviour existed those tools were dropped silently — absent from the report, from --explain, from everything. If you were relying on that silence, the tools have not changed; only the reporting has.

--widen-to overrides how far the core may go beyond your request, in either direction — you typed it for this one run, so it wins over the project's policy:

# Only what I named. Anything needing more is reported, not run.
datamitsu fix ./swagger.json --widen-to=target

# Run the whole-repository tools anyway.
datamitsu fix ./swagger.json --widen-to=repo

The policy governs unit-scoped tools too, not only whole-repository ones. A tool whose command line carries no path — go fmt ./..., golangci-lint run, and most formatters that find their own files — reads its entire project whatever you named, so naming one file runs it over that file's project and no other. Under target it is reported rather than run, because a whole project is more than what you named. Tools that take a {files} list are already narrowed by that list and are unaffected.

Set the project default in config with execution.widenTo.

--require-coverage turns an incomplete answer into a failure, for CI:

# Every unit the run touched was fully analysed.
datamitsu check --require-coverage=unit

# ...and the run covered the repository at all.
datamitsu check --require-coverage=repo

unit fails if a tool could not be narrowed, if a task covered only part of its unit, or if the host lacked a binary. repo adds that the run must have targeted the whole repository — without that clause, check --require-coverage pkg/x.ts would pass having checked one package of nine.

A tool you disabled with skip: true never counts against coverage: a repository that opts a tool out has given its answer, not an incomplete one.

It works under --explain too, since coverage is decided while planning — a CI gate that costs one plan and downloads nothing. It cannot be combined with --tools, which would make the assertion cover only the selected subset.

fix​

Run fix operations on files.

datamitsu fix [files...]
FlagDescription
--explain [mode]Show execution plan without running. Modes: summary (default), detailed, json
--file-scopedOnly process git staged files
--tools <list>Comma-separated list of tools to run
--fail-on-skipExit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools)
--widen-to <level>Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs)
--require-coverage <level>Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs)
--fail-fast[=false]Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs)
--fail-on <level>Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings)
--report <format>=<path>Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports)
--events <what>Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events)
--allow-partialWrite a report that lists findings for a narrowed run instead of refusing the run (see Reports)
--annotations <mode>Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity)
--output <mode>How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output)

Examples:

# Fix entire project
datamitsu fix

# Fix specific files
datamitsu fix src/main.go

# Fix only with specific tools
datamitsu fix --tools prettier,eslint

lint​

Run lint operations on files.

datamitsu lint [files...]
FlagDescription
--explain [mode]Show execution plan without running. Modes: summary (default), detailed, json
--file-scopedOnly process git staged files
--tools <list>Comma-separated list of tools to run
--fail-on-skipExit non-zero if any tool is skipped because its binary is unavailable for this platform (see Skipped tools)
--widen-to <level>Limit how far work may widen beyond what you asked for: target, unit or repo (see Narrowed runs)
--require-coverage <level>Exit non-zero unless the run answered completely: unit or repo (see Narrowed runs)
--fail-fast[=false]Stop at the first failing tool (the default); =false runs everything to the end (see Keep-going runs)
--fail-on <level>Fail on findings at this level or above in every operation: error, warning, info or hint; raises each operation's failOn, never lowers it (see Failing on findings)
--report <format>=<path>Write a report once the run ends, failed or not; - is stdout; repeatable; one that lists findings turns fail-fast off (see Reports)
--events <what>Which findings --log-format jsonl emits as diagnostic events: diagnostics=reported (default) or diagnostics=all (see Run events)
--allow-partialWrite a report that lists findings for a narrowed run instead of refusing the run (see Reports)
--annotations <mode>Print the run's findings as CI annotations once it ends: auto (the default), github, azure, teamcity or off (see GitHub annotations and Azure Pipelines and TeamCity)
--output <mode>How the run shows its results: human (the default) or agent, one line per finding for a program that reads the run (see Agent output)
--baseline <file>Gate only on findings the baseline does not hold: a report baseline document or a run's own JSON; a tool that exits non-zero still fails (see Baselines)

Examples:

# Lint entire project
datamitsu lint

# Lint specific files
datamitsu lint src/main.go

# Show detailed execution plan
datamitsu lint --explain detailed

inspect​

Explore the resolved configuration in a browser or export a portable HTML artifact.

datamitsu inspect # Local server on loopback port 7744
datamitsu inspect --port 0 # Choose an available port
datamitsu inspect --output atlas.html # Export a self-contained HTML file
datamitsu inspect --output - > atlas.html # Write HTML to stdout
datamitsu inspect --theme theme.json # Apply color overrides
datamitsu inspect --print-theme # Print the theme that would be applied

--output (-o) and --port are mutually exclusive, and so is --print-theme with either of them. The inspector loads the usual configuration chain and serves a fixed snapshot until restarted. Exported HTML works offline and on static hosts, with light/dark themes and hash-based links to views, filters, and selections.

--theme merges a JSON file of color overrides onto the built-in theme, token by token and mode by mode; an export bakes the result in. An unknown key or a value that is not a color is an error naming the full key path, and colors that fall below WCAG AA contrast against their surface produce a warning, not an error. --print-theme writes the merged theme — the complete built-in one when no --theme is given. See Config Inspector for navigation, artifact contents, theming, and publishing examples.

report​

Work with the reports fix, lint and check write with --report. None of these commands runs a tool.

report render​

Render a run's own JSON document (datamitsu.report/1) into a report format, offline:

datamitsu report render --input <run.json> --format <format> [--output <path>|-]
FlagDescription
--input <path>The own JSON document a run wrote with --report json=<path> (required)
--format <format>The format to write: json, markdown, sarif, junit, codequality, checkstyle or rdjsonl; a format's options follow a ? (required)
--output <path>Where to write it; -, the default, is stdout. Written atomically, like --report
--allow-partialRender a format that lists findings for a document of a narrowed run

The renderers and the completeness rule are the run's own: a document of a narrowed run is refused (exit 2) for a format that lists findings unless --allow-partial, and a document without its completeness fields is read as incomplete, never as complete — as is a tool an earlier build called complete although one of its processes failed while its parser found nothing (failed-without-findings). The document holds everything a renderer needs, so rendering works on another machine and after the checkout changed; json reproduces the document byte for byte, and every other format the file the run wrote, its completeness companion included, which is written beside --output and never on stdout. An --output that ends in / is a directory, for sarif; a document with more tools than one SARIF file holds is refused (exit 2) for a file or stdout. A document of another schema, or one that cannot be read, exits 1; an output that cannot be written exits 5.

# Print a run's report again
datamitsu report render --input out/run.json --format json

# The same run for a person
datamitsu report render --input out/run.json --format markdown --output out/run.md

# For code scanning, split into files of twenty tools
datamitsu report render --input out/run.json --format sarif --output sarif/

history renders too: it appends the document's line to --output.

report baseline​

Write the fingerprints of every finding a run reported as a baseline, datamitsu.baseline/1, which lint --baseline and check --baseline read:

datamitsu report baseline <run.json> [--output <path>|-]
FlagDescription
--output <path>Where to write the baseline; -, the default, is stdout. Written atomically

The baseline holds the fingerprint version (fingerprint: dmfp1), when it was made (createdAt, from SOURCE_DATE_EPOCH when set), the build and configuration of the run it was made from, that run's start, commit and ref, whether it was complete with the reasons of the run and its tools (source), and the sorted fingerprints of every finding of every operation, whatever its level; a tool that failed without findings adds none. A document of fingerprints of another version exits 2. A run that was not complete is taken with one WARN line: its baseline holds fewer fingerprints, which only suppresses fewer findings. A document that cannot be read, or of another schema, exits 1; an output that cannot be written exits 5.

datamitsu lint --report json=out/run.json
datamitsu report baseline out/run.json --output .datamitsu-baseline.json

report diff​

Compare the findings of two runs' own JSON documents by fingerprint, tool by tool:

datamitsu report diff <before.json> <after.json> [--format json|markdown] [--output <path>|-]
FlagDescription
--format <format>json (the default), datamitsu.diff/1, or markdown for a person
--output <path>Where to write it; -, the default, is stdout. Written atomically

Each document contributes the operation a report lists — lint, or fix for a fix-only run. A finding is:

ClassWhen
newonly the second run holds it
unchangedboth hold it, on the same row
movedboth hold it, on another row (beforeRow names the first run's)
fixedonly the first run holds it, and the second run covered the whole repository with the tool complete
unknownonly the first run holds it, and the second did not look everywhere: the tool's reasons say why
unobservedevery finding of a tool only one run holds (status: before-only or after-only)

The document counts each class per tool and in summary, and lists every finding with its fingerprint, level, rule, message, path and row. For two complete runs the counts are symmetric: what one direction calls new the other calls fixed. The command exits 0 whatever it finds; a document that cannot be read, or of another schema, exits 1, an unknown --format 2, and an output that cannot be written 5.

datamitsu report diff main.json branch.json --format markdown

config​

Configuration management commands.

config reconcile​

Reconcile project-owned configuration files declared under managedConfigs for the detected project types, then run datamitsu fix.

datamitsu config reconcile [flags]
FlagDescription
--dry-runPreview reconciliation without writing files or running datamitsu fix
--skip-fixReconcile managed files without running the post-reconciliation datamitsu fix
--opt-in-toolsAlso reconcile an all-disabled .datamitsuignore so tools are opt-in (enable them by removing names from the list)
--toolsComma-separated list of tools to scope reconciliation to (only their managed config files are considered)
--no-verify-hashSkip expectChainHash verification even when a pinned managed config drifted from its upstream chain

With no flags, the command creates, replaces, patches, links, or removes the managed files and then runs datamitsu fix. Use --dry-run to inspect the plan without either action, or --skip-fix when the files should be reconciled but formatters should not run. There are deliberately no --apply or --fix switches: writing and the post-reconciliation fix are the default workflow. There is also no general --force flag; the one narrow safety-check bypass is --no-verify-hash.

:::warning init and config reconcile are different operations

After editing datamitsu.config.*, run datamitsu init to provision apps, runtimes, bundles, .datamitsu links, and init commands. Run datamitsu config reconcile only when you explicitly intend to rewrite project-owned files declared under managedConfigs.

:::

With --tools, reconciliation is scoped to the named tools: it considers only the config files associated with them (via each managed config entry's tools field) and leaves everything else untouched — other tools' configs and unassociated infrastructure files (.gitignore, lefthook.yaml, pnpm-workspace.yaml, …) are skipped. The default post-reconciliation fix is scoped to the same tools; pass --skip-fix to omit it. This is the recommended way to iterate on a single tool's config without rewriting your whole project. Unknown tool names fail fast before anything is written; a selected tool that owns no config file is reported and skipped (not an error).

With --opt-in-tools, reconciliation also writes a .datamitsuignore at the git root that disables every configured tool via a single **/*: <tool names> rule. You then enable tools one at a time by removing their names from that list. The file is written before the default fix so that fix already respects the opt-in state. Reconciliation refuses to overwrite an existing .datamitsuignore; --dry-run previews the file without writing it. See Generating an all-disabled file.

An ejectable config the project has not ejected lives in .datamitsu/configs/, not in the repository: reconciliation removes its repository copy — and writes .datamitsu/configs/ so the post-reconciliation fix finds it — but only when the copy holds nothing the configuration would not render again. A copy with changes of the project's own is never deleted; reconciliation refuses before writing anything and names the file, to be kept by adding its tool to ejectConfigs or deleted by hand.

Any config file that pins expectChainHash is verified before reconciliation writes anything: if its upstream chain drifted from the pinned hash, reconciliation aborts with a report (the expected/actual hash and the full incoming content) and no files are written. Review the change against your overrides, update the pin, and re-run — or explicitly pass --no-verify-hash to write regardless. The check also runs during --dry-run.

Examples:

# Preview managed config changes without writing or fixing
datamitsu config reconcile --dry-run

# Reconcile managed files, then run fix (default)
datamitsu config reconcile

# Reconcile managed files without running fix
datamitsu config reconcile --skip-fix

# Reconcile configs and start with all tools disabled (opt-in model)
datamitsu config reconcile --opt-in-tools

# Regenerate only golangci-lint's config (e.g. .golangci.yml); nothing else is touched
datamitsu config reconcile --tools golangci-lint

# Preview a tool-scoped reconciliation without writing or fixing
datamitsu config reconcile --tools golangci-lint --dry-run

# Scope to several tools at once
datamitsu config reconcile --tools golangci-lint,prettier

config show​

Display the current configuration as JSON.

datamitsu config show

config types​

Display the TypeScript type definitions file (config.d.ts).

datamitsu config types

config runtime​

Display the full effective runtime configuration snapshot as JSON — env-resolved execution limits, the per-app install timeout, and the minimum release age. This is the introspection/debug surface: it reflects what the program runs with right now, including environment overrides. It is not the surface injected into the config JS VM (config JS receives only the minimal allowlisted datamitsuConfigInputs).

datamitsu config runtime

Examples:

# Inspect the full effective runtime config
datamitsu config runtime

# Mechanically verify a single value (and that env overrides apply)
datamitsu config runtime | jq .minimumReleaseAgeMinutes
DATAMITSU_INSTALL_TIMEOUT=1200 datamitsu config runtime | jq .installTimeoutSeconds # -> 1200

# The parse caps: bytes read from each output stream, findings kept per process
datamitsu config runtime | jq '{maxParseInputBytes, maxFindingsPerProcess}'

fix, lint and check refuse a DATAMITSU_MAX_PARSE_INPUT_BYTES or DATAMITSU_MAX_FINDINGS_PER_PROCESS that is not a positive integer with exit 2; config runtime reports the default in its place. See parse caps.

config chain-hash​

Print the XXH3-128 chain hash that datamitsu config reconcile verifies for managed config files — the hash of the content entering each file's root (topmost) config layer. Copy it into a managedConfigs entry's expectChainHash to pin the upstream baseline your overrides were written against, without having to trigger a drift error to read it.

datamitsu config chain-hash [file...]

The value is the input to the topmost layer, so declare your own entry for the file first (a placeholder expectChainHash is enough), then read the real hash here. With no arguments every managed config file is listed as file hash; with exactly one file only its hash is printed, ready to paste into a pin — expectChainHash accepts only the canonical xxh3:<32 lowercase hex> form.

Examples:

# List the chain hash of every managed config file
datamitsu config chain-hash

# Print just one file's hash (capture it into a variable / your config)
datamitsu config chain-hash eslint.config.mjs
pin=$(datamitsu config chain-hash eslint.config.mjs)

config lockfile​

Generate lock file content for a runtime-managed app (Bun/Node/UV/Go).

datamitsu config lockfile [appName]

Without arguments, lists all apps that use lock files. With an app name, creates the package-manager lock data from scratch and outputs it as a brotli-compressed, base64-encoded JSON string ready to paste into configuration. The command uses a special config-load path that permits the selected app's initially missing lockFile; every normal config load requires lock files for Bun, Node, UV, and Go apps.

Transitive dependencies are subject to the minimum release age: pnpm resolves Bun and Node apps with minimumReleaseAge, uv resolves UV apps with --exclude-newer P7D and records the window in the lock, and a Go app fails when any resolved module is younger than the effective minimum release age (DATAMITSU_MIN_RELEASE_AGE=0 skips that check). See Transitive dependencies.

Examples:

# List apps that support lock files
datamitsu config lockfile

# Generate lock file for a Bun or Node app
datamitsu config lockfile eslint

# Generate the go.mod + go.sum lock payload for a Go app
datamitsu config lockfile govulncheck

devtools​

Developer utility commands for maintaining datamitsu configurations.

devtools pull-releases​

Update binary configurations from release providers. Requires a file argument specifying the path to the binary apps JSON file. If the file doesn't exist, an empty appstate structure is created automatically. Fetches repository descriptions from the source API and stores them in the output JSON.

datamitsu devtools pull-releases <file>
datamitsu devtools pull-releases config/src/binaryApps.json
datamitsu devtools pull-releases config/src/binaryApps.json --update
FlagDescription
--updateFetch latest release tags before updating
--verify-extractionVerify that each downloaded asset extracts to an executable (ELF, Mach-O, PE or a #! script)
--min-age <minutes>Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age

By default, the command scans releases for all platform combinations using OS/Arch/Libc target tuples. For Linux, both glibc and musl variants are detected separately; an asset whose name says alpine or musl counts as a Linux build even without linux in it, so tool-alpine lands on linux/amd64/musl. The output JSON uses a nested three-level storage structure:

{
"binaries": {
"linux": {
"amd64": {
"glibc": { "url": "...", "hash": "...", "contentType": "tar.gz" },
"musl": { "url": "...", "hash": "...", "contentType": "tar.gz" }
}
},
"darwin": {
"arm64": {
"unknown": { "url": "...", "hash": "...", "contentType": "tar.gz" }
}
}
}
}

Add an optional top-level platforms array to the input manifest to restrict detection:

{
"sources": { "github": { "type": "github", "url": "https://github.com" } },
"platforms": ["darwin/arm64", "linux/amd64/musl"],
"apps": {},
"binaries": {}
}

Accepted identifiers are darwin/amd64, darwin/arm64, windows/amd64, windows/arm64, freebsd/amd64, freebsd/arm64, openbsd/amd64, openbsd/arm64, linux/amd64/glibc, linux/amd64/musl, linux/arm64/glibc and linux/arm64/musl. Values are case-sensitive and exact: there are no aliases or whitespace normalization. linux/amd64 is not accepted. An empty array, null, an invalid type or an unknown identifier fails before filtering or GitHub requests. Repeated identifiers are processed once.

When present, the array removes every unselected platform from existing binary entries before pulling releases, including entries without an apps record. This deletion is saved even if a subsequent pull fails. Only selected platforms are detected and, with --verify-extraction, downloaded and verified. A selected platform without a compatible asset is reported and omitted. The list filters available platforms; it does not require every app to publish every selected platform. An app fails if none of its selected platforms has a usable asset. A missing SHA-256 digest or failed verification still fails the app without saving a partial new entry. Explicit selections keep separate glibc and musl entries even when they share an asset.

The sorted, unique selection participates in configHash: changing the set reruns detection at the same tag; reordering it does not. Removing platforms restores default-all detection at the same tag. Without the field, existing all-platform behavior and libc deduplication are preserved.

Non-Linux platforms use unknown as the libc key. Without an explicit selection, a missing musl variant is simply omitted.

Before scoring, the detector filters out three categories of assets: checksum files (.sha256, .md5, .sha512, etc.), attestations published beside an asset (.proof, .sig, .asc, .minisig, .pem, .crt, .sigstore.json, .intoto.jsonl, .sbom, .spdx.json, .cdx.json) and non-executable package formats and installers (.vsix, .deb, .rpm, .nupkg, .whl, .msi, .msix, .appx, .pkg, .dmg, and a name in which setup or installer stands as a word of its own, such as Harper_2.11.0_x64-setup.exe — unless the app's own name says so). This prevents IDE extensions, package-manager bundles, a desktop app's installer or a signature file from outscoring — or standing in for — actual binaries when releases mix them.

When a release holds several programs — harper publishes harper-cli-*, harper-ls-* and the Harper desktop app together — the asset whose name carries the app's name as whole tokens (case and separators aside) outranks the others of the same platform, whatever their format. An asset that names the requested libc still ranks above it: a tool-alpine-* build is the musl build even when only tool-cli-linux-* carries the app's name.

The manifest has no schema version. Every app selects a named source and a repository path:

{
"sources": {
"github": { "type": "github", "url": "https://github.com" },
"codeberg": { "type": "forgejo", "url": "https://codeberg.org" },
"gitea": { "type": "gitea", "url": "https://gitea.com" },
"gitlab": { "type": "gitlab", "url": "https://gitlab.com" }
},
"apps": {
"tool": { "source": "github", "repository": "owner/tool", "tag": "v1.0.0" }
},
"binaries": {}
}

The repository and tag above are illustrative. Use the actual tag your source publishes. repository accepts nested GitLab namespaces. Supported type values are exactly github, gitlab, gitea and forgejo; Codeberg is a Forgejo instance, not a separate provider. url is the instance URL, including a self-hosted subpath when needed. Optional apiUrl overrides the derived API root (/api/v3 for GitHub Enterprise, /api/v4 for GitLab, /api/v1 for Gitea and Forgejo; public GitHub uses https://api.github.com). Source URLs require HTTPS; local loopback instances also accept HTTP.

An optional source tokenEnv names a third-party token environment variable. No token value is saved. It authorizes discovery API requests independently of artifact downloads. The optional downloadAuth field controls downloads:

ValueBehavior
auto (default)Resolve access separately for each asset from provider metadata and its endpoint. Public downloads have no auth. Restricted downloads require tokenEnv. Unknown access is an error with instructions to select an explicit policy.
requiredRequire tokenEnv and authorize downloads on the configured instance. External origins remain anonymous. This uses the provider's token scheme; it does not configure reverse-proxy Basic auth or SSO.
noneNever attach download credentials. Reject endpoints known to require auth; explicitly permit unknown endpoints anonymously. Discovery still uses tokenEnv.

For example, a source can specify "downloadAuth": "none" alongside a tokenEnv used only for metadata. Different aliases of one instance can choose different policies; app-level overrides are not supported.

GitHub reads explicit repository private metadata. Published public native assets retain browser_download_url; private assets and drafts use the API asset URL with an octet-stream Accept header. Gitea and Forgejo native release attachments use repository visibility and draft status, retaining their browser download URL with a Bearer token when restricted. Successful repository metadata with known visibility is cached for the current client; absent visibility is never silently treated as public.

GitLab reads visibility (public, internal, or private). It prefers the original release link target over the permanent redirect URL. Generic-package URLs for the same project use package_registry_access_level: public permits anonymous download, private requires auth, enabled follows project visibility, and disabled is an error. Missing package policy requires an explicit downloadAuth. Restricted permanent release URLs use the token-capable release API download route. Arbitrary same-origin links, including links to other projects, require an explicit policy. External links never inherit repository credentials, even when the project is private or the mode is required. See the GitLab package registry access rules and project API access levels.

Installation, checksum downloads, and --verify-extraction consume the same resolved download auth. Missing required tokens fail before the request. Cross-origin redirects drop credentials. There is no anonymous-to-authenticated fallback after HTTP errors. SHA-256 verification remains mandatory in every mode.

After upgrading from the initial multi-source implementation, run pull-releases again with the existing tags. The generation fingerprint invalidates old entries for all providers once, replacing public download auth and GitHub API asset URLs with anonymous download URLs. Changing downloadAuth also invalidates generated entries.

Integrity: GitHub API digests are used directly. GitLab generic-package links obtain SHA-256 from package-file metadata for the same project. A source without a published digest must use explicit per-app hashes (asset filename to SHA-256), or checksums (checksum asset filename to the checksum file's own SHA-256). Only candidate files for selected platforms resolve digests. Checksum files are downloaded only with their own validated SHA-256 pin; a binary is never downloaded to manufacture its expected hash. GNU SHA-256 lists and BSD SHA256 (filename) = hash records are supported. Conflicting records are errors.

Per-app pins apply by asset filename. When a new release needs those pins to resolve its digests, update them from independently trusted upstream metadata before using --update. With --verify-extraction, a stale matching pin fails verification and keeps the previous app entry. Without that flag, a manual hashes pin cannot be checked against the binary during pulling: the new tag and expected hash are recorded, and installation still refuses a mismatching download. Checksum-file pins are always verified when fetched. Pins are never refreshed by trusting unverified downloaded bytes. Native API digests permit automatic updates without maintaining per-app pins.

Breaking migration: rename your registry file to binaryApps.json, add sources, and replace each app's owner/repo with source/repository. Replace pull-github with pull-releases in scripts. The old command and old manifest shape are not accepted. Unknown fields, sources and repository shapes fail before pruning or network requests. configHash includes the source profile, repository, tag, integrity pins and sorted unique platform selection. Generated entries record their resolved provider, instance, repository and tag.

Examples:

# Detect binaries for the tags the file already pins (apps added or re-pinned by hand)
datamitsu devtools pull-releases config/src/binaryApps.json

# Update to latest releases
datamitsu devtools pull-releases config/src/binaryApps.json --update

# Update and verify that archives extract correctly
datamitsu devtools pull-releases config/src/binaryApps.json --update --verify-extraction

Failures, retries and the exit code:

Every app is attempted; one that fails does not stop the others. A request that fails for a transient reason — a network error, a timeout, a 5xx, a rate limit that names a short wait — is made up to four times with backoff, and each retry is printed with the app's request, the attempt and the reason. A 404, a release whose assets carry no digest, or a rate limit with no wait in sight is not retried. The run ends with a report naming every failed app, the stage it failed at (metadata, latest release, release <tag>, binaries for <tag>, verify <platform>) and the error, plus a hint when the cause is a GitHub rate limit, and exits with status 1 when any app failed.

With --verify-extraction, the download of each asset retries the same way. A platform whose asset turns out to be at fault — a hash mismatch, an archive that does not extract, a file that is not an executable — falls back to the next-ranked asset; a download that fails does not, since it says nothing about the asset, and a platform that still cannot be verified fails the app: its previous entry is kept, the report names the platform and the asset (verify linux/amd64/glibc), and the run exits with status 1.

The file is saved after each app that succeeds, and each save replaces the file whole. Every entry in it is therefore either the previous state of an app that failed or was not reached, or the complete new state of one that succeeded; a failed app never gets a partial new entry. The deletion of platforms excluded by platforms is applied and saved before any app pull, including pulls that fail. A save that fails stops the run, since nothing after it could be recorded either.

:::tip See also For a complete workflow including CI automation, see Maintaining Wrapper Packages — Binary Apps. :::

devtools pull-node​

Pull latest npm package versions from the npm registry. Requires a file argument specifying the path to the node apps JSON file. Descriptions are always fetched from the registry. If the file doesn't exist, an empty {} JSON file is created automatically.

datamitsu devtools pull-node <file>
datamitsu devtools pull-node config/src/nodeApps.json
datamitsu devtools pull-node config/src/nodeApps.json --update
FlagDescription
--updateUpdate versions in the JSON file with latest from npm
--dry-runShow results without writing to file
--min-age <minutes>Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age

Examples:

# Preview available npm updates without modifying files
datamitsu devtools pull-node config/src/nodeApps.json --dry-run

# Apply updates from npm registry
datamitsu devtools pull-node config/src/nodeApps.json --update

# After updating, regenerate lock files for affected apps
datamitsu config lockfile prettier
datamitsu config lockfile eslint

Registry requests that fail for a transient reason are made up to four times, each retry printed. With --update, a package whose entry changes is saved before the next one is looked up, so a later failure or an interrupted run keeps everything already pulled. A package that still fails keeps its previous entry; the run lists every failed package after the summary and exits with status 1. Without --update (or with --dry-run) the file is not written.

:::tip See also For the full node app update workflow including lock file regeneration, see Maintaining Wrapper Packages — Node Apps. :::

devtools pull-uv​

Pull latest Python package versions from PyPI. Requires a file argument specifying the path to the UV apps JSON file. Descriptions are always fetched from the registry. If the file doesn't exist, an empty {} JSON file is created automatically.

datamitsu devtools pull-uv <file>
datamitsu devtools pull-uv config/src/uvApps.json
datamitsu devtools pull-uv config/src/uvApps.json --update
FlagDescription
--updateUpdate versions in the JSON file with latest from PyPI
--dry-runShow results without writing to file
--min-age <minutes>Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). See Minimum Release Age

Examples:

# Preview available PyPI updates without modifying files
datamitsu devtools pull-uv config/src/uvApps.json --dry-run

# Apply updates from PyPI
datamitsu devtools pull-uv config/src/uvApps.json --update

# After updating, regenerate lock files for affected apps
datamitsu config lockfile yamllint

Registry requests that fail for a transient reason are made up to four times, each retry printed. With --update, a package whose entry changes is saved before the next one is looked up, so a later failure or an interrupted run keeps everything already pulled. A package that still fails keeps its previous entry; the run lists every failed package after the summary and exits with status 1. Without --update (or with --dry-run) the file is not written.

:::tip See also For the full UV app update workflow including lock file regeneration, see Maintaining Wrapper Packages — UV Apps. :::

devtools pull-runtimes​

Pull runtime configurations (Bun, Node, UV, JVM, Go, pnpm) with latest versions from upstream releases. Fetches latest releases from upstream, computes SHA-256 hashes, and writes the result to <file>.

datamitsu devtools pull-runtimes --update <file>
FlagDescription
--updateRequired. Fetch latest versions from upstream before updating
--dry-runShow what would be updated without writing files
--runtime <name>Update only the specified runtime (bun, node, uv, jvm, go, or pnpm)
--min-age <minutes>Minimum release age before a version is eligible (-1 = global default of 10080, 0 = disable, positive = custom). Applies to specific-version sources (GitHub releases, npm pnpm), not major-version-line lookups. See Minimum Release Age

The command detects binaries for all platform combinations (OS/Arch/Libc). For Linux, both glibc and musl variants are detected when upstream provides separate binaries. If a musl binary is identical to the glibc variant (same URL and hash), the musl entry is deduplicated.

Version sources:

  • bun: latest eligible Bun GitHub release; official archives and their SHA-256 digests from GitHub release metadata
  • node: latest Node.js LTS resolved automatically; archives + SHA-256 from nodejs.org/dist (glibc/darwin/windows, GPG-verified via SHASUMS256.txt.asc) and unofficial-builds.nodejs.org (musl)
  • pnpm: newest pnpm 12 release old enough for the minimum release age (judged by its npm publish date), with per-platform archive SHA-256 digests from the matching pnpm/pnpm GitHub release; the Node and Bun entries reference it with pnpmRuntime: "pnpm"
  • UV: Python stable from endoflife.date, UV binary from GitHub
  • JVM: Java version from Adoptium API, Temurin JDK from GitHub; when the newest feature release has no build old enough for the minimum release age yet, the previous feature release
  • go: latest stable Go release + per-file SHA-256 from go.dev (https://go.dev/dl/?mode=json); HTTPS with published SHA-256, no GPG (the git-pinned hash is the integrity anchor, same trust model as the musl Node path)

Examples:

# Update all runtimes
datamitsu devtools pull-runtimes --update config/src/runtimes.json

# Update only Bun runtime
datamitsu devtools pull-runtimes --update --runtime bun config/src/runtimes.json

# Update only UV runtime
datamitsu devtools pull-runtimes --update --runtime uv config/src/runtimes.json

# Update only Go runtime
datamitsu devtools pull-runtimes --update --runtime go config/src/runtimes.json

# Update only the pnpm runtime shared by Node and Bun
datamitsu devtools pull-runtimes --update --runtime pnpm config/src/runtimes.json

# Preview changes without writing
datamitsu devtools pull-runtimes --update --dry-run config/src/runtimes.json

Upstream requests that fail for a transient reason are made up to four times, each retry printed. Every runtime is attempted, pnpm first and then the rest in alphabetical order, and a runtime whose entry changes is saved before the next one starts, so a later failure or an interrupted run keeps everything already pulled. A runtime that fails keeps its previous entry — so does a Node or Bun entry that would name a pnpm runtime the file does not define — and the run lists every failed runtime with its error and exits with status 1. A save that fails stops the run.

:::tip See also For the full runtime update workflow and CI automation, see Maintaining Wrapper Packages — Runtimes. :::

devtools dockerfile​

Generate an optimized, digest-pinned multi-stage Dockerfile from the loaded config. Emits a config-free base, a config-split stage, then one build stage per binary app, per managed runtime, and per runtime-managed app (each inheriting its runtime stage), then assembles the populated store with COPY --link — one cacheable layer per app, so bumping one app re-pulls only its layer. The base never carries the config and each stage loads only its own slice (see devtools split-config), so editing or regenerating the config rebuilds only the stages whose slice actually changed instead of every tool.

datamitsu devtools dockerfile -o docker/Dockerfile
FlagDescription
-o, --output <path>Required. Output Dockerfile path including filename, fully overwritten on each run
--alpineTarget the Alpine (musl) base image variant (<version>-alpine)
--offlineSkip base-image digest resolution; emit an unpinned FROM with a warning
--no-verifyDo not run apps' version checks in their build stages (verification is on by default)
--config-js <path>Pre-built config file COPYed into the image (default datamitsu.config.js)
--repo <repo>Override the base image repository (default: the repo this datamitsu build was published under)
--label <key=value>OCI label for the final image (repeatable)
--arg <name[=val]>Build ARG declared before ENV in the final stage; bare name or name=default (repeatable)
--build-arg <n[=v]>Build-time ARG→ENV in a dm-build stage so install stages inherit it; not in the final image (repeatable)
--env <key=value>ENV var baked into the final image (repeatable)
--force-include <apps>Keep binary apps that lack a binary for the target libc (comma-separated, repeatable)
--emit-oci-map <path>Also write the layer→subtree map JSON consumed by the OCI bundle annotation post-process

libc filtering. The generated image targets one libc — musl with --alpine, glibc otherwise — and binary apps are filtered to those that ship a binary for it on every arch they declare. A glibc-only binary can't execute on a musl image (and vice versa), so incompatible apps are dropped from the Dockerfile and listed in a warning. Runtime-managed apps (bun/node/uv/jvm/go) are unaffected — their runtime carries the libc. Statically-linked tools that run on any libc but are under-declared in the registry (e.g. a static Go binary recorded as glibc-only) can be added back with --force-include name1,name2.

The base image repository and tag are baked into the datamitsu binary at release time, so the FROM points at the exact image this build came from — including across release channels, where the stable image (datamitsu/datamitsu:<version>) and the unstable image (datamitsu/datamitsu-unstable:<unstable-tag>) live in different repositories under different tags. The registry host defaults to ghcr.io (override with DATAMITSU_OCI_REGISTRY); --repo overrides the repository for mirrors or forks. The tag is resolved to a SHA-256 digest and pinned as FROM …@sha256:…. Pinning is best-effort and never fails the command: --offline, an unreachable registry, or a non-release (dev/unstable) build leave the FROM unpinned with a warning. The output file is fully overwritten (no managed regions).

Examples:

# Generate the glibc and Alpine variants
datamitsu devtools dockerfile -o docker/Dockerfile
datamitsu devtools dockerfile -o docker/Dockerfile.alpine --alpine

# Offline (no digest pinning) — useful for CI drift checks
datamitsu devtools dockerfile -o docker/Dockerfile --offline

# Resolve the base digest from a mirror registry
DATAMITSU_OCI_REGISTRY=mirror.internal datamitsu devtools dockerfile -o docker/Dockerfile

:::tip See also For the full image-publishing workflow and CI drift guard, see Maintaining Wrapper Packages — Generating a Docker image. :::

devtools split-config​

Write one minimal config slice per app and per runtime into a directory. Each slice is a self-contained config defining exactly one stage's target — an app with its transitive dependencies and all referenced runtimes (including pnpm), or a runtime with its pnpm reference — that install --config can load on its own.

datamitsu devtools split-config -o ./slices
FlagDescription
-o, --output <dir>Required. Output directory for the config slices

This is the build-cache primitive behind devtools dockerfile: the generated Dockerfile runs it in the config-split stage so every other stage loads only its own slice. Editing an app changes its slice and those of its dependents, invalidating their build caches instead of busting the whole image. You rarely run it directly; it is documented because it appears in the generated Dockerfile. The config is read from the usual sources (--config / --before-config / auto-discovery).

devtools verify-all​

Cross-platform config integrity checker. Downloads and hash-verifies binary apps and managed runtimes for all configured platforms. Every single file it extracts (all but extractDir archives) must be an executable: an ELF, Mach-O or PE file, or a script starting with #!.

datamitsu devtools verify-all
FlagDescription
--no-version-checkSkip version command execution and comparison
--concurrency <n>Concurrent download workers (default: DATAMITSU_CONCURRENCY or 3)
--jsonOutput machine-readable JSON
--skip-passedSkip checks whose config is unchanged and passed last run
--no-remoteSkip loading remote configs

devtools pack-inline-archive​

Pack a directory into a brotli-compressed tar archive for use in inline archive configs.

datamitsu devtools pack-inline-archive <directory>

Output is written to stdout in tar.br: format. Archives are deterministic.

devtools apps list​

List all configured apps with their type, version, description, and install status. For binary apps, the resolved target (including libc variant) is determined by the current host's target detection. This helps verify which binary variant would be selected on the current system.

datamitsu devtools apps list

devtools apps inspect​

Show install path and file tree for an installed app. For binary apps, the inspect output reflects the resolved target for the current host, including any libc fallback that may have occurred during resolution.

datamitsu devtools apps inspect <name>

devtools apps path​

Print the install directory path for an app.

datamitsu devtools apps path <name>

devtools bundles list​

List all configured bundles with name, version, and install status.

datamitsu devtools bundles list

devtools bundles inspect​

Show install path and file tree for a bundle (collapses heavy directories).

datamitsu devtools bundles inspect <name>

devtools bundles path​

Print the install directory path for a bundle.

datamitsu devtools bundles path <name>

devtools parsers​

Inspect the WASM output-parser modules declared in the parsers config and the tools they can parse. Each module self-describes (via its WASM describe export) which tools it parses, how to invoke each, its upstream URL, and its build-injected version — so this is the source of truth, not the config.

list aggregates every configured parser into a deduplicated catalog (a module declared by N tools is described once); inspect shows the full detail for one tool. A module built with descriptor schema 2 or later also says, per tool, which level words the tool prints (levels, none when it prints none), the unit it counts columns in where that was measured (columns), and whether it is a security scanner (category); an older module shows none of these lines, which is how to tell which contract a pinned module carries (see levels). A module built with descriptor schema 3 also lists its format parsers and the fallback sniffer, each marked format (format parsers). A module older than schema 3 is named once on stderr with a note: line, as datamitsu config show names one already in the store (older modules). Both accept:

  • --json — machine-readable output, for driving configs or build pipelines.
  • --wasm <path> — describe a local .wasm file directly, with no config or network access (handy in CI and release tooling).
  • --embedded — describe the fallback parser module built into this binary (format parsers and the fallback sniffer only), with no config or network access (the embedded fallback).
# Human-readable catalog of all tools the configured parsers can parse
datamitsu devtools parsers list

# Machine-readable catalog (tools is [] when none are configured)
datamitsu devtools parsers list --json

# Full detail for one tool
datamitsu devtools parsers inspect hadolint

# Describe a locally built module without any config
datamitsu devtools parsers list --wasm ./parsers/target/wasm32-unknown-unknown/release/datamitsu_parsers.wasm

run pipes a tool's raw output (from stdin) through its parser and prints the module's answer as JSON — abi, whether it recognized the output, the format it read, and the structured diagnostics — the quickest way to develop or debug a parser against real output. A module that answers with a bare array (ABI 1) is shown in the same shape, with recognized inferred as the core infers it. It reads stdout from stdin; pass --stderr-file / --exit-code for parsers that use them, and --wasm <path> to use a local module instead of a configured one.

# Run eslint through datamitsu, then parse its JSON into diagnostics
datamitsu exec eslint -- --format json file.js \
| datamitsu devtools parsers run eslint --wasm ./datamitsu_parsers.wasm --exit-code 1

# Run a format parser of the fallback built into this binary
datamitsu exec shellcheck -- -f checkstyle script.sh \
| datamitsu devtools parsers run checkstyle-xml --embedded --exit-code 1

sniff [<file>|-] runs the sniffer of the built-in fallback over a captured output (a file, or stdin with - or no argument) and prints the standard format it recognized and the findings it read, or no standard format recognized. It is how to check which format key a tool's flag produces before declaring it as outputParser.parser. --stderr-file and --exit-code pass the tool's other stream and exit code; --json prints the whole answer.

ruff check --output-format sarif src > out.sarif
datamitsu devtools parsers sniff out.sarif
# format: sarif (1 finding)
# src/a.py:1:8 error [F401] `os` imported but unused

prefetch downloads and SHA-256 verifies the configured parser modules into the store, without compiling or running anything. Each module lands at its content-addressed store path, so it can be COPYed into an OCI-bundle layer (the generated Dockerfile's parser stages do exactly this) and found offline later. A module already on disk whose bytes still match the declared SHA-256 is a no-op — and one whose bytes do not is discarded and fetched again, whatever put it there.

# Fetch every module declared in the parsers config
datamitsu devtools parsers prefetch

# Fetch only a subset
datamitsu devtools parsers prefetch datamitsu-parsers

devtools tools​

Inspect the tools declared in the config — the fix/lint units datamitsu plans and runs. list shows each tool's operations, project types, the app it runs, and its output parser; inspect shows full per-operation detail. Both accept --json.

# All configured tools: name [operations] (projectTypes) → parser
datamitsu devtools tools list

# Machine-readable
datamitsu devtools tools list --json

# Full detail for one tool (per-operation app/scope/globs/args)
datamitsu devtools tools inspect eslint

Troubleshooting devtools commands​

File not found errors:

If the JSON file argument doesn't exist, pull-releases, pull-node, and pull-uv create an empty file automatically. However, pull-runtimes requires the --update flag to write — running without it produces an error.

GitHub API rate limits:

Without a token the GitHub API allows 60 requests an hour, and a registry pull of a few dozen apps uses more. A rate limit that asks for a short wait (a Retry-After header, or a reset within two minutes) is waited out and retried; one that resets later is reported at once, with the reset time, and the run goes on with the next app and exits non-zero. Set the GITHUB_TOKEN environment variable to authenticate and raise the limit:

export GITHUB_TOKEN=ghp_your_token_here
datamitsu devtools pull-releases config/src/binaryApps.json --update

Hash mismatches:

If verify-all reports a hash mismatch, investigate the changed upstream artifact before changing the expected hash. For release manifests, update manual hashes or checksums pins only from independently trusted upstream metadata. For a re-released tag using native API digests, clear the affected generated entry's configHash to force discovery while preserving its archive-path history. An unchanged tag and config hash skip discovery; --update alone does not refresh manual pins. Then pull and verify again:

datamitsu devtools pull-releases config/src/binaryApps.json --verify-extraction
datamitsu devtools verify-all

Network errors:

Release and package devtools require network access to their configured forge, npm or PyPI. If you're behind a proxy, ensure HTTPS_PROXY is set. For intermittent failures, retry the command — downloads are idempotent.

cache​

Manage the per-project cache for linting and fixing operations.

cache clear​

Clear cache data.

datamitsu cache clear
FlagDescription
--allClear all project caches (not just the current project)
--dry-runShow what would be deleted without deleting

:::warning This removes project source-mode farms

A project's source-mode farm and manifest live in the same per-project cache directory, so cache clear deletes them along with the lint/fix caches. Every shell already activated for that project then reports exit 127 for every declared tool — the shim deliberately does not re-bake a farm that is not there. Run datamitsu source refresh (or re-activate) afterwards, and use --dry-run first to see what would go.

Machine-level farms created with datamitsu source --config <path> live in a separate configs/ namespace and are not removed by cache clear, not even with --all. Rebuild one deliberately with datamitsu source refresh --config <path> --force.

:::

cache clear also drops this project's cached config evaluations ({cache}/config-eval/projects/{hash}); --all drops the whole config-eval tree, including the machine-level --config chains whose farms survive. Clearing them costs one re-evaluation and nothing else.

cache path​

Print the absolute path to the global cache directory.

datamitsu cache path

cache path project​

Print the absolute path to the current project's cache directory.

datamitsu cache path project

store​

Manage the global binary and runtime store.

store path​

Print the absolute path to the global store directory.

datamitsu store path

store clear​

Remove the entire global store directory, including binaries, runtimes, app environments, bundles, parsers, package-manager data, managed Python installations, and remote configs.

datamitsu store clear
warning

This removes all downloaded and installed managed content. You will need to run datamitsu init again (and re-run lazy apps on demand) afterward.

store seed​

Pull the OCI bundle declared by the config's top-level oci key into the global store — no docker/podman required. Without arguments the whole bundle is pulled (airgap seeding); with --apps only the layers of the named tools plus their runtime dependencies are pulled.

# Full pull from the config's oci declaration
datamitsu store seed

# Selective pull
datamitsu store seed --apps golangci-lint,prettier

# Explicit digest-pinned reference (overrides the config)
datamitsu store seed ghcr.io/owner/tool-store@sha256:0123…cdef
FlagDescription
--apps <list>Seed only the layers of these apps and their runtime dependencies (repeatable)
--resolve-tagAllow a :<tag> reference: resolve it to a digest first and print it for pinning

Every manifest body and blob is verified against its SHA-256 descriptor before extraction; seeded single-file binaries and JVM jars are additionally re-hashed against the published hashes from the config. A repeated full pull is a no-op (zero network requests).

Blob downloads have no overall timeout — only an attempt that delivers zero bytes for 2 minutes is aborted and retried, so large layers on slow links finish instead of being cut off mid-stream. The store commands also work without a usable git context (no git binary, dubious ownership in containers): a broken project repo only skips the project-level config with a warning.

store status​

Show what the declared bundle contains for this platform and which configured apps it covers versus which still require the network.

datamitsu store status
datamitsu store status --json

store refs​

List every artifact the effective config fetches from outside the machine: OCI references (the bundle and any registry-sourced parser modules) first, then the hash-pinned https downloads. It answers "what do I have to mirror?" and touches no network, so it runs inside a firewall.

# Everything this config pulls from anywhere
datamitsu store refs

# Just the registry side — the input to a mirroring loop
datamitsu store refs --oci-only | xargs -n1 -I{} crane copy {} harbor.corp/dm/{}

# Every artifact with its mandatory hash and platform
datamitsu store refs --json | jq '.https[] | select(.kind == "app-binary")'
FlagDescription
--jsonEmit the references as JSON, including each artifact's mandatory hash
--oci-onlyList only OCI references, omitting the hash-pinned https downloads

The output is sorted and byte-stable, so it is safe to pipe or diff between config revisions. Managed runtime binaries declared by the effective config, including the native pnpm archives of the pnpm runtime, are included. Package-manager dependencies fetched later by pnpm, UV, or Go are not individual entries; their exact versions and integrity data live in each app's mandatory lock file.

store import​

Seed the store from a local OCI image layout directory (as produced by crane pull --format=oci, oras copy --to-oci-layout, or skopeo copy) — fully offline bundle transfer. Blobs are verified against the digest chain exactly like a registry pull.

datamitsu store import ./bundle-layout
datamitsu store import ./bundle-layout --digest sha256:0123…cdef

version​

Print the version number.

datamitsu version

completion​

Generate a shell-completion script for Bash, Zsh, Fish, or PowerShell.

datamitsu completion bash
datamitsu completion zsh
datamitsu completion fish
datamitsu completion powershell

The command writes the script to stdout. Run datamitsu completion <shell> --help for the installation instructions for that shell.

llms​

Print this documentation as plain markdown, for feeding to an AI agent.

The docs are compiled into the binary, so they always match the version you are running and are served without network access. Content goes to stdout and diagnostics to stderr, so the output pipes straight into a model's context.

With no arguments it prints an index of every page, where each entry is the exact argument that fetches it — an agent can read the index and choose its next call without knowing anything about the site's URLs.

# Index of every available page
datamitsu llms

# One page, by its documentation path
datamitsu llms about
datamitsu llms contributing/brand-guidelines

# A unique page name may be abbreviated
datamitsu llms brand-guidelines

# A directory's own page is addressed by the directory
datamitsu llms getting-started/installation

# A URL copied from the website works too
datamitsu llms https://datamitsu.com/docs/guides/architecture/caching.md

Flags:

FlagDescription
--listList every page slug, one per line
--jsonEmit JSON instead of markdown
--webPrint the standard llms.txt index, with website URLs
# Machine-readable page list, with titles and descriptions
datamitsu llms --list --json

# Provenance of the embedded snapshot
datamitsu llms --json | jq .pageSetHash

# One page as JSON, body included
datamitsu llms about --json

Exit codes: 0 on success, 2 for a usage error, 3 for an unknown or ambiguous page. An unknown page suggests the nearest matches on stderr and leaves stdout empty, so a failed fetch never pollutes a pipe.

lsp​

Run a Language Server Protocol server on stdin/stdout, so an editor can format files through datamitsu's configured tools.

datamitsu lsp

This is normally launched by an editor rather than by hand — see the VS Code extension for the packaged integration.

The server answers document formatting and nothing else. It serves one repository, which it takes from the editor at initialize (see lsp root), and it picks up a changed configuration by itself, with no restart (see lsp configuration reload).

A format request runs the project's fix tools on the real file, in place, so a format also saves the file. A buffer with unsaved changes is written to disk first, and the response holds the edits from that buffer to the fixed result. That write goes through a temporary file, with the original's permissions, renamed over the original, so it never leaves a truncated file behind, and it replaces the file a symlink points to, not the symlink. A buffer that already matches the file on disk gets an empty edit list: the fix is only on disk, and the client has to reload the file. VS Code does so by itself. In Neovim, run :edit after formatting; until then the buffer still shows the text from before the format.

stdout carries only JSON-RPC. stderr carries only JSON-L events, whatever --log-format says: status, progress, and log lines alike. --verbose sets the log level to debug, so datamitsu's info and debug lines join the stream as log events, never as plain text.

lsp root​

One server serves one repository for its whole life. At initialize it starts from the first of these that the editor sends:

  1. the first entry of workspaceFolders;
  2. rootUri;
  3. rootPath;
  4. none of them: the directory the server was started in.

From there it resolves the repository root the way the CLI does — the git root, climbing out of a submodule to its topmost superproject — changes into that root, and loads the configuration there. The initialize result names the root in capabilities.experimental.datamitsu.root (see the echo). Relative --config and --before-config paths are resolved once, against the directory the server was started in, before it changes directory.

File URIs are percent-decoded, and on Windows file:///C:/x is read as C:\x. The root and every document path are compared after resolving symlinks, so a workspace opened through a symlink formats like any other. A document outside the served repository is not formatted: the server answers with an empty edit list before planning or installing anything, and logs one info notice per document per session.

The first workspace folder decides the repository, and every folder inside that repository is served with it. For each other folder whose repository differs, the server logs one info notice saying it does not format that folder. The server does not advertise workspace-folder support, so an editor does not add folders to a running server: one that wants several repositories formatted starts one server for each.

Neither a missing repository nor a configuration that does not load stops the server or fails initialize. initialize succeeds, one log event at level error names the cause, and the session serves nothing: a format request gets an empty edit list rather than an error, so the editor does not raise one on every save, and the error is repeated once per distinct cause rather than on every request. Once the configuration is fixed, the next format request loads it.

lsp configuration reload​

The server picks up a changed configuration by itself. At the start of every format request — before planning, because formatting the config file itself changes it — it compares the contents of the configuration's inputs with what it read at the last load:

  • every file of the configuration chain: the discovered datamitsu.config.*, the configs it layers on (a shared config package in node_modules included), and any --config or --before-config file;
  • the checkout's HEAD (.git/HEAD, or a linked worktree's own), so a branch switch counts;
  • pnpm-lock.yaml at the root, so an install that bumps a shared config package counts;
  • every auto-config file name at the root, present or not, so a second datamitsu.config.* appearing counts.

A server started with --no-auto-config discovers no config at the root, so it compares only the files of its --config chain.

It compares content, not modification times, so an edit within the same second counts, and a file reached through a symlink is compared by what it points to. A file that appears or disappears counts as a change too. When nothing changed, the request proceeds. When something did, the server loads the configuration again:

  • It loads. The server replaces everything it built from the old configuration — tools, planner, execution cache (the old cache is flushed first) — and resolves the initializationOptions the editor sent at initialize again, so a format.tools entry for a tool the new configuration adds now applies. It logs configuration reloaded at info, followed by the policy line, and logs the option warnings again.
  • It fails. Saving a half-edited config is the normal case. The session keeps the configuration that last loaded, logs one warn saying the configuration changed but failed to load, with the cause, and keeps formatting with the previous configuration. The same content is not loaded or reported again on the next save; only a further change is.
  • No session yet, because the configuration did not load at initialize: a successful load starts one.

A reload writes nothing: it does not run datamitsu init. When the new configuration changes an ejectable config — what it renders to, or whether the project ejects it — a format that would run a tool reading that config fails the managed-config preflight until the files match the configuration again. The error names the command to run: datamitsu init for a changed render. A tool the new configuration adds is installed by the first format that needs it, like any other. The repository the server serves, its command-line flags and its environment stay as they were when it started.

lsp format policy​

A format request plans fix for the saved file, then decides for each operation whether it runs from the editor. The first matching rule wins; the reason is what the server logs for a tool it leaves out:

RuleResultLogged reason
The operation has lsp: falseLeft outlsp: false in config
format.tools[name] is falseLeft outdisabled by format.tools
Granularity repoLeft out, whatever format.tools saysrepository-wide, never on save
Granularity unit, a project without the saved fileLeft outNot logged
Granularity unit, no globs, no opt-inLeft outdeclares no globs, so it cannot tell which files it formats
Granularity unit, level below unitLeft outproject-wide, editor policy is target or project policy is target
Granularity unit, otherwiseRuns over the saved file's project—
Granularity fileRuns on the saved file—

A unit operation that declares no globs is planned for every file of its project, so it would claim every save there, whatever the file's type: saving a TypeScript file that lives in a Go module would run a glob-less golangci-lint fmt over the whole module. It is left out unless format.tools[name] is true. An author fixes it at the source by adding globs (such as ["**/*.go"]), so only the files it formats bring it in. A tool whose result for one file does not depend on its neighbours can also take {files} and declare granularity: "file", so a save runs it on the saved file alone.

The level of a unit operation is the project's execution.widenTo.fix when format.tools[name] is true, and otherwise the narrower of that and format.widenTo. An opt-in therefore lifts the session's target, never the project's. lsp: true on an operation is the same as leaving it unset. A tool the policy leaves out is never downloaded.

format.timeoutMs is a watchdog, not a deadline. The plan runs one priority group at a time, and once the limit has elapsed no further group starts. The first group always runs, a running tool is never interrupted, and downloads and installs happen before the clock starts. When it fires, a warning names the file, how many groups ran, and the tools that did not; the response still reflects what is on disk. A cancel, unlike the watchdog, can stop a format before its first group: see lsp cancellation.

lsp cancellation​

The server keeps reading messages while a format runs, so it sees a $/cancelRequest at once. Requests still run one at a time, in the order they arrived. Document changes that arrive during a format are applied as they come in, and a format request works on the text the document had when the request arrived.

A cancelled request is always answered with RequestCancelled (-32800), never with an error an editor would show. A request still waiting its turn never runs. A running format stops at its next checkpoint: at the start, after planning, after the managed-config preflight, after its tools are installed, before the unsaved buffer is written, and before each priority group, the first one included. A cancel never interrupts a running tool: killing a formatter that rewrites a file in place can leave that file truncated. It does not abort a download or install already under way either — the next save needs that tool anyway — so the request stops once it is done.

What a cancelled format leaves on disk depends on when the cancel arrived:

The cancel arrivesThe file on disk
Before the unsaved buffer is writtenUnchanged; the unsaved text stays in the editor only
After the buffer is written, before the first groupThe unsaved text, unformatted
While a priority group runsFixed by that group and every group before it; no later group runs

Each tool_run that started still gets its closing event, since the tool ran to completion. One info notice names the file and, once a tool group has run, how many ran and the tools that did not; a cancel before the first group only says that no tool ran. done closes the request with status: "fail", success: false and msg: "cancelled". The results of the tools that ran are cached as usual.

Stopping the server uses the same checkpoints:

  • shutdown — the running format stops at its next checkpoint, every format request still waiting is answered RequestCancelled, and any other request still waiting is handled as usual, so a queued initialize still initializes. shutdown is answered once nothing runs. Requests after it are answered InvalidRequest (-32600). exit ends the process: status 0 after shutdown, 1 without it.
  • stdin closes, or its framing breaks (a header without a valid Content-Length, for example) — the running format stops at its next checkpoint, waiting requests are dropped without a reply, the execution cache is flushed, and the server exits. A message whose body is not valid JSON is not a framing error: it is answered with a parse error, and the server keeps serving.
  • SIGTERM or an interrupt — the first one does the same as stdin closing. A second one stops the running tool as well and exits at once, which can leave a file that tool was rewriting incomplete.

On Unix the server ignores SIGPIPE, so an editor that goes away in the middle of a format cannot kill the server through a closed stdout or stderr.

lsp initialization options​

The server reads initializationOptions at initialize and keeps them: a configuration reload resolves them again against the new configuration.

interface DatamitsuInitializationOptions {
format?: {
widenTo?: "target" | "unit";
timeoutMs?: number; // non-negative integer; 0 disables the watchdog
tools?: Record<string, boolean>; // keyed by tool name in the config's `tools`
};
}

Each key is resolved on its own, highest first: initializationOptions, then the environment, then the default.

KeyValuesEnvironment variableDefault
format.widenTotarget or unitDATAMITSU_LSP_FORMAT_WIDEN_TOunit
format.timeoutMsMilliseconds, integer >= 0; 0 = offDATAMITSU_LSP_FORMAT_TIMEOUT_MS15000
format.toolsTool name to true or false—{}
  • widenTo — repo is refused: a repository-wide fix never runs on save. The environment variable set to repo or to an unknown value means unit.
  • timeoutMs — a negative or unparsable environment value means 15000.
  • tools — false keeps a tool out. true runs a project-wide tool under widenTo: "target", or one that declares no globs, but never beyond the project's execution.widenTo.fix, never for a repository-wide tool, and never against lsp: false.

A bad option never fails initialize. A value of the wrong type, a widenTo of repo, a negative or fractional timeoutMs, a non-boolean tools entry, a tools entry naming no configured tool, or an unknown key inside format is ignored with a warning: a rejected widenTo or timeoutMs falls back to the environment or the default, and a rejected tools entry has no effect. A tools entry naming a tool the configuration does not have yet applies once a reload adds that tool; the warnings are logged again with each reload. Unknown top-level keys are ignored silently.

The initialize result echoes the policy the session runs with and the repository it serves, under capabilities.experimental. tools holds the entries the server accepted, and root is the resolved repository root, absent when the server found none:

{
"capabilities": {
"experimental": {
"datamitsu": {
"format": { "widenTo": "unit", "timeoutMs": 15000, "tools": {} },
"root": "/work/example"
}
}
}
}

The echo is not repeated after a reload; the policy line the reload logs is the current one.

In Neovim 0.11 or later, init_options is sent as initializationOptions:

vim.lsp.config("datamitsu", {
cmd = { "datamitsu", "lsp" },
root_markers = { "datamitsu.config.ts", "datamitsu.config.mjs", "datamitsu.config.js" },
init_options = {
format = { widenTo = "target", timeoutMs = 5000, tools = { eslint = false } },
},
})
vim.lsp.enable("datamitsu")

Neovim sends the directory root_markers finds as the workspace folder, and the server serves that directory's repository, wherever Neovim itself was started. When no marker is found, Neovim sends no folder, and the server falls back to the directory it was started in.

lsp events​

Every stderr line is one JSON object carrying type and op_id, with --verbose or without. The stream opens with a hello event, as a --log-format jsonl stream does (see Run events). The language server never emits diagnostic events: an editor gets findings through publishDiagnostics. It emits:

typeMeaning
logA human-readable notice in msg, with level debug, info, warn or error
phaseop: "format", status: "start": opens each format request the server starts running; no phase event closes it
download, installTool provisioning progress
tool_runA tool task starting and ending: tool, dir (relative to the git root), success, duration_ms
errorA tool task that failed: tool, dir, msg
doneCloses the request's phase, under its op_id, failed and cancelled requests included: status done or fail

done also carries op: "format", success, duration_ms, tools, runs, failed and skipped. tools counts the distinct tools that ran and failed the distinct tools with a failed task; runs counts the tasks that ran and skipped the tasks the policy, the watchdog or a cancel left out. One tool failing in two projects therefore gives runs: 2 and failed: 1. A counter or duration_ms that is zero is left out of the line rather than written as 0: read a missing one as zero. A tool_run op id is the format request's op id followed by :<tool>:<dir>:<seq>, seq numbering the tasks of one priority group from 1, so a consumer can attribute every task to its request. Every task that starts also gets its closing tool_run, in a cancelled request too. A cancelled request's done has status: "fail", success: false and msg: "cancelled": there is no separate status for it.

log events carry the session's own notices:

  • At initialize: one info with the effective policy (format policy: widenTo=unit timeoutMs=15000 tools={}), naming the keys that came from initializationOptions; one warn per rejected option; one info for each other workspace folder in a different repository; and an error when there is no repository or the configuration does not load.
  • Per format request: one info listing the tools the policy left out and why, when there are any. A warn also marks a save the watchdog stopped, and an info a cancelled one. A document outside the served repository gets one info, once per session. With no session, the error from initialize is repeated, once per distinct cause.
  • On a configuration change: configuration reloaded at info, followed by the policy line, with the option warnings logged again; or one warn when the changed configuration fails to load.

Once per session, and once more after each reload, a cache on disk that the session will not write is reported too. Written by a different datamitsu version — the editor runs another binary than the CLI — it is an info: the two simply do not share cache entries. Written for a different configuration — for example by a run with --tools, or while a changed configuration fails to load in the session — it is a warn. Formatting works in both cases; it just does not warm the cache.

Log lines from the rest of datamitsu — config loading, tool provisioning — are log events too, each with its own op id, at its own level: warn and error by default, debug and info as well with --verbose or a lower DATAMITSU_LOG_LEVEL. msg carries the line's message followed by its fields.

source​

Put the project's declared toolchain on PATH for the current shell, so every tool runs as an ordinary command at the version this repository pins.

# bash / zsh
eval "$(datamitsu source bash)"
eval "$(datamitsu source zsh)"

# fish
datamitsu source fish | source

Activation downloads nothing — a tool is fetched the first time it is actually run. Switching branches needs no re-activation: the next tool invocation notices the config changed and re-resolves itself.

source requires a project. Run inside a repository whose git root holds a datamitsu.config.{js,mjs,ts}, or name one explicitly with the global --config flag; outside a project it exits non-zero and writes nothing to stdout.

stdout carries shell code and nothing else, so the output is always safe to eval. Warnings — tools not downloaded yet, system binaries this shadows — go to stderr.

--no-auto-config without a --config is refused: activating the built-in default config would bake a handful of built-in apps into the farm this root's real config owns. --before-config and --no-auto-config also bypass the manifest fast path and re-resolve the config, and a farm baked with them is not reused by a later plain datamitsu source — a manifest baked from a different config chain would otherwise activate a toolchain the plain invocation never asked for.

If the farm cannot be re-baked at all — a config that does not evaluate on this branch, a remote config unreachable offline — activation warns on stderr, activates the farm already on disk and exits 0. Emitting nothing would exit 0 with a shell that was never activated, and every declared tool would then fall through PATH to whatever the system has. source refresh is the repair command and inverts this: a failed bake exits non-zero, leaving the previous farm in place, so a CI step can trust refresh's exit code but not source's.

source --config: a machine-level toolchain​

# outside any repository, e.g. from a shell rc file
datamitsu source fish --config ~/.config/datamitsu/datamitsu.config.ts | source
eval "$(datamitsu source bash --config ~/.config/datamitsu/datamitsu.config.ts)"

With the global --config flag the toolchain comes from a config you name rather than from a project, so source works outside any git repository. status and refresh take the same flag and act on the same farm.

Such a farm's origin is explicit-config, and it differs from a project farm in two ways:

git-rootexplicit-config
identitythe git rootXXH3-128 of the resolved config chain
farm path{cache}/cache/projects/{hash}/{cache}/cache/configs/{hash}/
watch setconfig chain + .git/HEADthe config chain
shimre-resolves from cwd's git rootrevalidates its own recorded chain, never walks up for .git

The chain is resolved to absolute paths with symlinks followed before hashing, so a relative, an absolute and a symlinked spelling of the same file are one farm. Order is significant, and --before-config files are part of the chain rather than a modifier of it.

An explicit-config activation never merges a discovered config, even when it runs inside a repository: the chain is exactly what was named, and every re-bake the shim spawns carries --no-auto-config for the same reason. Unlike the other config flags, --config does not bypass the manifest fast path — a config farm's manifest lives at the path the chain itself hashes to, so the only manifest such an invocation can find is one baked from those very files.

The trust boundary: an explicit-config farm never evaluates a project's config. A shell activated against a machine-level config that cds into an untrusted clone keeps running the machine-level tools; entering a repository's toolchain still requires running datamitsu source there. This is a security property, not an omission — a config is evaluated JavaScript.

Both farms can be active at once. Layering is PATH order and nothing else: the project farm sits ahead of the config farm, so the project's pin wins, and a name only the machine-level config declares stays reachable from inside the project.

Activation exports DATAMITSU_FARM and DATAMITSU_FARM_CONFIG, and deliberately no DATAMITSU_ROOT — there is no git root, and an empty one would read as "the repository could not be determined".

See Machine-Level Toolchain for the full walkthrough.

source status​

datamitsu source status
datamitsu source status --json
datamitsu source status --config ~/.config/datamitsu/datamitsu.config.ts

Reports the farm for the current project: its origin, its root and directory, whether the baked manifest still matches the working tree, every tool the farm provides with how it is materialized (shim) and whether it has been downloaded yet, every declared name that was refused with the reason, and every system binary the farm shadows with the absolute path it was found at.

Names are never silently dropped, which is what makes this the first command to run when a tool is missing or resolves to the wrong version: a refused name is listed with its reason, so it can never be confused with a name that was never declared.

--json emits the same information as a single document and writes no human text to stdout; warnings still go to stderr. origin is always present; a git-root farm reports root, an explicit-config farm reports configPaths and prints config: lines in place of root:.

status resolves and reports. It never downloads and never re-bakes the farm.

source refresh​

datamitsu source refresh
datamitsu source refresh --force
datamitsu source refresh --config ~/.config/datamitsu/datamitsu.config.ts --force

Re-resolves the project's declared apps and rewrites the farm the current shell's PATH points at. With --config it acts on that chain's machine-level farm instead.

Nothing normally needs this. A tool invocation notices on its own that the config changed and re-bakes before running, which is what makes branch switching invisible. refresh exists for the case that detection cannot see: the staleness check watches the config chain's files and .git/HEAD, so a config that branches on an environment variable outside datamitsu's own namespace — reachable through facts().env — changes what it produces without changing anything the check compares. --force re-bakes unconditionally and is the escape hatch for exactly that.

Without --force, a farm that already matches the tree is left alone and the command exits 0.

Refresh resolves and materializes; it downloads nothing. A tool that has not been fetched yet stays a shim entry and installs on its first real use.

The summary goes to stderr and stdout stays empty, so refresh is safe to call from the same shell function that runs an activation through eval.

Environment Variables​

VariableDescriptionDefault
DATAMITSU_CACHE_DIRCustom base directory; ephemeral data goes in {base}/cache, downloaded artifacts in {base}/store$XDG_CACHE_HOME/datamitsu or ~/.cache/datamitsu
DATAMITSU_CONCURRENCYNumber of concurrent download workers3
DATAMITSU_INSTALL_TIMEOUTPer-app install timeout in seconds (0 = disabled)600
DATAMITSU_MIN_RELEASE_AGEMinimum release age in minutes for pull-* and the Go lock-file check (0 = disabled)10080
DATAMITSU_MAX_CMD_LENGTHMaximum command-line length before a list-taking operation is split into chunks32000
DATAMITSU_MAX_ERROR_CMD_DISPLAYMaximum command length shown in an error before truncation120
DATAMITSU_MAX_PARALLEL_WORKERSMaximum parallel tool execution workersmax(4, floor(NumCPU * 0.75)), capped at 16
DATAMITSU_UNIT_CACHE_TTLMinutes a cached unit-level verdict stays trusted; 0 disables verdict caching1440 (24h)
DATAMITSU_FAIL_FASTStop fix, lint and check at the first failing tool (true/1) or run everything (false/0)true
DATAMITSU_FAIL_ONRaise every operation's failOn for fix, lint and check to error, warning, info or hint; never lowers one (twin of --fail-on)-
DATAMITSU_REPORTReports fix, lint and check write, as comma-separated format=path pairs (twin of --report; see Reports)-
DATAMITSU_ALLOW_PARTIALWrite a report that lists findings for a narrowed run (true/1) instead of refusing it (twin of --allow-partial)false
DATAMITSU_EVENTSWhich findings the JSON-L stream of fix, lint and check emits: diagnostics=reported or diagnostics=all (twin of --events)diagnostics=reported
DATAMITSU_ANNOTATIONSWhether fix, lint and check print CI annotations: auto, github, azure, teamcity or off (twin of --annotations)auto
DATAMITSU_OUTPUTHow fix, lint and check show their results: human or agent (twin of --output)human
DATAMITSU_CONFIG_CACHEServe evaluated config chains from disk (0/false/off/no disables it)1
DATAMITSU_LSP_FORMAT_WIDEN_TOHow far editor format-on-save may widen: target or unitunit
DATAMITSU_LSP_FORMAT_TIMEOUT_MSFormat-on-save watchdog in ms: no further tool group starts once it has elapsed (0 = disabled)15000
DATAMITSU_LOG_LEVELLog level (debug, info, warn, error)warn
DATAMITSU_LOG_FORMATStatus output format (console, or newline-delimited jsonl with log lines as log events)console
DATAMITSU_TIMINGSEnable detailed planner/runner timings (1 = enabled)0
DATAMITSU_STARTUP_TIMINGSReport per-phase startup/config-load durations to stderr (1 = enabled)0
DATAMITSU_TRACERecord a full execution trace and print its summary (1 = enabled)0
DATAMITSU_TRACE_DIRDirectory for execution trace files{cache}/traces
DATAMITSU_FORCE_GIT_SUBPROCESSResolve the git root by forking git instead of walking the filesystem (1 = enabled)0
DATAMITSU_BINARY_COMMANDOverride binary command path-
DATAMITSU_NO_SPONSORSuppress sponsor messages (any non-empty value)-
DATAMITSU_OFFLINERefuse all network access (any non-empty value; requires a pre-seeded store)-
DATAMITSU_NO_OCIDisable OCI bundle store seeding (any non-empty value; twin of --no-oci)-
DATAMITSU_NO_PARSEShow tools' raw output instead of parsed findings; parsing still runs (twin of --no-parse)-
DATAMITSU_MAX_PARSE_INPUT_BYTESBytes of each output stream of one tool process a parser reads; a longer stream is cut and the process is truncated8388608 (8 MiB)
DATAMITSU_MAX_FINDINGS_PER_PROCESSFindings kept from one tool process's output; the rest are dropped and the process is truncated10000
DATAMITSU_LIBCOverride host libc detection (glibc or musl); affects store paths and OCI bundle selectionauto-detected
DATAMITSU_OCI_REGISTRYRegistry host for base-image digest resolution in devtools dockerfileghcr.io
DATAMITSU_PARSERS_DIROverride directory for downloaded WASM output-parser modules{store}/.parsers
DATAMITSU_ROOTGit root of the source-mode farm activated in this shell (exported by datamitsu source)-
DATAMITSU_FARMFarm directory activated in this shell (exported by datamitsu source)-
DATAMITSU_FARM_CONFIGConfig chain of the farm activated in this shell (exported by datamitsu source --config)-
NO_COLORDisable color output-
FORCE_COLORForce color output-

DATAMITSU_ROOT and DATAMITSU_FARM are written by datamitsu source. Neither is used to resolve a tool — the shim discovers the repository root from the working directory, so a tool run in another repository resolves against that repository, not against the one the shell was activated in. DATAMITSU_ROOT is purely informational; DATAMITSU_FARM is read in one place, to recognize that an invocation arrived through a farm directory when the computed cache path and the activated one spell the same directory differently (a DATAMITSU_CACHE_DIR or HOME that resolves differently in this process). Without it a genuine tool invocation could be mistaken for a renamed datamitsu binary and handed to the CLI. DATAMITSU_FARM_CONFIG is written only by an explicit-config activation, which exports no DATAMITSU_ROOT because it has no git root; it records the config chain the farm was baked from, joined with the platform's list separator, and is informational in the same way. All three are excluded from the farm's staleness fingerprint, so exporting them cannot make a farm look stale. DATAMITSU_FAIL_FAST, DATAMITSU_FAIL_ON, DATAMITSU_REPORT, DATAMITSU_ALLOW_PARTIAL, DATAMITSU_EVENTS, DATAMITSU_ANNOTATIONS and DATAMITSU_OUTPUT are excluded too: they change how far one run goes and what it prints or writes, never what a farm contains, so setting one for one command does not re-bake the farm.

DATAMITSU_FORCE_GIT_SUBPROCESS applies to the config loader's memoized git-root lookup, which is where the pure-Go walk runs; the command handlers use a separate resolver that always forks git. See Startup and Config Load.

:::note --no-oci is not a network switch --no-oci / DATAMITSU_NO_OCI turns off OCI bundle store seeding — an accelerator that degrades gracefully to fetching each artifact individually. It does not disable a parser that declares an oci source: that is not an accelerator but the only route to those bytes, so disabling it would leave no way to get the module at all.

DATAMITSU_OFFLINE is the hard gate. It refuses every network operation, including a registry-sourced parser, and expects a pre-seeded store. :::