Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cea0066
feat(cli): route table as data, sectioned help, cost labels, data schema
napetrov Oct 29, 2026
cf5166b
fix(cli): tombstone check follows groups, aliases and roots
napetrov Oct 29, 2026
a654a8b
fix(cli): tombstone on a path under a live leaf; no stray space in la…
napetrov Oct 29, 2026
08ac435
fix(cli): tombstones match alias argv, reject an empty or option old …
napetrov Oct 29, 2026
84bb4ff
Merge branch 'main' of https://github.com/intel/dokimi into pr284-work
napetrov Oct 29, 2026
3bbfc63
fix(cli): add the two routes main gained to the route table
napetrov Oct 29, 2026
55601ae
fix(cli): pin the cli_routes.v1 document with a schema
napetrov Oct 29, 2026
1f26a76
fix(cli): say in the data help what data schema prints today
napetrov Oct 29, 2026
f97bb3e
refactor(cli): drop the routed flag, build subparsers as plain Argume…
napetrov Oct 29, 2026
49df9a8
test(cli): pin the cost labels, the route document and the tombstone …
napetrov Oct 29, 2026
fe1c6a7
test(cli): name the leaves whose help line was checked, not count the…
napetrov Oct 29, 2026
c8d0eba
test(cli): cover the empty argv in moved_argv
napetrov Oct 29, 2026
eb5e612
Address review: the summary items are answered by the commits of thei…
napetrov Oct 29, 2026
50a9ab9
test(cli): pin the help width the top-level cost label is read at
napetrov Oct 30, 2026
79cc91d
Merge remote-tracking branch 'intel/main' into feat/cli-routes-table
napetrov Oct 30, 2026
c0c3f15
test(cli): fit the leaf-without-help comment in the two-line limit
napetrov Oct 30, 2026
dd60110
test(cli): pin each root's help section with a literal
antonwolfy Oct 30, 2026
3b224b6
docs(cli): describe the data schema group, not its one kind
napetrov Oct 30, 2026
2cf13d0
Merge remote-tracking branch 'intel/main' into feat/cli-routes-table
napetrov Oct 30, 2026
1eeeae8
test(cli): an unknown schema kind exits 2 before the handler runs
napetrov Oct 30, 2026
f551d36
docs(cli): say what the data and schema groups hold, not one kind
napetrov Oct 30, 2026
9a79713
chore(cli): regenerate the CLI snapshot on current main
napetrov Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions dokimi/changelog.d/20261028_120000_napetrov_cli-routes-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### Added

- **CLI route table** — the top-level `dokimi --help` lists the commands by
section (The loop, Subjects, Evals, Analysis and data), and every command's
help line ends in `[free]` or `[paid]`: `paid` when the command can make a
model call in some flag combination. The table lives in `dokimi/cli_routes.py`;
`dokimi data schema show cli_routes` prints it as JSON, and
`schemas/cli_routes.v1.json` pins its shape.
- **`dokimi data schema list|show KIND`** — the new `data` command group; `show`
prints a schema kind as JSON. `cli_routes` is the only kind for now.
- **Tombstones** — the route table can name an old argv prefix that moved; running
it prints `moved: dokimi <new command>` and exits 2 without running anything.
No command has moved yet, so the table is empty.

### Changed

