Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
af07f85
feat(llm): claude-cli provider: run the Q&A track through a logged-in…
napetrov Oct 27, 2026
c3c88ab
fix(llm): claude-cli reaches the answerer and generators, refuses sam…
napetrov Oct 27, 2026
aab69f7
fix(judge): refuse --judge-temperature and --seed for a CLI judge bef…
napetrov Oct 27, 2026
e9f759a
fix(judge): the CLI sampling refusal covers eval panel-score too
napetrov Oct 27, 2026
ea38db4
test(llm_cli): keep the missing-binary test's work dir in tmp_path
antonwolfy Oct 27, 2026
cb98628
docs(spec): update the measured doc count to 33
antonwolfy Oct 27, 2026
35c5219
fix(llm): claude-cli review: key, work dir, sampling, model, login wa…
napetrov Oct 28, 2026
fff2f19
docs(subscription-backends): name the per-user work dir
antonwolfy Oct 28, 2026
d590d3c
Merge intel/main into feat/claude-cli-backend
napetrov Oct 28, 2026
9147dfa
fix(llm): claude-cli refuses what it cannot run, and keeps its env to…
napetrov Oct 28, 2026
2c491ec
fix(llm): review round 1 on claude-cli: import, identity, env, tests
napetrov Oct 28, 2026
d14a762
test(llm): pin the arms entry point, the session default, the login env
napetrov Oct 28, 2026
4e33134
test(llm): the entry-point refusal is observed through the runner start
napetrov Oct 28, 2026
8d65d10
test(llm): lint and bandit fixes in the new tests
napetrov Oct 28, 2026
4937a07
fix(llm): record the model a CLI alias resolved to, and key the cache…
napetrov Oct 29, 2026
1688652
test(llm): drop the raising=False on the cli_version patches, now the…
napetrov Oct 29, 2026
a3c2923
test(llm): pin the benchmark preflight exit status on the CLI login
napetrov Oct 29, 2026
3f18ef4
Merge origin/main into feat/claude-cli-backend
napetrov Oct 29, 2026
090122c
fix(llm): refuse a CLI matrix before its first cell; close review tes…
napetrov Oct 29, 2026
c012773
Merge branch 'feat/claude-cli-backend' of https://github.com/napetrov…
napetrov Oct 29, 2026
c8f0ca9
fix(novelty): refuse an API judge of a CLI alias answerer's family up…
xaleryb Oct 29, 2026
dbe7e28
test(novelty): pin the CLI default judge reached through the command
xaleryb Oct 29, 2026
52a1f38
fix(arms): record judge_resolved_models in the arms judge record
xaleryb Oct 29, 2026
4255875
fix(arms): refuse a matrix cell pairing a CLI provider with a non-Cla…
xaleryb Oct 29, 2026
9ad3fe1
Merge intel/main into feat/claude-cli-backend
xaleryb Oct 29, 2026
3d6bc5c
Test that a bad request or auth failure is not retried
antonwolfy Oct 29, 2026
5ebba4f
Recognise the model family in Vertex ids with an @ version
antonwolfy Oct 29, 2026
925efd8
Describe the full environment drop in the subscription backends doc
antonwolfy Oct 29, 2026
d0ca6cc
Merge branch 'main' of https://github.com/intel/dokimi into pr255-add…
napetrov Oct 29, 2026
481c61d
fix(benchmark): probe a CLI provider once in the preflight
napetrov Oct 29, 2026
2876e3b
fix(utils): get_llm imports llm_cli from its own module
napetrov Oct 29, 2026
75991a4
fix(judge): report a CLI judge's sampling refusal as an error line, e…
napetrov Oct 29, 2026
96ca858
docs(reviewing): list claude-cli among the providers
napetrov Oct 29, 2026
b03bd76
Address review: four reviews whose findings earlier commits fixed
napetrov Oct 29, 2026
7ccbb93
fix(backends): an installed CLI with no version output is 'unknown', …
napetrov Oct 30, 2026
509867c
fix(llm_cli): a failure with no JSON reply is retried on its transien…
napetrov Oct 30, 2026
e643423
fix(llm_cli): pass CLAUDE_CODE_GIT_BASH_PATH to the CLI
napetrov Oct 30, 2026
2881692
fix(llm_cli): accept the 1M-context form (sonnet[1m]) for claude-cli
napetrov Oct 30, 2026
d436854
fix(llm_cli): stop git at the work dir's parent so a repo above it is…
napetrov Oct 30, 2026
efd401f
test(llm_cli): pin the empty reply shapes the parser branches on
napetrov Oct 30, 2026
720da72
docs(subscription-backends): name tools as the command without a CLI …
napetrov Oct 30, 2026
39d4ce4
Merge remote-tracking branch 'intel/main' into pr255-1029-review
napetrov Oct 30, 2026
988089e
docs(ci): keep the comment blocks this PR edits to two lines
napetrov Oct 30, 2026
772801c
docs(llm): list every kept env variable and the git ceiling
antonwolfy Oct 30, 2026
9134bca
docs(llm): say the exclude-dynamic flag is not passed
antonwolfy Oct 30, 2026
cbefbe3
docs(llm): note the system prompt is bound by the argument limit
antonwolfy Oct 30, 2026
1b6d131
docs(llm): list the errors that fail on the first attempt
antonwolfy Oct 30, 2026
3aed8c8
test(llm): pin that the caller's git ceiling entries are kept
antonwolfy 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
10 changes: 3 additions & 7 deletions dokimi/benchmarks/spec.v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,11 @@ future:
enabled: false
min_score: 0.85

