robot_lab-to 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.loki +21 -0
  5. data/.quality/reek_baseline.txt +17 -0
  6. data/.rubocop.yml +7 -0
  7. data/CHANGELOG.md +67 -0
  8. data/CLAUDE.md +74 -0
  9. data/COMMITS.md +27 -0
  10. data/README.md +352 -0
  11. data/Rakefile +132 -0
  12. data/bin/robot-to +14 -0
  13. data/docs/about/changelog.md +82 -0
  14. data/docs/assets/architecture.svg +110 -0
  15. data/docs/assets/images/robot_lab-to.jpg +0 -0
  16. data/docs/assets/iteration-loop.svg +97 -0
  17. data/docs/concepts/evals.md +254 -0
  18. data/docs/concepts/iteration-loop.md +128 -0
  19. data/docs/concepts/notes.md +91 -0
  20. data/docs/concepts/run-state.md +93 -0
  21. data/docs/concepts/stop-conditions.md +111 -0
  22. data/docs/concepts/verification.md +75 -0
  23. data/docs/configuration/cli.md +111 -0
  24. data/docs/configuration/index.md +93 -0
  25. data/docs/configuration/settings.md +270 -0
  26. data/docs/getting-started/anatomy-of-a-run.md +111 -0
  27. data/docs/getting-started/installation.md +78 -0
  28. data/docs/getting-started/quick-start.md +105 -0
  29. data/docs/index.md +96 -0
  30. data/docs/local-models/guardrails.md +117 -0
  31. data/docs/local-models/index.md +88 -0
  32. data/docs/local-models/ollama.md +122 -0
  33. data/docs/local-models/tools.md +92 -0
  34. data/docs/reference/architecture.md +139 -0
  35. data/examples/01_basic_usage/.gitignore +9 -0
  36. data/examples/01_basic_usage/README.md +173 -0
  37. data/examples/01_basic_usage/basic_usage.rb +386 -0
  38. data/examples/02_advanced_usage/.gitignore +8 -0
  39. data/examples/02_advanced_usage/README.md +117 -0
  40. data/examples/02_advanced_usage/advanced_usage.rb +308 -0
  41. data/examples/02_advanced_usage/quality_gate.rb +99 -0
  42. data/examples/03_scored/.gitignore +2 -0
  43. data/examples/03_scored/README.md +117 -0
  44. data/examples/03_scored/scored_run.rb +325 -0
  45. data/examples/04_prose/.gitignore +2 -0
  46. data/examples/04_prose/README.md +43 -0
  47. data/examples/04_prose/prompts_dir/grade_message.md +14 -0
  48. data/examples/04_prose/prompts_dir/judge.md +11 -0
  49. data/examples/04_prose/prompts_dir/outline_criteria.md +9 -0
  50. data/examples/04_prose/prompts_dir/outline_objective.md +11 -0
  51. data/examples/04_prose/prompts_dir/section_criteria.md +7 -0
  52. data/examples/04_prose/prompts_dir/sections_objective.md +14 -0
  53. data/examples/04_prose/prose_run.rb +302 -0
  54. data/lib/robot_lab/to/atomic_file.rb +69 -0
  55. data/lib/robot_lab/to/backoff.rb +46 -0
  56. data/lib/robot_lab/to/cli.rb +176 -0
  57. data/lib/robot_lab/to/commit_manager.rb +135 -0
  58. data/lib/robot_lab/to/config/defaults.yml +27 -0
  59. data/lib/robot_lab/to/config.rb +86 -0
  60. data/lib/robot_lab/to/decision.rb +40 -0
  61. data/lib/robot_lab/to/decision_manager.rb +193 -0
  62. data/lib/robot_lab/to/errors.rb +30 -0
  63. data/lib/robot_lab/to/evals/base.rb +23 -0
  64. data/lib/robot_lab/to/evals/code.rb +77 -0
  65. data/lib/robot_lab/to/evals/context.rb +17 -0
  66. data/lib/robot_lab/to/evals/factory.rb +84 -0
  67. data/lib/robot_lab/to/evals/null.rb +17 -0
  68. data/lib/robot_lab/to/evals/prose.rb +110 -0
  69. data/lib/robot_lab/to/evals/score.rb +22 -0
  70. data/lib/robot_lab/to/exit_summary.rb +75 -0
  71. data/lib/robot_lab/to/guards/checkpoint.rb +84 -0
  72. data/lib/robot_lab/to/guards/grader_lock.rb +62 -0
  73. data/lib/robot_lab/to/guards/path_resolution.rb +61 -0
  74. data/lib/robot_lab/to/guards/quality_monitor.rb +113 -0
  75. data/lib/robot_lab/to/guards/read_before_edit.rb +83 -0
  76. data/lib/robot_lab/to/guards/run_store.rb +31 -0
  77. data/lib/robot_lab/to/guards/write_guard.rb +71 -0
  78. data/lib/robot_lab/to/guards.rb +54 -0
  79. data/lib/robot_lab/to/iteration_result.rb +29 -0
  80. data/lib/robot_lab/to/jsonl_logger.rb +59 -0
  81. data/lib/robot_lab/to/notes_manager.rb +121 -0
  82. data/lib/robot_lab/to/orchestrator.rb +642 -0
  83. data/lib/robot_lab/to/prompt_builder.rb +208 -0
  84. data/lib/robot_lab/to/run.rb +109 -0
  85. data/lib/robot_lab/to/stop_conditions.rb +71 -0
  86. data/lib/robot_lab/to/tools/bash.rb +75 -0
  87. data/lib/robot_lab/to/tools/edit.rb +52 -0
  88. data/lib/robot_lab/to/tools/file_tool.rb +32 -0
  89. data/lib/robot_lab/to/tools/read.rb +57 -0
  90. data/lib/robot_lab/to/tools/request_decision.rb +66 -0
  91. data/lib/robot_lab/to/tools/submit_result.rb +50 -0
  92. data/lib/robot_lab/to/tools/write.rb +32 -0
  93. data/lib/robot_lab/to/verifier.rb +63 -0
  94. data/lib/robot_lab/to/version.rb +7 -0
  95. data/lib/robot_lab/to.rb +81 -0
  96. data/mkdocs.yml +145 -0
  97. metadata +175 -0