- `dokimi run` and `dokimi compare` have a one-line help in the top-level list.
Their parsers have no help text, so the route table supplies it.
116 changes: 116 additions & 0 deletions dokimi/docs/reference/cli-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3512,6 +3512,122 @@
}
}
},
"data": {
"allow_abbrev": true,
"params": [
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi data",
"subcommand_required": true,
"subcommands": {
"schema": {
"allow_abbrev": true,
"params": [
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi data schema",
"subcommand_required": true,
"subcommands": {
"list": {
"allow_abbrev": true,
"params": [
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi data schema list",
"subcommand_required": null,
"subcommands": {}
},
"show": {
"allow_abbrev": true,
"params": [
{
"action": "_StoreAction",
"choices": [
"'cli_routes'"
],
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "kind",
"kind": "argument",
"nargs": null,
"position": 0,
"required": true,
"type": null
},
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi data schema show",
"subcommand_required": null,
"subcommands": {}
}
}
}
}
},
"dataset": {
"allow_abbrev": true,
"params": [
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"result": "result.v1",
"repo_budget": "repo_budget.v1",
"task_inspect": "task_inspect.v1",
"cli_routes": "cli_routes.v1",
}

# kind -> superseded versions still accepted on *load*, newest first. An artifact written under an
Expand Down
75 changes: 71 additions & 4 deletions dokimi/dokimi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
catalog,
context,
dashboard,
data,
dataset,
evaluate,
explore,
Expand All @@ -35,11 +36,71 @@
tasks,
tools,
)
from dokimi import __version__, process
from dokimi import __version__, cli_routes, process
from dokimi.console import tolerate_console_encoding
from dokimi.defaults import JUDGE_MODEL_FOR_PROVIDER, default_judge_model


def _subparsers(parser: argparse.ArgumentParser) -> argparse._SubParsersAction | None:
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
return action
return None


def _route_help(text: str | None, path: tuple[str, ...]) -> str:
"""The one-line help of a route with its cost label: the parser's text, else the table's."""
base = text or cli_routes.summary_of(path) or ""
cost = cli_routes.cost_of(path)
return " ".join(part for part in (base, f"[{cost}]" if cost else "") if part)


def _label_routes(parser: argparse.ArgumentParser, prefix: tuple[str, ...] = ()) -> None:
"""Append the cost label to every subcommand's help line, at every depth."""
sub = _subparsers(parser)
if sub is None:
return
listed = {action.dest: action for action in sub._choices_actions}
visited: set[int] = set()
for name, child in sub.choices.items():
if id(child) in visited: # an alias: the same parser under a second name
continue
visited.add(id(child))
path = (*prefix, name)
if name in listed:
listed[name].help = _route_help(listed[name].help, path)
_label_routes(child, path)


class _RoutedParser(argparse.ArgumentParser):
"""The top-level parser. Its ``--help`` lists the roots by section (``dokimi.cli_routes``)."""

def format_help(self) -> str:
sub = _subparsers(self)
listed = {action.dest: action for action in sub._choices_actions} if sub else {}
fmt = self._get_formatter()
fmt.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
# A parser root missing from cli_routes.ROOTS is left out of this list;
# test_route_table_matches_the_parser_leaves_and_roots fails on it.
for section in cli_routes.SECTIONS:
roots = [root for root in cli_routes.ROOTS if root.section == section]
if not roots:
continue
fmt.start_section(section) # the formatter adds the colon
for root in roots:
action = listed.get(root.name)
if action is None: # a route with no help= in its parser: listed from the table
action = argparse.Action([], root.name, help=_route_help(None, (root.name,)))
if root.aliases:
action.metavar = f"{root.name} ({', '.join(root.aliases)})"
fmt.add_argument(action)
fmt.end_section()
fmt.start_section("options")
fmt.add_arguments([action for action in self._actions if action.option_strings])
fmt.end_section()
return fmt.format_help()


def build_parser() -> argparse.ArgumentParser:
"""Build CLI argument parser."""

Expand All @@ -53,9 +114,9 @@ def positive_int(value: str) -> int:
raise argparse.ArgumentTypeError(f"must be >= 1, got {value}")
return ivalue

p = argparse.ArgumentParser(prog="dokimi")
p = _RoutedParser(prog="dokimi")
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
sub = p.add_subparsers(dest="cmd", required=True)
sub = p.add_subparsers(dest="cmd", required=True, parser_class=argparse.ArgumentParser)

# Order preserved to keep --help output stable.
run.register(sub, positive_int) # run
Expand All @@ -81,7 +142,9 @@ def positive_int(value: str) -> int:
repo.register(sub, positive_int) # repo (repositories as subjects)
tools.register(sub, positive_int) # tools (tool-choice probes)
context.register(sub, positive_int) # context (context window of a run)
data.register(sub, positive_int) # data (schema; M2 adds export, import and models)

_label_routes(p)
return p


Expand All @@ -92,7 +155,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
read once every flag is parsed, so the two flags can come in either order.
"""
parser = build_parser()
args = parser.parse_args(argv)
raw = list(sys.argv[1:] if argv is None else argv)
moved = cli_routes.moved_argv(raw)
if moved is not None: # a tombstone: say where the command went, do not run anything
parser.exit(2, cli_routes.moved_text(moved) + "\n")
args = parser.parse_args(raw)
if getattr(args, "judge_model", None) is JUDGE_MODEL_FOR_PROVIDER:
model = default_judge_model(args.judge_provider)
if model is None:
Expand Down
Loading
Loading