Results
lxask was called linu from 0.3 to 0.8, and these notes use the names of their time.
Every version and training round, newest first, with what was tried and not shipped.
#v0.8.0: a restriction adapter for the requests that need it
Round 6's fine-tune (6d, below) keeps restrictions far better than round 4 but drifts
on everyday requests. It ships as a 37 MB LoRA adapter of round 4 instead of a new
model, and is applied only to requests that rule out a program the catalogue knows,
or that mention a proxy (linu_core::route); every other request gets round 4
unchanged.
| set | 0.7.1 | 0.8 |
|---|---|---|
| forbidden (40) passed | 60.0% | 80.0% |
| forbidden: ruled-out program used | 30.0% | 10.0% |
| forbidden_heldout (79) passed | 60.8% | 75.9% |
| ambiguous (24) passed | 70.8% | 79.2% |
| exec_tasks (46), strict | 84.8% | 87.0% |
| exec_heldout (200), strict | 75.0% | 75.0% |
| the other sets (everyday, tldr, nl2bash, multistep, regressions) | 1 of 745 answers changed, same scores |
(GPU for the sets, the program on the CPU in Docker for the execution sets.) Only
the restriction tasks of exec_tasks are routed: 3 more pass (awk 'END {print NR}'
without wc, awk /error/ without grep, cat > copy without cp) and 2 fewer (cat /dev/null > old.txt empties the file instead of deleting it; perl counting lines
for words). None of the 200 heldout tasks is routed; their commands are identical.
The adapter adds about 90 MB of memory (1.30 GB peak) and switching it costs the
prompt cache, a few percent of time.
Of those 745 questions one is routed (an ssh SOCKS proxy; same answer). The one
changed answer (rsync -an for rsync -av on a dry run) is the question after it:
switching back empties the prompt cache, and a prompt computed whole rounds slightly
differently from one that reuses a cached prefix.
Applied at run time to the quantized model, the adapter does a little worse than the
same fine-tune merged before quantizing (forbidden 80.0% against 87.5%), and the same
elsewhere. The model alone is a --model or LINU_ADAPTER=off away.
#v0.7.1: three rule fixes (same model)
- A list of ruled-out programs is read to its end: "without lsof, netstat, or ss" rules out all three, while "show the log without the header, and grep for ERROR" still rules out nothing but the header.
- A request that only asks to look ("show which files can safely be removed",
"list", "check", "count") answered with a command that deletes (
rm,rmdir,unlink,shred,find -delete) is an objection: retried, and marked if it stays. It flags 2 of 32,421 known-good pairs. - "Set a proxy in .curlrc" (or
.wgetrc) names a context and is no longer asked back.
Every evaluation answer is the same as 0.7.0 (0 of 809 changed on the GPU); the fixes are for requests the sets do not have.
#Round 6: training on restrictions and proxy contexts (shipped as the 0.8 adapter)
The gaps 0.7.0 left need data: new generator jobs forbid (963 requests that rule a
program out, answered without it), forbid_scenarios (98 multi-part ones),
contexts (311 proxy requests for APT, /etc/environment, the Docker daemon, npm
and others) and forbid_twins (905 of the same requests with the restriction
removed and the usual command, so that the model does not learn to avoid the tool
everywhere). A short LoRA fine-tune of round 4 on them plus a replay sample of the
old training data (scripts/mix_finetune.py), with a held-out set of programs never
ruled out in training (forbidden_heldout):
| set | round 4 | 6 (no twins) | 6b (twins) | 6c (replay answered by round 4) | 6d |
|---|---|---|---|---|---|
| forbidden passed | 60.0% | 82.5% | 75.0% | 82.5% | 87.5% |
| forbidden_heldout passed | 60.8% | 79.7% | 78.5% | 77.2% | 75.9% |
| ambiguous passed | 70.8% | 79.2% | 91.7% | 79.2% | 79.2% |
| everyday program | 98.6% | 94.3% | 97.1% | 98.6% | 97.1% |
| test_everyday program | 96.2% | 93.8% | 95.0% | 93.8% | 95.0% |
| nl2bash_test program | 80.0% | 78.0% | 81.3% | 76.0% | 77.3% |
| tldr_heldout exact | 50.0% | 55.3% | 52.0% | 54.0% | 52.7% |
| exec_tasks / exec_heldout, strict | 84.8% / 75.0% | 82.6% / 74.0% | 87.0% / 71.0% |
Restrictions are learned, and they carry over to programs never seen ruled out
(heldout 60.8% -> 77-80%). The price is drift elsewhere: without twins the model
avoided the usual tool even when nothing was ruled out (watch -> free --seconds 2, lsb_release -> uname); replaying the original references pulled the
RL-trained model back toward pre-RL habits ("$HOME/work" for a relative path);
answering the replay with round 4 itself fixed most of that, but 6c still turns
some find answers into grep -r or ls and fails the ship rule on
test_everyday, nl2bash_test and exec_heldout. 6c also kept its step-100 checkpoint:
train_lora.py keeps the lowest loss on data/val.jsonl, whose original references
an RL-trained model has moved away from. 6d validates on 400 examples held out of its
own mix (mix_finetune.py --val-out), repeats the restriction examples once instead
of twice, replays 600 more answers that run find, umount or apt, at half the
learning rate. It is the best on restrictions and closer elsewhere, but still drifts
on a few everyday requests (watch -n 2 -> free --seconds 2, rm -r -> rmdir):
every fine-tune on this data has paid for restrictions with everyday answers. So 6d
ships as an adapter used only where it is better (v0.8.0).
#v0.7.0: restrictions kept, vague requests asked back, stricter execution checks
Same model. Three rules, measured on two new sets and the strict execution checks:
| 0.6.1 | 0.7 | |
|---|---|---|
| forbidden (40): request's ruled-out program used | 57.5% | 32.5% |
| forbidden: right program and nothing ruled out | 37.5% | 60.0% |
| ambiguous (24): passed (vague ones asked back, others in form) | 37.5% | 70.8% |
| exec_tasks (46), strict / without the strict checks | 78.3% / 93.5% | 84.8% / 95.7% |
| exec_heldout (200), strict | 75.0% | 75.0% |
| the 745 answers of the other sets | 0 changed |
(The program on the CPU for the first rows and the execution sets; the other sets on the GPU through the same core.) The ruled-out check flags none of about 30,000 known-good generated and reference pairs, and asking back fires on none of the 1,018 questions of the other sets or the 18,777 of the training data.
What is left: the model still reaches for the tool it was told to avoid when it
knows no other way (tail without tail, find without find, uname without
uname: three tries, all the same). Asked a clear proxy question it knows the shell,
Git, pip and SSH forms, not APT's Acquire::http::Proxy, the system-wide
/etc/environment, Docker's daemon settings or npm config set proxy. Both need
training data, not rules. The strict execution checks changed no heldout result
(every failure there was already a wrong output); on exec_tasks they catch the
ruled-out programs that print the right answer.
#Retries told what was wrong (tried, not shipped)
A retry samples the same prompt again. The alternative tried: show the model its answer and the objection as a second turn ("That command is not right: the request says '.log'; ..."), greedy first, then sampled. Full GPU evaluation against 0.6.1:
| set | resample (0.6.1) | told what was wrong |
|---|---|---|
| regressions passed | 7 of 7 | 6 of 7 (pip list for installed packages) |
| everyday program / clean | 98.6% / 100% | 97.1% / 95.7% |
| tldr_heldout program / exact | 74.7% / 50.0% | 76.0% / 49.3% |
| nl2bash_test clean | 98.0% | 96.7% |
| multistep_heldout coverage | 90.9% | 91.4% |
The model was trained on single-turn prompts; given a second turn it wrote
placeholders (systemctl status unit.service|timer|socket) or dropped what the
objection named, where a fresh sample of the first prompt usually found a clean
command. Used only after both samples fail it would help little: 15 of about 1,100
steps still have an objection then, and the feedback turn fixed 1 of them (git rename-tag -> git tag --delete and git tag). Retries stay as they are.
#v0.6.1: verbs checked against the manual
systemctl inspect passed the validator in 0.6.0: options were checked, but not the
command word after the program. For the programs that take a verb first (systemctl,
loginctl, hostnamectl, timedatectl, localectl, resolvectl, networkctl,
busctl) the index now keeps the verbs their man pages list under COMMANDS, plus the
older names they still accept (systemctl force-reload, hostnamectl set-hostname),
and any other word there is an objection: retried, and marked if it stays
(unverified: systemctl: inspect is not a documented command). It flags none of the
1,469 known-good commands that use these programs (tldr examples, evaluation
references, training data), and of all the 0.6.0 evaluation and execution answers
only that one; the rest are unchanged. Its retries did not find a better command
there: the split had already asked to "inspect" a directory. An index built before
0.6.1 has no verbs, and nothing is checked with it.
#v0.6.0: the runtime in Rust (same model, same rules)
The whole runtime was ported to Rust, one module at a time against 0.5 as the reference; Python keeps training and evaluation and calls the same core.
- The core (split, prompts, retrieval ranking, validator with an exact port of
Python's
shlex, task checks and repairs, explanations, safety): 1,273,962 calls over every evaluation question and training reference, 0 differences from 0.5. All 346 Python tests pass on it. Evaluation through it on the GPU changed 0 of 745 answers, and in Docker 233 of 233 execution-task commands are the same. - The models (llama.cpp through
llama-cpp-2, set up and sampled the way llama-cpp-python 0.3.36 does): prompt tokens identical on all 622 prompts, encoder vectors within cosine 0.99988, and 310 of 311 answers byte-identical to 0.5 on the same CPU build. The release builds target AVX2 rather than the build machine's AVX-512, and 270 of 311 come out byte-identical; the rest differ the way two CPUs' rounding does, so they are judged on the evaluation below.
The program (linu-x86_64-v3, CPU, through linu batch) against 0.5 (GPU, measured
again for this comparison):
| set | 0.5 | 0.6 program |
|---|---|---|
| regressions passed | 7 of 7 | 7 of 7 |
| everyday / test_everyday program | 98.6% / 96.3% | 98.6% / 97.5% |
| tldr_heldout program / exact | 74.7% / 50.0% | 74.0% / 50.0% |
| nl2bash_test program / exact | 80.0% / 8.0% | 80.7% / 6.7% |
| multistep (10) coverage | 100% | 97.5% |
| multistep_heldout complete / coverage | 99.4% / 90.9% | 99.4% / 90.7% |
| multistep3_heldout complete | 94.4% | 94.3% |
| exec_tasks (33) / exec_heldout (200) | 93.9% / 75.5% | 93.9% / 75.0% |
Two sets fall by more than a point, each by one or two answers whose numbers came
out differently: the nl2bash exact matches lost are alias m='...' for "..." and
find / -user admin without -print, and the multistep loss is the systemd request,
where the answer now has an invented systemctl inspect (0.6.1 marks it) and
journalctl --follow in a pipeline. The execution loss is uniq -d for uniq -u.
- Install: one 9 MB program and the model files. Tested in fresh containers with
no Python and no network: Debian 12 and Ubuntu 20.04 install and answer; CentOS 7
(glibc 2.17) and Alpine (musl) are refused with a message.
linu-x86_64answers on an emulated Nehalem CPU (SSE4.2, no AVX), wherelinu-x86_64-v3stops with an illegal instruction, as it should;linu-aarch64installs and answers under emulation. - Memory: in a 2 GB container (no swap) all 10 multistep requests, 5 single
questions and 5 more through
--serve, no OOM; capped at 1.5 GB, the same with a container peak of 1,430 MB, page cache included. The process itself peaks at 1,280 MB (0.5: 1,347 MB). - Time: start-up is the model load either way (3.5 to 6 s cold); answers take what llama.cpp takes, the same as 0.5.
#v0.5.0: rules for round 4's real losses (same model)
Two of round 4's losses had one right fix, so they are rules, not training: the
package manager of this machine (yum list installed -> dpkg -l, yum search ->
apt search, any other foreign manager a slip that is retried) and sudo for
commands that always need root. Round 4 through the pipeline, before and after (GPU
evaluation on WSL Ubuntu as a normal user; execution sets in Docker as root, where
no sudo is added):
| set | before | after |
|---|---|---|
| regressions passed | 6 of 7 | 7 of 7 |
| everyday / test_everyday program | 97.1% / 95.0% | 98.6% / 95.0% |
| tldr_heldout program / exact | 74.0% / 50.7% | 74.7% / 50.0% |
| nl2bash_test program / exact | 80.0% / 8.7% | 80.0% / 8.0% |
| multistep coverage, multistep_heldout / multistep3 complete | 100%, 99.4% / 94.4% | same |
| exec_tasks (33) / exec_heldout (200) | 93.9% / 75.5% | 93.9% / 75.5% (no task changed) |
The two exact-match losses are one case each, where the reference leaves out the
sudo a normal user needs (systemctl restart unit1 unit2, mount --bind).
Memory: 1,645 MB peak in a 2 GB container, 1,300 MB at 1.5 GB, no OOM; the server
and its clients 1,319 MB.
#Round 5 (a second RL run, not shipped)
Aimed at round 4's side effects. 1,200 more execution tasks on its weak spots
(exec2: windows of minutes and hours, only directories or files, counts, cat versus
paste; 1,118 proved, $0.10), and 3 of every 8 tasks per step taken from the
supervised training set and scored against their reference (right program 0.6, exact
form 0.4, less 0.2 when a needed sudo is dropped), with twice the KL weight (0.1),
200 steps from round 4. The held-out checks barely moved (execution 74% -> 76%,
right program on held-out ordinary questions 87% -> 88%), and the full evaluation
of the best checkpoint (step 40) matches round 4 instead of repairing it:
| set | round 4 | round 5 |
|---|---|---|
| exec_heldout (200) | 75.5% | 76.0% |
| exec_tasks (33) | 93.9% | 93.9% |
| everyday / test_everyday program | 97.1% / 95.0% | 97.1% / 95.0% |
| tldr_heldout program / exact | 74.0% / 50.7% | 72.0% / 50.7% |
| nl2bash_test program | 80.0% | 80.7% |
| multistep (10) coverage | 100% | 95% |
| regressions passed | 6 of 7 | 6 of 7 |
Round 4 stays installed. Most ordinary questions already earned full reward, so
they gave little signal, and the stronger KL held the model in place; recovering the
flag style or the yum regression would need examples aimed at those, not a broader
reward.
#Round 4 (reinforcement learning; the installed model since v0.4.0)
Reinforcement learning on executed commands. generate_openai.py exec wrote 3,600
file and text tasks, each with a fixture, an expected output or check, and a
reference command ($0.27); prove_exec_tasks.py kept the 3,353 whose reference passes
and whose check fails true and an empty command; prepare_rl.py removed
near-duplicates and anything close to a test question, froze 200 as
evalsets/exec_heldout.jsonl, and kept 1,546 for training with the CLI's own prompts.
train_grpo.py (GRPO, LoRA on round 3, laptop GPU, 300 steps x 8 tasks x 6 samples,
about 1.7 h, 6.5 GB GPU, 4 GB RAM) rewards a reply 1 when its command does the job in
the sandbox (exec_server.py: Docker, no network, nobody), less 0.3 for an
unverified option or a dropped literal and 0.5 for unasked-for destruction, with a
KL penalty towards round 3. Held-out greedy success, raw (no repairs, no retries),
went 52% -> 78%.
Through the full pipeline (checks, retries, repairs), against round 3 measured again with the same code:
| set | round 3 | round 4, step 200 | round 4, step 300 |
|---|---|---|---|
| exec_heldout (200), task done | 60.0% | 72.0% | 75.5% |
| ... of which the 100 never used to pick a checkpoint | 65 | 71 | 75 |
| exec_tasks (33), task done | 87.9% | 100% | 93.9% |
| everyday (70) program | 94.3% | 95.7% | 97.1% |
| test_everyday (80) program | 96.2% | 96.2% | 95.0% |
| tldr_heldout (150) program / exact | 74.0% / 54.0% | 72.7% / 51.3% | 74.0% / 50.7% |
| nl2bash_test (150) program / exact | 81.3% / 12.7% | 81.3% / 8.7% | 80.0% / 8.7% |
| multistep (10) coverage / violations | 95% / 0 | 95% / 1 of 10 | 100% / 0 |
| multistep_heldout complete / coverage | 98.2% / 91.2% | 98.8% / 91.8% | 99.4% / 90.9% |
| multistep3_heldout complete | 94.5% | 94.4% | 94.4% |
| regressions passed | 7 of 7 | 7 of 7 | 6 of 7 |
The rule set before training was: ship only if execution success rises by 3 points
and no other set falls by more than 1. Both checkpoints fail it. Step 300 was shipped
anyway, by the owner's decision: 15 more points of commands that do the job against
losses that are mostly style. Round 3 stays installed beside it
(models/linu-round3-Q4_K_M.gguf; point model.gguf at it to go back). Most of the
exact-match loss is style: the reinforced model
writes short flags (tail -n 20, grep -i, cp -p) where tldr's references spell
them out (--lines 20). Some changes are real losses: find . -mtime 0 for "changed
in the last hour", find . -type d for "only the directories here", sudo dropped
from fsck and apt update, yum list installed for installed packages (step 300),
chmod -R on a single file (step 200). Some are real gains: watch --interval 2,
setfacl -m u:appuser:r-- as the smallest permission change, the biggest files
rather than directories.
#Round 3
Round 2 dropped literals: a third of its training scenarios described paths and
counts in words while their commands used literal ones, so the model learned to
leave .log or /var/log/myapp out and guess. Round 3 adds four generation jobs
aimed at that: scenarios3 (1,416 new multi-part requests where every literal
appears in the steps), retask (1,012 round-2 scenarios rewritten the same way),
constraints (1,727 single tasks built around a size, duration, count, port,
extension or path) and fix (877 corrected answers to tasks the reviewer rejected).
38,873 training examples, one epoch on a Colab T4 (2,430 steps, about 4 hours);
validation loss fell the whole way, to 0.235 (on round 3's validation set, so not
comparable with round 2's 0.290).
The 873 corrected answers were also tried as DPO pairs (scripts/train_dpo.py, on
the laptop GPU: 18 minutes, 5.2 GB peak). It worked on its own terms (the model
preferred the corrected answer on 79% of held-back pairs, up from a tie) but cost
tldr exact match 53% → 30% and multistep coverage 95% → 87.5%, so the shipped model
is the supervised one without it.
All three with checks and retries, retrieval on, GPU:
| set | round 2 | round 3 | round 3 + DPO |
|---|---|---|---|
| everyday (70, dev) program | 94.3% | 92.9% | 92.9% |
| test_everyday (80) program | 95.0% | 96.2% | 96.2% |
| tldr_heldout (150) program / exact | 74.0% / 52.0% | 74.0% / 53.3% | 72.7% / 30.0% |
| nl2bash_test (150) program / exact | 77.3% / 11.3% | 81.3% / 12.7% | 81.3% / 6.0% |
| multistep (10) coverage | 95.0% | 95.0% | 87.5% |
| multistep_heldout (205) complete / coverage | 92.4% / 89.6% | 98.2% / 91.2% | 98.2% / 91.1% |
| multistep3_heldout (73) complete / coverage | 81.5% / 92.7% | 94.5% / 94.3% | 96.3% / 94.5% |
| regressions passed | 6 of 7 | 6 of 7 | 6 of 7 |
"Complete" is the share of answers that keep every literal of the request (size,
duration, extension, path, ...). multistep3_heldout was set aside from the
round-3 scenarios before training. The everyday difference is one question.
Read by hand, the 10 multistep requests come out about 5 right, 3 partly right and
2 wrong (round 2: 3, 4, 3). The old-.log-files delete now keeps -name '*.log'
on the first try. Still wrong: a grep on /var/log without -r and without the
24-hour limit, and the CSV request (wrong columns). Through the CLI, offline on the
CPU: peak memory 1.34 GB, 7-12 s per question.
#Slip checks and repairs, and the first execution tests
The slip rules and repairs described under "How it works" were added after reading round 3's failures (same model, GPU):
| set | before | after |
|---|---|---|
| everyday (70) program | 92.9% | 94.3% |
| regressions passed | 6 of 7 | 7 of 7 |
| every other set | unchanged on the main metrics |
What changed, case by case: grep -r for the /var/log search, the 3 am schedule
installed with crontab instead of printed as a crontab line, apt list --upgradable instead of pip list --outdated. Not all good: two pip list
answers became rpm --query --list --all, which lists every package's files.
The $HOME check adds a flag to four guessed paths and fixes none of them.
Execution tests (exec_eval.py, 33 file and text tasks, each command run as
nobody in a Docker container with no network; HOME is an empty directory, so a
guessed $HOME/data for "the data directory" fails), same pipeline and rules:
| model | commands that did the job |
|---|---|
| stock Qwen2.5-Coder-1.5B | 63.6% (21 of 33) |
| round 2 | 84.8% (28) |
| round 3 | 87.9% (29) |
Round 3 still fails: two $HOME/... guesses (flagged, but no retry did better),
wc -l < <(find docs) (counts the directory itself), and paste where cat was
needed. Before the head/tail, path/to/ and relative-grep repairs, round 3
passed 27 of the same 33 under the looser harness.
On a 2 GB machine: the CLI in a container capped at 2 GB of memory with no swap, CPU only, answered all 10 multistep requests and 5 single questions with no out-of-memory kill; the container's peak, page cache of the model included, was 1,707 MB. Capped at 1.5 GB it still ran (peak 1,287 MB). 7-24 s per answer.
#Round 2
Qwen2.5-Coder-1.5B with LoRA on 33,167 examples, trained on a Colab T4 (fp16, batch 16). Planned for two epochs; stopped at step 1,900 of 4,146 because the validation loss stopped improving once the second epoch began (0.290 at 1,900, 0.292-0.297 up to 2,400 while training loss kept falling). Q4_K_M, 986 MB. All three models go through the same round-2 pipeline (split, retrieve, answer, validate), with retrieval on. "Program" is the share of answers using the right program; "coverage" the share of a request's parts handled with an accepted program.
| set | stock | round 1 | round 2 |
|---|---|---|---|
| everyday (70, dev) program | 84.3% | 94.3% | 95.7% |
| test_everyday (80, unseen) program | 91.2% | 92.5% | 95.0% |
| tldr_heldout (150) program / exact | 68.7% / 26.0% | 68.7% | 74.0% / 52.7% |
| nl2bash_test (150) program / exact | 71.3% / 2.7% | 85.3% | 76.7% / 11.3% |
| multistep (the 10 hand-written requests) coverage | 80.8% | 62.5% | 95.0% |
| multistep_heldout (205) coverage / step count off by | 70.9% / 0.65 | ~55%* | 89.6% / 0.05 |
| regressions passed | 5 of 7 | 6 of 7 |
* round 1 was stopped after 49 of the 205; it cannot split, so most of its answers are one command. The no-retrieval runs have not been run; for the execution tests see round 3.
Round 1 is better on NL2Bash: it was trained on 14,578 mostly-NL2Bash pairs, round 2 on at most 4,000 of them (macOS forms removed) next to generated questions phrased the way people ask. Peak memory 1.2-1.5 GB; 4.3 s per question on the laptop CPU, 0.7 s on its GPU, with the same answers to within a couple of cases.
With the task checks, retries and the head -n 1 repair (run new-rag-checks,
same model, GPU):
| set | valid without → with | right program without → with | steps retried |
|---|---|---|---|
| everyday | 98.6% → 100% | 95.7% → 94.3% | 1.4% |
| test_everyday | 97.5% → 100% | 95.0% → 95.0% | 2.5% |
| tldr_heldout | 93.7% → 95.7% | 74.0% → 74.0% | 7.0% |
| nl2bash_test | 98.7% → 99.3% | 76.7% → 77.3% | 2.7% |
| multistep_heldout | 99.6% → 100% | coverage 89.6% → 89.6% | 0.6% |
Retries swap invented options for documented ones (a ps report for a nonexistent
sysstat --processes), which is what "valid" measures; whether the replacement does
the job is another matter (kill -0 PID replaced an invalid kill -TERM -p PID, and
signal 0 stops nothing). 7.6% of the held-out multi-step answers lose a literal of the
request in every command and get a note:. Most of those come from the split: the
training scenarios often name a path or count in words ("the compressed nginx log"),
the model learned to do the same, and its steps then guess paths.
Read by hand, the 10 multistep requests come out about 3 right, 4 partly right and
3 wrong. Coverage counts the right programs, not right commands, so it reads higher
than that. Mistakes seen: ps ... | head -n 1 (shows only the header), a dropped
-name '*.log' that widens a delete to every old file, a backup split into four
separate tar runs, and an invented systemd-env that the validator flagged.
#Round 1
Qwen2.5-Coder-1.5B, stock against fine-tuned (LoRA, one epoch, 14,578 examples), Q4_K_M, CPU, answering with a command plus a model-written sentence. "Command" is the share of answers using the right program; "exact" is the share matching the reference up to whitespace.
| everyday (70) command | tldr held-out (150) command / exact | NL2Bash held-out (100) command / exact | |
|---|---|---|---|
| stock, no retrieval | 95.7% | 44.7% / 1.3% | 57.0% / 9.0% |
| stock + retrieval | 84.3% | 73.3% / 37.3% | 67.0% / 7.0% |
| fine-tuned, no retrieval | 91.4% | 50.0% / 6.7% | 79.0% / 13.0% |
| fine-tuned + retrieval | 91.4% | 76.0% / 64.0% | 78.0% / 13.0% |
Peak memory for the whole pipeline was 1.4 GB (Q4_K_M).
What round 1 got wrong, and what was changed because of it:
| failure | cause | change |
|---|---|---|
macOS command for a Linux question (top -l 1 -s 10) |
NL2Bash contains macOS commands | validator; NL2Bash filtered by it |
| explanation repeated the question, or was wrong | 96% of NL2Bash training explanations were the question restated | model writes no prose; explanations come from the documentation |
| one command for a four-part request | every training answer was a single line | requests are split and answered step by step |
| 3 of 100 long prompts looped | no repeat penalty or loop check | both, with one retry |
| numbers hard to trust | dev set reported as a test; exact match as the main metric | frozen test sets; separate metrics; execution tests |