robot_lab-to 0.2.7 → 0.2.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a600189accdf098fb2cea44bec912fbc3399b89d48b7775d1237a78fd5bac7fd
4
- data.tar.gz: 8729282eb38252b3e2227b02cc9b48dc76b04005572ede0d24fccdbab1b47940
3
+ metadata.gz: 43c5ef1239f3c91dcf0b033466a2a346f3dafb385897d1e5696e2952a752a49d
4
+ data.tar.gz: 29fb0fda8e3fbd13f079a3244c577c69e53fcb4e1c8dbdc5066b32c059bd7f67
5
5
  SHA512:
6
- metadata.gz: 58a538bc23b7a68822dae324d6b179f0eba4ea4f8b79bfb489ea6cfcaedd59e82c592197ca67c11873404d52c78bae8a74f26b53b05e061c5753d43ad15c3f68
7
- data.tar.gz: bc1e417731a76c43f5671ff3fcfaec038a59884d9ac62a63c1cb2bbb53c8fade6f051560da30db2d1c517645cc2f448100b1e551fd458ea040bf738447edd145
6
+ metadata.gz: 7af7eca43dac57ae02b21b24b9ad8b7a813e473b5470372ee5048980948d0bb431e6b164d88f532394da9cd946d7ed75cafe2746efac41f79dc2cc1c0a1aaad6
7
+ data.tar.gz: beb2270499d125d74f2a3333610c44b9cd030f8ee9142bf4b2b3e87b1f0e410b9f768ffdb83c7187e80e0d1d593ce77ebbf5f89cdee9b5d640432d788b429733
data/.envrc CHANGED
@@ -1 +1,7 @@
1
+ # robot_lab_project/robot_lab-to/.envrc
2
+
3
+ source_up
4
+
1
5
  export RR=$(pwd)
6
+
7
+ export BUNDLE_GEMFILE=Gemfile
data/Archspec.rb ADDED
@@ -0,0 +1,34 @@
1
+ # robot_lab-to is a plain Ruby gem (RobotLab::To), not a Rails app -- there is
2
+ # no app/ tree, no controllers/models/views, so the :rails preset doesn't
3
+ # apply. These are the actual boundaries documented in CLAUDE.md.
4
+
5
+ # Matched by exact constant, not a file glob: every file here reopens the
6
+ # bare `module RobotLab` namespace (shared with the external robot_lab gem's
7
+ # own top-level module), so an `in:` glob would misattribute any bare
8
+ # `RobotLab.xxx` call anywhere in the gem to this component.
9
+ component :orchestrator, constants: "RobotLab::To::Orchestrator"
10
+ component :tools, in: "lib/robot_lab/to/tools/**/*.rb"
11
+ component :guards, in: %w[lib/robot_lab/to/guards.rb lib/robot_lab/to/guards/**/*.rb]
12
+ component :evals, in: "lib/robot_lab/to/evals/**/*.rb"
13
+ component :commit_manager, in: "lib/robot_lab/to/commit_manager.rb"
14
+
15
+ # Tools are RobotLab::Tool subclasses the robot calls mid-turn (see
16
+ # lib/robot_lab/to/tools/file_tool.rb); Guards are RobotLab::Hook subclasses
17
+ # wired onto a robot by Orchestrator#build_robot; Evals are "orchestrator-owned
18
+ # scorers" per CLAUDE.md. All three are built and consumed by the orchestrator
19
+ # -- the dependency runs one way, never back into the loop that drives them.
20
+ tools.cannot_use :orchestrator,
21
+ because: "tools run inside an LLM turn, invoked by the robot -- " \
22
+ "they must not reach back into the loop that drives them"
23
+ guards.cannot_use :orchestrator,
24
+ because: "guards are wired onto a robot by Orchestrator#build_robot -- " \
25
+ "the dependency runs one way"
26
+ evals.cannot_use :orchestrator,
27
+ because: "evals are orchestrator-owned scorers (CLAUDE.md) -- " \
28
+ "Orchestrator calls Evals.build, not the reverse"
29
+
30
+ # CLAUDE.md: "CommitManager -- all git ops via Open3.capture3 (no shell
31
+ # interpolation)". Enforce the "no shell interpolation" half architecturally.
32
+ commit_manager.cannot_call :system, receiver: :none,
33
+ because: "git ops must go through Open3.capture3 with an argv array, " \
34
+ "never system()/backticks with an interpolated string"
data/CHANGELOG.md CHANGED
@@ -8,6 +8,10 @@ and [Conventional Commits](https://www.conventionalcommits.org/) (see `COMMITS.m
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.8] - 2026-09-09
12
+
13
+ Released in lockstep with `robot_lab` core v0.2.8: this gem now resolves the released core gem from RubyGems instead of the local sibling checkout (local-path development remains available via `BUNDLE_GEMFILE=Gemfile.local`). Also in this release: reek pass completed, archspec added to the development bundle, and shared LM Studio example configuration extracted for the examples.
14
+
11
15
  ### Added
