Skip to content
open tomato/ rafa
GitHub
rafa pages
  1. rafa
  2. rafa effort

rafa effort

Collect session and commit records into the effort store, report what each plan cost, and copy, merge, migrate or repair the store.

rafa effort keeps the project's effort store, a SQLite file under .rafa/effort/ recording every session, task and commit and what each cost. collect fills it from Claude Code's session logs and git, report and dashboard read it, and the rest copy, merge, migrate or repair it.

Spends: nothing. Changes: the store; merge, import, migrate and fix-schema keep the original beside it as a backup. Needs: collect reads Claude Code's session logs for the project, so it fails on a machine with none (pass --no-sessions to read git only).

Things to know before you use it:

  • The store holds private text (task descriptions, findings, notes). Copy it only between your own devices.
  • Never copy the store file by hand while rafa writes it; copy makes a consistent copy.
  • Several devices walks the copy, import and merge flow.
NOTE

In the target layout this code lives in the private package rafa-effort, bundled into @open-tomato/rafa (Repository layout). In 0.47.0 it is already a package, @open-tomato/rafa-effort in packages/rafa-effort, but only as a facade: it re-exports this code from the root package's src/, where the files still live until their move. You install and type the same either way. [verify: the release the move lands in]

Actions

ActionDoes
collectcollect session and commit rows into the effort store
reportroll the stored session rows up per plan, then tally the task reports and list preflight halts
dashboardevery effort reading in one call: running loops and their estimates, trend, loops, skills and totals
fix-schemarebuild an effort store this rafa refuses at the migrations it knows, keeping the original
copycopy the effort store into a scratch directory and print the RAFA_EFFORT_DIR= line that points a command at it
schemasay whether this rafa can read and write the effort store, and what to run next when it cannot
migrateapply the effort store's pending migrations, one that breaks older runtimes included, keeping the original
mergemerge another device's effort store file into this one, keeping every row and the original
importimport the effort store file another device carried, merging it in through the file sync strategy
movemove a store: ndjson project's sessions and commits into the SQLite store and switch to it

rafa effort collect

Collect session and commit rows into the effort store.

rafa effort collect [--since=<string>] [--no-git] [--no-sessions] [--skills]
  [--verbose]

Reads the Claude Code session logs of this repo and its commit history, and appends every session and commit the effort store does not hold yet to the store store in .rafa/config.yaml selects, then counts the skills each appended session invoked into effort.sqlite. A run over an unchanged history appends nothing. An unrecognised argument, a --since that is no date and a config the loop cannot run on are each refused, one line per problem.

FlagType and defaultWhat it does
--sincestringCollects only the session logs and commits from this date on, as --since=<date>.
--gitboolean, default trueCollects commit rows; --no-git skips them.
--sessionsboolean, default trueCollects session rows; --no-sessions skips them. Refused beside --no-git, which would leave nothing to collect, unless --skills is given.
--skillsbooleanCounts the Skill calls of every session the store already holds, not only the ones this run appends. Lifts the refusal of --no-git beside --no-sessions.
--verbosebooleanPrints what each half found before the summary: the logs and plan stubs it read, each session read, and the commits parsed.

Examples:

  • rafa effort collect: Appends every session and commit not stored yet, then prints what each half appended.
  • rafa effort collect --since=2026-09-01 --no-git: Appends the sessions logged since 1 September 2026, and reads no commit.

Output formats: text, json.

rafa effort report

Roll the stored session rows up per plan, then tally the task reports and list preflight halts.

rafa effort report [--kind=<string>] [--entrypoint=<string>] [--skills]
  [--plan=<string>] [--trend] [--days=<string>] [--recent=<string>]
  [--loops=<string>] [--by=<string>] [--json]

Rolls the session rows rafa effort collect stored up per plan, or per branch for a session no plan claims, then tallies the stored task reports by plan, status and outcome, and lists the runs whose preflight halted, one row per required item that failed. It reads only what is stored, so two runs over an unchanged store print the same bytes. With --output=json the report is the data of the terminal result event. With --skills it prints the skills report instead: per plan, and per resolver within one, whether each offered skill and injected lesson co-occurred with a recurrence of the failure it should prevent, read from the stored rows and the skills' frontmatter, never from a session log. With --trend it prints the trend report instead: the recent task sessions against a baseline, then one row per loop and the drift of its per-task figures across loops. An unrecognised argument and a config the loop cannot run on are each refused, one line per problem.

