Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
### Added

- **`tasks inspect <path> [--json]`** — detects the eval-task format of a file,
a task directory or a whole tree and reports its structure: kind
(executable, answer-only, trigger), item count, present and missing parts,
lint problems and which dokimi backends consume it today. Recognises Harbor,
BenchFlow `task.md`, terminal-bench v1, SWE-bench instances, Inspect AI
samples and `@task` files (scanned as text, never imported), OpenAI evals
JSONL, skill-creator `evals.json` and trigger sets, skill-reach query sets,
promptfoo configs and dokimi question sets; anything else is reported as
`unknown` with its files. Detection only: no new format runs yet.
10 changes: 10 additions & 0 deletions dokimi/changelog.d/20261024_120000_napetrov_trigger-fidelity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### Added

- **`catalog trigger-probe`** — runs each trigger-set query in a fresh, unannounced
`claude -p` session with the catalog committed as project skills, a scratch `HOME`
and config, and the query sent verbatim; scores the `Skill` calls with the same
selection summary as the `skill-catalog:` arm, one column per trial.
- **`dokimi.harnesses.payload_audit`** — `audit_payload` / `new_leaks` report dokimi
artifacts (arm names, label keys, question ids, grading words, task paths) in a
captured request, argv or env, with operator content blanked and the stock harness's
own text subtracted.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Added

- **Read trigger sets back** — `dokimi catalog trigger-set --trigger-set PATH[=TARGET]` reads a google/skill-reach QuerySet, an anthropics skill-creator / agentskills.io `[{query, should_trigger}]` set (TARGET names its skill), or a questions set with `expected_skill`, relints it against `--catalog`, and writes any of the three formats, so an imported set runs on the `skill-catalog:` arm. `arms run` given a skill-reach or skill-creator file names the conversion command; `tasks inspect` lists `catalog trigger-set` as the consumer of both formats.
- **Skill package formats** — `dokimi catalog formats --root DIR [--json]` lists Agent Skills `SKILL.md` packages (with the host read from the path, the agentskills.io name/description limits linted, and Codex `agents/openai.yaml` noted), Cursor `.mdc` rules, Copilot `*.instructions.md`, Gemini CLI `gemini-extension.json` and Claude Code `.claude-plugin/plugin.json` bundles, with how each is selected (description, globs, always, install).
- **Skill selection precision** — `skill_selection_summary` and the arms report's *Skill selection* table carry `precision`: hits over the questions where the arm loaded any skill, so misroutes and false triggers count against it.
2 changes: 1 addition & 1 deletion dokimi/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ points up:
| Layer | Modules |
| --- | --- |
| front end | `cli`, `commands`, `__main__` (the argparse front end; no layer below imports it) |
| tracks | `runner`, `report`, `subjects`, `novelty`, `harnesses`, `context`, `repos`, `explore`, `tool_preference`, `external`, `dashboard`, `orchestrator`, `datasets_export`, `coverage`, and the checkers `config_check`, `schema_check`, `schema_compat` |
| tracks | `runner`, `report`, `subjects`, `novelty`, `harnesses`, `context`, `repos`, `explore`, `tool_preference`, `external`, `dashboard`, `orchestrator`, `datasets_export`, `coverage`, `task_formats`, and the checkers `config_check`, `schema_check`, `schema_compat` |
| evaluation | `eval`, `personas`, `questions`, `treatments`, `plugins` |
| components | `ingest`, `metrics`, `gate`, `mcp`, `skills`, `agent_profiles` |
| foundation | `__init__` (the `dokimi` package itself), `_version`, `artifacts`, `records`, `llm`, `process`, `caching`, `config`, `registry`, `intents`, `defaults`, `console`, `utils`, `metrics/usage.py` |
Expand Down
29 changes: 29 additions & 0 deletions dokimi/docs/coding-harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,35 @@ refused (a few hundred bytes of nested aliases expand without bound), and
frontmatter nested too deeply to parse is a parse error. YAML and TOML dates
and times become ISO strings, and YAML sets sorted lists.

### Other eval formats: `tasks inspect`

`python cli.py tasks inspect <path> [--json]` names the eval-task format of a
file, a task directory, or every item under a tree, and reports its structure:
kind, item count, which parts are present or missing (instruction,
environment, oracle, verifier, metadata), lint problems, and what in dokimi
can consume it today. It only reads: nothing is run or converted, a Python
file is scanned as text and never imported, and a file that does not parse is
a problem on its report, not an error. A path no format claims is reported as
`unknown` with its files (or keys) and likely prompt and verifier files; when
two formats claim a path about equally, the report lists the other as
`ambiguous` with its evidence.