# Bounds on the document set the static track scores. They are a sanity check
# on the include/exclude globs, not a target: min_docs catches a glob that
# matched almost nothing, max_docs catches one that escaped into the tree (8
# markdown files live outside docs/). Measured selection at the time of
# writing: 35 files. Adding a doc past the ceiling is meant to be a deliberate
# edit here, so raise max_docs in the same commit that adds the doc.
# Sanity bounds on the static track's doc set, not targets: min_docs catches a glob
# that matched almost nothing, max_docs one that escaped; raise it with each new doc.
golden_manifest:
min_docs: 10
max_docs: 35
max_docs: 36
include:
- docs/**/*.md
exclude:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### Added

- **`--provider claude-cli` / `--judge-provider claude-cli`**: answers, judging,
question generation, personas and novelty can run through a logged-in Claude
Code CLI (`claude -p`) instead of an API key. Each call runs with no tools, no
user or project settings and no MCP servers, in one fixed empty directory, with
the prompt on stdin. A requested `temperature` or `seed` is refused, since
the CLI cannot honour them. `cost_usd` is `None`, because the CLI's figure is
list price. Rate limits, overload and timeouts are retried; an unknown model or
a missing login is not. See `docs/subscription-backends.md`.
- **`backends`**: lists the CLI backends with install, version and login status;
calls no model. `benchmark preflight` checks a CLI backend's login instead
of an API key.

### Changed

