Updated 2026-08-11. This file is the session handoff: current gate status, exact next commands, open points. See PLAN.md for gates and success criteria.
| Gate | Status | Notes |
|---|---|---|
| G0 — schema + grammar | ✅ GREEN | 12 intents incl. out_of_scope, multi-action, seeded grammar |
| G1 — dataset + frozen eval | ✅ GREEN | 639 verified (586 kept), eval_v0.jsonl (400) frozen + dev.jsonl (186) split, see EVAL_FREEZE.md |
| G2 — baselines | ✅ GREEN | 3 models run on eval_v0.jsonl, see table below and reports/ |
| G3 — LoRA fine-tune | ✅ GREEN | 98.5% exact-match on eval_v0 — beats Baseline A outright, success criterion met |
| G4 — writeup + publish | ✅ GREEN | README rewritten as the report; pushed; repo public at https://github.com/comprido96/comando |
Tests: 40/40 green (uv run pytest -q).
| Model | exact_match | action_f1 | invalid_rate | cost |
|---|---|---|---|---|
| gpt-5-mini | 90.25% | 91.72% | 0.0% | $0.08 |
| claude-haiku-4-5 | 88.0% | 89.81% | 0.0% | $0.34 |
| Qwen3-4B-Instruct-2507-4bit (local, prompted) | 68.75% | 73.80% | 5.5% | $0 |
Local Qwen well behind both API models, with a real invalid-JSON rate the API models never hit — the gap G3's LoRA fine-tune targets.
Ablation on data/dev.jsonl (n=186) — sizes are nested prefixes of one shuffle (500 ⊂ 1500 ⊂ 5000), not independent samples:
| Train size | exact_match | action_f1 | invalid_rate |
|---|---|---|---|
| 500 | 73.12% | 76.61% | 1.61% |
| 1500 | 95.70% | 96.76% | 0.54% |
| 5000 | 94.62% | 95.61% | 0.0% |
5000 scoring below 1500 despite being a strict superset is the interesting non-monotonic result for the writeup — not sampling noise, since it's a nested subset relationship. Candidate hypothesis: fixed-epoch-count training means 5000 sees the same 1 epoch as 1500 but with more surface diversity per gradient step, vs 1500 getting more repetition of a smaller pattern set — needs a real ablation (e.g. matched-iters vs matched-epochs) to confirm, flagged for G4.
Picked 1500 (best dev exact-match) for the one-shot eval_v0.jsonl run, per the "touched once" policy:
| Model | exact_match | action_f1 | invalid_rate | cost |
|---|---|---|---|---|
| gpt-5-mini (baseline A) | 90.25% | 91.72% | 0.0% | $0.08 |
| Qwen3-4B + LoRA (1500) | 98.5% | 98.73% | 0.0% | $0 |
Success criterion (PLAN.md G3, fixed before training) met outright: beats Baseline A on exact-match, at $0 marginal cost.
Training config (uv run mlx_lm.lora): rank 8 (default), 16 layers, batch 4, lr 1e-4, 1 epoch (iters = size/4), --mask-prompt (completion-only loss on the JSON target), seed 0. Peak mem 9.5 GB — safely under the 18 GB M3 Pro budget (batch 16 was tried first and hit 26 GB / thrashed; reverted). dev.jsonl served as mlx-lm's internal validation set during training (loss monitoring only — no early stopping, no selection bias) and as the ablation/model-selection set after.
Fine-tuned model's system prompt: same style as the prompted baselines, minus the few-shot block (comando.lora_data.SYSTEM_PROMPT = build_system_prompt([])) — the adapter is trained on this exact zero-shot prompt, and comando.baselines --adapter ... automatically drops few-shot too, so train/inference distributions match exactly. Resolves the open point from G1/G2.
train.jsonl: 7,149 examples (grammar + gpt-5-mini paraphrases, label-guard filtered, deduped)eval_candidates.jsonl: 639 examples, zero overlap with train (tested invariant)out_of_scope: 34 eval / 309 train — includes near-domotic traps (TV, oven, locks, gates)- Seeds:
TRAIN_SEED=1000,EVAL_SEED=2000,FEW_SHOT_SEED=3000— never reuse across sets - Paraphrase backend:
gpt-5-miniwithreasoning_effort="minimal"(2.6× faster, 3× cheaper than default — measured). Total API spend so far ≈ $3
Hand-verify eval candidatesdone — 586/639 kept.Freeze eval_v0done —data/eval_v0.jsonl(400, committed) +EVAL_FREEZE.md.data/dev.jsonl(186) split from leftover verified pool, short of ~200 target (see open points).G2 baselinesdone — all 3 models run, see results table above.uv syncneeds all three extras together (--extra gen-openai --extra eval-anthropic --extra gen-mlx) or it drops the others.G3 LoRAdone — see results table above. Adapters inadapters/{500,1500,5000}/(gitignored, reproducible viacomando.lora_data+ the training config documented above).G4 writeupdone — README rewritten as the report: results table, ablation curve, failure taxonomy (dominant frontier-baseline error is room-synonym canonicalization, not reasoning — 85%/92% of gpt-5-mini/claude-haiku misses; LoRA's 6 residual eval_v0 misses split into a dataset-convention tension and a real terse-phrasing generalization gap), honest limitations.Publishdone — pushed, repo flipped to public by Federico. All 4 gates green.Link from profile READMEdone — added a bullet + Highlights entry incomprido96/comprido96.
- No provider strict-output modes — identical prompt + strict shared parser for all models; comparison measures models, not tooling
- Paraphraser may only change surface text — labels copied, never re-derived; conservative lexical guard; numbers stay digits
- Wrong eval labels are dropped, not edited (single-annotator edit risk)
- Grammar prepositions are crude by design ("del studio") — paraphrase smooths, hand-verification gates the frozen set
- Employer-clean is absolute: no TomatoAI data, no TomatoAI API keys (personal keys only, in
.env, gitignored) - Repo was private until G4 results were real — now public at https://github.com/comprido96/comando, linked from the profile README
out_of_scopeshare in eval (~5%) is thin — acceptable for v0, note in G4 limitationsPricing constants indone, 2026-08-11: gpt-5-mini $0.25/$2.00 per MTok confirmed via developers.openai.com/api/docs/pricing; claude-haiku-4-5 $1/$5 per MTok confirmed via platform.claude.com official pricing docs. Both match the existing constants exactly — no code change neededbaselines.pyre-verificationdata/dev.jsonlis 186, short of ~200 target (leftover pool after 400-cut eval_v0) — top up from fresh generation before G3 if needed, not a G2 blocker- Found several recurring paraphrase bugs during hand-verification, worth fixing at the source (
paraphrase.py/grammar.py) before any future eval_v1 cut: bare "di" + article not contracted ("di le" / "di il" / "di l'" instead of "delle"/"del"/"dell'"); some declarative statements ("le luci sono accese") mislabeledquery_stateinstead of the real imperative intent when a command follows ("...spegnile"); explicit numeric values in text sometimes dropped from label instead of triggering the_set/_dimvalue intent - The 5000 < 1500 ablation non-monotonicity is unexplained — candidate hypothesis (fixed-epoch vs fixed-iters training budget) not yet tested. Worth a follow-up run before G4 if there's time, otherwise publish as an honest open question
- G2's Baseline C only ran few-shot prompted (per PLAN.md checklist item, zero-shot variant was skipped as not needed for the story) — note if anyone re-derives numbers from PLAN.md expecting both