Statistics

Every run records what it did in a SQLite file, and atkins --stats reports the commands by the time they have taken altogether. It shows where a repository's day goes: the command that costs the most once its frequency counts.

  atkins --stats

The report is a section per database, the project first, each with three parts: a column per day of the window, the tools ranked by the time they have taken, and the commands in a table. A project section names the repository its runs came from.

  project · github.com/titpetric/atkins  /w/atkins/.atkins/atkins.db
31 days, 2026-09-22 to 2026-10-23, 30.5s across 624 runs

2.9s │    ▂     ▂▃     █▄▄         ▂
     │    █     ██    ▅███         █▃▄
     │    █     ██    ████     ▃▇  ████
     │    █     ██    ████     ██  ████
     │    █     ██    ████     ██  ████
     │    █     ██    ████     ██  ████
   0 └─────────────────────────────────
       2026-09-22            2026-10-23

sleep  ████████████████████████  28.7s  78 runs
go     ▇                          1.1s  208 runs
git    ▂                         318ms  130 runs
head   ▂                         306ms  78 runs

| Command | Runs | Total | Avg | Max | Failed | Last |
| --- | --- | --- | --- | --- | --- | --- |
| sleep | 78 | 28.7s | 367ms | 574ms | 5 | just now |
| go version | 130 | 686ms | 5ms | 11ms | 4 | just now |
| go env GOOS | 78 | 379ms | 4ms | 11ms | 1 | just now |

workspace  /home/you/.atkins/atkins.db
31 days, 2026-09-22 to 2026-10-23, 4m12s across 3104 runs
...

The daily chart shows whether the month runs steady or spikes on one Tuesday. Each column is a day, every bar starts from zero, and the busiest day carries the one label, which keeps the shape readable. A day with no runs shows as an empty column.

The tool ranking rolls up every form of a tool: go build , go test and go vet count as go. The ranking shows a tool that runs two hundred times for a second in total next to one that runs twice for half a minute.

On a terminal all three render in colour, and the table gets the borders of the atkins --help tables. Redirected to a file or a pipe the output is plain text. One command serves a terminal and a document.

Reading it from a program

--json and --yaml emit the whole report, with the durations in milliseconds: a row per tool per day in usage and a row per command key per day in command_usage , the series a chart of tool or command use over time draws.

  atkins --stats --json

A scope per database, keyed by its kind. The project key is absent for a directory with no project of its own.

  {
  "project": {
    "name": "project",
    "repository": "github.com/titpetric/atkins",
    "database": "/w/atkins/.atkins/atkins.db",
    "days": 31,
    "from": "2026-09-22",
    "to": "2026-10-23",
    "runs": 624,
    "total_ms": 30512,
    "commands": [
      {
        "command": "sleep",
        "tool": "sleep",
        "runs": 78,
        "total_ms": 28700,
        "average_ms": 367,
        "max_ms": 574,
        "failures": 5,
        "last_run_at": "2026-10-23T13:41:02.118Z"
      }
    ],
    "tools": [
      { "tool": "sleep", "commands": 1, "runs": 78, "total_ms": 28700, "failures": 5 }
    ],
    "usage": [
      { "date": "2026-09-25", "tool": "sleep", "runs": 3, "total_ms": 1104 },
      { "date": "2026-09-25", "tool": "go", "runs": 10, "total_ms": 52 }
    ],
    "command_usage": [
      { "date": "2026-09-25", "command": "sleep", "tool": "sleep", "runs": 3, "total_ms": 1104 },
      { "date": "2026-09-25", "command": "go version", "tool": "go", "runs": 6, "total_ms": 31 }
    ]
  },
  "workspace": {
    "name": "workspace",
    "database": "/home/you/.atkins/atkins.db",
    "days": 31,
    "from": "2026-09-22",
    "to": "2026-10-23",
    "runs": 3104,
    "total_ms": 252018,
    "commands": [],
    "tools": [],
    "usage": [],
    "command_usage": []
  }
}

The two scopes carry the same fields. A reader draws either with one piece of code. repository is on the project alone: a workspace holds many and names none.

A day a tool or a command did not run has no row in usage or command_usage . A reader drawing a series fills the gaps with zero.

Every command

--all adds the commands themselves, one by one, oldest first: every command of the window that the report counts, builtins left out as they are from the totals. A chart finer than a day, an hour or three, buckets these itself.

  atkins --stats --json --all
atkins --stats --json --all --limit 500

Each scope gains a samples array. Without --all the key is absent and the report is as above.

  "samples": [
  {
    "at": "2026-10-23T16:23:37.551Z",
    "command": "docker compose build",
    "tool": "docker",
    "kind": "step",
    "duration_ms": 2789,
    "exit_code": 0,
    "failed": false,
    "pipeline": "Image",
    "job": "build",
    "invocation_id": "01M5MS6JS6S2VE63XVY5WWMQRY"
  }
]
Field What it holds
at When the command started, RFC 3339 in UTC to the millisecond
command The command key, as the report groups by it
tool The first word of the key
kind step , substitution for a $(...) , or interpolation for a variable
duration_ms How long it took
exit_code The code it exited with, and failed whether that code is other than zero
pipeline The pipeline the run ran, as its name: spells it
job The job the command ran in, absent when it ran in none
invocation_id The atkins process, the same for every command of one run