12
16
 
13
17
  - **Human-in-the-loop decision files.** When the robot hits a choice it must not
data/CLAUDE.md CHANGED
@@ -11,7 +11,8 @@ autonomous loop toward a stated objective, committing one focused change per ite
11
11
  bundle install
12
12
  bundle exec rake test # all tests
13
13
  bundle exec rake test_verbose # verbose
14
- bundle exec rake quality # tests + rubocop + flog
14
+ asgard quality # all *_check gates in parallel (tests + coverage,
15
+ # rubocop, flog, flay, reek, fasterer, typos, ...)
15
16
  bin/console # IRB shell
16
17
  ```
17
18
 
@@ -71,4 +72,4 @@ All run state lives in `.robot_lab_to/runs/<run_id>/` (added to `.git/info/exclu
71
72
  ## Testing
72
73
 
73
74
  Minitest. Git-dependent tests use `Dir.mktmpdir` with real `git init`.
74
- Coverage gate: 95% line / 75% branch (`rake quality` also runs rubocop + flog + flay).
75
+ Coverage gate: 95% line / 75% branch (`asgard quality` also runs rubocop + flog + flay + reek + more).
data/Rakefile CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
3
+ # Quality gates (quality, rubocop_check, flog_check, flay_check, ...),
4
+ # documentation tasks (doc_builder, doc_server), and the gem lifecycle
5
+ # (build, install, release) live in asgard — see .loki and the shared
6
+ # dev/*.loki files it imports. This Rakefile keeps only the task asgard
7
+ # itself delegates to: the test suite.
8
+
4
9
  require 'rake/testtask'
5
10
 
6
11
  Rake::TestTask.new(:test) do |t|
@@ -23,110 +28,3 @@ desc 'Run a single test file'
23
28
  task :test_file, [:file] do |_t, args|
24
29
  ruby "test/#{args[:file]}"
25
30
  end