FlagType and defaultWhat it does
--kindstringKeeps only the sessions of these kinds, comma-separated, and may be repeated. A value that is no session kind is refused, and the refusal lists the kinds.
--entrypointstringKeeps only the sessions whose dominant entrypoint is one of these, comma-separated. sdk-cli is the loop.
--skillsbooleanPrints the skills report in place of the session tables: per plan and per resolver, each skill's and lesson's signal, the skills invoked and never offered, M1, M2 and plan CI. Refused beside --kind or --entrypoint, which narrow session rows it does not read.
--planstringKeeps only these plan stubs in the skills report, comma-separated, and may be repeated. A stub the store holds no row for reads as no plan. Refused without --skills.
--trendbooleanPrints the trend report in place of the session tables: the task sessions of a recent window against a baseline, per day, the outliers, then one row per loop with the drift of its per-task figures. Refused beside --skills, --kind or --entrypoint.
--daysstringDays of the trend baseline and of the loop window. 14 when left out. Refused without --trend.
--recentstringDays of the trend's recent window, the newest session's day included. 3 when left out. Refused without --trend.
--loopsstringLists the newest n loops, whatever their age, in place of the loop window. Refused without --trend.
--bystringSplits each loop row by effort, model or agent. Refused without --trend.
--jsonbooleanDeprecated: the phase 0 spelling of --output=json, read as that flag after one deprecation line on stderr.

Examples:

  • rafa effort report: Prints the per-plan tables, the task report tallies, then the preflight halts.
  • rafa effort report --skills --plan=rafa-24-know-which-skills-earn: Prints one plan's skills and lessons with their signals, per resolver, under the fixed co-occurrence line.
  • rafa effort report --trend --loops=10 --by=effort: Prints the trend metrics, then the ten newest loops, each split by effort, and their drift.
  • rafa effort report --entrypoint=sdk-cli --kind=task --output=json: Writes a start event, then a result event whose data is the report of the task sessions the loop ran.

Output formats: text, json.

rafa effort dashboard

Every effort reading in one call: running loops and their estimates, trend, loops, skills and totals.

rafa effort dashboard [--days=<string>] [--recent=<string>] [--loops=<string>]
  [--by=<string>]