| Format | Recognised by | Kind | Consumed by |
|---|---|---|---|
| `harbor` | directory with `task.toml` | executable | Harbor-backed harnesses (unless Harbor would refuse it), `docker-*` |
| `task-md` | directory with a BenchFlow `task.md` | executable | `benchflow*`, `docker-*` |
| `terminal-bench-v1` | directory with `task.yaml` + `run-tests.sh` / `Dockerfile` / `solution.sh` | executable | none yet |
| `swe-bench` | JSON/JSONL rows with `instance_id`, `repo`, `base_commit`, `problem_statement` (`FAIL_TO_PASS`) | executable | none yet |
| `inspect-samples` | rows with `input` and `target` | answer-only | none yet |
| `inspect-task` | `.py` importing `inspect_ai` with a `@task` function | answer-only (executable with a `sandbox=`) | none yet |
| `openai-evals` | JSONL rows with `input` (chat messages) and `ideal` | answer-only | none yet |
| `skill-creator-evals` | `evals.json` with `skill_name` and `evals[].prompt` | answer-only | none yet |
| `skill-creator-trigger` | rows with `query` and `should_trigger` | trigger | none yet |
| `skill-reach` | `QuerySet` (`queries[]` with `query_id`/`id`, `text`, `expected_skill`) | trigger | none yet |
| `dokimi-questions` | questions.v1/v2 JSON | answer-only (trigger with `expected_skill`) | questions track |

`tasks list` and `tasks run` still read only the first two.

The `docker-*` harnesses mount the oracle at `/solution` and `/oracle` and the
verifier at `/tests` and `/verifier`, because a script written for one layout
refers to its siblings by that layout's path. The directory's contents are
Expand Down
13 changes: 13 additions & 0 deletions dokimi/docs/evaluating-treatments.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,19 @@ uv run reach eval --skills <skills-checkout>/skills --queries trigger_set.json \
uv run reach diff --vary description control.json treatment.json # A/B vs noise floor
```

**Real harness, in dokimi: `catalog trigger-probe`.** Runs each query in a fresh
`claude -p`: a new git repository with the catalog committed in `.claude/skills/`, a
scratch `HOME` and `CLAUDE_CONFIG_DIR` (your own skills, settings and `CLAUDE.md` never
reach it), the query sent verbatim with no notice or task framing, and bundled skills
left on as a user has them (`--sealed` turns them off). The skill chosen is read from the
transcript's `Skill` calls, and the output's `skill_selection_summary` is scored by the
same code as the `skill-catalog:` arm, one column per trial:

```bash
python cli.py catalog trigger-probe --catalog <skills-checkout>/skills \
--trigger-set trigger_set.json --model <model> --trials 3 --out probe.json
```

Two things decide whether such a number means anything:

- **Listing budget.** Claude Code shows skill descriptions only up to a budget (a
Expand Down
3 changes: 2 additions & 1 deletion dokimi/dokimi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
[`dashboard`][dokimi.dashboard] aggregates results across libraries and
[`orchestrator`][dokimi.orchestrator] runs the one-command pipeline.
- Executable tasks: [`harnesses`][dokimi.harnesses] runs coding agents on task
directories.
directories; [`task_formats`][dokimi.task_formats] names the format of other
eval-task files and directories (detection only).
- [`context`][dokimi.context] reads what a run's context window held.
- Content novelty: [`novelty`][dokimi.novelty]. Repositories as subjects:
[`repos`][dokimi.repos]. Subject scorecards, one per run:
Expand Down
25 changes: 25 additions & 0 deletions dokimi/dokimi/commands/arms.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ def _load_questions(path: str) -> list[dict]:
"""
from dokimi.questions.artifact import load_questions

_refuse_external_trigger_set(path)
questions = load_questions(path).questions
ids = [question_id(q) for q in questions]
for i, qid in enumerate(ids):
Expand All @@ -461,6 +462,30 @@ def _load_questions(path: str) -> list[dict]:
return questions


def _refuse_external_trigger_set(path: str) -> None:
"""Point a skill-reach or skill-creator trigger set at the command that reads it.

Those sets are read through ``catalog trigger-set --trigger-set``, which also
lints them against the catalog the arm will offer; a questions loader that
guessed at them would drop that lint and, for skill-creator, the target skill.
"""
import json

from dokimi.external.trigger_set import FORMAT_QUESTIONS, detect_trigger_format

try:
data = json.loads(Path(path).read_text(encoding="utf-8"))
except (OSError, ValueError):
return # the questions loader reports it
fmt = detect_trigger_format(data)
if fmt and fmt != FORMAT_QUESTIONS:
target = "=<skill>" if fmt == "skill-creator" else ""
raise ValueError(
f"{path} is a {fmt} trigger set, not a questions set; convert it with "
f"`dokimi catalog trigger-set --catalog <skills> --trigger-set {path}{target} "
f"--format questions --out q.json`, then run the skill-catalog: arm on q.json")