26
-
27
- desc 'Check code complexity with Flog (warn >=20, fail >=50)'
28
- task :flog_check do
29
- require 'flog'
30
-
31
- method_warn = 20.0
32
- method_fail = 50.0
33
-
34
- flogger = Flog.new(all: true)
35
- flogger.flog(*Dir.glob('lib/**/*.rb'))
36
-
37
- warnings = []
38
- failures = []
39
-
40
- flogger.each_by_score do |method, score|
41
- next if method.end_with?('#none')
42
-
43
- if score > method_fail
44
- failures << "#{format('%.1f', score)}: #{method}"
45
- elsif score > method_warn
46
- warnings << "#{format('%.1f', score)}: #{method}"
47
- end
48
- end
49
-
50
- unless warnings.empty?
51
- puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
52
- warnings.each { |v| puts " #{v}" }
53
- end
54
-
55
- if failures.empty?
56
- puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
57
- else
58
- puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
59
- failures.each { |v| puts " #{v}" }
60
- abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
61
- end
62
- end
63
-
64
- desc 'Check for structural code duplication with Flay (mass >= 50)'
65
- task :flay_check do
66
- require 'flay'
67
-
68
- mass_threshold = 50
69
-
70
- flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
71
- flay.process(*Dir.glob('lib/**/*.rb'))
72
- flay.analyze
73
-
74
- if flay.hashes.empty?
75
- puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
76
- else
77
- puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
78
- flay.report
79
- abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
80
- end
81
- end
82
-
83
- desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
84
- task :quality do
85
- gates = [
86
- ['Tests + Coverage', 'bundle exec rake test'],
87
- ['RuboCop', 'bundle exec rubocop'],
88
- ['Flog Complexity', 'bundle exec rake flog_check'],
89
- ['Flay Duplication', 'bundle exec rake flay_check']
90
- ]
91
-
92
- results = gates.map do |label, command|
93
- puts "\n#{'=' * 60}"
94
- puts "Quality Gate: #{label}"
95
- puts '=' * 60
96
- [label, system(command) ? :pass : :fail]
97
- end
98
-
99
- green = ->(s) { "\e[32m#{s}\e[0m" }
100
- red = ->(s) { "\e[31m#{s}\e[0m" }
101
- width = results.map { |label, _| label.length }.max
102
-
103
- puts "\n#{'=' * 60}"
104
- puts 'Quality Gate Summary'
105
- puts '=' * 60
106
- results.each do |label, status|
107
- badge = status == :pass ? green.call('PASS') : red.call('FAIL')
108
- puts " [#{badge}] #{label.ljust(width)}"
109
- end
110
- puts '-' * 60
111
-
112
- passed = results.count { |_, s| s == :pass }
113
- failed = results.count { |_, s| s == :fail }
114
- tally = "#{passed} passed, #{failed} failed"
115
- puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
116
- puts '=' * 60
117
-
118
- abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
119
- puts "\n#{green.call('All quality gates passed.')}"
120
- end
121
-
122
- namespace :docs do
123
- desc 'Build MkDocs documentation'
124
- task :build do
125
- sh 'mkdocs build'
126
- end
127
-
128
- desc 'Serve MkDocs documentation locally on http://localhost:8000'
129
- task :serve do
130
- sh 'mkdocs serve'
131
- end
132
- end
data/examples/.envrc ADDED
@@ -0,0 +1,8 @@
1
+ # robot_lab-to/examples/.envrc
2
+
3
+ source_up
4
+
5
+ export LMS_BASE_URL=http://localhost:1234/v1
6
+
7
+ export RLTO_PROVIDER=lms
8
+ export RLTO_MODEL=qwen/qwen3.8-27b
@@ -30,14 +30,13 @@ the `1..3999` range, round-trip correctness, case-insensitive parsing, and
30
30
 
31
31
  ## Prerequisites
32
32
 
33
- Either a **local Ollama** server (default, no API key) or a **cloud** provider key.
33
+ Either **local LM Studio** (default, no API key) or a **cloud** provider key.
34
34
 
35
- === "Local Ollama (default)"
35
+ === "Local LM Studio (default)"
36
36
 
37
- ```bash
38
- ollama serve & # if not already running
39
- ollama pull qwen3.6:latest # a tool-capable model
40
- ```
37
+ Nothing to start by hand -- `common.rb` (required by the script) starts the
38
+ LM Studio server and loads the model for you if they aren't already
39
+ running/loaded.
41
40
 
42
41
  === "Cloud provider"
43
42
 
@@ -50,7 +49,7 @@ Either a **local Ollama** server (default, no API key) or a **cloud** provider k
50
49
  From the gem root:
51
50
 
52
51
  ```bash
53
- # Local Ollama (default)
52
+ # Local LM Studio (default)
54
53
  bundle exec ruby examples/01_basic_usage/basic_usage.rb
55
54
 
56
55
  # A cloud model instead
@@ -64,10 +63,10 @@ All optional, set via environment variables:
64
63
 
65
64
  | Variable | Default | Description |
66
65
  |----------|---------|-------------|
67
- | `RLTO_LOCAL` | `true` | Use a local Ollama model. Set `false` for a cloud provider. |
68
- | `RLTO_PROVIDER` | `openai` (local) | LLM provider passed to `RobotLab::To.run`. |
69
- | `RLTO_MODEL` | `qwen3.6:latest` (local) | Model id. Any tool-capable model works. |
70
- | `OLLAMA_BASE` | `http://localhost:11434/v1` | Ollama's OpenAI-compatible endpoint. |
66
+ | `RLTO_LOCAL` | `true` | Use a local LM Studio model. Set `false` for a cloud provider. |
67
+ | `RLTO_PROVIDER` | `lms` (local) | Provider label. `lms` resolves to RubyLLM's `:openai` adapter pointed at `LMS_BASE_URL` -- ruby_llm has no native `lms` adapter. |
68
+ | `RLTO_MODEL` | `qwen/qwen3.8-27b` (local) | Model id. Any tool-capable model works. |
69
+ | `LMS_BASE_URL` | `http://localhost:1234/v1` | LM Studio's OpenAI-compatible endpoint. |
71
70
 