- The `--provider` choices of every command come from one tuple,
`dokimi.llm.ANSWER_PROVIDERS`. `tools` keeps API providers only, since it needs
tool calling.
- A CLI provider refuses an agentic arm (`mcp_agent`, `skill_agent`, `skill_catalog`) and
`--session-turns` in `arms run` and `arms matrix-run`, before any answer is paid for: the CLI
runs one prompt with no tools and no conversation.
- A CLI reply records the model the CLI resolved its alias to: `resolved_model` on each usage
row (answers, judge calls), `judge_resolved_models` in the judge record. The response-cache
key takes the CLI version, so a new release that moves `sonnet` does not replay the old
model's replies. The resolved id is known only after a call, so a move the CLI does not
ship with is not caught.
- Independence checks treat an alias and its full id as one model (`opus` and
`claude-opus-…`). The novelty default judge on `claude-cli` is another Claude family than the answerer.
- The CLI child process drops `ANTHROPIC_*`, `CLAUDE_*` and `CLAUDECODE` variables from the
environment (except `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_OAUTH_TOKEN` and
`CLAUDE_CODE_GIT_BASH_PATH` (Windows)), so a billing or
session setting in the caller does not reroute the call. Git is stopped at the work dir's parent. A Bedrock or Vertex route set
in that environment is dropped too: the CLI then uses its own login, and `backends` reports
that login, not the route.
2 changes: 2 additions & 0 deletions dokimi/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ code.
verifier pass rate against a baseline.
- [answer-formats.md](answer-formats.md) — run Inspect AI, OpenAI evals,
skill-creator and promptfoo answer-only sets on the answers track
- [subscription-backends.md](subscription-backends.md) — run the Q&A track through a
logged-in coding-agent CLI (`--provider claude-cli`) instead of an API key
- [context-window.md](context-window.md) — read a run's context window from
its Claude Code trace with `cli.py context trace`: start-up load, growth,
compactions, tool-result weight. No LLM call.
Expand Down
4 changes: 4 additions & 0 deletions dokimi/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ Guide: [run-consistency.md](run-consistency.md).
adapter seam: `LiteLLMAdapter` in production, `FakeAdapter` in tests (the
`fake_llm` fixture). `ask_parsed` is the redraw-until-it-parses loop.
`llm_call`, `chat_completion` and `utils.get_llm` delegate to `LLM`.
`dokimi/llm_cli.py` serves the CLI providers (`claude-cli`): `LiteLLMAdapter`
hands their requests to it, and it runs the logged-in CLI through `process.run`
([subscription-backends.md](subscription-backends.md)). `dokimi/llm_json.py`
holds the JSON extraction helpers `llm.py` re-exports.
- `dokimi/caching.py` — the two caches that hang off `llm.py`: provider prompt
caching (the stable part of a prompt goes first, so the provider can reuse it)
and an opt-in response cache for byte-identical single-shot requests
Expand Down
4 changes: 3 additions & 1 deletion dokimi/docs/reviewing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ pip install -e . # installs the `dokimi` command
The `novelty` and `arms` steps take `--model` and `--provider`; `arms run` also
needs `--judge-provider` (it does not fall back to `--provider` for the judge);
`tasks run` and `repo` take `--model` only. The provider is one of `openai`,
`anthropic`, `amazon-bedrock`, `google-vertex`, `openrouter` or `openai-codex`;
`anthropic`, `amazon-bedrock`, `google-vertex`, `openrouter`, `openai-codex` or
`claude-cli` (a logged-in Claude Code CLI, no key; see
[subscription backends](subscription-backends.md));
`arms run --judge-provider` takes `google`, an alias of `google-vertex`, but not
`openai-codex`.
Credentials are read from the environment: `OPENAI_API_KEY`,
Expand Down
107 changes: 107 additions & 0 deletions dokimi/docs/subscription-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
depends_on:
- dokimi/dokimi/llm_cli.py
- dokimi/dokimi/commands/backends.py
---
<!--
Copyright (C) 2026 Intel Corporation
SPDX-License-Identifier: Apache-2.0
-->

# Subscription backends: a logged-in CLI as the LLM provider

