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,325 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # ===========================================================================
5
+ # 03_scored — drive robot_lab-to with an EVAL (measured descent)
6
+ # ===========================================================================
7
+ #
8
+ # 01_basic_usage uses a verify command: a change commits if the test suite exits
9
+ # 0 — pure pass/fail. This example instead scores each iteration with an **eval**
10
+ # and makes the loop *descend* toward a target.
11
+ #
12
+ # The metric is the number of PASSING test methods. A small grader script
13
+ # (score.rb) runs the seeded Roman-numeral suite and prints that count; higher is
14
+ # better. The orchestrator commits an iteration ONLY if it raises the count, rolls
15
+ # back anything that doesn't, and stops once every test passes (the target). Each
16
+ # iteration's prompt gets a "Score Feedback" section — "Best score so far: 6/9" —
17
+ # so the robot's next attempt is aimed, not blind.
18
+ #
19
+ # The grader (score.rb) and the test file are LOCKED with --protect-path: the
20
+ # GraderLock guard refuses any attempt by the robot to edit the criteria it is
21
+ # scored against, so it can't "win" by deleting a failing test.
22
+ #
23
+ # Everything stays under examples/03_scored/ (project/ and .robot_lab_to/), both
24
+ # git-ignored and wiped at the start of every run.
25
+ #
26
+ # ---------------------------------------------------------------------------
27
+ # Run it
28
+ # ---------------------------------------------------------------------------
29
+ # # Local Ollama (default — no API key; requires `ollama serve`):
30
+ # ollama pull gpt-oss:20b
31
+ # bundle exec ruby examples/03_scored/scored_run.rb
32
+ #
33
+ # # A cloud model instead:
34
+ # RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \
35
+ # ANTHROPIC_API_KEY=sk-... \
36
+ # bundle exec ruby examples/03_scored/scored_run.rb
37
+ #
38
+ # Configuration (all optional, via environment):
39
+ # RLTO_LOCAL true|false use a local Ollama model (default true)
40
+ # RLTO_PROVIDER name LLM provider (default: openai for local)
41
+ # RLTO_MODEL id model id (default: qwen3.6:latest for local)
42
+ # OLLAMA_BASE url Ollama OpenAI endpoint (default localhost:11434/v1)
43
+ # ===========================================================================
44
+
45
+ require "fileutils"
46
+ require "logger"
47
+ require "open3"
48
+ require "net/http"
49
+
50
+ # Make the example runnable straight from the repo during development, with or
51
+ # without `bundle exec`. (When the gem is installed normally these paths don't
52
+ # exist and are skipped.)
53
+ [
54
+ File.expand_path("../../lib", __dir__), # robot_lab-to/lib
55
+ File.expand_path("../../../robot_lab/lib", __dir__) # sibling robot_lab/lib
56
+ ].each { |p| $LOAD_PATH.unshift(p) if Dir.exist?(p) }
57
+
58
+ require "ruby_llm"
59
+ require "robot_lab"
60
+ require "robot_lab/to"
61
+
62
+ # --- configuration ---------------------------------------------------------
63
+
64
+ LOCAL = ENV.fetch("RLTO_LOCAL", "true") == "true"
65
+ PROVIDER = ENV.fetch("RLTO_PROVIDER", LOCAL ? "openai" : "anthropic").to_sym
66
+ MODEL = ENV.fetch("RLTO_MODEL", LOCAL ? "qwen3.6:latest" : "claude-sonnet-4-6")
67
+ OLLAMA = ENV.fetch("OLLAMA_BASE", "http://localhost:11434/v1")
68
+
69
+ TOTAL_TESTS = 9 # the seeded suite has 9 test methods — the target
70
+
71
+ # Route RubyLLM's :openai provider at Ollama's OpenAI-compatible endpoint for a
72
+ # local run (non-streaming, since Ollama suppresses tool calls when streaming).
73
+ def configure_local!
74
+ RubyLLM.configure do |c|
75
+ c.openai_api_base = OLLAMA
76
+ c.openai_api_key = "ollama"
77
+ c.request_timeout = 600
78
+ end
79
+ RubyLLM.logger.level = Logger::ERROR
80
+ RubyLLM.models.refresh!
81
+ rescue StandardError => e
82
+ warn "warning: could not refresh Ollama models (#{e.class}: #{e.message})"
83
+ end
84
+
85
+ def preflight_local!
86
+ uri = URI.join(OLLAMA, "models")
87
+ Net::HTTP.start(uri.host, uri.port, open_timeout: 2, read_timeout: 2) { |h| h.get(uri.request_uri) }
88
+ rescue StandardError
89
+ abort <<~MSG
90
+ Cannot reach an Ollama server at #{OLLAMA}.
91
+ Start it and pull a tool-capable model first:
92
+
93
+ ollama serve &
94
+ ollama pull #{MODEL}
95
+
96
+ Or run against a cloud model:
97
+ RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \\
98
+ ANTHROPIC_API_KEY=sk-... ruby #{File.basename(__FILE__)}
99
+ MSG
100
+ end
101
+
102
+ # --- sandbox repository ----------------------------------------------------
103
+
104
+ def sh(*args, chdir:)
105
+ out, err, status = Open3.capture3(*args, chdir: chdir)
106
+ raise "command failed: #{args.join(' ')}\n#{err}" unless status.success?
107
+
108
+ out
109
+ end
110
+
111
+ SANDBOX_DIR = File.expand_path("project", __dir__)
112
+ RUN_DIR = File.expand_path(".robot_lab_to", __dir__)
113
+ ARTIFACTS = [SANDBOX_DIR, RUN_DIR].freeze
114
+
115
+ def clean_slate!
116
+ ARTIFACTS.each do |path|
117
+ next unless File.exist?(path)
118
+
119
+ puts "Cleaning leftover: #{path}"
120
+ FileUtils.rm_rf(path)
121
+ end
122
+ end
123
+
124
+ # The fixed, *failing* Minitest suite the robot must satisfy (9 test methods).
125
+ # Seeded into the sandbox and locked, so the robot can't game its own success.
126
+ ROMAN_TEST = <<~'RUBY'
127
+ # frozen_string_literal: true
128
+
129
+ require "minitest/autorun"
130
+ require "roman_numeral"
131
+
132
+ class RomanNumeralTest < Minitest::Test
133
+ def test_to_roman_basic_digits
134
+ assert_equal "I", RomanNumeral.to_roman(1)
135
+ assert_equal "III", RomanNumeral.to_roman(3)
136
+ assert_equal "XII", RomanNumeral.to_roman(12)
137
+ end
138
+
139
+ def test_to_roman_subtractive_pairs
140
+ { 4 => "IV", 9 => "IX", 40 => "XL", 90 => "XC", 400 => "CD", 900 => "CM" }.each do |n, roman|
141
+ assert_equal roman, RomanNumeral.to_roman(n)
142
+ end
143
+ end
144
+
145
+ def test_to_roman_composite_values
146
+ assert_equal "MMXXIV", RomanNumeral.to_roman(2024)
147
+ assert_equal "MCMLIV", RomanNumeral.to_roman(1954)
148
+ end
149
+
150
+ def test_to_roman_boundaries
151
+ assert_equal "I", RomanNumeral.to_roman(1)
152
+ assert_equal "MMMCMXCIX", RomanNumeral.to_roman(3999)
153
+ end
154
+
155
+ def test_from_roman_round_trips_to_roman
156
+ (1..3999).step(7) { |n| assert_equal n, RomanNumeral.from_roman(RomanNumeral.to_roman(n)) }
157
+ end
158
+
159
+ def test_from_roman_is_case_insensitive
160
+ assert_equal 14, RomanNumeral.from_roman("xiv")
161
+ end
162
+
163
+ def test_to_roman_rejects_out_of_range
164
+ assert_raises(ArgumentError) { RomanNumeral.to_roman(0) }
165
+ assert_raises(ArgumentError) { RomanNumeral.to_roman(4000) }
166
+ end
167
+
168
+ def test_to_roman_rejects_non_integers
169
+ assert_raises(ArgumentError) { RomanNumeral.to_roman("12") }
170
+ assert_raises(ArgumentError) { RomanNumeral.to_roman(3.5) }
171
+ end
172
+
173
+ def test_from_roman_rejects_non_canonical_or_invalid
174
+ ["IIII", "VV", "ABC", ""].each { |bad| assert_raises(ArgumentError) { RomanNumeral.from_roman(bad) } }
175
+ end
176
+ end
177
+ RUBY
178
+
179
+ # The GRADER: prints the number of passing test methods (higher = better). Wired
180
+ # as --measure and locked with --protect-path so the robot can't edit the metric.
181
+ # When the library is missing, the suite fails to load and this prints 0.
182
+ SCORE_SCRIPT = <<~'RUBY'
183
+ #!/usr/bin/env ruby
184
+ # frozen_string_literal: true
185
+ out = `ruby -Ilib -Itest test/roman_numeral_test.rb 2>&1`
186
+ m = out.match(/(\d+)\s+runs?,.*?(\d+)\s+failures?,\s*(\d+)\s+errors?/m)
187
+ puts(m ? (m[1].to_i - m[2].to_i - m[3].to_i) : 0)
188
+ RUBY
189
+
190
+ def make_sandbox
191
+ FileUtils.mkdir_p(File.join(SANDBOX_DIR, "test"))
192
+ sh("git", "init", "-q", chdir: SANDBOX_DIR)
193
+ sh("git", "config", "user.email", "example@example.com", chdir: SANDBOX_DIR)
194
+ sh("git", "config", "user.name", "robot_lab-to example", chdir: SANDBOX_DIR)
195
+
196
+ File.write(File.join(SANDBOX_DIR, "test", "roman_numeral_test.rb"), ROMAN_TEST)
197
+ File.write(File.join(SANDBOX_DIR, "score.rb"), SCORE_SCRIPT)
198
+ File.write(File.join(SANDBOX_DIR, "README.md"),
199
+ "# Roman numeral kata (scored)\n\n" \
200
+ "Implement `lib/roman_numeral.rb` to make more of the suite pass.\n" \
201
+ "Score = passing test methods (grader: `ruby score.rb`). Target #{TOTAL_TESTS}.\n")
202
+
203
+ sh("git", "add", "-A", chdir: SANDBOX_DIR)
204
+ sh("git", "commit", "-qm", "initial: failing suite + grader", chdir: SANDBOX_DIR)
205
+ SANDBOX_DIR
206
+ end
207
+
208
+ # --- live feedback (robot_lab hook) ----------------------------------------
209
+
210
+ # Narrates each robot action so a run never sits silent. See 01_basic_usage for a
211
+ # fuller writeup of the hook system.
212
+ class FeedbackHook < RobotLab::Hook
213
+ class << self
214
+ def before_llm_generation(_ctx)
215
+ say "🤔 thinking…"
216
+ rescue StandardError
217
+ nil
218
+ end
219
+
220
+ def before_tool_call(ctx)
221
+ icon, detail = describe(ctx.tool_name.to_s, ctx.tool_args || {})
222
+ say "#{icon} #{detail}"
223
+ rescue StandardError
224
+ nil
225
+ end
226
+
227
+ private
228
+
229
+ def say(message)
230
+ $stderr.puts " #{message}"
231
+ end
232
+
233
+ def describe(name, args)
234
+ path = args["path"] || args[:path]
235
+ case name
236
+ when "read" then ["📖", "read #{tidy(path)}"]
237
+ when "write" then ["📝", "write #{tidy(path)}"]
238
+ when "edit" then ["✏️ ", "edit #{tidy(path)}"]
239
+ when "bash" then ["💻", "bash: #{snippet(args['command'] || args[:command])}"]
240
+ when /submit_result/ then ["✅", "submit: #{snippet(args['summary'] || args[:summary], 160)}"]
241
+ else ["🔧", name]
242
+ end
243
+ end
244
+
245
+ def tidy(text)
246
+ text.to_s.gsub("#{Dir.pwd}/", "").gsub(Dir.home, "~")
247
+ end
248
+
249
+ def snippet(text, limit = 90)
250
+ line = tidy(text.to_s.lines.first.to_s.strip)
251
+ line.length > limit ? "#{line[0, limit]}…" : line
252
+ end
253
+ end
254
+ end
255
+
256
+ # --- report ----------------------------------------------------------------
257
+
258
+ def final_score(dir)
259
+ out, = Open3.capture2e("ruby", "score.rb", chdir: dir)
260
+ out.strip
261
+ end
262
+
263
+ def report(dir)
264
+ puts "\n=== Result ==="
265
+ puts "Branch: #{sh('git', 'branch', '--show-current', chdir: dir).strip}"
266
+ puts "Score: #{final_score(dir)}/#{TOTAL_TESTS} tests passing"
267
+ puts "\nCommits (one per improvement):"
268
+ puts sh("git", "log", "--oneline", chdir: dir)
269
+
270
+ notes = Dir.glob(File.join(RUN_DIR, "runs", "*", "notes.md")).max
271
+ puts "\nNotes: #{notes}" if notes
272
+ puts "Project: #{dir}"
273
+ end
274
+
275
+ # --- main ------------------------------------------------------------------
276
+
277
+ OBJECTIVE = <<~OBJ.strip
278
+ Implement a Roman numeral library in lib/roman_numeral.rb so that MORE of the
279
+ Minitest suite in test/roman_numeral_test.rb passes each iteration.
280
+
281
+ The module RomanNumeral must provide:
282
+ 1. RomanNumeral.to_roman(integer) -> String (range 1..3999, subtractive
283
+ notation; raises ArgumentError for non-Integers and out-of-range values)
284
+ 2. RomanNumeral.from_roman(string) -> Integer (inverse of to_roman, case
285
+ insensitive; raises ArgumentError for non-canonical strings)
286
+
287
+ Your score is the number of passing test methods (run `ruby score.rb`). Make
288
+ incremental progress — one focused change per iteration that raises the score.
289
+ You may NOT edit test/roman_numeral_test.rb or score.rb (they are locked).
290
+ Call submit_result each iteration describing what you improved.
291
+ OBJ
292
+
293
+ if LOCAL
294
+ preflight_local!
295
+ configure_local!
296
+ end
297
+
298
+ clean_slate!
299
+ sandbox = make_sandbox
300
+ puts "Project dir: #{sandbox}"
301
+ puts "Provider/model: #{PROVIDER}/#{MODEL} (#{LOCAL ? 'local Ollama' : 'cloud'})"
302
+ puts "Eval: measured descent — score = passing tests, target #{TOTAL_TESTS}"
303
+ puts "Locked grader: score.rb, test/roman_numeral_test.rb"
304
+ puts
305
+
306
+ RobotLab.on(FeedbackHook)
307
+
308
+ Dir.chdir(sandbox) do
309
+ RobotLab::To.run(
310
+ OBJECTIVE,
311
+ provider: PROVIDER,
312
+ model: MODEL,
313
+ local_guards: LOCAL,
314
+ stream: !LOCAL,
315
+ max_iterations: 12,
316
+ run_dir: RUN_DIR,
317
+ # The eval: commit only when the passing-test count RISES; stop at the target.
318
+ eval_measure: "ruby score.rb",
319
+ eval_target: TOTAL_TESTS.to_f,
320
+ # Lock the grader + the test file so the robot can't game the metric.
321
+ protect_paths: ["score.rb", "test/roman_numeral_test.rb"]
322
+ )
323
+ end
324
+
325
+ report(sandbox)
@@ -0,0 +1,2 @@
1
+ /project/
2
+ /.robot_lab_to/
@@ -0,0 +1,43 @@
1
+ # 04 — Prose: split the doer from the verifier
2
+
3
+ Demo 03 scores **code** with a deterministic command (passing tests). Prose has no
4
+ `rake coverage`, so this demo uses the **`prose` eval** — a pairwise LLM judge — and
5
+ puts two *different* models in two roles:
6
+
7
+ | Role | Model (default) | Job |
8
+ |------|-----------------|-----|
9
+ | **Doer** | `qwen3.6:latest` | writes and improves `guide.md` |
10
+ | **Verifier** (judge) | `gpt-oss:latest` | compares each draft to the last committed one and rules it **better / worse / same** |
11
+
12
+ Only a draft the judge rules **better** is committed, so every commit is a genuine
13
+ improvement. There's no absolute target (LLM scores are too noisy to descend), so
14
+ the run ends on `--stop-on-plateau` (N drafts with no improvement) or max
15
+ iterations. The spec (`outline.md`) is **locked** with `--protect-path` — the doer
16
+ cannot edit the criteria it's judged against.
17
+
18
+ ## Run it
19
+
20
+ ```bash
21
+ ollama pull qwen3.6:latest # doer
22
+ ollama pull gpt-oss:latest # verifier / judge
23
+ bundle exec ruby examples/04_prose/prose_run.rb
24
+ ```
25
+
26
+ Override the models via env: `RLTO_MODEL` (doer), `RLTO_JUDGE_MODEL` (verifier).
27
+
28
+ ## Why gpt-oss works as the judge here (but not as a doer)
29
+
30
+ gpt-oss is a reasoning model that, when *offered tools*, tends to call its own
31
+ built-ins (`container.exec`) instead of the provided ones — which makes it a poor
32
+ **doer**. But the **judge** is given **no tools**; it just reads two versions and
33
+ replies `better`/`worse`/`same`. As a pure text responder it's reliable, which is
34
+ exactly the verifier's job. This is the separation-of-duties payoff: the model that
35
+ *decides* is independent of the model that *acts*.
36
+
37
+ ## The mechanism
38
+
39
+ `RobotLab::To.run(..., eval: "prose", eval_judge_model: "gpt-oss:latest")` builds an
40
+ `Evals::Prose`. Each iteration, after the doer edits `guide.md`, the eval diffs the
41
+ working tree against the parent commit, shows both versions plus the spec to the
42
+ judge model, and maps its verdict onto `Score#improved`. Commit on *better*, roll
43
+ back otherwise.
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: The message handed to the judge — the criteria plus the artifact to grade.
3
+ parameters:
4
+ criteria: null
5
+ artifact: null
6
+ ---
7
+ CRITERIA:
8
+ <%= criteria %>
9
+
10
+ ARTIFACT TO GRADE:
11
+ -----
12
+ <%= artifact %>
13
+ -----
14
+ Answer with READY or "NEEDS_WORK: <one improvement>".
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: >-
3
+ A demanding editor that grades one artifact against explicit criteria and
4
+ answers only READY or NEEDS_WORK. Used as the verifier/judge robot's system
5
+ prompt (RobotLab.build(template: :judge)).
6
+ ---
7
+ You are a demanding editor. You grade a single artifact against explicit
8
+ criteria and answer in ONE of exactly two forms, nothing else:
9
+ READY
10
+ NEEDS_WORK: <one specific, actionable improvement>
11
+ Judge substance, not length. Be strict but fair.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Grading rubric the judge applies to the outline (Phase 1).
3
+ parameters:
4
+ topic: null
5
+ ---
6
+ This is an OUTLINE for an opinionated guide on "<%= topic %>". It is READY only
7
+ when it lists comprehensive, well-ordered sections as a numbered markdown
8
+ list — each item a short section title plus a one-line description — enough
9
+ to write a strong, non-repetitive guide from.
@@ -0,0 +1,11 @@
1
+ ---
2
+ description: Phase-1 doer objective — create a comprehensive, graded outline.
3
+ parameters:
4
+ topic: null
5
+ ---
6
+ Create outline.md — a comprehensive outline for an opinionated guide on
7
+ <%= topic %>. Write outline.md in your CURRENT directory (bare filename, no
8
+ ".robot_lab_to" path). List the sections as a NUMBERED markdown list, one per
9
+ line: "N. <short section title> — <one-line description>". A separate reviewer
10
+ grades the outline; read REVIEW.md for its feedback and revise outline.md until
11
+ the reviewer marks it READY. Call submit_result each iteration.
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: Grading rubric the judge applies to a single section (Phase 2).
3
+ parameters:
4
+ topic: null
5
+ ---
6
+ This is ONE section of a guide on "<%= topic %>". It is READY when it is
7
+ opinionated, concrete, and complete for its topic — no placeholders.
@@ -0,0 +1,14 @@
1
+ ---
2
+ description: Phase-2 doer objective — write each section, one per iteration, as its own file.
3
+ parameters:
4
+ total: null
5
+ ---
6
+ Following the approved outline.md, write the guide ONE section at a time. There
7
+ are <%= total %> sections. Each section goes in its OWN new file under sections/,
8
+ named sections/NN_slug.md (NN = 01, 02, …, matching the outline order).
9
+
10
+ Each iteration: read outline.md and list the sections/ directory, then write the
11
+ NEXT outline section that has no file yet. Write real, opinionated, concrete
12
+ prose — no placeholders. Read REVIEW.md for the reviewer's feedback on the last
13
+ section and fix it if asked. Do not touch outline.md. Use bare relative paths
14
+ (no ".robot_lab_to"). Call submit_result each iteration naming the section.