72
71
  ## What you'll see
73
72
 
@@ -77,7 +76,7 @@ live, so the run never sits silent while the model works:
77
76
  ```
78
77
  Cleaning leftover: .../examples/01_basic_usage/project
79
78
  Project dir: examples/01_basic_usage/project
80
- Provider/model: openai/qwen3.6:latest (local Ollama)
79
+ Provider/model: lms/qwen/qwen3.8-27b (local LM Studio)
81
80
  Objective: implement lib/roman_numeral.rb to pass the test suite
82
81
 
83
82
  🤔 thinking…
@@ -157,10 +156,10 @@ The whole example is one call:
157
156
  ```ruby
158
157
  RobotLab::To.run(
159
158
  objective,
160
- provider: :openai, # routes to Ollama for local runs
161
- model: "qwen3.6:latest",
159
+ provider: :openai, # "lms" (RLTO_PROVIDER's default) resolves to :openai, routed at LM Studio
160
+ model: "qwen/qwen3.8-27b",
162
161
  local_guards: true, # built-in file tools + guardrails
163
- stream: false, # Ollama tool calls need non-streaming
162
+ stream: false, # local LM Studio tool calls run non-streaming
164
163
  max_iterations: 6,
165
164
  verify_command: "ruby -Ilib -Itest test/roman_numeral_test.rb",
166
165
  stop_when: "test/roman_numeral_test.rb passes with 0 failures and 0 errors"
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
  #
4
4
  # Basic Usage would generally be as a CLI tool; however, its a library
5
- # so that means you can build it inot an application program.
5
+ # so that means you can build it into an application program.
6
6
  #
7
7
  # ===========================================================================
8
8
  # 01_basic_usage — drive robot_lab-to programmatically
@@ -32,8 +32,8 @@
32
32
  # ---------------------------------------------------------------------------
33
33
  # Run it
34
34
  # ---------------------------------------------------------------------------
35
- # # Local Ollama (default — no API key needed; requires `ollama serve`):
36
- # ollama pull gpt-oss:20b
35
+ # # Local LM Studio (default — no API key needed; common.rb starts the server
36
+ # # and loads the model for you if they aren't already running/loaded):
37
37
  # bundle exec ruby examples/01_basic_usage/basic_usage.rb
38
38
  #
39
39
  # # A cloud model instead:
@@ -41,17 +41,15 @@
41
41
  # ANTHROPIC_API_KEY=sk-... \
42
42
  # bundle exec ruby examples/01_basic_usage/basic_usage.rb
43
43
  #
44
- # Configuration (all optional, via environment):
45
- # RLTO_LOCAL true|false use a local Ollama model (default true)
46
- # RLTO_PROVIDER name LLM provider (default: openai for local)
47
- # RLTO_MODEL id model id (default: gpt-oss:20b for local)
48
- # OLLAMA_BASE url Ollama OpenAI endpoint (default localhost:11434/v1)
44
+ # Configuration (all optional, via environment; examples/.envrc sets these for you):
45
+ # RLTO_LOCAL true|false use a local LM Studio model (default true)
46
+ # RLTO_PROVIDER name LLM provider label (default: lms for local)
47
+ # RLTO_MODEL id model id (default: qwen/qwen3.8-27b for local)
48
+ # LMS_BASE_URL url LM Studio OpenAI-compatible endpoint (default localhost:1234/v1)
49
49
  # ===========================================================================
50
50
 
51
51
  require "fileutils"
52
- require "logger"
53
52
  require "open3"
54
- require "net/http"
55
53
 
56
54
  # Make the example runnable straight from the repo during development, with or
57
55
  # without `bundle exec`. (When the gem is installed normally, these paths simply
@@ -61,52 +59,22 @@ require "net/http"
61
59
  File.expand_path("../../../robot_lab/lib", __dir__) # sibling robot_lab/lib
62
60
  ].each { |p| $LOAD_PATH.unshift(p) if Dir.exist?(p) }