@@ -0,0 +1,78 @@
1
+ # Installation
2
+
3
+ ## Requirements
4
+
5
+ | Requirement | Notes |
6
+ |-------------|-------|
7
+ | Ruby **>= 3.2** | Set in the gemspec `required_ruby_version`. |
8
+ | Git | A repository with at least one commit. The loop creates a branch and commits there. |
9
+ | `robot_lab` | The core framework `robot_lab-to` builds on. |
10
+ | An LLM provider | A cloud API key, **or** a local [Ollama](https://ollama.com) server (see [Local Models](../local-models/index.md)). |
11
+
12
+ ## Install the gem
13
+
14
+ ```bash
15
+ gem install robot_lab-to
16
+ ```
17
+
18
+ Or add it to a `Gemfile`:
19
+
20
+ ```ruby
21
+ gem "robot_lab-to"
22
+ ```
23
+
24
+ ```bash
25
+ bundle install
26
+ ```
27
+
28
+ This installs the `robot-to` executable on your `PATH`.
29
+
30
+ ## Provider credentials
31
+
32
+ `robot_lab-to` passes your configured provider and model straight through to
33
+ RobotLab / RubyLLM. Provide credentials the usual way for your provider:
34
+
35
+ === "Anthropic"
36
+
37
+ ```bash
38
+ export ANTHROPIC_API_KEY="sk-ant-..."
39
+ robot-to "..." --provider anthropic --model claude-sonnet-4-6
40
+ ```
41
+
42
+ === "OpenAI"
43
+
44
+ ```bash
45
+ export OPENAI_API_KEY="sk-..."
46
+ robot-to "..." --provider openai --model gpt-5.5
47
+ ```
48
+
49
+ === "Local (Ollama)"
50
+
51
+ No API key required. See [Ollama Setup](../local-models/ollama.md) for the
52
+ full configuration — local models need `--provider openai` against Ollama's
53
+ OpenAI-compatible endpoint plus `--no-stream` and `--local-guards`.
54
+
55
+ The default provider/model is `openai` / `gpt-5.5` (see
56
+ [Settings Reference](../configuration/settings.md)).
57
+
58
+ ## Verify the installation
59
+
60
+ ```bash
61
+ robot-to --version
62
+ ```
63
+
64
+ ## Prepare your project
65
+
66
+ The loop branches from `HEAD` and commits each successful iteration, so the
67
+ working repository must have at least one commit:
68
+
69
+ ```bash
70
+ git init
71
+ git add -A
72
+ git commit -m "initial" # or: git commit --allow-empty -m "initial"
73
+ ```
74
+
75
+ Run state is written under `.robot_lab_to/` and automatically added to
76
+ `.git/info/exclude`, so it never pollutes your working tree or commits.
77
+
78
+ You're ready — continue to the [Quick Start](quick-start.md).
@@ -0,0 +1,105 @@
1
+ # Quick Start
2
+
3
+ This walkthrough takes you from a clean repository to a finished overnight run.
4
+
5
+ ## 1. Start in a git repository with a commit
6
+
7
+ ```bash
8
+ cd my-project
9
+ git status # must be a git repo
10
+ git log --oneline # must have at least one commit
11
+ ```
12
+
13
+ If there are no commits yet:
14
+
15
+ ```bash
16
+ git commit --allow-empty -m "initial"
17
+ ```
18
+
19
+ ## 2. Run a bounded objective
20
+
21
+ Give `robot-to` a clear, single-sentence objective and a stop condition. Keep
22
+ the first run small and bounded so you can watch it work:
23
+
24
+ ```bash
25
+ robot-to "Add a CHANGELOG.md and document the public API in the README" \
26
+ --max-iterations 5 \
27
+ --provider anthropic \
28
+ --model claude-sonnet-4-6
29
+ ```
30
+
31
+ What happens:
32
+
33
+ - A branch `robot-to/<slugified-objective>-<timestamp>` is created.
34
+ - For up to 5 iterations, a fresh robot makes one focused change and reports a
35
+ result.
36
+ - Each successful iteration becomes one commit on the branch.
37
+ - Failures are rolled back (`git reset --hard`) and recorded in the notes.
38
+
39
+ ## 3. Add an independent verification gate
40
+
41
+ The robot's self-reported success only counts if a command **you** choose
42
+ passes. This is the single most important flag for unattended runs:
43
+
44
+ ```bash
45
+ robot-to "Fix the failing tests in spec/parser" \
46
+ --verify-command "bundle exec rake test" \
47
+ --max-iterations 15 \
48
+ --max-consecutive-failures 4
49
+ ```
50
+
51
+ If `--verify-command` exits non-zero (or times out), the iteration is rolled
52
+ back even though the robot claimed success. See [Verification Gate](../concepts/verification.md).
53
+
54
+ ## 4. Let it stop itself
55
+
56
+ For a true overnight run, combine a hard ceiling with a natural-language stop
57
+ condition:
58
+
59
+ ```bash
60
+ robot-to "Migrate the codebase from Minitest to RSpec" \
61
+ --verify-command "bundle exec rake" \
62
+ --max-iterations 50 \
63
+ --max-tokens 2000000 \
64
+ --stop-when "all test files are RSpec and the suite passes"
65
+ ```
66
+
67
+ The robot evaluates `--stop-when` after each successful iteration and sets
68
+ `should_fully_stop` when it judges the condition met. See
69
+ [Stop Conditions](../concepts/stop-conditions.md).
70
+
71
+ ## 5. Review in the morning
72
+
73
+ ```bash
74
+ git log --oneline robot-to/migrate-the-codebase-from-... # the iteration history
75
+ git diff main..HEAD # the full change
76
+ cat .robot_lab_to/runs/*/notes.md # the reasoning log
77
+ ```
78
+
79
+ Each commit is one iteration. Squash, cherry-pick, or reset as you like — it's
80
+ an ordinary branch.
81
+
82
+ ## Programmatic use
83
+
84
+ You can also drive a run from Ruby instead of the CLI:
85
+
86
+ ```ruby
87
+ require "robot_lab"
88
+ require "robot_lab/to"
89
+
90
+ RobotLab::To.run(
91
+ "Add a CHANGELOG.md and document the public API",
92
+ provider: :anthropic,
93
+ model: "claude-sonnet-4-6",
94
+ max_iterations: 5,
95
+ verify_command: "bundle exec rake test"
96
+ )
97
+ ```
98
+
99
+ `RobotLab::To.run(objective, **opts)` accepts the same options as the CLI flags
100
+ (as keyword arguments). See the [Settings Reference](../configuration/settings.md).
101
+
102
+ ---
103
+
104
+ Next: understand exactly what one run produces in
105
+ [Anatomy of a Run](anatomy-of-a-run.md).
data/docs/index.md ADDED
@@ -0,0 +1,96 @@
1
+ <p align="center">
2
+ <img src="assets/images/robot_lab-to.jpg" alt="robot_lab-to" width="320"><br />
3
+ "Robot, takeover!"
4
+ </p>
5
+
6
+ # robot_lab-to
7
+
8
+ **Autonomous overnight agent loop for [RobotLab](https://github.com/MadBomber/robot_lab) — run a robot while you sleep.**
9
+
10
+ `robot_lab-to` ("takeover") runs a RobotLab `Robot` in an autonomous loop toward a
11
+ stated objective, committing **one focused change per iteration**. Each iteration
12
+ spins up a fresh robot, lets it work, and asks it to report a result. Good
13
+ iterations are committed; failures are rolled back. A running log (`notes.md`)
14
+ carries memory across iterations so the robot learns from what came before.
15
+
16
+ Start it before bed; review the branch in the morning.
17
+
18
+ ```bash
19
+ robot-to "Increase test coverage of the parser to 90%" \
20
+ --max-iterations 20 \
21
+ --verify-command "bundle exec rake test"
22
+ ```
23
+
24
+ ---
25
+
26
+ ## How it works
27
+
28
+ Every iteration is an independent, sandboxed attempt at *one* improvement. The
29
+ orchestrator owns the git history and the stop conditions; the robot owns the
30
+ work.
31
+
32
+ ![The robot-to iteration loop](assets/iteration-loop.svg)
33
+
34
+ 1. **Build a fresh robot** with a per-iteration system prompt that includes the
35
+ objective and the accumulated notes.
36
+ 2. **The robot works** and calls `submit_iteration_result` to report success or
37
+ failure.
38
+ 3. **The orchestrator decides** — an independent `--verify-command` (if set) must
39
+ pass, then the change is committed. Otherwise the working tree is reset.
40
+ 4. **Notes are updated** with what happened, becoming context for the next round.
41
+ 5. **Stop conditions are checked** — iterations, tokens, consecutive failures, or
42
+ a natural-language `--stop-when` — and the loop continues or ends.
43
+
44
+ See [The Iteration Loop](concepts/iteration-loop.md) for the full lifecycle.
45
+
46
+ ---
47
+
48
+ ## Why use it
49
+
50
+ - **Bounded, reviewable history.** Each iteration is a single commit on a
51
+ dedicated branch. Bad attempts never reach the branch — they are reset before
52
+ the next try.
53
+ - **Cross-iteration memory.** `notes.md` records summaries, changes, and learnings
54
+ so the robot doesn't repeat itself. See [Cross-Iteration Memory](concepts/notes.md).
55
+ - **Independent verification.** The robot can't self-certify success — a real
56
+ command you choose is the deciding authority. See [Verification Gate](concepts/verification.md).
57
+ - **Scored, descending iterations.** An **eval** replaces the robot's self-report
58
+ with an orchestrator-owned measurable judgement — a deterministic `--measure`/
59
+ `--target` for code, or a pairwise LLM judge for prose — so the branch only
60
+ ever moves forward. See [Evals](concepts/evals.md).
61
+ - **It stops on its own.** Hard limits plus a natural-language stop condition mean
62
+ you can leave it unattended. See [Stop Conditions](concepts/stop-conditions.md).
63
+ - **It asks before overstepping.** For irreversible or ambiguous choices the robot
64
+ writes a **decision file** — situation, options, and its recommendation — that
65
+ you resolve out-of-band instead of letting it guess. Run `robot-to decisions`
66
+ to see what's waiting on you.
67
+ - **Resumable & cron-friendly.** Run state is persisted to `run.json`, so
68
+ `robot-to --resume <run_id>` continues a stopped run and an external scheduler
69
+ can drive it one commit per tick.
70
+ - **Runs on local models.** With `--local-guards` it ships built-in file tools and
71
+ small-model guardrails, so it can drive a local Ollama model offline. See
72
+ [Local Models](local-models/index.md).
73
+
74
+ ---
75
+
76
+ ## Quick links
77
+
78
+ <div class="grid cards" markdown>
79
+
80
+ - :material-rocket-launch: **[Installation](getting-started/installation.md)** — install the gem and the `robot-to` CLI.
81
+ - :material-play: **[Quick Start](getting-started/quick-start.md)** — your first overnight run in five minutes.
82
+ - :material-cog: **[Configuration](configuration/index.md)** — every setting, the config cascade, and the CLI.
83
+ - :material-laptop: **[Local Models](local-models/index.md)** — drive a local Ollama model with guardrails.
84
+ - :material-sitemap: **[Architecture](reference/architecture.md)** — how the pieces fit together.
85
+
86
+ </div>
87
+
88
+ ---
89
+
90
+ ## Requirements
91
+
92
+ - Ruby **>= 3.2**
93
+ - A git repository with at least one commit (the loop branches and commits there)
94
+ - The [`robot_lab`](https://github.com/MadBomber/robot_lab) gem and an LLM provider
95
+ (a cloud key such as `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`, **or** a local
96
+ [Ollama](https://ollama.com) server)
@@ -0,0 +1,117 @@
1
+ # Guardrails
2
+
3
+ The guardrails are `RobotLab::Hook` policies that intercept the robot's tool calls
4
+ to catch the mistakes small local models reliably make. They are enabled with
5
+ `--local-guards` and are no-ops you'd never notice with a frontier model — they
6
+ exist for the 8–35B local case.
7
+
8
+ They are adapted from the open-source [`little-coder`](https://github.com/itayinbarr/little-coder)
9
+ harness, whose core finding is that a guard-rich scaffold raised the same small
10
+ model's coding-benchmark score by **2.4×**. The harness, not the model, does the
11
+ heavy lifting.
12
+
13
+ ## The four guards
14
+
15
+ ### write-guard
16
+
17
+ **Problem:** small models love to "fix" a file by rewriting the whole thing with
18
+ `write`, destroying everything they didn't think to include.
19
+
20
+ **Policy:** refuse `write` to a file that already exists and tell the model to use
21
+ `edit` instead. It also normalizes a root-bare path like `/foo.rb` (a common
22
+ small-model mistake) to the working directory.
23
+
24
+ Implemented as `around_tool_call` (to block) + `before_tool_call` (to normalize
25
+ the path).
26
+
27
+ **Opting out:** `write-guard` is the one guard in the set that can get in the way
28
+ of *legitimate* work — a robot iteratively rewriting a whole prose draft each
29
+ pass wants plain `write`, not a forced `edit`. Set `write_guard: false` (Ruby-only,
30
+ default `true`; no CLI flag) to drop it from the installed set while keeping
31
+ `read-before-edit`, `checkpoint`, and `quality-monitor`:
32
+
33
+ ```ruby
34
+ RobotLab::To.run(objective, local_guards: true, write_guard: false)
35
+ ```
36
+
37
+ Internally this calls `Guards.install(robot, run:, except: [Guards::WriteGuard])`.
38
+
39
+ ### read-before-edit
40
+
41
+ **Problem:** models fire `edit` with an `old_text` they never actually saw —
42
+ guessing the file's contents — which either fails the exact-match (a wasted turn)
43
+ or, worse, matches the wrong span.
44
+
45
+ **Policy:** refuse `edit` on any file that wasn't `read` this run, directing the
46
+ model to read it first. A successful `read`, `edit`, or `write` marks the file as
47
+ known. The read-set is per-run state.
48
+
49
+ ### checkpoint
50
+
51
+ **Problem:** a single botched edit can spoil otherwise-good work within an
52
+ iteration.
53
+
54
+ **Policy:** before the first `write`/`edit` of a file, snapshot its current bytes
55
+ (first-write-wins) to `runs/<run_id>/checkpoints/`. Files that didn't exist get an
56
+ `.absent` sentinel. This is finer-grained than the orchestrator's
57
+ `git reset --hard` rollback — it survives *within* an iteration. Best-effort:
58
+ checkpointing never raises into the tool path.
59
+
60
+ ### quality-monitor
61
+
62
+ **Problem:** a small model can get stuck repeating the exact same tool call, which
63
+ on an unattended overnight run silently burns the entire token budget.
64
+
65
+ **Policy:** detect the same `name` + arguments called on consecutive turns. After
66
+ a couple of consecutive repeats it raises a `QualityError`, which the orchestrator
67
+ handles like any iteration error — roll back, record the reason in `notes.md`, and
68
+ move on. This is the load-bearing guard for long unattended runs.
69
+
70
+ !!! info "Empty-response handling is intentionally left out"
71
+ An earlier version also flagged "empty" responses, but RobotLab fires the
72
+ generation hook once per *run* (not per turn), so that check false-tripped on
73
+ legitimate tool-using turns. Empty / no-progress is handled instead by the
74
+ orchestrator's existing nudge → "did not submit" path, which recovers
75
+ gracefully rather than hard-erroring.
76
+
77
+ !!! info "A fifth guard, `GraderLock`, is not part of this set"
78
+ `Guards::GraderLock` (`guards/grader_lock.rb`) lives alongside these four but
79
+ is **not** gated by `--local-guards` — it locks an [eval's](../concepts/evals.md)
80
+ grading criteria (a spec, a floor script, `--protect-path` globs) from robot
81
+ edits, and is installed whenever such paths exist, regardless of model size.
82
+ A frontier model can reward-hack its own grading criteria just as easily as a
83
+ small model can mangle a file. See
84
+ [Evals: Guarding the grader](../concepts/evals.md#guarding-the-grader-graderlock).
85
+
86
+ ## How they're registered
87
+
88
+ `Guards.install(robot, run:)` registers all four on the per-iteration robot. The
89
+ orchestrator calls it from `build_robot` only when `local_guards` is enabled:
90
+
91
+ ```ruby
92
+ robot = RobotLab.build(..., local_tools: iteration_tools(submit_tool))
93
+ RobotLab::To::Guards.install(robot, run: @run) if @config.local_guards?
94
+ ```
95
+
96
+ ## Per-run state
97
+
98
+ RobotLab builds a fresh hook context per tool call with no shared metadata across
99
+ calls, so cross-call guard state (the read-set, the checkpoint-set, the loop
100
+ tracker) can't live in the hook context. Because `robot_lab-to` runs each
101
+ iteration in its own thread, the guards keep that state in a thread-local
102
+ `RunStore` — the same idiom `robot_lab-audit` uses for run IDs. It's reset at the
103
+ start of each run.
104
+
105
+ ## Relationship to the git rollback
106
+
107
+ The guards and the orchestrator's git rollback are complementary, at two
108
+ granularities:
109
+
110
+ | Layer | Granularity | Recovers |
111
+ |-------|-------------|----------|
112
+ | `checkpoint` guard | a single file, within an iteration | a botched individual edit |
113
+ | `git reset --hard` | the whole working tree, per iteration | a failed iteration |
114
+
115
+ ---
116
+
117
+ Next: [Architecture](../reference/architecture.md).
@@ -0,0 +1,88 @@
1
+ # Local Models
2
+
3
+ `robot_lab-to` can drive a **local** model — running entirely offline against an
4
+ [Ollama](https://ollama.com) server — instead of a cloud API. This is the
5
+ "local assistant" mode: no API keys, no per-token cost, no data leaving your
6
+ machine.
7
+
8
+ Local models are smaller and less forgiving than frontier models, so this mode
9
+ adds two things:
10
+
11
+ - **Built-in file tools** (`read`, `write`, `edit`, `bash`) — a small, predictable
12
+ tool surface the model can use to do real work.
13
+ - **Guardrails** — `RobotLab::Hook` policies that catch the mistakes small models
14
+ reliably make, before they corrupt the working tree or burn the token budget.
15
+
16
+ Both are enabled together with `--local-guards`.
17
+
18
+ ## The short version
19
+
20
+ ```bash
21
+ # 1. Serve a tool-capable model
22
+ ollama pull gpt-oss:20b
23
+
24
+ # 2. Run robot-to against it
25
+ robot-to "Add a greet(name) method in greeter.rb" \
26
+ --provider openai \
27
+ --model gpt-oss:20b \
28
+ --local-guards \
29
+ --no-stream \
30
+ --max-iterations 5
31
+ ```
32
+
33
+ The full setup — including the RubyLLM configuration that points `:openai` at
34
+ Ollama — is on the [Ollama Setup](ollama.md) page.
35
+
36
+ ## Why these flags
37
+
38
+ Driving a local model end-to-end requires three non-obvious settings. Each exists
39
+ because of a concrete limitation discovered in testing:
40
+
41
+ | Flag / setting | Why |
42
+ |----------------|-----|
43
+ | `--provider openai` (+ Ollama base URL) | RubyLLM's native `:ollama` provider doesn't reliably get these models to emit tool calls. Routing through the `:openai` provider against Ollama's OpenAI-compatible `/v1` endpoint does. |
44
+ | `--no-stream` | Ollama suppresses tool calls when the response is streamed. With streaming off, tool calls come through. |
45
+ | `--local-guards` | Attaches the file tools the model needs to do work, plus guardrails that make those tools safe for a small model. |
46
+
47
+ See [Ollama Setup → Streaming and tool calls](ollama.md#streaming-and-tool-calls)
48
+ for the details.
49
+
50
+ ## The design philosophy
51
+
52
+ This mode is inspired by the research behind
53
+ [`little-coder`](https://github.com/itayinbarr/little-coder): the same small model
54
+ scored **2.4× higher** on a coding benchmark through a guard-rich harness than it
55
+ did unscaffolded. With small local models, **the harness is the product** — a
56
+ frontier model forgives a sloppy tool loop; a 9–35B local model does not.
57
+
58
+ The guardrails are policies that intercept the model's tool calls:
59
+
60
+ - **`write-guard`** — refuses `write` on a file that already exists (small models
61
+ rewrite whole files and destroy content); redirects to `edit`.
62
+ - **`read-before-edit`** — refuses `edit` on a file the model hasn't read this run,
63
+ so `oldText` reflects the real contents.
64
+ - **`checkpoint`** — snapshots a file before the first Write/Edit, for fine-grained
65
+ recovery within an iteration.
66
+ - **`quality-monitor`** — detects a model spinning on the same tool call and stops
67
+ it before it burns the token budget.
68
+
69
+ See [Guardrails](guardrails.md) for how each works.
70
+
71
+ ## Choosing a model
72
+
73
+ The model **must support tool calling**. In testing on an M2 Max:
74
+
75
+ | Model | Size | Tool calls? | Notes |
76
+ |-------|------|-------------|-------|
77
+ | `gpt-oss:20b` | 20B | ✅ reliable | Called tools *and* `submit_iteration_result`; completed full runs cleanly. **Recommended.** |
78
+ | `qwen3` | 8B | ✅ tools work | Created files via the write tool, but didn't reliably call `submit_iteration_result` (the final-report step). |
79
+ | `phi4-mini` | 3.8B | ❌ | Explains instead of calling tools. |
80
+
81
+ Prefer a larger, instruction-following model for the autonomous loop — it has to
82
+ both use tools *and* remember to submit its result every iteration.
83
+
84
+ ---
85
+
86
+ - [Ollama Setup](ollama.md) — install, serve, and configure.
87
+ - [Built-in Tools](tools.md) — what `read`/`write`/`edit`/`bash` do.
88
+ - [Guardrails](guardrails.md) — the small-model safety policies.
@@ -0,0 +1,122 @@
1
+ # Ollama Setup
2
+
3
+ This page covers running `robot_lab-to` against a local
4
+ [Ollama](https://ollama.com) server end-to-end.
5
+
6
+ ## 1. Install and start Ollama
7
+
8
+ ```bash
9
+ # macOS / Linux
10
+ curl -fsSL https://ollama.com/install.sh | sh
11
+
12
+ # Ollama runs a server on http://localhost:11434
13
+ ```
14
+
15
+ ## 2. Pull a tool-capable model
16
+
17
+ The model **must** support tool calling. `gpt-oss:20b` is the recommended choice
18
+ (see [model selection](index.md#choosing-a-model)):
19
+
20
+ ```bash
21
+ ollama pull gpt-oss:20b
22
+ ```
23
+
24
+ You can confirm a model advertises tool support:
25
+
26
+ ```bash
27
+ curl -s http://localhost:11434/api/tags | jq '.models[] | {name, caps: .capabilities}'
28
+ ```
29
+
30
+ ## 3. Point RubyLLM at Ollama
31
+
32
+ `robot_lab-to` reaches the model through RobotLab / RubyLLM. The robust path is
33
+ the **`:openai` provider** aimed at Ollama's OpenAI-compatible `/v1` endpoint.
34
+ Configure RubyLLM once, before launching the run:
35
+
36
+ ```ruby
37
+ require "ruby_llm"
38
+ require "robot_lab"
39
+ require "robot_lab/to"
40
+
41
+ RubyLLM.configure do |c|
42
+ c.openai_api_base = "http://localhost:11434/v1"
43
+ c.openai_api_key = "ollama" # ignored by Ollama, but RubyLLM requires a value
44
+ c.request_timeout = 600
45
+ end
46
+
47
+ RobotLab::To.run(
48
+ "Add a greet(name) method in greeter.rb",
49
+ provider: :openai,
50
+ model: "gpt-oss:20b",
51
+ local_guards: true,
52
+ stream: false,
53
+ max_iterations: 5
54
+ )
55
+ ```
56
+
57
+ !!! note "Why a wrapper script"
58
+ The Ollama base-URL configuration lives in RubyLLM's global config, so it's
59
+ set in a small Ruby launcher (as above) rather than via a `robot-to` CLI flag.
60
+ The CLI flags `--provider openai --model gpt-oss:20b --local-guards --no-stream`
61
+ cover the rest; only the base URL needs the wrapper. You can also set it in
62
+ your application's RubyLLM initializer.
63
+
64
+ ## 4. Run
65
+
66
+ From the launcher above, or — once the base URL is configured in your environment
67
+ — from the CLI:
68
+
69
+ ```bash
70
+ robot-to "Add a greet(name) method in greeter.rb" \
71
+ --provider openai \
72
+ --model gpt-oss:20b \
73
+ --local-guards \
74
+ --no-stream \
75
+ --max-iterations 5
76
+ ```
77
+
78
+ ## Streaming and tool calls
79
+
80
+ **Local Ollama models must run non-streaming** (`--no-stream` / `stream: false`).
81
+
82
+ Ollama's OpenAI-compatible endpoint suppresses tool calls when the response is
83
+ streamed — the model "thinks" but never emits a tool call, so the robot can't do
84
+ any work. With streaming disabled, tool calls come through normally.
85
+
86
+ The trade-off: streaming is what enables per-chunk token accounting and
87
+ mid-iteration token-budget enforcement. With `--no-stream`, tokens are accounted
88
+ from each iteration's *result* instead, and `--max-tokens` is enforced at
89
+ iteration boundaries rather than mid-stream. For overnight local runs this is
90
+ almost always fine.
91
+
92
+ ## Why not the native `:ollama` provider?
93
+
94
+ RubyLLM ships a native `:ollama` provider, but in testing it did **not** reliably
95
+ get tool-capable models (e.g. qwen3) to emit tool calls — the same model tool-calls
96
+ correctly through the `:openai` provider against Ollama's `/v1` endpoint. Until
97
+ that changes, prefer `provider: :openai` + `openai_api_base` for `robot_lab-to`.
98
+
99
+ ## Registering models (if needed)
100
+
101
+ RubyLLM validates models against its registry. `robot_lab-to` sets
102
+ `assume_model_exists` when a provider is given, which is enough for a bare chat —
103
+ but attaching tools triggers a capability lookup that can raise
104
+ `ModelNotFoundError` for an unregistered model. If you hit that, refresh the
105
+ registry so your local models are known:
106
+
107
+ ```ruby
108
+ RubyLLM.models.refresh! # discovers locally-served Ollama models
109
+ ```
110
+
111
+ ## Troubleshooting
112
+
113
+ | Symptom | Cause | Fix |
114
+ |---------|-------|-----|
115
+ | Model thinks but never calls a tool | Streaming is on | Add `--no-stream`. |
116
+ | `RobotLab::ModelNotFoundError` when tools attach | Model not in RubyLLM registry | `RubyLLM.models.refresh!`. |
117
+ | Every iteration "did not submit" | Model too small to follow the final-report step | Use a larger model (e.g. `gpt-oss:20b`). |
118
+ | No tool calls at all, model only explains | Model lacks tool support | Pick a model whose `capabilities` include `tools`. |
119
+
120
+ ---
121
+
122
+ Next: [Built-in Tools](tools.md).