Repository navigation
CLI API: the evaluation loop at the top level, subject and eval groups below, one enforced flag vocabulary (proposal v2.1) #94
Description
v2.1, 2026-10-25. v2 (10-24) rewrote the 10-19 proposal. It put the evaluation loop from #121 (
plan → validate → run → compare → gate → report) at the top level, with the subject andevalgroups below it. v2.1 adds three things:
- every point of @antonwolfy's review, now mapped in §8;
- the six persona walkthroughs (§7), folded into §2 and §3;
- a full flag audit with an enforcement plan (§2.4, stage F).
Each decision in §6 now has its context, the options and a recommendation. v1's code audit and experiments E1–E3 (cited below) still hold and are in this issue's edit history (the 10-19 and 10-24 versions).
Why
dokimi has 23 top-level commands, plus the product alias. They hold 68 leaf operations: 64 on 10-19, then repo inspect, repo budget, tasks inspect (#210) and catalog formats (#211). Open PRs add about 7 more. The v1 diagnosis still holds:
- The names describe where code was added, not what a user wants.
runis a docs lint.comparediffs lint snapshots.evaluateandevalsit side by side. - One operation lives in many places. Execution is spread over 8 groups, comparison over 5 and reports over 4.
- The flags are getting worse, not better. On
main@66cd3d83, the 68 leaves declare 222 distinct option strings (full audit in §2.4):- Output is spelled 11 ways.
--out(16 leaves) is a file in 13 and a directory in 3. The others are--output15,--out-json10,--out-md8,--output-dir7,--out-dir5,--json10,--format json,--results-dir,--items-outand--no-json. - "Exit 1 on a failed check" is spelled five ways,
--strictin 7 leaves with 7 different meanings among them. - Spend caps are
--budget,--max-usd,--max-cost-usdand--max-batch-usd, while--budgetsmeans token fractions. - Some names carry different meanings.
--max-tokensis retrieval tokens inanswers/benchmarkbut an output ceiling inarms/novelty/tools.--modelis the model under test, the extractor or the adjudicator, depending on the leaf.--judgeis a model in one place and a boolean inarms run. - Open PRs add
-o,--answers-out/--eval-out,--trialsand a third JSON convention.
- Output is spelled 11 ways.
Two things changed since v1, and they change the answer:
- User loop: eval engine contracts (plan/validate/compare/gate/record), 3 skills, GitHub Action #121 defined the user loop:
plan → validate → run → compare → gate → record, withplan.v1/result.v1(feat(runner): plan.v1 / result.v1 and engine-computed completeness #161), a policy gate (J11/J12: distinct inconclusive verdict and blocking policy (AC-J11-002, AC-J12-002) #99), bundled skills (feat(skills): draft dokimi-design-evals skill #162) and a GitHub Action. v1 had anexperiment_plan.v1andruns gatefor the same ideas. Two plan schemas and two gate commands for one concept is the conflict to resolve first. - The CLI is already an API.
- Bundled skills name commands, and a test fails if a named command does not parse (feat(skills): draft dokimi-design-evals skill #162).
journeys.yamlresolves itsinvokesthroughbuild_parser()(docs, tests: user-journey catalog (J1–J15) and criterion evidence trace #102).task_inspect.v1records consumer command names in an artifact (feat(tasks): detect eval-task formats withtasks inspect#210).repo-eval.ymlinputs map to flags (ci: reusable repo-eval workflow (validate, scan, tasks check) with dokimi pinned #113), and the workflow parsesrepo validate --jsonfields.VISION.md: "Agents are first-class users… The CLI and the library are the interface."
So the question is no longer only "which group does each leaf go in". It is which API an agent, a CI workflow and a person can all drive through the journeys J1–J15.
1. Design goals, taken from VISION.md and the journeys
| # | Goal | Source |
|---|---|---|
| G1 | The A/B comparison is the core. Every journey that measures ends in "compare variants, with statistics". That path should be the shortest and the most uniform one. | VISION "How we build it"; J2, J4, J8–J10, J12, J13 and J15 all route through J10 |
| G2 | Cheap first. Every operation that spends money can say what it would spend (--dry-run) and stop at a cap (--max-usd), with one spelling. Paid and free steps are labelled. |
VISION; J14; AC-J01-002, AC-J14-001/002 |
| G3 | "Not measured" is never "pass". One verdict vocabulary, the same in the JSON, the human output and the exit code. | VISION; #176, #99; AC-J11-002, AC-J12-002 |
| G4 | Every run can be replayed. The settings that produced a result are a file in the result, not argv someone has to remember. | J13 (AC-J13-001), J10 (AC-J10-008), J11 (AC-J11-003); #121 acceptance |
| G5 | Agents can drive it without reading the docs. Every command can emit one versioned JSON document. A failure names the next command to run. Moved commands say where they went. | VISION; #162; J14 outputs |
| G6 | One front door for "I don't know what to check". | J1 (today "proposed") |
| G7 | Subject-specific methods stay specific. Novelty, reach, routing and repo grading keep their own inputs, statistics and limits. Only the envelope is shared. | v1 rules 1–3, still valid |
| G8 | One name, one meaning, everywhere. A concept has one flag spelling, one type and one default policy in every command, in plan.v1 and in dokimi.api, and CI enforces it. |
§2.4; the audit above |
2. The API
2.1 Shape
dokimi
── the loop (any subject; what CI, skills and the Action call) ──────────────
assess <path> what is this, what is free to check, what is unknown; never spends
plan <executor> [flags] write plan.v1: cells (arm × model × harness) × evals × reps, budget, analysis, policy
validate <plan> every pre-spend check: schema, inputs, judge validity, comparability, estimate vs cap
run <executor> [flags] plan, then execute into a run directory; or: run --plan FILE; resumes by --name
compare <run|baseline:NAME>… paired comparison; method, unit and contrasts are declared and recorded
gate <run|comparison> apply policy.v1, print a verdict, set the exit code
report <run|comparison>… render saved results (md | json | html | pr-comment | record | badge); no model calls
runs list · show · explain · resume · replay · export · import · rm · gc · baseline {save, list, rm}
── subjects: cheap and specific diagnostics (J14 direct operations) ─────────
skill check · routing · novelty {analyze, atoms, probe, items} · shorten
repo fetch · inspect · scan · lint · budget · reach (+ run · compare · report as shortcuts, §5 L6)
catalog check · formats · list · show · intents · routing · trigger-probe
docs check · compare
── evals: building them and trusting them (J7) ──────────────────────────────
eval inspect · import · check · coverage · split {lock, status}
personas {discover, validate} · questions {generate, analyze, refine, review}
triggers {generate} · tasks {list}
judge {validate, wording, agreement}
score {pointwise, pairwise, panel, checklist, grounding, ragas, reference}
── analysis and data ────────────────────────────────────────────────────────
trace inspect · consistency
tools preference
data export · import · schema {list, show} · models {list}
That is 15 roots in four sections. dokimi --help prints the sections in this order; argparse does not group subcommands by itself, so a small help formatter does it. Every leaf's one-line help ends in [free] or [paid].
Why verbs at the top and nouns below:
- The loop is the same for every subject. A skill A/B, an
AGENTS.mdA/B and a model comparison are all plan, validate, run, compare, gate. If the verbs sat under each noun (repo run,arms run,tasks run), every subject would need its own copy of resume, budget, comparison and gating. That is the code today, with 18 places that pair, diff or test results. - Diagnostics are not the same across subjects. Novelty needs an atom inventory, reach needs a harness profile,
repo scanneeds a tree. They belong to the subject group (G7). They stay specialised, but they share the envelope and the flag vocabulary. - CI, skills and the Action read the loop verbs, not the nouns. User loop: eval engine contracts (plan/validate/compare/gate/record), 3 skills, GitHub Action #121 says the skills and the Action "never compute a verdict. They call
dokimi gate --policy …". With the loop at the top level, that sentence is a literal command.
The positional is the subject, and the subject is what the result is keyed by.
- One skill:
skill VERB SKILL, for exampleskill routing ext/foo --against skills/. This gives one record per skill. - Several skills, or a property between skills:
catalog VERB ROOT, for examplecatalog routing skills/(all-vs-all) orcatalog trigger-probe skills/. This gives a matrix or an index, plus a record per skill. - Flags never change what is being evaluated, so v2's
--all-vs-alland--catalogflags go away. --armnames variants of the subject. It never names the subject.
2.2 plan and run: one plan schema, executors as positionals
dokimi plan <executor> [executor flags] [shared flags] --out plan.yaml # write only
dokimi run <executor> [executor flags] [shared flags] [--name NAME] # plan + execute
dokimi run --plan plan.yaml [--name NAME] # execute a saved plan
dokimi validate plan.yaml # check + estimate, no spend
- Executors:
arms: the question/answer track. Questions come from--questions FILE, or from the subject's eval set.answerstasks: Harbor, BenchFlow, terminal-bench, SWE-bench.repo: container graders,.agent-eval/.localization:explore.catalog: a composite. Each cell carries its own executor (tasks, routing, trigger-probe).--preset static|live|fullpicks the cells, andstaticresolves to free checks only.- Recipes that become plans once there is one answering stack:
pipeline(evaluate),benchmark,subject.
plan <x>andrun <x>take the same flags. They are built from one argparse parent per executor; E1 showed this can be done without touching handlers.run <x>writes the plan into the run directory before the first call. So every run has a replayableplan.v1, including runs started from flags (G4).- One schema:
plan.v1from feat(runner): plan.v1 / result.v1 and engine-computed completeness #161.experiment_plan.v1from v1 is dropped. feat(runner): plan.v1 / result.v1 and engine-computed completeness #161 is extended additively. The existing fields (subjects,suites,models,harnesses,grader,varied_variable,cells,budget) keep their meaning. New:executor, andcells[].executorfor composite plans.cells[]as explicit (arm, harness, model, bin) tuples. This is the--cellgrammar in §2.4. Repeated--model/--harnessstill give the full cross.arms: {name: ref}, using the arm grammar in §2.5.budget: {max_usd, max_usd_per_cell, max_cells}.analysis: see §2.10.judges[]: model, role, aggregation, andvalidity: RUNwith a threshold.policy: a path, or an inlinepolicy.v1.splits:source,dev,valorsealedper task set.sourceis J15's originating task, kept out of scoring. The sealed state lives in theeval splitrecord (§2.3), not in the plan.env: network, delegation, work dir, cache dirs, image, timeouts.dokimi_version,plan_digest: both required.
- The executor is a positional, not
--type. E2 settles this: 95 distinct flags across the 10 execution commands, 58 of them unique to one command. The flags a user sees are the executor's own flags plus the shared vocabulary (§2.4). - Resume.
run --planresumes by default and skips finished cells, asrepos/runner.pydoes today.--forcere-runs them.run <x> --name NAMEputs the run atRUNS_ROOT/NAME/. Re-running the same command with the same name resumes it. A different plan digest under the same name is an input error, not a silent fork.- Resume refuses cells written by another
dokimi_versionorplan_digest, unless--forceis given.
validateis one place for every check before spending.- The plan schema, and that the inputs exist (today
benchmark preflight). - The eval self-checks (
eval check: null, oracle, isolation; later the mutation controls of J7: draft tasks, negative controls and coverage gaps (AC-J07-004, AC-J07-006, AC-J07-007) #97). These take hours on heavy builds, so they are opt-in (--self-check) or reused from an earlier result (--reuse-check check.json). - Judge validity: a judged metric whose judge has no
validityrun, or one calibrated on another model or prompt digest, givesinvalid. - Comparability against
--baseline, when one is given. - Sealed splits: refused once burned.
- The cost estimate:
expected_calls, andexpected_usdas an upper bound, judge included, as in feat(novelty): estimate-only projects the calls and dollars a run will spend #166. An estimate above the cap givesfailwithreason: over_budget, before the first paid call. run <x> --dry-runis the same thing without writing a plan file.
- The plan schema, and that the inputs exist (today
2.3 Run directories, records and baselines
- One directory per run under the runs root:
results/by default, overridden byDOKIMI_RUNS_DIR. A run directory holds:run.json: the envelope (R1)plan.v1.yamlresult.v1.json, written even when the run is interrupted (SIGTERM, cap), withstatus: incomplete- the executor's own artifacts
- Paths inside
artifactsare relative to the run directory.
runs listscans the root and readsschema_versionandrun_id(--subject,--verdict,--kindfilter it). Anindex.jsonlis only a cache that can be rebuilt (as @antonwolfy proposed). Files written before the envelope existed are listed too.- Diagnostics write run directories too (
skill novelty analyze,repo reach,repo scan,tools preference). Soruns listshows the whole history, not onlyrun.--out-dirputs the run directory somewhere else;--out FILEalso writes the primary artifact toFILE. - Reuse instead of paying twice. A command that consumes an earlier result takes
--from RUN, for exampleskill shorten --from $NOVELTY_RUNorskill novelty probe --from $RUN.--cache-diris a shared flag. - A baseline is a named pointer to a run, like a git tag.
runs baseline save NAME RUNcreates one, andcompare RUN baseline:NAMEresolves it. Any artifact kind can be a baseline.- Names can be namespaced per subject, for example
skill/a@main. --expect-previous RUN_IDmakes the save a compare-and-swap, so two nightlies cannot race on the pointer.
- Names can be namespaced per subject, for example
- Baselines survive ephemeral CI runners.
runs export NAME|RUN > bundle.tgzandruns import bundle.tgzmove a run with its plan and result. The bundle is content-addressed byplan_digestplus the result sha, and import verifies hash and schema. A PR can poison a cached artifact, so import never trusts it blindly.baseline:also accepts a file or a URL, for example a committedbaselines/main.json. Storage can then be a git branch,actions/cache, OCI or S3 without dokimi knowing which (User loop: eval engine contracts (plan/validate/compare/gate/record), 3 skills, GitHub Action #121 Q1 and Skill catalog integration (intel/skills): per-skill evals in catalog CI, results, promotion to validated #138 Q2 stay open; the API does not depend on them).
- Records for the Action, the catalog and a site or badge (User loop: eval engine contracts (plan/validate/compare/gate/record), 3 skills, GitHub Action #121
record, Skill catalog integration (intel/skills): per-skill evals in catalog CI, results, promotion to validated #138 P1.6) areresult.v1plus the envelope.- The key is
skill_sha(the tree hash of the skill directory, not the repo commit),task_set_hash, the resolvedmodel_id, the harness and its version, anddokimi_version. report RUN --per-subject --format record|badge|site-json --out-dir records/writes one record per subject. A run over two skills gives two records.- A skill that is evaluated upstream gets the record state
evaluated_upstream, notnot_run. - dokimi writes files only; the workflow commits them (Skill catalog integration (intel/skills): per-skill evals in catalog CI, results, promotion to validated #138 P2.12).
- The key is
- Stateful eval-set records.
eval split lock DIR --sealedrecords a digest. Anyrunthat scores a sealed split marks it burned, andvalidaterefuses to use it again (Skill auto-improvement: measurement gates (A) and improve command (B) #220).eval split statusprints the state. - Housekeeping. Repo batches are gigabytes, so
runs rm RUNandruns gc --keep-baselines --older-than 30dexist.
2.4 One flag vocabulary, enforced (dokimi/cli_flags.py; this is #4 wave 5)
Audit. These are all 222 option strings on main @ 66cd3d83, grouped by concept. The generator (scripts/cli_flag_audit.py) ships with F1, and the per-flag listing goes into that PR.
| Concept | Canonical spelling and type | Spellings today (leaves) | Meaning drift today |
|---|---|---|---|
| Model under test | --model PROVIDER/MODEL, repeatable where several are allowed |
--model 25 + --provider 20, --work-model 1 |
--model is the extractor in novelty atoms, the adjudicator in novelty shorten, and an optional extra in novelty routing. Defaults are gpt-6.1-sol…, claude-sonnet-5-5 or none |
| Role models | --judge P/M (repeatable = panel), --extract-model P/M, --escalate-model P/M, --fill-model P/M, --ragas-model P/M |
--judge-model 14 + --judge-provider 14, --judge 3, --judges, --judge-panel 2, --judge-configs, --extract-model 4 + --extract-provider 2, --escalate-* 2, --fill-*, --ragas-* 2 |
--judge is a boolean in arms run/subjects run and a list elsewhere |
| Sampling | --temperature, --ROLE-temperature, --seed N |
--judge-temperature 10, --temperature 2, --seed 9 |
--seed defaults to 0 in some leaves and to none in others; the resolved seed is now always recorded |
| Variants and cells | --arm [NAME=]REF (§2.5), --cell k=v,… |
--arm 3, --arms 7, --placebo-text 2, --plugins 2, --matrix-cell(s) 3 |
4 grammars for --arms, 3 for --arm |
| Baseline | --baseline RUN|baseline:NAME|FILE, --baseline-arm NAME |
--baseline 2, --baseline-arm 6, --treatment-arm, --baseline-harness, --work-baseline-harness, --base/--candidate, --baseline-json/--plugin-json |
|
| Harness | --harness NAME, repeatable; --bin NAME=PATH |
--harness 6, --harnesses, --work-harnesses, --command-template, --work-command-template, --bin 2 |
tasks --harnesses benchflow-skill:<agent>:<path> puts harness and arm in one string |
| Repetitions | --reps N |
--reps 2, --repeats 2, --work-repeats, --multi-run (open PR: --trials) |
|
| Concurrency | -j/--concurrency N |
--concurrency 18, -j/--jobs 2 |
Defaults of 5 or 6, plus a hard-coded 5 in evaluate |
| Spend | --max-usd X (whole command), --max-usd-per-cell X, --max-cells N; DOKIMI_MAX_USD / --budget-ledger FILE across commands |
--max-usd 5, --max-cost-usd, --max-batch-usd, --budget 2 |
--budget is USD per run in repo run and USD per scenario in repo reach |
| Estimate | --dry-run |
--dry-run 4, --estimate-only, --work-dry-run |
questions refine --dry-run means "don't write the file" |
| Tokens | --max-output-tokens N, --retrieval-tokens N, --token-budgets F,F |
--max-tokens 13, --budgets 4 |
--max-tokens is retrieval in 6 leaves and output in 7 |
| Output | --out FILE (the primary artifact, always a file), --out-dir DIR (the run directory), --candidate-dir DIR (a written skill or patch), --json (cli_output.v1 on stdout), --format (render formats only) |
--out 16, --output 15, --out-json 10, --out-md 8, --output-dir 7, --out-dir 5, --json 10, --no-json, --results-dir, --items-out, --view-format; --format 5 |
--out is a directory in repo run, repo reach and repo tasks check. --format is a render format in 3 leaves and an input dialect in catalog trigger-set (which becomes --dialect) |
| Exit on findings | --fail-on {warn,fail} |
--strict 7, --fail-on-warning 2, --fail-on-error, --fail-on-fail |
--strict means 7 different things. Two of them become the default under §2.6: baseline compare --strict (model mismatch → invalid) and judge-validity --strict (inconclusive → 3) |
| Run identity | --name NAME; a positional RUN |
--run-id 3, --run-ids, --batch, --name |
|
| Environment | --network [PHASE=]MODE[:hosts] (#218), --timeout [PHASE=]SEC, --delegation native|none (#107), --work-dir DIR, --cache-dir DIR, --cache NAME=DIR |
--agent-network, --build-network 2, --timeout 2, --grade-timeout 2, --build-timeout 2, --cache-dir 12, --cache 2, --cache-root 2, --work-dir |
|
| Inputs | subject positional; --tasks DIR|FILE, --task ID (a filter), --questions FILE, --answers FILE, --registry FILE, --from RUN |
--product 20, --library/--target 2, --libraries, --root 3, --repo-root 2, --tasks-root 5, --tasks 2, --subjects-root, --questions-root 4, --questions 10, --questions-from 2, --custom-questions, --atoms 3, --novelty-artifact, --from-eval |
--target is a library in benchmark and a skill in catalog trigger-set |
| Docs and retrieval | --arm docs:SOURCE; --top-k, --rerank-threshold |
--doc-source 5, --context7-id 6, --top-k 6, --rerank-threshold 5 |
|
| Re-run | --force |
--force, --force-regen 2 |
|
| Policy and analysis | --policy FILE|NAME, --method, --unit, --contrast, --correction (§2.10) |
--stats-correction 3, --pool-replicates, --metric |
The remaining ~60 flags are specific to one method (for example --max-atoms, --no-reversal, --typed-threads, --salt, --large-bytes). They stay local to their leaf but follow the naming patterns below.
Patterns for local flags.
- A cap is
--max-THING. - "Process only the first N" is
--limit N. - A boolean is
--x/--no-x(BooleanOptionalAction). - A role model is
--ROLE-model P/M. - A per-phase setting is
--SETTING [PHASE=]VALUE. - No local flag may reuse a stem that the registry owns:
out,model,judge,usd,budget,token,reps,json,format,strict.
The contract. Every paid leaf takes --dry-run, --max-usd and --json, and its one-line help is labelled [paid]. Every leaf takes --json. The same concept has the same spelling in four places:
- the flag;
- the
plan.v1field; - the
dokimi.apikeyword (max_usd); - the environment variable (
DOKIMI_MAX_USD) and therepo-eval.yml/ Action input.
There is one precedence rule: the most specific source wins, except for caps, where the lowest cap wins. So a CLI --max-usd can lower plan.budget.max_usd but never raise it.
How the code enforces it (stage F, §5):
- One registry.
dokimi/cli_flags.pyholds aFlagSpecper concept: canonical name, type and parser (parse_model_ref,parse_arm,parse_cell,parse_phase_map,parse_usd), nargs, metavar, help, default policy,plan_field,api_kwarg,env, and banned synonyms. Leaves callcli_flags.add(parser, "max_usd"). The created action is tagged with its concept. - A contract test walks
build_parser(). It fails when:- a registry name or a banned synonym was added with a raw
add_argument; - one name has two types, nargs, choices or concepts across leaves;
- a banned synonym is anything other than a tombstoned flag (which exits 2 and prints the new spelling);
- a local flag is not listed in
dokimi/cli_flags_local.toml(leaf, flag, one-line reason), or uses a stem the registry owns; - a
[paid]leaf lacks--dry-run/--max-usd/--json.
- a registry name or a banned synonym was added with a raw
- Parity tests.
- Every
plan.v1field maps to a registry concept or is markedfile_only. - The
dokimi.apikeywords equal the registryapi_kwargs. - The
repo-eval.ymland Action inputs equal the registry names; the test parses the workflow YAML. - Every
DOKIMI_*environment variable read in the code is in the registry.
- Every
- A snapshot in CI.
docs/reference/cli-interface.json(routes, flags,--jsonschema names) is generated, andcli-interfaceCI (ci: report user-facing CLI surface changes on every PR #88) fails when it is stale. So every flag change or JSON shape change appears as a diff in the PR and needs achangelog.dfragment.repo-eval.ymlreadsd["ok"], so a JSON shape change is breaking in the same way a rename is. - The docs follow.
test_docs_code_refs, the feat(skills): draft dokimi-design-evals skill #162 skills test and thejourneys.yamlresolver reject banned spellings in docs, skills, journeys and workflows. - A ratchet, not a big bang. The first PR records today's 222 strings as the allowlist, so the test only fails on new drift. Each later PR moves one concept into the registry and deletes its allowlist rows. The test then flips to strict.
2.5 One arm grammar
--arm none no added context
--arm skill:PATH[@REV] a skill bundle, optionally at a git revision --arm skill-catalog:DIR
--arm guidance:none|raw|rev:REV --arm patch:FILE
--arm docs:SOURCE context7 | local:PATH | url:URL | mcp:…
--arm placebo:FILE|auto same-length off-topic control; auto = generated, length-matched, digest recorded
--arm selfgen:CREATOR_DIR a skill generated by a named skill-creator (J15); its model and cost count toward --max-usd
--arm NAME=REF+REF combination, named (#137 `guidance:none+skill:…`, #41 composite arms)
The executor decides which refs it supports and refuses the others with a message that lists them. The harness (claude-code, codex, copilot, benchflow:<agent>, docker-oracle) is --harness or a --cell key, separate from the arm. A harness that needs a model of its own family is paired explicitly: --cell harness=codex,model=openai/gpt-x --cell harness=claude-code,model=anthropic/….
2.6 One verdict vocabulary and one exit-code table
| Verdict | Meaning | Exit |
|---|---|---|
pass |
Measured, and meets the policy | 0 |
fail |
Measured, and does not meet the policy; or validate refuses the plan |
1 |
| (usage or input error) | Bad flags, a missing or malformed file (including a malformed policy), a moved command (error.code: moved), no credentials, an unpriced model under --max-usd |
2 |
inconclusive |
Measured, but undecided | 3 |
invalid |
Not comparable or not trustworthy: suite-hash/model/prompt-digest mismatch outside the declared varied_variable, a broken grader, an uncalibrated judge, a stale baseline, nothing selected |
4 |
infra_error |
Harness crash, docker failure, vanished scratch, an uncaught exception, SIGTERM | 5 |
not_run |
Cell level only: unsupported host, egress task, skipped | the run's verdict decides |
- Exit 2 stays the input error. argparse hard-codes it, and
cli.pyalready mapsInputErrorto it. User loop: eval engine contracts (plan/validate/compare/gate/record), 3 skills, GitHub Action #121 proposed 2 forinconclusive, which collides, soinconclusivemoves to 3. This matches feat(runner): plan.v1 / result.v1 and engine-computed completeness #161's cell statuses. - Every command that does work follows the table, not only the commands that issue verdicts.
run,resumeandreplayexit 5 on an infra failure and 3 on a budget stop, even though they issue no pass/fail.compareexits 4 oninvalid, so a pipeline stops beforegate. An uncaught exception is caught at the top ofcli.pyand exits 5, so a crash never reads as a regression. reasonsays why.- With
inconclusive:interval_crosses,sample_cap,budget_stop,incomplete. - With
invalid:mismatch:FIELD,uncalibrated_judge,burned_split,no_cells. - With
failfromvalidate:over_budget. - With
infra_error:kind: transient|config.
- With
- Several subjects (a catalog run): the exit code is the worst subject verdict, and
subjects[]lists each one. --jsonoutput always carriesverdictandstatus, and exits 0 only forpass. Today a capped judge-validity run printsRESULT: PASS (inconclusive …)and exits 0 (AC-J11-002 gap); under this table it exits 3.
2.7 Machine output: one JSON shape, with next steps
Every command with --json prints exactly one document on stdout, on every exit code including 2 and 5. Logs go to stderr.
{
"schema_version": "cli_output.v1",
"kind": "comparison.v1",
"run_id": "…",
"status": "complete | partial | budget_stopped | cancelled | failed",
"verdict": "pass | fail | inconclusive | invalid | infra_error | null",
"reason": "interval_crosses",
"error": {"code": "moved | input | no_credentials | infra", "message": "…", "retryable": false, "field": "--arms"},
"summary": {"contrast": "skill-none", "effect": 0.12, "ci": [0.03, 0.21], "n_units": 40, "unit": "task",
"method": "cluster-bootstrap", "models": ["anthropic/claude-…@resolved"], "dokimi_version": "0.2.0"},
"subjects": [{"subject": "skill:skills/a", "verdict": "pass"}],
"artifacts": {"run_dir": "results/…", "result": "result.v1.json", "report": "report.md"},
"cost": {"spent_usd": 0.42, "cap_usd": 5.0, "remaining_usd": 4.58, "estimate_kind": "upper_bound", "calls": 118, "priced": true},
"limits": [{"code": "property_of_pair", "text": "novelty is a property of (skill, model); says nothing about task outcomes"}],
"next": [{"argv": ["dokimi", "validate", "results/…/plan.v1.yaml"], "why": "estimate before spend",
"spends": false, "est_usd": 0, "kind": "sequence | alternative"}]
}verdict: nullmeans "this command issues no verdict", for examplereport. A run that measured nothing hasstatus, not anullverdict.nextis ordered.next[].argvis an array, so paths with spaces survive.est_usdcomes fromvalidate's estimator, never from a guess inside a leaf. It is printed in human output too.assesshands over its ladder innext, cheapest first: free checks, then a novelty--dry-run, then a run withnone, the skill andplacebo:auto.data schema list|show KINDprints any artifact schema, includingcli_output.v1,policy.v1and the route tablecli_routes, so an agent can check what it parses.data models list --jsonlists the known models, the aliases (sonnet→ a pinned ID) and the prices.- Moved commands are tombstones, not hidden aliases (§5). For example,
dokimi novelty analyze …printsmoved: dokimi skill novelty analyze …with the argv rewritten, pluserror.code: movedandnext[0].argvunder--json, and exits 2. Tombstones never forward on their own, because forwarding would skip the--dry-runa user would make first. Names that change meaning (run,compare,report) also detect their old argument shapes:run --spec,compare a.json b.jsonwith lint snapshots,report eval FILE. They answer with the tombstone instead of a generic parse error. A name freed in one release is not reused in that same release (§5).
2.8 The library mirrors the loop
The CLI is a thin layer over dokimi.api:
plan(executor, **settings) -> Planvalidate(plan) -> Validationrun(plan) -> Runcompare(*runs, analysis=None) -> Comparisongate(x, policy) -> Verdictreport(x, fmt) -> str
The keyword names are the registry's api_kwargs (§2.4). Handlers stop passing argparse.Namespace around (#4 wave 5: "a typed run config instead of an argparse.Namespace hand-off", for example subjects.py:138). Tests call dokimi.api instead of build_parser().parse_args([...]) (151 call sites today), so the next rename does not touch them.
2.9 CI and the Action are the loop, too
| Action recipe (#121) | Commands |
|---|---|
pr-check |
runs import baseline.tgz → validate plan.yaml --baseline baseline:main → run --plan plan.yaml → compare $RUN baseline:main → gate --policy policy.yaml → report --format pr-comment |
record |
run --plan → gate → runs baseline save main $RUN --expect-previous $OLD (only on status: complete + pass) → runs export main > baseline.tgz |
revalidate |
runs replay baseline:main --set model=NEW → compare → gate. replay --set writes the changed field as varied_variable, so compare accepts the model difference, lists any other differences, and returns invalid only for those (AC-J13-001) |
| catalog (#138) | plan catalog ROOT --changed-from BASE --preset static|live|full → as pr-check; report --per-subject --format record |
Rules for CI:
--max-usdis required whenCI=true.--out-dirhas a stable layout forupload-artifact.- The concurrency key is the repo plus
plan_digest. --seedis recorded. LLM runs stay nondeterministic, sogatedecides on intervals, never on an exact replay.- The inputs of
repo-eval.ymland the proposedskill-eval.ymlare registry names (§2.4), and the workflow inputs are versioned as their own contract.
2.10 Statistics are declared, not chosen silently
The code has four comparison methods today:
- McNemar in
task_runs_report.py;binary_stats.py:91warns that it assumes independent trials; - a task-level sign-flip test in
repos/compare.py; - Holm/BH with
--pool-replicatesinmodel_compare; - judge-score bootstraps.
A compare that dispatches on schema_version alone would pick among them without saying so. With 5 reps, McNemar on trials counts n five times over. So:
-
plan.v1.analysisdeclares:contrasts, for exampleskill-noneandskill-placebo, each confirmatory or exploratory;unit, defaulttask(the cluster);method(auto,mcnemar,sign-flip,cluster-bootstrap,wilcoxon);correction(holm,bh,none) andfamily(per-modelorall);metric, andpass_k(k, and whether it is per task or per trial);stopping(fixed n, or a sequential rule with alpha spending);blockingandrandomization(seeded run order, judge position order, judge blinding to arm names).
-
comparereads the plan's analysis by default.--method,--unit,--contrast,--baseline-arm,--correctionand--metricoverride it, and an override marks the result exploratory. Underauto, the method chosen is printed. Trial-level pairing with reps > 1 is refused unless--unit trialis given. -
comparison.v1records the resolved method, unit,n_units, whether reps were pooled, the family size, raw and adjusted p, the CI method, and each judge with its validity run. -
policy.v1is versioned now (J11/J12: distinct inconclusive verdict and blocking policy (AC-J11-002, AC-J12-002) #99). Each rule has:- a contrast id;
- a statistic (the CI lower bound, not the point estimate);
- a margin (superiority or non-inferiority);
min_n_units;- how
inconclusivemaps to block or warn.
"Beat a placebo" is a rule (
require: skill > placebo). There is a built-in--policy helps: skill > none and skill > placebo, on the lower bound.data schema show policyprints the schema. -
Adaptive stopping is not optional stopping. A budget stop without a declared sequential rule is
inconclusive(reason: budget_stop), never a pass.
3. The journeys under this API
Commands marked new need new code; the rest are moves of existing commands. The journey pages show the short path (about six commands, no plan file); the reference shows everything.
| Journey | Commands |
|---|---|
| J1 Start an assessment | assess ./subject (new). It detects skill / repo / guidance / catalog / task set, runs the free checks, lists the unknowns, and never spends. next holds a cheapest-first ladder; after L2 it also holds a draft plan.v1 and its estimate. Then validate plan.yaml → run --plan plan.yaml |
| J2 Improve my skill | skill routing skills/s [free] → skill novelty analyze skills/s --model M --dry-run → … --max-usd 3 (= $N) → skill shorten skills/s --from $N --candidate-dir cand/ → skill novelty probe cand/ --from $N --edited (the same probes; the declared edit is allowed, AC-J02-001/002) → run arms --arm none --arm skill:skills/s@HEAD --arm skill:cand/ --arm placebo:auto --model M --reps 3 --max-usd 5 → compare $RUN --baseline-arm none → gate $RUN --policy helps → report $N $RUN (novelty joined to effect) |
| J3 Enable a repository | repo inspect · repo scan --fail-on fail · repo budget [free] → eval check .agent-eval/ --out check.json → run repo . --arm guidance:none --arm guidance:raw --cell harness=claude-code,model=… --cell harness=codex,model=… --reps 3 --max-usd 20 --max-usd-per-cell 1 --name pilot → compare pilot --baseline-arm guidance:none --metric root_cause_fixed → report pilot. validate --reuse-check check.json skips the hours of oracle builds |
J4 Check AGENTS.md |
correctness repo scan (repo scan checks readiness: manifest, tasks, environment. repo lint checks the guidance text, guidance_lint.v1 from the local branch: harness size limits, the skill-listing budget, CI commands the guidance never names, and files the harness never reads. They stay two commands so that scan --fail-on fail keeps its meaning); reach repo reach --dry-run, then repo reach --fail-on fail; adherence (proposed, #96); effect as in J3. report $SCAN $REACH $RUN --claims correctness,reach,adherence,effect writes one row per claim and marks the missing ones not measured |
| J5/J6 Third-party skill, my use case | skill check ext/foo → skill routing ext/foo --against skills/ (does it steal routing from mine?) → skill novelty analyze ext/foo; then eval import my-cases/ → run arms --arm none --arm skill:ext/foo --network all=none. Third-party runs default to --network all=none |
| J7 Create or improve evals | eval inspect DIR (format detection, #210) → eval import → eval check (null / oracle / isolation; mutation controls, #97) → eval judge validate (its run id goes into plan.judges[].validity) → eval coverage (new, AC-J07-007) → eval split lock DIR --sealed |
| J8/J9 Which models, cheapest suitable | data models list → plan tasks --model A --model B --model C --reps 5 --out p.yaml → validate p.yaml → run --plan p.yaml → compare $RUN --by model → gate --policy min-quality.yaml (feasible set) → report --sort cost |
| J10 Effect of a change | one run with --arm A --arm B, or compare $RUN1 $RUN2; invalid (exit 4) when the suite hash, prompt digest or model differs outside varied_variable |
| J11 Explain or recover | runs show $RUN (with an explain section: new, it classifies each cell as task fail / eval fail / missing / incomparable) → runs resume $RUN; runs explain prints only that section |
| J12 Regressions in CI | record on main; pr-check on each PR (§2.9). gate exits 1 on a regression beyond the policy, and 3 on inconclusive with reason (AC-J12-001/002) |
| J13 Reassess later | runs replay $RUN --set model=NEW (new, but cheap: the plan is in the run) → compare $RUN $NEW |
| J14 One capability | skill novelty …, skill routing, repo reach, trace inspect, eval judge validate, tools preference. Each has --dry-run, --max-usd and --json; report $RUN re-renders without model calls |
| J15 Solved chat to skill | eval import --from-transcript session.jsonl --out-dir tasks/x/ (proposed; Claude Code JSONL first; the originating task goes into the source split) → skill novelty analyze skills/draft/ → run tasks --tasks tasks/x/ --arm none --arm skill:skills/draft/ --arm selfgen:vendor/skill-creator/ --harness benchflow:claude --reps 3 --max-usd 5 → compare → gate --policy helps |
Every journey uses the same loop verbs. What differs between journeys is the subject group and the executor, as the vision says: "Novelty, routing, repository scans, tool preference and whatever comes next are ways to get sharper or cheaper answers to the same question."
4. Full mapping
No path moves twice. The New column is final for every row. v1's low-level forms (runs compare plugins, experiment recipe evaluate) are gone. The recipes run pipeline|benchmark|subject are permanent names: their implementation changes once there is one answering stack (X), but their names do not. This settles @antonwolfy's M4 concern.
Current 68 leaves, open-PR leaves and planned commands (expand)
Current leaves
| Current | New | Note |
|---|---|---|
run |
docs check |
Can execute doc examples; default output moves out of baselines/ |
compare |
docs compare |
Static snapshots, not model runs (v1 Q3) |
evaluate |
run pipeline |
Becomes a plan recipe after one answering stack (separate issue) |
benchmark run |
run benchmark |
as above |
benchmark batch |
run --plan P1 P2 … |
|
benchmark preflight |
validate |
|
subjects run |
run subject |
recipe |
answers generate |
run answers |
|
arms run |
run arms |
|
arms matrix-run |
run --plan (cells) |
matrix.cells becomes plan.v1.cells |
tasks run |
run tasks |
|
explore run |
run localization |
|
repo run |
run repo; repo run stays as a thin shortcut |
At L6, when run has a real dispatcher: one implementation, two names (antonwolfy, Q4) |
tasks harnesses |
run tasks --list-harnesses |
|
arms plugin-delta |
compare |
dispatch on arms.v1 |
repo compare |
compare; repo compare stays as a shortcut |
Its comparator keeps the pairing, the sign-flip test, headroom flags and pin warnings; --baseline, --metric and BATCH/ARM selectors carry over (§2.10) |
baseline compare |
compare $RUN baseline:NAME |
|
report model-compare |
compare --by model, then report |
Statistics leave the renderer |
baseline save|list |
runs baseline save|list |
Any kind |
report eval |
report |
Numbers reconciled with generate in its own PR (#4 wave 4) |
report generate |
report |
|
repo report |
report; repo report stays as a shortcut |
at L6 |
dashboard generate |
report --over RUN… |
|
novelty view |
report $RUN |
|
novelty analyze|atoms|probe|items |
skill novelty … |
items needs ≥3 probe runs |
novelty routing |
skill routing SKILL [--against ROOT] |
One skill's routing; the all-vs-all form is catalog routing |
novelty shorten |
skill shorten |
Writes a candidate |
catalog scan |
catalog check |
--fail-on, --json (#163), --changed-from BASE. skill check SKILL runs the same checks scoped to one skill (a new leaf over the same code) |
catalog trigger-set |
eval triggers generate |
Can call an LLM; --format reach|skill-creator|questions becomes --dialect |
library list|show |
catalog list|show --kind product |
|
library intents |
catalog intents |
|
subjects list|show |
catalog list|show --kind subject |
Needs a default root |
personas discover |
eval personas discover |
|
personas approve |
eval personas validate |
No approval exists |
questions generate|analyze|refine |
eval questions … |
|
questions panel-review |
eval questions review |
|
tasks list |
eval tasks list |
|
explore list |
eval tasks list --format localization |
|
repo tasks check |
eval check |
Plus repo validate --manifest |
repo validate |
eval check --schema-only |
|
eval score |
eval score pointwise |
|
eval pairwise|panel-score |
eval score pairwise|panel |
|
eval checklist|grounding|ragas |
eval score … |
|
eval judge-validity |
eval judge validate |
Exit 3 on a cap without a decision |
eval judge-wording |
eval judge wording |
|
eval cross-judge |
eval judge agreement |
|
repo fetch|inspect|scan|budget|reach |
unchanged | repo scan and repo reach write run directories; repo reach --fail-on |
tasks inspect (#210, merged) |
eval inspect |
|
catalog formats (#211, merged) |
catalog formats |
|
tools preference |
unchanged | |
context trace |
trace inspect |
|
dataset export |
data export |
|
ingest docling |
data import docling |
|
product (alias) |
removed |
Open PRs
| PR | Spelling in the PR | New |
|---|---|---|
| #214 | questions import, eval native-import |
eval import (format detected; --format overrides) |
| #214 | eval reference |
eval score reference |
| #217 | catalog trigger-probe |
catalog trigger-probe ROOT [--only SKILL], labelled [paid] (real claude -p) |
| #155 | context consistency |
trace consistency |
| #155 | repo compare --pass-k, --path-stability |
compare options |
| #218 | tasks run --network |
shared --network |
| #213 | tasks run --swe-bench rows.jsonl |
run tasks --tasks-from rows.jsonl (format detected by eval inspect) |
| #107 | repo run --delegation |
shared --delegation |
| #174 | arms run --session-turns |
run arms --session-turns |
| #172/#175 | novelty routing --all-vs-all, --repeats |
catalog routing ROOT, --reps |
Planned (#121, #138, #220, #97, #96, J1/J11/J13)
| Planned | New |
|---|---|
dokimi plan (#121) |
plan <executor> |
dokimi validate (#121) |
validate |
dokimi compare (#121) |
compare |
dokimi gate --policy (#121, #99) |
gate --policy |
catalog eval --changed-from --tier (#138) |
plan catalog ROOT --changed-from BASE --preset static|live|full (composite executor, §2.2) |
report --format pr-comment (#138) |
report --format pr-comment |
repo lint (local branch) |
repo lint |
repo inspect --base |
repo inspect --base REV |
| mutation controls (#97) | eval check --mutations |
| coverage gaps (AC-J07-007) | eval coverage |
| task drafting (AC-J07-006, AC-J15-001) | eval import --from-transcript|--from-repo (writes hypotheses) |
| adherence (#96) | run repo --measure adherence or a repo diagnostic, after #96 decides |
dokimi improve (#220 B) |
skill improve (writes a candidate patch, never the skill repo) |
| split lock, sealed set (#220 A) | eval split lock|status + plan.v1.splits + validate refusing a burned sealed split |
| J1 single entry | assess |
| J11 explain | an explain section in runs show; runs explain prints only that |
| baselines on CI runners (§2.3) | runs export|import, baseline:FILE|URL, baseline save --expect-previous |
| housekeeping | runs rm, runs gc |
| model discovery | data models list |
| per-subject records, badges (#138) | report --per-subject --format record|badge|site-json |
| default "does it help" policy | gate --policy helps |
| J13 replay | runs replay |
5. Plan
Conventions from #4 still apply: one purpose per PR; renames, refactors and number-changing changes are never mixed; every PR that touches cli.py or a flag adds a changelog.d fragment.
Migration policy: tombstones for one minor release. v0.1.0 is a GitHub release with no PyPI upload, and no consumer outside this repo was found. The one pinned caller, oneDAL #3818, pins repo-eval.yml by SHA, so it is unaffected until it bumps the pin. @antonwolfy proposed renaming with no deprecation before 0.1.0, but 0.1.0 has since shipped. So each moved path parses its old argv, prints the new command with the argv rewritten, and exits 2 (§2.7). That still gives agents and scripts a message they can act on. Tombstones are removed one minor release later. A name is not reused in the release that frees it: run, compare and report get their loop meaning only in the release after the docs lint, the snapshot diff and the old reports have moved out.
| Stage | PRs | Depends on |
|---|---|---|
| A groundwork | A1 route table as data (cli_routes, printed by data schema show cli_routes), tombstones, sectioned help, [free]/[paid] labels. A2 the exit-code table (§2.6) as a full audit of the 69 exit(1) and 28 exit(2) calls in commands/. For example, a missing input file still exits 1 in questions, evaluate and personas approve (personas.py:79). The top-level handler maps uncaught exceptions to 5. A2 changes exit codes users see, so it has its own changelog fragment. A3 hygiene: cmd_report, and the four python cli.py hints left (personas.py:70,100, baseline.py:188,262) |
— |
| F flags (§2.4) | F1a the cli_flags.py registry, the contract test in ratchet mode (allowlist = today's 222 strings), the parity tests, and the cli-interface.json snapshot in #88's CI. No flag changes yet, so new drift stops from here on. F1b one PR per concept: output; model/judge/role models; spend + dry-run; tokens; fail-on; reps/concurrency; network/timeout/cache; arms/cells/baseline. Each PR moves the leaves to the registry, tombstones the old spellings, updates docs/skills/workflows, adds a test that the old and new argv parse to the same config, and deletes its allowlist rows. F1c the test turns strict: banned synonyms are allowed only as tombstones, and local flags must pass the stem rule. F2 --json → cli_output.v1 on every leaf, a test that every leaf is wrapped by @emits_cli_output, and data schema |
F1a: A1. F1b/F1c = #4 wave 5. F2: F1a |
| M renames (handler-neutral) | M1 eval (21 leaves + #214). M2 skill, catalog (including catalog routing and catalog trigger-probe), trace, tools, data. M3 docs (frees run, compare, report for the next release). Fix the two tests that assert dest (test_matrix_rollup.py:163 args.arms_cmd, test_novelty_cli.py:33 args.novelty_cmd), because E1's "mismatches=0" does not cover them |
F1a; may land before #4 wave 4, since it touches only commands/* and cli.py |
| R records | R1 the envelope in save_artifact, for run kinds only. Input kinds (harness_profile, repo_manifest) are untouched. The four schemas with top-level additionalProperties: false (harness_profile, repo_manifest, repo_readiness, tool_preference_groups) are widened explicitly, one by one. runs list/show accept files without an envelope. R2 run directories, the runs root, --name, and result.v1 on interrupt. R3 runs list|show|rm|gc, runs baseline with --expect-previous, runs export|import. R4 route the writers that bypass save_artifact (panel, ragas, grounding, judge reliability in eval/*) through it |
R4 after #4 wave 4 |
| L loop | L1 plan.v1 extended (§2.2), with analysis and policy.v1 (§2.10), agreed with #161's author and reviewer. L2 plan <x> / run <x> / run --plan for arms, tasks, answers, localization, plus validate with the estimate, judge validity and --reuse-check. L3 the compare dispatcher with declared statistics (#121 item 2, #4 wave 4 eval/compare.py). L4 gate --policy and --policy helps (#99). L5 the report renderers, including --per-subject and --claims. L6 run repo, compare and report take over the implementation of repo run/compare/report, which stay as thin shortcuts over the same handlers (one implementation, two names) |
R2, #161 |
| N new commands | assess (free-only first; after L2 it adds the costed draft plan), runs explain, runs replay, eval coverage, eval split, data models list, catalog composite executor, placebo:auto |
assess: A1 + F2. Others: L2 |
| X separate issues | #228 one answering stack (parity test first); the recipes in §2.2 wait for it. #229 the other duplicated subsystems, each tagged with its stage here: the rounded-p significance bug and the report eval/generate split before L3/L5, digests before R1, spend accounting with F1b. Also skill improve (#220), which absorbs skill shorten |
L2 recipes: #228 |
| Z removal | remove tombstoned commands and flags and the product alias |
one minor release after M, F1b and L6 |
Order:
- A1, A3 and F1a now.
- A2 as the full audit.
- M1–M3 after A1/F1a.
- F1b concept by concept, as Architecture cleanup: fix live bugs, then deepen the LLM, eval-record, novelty, arms and process modules #4 wave 5.
- R1–R3; R4 after wave 4.
- L1 with feat(runner): plan.v1 / result.v1 and engine-computed completeness #161; L2–L6.
- N.
6. Decisions
Settled by the v1 review (please object here if not):
| v1 question | Decision |
|---|---|
Q1/Q9 eval vs evals |
eval, singular. runs stays plural because run is a loop verb, and run cannot be a root while the old docs-lint run still has a tombstone (antonwolfy's reason) |
Q2 executor positional or --type |
Positional (E2) |
| Q3 static snapshot comparison | docs compare |
Q4 repo run/compare/report |
Stay in repo until L6, then become thin shortcuts over the loop verbs: a second name stays, a second implementation does not (antonwolfy) |
| Q5 cheapest adequate model | compare reports cost against quality; gate filters; report --sort cost orders. Nothing selects for the user |
| Q6 runs root | results/, overridden by DOKIMI_RUNS_DIR; runs list scans, and the index is only a cache |
| Q7 ordering against #4 | M before wave 4; R4 and L3 after; F1b/F1c are wave 5 |
| Q8 deprecation | Tombstones for one minor release, because 0.1.0 has shipped (§5) |
Open for this round. Each has the context, the options, and a recommendation. The body above is written as if every recommendation is accepted, so a "no" here names the sections to revert. Votes from the persona walkthrough (§7) are in brackets.
D1. Where the loop verbs live.
Context: #121, the skills and the Action already spell dokimi plan|validate|compare|gate. v1 put them under experiment/runs. Every journey in §3 passes through them.
Options: (a) top-level roots; (b) an experiment group; (c) top level, but each verb lands only when a real dispatcher exists behind it.
Recommendation: (c), as in §5 L. [6/6 for top level. The repo maintainer and the researcher make it conditional on real dispatchers.]
D2. One plan schema, and how big.
Context: v1 proposed experiment_plan.v1 beside #161's plan.v1. The personas found v2's five added fields too few. Catalog plans need a per-cell executor. Repo runs need (harness, model, bin) cells and two caps. Statistics need a declared analysis. CI needs a version and a digest.
Options: (a) extend plan.v1 with the five fields only; (b) keep two schemas; (c) extend plan.v1 with everything in §2.2.
Recommendation: (c). This needs #161's author and reviewer to agree. [6/6 for one schema; 4 personas found (a) too small.]
D3. Exit codes.
Context: argparse uses 2 for usage errors, so #121's 0–4 cannot hold. CI must tell "regression" from "crashed" from "not enough data".
Options: (a) 0 pass, 1 fail, 2 input, 3 inconclusive, 4 invalid, 5 infra_error, on verdict commands only; (b) the same table on every command that runs work, with crashes mapped to 5 and reason codes; (c) #121's 0–4.
Recommendation: (b), as in §2.6. Under (a) a crash exits 1 and blames the PR, and run exits 0 on a Docker failure. [6/6 for 3/4/5; the CI engineer and the coding agent required (b).]
D4. Tombstones or working aliases.
Context: there are no installed users, and the one external pin is a SHA. run, compare and report change meaning, so a tombstone cannot catch an old call to them.
Options: (a) tombstones for moved paths; (b) aliases for one release; (c) tombstones, plus detection of the old argument shapes, error.code: moved in JSON, and no reuse of a freed name in the same release.
Recommendation: (c), as in §2.7 and §5. The repo shortcuts of L6 are the one deliberate exception, per Q4. [6/6 for tombstones.]
D5. assess as a root.
Context: J1 is the only journey with no entry today, and agents and new users reach for it first. It is new code. The maintainer persona sees little value for users who already know their subject.
Options: (a) root, after L2; (b) root, early, free-only, with a cheapest-first next ladder; the costed draft plan after L2; (c) defer, ship runs explain first.
Recommendation: (b). [5 yes, 3 of them "later"; 1 defer.]
D6. catalog as a root, and what it owns.
Context: #138 treats a skill collection as a subject. v2 gave catalog only list/check, and hid the catalog-wide operations behind flags elsewhere.
Options: (a) a root with list/check only; (b) data catalog; (c) a root, with the rule that the positional is what the result is keyed by: catalog routing, catalog trigger-probe, plan catalog, and skill routing SKILL --against ROOT.
Recommendation: (c), as in §2.1. [5 yes, 1 no view.]
D7. cli_output.v1 on every leaf.
Context: "agents are first-class users", and today each command prints its own JSON shape. Under v2's envelope, the coding agent could not answer "does it help?" from stdout.
Options: (a) v2's envelope; (b) minimal, without next; (c) the envelope in §2.7, with status, reason, error, summary, subjects[], cost.remaining_usd, next[] as argv, and emitted on every exit code.
Recommendation: (c). [6/6 yes; the maintainer warns that est_usd must come from one estimator, which §2.7 now requires.]
D8. Statistics in compare and gate.
Context: there are four methods in the code, and a dispatcher that picks among them silently. With 5 reps, McNemar on trials counts n five times over.
Options: (a) dispatch silently; (b) flags only; (c) declared in plan.v1.analysis, recorded in comparison.v1, the task as the default unit, trial-level pairing refused with reps, overrides flagged exploratory, and policy.v1 versioned now.
Recommendation: (c), as in §2.10.
D9. Baselines on ephemeral runners.
Context: a baseline pointer under results/ disappears with the runner, and two nightlies can race on it.
Options: (a) leave storage to the user; (b) runs export/import bundles that are content-addressed and verified, baseline: as a file or URL, and save --expect-previous; (c) a dokimi-hosted store.
Recommendation: (b), as in §2.3. Where records live stays open (#121 Q1).
D10. How strictly flags are enforced.
Context: the audit in §2.4 shows 222 option strings. Three names (--max-tokens, --out, --model) carry different meanings, and --judge has two types. v2 said only that CI "can check" the vocabulary. Without enforcement, every new command adds a spelling: open PRs already add -o, --trials and a third JSON convention.
Options:
- (a) review only: a written vocabulary and reviewers catch drift;
- (b) a lint that warns on unknown flags;
- (c) a registry plus a contract test, in ratchet mode first (today's state allowed, new drift fails), then strict once F1b lands; with parity tests across the flag,
plan.v1,dokimi.api, the env vars and the workflow inputs, and the snapshot diff in ci: report user-facing CLI surface changes on every PR #88's CI; - (d) strict from the first PR, migrating all 222 strings at once.
Recommendation: (c). (a) has already failed: the drift happened under review. (d) mixes renames with each other, against #4's one-purpose rule, and blocks every open PR at once.
7. Persona walkthrough (2026-10-25)
Six agents, each playing one user, wrote out their journey with the v2 commands: skill author (J2/J15), repo maintainer (J3/J4), cold coding agent (J1/J14), CI engineer (J12/J13), eval researcher (J7/J8) and catalog maintainer (#138). They agreed on the shape (all six voted for D1–D4 and D7). Every one of them got stuck where a loop verb was named but the flags or fields that make it answer something were not. Each finding below is now in the body:
| Finding (who) | Where it went |
|---|---|
Unclear whether the skill is a positional, --artifact or --arm (author, catalog) |
§2.1 positional rule |
| No command to re-probe an edited skill on the same probes (author) | §3 J2: skill novelty probe … --from --edited |
Paying twice for atoms; no --cache-dir (author) |
§2.3 --from RUN; §2.4 --cache-dir |
No budget across commands; unclear precedence between plan.budget and --max-usd (author, agent, catalog) |
§2.4 spend row and precedence rule; §2.7 cost |
| Per-cell cap lost (maintainer) | §2.4 --max-usd-per-cell; §2.2 budget |
| Harness × model cross wrongly (maintainer) | §2.4/§2.5 --cell; §2.2 cells[] |
| No run name; awkward resume; silent fork on a re-run (maintainer) | §2.2 --name, digest check |
validate self-checks take hours (maintainer) |
§2.2 --self-check, --reuse-check |
| No baseline arm; no default policy; hand-written placebo (author, agent, researcher) | §2.4 --baseline-arm; §2.10 --policy helps; §2.5 placebo:auto |
skill: has no revision (author) |
§2.5 skill:PATH@REV |
--out-dir overloaded; --tier means two things (author) |
§2.4 --candidate-dir; --preset |
run exits 0 on a Docker failure; a crash exits 1 (CI, agent) |
§2.6 |
| One exit 3 for different causes (CI, agent) | §2.6 reason |
No error/status/summary; next as a shell string (agent) |
§2.7 |
| Tombstones collide with real usage errors and can't catch reused names (CI, agent) | §2.7 shape detection, error.code: moved; §5 no same-release reuse |
| Baselines die with the runner; pointer race (CI) | §2.3, §2.9, D9 |
revalidate returns invalid every time (CI) |
§2.9 varied_variable from replay --set |
JSON shape changes break repo-eval.yml (CI) |
§2.4 snapshot, step 4 |
compare picks its method silently; McNemar with reps (researcher) |
§2.10, D8 |
plan.v1 can't declare contrasts, blocking, stopping or a judge panel (researcher) |
§2.2, §2.10 |
| Judge calibration not tied to the run; sealed splits enforced only by a plan field (researcher) | §2.2 judges[].validity; §2.3 eval split |
| Catalog-wide operations hidden behind flags; static tier is not a plan (catalog) | §2.1 catalog routing|trigger-probe; §2.2 composite catalog executor |
| No per-skill records; record key too small (catalog) | §2.3 records |
repo lint vs repo scan undefined; skill check unmapped (maintainer, catalog) |
§3 J4; §4 |
| "Four verdicts in one report" not deliverable (maintainer) | §3 J4 report --claims with not measured rows |
| No model listing (agent) | §2.7 data models list |
No source split for J15; transcript formats unclear (author) |
§2.2 splits; §3 J15 |
| Large run directories (maintainer) | §2.3 runs rm, runs gc |
runs show/explain/report overlap (author) |
§3 J11: explain is a section of show |
skill shorten and the planned skill improve overlap (author) |
§5 X: skill improve --strategy shorten |
| 15 roots are too many for six commands (author, maintainer) | §3 short path on the journey pages |
Kept open for M1 review: the researcher's rename of eval score/eval judge to eval score --mode … --judge … (several judges = a panel) and eval grader {check, validate, agreement, wording}, covering deterministic and LLM graders under one noun. It is a good fit but not a blocker, and it changes 10 leaf names.
Pushed back on: the maintainer suggested dropping next from most leaves, because the costs would drift. §2.7 keeps next but requires every estimate to come from validate's estimator.
8. @antonwolfy's review: where each point went
| Review point | Status in v2.1 |
|---|---|
23 roots + the product alias, not 25 |
Fixed (Why) |
A2 is a full audit, changes visible exits, needs its own fragment; missing input exits 1 in questions, evaluate, personas approve |
§5 A2, re-counted on main: 69/28 |
datasets_export.py:147 hint already fixed; remaining python cli.py hints |
§5 A3: four left (personas.py:70,100, baseline.py:188,262; the model_compare.py one is gone) |
Two tests assert dest |
§5 M |
| M4 double migration | §4 "No path moves twice"; v2 has no M4 |
The wave 4 freeze covers B1b (the eval/* writers); B2 is wave 5 |
§5 R4 after wave 4; F1b/F1c are wave 5 |
B1: run artifacts vs input kinds; widen the four additionalProperties: false schemas; runs list accepts files without an envelope |
§5 R1 |
| D4 (one answering stack) deserves its own issue | #228; the other duplication is in #229 |
Q1/Q9 eval, and why runs stays plural |
§6 settled table |
Q2 positional, Q3 docs compare, Q5 compare reports and gate selects |
§6 settled table |
Q4 keep repo run/compare/report until dispatchers exist, then thin shortcuts |
§4, §5 L6, §6 (v2 had tombstones here; changed to shortcuts as proposed) |
| Q6 scan the root; the index is only a cache | §2.3 |
| Q8 no deprecation before 0.1.0 | 0.1.0 has shipped, so one release of tombstones (§5) |
| Suggested order | §5 order |
Definition of done
- Every operation in §4 is reachable at its new path. Old paths are tombstones for one minor release, then removed.
repo run/compare/reportare shortcuts over the same handlers. dokimi --helpshows the 15 roots in four sections, with[free]/[paid]labels, and the generated CLI reference matches.- Every run writes a run directory with
run.json,plan.v1andresult.v1, including interrupted runs.runs list|show|replay|export|importwork over every run kind.compare,gateandreportdispatch onschema_version, andcomparison.v1records the method and unit. - The flag contract test is strict: every shared concept comes from
cli_flags.py, every local flag is in the reviewed list, one name has one meaning, and the parity tests (plan.v1,dokimi.api, env, workflow inputs) pass.cli-interface.jsonis current. --jsonemitscli_output.v1on every leaf and every exit code.- Every command that runs work follows the §2.6 table, no crash exits 1, and no
inconclusiveexits 0. - The J1–J15 docs pages use only the new commands and show the short path, and
test_docs_code_refsplus thejourneys.yamlresolver pass.
Activity
antonwolfy commentedon Oct 19, 2026
The diagnosis holds and the nine groups look right; the concerns are about ordering and a few contracts, not the tree. I checked the claims against
mainat 964c87f.Corrections
productalias, not 25.baseline". The 0/1/2 rule exists only as a comment indokimi/cli.py:117, andcommands/has 83exit(1)against 20exit(2). A missing input file also exits 1 inquestions(commands/questions.py:45),evaluate(commands/evaluate.py:117,169) andpersonas approve(commands/personas.py:78). A2 is an audit of every command, it changes exit codes users see, and it needs its own changelog fragment.datasets_export.py:147hint is already fixed. It saysdokimi dataset export. Thepython cli.pyhints that remain arepersonas.py:70,100,baseline.py:143,170andmodel_compare.py:916.dest, so stage M is not quite handler-neutral:test_matrix_rollup.py:163(args.arms_cmd) andtest_novelty_cli.py:33(args.novelty_cmd). This is small, but E1's "mismatches=0" does not cover it.On the plan
runs compare pluginsbecomes theruns comparedispatcher, andexperiment recipe evaluatebecomes a plan recipe. Either decide now that the subcommand forms are permanent, as the explicit low-level path, or land M4 together with C/D. "Can stay" leaves users unsure whether they will move twice.eval/*andreport/*. Stage M touches onlycommands/*andcli.py, so it can proceed. B1b, though, reroutes writers that live ineval/*(panel, ragas, grounding, judge reliability), so it waits for wave 4. B2 should be wave 5 itself rather than a parallel effort.save_artifactstamps every kind inSCHEMA_VERSIONS, butharness_profileandrepo_manifestare inputs; arun_idor cost there means nothing. Four schemas also set top-leveladditionalProperties: false:harness_profile,repo_manifest,repo_readinessandtool_preference_groups. Adding an optional envelope is a widening, so it can be done in place, but each of those schemas has to be widened explicitly.runs list/showmust accept files that have no envelope.EvaluationPipeline/AnswererandArmRunnerinto one answering stack is the largest refactor in the plan, and "blocked on" hides its size.On the open questions
Q1 / Q9: (b), name the group
eval. Singular everywhere has one forced exception:runcannot be a root while the oldrun(the docs lint) is still a deprecated alias. That collision is worse thaneval/evals, sorunsstays plural. I'd write that reason down so Q9 has an answer.Q2: a positional
experiment run <executor>. E2's numbers (94 flags, 57 unique) settle it.Q3:
docs compare. Static snapshots are not model runs.Q4: keep
repo run/compare/reportinrepoas ordinary commands, not deprecated aliases, until C/D bring real dispatchers. Rule 1 is a weak fit here.repo runhas 28 flags, 26 of them unique, plus container graders, resume and network;repo comparekeeps its own pairing and sign-flip test. At M4,experiment run repoandruns compare repowould be the same handlers under new names, with no shared code behind them. That splits a flow that is coherent today across four groups for naming alone, and an epilog inrepo --helpis a weaker route than a command that is already in the group. Once theexperiment run/runs comparedispatchers exist, therepocommands can become thin shortcuts over them: a second name stays, a second implementation does not. So I'd take the threerepoleaves out of M4.Q5:
runs compareshould only report cost against quality. Selection isruns gatethresholds plus ordering by cost; otherwise the comparator starts making decisions.Q6:
results/by default, withDOKIMI_RUNS_DIRoverriding it. On the index: if onlyexperiment runwrites it, directevals score,repo reachortools preferenceruns never appear inruns list, and a partial history is worse than none. But writing it fromsave_artifact(17 call sites) has its own problems:evaluatewritesanswersandeval, andsubjects runwritessubject_scorecardandtask_runs. The index then holds artifacts, not runs, and has to be grouped byrun_id.--outcan point anywhere, so it is unclear which index an artifact outside the runs root belongs to.repo run -jand the arms threads need a lock.I'd have
runs listscan the runs root and readschema_version(andrun_idwhen present), withindex.jsonlonly as a cache that can be rebuilt. That needs nothing from writers, and it also lists files written before the envelope existed.Q8: no consumer outside this repo. Nothing has been released either, so a deprecation period has no release to run over and no installed users to protect. I'd rename before 0.1.0 with no deprecation at all.
Suggested order: A1 and A3 now. A2 rewritten as a full audit. M1–M3 after A1. B1/B1b after wave 4, and B2 as wave 5. M4 only with C/D, or with an explicit decision that the subcommand forms are permanent.
feat(research): every open issue accounted for; untracked list for en…
Add a research register for ideas, experiments and decisions (#139)
[-]CLI restructure: nine task-oriented groups over the existing 64 operations (proposal and plan)[/-][+]CLI API: the evaluation loop at the top level, subject and eval groups below (proposal v2)[/+][-]CLI API: the evaluation loop at the top level, subject and eval groups below (proposal v2)[/-][+]CLI API: the evaluation loop at the top level, subject and eval groups below, one enforced flag vocabulary (proposal v2.1)[/+]