A sample names its command by the key and never by the command line. A command line holds its values, and a value can be a token: docker login -p ... reports as docker login .

The window bounds the list: 31 days of history, which for a busy workspace runs to tens of thousands of commands. --limit N keeps the newest N, still oldest first. With no limit the list holds every command of the window.

On a terminal, or redirected as markdown, --all adds a table after the commands: the time in the local zone, the key, the job, the duration and the exit code.

Two databases

A run records twice, because the two answer different questions.

The project's is the atkins.db of the nearest .atkins folder, found by walking up from the directory atkins runs in. Every run in a checkout counts towards the same history, from any subdirectory, and it says what standing in that checkout costs.

The workspace's is $HOME/.atkins/atkins.db , which every run records to. It holds every project on the machine together, and says where a week went across them. A command run in a directory outside any project lands here alone, and a stray sleep in /tmp counts too. ATKINS_DB names another file for it.

A directory inside $HOME whose nearest .atkins folder is the workspace's own has no project of its own. There is one database, and the run records to it once.

Where atkins runs Project Workspace
/w/atkins , holding .atkins/ /w/atkins/.atkins/atkins.db $HOME/.atkins/atkins.db
/w/atkins/runner , under it /w/atkins/.atkins/atkins.db $HOME/.atkins/atkins.db
/tmp/scratch , under no project none $HOME/.atkins/atkins.db
$HOME/notes , under $HOME/.atkins none $HOME/.atkins/atkins.db

storage.database in .atkins/config.yml , or ATKINS_DB , names the workspace's database, $HOME/.atkins/atkins.db when unset; the project's records as usual. A list of files in the form of PATH (a.db:b.db ) names the workspace's first and mirrors after it: each mirror takes every row the workspace's takes, and reports read the project's file and the workspace's. A runner that sets a $HOME per checkout names the checkout's file first and its own second, and keeps one history across them. atkins reports a mirror that fails to open once, and the other databases still take the row.

What a run records

Two tables, because a job and a command are different questions.

The run table holds one row per node of the tree atkins walked: the pipeline, its jobs, their steps. Each row carries the time that node took with everything underneath it. A default job that runs sleep 1 and sleep 2 records three seconds, and the two steps record one and two.

The command table holds one row per command a step executed, with the command as the shell received it, its exit code, its directory and how long it took. A $(...) substitution is a command too, and records as one.

Both tables carry the invocation_id of the pipeline row, which groups the rows of one atkins process. The pipeline row also carries the file, the module, the repository, the branch and the commit. A reader can split a history by branch.

The command key

The report groups commands by their key: the words up to the first argument that starts with - , at most three of them, and none that is a path or a value. A word holding / , . , = , $ , a quote or another shell character ends the key, and so does a number. The key leaves out an environment assignment ahead of the tool, and a pipeline or a list keys by its first command.

Command Key
go test ./... go test
go test -v -cover ./... go test
docker build -q -t app . docker build
docker compose up -d docker compose up
git clone --depth 1 <url> git clone
`git rev-parse HEAD cut -c1-8`
kubectl get pods web-1 -o yaml kubectl get pods
npm run build npm run build
cat README.md cat
CGO_ENABLED=0 go build -o bin/x . go build

Shell builtins a step runs to set itself up, set , cd , export , echo , test , [ , true , false and : , are neither recorded nor reported. set -eu at the top of a script is not a tool.

The database also stores each command in a normalised form, the tool and its subcommands with the flags sorted and their values removed, such as go test -cover -v . The report folds those forms into their keys as it reads them. The rows a run recorded before the key existed count under it too.

Not recording

  atkins --local build

--local runs without touching the database. storage.record: false in the configuration turns recording off for every run in the project, and ATKINS_RECORD=false for one shell.

When a database does not open, atkins says so once and runs the pipeline unrecorded.

Retention

Atkins keeps a recorded run for 31 days from its start, on every machine. No setting changes it: 31 days is long enough to hold a month of work and compare this week against the last, and short enough that a checkout nobody prunes does not grow without bound.

The run that records is the run that prunes, in both databases. Each write drops the rows that have fallen outside the window, in bounded batches. The window holds through the runs themselves. The workspace database takes every project's rows, and 31 days keep it a month of history.

A database nothing records to any more keeps its last month until something prunes it, and a write prunes rows and releases no space to the file system. --prune does both for one database, by name, at an age of the caller's:

  atkins --prune --db ~/.atkins/atkins.db --older-than 31d --dry-run
atkins --prune --db ~/.atkins/atkins.db --older-than 90d

It removes every run and command that started before the age, 31d unless --older-than says otherwise, days or a duration such as 720h , vacuums the file and truncates its write-ahead log, and prints one line: the file, what went, what remains, and the file's size before and after. --dry-run counts and changes nothing. A file that is not there is an error; atkins never creates an empty database. A runner that holds many checkouts' databases, such as task-ui , runs it on each from its own cleanup, under the age its settings hold.

  /home/you/.atkins/atkins.db: removed 1824 runs, 608 commands older than 31d; 3104 runs, 1035 commands left; 4.2 MB before, 2.9 MB after

Other readers

Both files are ordinary SQLite databases. atkins closes them at the end of each run, and any SQLite client may read them. task-ui reads them to chart tool usage over time, a project's beside the workspace's.

See Also

  • CLI Flags - --stats , --local and --config
  • Automation - the JSON and YAML a run can emit instead