The Q&A track (answers, judging, question generation, personas, novelty) can call
a model through a coding-agent CLI the machine is already signed in to, instead
of an API key. Today one backend exists, `claude-cli` (Claude Code's `claude -p`).
Pass it wherever a command takes `--provider` or `--judge-provider`, except `tools`, which needs tool calling.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · low
Overstated. arms run and arms matrix-run also refuse a CLI provider with an agentic arm (mcp_agent, skill_agent, skill_catalog, or a combined arm containing one) and with --session-turns (see commands/arms_refusals.py). A non-Claude --model is refused at parse time too. The changelog says this, but this page doesn't, and it's the one a user reads. Name these exceptions here, e.g. in the differences table next to "Tools".


## Check what this machine can use

```bash
python cli.py backends # one row per backend: installed, version, signed in, how
python cli.py backends --json
```

This calls no model. A backend that is not signed in prints how to fix it. On a
machine without a browser, `claude setup-token` prints a token; export it as
`CLAUDE_CODE_OAUTH_TOKEN`. `benchmark preflight` checks the login the same way,
instead of looking for an API key.

## Run

```bash
python cli.py answers generate --product oneTBB --questions questions/oneTBB.json \
--output answers/oneTBB.json --provider claude-cli --model haiku
python cli.py eval score --product oneTBB --answers answers/oneTBB.json \
--output eval/oneTBB.json --judge-provider claude-cli --judge-model sonnet
```

`--model` takes what `claude --model` takes: an alias (`haiku`, `sonnet`,
`opus`) or a full id (`claude-sonnet-5-5`). The default judge model works on
`claude-cli` as it does on `anthropic`. When the answerer and the judge are the
same model, the judge grades its own answers.

## How it differs from an API provider

| | API provider | `claude-cli` |
|---|---|---|
| Credential | `*_API_KEY` | the CLI's own login |
| `--temperature`, `--seed` | sent when set | refused before the run (`unsupported_sampling_params`) |
| `max_tokens` | output ceiling | ignored: the CLI has no flag for it |
| Hidden prompt | none | fixed CLI preamble (≈0.5k–1.3k tokens, by CLI version), plus working directory, git status, platform and date |
| Tools | `chat(tools=...)` | refused: one prompt, one reply |
| `cost_usd` | litellm price | `None`: the CLI's `total_cost_usd` is list price, not what a subscription is charged |
| Latency | model only | plus ≈2–3 s CLI start-up per call |

What this means for results:

- **Don't pool scores across transports.** `claude-cli:sonnet` and
`anthropic:claude-sonnet-5-5` get different prompts (the CLI adds its own text)
and different sampling. Compare arms within one backend.
- **The CLI states its environment, and nothing turns this off.** It does so
with no tools and with its own system prompt replaced;
`--exclude-dynamic-system-prompt-sections` was tried and does not remove it.
Tested and not working:
`CLAUDE_CODE_HIDE_CWD`, `CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS` (Claude Code
2.1.304). dokimi runs every call in one fixed empty directory
(`$XDG_CACHE_HOME/dokimi/cli/claude-cli`, default
`~/.cache/dokimi/cli/claude-cli`), so the stated environment is the same in the
with-docs and the without-docs arm, and the response cache still hits. A
without-docs answer may still mention it ("the working directory isn't a git
repository").
- **Repeats differ.** With no temperature or seed, the same prompt gives a
different answer each time; measure variance with repeats, not a pinned seed.

## Isolation

Each call is `claude -p --input-format text --output-format json --model <m>
--tools "" --setting-sources "" --strict-mcp-config --no-session-persistence
--system-prompt <s>`. The prompt goes on stdin, so its size is not bounded by
the argument limit; the system prompt is an argument and is. Without these flags, a plain `claude -p` in a repository
carries about 37k tokens of agent prompt, tool schemas and `CLAUDE.md`.

- `--bare` is never used: it reads only `ANTHROPIC_API_KEY`, so a subscription
login fails with "Authentication error".
- The child gets the caller's environment minus every variable starting with
`ANTHROPIC_`, `CLAUDE_` or `CLAUDECODE`, except `CLAUDE_CONFIG_DIR`,
`CLAUDE_CODE_OAUTH_TOKEN` and `CLAUDE_CODE_GIT_BASH_PATH` (Windows). That
drops the session variables, an API key or
token that would win over the login, and a base URL, model alias or
Bedrock/Vertex switch, so the CLI runs on its own login. If dokimi itself
runs inside Claude Code and the session variables were passed on, the child
would join the parent's session. Git is stopped at the work dir's parent
(`GIT_CEILING_DIRECTORIES`), so it finds no repository above it.
- The CLI keeps its own config directory, because that is where the login is
stored. User settings and `CLAUDE.md` stay out because of
`--setting-sources ""`.

## Errors and retries

Rate limits, overload, 5xx and a timeout are retried with the same backoff as
an API provider. An unknown model, a missing login, a missing binary, an
unusable work directory and a subscription usage limit fail on the first attempt.

## Terms of use

A consumer subscription is for personal use. Keep CI and shared benchmark
runs on API keys.
11 changes: 9 additions & 2 deletions dokimi/dokimi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from dokimi.commands import (
answers,
arms,
backends,
baseline,
benchmark,
catalog,
Expand All @@ -35,7 +36,7 @@
tasks,
tools,
)
from dokimi import __version__, process
from dokimi import __version__, llm_cli, process
from dokimi.console import tolerate_console_encoding
from dokimi.defaults import JUDGE_MODEL_FOR_PROVIDER, default_judge_model

Expand Down Expand Up @@ -81,6 +82,7 @@ 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)
backends.register(sub, positive_int) # backends (CLI providers)

return p

Expand All @@ -99,6 +101,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.error(f"--judge-provider {args.judge_provider} has no default judge model; "
f"pass --judge-model")
args.judge_model = model
provider, model = getattr(args, "provider", None), getattr(args, "model", None)
if provider in llm_cli.BACKENDS and model is not None and not llm_cli.accepts_model(provider, model):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: this validates an unused model before matrix cells override it. For example, python cli.py arms matrix-run --product p --questions q.json --arms baseline --matrix-cells cells.json --provider claude-cli exits with a model error when cells.json contains valid claude-cli/sonnet cells, because the top-level model retains its non-Claude default. The same problem affects arms run --matrix-cells cells.json --matrix-cell cli. Both paths unconditionally replace provider/model in _apply_matrix_cell, which already validates the cell model. Skip this top-level check for matrix-driven runs (or validate after applying the selected cell), and cover both paths through parse_args.

# The default answer model is not a Claude model: left out, every call would fail.
parser.error(f"--provider {provider} takes a Claude model (sonnet, haiku, opus or a claude-* id), "
f"not {model!r}: pass --model")
return args


Expand All @@ -113,7 +120,7 @@ def main() -> None:

try:
args.func(args)
except (EmptyQuestionError, InputError) as exc: # a bad input record, not a bug: no traceback
except (EmptyQuestionError, InputError, llm_cli.SamplingError) as exc: # bad input, not a bug: no traceback
print(f"Error: {exc}", file=sys.stderr)
# 2, as the commands' other input errors: 1 is the verdict of `eval judge-validity`
# and of the --strict gates, and a malformed file must not read as a failed gate.
Expand Down
3 changes: 2 additions & 1 deletion dokimi/dokimi/commands/answers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import sys
from pathlib import Path

from dokimi.llm import ANSWER_PROVIDERS
from dokimi.commands.evaluate import _require_ragas, _run_ragas_eval
from dokimi.defaults import (
DEFAULT_ANSWER_MODEL,
Expand Down Expand Up @@ -158,7 +159,7 @@ def register(sub, positive_int) -> None:
"Sets output to results/{product}_{run_id}/answers/{product}.json")
gen_a_p.add_argument("--output", default=None, help="Output file (default: answers/{product}.json)")
gen_a_p.add_argument("--model", default=DEFAULT_ANSWER_MODEL, help="LLM model for answering")
gen_a_p.add_argument("--provider", default=DEFAULT_ANSWER_PROVIDER, choices=["openai", "anthropic", "amazon-bedrock", "google-vertex", "openrouter", "openai-codex"])
gen_a_p.add_argument("--provider", default=DEFAULT_ANSWER_PROVIDER, choices=ANSWER_PROVIDERS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--provider claude-cli keeps the default answer model gpt-6.1-sol, so answers generate --provider claude-cli without --model sends --model gpt-6.1-sol to claude and every answer fails with an unknown-model error. The same holds for questions, personas and subjects run, where --model is hidden. Please refuse a non-Claude model for a CLI provider in parse_args, or pick a Claude default as default_judge_model does.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 35c5219, with the coverage test in 9147dfa. Head is 1688652.

A non-Claude model with --provider claude-cli is refused at parse time (cli.py:107): "takes a Claude model (sonnet, haiku, opus or a claude-* id)". This covers answers generate (default gpt-6.1-sol), questions, personas and subjects run, where --model is hidden.

Test: test_every_command_with_a_hidden_or_default_model_refuses_the_cli_without_a_claude_model.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_args refuses a non-Claude model with --provider claude-cli, so answers generate, questions, personas and subjects fail at parse time and ask for --model. The parse-time test now includes answers generate, which it did not before, so my earlier claim was not backed by a test. Pinned by test_every_command_with_a_hidden_or_default_model_refuses_the_cli_without_a_claude_model. Fixed in 090122c.

gen_a_p.add_argument("--max-tokens", type=int, default=DEFAULT_MAX_TOKENS_PER_QUESTION, help="Max tokens to retrieve per question")
gen_a_p.add_argument("--top-k", type=int, default=None, help=f"Number of docs to retrieve before reranking (default from config/products.yaml retrieval.top_k or {DEFAULT_RETRIEVAL_TOP_K})")
gen_a_p.add_argument("--rerank-threshold", type=float, default=0.3, help="Min relevance score (0-1) to keep docs")
Expand Down
9 changes: 7 additions & 2 deletions dokimi/dokimi/commands/arms.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import sys
from pathlib import Path

from dokimi.commands.arms_refusals import refuse_cli_arms, refuse_cli_cell, refuse_cli_cell_model
from dokimi.llm import ANSWER_PROVIDERS
from dokimi.utils import question_id
from dokimi.eval.judge import JUDGE_PROVIDERS
from dokimi.defaults import (
Expand Down Expand Up @@ -123,6 +125,7 @@ def cmd_arms_matrix_run(args: argparse.Namespace) -> None:
cell_args = argparse.Namespace(**vars(args))
_apply_matrix_cell(cell_args, cell)
_check_sampling_support(cell_args)
refuse_cli_cell(cell_args, specs)
except (ValueError, OSError, RecursionError) as exc:
print(f"Error loading matrix run inputs: {exc}", file=sys.stderr)
sys.exit(1)
Expand Down Expand Up @@ -233,7 +236,7 @@ def register(sub, positive_int) -> None:
)
arms_run_p.add_argument("--model", default=DEFAULT_ANSWER_MODEL, help="LLM for answering")
arms_run_p.add_argument("--provider", default=DEFAULT_ANSWER_PROVIDER,
choices=["openai", "anthropic", "amazon-bedrock", "google-vertex", "openrouter", "openai-codex"])
choices=ANSWER_PROVIDERS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arms run --provider claude-cli (and matrix-run, line 384) is accepted with agentic arms (agent, skill-agent:, skill-catalog:, tool plugins) and with --session-turns, but the CLI path refuses tools and multi-turn chat on every call. The run starts, pays for the plain arms, and records agent_failed for every agentic answer; a session stops after its first turn. Please refuse this in _build_arms_output before any answer, as tools does by leaving CLI providers out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly addressed, at 1688652. Fixed in 9147dfa for arms run, with the import fix in 2c491ec and the entry-point test in 4e33134.

  • arms run with --provider claude-cli refuses agentic arms (MCP agent, skill agent, skill catalog, and tool plugins) and --session-turns. refuse_cli_arms (commands/arms_refusals.py) runs in _build_arms_output before any answer. It reaches through plugin wrappers and combined arms.
  • matrix-run calls the same _build_arms_output for each cell, so the refusal applies there too, but per cell. Not done: a matrix whose earlier cells use another provider runs those cells before the first CLI cell is refused. Pre-checking every cell before the first answer is open for the author to decide.
  • --session-turns defaults to None (test_the_session_turns_flag_defaults_to_none, d14a762).
  • The entry-point test patches ArmRunner to fail on start (RunnerMustNotStart).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arms run refuses agentic arms and session turns on claude-cli in _build_arms_output, before any answer. matrix-run now applies the same refusal to every cell before the first cell is answered, so an API cell does not run ahead of a refused CLI cell. Pinned by test_a_matrix_with_a_cli_agentic_cell_is_refused_before_the_first_cell_is_answered and test_the_arms_entry_point_refuses_a_cli_agentic_arm_before_any_answer. Fixed in 090122c. Correction: this branch has no tool-plugin kind, only prompt_middleware and output_shaper, so my earlier mention of tool plugins was wrong.

arms_run_p.add_argument("--harness", default="arms-runner",
help="Execution harness label stamped into output (default: arms-runner)")
arms_run_p.add_argument("--plugins", default="",
Expand Down Expand Up @@ -379,7 +382,7 @@ def _add_common_run_args(parser, positive_int, *, matrix_required: bool = False)
)
parser.add_argument("--model", default=DEFAULT_ANSWER_MODEL, help="LLM for answering")
parser.add_argument("--provider", default=DEFAULT_ANSWER_PROVIDER,
choices=["openai", "anthropic", "amazon-bedrock", "google-vertex", "openrouter", "openai-codex"])
choices=ANSWER_PROVIDERS)
parser.add_argument("--harness", default="arms-runner",
help="Execution harness label stamped into output (default: arms-runner)")
parser.add_argument("--plugins", default="",
Expand Down Expand Up @@ -552,6 +555,7 @@ def _apply_named_matrix_cell(args: argparse.Namespace) -> None:


def _apply_matrix_cell(args: argparse.Namespace, matrix_cell) -> None:
refuse_cli_cell_model(matrix_cell)
args.matrix_cell = matrix_cell.name
args.model = matrix_cell.model
args.provider = matrix_cell.provider
Expand Down Expand Up @@ -625,6 +629,7 @@ def _build_arms_output(args: argparse.Namespace, specs: list[str], questions: li
plugins = create_plugins([s.strip() for s in args.plugins.split(",") if s.strip()])
validate_plugins_for_harness(plugins, args.harness)
treatments = wrap_treatments(treatments, plugins)
refuse_cli_arms(args, treatments)
plugin_set = plugin_set_metadata(plugins)
print(f"Arms: {', '.join(t.name for t in treatments)}")
if args.matrix_cell:
Expand Down
62 changes: 62 additions & 0 deletions dokimi/dokimi/commands/arms_refusals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Refusals for the arms commands on a CLI provider, raised before any answer is paid for."""

from __future__ import annotations

import argparse


def _is_agentic_arm(treatment) -> bool:
"""True when the arm offers tools, so it runs the agentic loop (see ``AgentConfig.tools``).

The tools are added in ``prepare()``, which needs a question and may retrieve, so the
arm types that offer tools are named here instead of probed per question.
"""
from dokimi.plugins import PluginWrappedTreatment
from dokimi.treatments.arms import (
CombinedTreatment,
MCPAgentTreatment,
SkillAgentTreatment,
SkillCatalogTreatment,
)

inner = treatment.inner if isinstance(treatment, PluginWrappedTreatment) else treatment
if isinstance(inner, CombinedTreatment):
return any(_is_agentic_arm(sub) for sub in inner.treatments)
return isinstance(inner, (MCPAgentTreatment, SkillAgentTreatment, SkillCatalogTreatment))


def refuse_cli_arms(args: argparse.Namespace, treatments: list) -> None:
"""Refused before any answer: a CLI provider runs one prompt, with no tools and no conversation."""
from dokimi import llm_cli

if llm_cli.backend(args.provider) is None:
return
agentic = [t.name for t in treatments if _is_agentic_arm(t)]
if agentic:
raise ValueError(f"--provider {args.provider} runs one prompt with no tools, so it cannot run "
f"the agentic arm(s) {', '.join(agentic)}: use an API provider for them")
if getattr(args, "session_turns", None) is not None:
raise ValueError(f"--session-turns needs a conversation, which --provider {args.provider} "
"does not have: use an API provider for a session")


def refuse_cli_cell_model(matrix_cell) -> None:
"""A matrix cell sets provider and model after the parse-time model gate has read only --model."""
from dokimi import llm_cli

if llm_cli.backend(matrix_cell.provider) and not llm_cli.accepts_model(matrix_cell.provider,
matrix_cell.model):
raise ValueError(f"matrix cell {matrix_cell.name!r}: provider {matrix_cell.provider} takes a "
f"Claude model (sonnet, haiku, opus or a claude-* id), not {matrix_cell.model!r}")


def refuse_cli_cell(args: argparse.Namespace, specs: list[str]) -> None:
"""A matrix cell on a CLI provider with an agentic arm is refused before the first cell is answered."""
from dokimi import llm_cli
from dokimi.treatments import create_treatments

if llm_cli.backend(args.provider) is not None:
refuse_cli_arms(args, create_treatments(specs, top_k=args.top_k, rerank_threshold=args.rerank_threshold))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · low
Nit: this builds every arm again for each matrix cell on a CLI provider, and _build_arms_output builds them once more per cell. create_treatments loads skills and profiles, creates doc-source clients, and for skill-catalog: prints discovery notes to stderr. With N CLI cells the same notes are printed N+1 times, and each arm is loaded N+1 times. The arm set doesn't depend on the cell, so build it once, or check the arm specs without constructing them.

Loading