Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f67147d
fix(tools): preference exits with a message on a non-UTF-8 --groups file
vchamarthi Oct 25, 2026
74000dc
fix(tools): preference exits 1 with a message when --items-out cannot…
vchamarthi Oct 25, 2026
891f9f7
fix(catalog): trigger-set exits 1 with a message when --out cannot be…
vchamarthi Oct 25, 2026
c4df21c
fix(catalog): scan names the manifest file when skills.yaml or suites…
vchamarthi Oct 25, 2026
192057b
chore: changelog fragment and growth baseline
vchamarthi Oct 26, 2026
766eae4
Finish the trigger-set writer docstring
antonwolfy Oct 26, 2026
8940627
Escape the --out path in the trigger-set test regex
antonwolfy Oct 26, 2026
0f9bfd9
fix(cli): reject wrong-shaped suites.json and groups files, guard --o…
vchamarthi Oct 26, 2026
5d1c627
fix(cli): report a rejected questions set separately from an unwritab…
vchamarthi Oct 26, 2026
53e5e33
fix(cli): exit with a message for a null groups list, deep or huge JS…
vchamarthi Oct 26, 2026
6c4295c
fix(catalog): report deeply nested manifests and unusable task names …
vchamarthi Oct 26, 2026
61cdeb2
fix(catalog): exit with a message on a malformed SKILL.md or a task d…
vchamarthi Oct 26, 2026
23cdfe4
Drop the suites.json prefix from catalog scan task errors
antonwolfy Oct 26, 2026
061682e
fix(catalog): report a deeply nested SKILL.md as an error row
vchamarthi Oct 26, 2026
263ccde
fix(tools): refuse an unwritable --out-json before any probe call
vchamarthi Oct 26, 2026
ad114ec
fix(tools): probe --out-json with the save's temp-file rule
vchamarthi Oct 26, 2026
82407cd
test(tools): pin the out-json probe against read-only and symlinked t…
vchamarthi Oct 26, 2026
41466dd
test(tools): move the out-json pre-flight tests to their own file
vchamarthi Oct 26, 2026
8f04440
fix(tools): probe --out-json with the save's temp file name
antonwolfy Oct 26, 2026
6980cd1
Merge origin/main into fix/cli-input-tools-catalog-skills
vchamarthi Oct 27, 2026
2a23d6a
fix(catalog): a too-deep SKILL.md listed in skills.yaml is an error row
xaleryb Oct 27, 2026
3d29187
fix(catalog): reject falsy wrong-typed suites and tasks in suites.json
xaleryb Oct 27, 2026
f07639e
Merge remote-tracking branch 'intel/main' into fix/cli-input-tools-ca…
napetrov Oct 27, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- **`tools preference` and `catalog`** exit with a message on a non-UTF-8 `--groups` file, an unwritable `--out`, `--items-out` or `--out-json`, an unreadable or wrong-shaped manifest, a malformed `SKILL.md` under `--catalog`, or a task directory without `instruction.md`, instead of a traceback.
47 changes: 35 additions & 12 deletions dokimi/dokimi/commands/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def _scan_tasks(args: argparse.Namespace, root: Path, report: dict, as_json: boo
try:
tasks = read_task_manifest(root, tasks_subdir=args.tasks_subdir)
except ValueError as exc:
raise SystemExit(f"suites.json: {exc}") from exc
raise SystemExit(str(exc)) from exc
if not as_json:
print(f"# tasks ({len(tasks)} declared under {root})")
failed = False
Expand Down Expand Up @@ -136,16 +136,47 @@ def cmd_catalog_scan(args: argparse.Namespace) -> None:
raise SystemExit(1)


def cmd_catalog_trigger_set(args: argparse.Namespace) -> None:
def _write_trigger_set(args: argparse.Namespace, queries, catalog_root: Path) -> None:
"""Write the query set to ``--out`` in ``--format``, or exit 1 naming ``--out`` when it cannot be written."""
import json

from dokimi.artifacts import ArtifactValidationError
from dokimi.external import trigger_set as ts

try:
if args.format == "questions":
from dokimi.questions.artifact import save_questions_artifact

save_questions_artifact(ts.to_questions(queries), args.out)
else:
# External tools' formats: no schema of ours to hold them to.
payload = (ts.to_reach(queries, catalog_id=str(catalog_root)) if args.format == "reach"
else ts.to_skill_creator(queries, args.target))
Path(args.out).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
except ArtifactValidationError as exc: # the path was writable; the payload failed the schema
raise SystemExit(f"Error: questions set rejected: {exc}") from exc
except (OSError, ValueError) as exc: # unwritable --out: a directory, a file as parent, no permission, empty path (pathlib ValueError)
raise SystemExit(f"Error: cannot write --out {args.out}: {exc}") from exc
Comment thread
vchamarthi marked this conversation as resolved.
Resolved


def _discover_catalog_skills(catalog_root: Path) -> list:
from dokimi.skills import discover_skills

try:
return discover_skills(catalog_root)
except (OSError, ValueError, RecursionError) as exc: # unreadable, non-UTF-8, bad frontmatter or nesting too deep
Comment thread
vchamarthi marked this conversation as resolved.
Resolved
raise SystemExit(f"--catalog {catalog_root}: {exc}") from exc


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

from dokimi.external import trigger_set as ts

catalog_root = Path(args.catalog)
if not catalog_root.is_dir():
raise SystemExit(f"--catalog is not a directory: {catalog_root}")
catalog = discover_skills(catalog_root)
catalog = _discover_catalog_skills(catalog_root)
# Checked before any --rewrite call, so a bad invocation costs no LLM calls.
if args.format == "skill-creator":
if not args.target:
Expand Down Expand Up @@ -191,15 +222,7 @@ def rewrite(text: str) -> str:
# cannot pick up a set with a mistyped label.
print(json.dumps(summary, indent=2))
raise SystemExit(1)
if args.format == "questions":
from dokimi.questions.artifact import save_questions_artifact

save_questions_artifact(ts.to_questions(queries), args.out)
else:
# External tools' formats: no schema of ours to hold them to.
payload = (ts.to_reach(queries, catalog_id=str(catalog_root)) if args.format == "reach"
else ts.to_skill_creator(queries, args.target))
Path(args.out).write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
_write_trigger_set(args, queries, catalog_root)

print(json.dumps(summary, indent=2))
multi = sum(len(q.acceptable) > 1 for q in ts.labelled(queries))
Expand Down
53 changes: 49 additions & 4 deletions dokimi/dokimi/commands/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
from __future__ import annotations

import argparse
import errno
import json
import math
import os
import sys
import threading
import time
from pathlib import Path

Expand Down Expand Up @@ -91,13 +94,54 @@ def _print_skipped(skipped: dict, n_groups: int) -> None:
print(f" {factor}: skipped for {len(ids)} of {n_groups} groups ({', '.join(ids)})")


def _write_items(path: str, items: list) -> None:
"""Write the probe items to ``path``, or exit 1 naming it when it cannot be written."""
try:
Path(path).write_text(json.dumps(items, indent=1), encoding="utf-8")
except OSError as exc:
print(f"Error: cannot write --items-out {path}: {exc}", file=sys.stderr)
raise SystemExit(1) from None


def _save_result(data: dict, path: str) -> None:
"""Save the tool_preference artifact to ``path``, or exit 1 naming it when it cannot be written."""
from dokimi.artifacts import ArtifactValidationError, save_artifact

try:
save_artifact("tool_preference", data, path)
except ArtifactValidationError as exc: # the path was writable; the payload failed the schema
Comment thread
vchamarthi marked this conversation as resolved.
Resolved
print(f"Error: tool_preference artifact rejected: {exc}", file=sys.stderr)
raise SystemExit(1) from None
except (OSError, ValueError) as exc: # unwritable path, including an empty one (pathlib ValueError)
print(f"Error: cannot write {path}: {exc}", file=sys.stderr)
raise SystemExit(1) from None


def _check_out_json_writable(path: str) -> None:
"""Exit 2 before any probe call when a save to ``path`` would fail, probed with a temp file that is removed."""
from dokimi.artifacts import _write_target

target = Path(path)
try:
target.parent.mkdir(parents=True, exist_ok=True)
resolved = _write_target(target)
if resolved.is_dir():
raise IsADirectoryError(errno.EISDIR, "Is a directory", path)
tmp = resolved.with_name(f"{resolved.name}.{os.getpid()}.{threading.get_ident()}.tmp") # the save's name
with tmp.open("xb"):
pass
tmp.unlink()
except (OSError, ValueError) as exc:
print(f"Error: cannot write {path}: {exc}", file=sys.stderr)
raise SystemExit(2) from None


def cmd_tools_preference(args: argparse.Namespace) -> None:
from dokimi.artifacts import save_artifact
from dokimi import tool_preference as tp

try:
doc = tp.load_groups(json.loads(Path(args.groups).read_text(encoding="utf-8")))
except (OSError, json.JSONDecodeError, tp.GroupsError) as exc:
except (OSError, RecursionError, ValueError, tp.GroupsError) as exc:
raise SystemExit(f"--groups {args.groups}: {exc}") from exc
factors = list(dict.fromkeys(f for f in args.factors.split(",") if f)) if args.factors else list(tp.FACTORS)
unknown = sorted(set(factors) - set(tp.FACTORS))
Expand All @@ -110,12 +154,13 @@ def cmd_tools_preference(args: argparse.Namespace) -> None:
+ (f", stopping at ${args.max_cost_usd:g}" if args.max_cost_usd else ""))
_print_skipped(tp.skipped_factors(doc, factors), len(doc["groups"]))
if args.items_out:
Path(args.items_out).write_text(json.dumps(items, indent=1), encoding="utf-8")
_write_items(args.items_out, items)
print(f"items -> {args.items_out}")
if args.dry_run:
return
if not args.model:
raise SystemExit("--model is required unless --dry-run")
_check_out_json_writable(args.out_json)

def progress(done: int, total: int) -> None:
if done % 100 == 0 or done == total:
Expand All @@ -127,7 +172,7 @@ def progress(done: int, total: int) -> None:
data = tp.payload(doc, items, results, model=args.model, provider=args.provider,
reps=args.reps, factors=factors, max_tokens=args.max_tokens,
max_cost_usd=args.max_cost_usd)
save_artifact("tool_preference", data, args.out_json)
_save_result(data, args.out_json)
Comment thread
vchamarthi marked this conversation as resolved.
Resolved
_print_summary(data["summary"])
u = data["usage"]
print(f"cost: {u['cost_usd']} USD over {u['n_calls']} calls ({u['cost_known_calls']} priced)")
Expand Down
Loading
Loading