63
61
 
64
- require "ruby_llm"
65
62
  require "robot_lab"
66
63
  require "robot_lab/to"
64
+ require_relative "../common"
67
65
 
68
66
  # --- configuration ---------------------------------------------------------
69
67
 
70
68
  LOCAL = ENV.fetch("RLTO_LOCAL", "true") == "true"
71
- PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "openai" : "anthropic").to_sym
72
- MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen3.6:latest" : "claude-sonnet-4-6")
73
- OLLAMA = ENV.fetch("OLLAMA_BASE", "http://localhost:11434/v1")
74
-
75
- # For a local model we route RubyLLM's :openai provider at Ollama's
76
- # OpenAI-compatible endpoint, refresh the registry so tool attachment works, and
77
- # run non-streaming (Ollama suppresses tool calls when streaming). See the
78
- # "Local Models" guide in the docs for why.
79
- def configure_local!
80
- RubyLLM.configure do |c|
81
- c.openai_api_base = OLLAMA
82
- c.openai_api_key = "ollama" # ignored by Ollama, but RubyLLM wants a value
83
- c.request_timeout = 600
84
- end
85
- RubyLLM.logger.level = Logger::ERROR
86
- RubyLLM.models.refresh!
87
- rescue StandardError => e
88
- warn "warning: could not refresh Ollama models (#{e.class}: #{e.message})"
89
- end
69
+ PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "lms" : "anthropic").to_sym
70
+ MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen/qwen3.8-27b" : "claude-sonnet-4-6")
90
71
 
91
- # Fail fast with a friendly message if a local run can't reach Ollama.
92
- def preflight_local!
93
- uri = URI.join(OLLAMA, "models")
94
- Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) do |http|
95
- http.get(uri.request_uri)
96
- end
97
- rescue StandardError
98
- abort <<~MSG
99
- Cannot reach an Ollama server at #{OLLAMA}.
100
- Start it and pull a tool-capable model first:
101
-
102
- ollama serve &
103
- ollama pull #{MODEL}
104
-
105
- Or run against a cloud model:
106
- RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \\
107
- ANTHROPIC_API_KEY=sk-... ruby #{File.basename(__FILE__)}
108
- MSG
109
- end
72
+ # ruby_llm has no native "lms" adapter. "lms" is this example's friendly label for
73
+ # "a local LM Studio model"; setup (common.rb) resolves it to RubyLLM's :openai
74
+ # adapter pointed at LM Studio, starting the server and loading MODEL as needed.
75
+ # Everything passed to RobotLab uses the resolved provider; PROVIDER itself is
76
+ # kept only for display.
77
+ LLM_PROVIDER = setup(provider: PROVIDER, model: MODEL)
110
78
 
111
79
  # --- sandbox repository ----------------------------------------------------
112
80
 
@@ -352,15 +320,10 @@ OBJECTIVE = <<~OBJ.strip
352
320
  file. Call submit_result when the suite passes.
353
321
  OBJ
354
322
 
355
- if LOCAL
356
- preflight_local!
357
- configure_local!
358
- end
359
-
360
323
  clean_slate! # delete any project / .robot_lab_to left by a previous run
361
324
  sandbox = make_sandbox
362
325
  puts "Project dir: #{sandbox}"
363
- puts "Provider/model: #{PROVIDER}/#{MODEL} (#{LOCAL ? 'local Ollama' : 'cloud'})"
326
+ puts "Provider/model: #{PROVIDER}/#{MODEL} (#{LOCAL ? 'local LM Studio' : 'cloud'})"
364
327
  puts "Objective: implement lib/roman_numeral.rb to pass the test suite"