Reads the loops running now with their tasks done over total and three estimates of the time left (by the mean task session, by the time since the plan first started per task done, and rafa loop status's own), then the trend and loops segments of rafa effort report --trend, each plan's skills M1 and M2, and the totals of the stored sessions and task reports. With --output=json each widget is one key of the result's data, so a dashboard fills every widget from one call. It writes nothing; run rafa effort collect first for the newest sessions.

FlagType and defaultWhat it does
--daysstringDays of the trend baseline and of the loop window. 14 when left out.
--recentstringDays of the trend's recent window, the newest session's day included. 3 when left out.
--loopsstringLists the newest n loops, whatever their age, in place of the loop window.
--bystringSplits each loop row by effort, model or agent.

Examples:

  • rafa effort dashboard: Prints the status, trend, loops, skills and totals blocks; run rafa effort collect first for the newest sessions.
  • rafa effort dashboard --loops=10 --by=effort --output=json: Writes one result whose data holds a key per widget, the ten newest loops split by effort.

Output formats: text, json.

rafa effort fix-schema

Rebuild an effort store this rafa refuses at the migrations it knows, keeping the original.

rafa effort fix-schema [--dry-run]

Repairs .rafa/effort/effort.sqlite, or the store under RAFA_EFFORT_DIR, when this rafa refuses it for a reason a rebuild repairs: no migration log past the legacy entries (pre-log-unreleased), a log with a schema version it does not match (gate-mismatch), a logged checksum this rafa holds otherwise (edited), or a logged migration this rafa does not know that breaks older readers or writers (unknown-breaks-readers, unknown-breaks-writers). Installing a rafa that knows the store's migrations comes first where one exists. It builds a parallel store beside it at this rafa's migrations, with a migration log naming this rafa, copies every table and column this rafa knows, checks the row counts, SQLite's integrity_check and the schema plan, and lists what only the newer schema holds, unknown migrations included. Then it writes the original to effort.sqlite.v<user_version>-<stamp>.bak, whole, and moves the rebuild into its place. The rebuild keeps the store's id; the backup is a copy, so renamed back it takes a new id on its next write. A store this rafa uses, current or behind, or no store, is left alone. It refuses, changing nothing, while a loop session under the project is running or paused, from a development build, while a journal beside the store shows a write in flight, when the newer schema lacks a table or column this rafa writes, and on a refusal a rebuild does not repair. With --output=json the outcome is the data of the terminal result event. Starts no session.

FlagType and defaultWhat it does
--dry-runbooleanBuild and check the rebuild, print what it keeps and leaves behind, then delete it. The live store is only read, so this runs beside a live loop and from a development build, and can be repeated.

Examples:

  • rafa effort fix-schema --dry-run: Prints what a rebuild would keep and leave behind, changing nothing.
  • rafa effort fix-schema: Swaps the rebuild in and prints where the original was kept.

Output formats: text, json.

rafa effort copy

Copy the effort store into a scratch directory and print the RAFA_EFFORT_DIR= line that points a command at it.

rafa effort copy [--to=<string>]

Copies .rafa/effort/ into a directory of its own: the SQLite file through VACUUM INTO, which only reads the store and keeps its schema version and migration log, and the NDJSON files by file copy. The copy lands in --to, or in .rafa/scratch/effort-<stamp>/. It prints the directory and the RAFA_EFFORT_DIR= line to put in front of each command that should run over the copy instead of the live store. It plans no schema, so a development build may run it and a running loop is not disturbed. Exit code 1, making nothing, when RAFA_EFFORT_DIR is set, when there is no store, or when the target is not empty; exit code 2 when a read or write failed, with what it made removed. With --output=json the copy is the data of the terminal result event. Starts no session.

FlagType and defaultWhat it does
--tostringThe directory to copy into, absent or empty; a relative one is read from the project root. Defaults to .rafa/scratch/effort-<stamp>/.

Examples:

  • rafa effort copy: Copies the store into .rafa/scratch/effort-<stamp>/ and prints the RAFA_EFFORT_DIR= line.
  • rafa effort copy --to=.rafa/scratch/rafa-234-effort: Copies the store into a directory a plan names.

Output formats: text, json.

rafa effort schema

Say whether this rafa can read and write the effort store, and what to run next when it cannot.

rafa effort schema [--check]

Opens the SQLite effort store read-only, the one under RAFA_EFFORT_DIR when it is set, and migrates nothing. Prints the migrations it holds that this rafa knows (applied), the ones it lacks (pending), the ones this rafa does not know with what they break (unknown), and the ones recorded under another checksum (edited), then the legacy gate user_version holds and whether this rafa would use the store or refuse it, and why. It ends with Next safe step: <command>, the command the matching refusal names, or none. With --check it exits 1 when this rafa would refuse to read or to write the store. Exit code 2 when the file cannot be read as a store. With --output=json the report, with its nextStep, is the data of the terminal result event. Starts no session.

FlagType and defaultWhat it does
--checkbooleanExit 1 when this rafa would refuse to read or to write the store; exit 0 otherwise.

Examples:

  • rafa effort schema: Prints the store's migrations, the gate and the verdict, ending with the next safe step.
  • rafa effort schema --check: The same report, exiting 1 when this rafa would refuse the store.

Output formats: text, json.

rafa effort migrate

Apply the effort store's pending migrations, one that breaks older runtimes included, keeping the original.

rafa effort migrate [--dry-run]

Brings .rafa/effort/effort.sqlite, or the store under RAFA_EFFORT_DIR, to the migrations this rafa knows, including one that breaks older runtimes, which no ordinary open applies. It writes the store to effort.sqlite.migrate-<stamp> beside it with VACUUM INTO, applies the pending migrations there with log rows naming this rafa, checks the row count of every table against the store, SQLite's integrity_check and the schema plan, then writes the original to effort.sqlite.before-<id>-<stamp>.bak, whole, and moves the migrated file into its place. The migrated store keeps the store's id; the backup is a copy, so renamed back it takes a new id on its next write. A store with nothing pending, or no store, is left alone. It refuses, changing nothing, while a loop session under the project is running or paused, from a development build over any store but a copy under RAFA_EFFORT_DIR, while a journal beside the store shows a write in flight, when a row count changes, and on any refusal of the schema plan, whose text ends with the next safe step. With --output=json the outcome is the data of the terminal result event. Starts no session.

FlagType and defaultWhat it does
--dry-runbooleanBuild and check the migrated store, print what it applies, then delete it. The live store is only read, so this runs beside a live loop and from a development build, and can be repeated.

Examples:

  • rafa effort migrate --dry-run: Prints what the migration applies and how it checked, changing nothing.
  • rafa effort migrate: Swaps the migrated store in and prints where the original was kept.

Output formats: text, json.

rafa effort merge

Merge another device's effort store file into this one, keeping every row and the original.

rafa effort merge <file> [--dry-run]

Adds to .rafa/effort/effort.sqlite, or the store under RAFA_EFFORT_DIR, every row another device's store file holds that this one lacks. A row matches on its origin pair, or on its table's identity columns when either side has no origin; an equal pair is skipped, a set-once field NULL here is filled, and any other difference is kept on both sides and recorded in merge_conflicts. The gap of each commit brought in, and of the commit after it, is recomputed. The other file is only read. The merged store is built beside this one, checked (each table's count plus the rows added, integrity_check, the schema plan), and swapped in once every row the original holds is copied to effort.sqlite.before-merge-<stamp>.bak. The merged store keeps this store's id; the backup is a copy, so renamed back it takes a new id on its next write. It prints the rows added, skipped and in conflict per table and the backup's name. Exit code 2 for another project's store, and for a store: ndjson project, naming rafa effort move --to=sqlite; exit code 1, changing nothing, for a missing or damaged file, a write in flight, a loop session running or paused, and a development build over a store it does not own. With --output=json the outcome is the data of the terminal result event. Starts no session.

ArgumentTypeWhat it is
filestring, requiredThe other device's effort.sqlite, a consistent copy such as rafa effort copy makes; a relative path is read from the directory the command runs in.
FlagType and defaultWhat it does
--dry-runbooleanBuild and check the merged store, print what it would add, then delete it. This store is only read, so this runs beside a live loop and from a development build, and can be repeated.

Examples:

  • rafa effort merge ../laptop-effort/effort.sqlite --dry-run: Prints what the merge would add, skip and record as conflicts, changing nothing.
  • rafa effort merge ../laptop-effort/effort.sqlite: Swaps the merged store in and prints where the original was kept.

Output formats: text, json.

rafa effort import

Import the effort store file another device carried, merging it in through the file sync strategy.

rafa effort import <file> [--dry-run]

Pulls another device's effort.sqlite, the one rafa effort copy makes, into .rafa/effort/effort.sqlite, or the store under RAFA_EFFORT_DIR, through the file sync strategy, whose pull is the merge rafa effort merge runs: every row the file holds that this store lacks is added, a matching row is skipped, a set-once field NULL here is filled, and any other difference is kept on both sides and recorded in merge_conflicts. It merges whatever effort.sync names. The file is only read; the merged store is built beside this one, checked, and swapped in once every row the original holds is copied to effort.sqlite.before-merge-<stamp>.bak. The merged store keeps this store's id; the backup is a copy, so renamed back it takes a new id on its next write. It prints the rows added, skipped and in conflict per table and the backup's name, as rafa effort merge does. Exit code 2 for another project's store, and for a store: ndjson project, naming rafa effort move --to=sqlite; exit code 1, changing nothing, for a missing or damaged file, a write in flight, a loop session running or paused, and a development build over a store it does not own. With --output=json the outcome is the data of the terminal result event. Starts no session.

ArgumentTypeWhat it is
filestring, requiredThe other device's effort.sqlite, as rafa effort copy carried it; a relative path is read from the directory the command runs in.
FlagType and defaultWhat it does
--dry-runbooleanBuild and check the merged store, print what it would add, then delete it. This store is only read, so this runs beside a live loop and from a development build, and can be repeated.

Examples:

  • rafa effort import ../from-laptop/effort.sqlite --dry-run: Prints what the import would add, skip and record as conflicts, changing nothing.
  • rafa effort import ../from-laptop/effort.sqlite: Swaps the merged store in and prints where the original was kept.

Output formats: text, json.

rafa effort move

Move a store: ndjson project's sessions and commits into the SQLite store and switch to it.

rafa effort move [--to=<string>]

Appends every session and commit row the NDJSON files under .rafa/effort/, or under RAFA_EFFORT_DIR, hold to the SQLite store beside them, through the store port, so each row is deduplicated by its key as rafa effort collect would. It then checks that the SQLite store holds every key read and that its count is the old count plus the rows added, and only then sets store: sqlite in .rafa/config.yaml, keeping every other line. The NDJSON files are left where they are, and a move run again adds nothing, so it can be repeated once a loop started before it has ended. Exit code 1, with nothing moved, for a --to other than sqlite, a config it cannot edit, and a development build over a store it does not own; a failed count check is exit code 1 with the config left as it was. With --output=json the outcome is the data of the terminal result event. Starts no session.

FlagType and defaultWhat it does
--tostringThe store to move to; sqlite is the one value it takes, and it is required.

Examples:

  • rafa effort move --to=sqlite: Moves the NDJSON rows into the SQLite store, checks the counts and sets store: sqlite.

Output formats: text, json.