Letting the brain test and refine a skill, and resuming a run that stopped.
nexusbrain skillopt improves a skill by rewriting its SKILL.md for you. It runs the skill on a set of test tasks, scores the results, asks a model to propose small edits, and keeps an edit only if the skill scores clearly better on tasks it did not train on.
Use it when a skill works but is unreliable, and you can describe what a good result looks like. This guide covers building the test set, previewing cost, running, reviewing, resuming, and the extra safety rules for skills that ship with nexusbrain.
skillopt exits with requires a configured brain if the engine cannot connect.SKILL.md, found under a skills/ directory in your workspace, or passed with --skills-dir <path>.SKILL.md with uncommitted changes unless you pass --force.Think of SKILL.md as the thing being trained, and three models as the trainers. The target model runs the skill on each task. The judge model scores each output. The optimizer model reads the successes and failures and proposes edits.
Your test tasks are split, sorted by task_id, into three groups using --split TRAIN:SEL:TEST (default 4:1:5). Training tasks feed the optimizer. Selection tasks decide whether an edit is kept. Test tasks are only scored at the end, to show whether the improvement holds up.
Each step runs a batch of training tasks, asks the optimizer (two reflect calls) for edits, applies up to the current edit budget, then scores the edited skill on every selection task three times and takes the median. The edit is accepted only if the average beats the best score so far by more than 0.05. Rejected edits are remembered so they are not proposed again.
The edit budget starts at --lr (default 4 edits per step) and shrinks over the run following --lr-schedule (default cosine). There are --epochs passes (default 4), each with training tasks divided by --batch-size (default 8) steps.
| Flag | Default | What it controls |
|---|---|---|
--epochs N | 4 | Passes over the training tasks. |
--batch-size N | 8 | Training tasks per step. |
--lr N | 4 | Maximum edits applied per step. |
--lr-schedule cosine|linear|constant | cosine | How the edit budget shrinks over the run. |
--split TRAIN:SEL:TEST | 4:1:5 | How tasks are divided. Refuses if fewer than 5 selection tasks result. |
--optimizer-model MODEL | models tier deep | Reflects and proposes edits. |
--target-model MODEL | models tier subagent | Runs the skill. |
--judge-model MODEL | models tier reasoning | Scores outputs. |
--patch / --rewrite | --patch | Edit operations only, or allow whole sections to be rewritten. |
The benchmark is a JSONL file: one JSON object per line with a unique task_id, a task prompt, and a judge. By default it is read from skills/<skill>/skillopt-benchmark.jsonl; pass --benchmark <path> to use another file.
This reads SKILL.md, asks the optimizer model for about 15 tasks with rule checks, and writes them to skills/<skill>/skillopt-benchmark.jsonl. Use --bootstrap-tasks N for a different count, up to 50.
nexusbrain skillopt meeting-prep --bootstrap-from-skillYou should seeThe file is created and its last line is # BOOTSTRAP_PENDING_REVIEW. If a benchmark already exists you get Benchmark already exists at <path>.; pass --force to overwrite.
Read each task and its checks. Remove vague checks, tighten limits, and make sure headings and tool names are real. The optimizer only chases what the judges reward, so weak judges produce a weak skill.
Remove the # BOOTSTRAP_PENDING_REVIEW line. Until you do, the run refuses the file.
With 15 tasks the default 4:1:5 gives only 1 selection task, so use --split 1:1:1 (5 train, 5 selection, 5 test) or write more tasks.
op values: contains, regex, section_present, tool_called, tool_not_called (string arg), and max_chars, min_citations (number arg).{"task_id":"t01","task":"Prep me for my 2pm with the design team","judge":{"kind":"rule","checks":[{"op":"section_present","arg":"Agenda"},{"op":"max_chars","arg":4000}]}}.routing-eval.jsonl, --bootstrap-from-routing builds the starter from that instead.| Judge | Shape | Notes |
|---|---|---|
| Rule | {"kind":"rule","checks":[{"op":"max_chars","arg":4000}]} | Deterministic and free. Needs at least one check. |
| LLM | {"kind":"llm","rubric":"..."} | A rubric describing how to score 0 to 1. Optional model. |
| Qrels | {"kind":"qrels","expected_slugs":["..."],"k":10} | For retrieval skills. k defaults to 10. |
A dry run loads and validates the benchmark, checks the safety gates, and estimates cost. It makes no model calls and changes no files.
nexusbrain skillopt meeting-prep --bootstrap-reviewed --split 1:1:1 --dry-runYou should seeA block starting [skillopt] Cost estimate for with rollout, reflect and judge call counts, estimated tokens, and Est. cost: $X (cap: $5.00), then [skillopt] Outcome: aborted. The exit code is 2, which is normal for a dry run.
If the estimate is over --max-cost-usd (default 5.00), the run is refused before anything is spent. Raise the cap or lower --epochs or --batch-size.
The estimate uses fixed token guesses per call and assumes some prompt caching. During the real run a budget tracker stops the run once actual spend reaches --max-cost-usd, and --max-runtime-min (default 30) stops it on wall-clock time. Both end with outcome aborted.
Add --json for one machine-readable line on stdout that includes the receipt and its run_id.
nexusbrain skillopt meeting-prep --bootstrap-reviewed --split 1:1:1You should seeThe cost estimate, then at the end [skillopt] Outcome:, Best sel-score:, Final cost: and either SKILL.md rewritten with N optimization steps. or Proposed improvements written to <path>. Review + copy manually.
0 means an improvement was accepted (or proposed.md was written). 1 means no improvement, so SKILL.md is unchanged. 2 means a gate stopped the run.
For a user-owned skill, SKILL.md is replaced in place and each accepted version is saved under skills/<skill>/skillopt/versions/. Use git diff to read the change.
To keep SKILL.md untouched and get skills/<skill>/skillopt/proposed.md instead, pass --no-mutate.
git diff skills/meeting-prep/SKILL.md--all optimizes every skill that has a benchmark, with --max-cost-usd per skill and --brain-wide-max-cost-usd (default 10.00) for the whole batch.--target-models a,b,c runs once per target model in no-mutate mode and reports the best model.~/.nexusbrain/audit/skillopt-YYYY-Www.jsonl.While a run is going, its state is saved as skills/<skill>/skillopt/checkpoint-<run-id>.json. The file is deleted when a run finishes with an improvement or with no improvement, and kept when it is aborted or errors. Stale checkpoints older than 7 days are cleaned up by the dream cycle.
nexusbrain skillopt meeting-prep --bootstrap-reviewed --split 1:1:1 --resume <run-id>You should seeThe run skips the epochs and steps already completed and continues from the best skill text saved in the checkpoint.
The checkpoint is saved after each accepted step and at the end of each epoch, so rejected steps since the last save run again. The baseline is also scored again, which costs money. If no checkpoint exists for that id, the run starts fresh under that id without a warning.
A bundled skill is one that lives in the skills/ folder of your nexusbrain install. By default skillopt never overwrites it: an accepted improvement goes to proposed.md for you to review.
To change a bundled skill in place you need both --allow-mutate-bundled and a held-out file with at least 5 tasks. The held-out file uses exactly the same JSONL format as the benchmark, and none of its task_id values may appear in the benchmark. Before any edit is accepted, the edited skill and the original are both scored on the held-out tasks, and the edit is refused if it scores lower than the original.
Use real tasks you did not put in the benchmark, with at least 5 lines.
nexusbrain skillopt brain-ops --allow-mutate-bundled --held-out skills/brain-ops/held-out.jsonl --dry-runYou should seeThe cost estimate now includes held-out scoring, then Outcome: aborted for the dry run.
nexusbrain skillopt brain-ops --allow-mutate-bundled --held-out skills/brain-ops/held-out.jsonlYou should seeSKILL.md rewritten if an edit passed both the selection gate and the held-out gate.
You can pass --held-out for your own skills too. It is the best protection against a skill that learns to pass your benchmark while getting worse at real work.
Mutating bundled skill '<name>' in place requires a non-empty held-out set (>= 5 rows); got 0.Add --held-out <path> with at least 5 tasks, or drop --allow-mutate-bundled to get proposed.md instead.
Benchmark at <path> is a bootstrap output awaiting human review.Review the file, delete the trailing # BOOTSTRAP_PENDING_REVIEW line, then re-run with --bootstrap-reviewed.
D_sel has N task(s) after split (need >=5 for meaningful validation).Add more tasks, or pass a --split with a larger middle number, such as --split 1:1:1 for 15 tasks.
estimated cost $X exceeds --max-cost-usd $Y.Raise the cap with --max-cost-usd, or reduce --epochs or --batch-size. Check again with --dry-run.
<path> has uncommitted changes.Commit or stash your edits to SKILL.md first. --force bypasses this, but then you lose the clean git diff for review.
Held-out set shares N task_id(s) with the benchmark.Give the held-out tasks their own task_id values. An overlapping held-out set cannot catch overfitting.
Another SkillOpt run is in progress for skill '<name>'.Wait for the other run to finish, or check nexusbrain jobs supervisor status. A stale lock expires on its own.
Cannot find SKILL.md for '<name>' or cannot find skills directory.Run from a workspace that has a skills/ folder, or pass --skills-dir <path>. Create a new skill with nexusbrain skillify scaffold <name>.