365
328
  puts
366
329
 
@@ -370,10 +333,10 @@ RobotLab.on(FeedbackHook)
370
333
  Dir.chdir(sandbox) do
371
334
  RobotLab::To.run(
372
335
  OBJECTIVE,
373
- provider: PROVIDER,
336
+ provider: LLM_PROVIDER,
374
337
  model: MODEL,
375
338
  local_guards: LOCAL, # built-in file tools + small-model guardrails
376
- stream: !LOCAL, # local Ollama tool calls require non-streaming
339
+ stream: !LOCAL, # local LM Studio tool calls run non-streaming
377
340
  max_iterations: 6, # a richer task needs room to iterate
378
341
  run_dir: RUN_DIR, # keep logs + notes under this example directory
379
342
  # The change only commits if the seeded test suite passes:
@@ -11,7 +11,7 @@ autonomously implements the plan behind a real **quality gate**.
11
11
  │ (AskUser tool) │ │ acceptance │ spec │ passes AND the quality gate │
12
12
  │ │ │ test suite │ │ is clean │
13
13
  └───────────────────┘ └──────────────────┘ └───────────────────────────────┘
14
- gpt-5.5 (cloud) · robot_lab Network qwen3.6:latest (local Ollama)
14
+ gpt-5.5 (cloud) · robot_lab Network qwen/qwen3.8-27b (local LM Studio)
15
15
  ```
16
16
 
17
17
  This is the natural progression from
@@ -25,11 +25,11 @@ the implementer) writes the spec, so the implementer still can't game it.
25
25
  |-------|-------------------|-------|
26
26
  | 1 — Ideate | `AskUser` tool, network task, templated robot | OpenAI `gpt-5.5` |
27
27
  | 2 — Plan | sequential network `task … depends_on`, data hand-off, file tools | OpenAI `gpt-5.5` |
28
- | 3 — Implement | `RobotLab::To.run` autonomous loop, verify gate, `stop_when`, guardrails | local Ollama `qwen3.6:latest` |
28
+ | 3 — Implement | `RobotLab::To.run` autonomous loop, verify gate, `stop_when`, guardrails | local LM Studio `qwen/qwen3.8-27b` |
29
29
 
30
30
  The reasoning phases run on a capable cloud model; the implementation loop runs
31
31
  fully local. Because both use RubyLLM's `:openai` provider but different endpoints
32
- (`api.openai.com` vs Ollama's `/v1`), and `openai_api_base` is global, the example
32
+ (`api.openai.com` vs LM Studio's `/v1`), and `openai_api_base` is global, the example
33
33
  toggles it between the sequential phases.
34
34
 
35
35
  ## The quality gate
@@ -55,10 +55,12 @@ flog + flay) into an autonomous gate. Tune the thresholds with `FLOG_MAX` /
55
55
 
56
56
  ```bash
57
57
  export OPENAI_API_KEY="sk-..." # ideation + planning (gpt-5.5)
58
- ollama serve & # implementation (local)
59
- ollama pull qwen3.6:latest
60
58
  ```
61
59
 
60
+ `common.rb` starts the LM Studio server and loads the build model itself if
61
+ they aren't already running/loaded, so there's nothing to start by hand for
62
+ the implementation phase.
63
+
62
64
  ## Run it
63
65
 
64
66
  ```bash
@@ -75,8 +77,9 @@ acceptance suite and the local model implement it.
75
77
  |----------|---------|-------------|
76
78
  | `RLTO_REASON_MODEL` | `gpt-5.5` | model for ideate + plan |
77
79
  | `RLTO_REASON_PROVIDER` | `openai` | provider for ideate + plan |
78
- | `RLTO_BUILD_MODEL` | `qwen3.6:latest` | model for implementation |
79
- | `OLLAMA_BASE` | `http://localhost:11434/v1` | Ollama OpenAI-compatible endpoint |
80
+ | `RLTO_BUILD_PROVIDER` | `lms` (falls back to `RLTO_PROVIDER`) | provider label for implementation; `lms` resolves to `:openai` routed at `LMS_BASE_URL` |
81
+ | `RLTO_BUILD_MODEL` | `qwen/qwen3.8-27b` (falls back to `RLTO_MODEL`) | model for implementation |
82
+ | `LMS_BASE_URL` | `http://localhost:1234/v1` | LM Studio OpenAI-compatible endpoint |
80
83
  | `FLOG_MAX` | `25` | per-method complexity ceiling |
81
84
  | `FLAY_MAX` | `40` | duplication-mass ceiling |
82
85
 
@@ -18,28 +18,29 @@
18
18
  # suite* into the project (test/). It does not implement anything. Its
19
19
  # reply is a one-paragraph implementation objective.
20
20
  #
21
- # Phase 3 IMPLEMENT (robot_lab-to, local Ollama qwen3.6:latest)
21
+ # Phase 3 IMPLEMENT (robot_lab-to, local LM Studio qwen/qwen3.8-27b)
22
22
  # robot_lab-to runs an autonomous loop that writes lib/ code until the
23
23
  # Planner's acceptance suite passes AND a quality gate is clean. The
24
24
  # verify command (quality_gate.rb) runs tests + rubocop + flog + flay, so
25
25
  # the robot must earn each commit on correctness AND quality.
26
26
  #
27
27
  # Models (per your request): reasoning on OpenAI gpt-5.5, building on local
28
- # Ollama qwen3.6. Because both use RubyLLM's :openai provider but different
29
- # endpoints (api.openai.com vs Ollama's /v1), and openai_api_base is global, we
30
- # toggle it between the (sequential) phases.
28
+ # LM Studio qwen/qwen3.8-27b. Because both use RubyLLM's :openai provider but different
29
+ # endpoints (api.openai.com vs LM Studio's /v1), and openai_api_base is global,
30
+ # we toggle it between the (sequential) phases.
31
31
  #
32
32
  # Everything stays under examples/02_advanced_usage/ (project/, .robot_lab_to/),
33
33
  # both git-ignored and recreated on each run.
34
34
  #
35
- # Run it (from the gem root, with OPENAI_API_KEY set and Ollama serving qwen3.6):
35
+ # Run it (from the gem root, with OPENAI_API_KEY set; common.rb starts the LM
36
+ # Studio server and loads the build model for you if they aren't already
37
+ # running/loaded):
36
38
  # bundle exec ruby examples/02_advanced_usage/advanced_usage.rb
37
39
  # ===========================================================================
38
40
 
39
41
  require "fileutils"
40
42
  require "logger"
41
43
  require "open3"
42
- require "net/http"
43
44
 
44
45
  # Make the example runnable straight from the repo, with or without bundler.
45
46
  [
@@ -47,17 +48,19 @@ require "net/http"
47
48
  File.expand_path("../../../robot_lab/lib", __dir__) # sibling robot_lab/lib
48
49
  ].each { |p| $LOAD_PATH.unshift(p) if Dir.exist?(p) }
49
50
 
50
- require "ruby_llm"
51
51
  require "robot_lab"
52
52
  require "robot_lab/to"
53
+ require_relative "../common"
53
54
 
54
55
  # --- configuration ---------------------------------------------------------
55
56
 
56
57
  REASON_PROVIDER = ENV.fetch("RLTO_REASON_PROVIDER", "openai").to_sym
57
- REASON_MODEL = ENV.fetch("RLTO_REASON_MODEL", "gpt-5.5") # real OpenAI
58
- BUILD_PROVIDER = ENV.fetch("RLTO_BUILD_PROVIDER", "openai").to_sym
59
- BUILD_MODEL = ENV.fetch("RLTO_BUILD_MODEL", "qwen3.6:latest") # local Ollama
60
- OLLAMA_BASE = ENV.fetch("OLLAMA_BASE", "http://localhost:11434/v1")
58
+ REASON_MODEL = ENV.fetch("RLTO_REASON_MODEL", "gpt-5.5") # real OpenAI
59
+ # Falls back to the shared RLTO_PROVIDER / RLTO_MODEL (examples/.envrc sets these)
60
+ # so the build phase picks up the same local model as the other examples unless
61
+ # RLTO_BUILD_PROVIDER / RLTO_BUILD_MODEL override it specifically.
62
+ BUILD_PROVIDER = ENV.fetch("RLTO_BUILD_PROVIDER", ENV.fetch("RLTO_PROVIDER", "lms")).to_sym
63
+ BUILD_MODEL = ENV.fetch("RLTO_BUILD_MODEL", ENV.fetch("RLTO_MODEL", "qwen/qwen3.8-27b")) # local LM Studio
61
64
 
62
65
  SANDBOX_DIR = File.expand_path("project", __dir__)
63
66
  RUN_DIR = File.expand_path(".robot_lab_to", __dir__)
@@ -95,28 +98,17 @@ def use_real_openai!
95
98
  RubyLLM.logger.level = Logger::ERROR # keep raw API traffic out of the feed
96
99
  end
97
100
 
98
- # Implementation phase routes the :openai provider at the local Ollama endpoint.
99
- def use_ollama!
100
- RubyLLM.configure do |c|
101
- c.openai_api_base = OLLAMA_BASE
102
- c.openai_api_key = "ollama" # ignored by Ollama
103
- c.request_timeout = 600
104
- end
105
- RubyLLM.logger.level = Logger::ERROR
106
- RubyLLM.models.refresh! # register local models so tool attachment works
107
- rescue StandardError => e
108
- warn "warning: could not refresh Ollama models (#{e.class}: #{e.message})"
101
+ # Implementation phase routes the :openai provider at the local LM Studio endpoint
102
+ # (or whatever BUILD_PROVIDER resolves to). common.rb's setup starts the LM Studio
103
+ # server and loads BUILD_MODEL as needed; the "lms" label resolves to :openai.
104
+ def use_build_provider!
105
+ setup(provider: BUILD_PROVIDER, model: BUILD_MODEL)
109
106
  end
110
107
 
111
108
  # --- preflight -------------------------------------------------------------
112
109
 
113
110
  def preflight!
114
111
  abort "Set OPENAI_API_KEY (the ideation/planning phases use #{REASON_MODEL})." unless ENV["OPENAI_API_KEY"]
115
-
116
- uri = URI.join(OLLAMA_BASE, "models")
117
- Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) { |h| h.get(uri.request_uri) }
118
- rescue StandardError
119
- abort "Cannot reach Ollama at #{OLLAMA_BASE}. Start it and `ollama pull #{BUILD_MODEL}`."
120
112
  end
