tap-model-training
SkillOperational playbook for training, evaluating, and publishing muvon/octomind-embed, the granite-embedding-30m WiSE-FT soup that powers octomind's capability auto-activation and skill semantic() rules. Covers when to retrain (any change to capabilities/*/config.toml triggers or skills/*/SKILL.md semantic phrases), the bin/train pipeline (LLM augmentation, dataset build, hard-negative re-mining, CachedMNRL + Matryoshka fine-tune, soup sweep, holdout and eval_real evaluation, int8 ONNX export and calibration), resuming an interrupted run, calibrating the runtime threshold and margin, publishing behind the regression gate, and recording the baseline. Use when triggers or semantic rules change, when retraining or republishing the tap model, or when auto-activation picks the wrong capability.
Instructions
Overview
muvon/octomind-embed is the bi-encoder octomind uses to score a user message against capability triggers and skill semantic() phrases. Since 2026-09 it is ibm-granite/granite-embedding-30m-english (6 layers, 384-dim, CLS-pooled) given a short contrastive fine-tune and blended back into the base as a WiSE-FT soup; the runtime loads its int8 ONNX graph. Whenever triggers or semantic phrases change, the model is stale. This skill walks through retraining, calibration, publishing, and recovery.
The cross-encoder reranker is intentionally not trained or shipped: at the current capability count it does not improve routing over a well-calibrated bi-encoder.
Mental model
The runtime decision is: intent → one installed capability, or abstain. It scores each capability as the mean of its top-3 trigger cosines and accepts the winner only if it clears an absolute threshold and beats the runner-up by a margin. The model, the trigger corpus, and those two constants are one system: retraining moves the cosine scale, so the constants must be recalibrated on the int8 graph and shipped together with the weights.
The fine-tune is a nudge. A full fine-tune forgets the base's generalization; interpolating fine-tuned and base weights beats both parents, so bin/train sweeps the soup alpha and ships the best gate.
Rules
When to retrain
| Change | Retrain? |
|---|---|
Added or removed a capability (capabilities/<name>/config.toml) | yes |
Edited triggers = [...] | yes |
Added or edited semantic(...) rules in any SKILL.md | yes |
Edited the _oos curated phrases in build_dataset.py | yes |
| Agent, skill body, or docs change with no trigger touched | no |
Threshold/margin change in octomind capability.rs / skill.rs | no — rebuild octomind |
Standard retrain flow
Run from model/, inside tmux or nohup so a dropped connection doesn't kill it:
cd model && uv sync
tmux new -s train
ANTHROPIC_API_KEY=... bin/train --llm --resume --iterations 2What runs (bin/train):
build_eval_seed.pyonce, ifdata/eval_real.jsonlis missing.augment_llm.py --resume— LLM paraphrases for new(label, trigger)pairs only, appended todata/intents.jsonl.build_dataset.py—pairs.jsonl,triplets.jsonl,holdout.jsonl, andraw_triggers.jsonl(the un-augmented corpus the runtime actually embeds), with the_oossink label, multi-turn surfaces, and hard-negative mining.train.py— CachedMNRL (scale 10) + Matryoshka, one epoch capped bytrain.max_steps; with--iterations N, triplets are re-mined from the previous round's mistakes between rounds.model_soup.py+compare_bases.py— sweepsoup.alphasagainst the base and keep the best gate onraw_triggers.jsonl.eval.py(holdout) andeval_gate.py(eval_real, informational here).export_onnx.py(fp32 + int8), theneval_gate.pyandcalibrate_thresholds.py --target-fpr 0.03on the int8 graph.
Other flags: --skip-build (reuse data/), --skip-export, --ft checkpoints/<dir> (redo soup, eval, export, and calibration on an existing run), --legacy (diagnostic model.fit path).
Calibrating the runtime gate
bin/train prints the recommended operating point. To rerun by hand:
uv run python scripts/calibrate_thresholds.py \
--model checkpoints/<run>/onnx --onnx-file model_quantized.onnx --target-fpr 0.03It sweeps τ × δ over data/eval_real.jsonl and recommends the highest gate accuracy with null false-positive rate under the target. Copy the values into:
octomind/src/mcp/runtime/capability.rs—AUTO_ACTIVATE_THRESHOLD,AUTO_ACTIVATE_MARGINoctomind/src/mcp/runtime/skill.rs—SEMANTIC_DEFAULT_THRESHOLD,SEMANTIC_MARGINmodel/configs/default.yaml—eval.runtime_threshold,eval.runtime_margin(the publish gate scores at these)
The shipped soup runs at 0.65 / 0.06. Calibrate on the int8 graph: quantization shifts cosines, and that graph is what users load.
Publishing
HF_TOKEN=hf_... bin/publish # latest checkpoint; --run <dir> to choosebin/publish runs eval_gate.py against eval_baselines.json on the fp32 weights and again on the int8 graph, and aborts on regression (top-1 accuracy drop > 0.01, null FPR rise > 0.01, any per-capability recall drop > 0.05). It warns if onnx/model_quantized.onnx is missing, since the runtime prefers it. --force/--skip-gate bypass the gate for documented emergencies only; --dry-run prints without uploading.
New weights reach fresh installs only — a cached HF repo is not re-checked — so ship the recalibrated octomind constants in the same release. After the rollout is confirmed, record the baseline the next publish gates against:
uv run python scripts/eval_gate.py --model muvon/octomind-embed --write-baseline eval_baselines.jsonResuming an interrupted run
The trainer checkpoints every epoch (save_total_limit=2). If checkpoints/embed-*/checkpoint-N/ exists:
uv run python scripts/train.py --resume # latest run dir
uv run python scripts/train.py --resume-from <run-dir> # explicitIf iteration 1 was interrupted: finish it with --resume, re-mine with build_dataset.py --neg-embed-model <iter1-dir>, train iteration 2 with train.py, then bin/train --ft <iter2-dir> for soup, eval, export, and calibration. --resume covers the modern train.py path only, not --legacy.
Examples
Example 1: new triggers not picked up
❌ bin/train --skip-build after editing triggers — the build is what turns triggers into training surfaces, so the new run inherits the old weaknesses.
✅ bin/train --llm --resume --iterations 2 — rebuild, augment only the new triggers, retrain.
Example 2: wrong capability wins but triggers look right
Retraining rarely fixes this. First run calibrate_thresholds.py to see whether the shipped τ/δ still fit the model; add the failing prompt to data/eval_real.jsonl with its correct label and rerun eval_gate.py; then compare the competing capabilities' triggers — shared generic vocabulary is fixed by differentiating triggers, not by more training.
Example 3: publishing without a baseline update
Publishing and forgetting --write-baseline leaves the next publish gating against the previous model, so a later regression can slip through. Record the baseline once the rollout is confirmed.
Checklist
- Trigger or semantic-phrase change → full rebuild with
--llm --resume, not--skip-build? - Training run inside tmux or nohup?
- Soup winner evaluated on holdout and
eval_real, int8 graph exported and scored? - τ/δ calibrated on the int8 graph and copied into
capability.rs,skill.rs, andconfigs/default.yaml? bin/publishpassed the gate without--force, and the octomind release carries the new constants?- Baseline rewritten after the rollout was confirmed?
Composition / References
model/README.md— pipeline, dataset, loss stack, eval setmodel/configs/default.yaml— base model, training, soup alphas, export, runtime-mirror constantsmodel/bin/train,model/bin/publish— entry pointstap-capability-authoringskill — writing triggers