def _refuse_duplicate_ids(ids: list, path: str) -> None:
"""Raise on a repeated id."""
counts = Counter(qid for qid in ids if qid is not None)
Expand Down
115 changes: 102 additions & 13 deletions dokimi/dokimi/commands/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def cmd_catalog_trigger_set(args: argparse.Namespace) -> None:

from dokimi.external import trigger_set as ts
from dokimi.skills import discover_skills
from dokimi.utils import read_json_input

catalog_root = Path(args.catalog)
if not catalog_root.is_dir():
Expand All @@ -80,16 +79,11 @@ def cmd_catalog_trigger_set(args: argparse.Namespace) -> None:
if args.target not in {s.name for s in catalog}:
raise SystemExit(f"--target {args.target!r} is not a skill in --catalog "
f"(have: {', '.join(sorted(s.name for s in catalog))})")
sources = []
for spec in args.questions:
path, _, rule = spec.rpartition("=") # labels never contain '=', paths may
if not path or not rule:
raise SystemExit(f"--questions needs PATH=LABEL (a skill name, 'none' or '@field'): {spec}")
data = read_json_input(path, flag="--questions")
questions = data.get("questions", data) if isinstance(data, dict) else data
if not isinstance(questions, list) or not all(isinstance(q, dict) for q in questions):
raise SystemExit(f"{path}: expected a list of question objects")
sources.append((questions, rule))
sources = _question_sources(args.questions or [])
sources += _trigger_set_sources(args.trigger_set or [], catalog)
if not sources:
raise SystemExit("give --questions PATH=LABEL or --trigger-set PATH[=TARGET]")
specs = list(args.questions or []) + list(args.trigger_set or [])

rewrite = None
if args.rewrite:
Expand All @@ -107,7 +101,7 @@ def rewrite(text: str) -> str:
# An @field rule that resolves for no question in its source is a typo in the
# path, not a set of negatives; refuse it rather than write "nothing loads".
start = 0
for (qs, rule), spec in zip(sources, args.questions, strict=True):
for (qs, rule), spec in zip(sources, specs, strict=True):
chunk = queries[start:start + len(qs)]
start += len(qs)
if rule.startswith("@") and chunk and not any(q.labelled for q in chunk):
Expand Down Expand Up @@ -143,6 +137,82 @@ def rewrite(text: str) -> str:
print(f"wrote {args.out} ({args.format}, {n_out} queries, catalog of {len(catalog)})")


def cmd_catalog_formats(args: argparse.Namespace) -> None:
import json

from dokimi.skills import formats

root = Path(args.root)
if not root.is_dir():
raise SystemExit(f"--root is not a directory: {root}")
packages = formats.detect_skill_packages(root)
summary = formats.summarize(packages)
if args.json:
print(json.dumps({"root": str(root), "summary": summary,
"packages": [p.to_dict(root) for p in packages]}, indent=2))
return
for p in packages:
problems = "; ".join(p.problems) or "ok"
print(f"{p.format}\t{p.name or '-'}\t{p.selected_by}\t{p.host or '-'}\t"
f"{p.path.relative_to(root).as_posix()}\t{problems}")
print()
for fmt, row in summary.items():
print(f"# {fmt}: {row['n']} ({row['loadable']} loadable as skills, "
f"{row['with_problems']} with problems); selected by {row['selected_by']}")


def _question_sources(specs: list) -> list:
"""Each ``--questions PATH=LABEL`` as a (questions, rule) source."""
from dokimi.utils import read_json_input