121
113
 
122
114
  # --- sandbox ---------------------------------------------------------------
@@ -243,7 +235,7 @@ RobotLab::Narrator.enable! # live narration for every robot, across all phases
243
235
 
244
236
  puts "Project dir: #{sandbox}"
245
237
  puts "Reasoning: #{REASON_PROVIDER}/#{REASON_MODEL} (cloud) → ideate + plan"
246
- puts "Building: #{BUILD_PROVIDER}/#{BUILD_MODEL} (local Ollama) → implement"
238
+ puts "Building: #{BUILD_PROVIDER}/#{BUILD_MODEL} (local LM Studio) → implement"
247
239
  puts
248
240
 
249
241
  # -- Phases 1 & 2: ideate -> plan, as a robot_lab network --------------------
@@ -289,15 +281,15 @@ puts "Objective derived from the spec.\n\n"
289
281
 
290
282
  # -- Phase 3: autonomous implementation with robot_lab-to --------------------
291
283
  puts "── Phase 3: implementation (robot_lab-to, quality-gated) ──"
292
- use_ollama!
284
+ build_llm_provider = use_build_provider!
293
285
 
294
286
  Dir.chdir(sandbox) do
295
287
  RobotLab::To.run(
296
288
  objective,
297
- provider: BUILD_PROVIDER,
289
+ provider: build_llm_provider,
298
290
  model: BUILD_MODEL,
299
291
  local_guards: true, # built-in file tools + small-model guardrails
300
- stream: false, # Ollama tool calls require non-streaming
292
+ stream: false, # local LM Studio tool calls run non-streaming
301
293
  max_iterations: 8,
302
294
  run_dir: RUN_DIR,
303
295
  verify_command: "ruby quality_gate.rb", # tests + rubocop + flog + flay