sources = []
for spec in specs:
path, _, rule = spec.rpartition("=") # labels never contain '=', paths may
if not path or not rule:
raise SystemExit(f"--questions needs PATH=LABEL (a skill name, 'none' or '@field'): {spec}")
data = read_json_input(path, flag="--questions")
questions = data.get("questions", data) if isinstance(data, dict) else data
if not isinstance(questions, list) or not all(isinstance(q, dict) for q in questions):
raise SystemExit(f"{path}: expected a list of question objects")
sources.append((questions, rule))
return sources


def _trigger_set_sources(specs: list, catalog) -> list:
"""Each ``--trigger-set PATH[=TARGET]`` read back as a (questions, rule) source.

The set is read into the canonical trigger model and handed on as questions
labelled by their own ``expected_skill``, so it is linted (and rewritten) like
any other source. TARGET names the skill a skill-creator set was written for.
"""
from dokimi.external import trigger_set as ts
from dokimi.utils import read_json_input

names = {s.name for s in catalog}
out = []
for spec in specs:
path, target = spec, None
if not Path(spec).exists() and "=" in spec:
path, _, target = spec.rpartition("=")
data = read_json_input(path, flag="--trigger-set")
try:
fmt, queries = ts.read_trigger_set(data, target=target)
except ValueError as exc:
raise SystemExit(f"{path}: {exc}") from exc
if fmt == ts.FORMAT_SKILL_CREATOR:
if target not in names:
raise SystemExit(f"{spec}: skill-creator target {target!r} is not a skill in "
f"--catalog (have: {', '.join(sorted(names))})")
if len(names) > 1:
print(f"note: {path}: skill-creator negatives are read as 'no skill loads'; in a "
f"catalog of {len(names)} a rival load counts as a false trigger", file=sys.stderr)
elif target:
raise SystemExit(f"{spec}: =TARGET applies to skill-creator sets only, this is {fmt}")
print(f"read {path} as {fmt} ({len(queries)} queries)", file=sys.stderr)
out.append((ts.to_questions(queries)["questions"], "@expected_skill"))
return out


def register(sub, positive_int) -> None:
"""Add the `catalog` subcommand group."""

Expand All @@ -167,16 +237,31 @@ def register(sub, positive_int) -> None:
)
scan_p.set_defaults(func=cmd_catalog_scan)

fm_p = catalog_sub.add_parser(
"formats",
help="List the skill/rule packages under --root by ecosystem (Agent Skills, Cursor "
"rules, Copilot instructions, Gemini extensions, Claude plugins)",
)
fm_p.add_argument("--root", required=True, help="Local checkout to scan")
fm_p.add_argument("--json", action="store_true", help="Print JSON instead of a table")
fm_p.set_defaults(func=cmd_catalog_formats)

ts_p = catalog_sub.add_parser(
"trigger-set",
help="Build a skill-selection query set (labels + leakage/framing lint) from question sets",
)
ts_p.add_argument("--catalog", required=True, help="Directory of the skills the agent will see")
ts_p.add_argument(
"--questions", action="append", required=True, metavar="PATH=LABEL",
"--questions", action="append", metavar="PATH=LABEL",
help="Question file and its label: a skill name, 'none' (no skill should load), or "
"'@field.path' read per question (e.g. @metadata.skill_under_test). Repeatable.",
)
ts_p.add_argument(
"--trigger-set", action="append", dest="trigger_set", metavar="PATH[=TARGET]",
help="An existing trigger set to read back: a skill-reach QuerySet, a skill-creator / "
"agentskills.io [{query, should_trigger}] set (=TARGET names its skill), or questions "
"with expected_skill. Repeatable; combines with --questions.",
)
ts_p.add_argument("--format", choices=["reach", "skill-creator", "questions"], default="reach",
help="reach: google/skill-reach QuerySet; skill-creator: one target's "
"eval set; questions: a questions artifact for the skill-catalog: arm")
Expand All @@ -189,3 +274,7 @@ def register(sub, positive_int) -> None:
help="Exit 1 if any query names a skill, keeps exam framing, or has an unresolved @field label")
ts_p.add_argument("--out", required=True, help="Output JSON path")
ts_p.set_defaults(func=cmd_catalog_trigger_set)

from dokimi.commands import catalog_probe

catalog_probe.register(catalog_sub, positive_int)
Loading
Loading