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,92 @@
1
+ # Built-in Tools
2
+
3
+ When `--local-guards` is enabled, the per-iteration robot is given a small set of
4
+ built-in workspace tools in addition to `submit_iteration_result`. They are
5
+ named with short, conventional names (`read`, `write`, `edit`, `bash`) β€” small
6
+ models do measurably better with short tool names than with long namespaced ones.
7
+
8
+ !!! note "Why these are bundled"
9
+ Without file tools, a robot can only *report* β€” it can't change code. The
10
+ built-in tools give a local model a predictable, guarded surface to do real
11
+ work. With a cloud model you'd typically supply richer tools yourself; these
12
+ are a dependable baseline for the local-assistant use case.
13
+
14
+ ## The tools
15
+
16
+ ### `read`
17
+
18
+ Read a UTF-8 text file, optionally a line range.
19
+
20
+ | Parameter | Required | Description |
21
+ |-----------|----------|-------------|
22
+ | `path` | yes | File to read. |
23
+ | `offset` | no | 1-based first line to return. |
24
+ | `limit` | no | Maximum number of lines to return. |
25
+
26
+ Output is capped (2 000 lines) so one oversized read can't blow a small model's
27
+ context, and a truncation note tells the model how to read more. Reading a file
28
+ also satisfies the [read-before-edit](guardrails.md#read-before-edit) guard.
29
+
30
+ ### `write`
31
+
32
+ Create a **new** file. Parent directories are created as needed.
33
+
34
+ | Parameter | Required | Description |
35
+ |-----------|----------|-------------|
36
+ | `path` | yes | File to create. |
37
+ | `content` | yes | Full text content. |
38
+
39
+ `write` is for new files only. The [write-guard](guardrails.md#write-guard)
40
+ refuses it on a file that already exists and redirects the model to `edit` β€” small
41
+ models otherwise rewrite whole files and lose content.
42
+
43
+ ### `edit`
44
+
45
+ Exact-string replacement in an existing file.
46
+
47
+ | Parameter | Required | Description |
48
+ |-----------|----------|-------------|
49
+ | `path` | yes | File to edit. |
50
+ | `old_text` | yes | Exact text to replace (whitespace included). |
51
+ | `new_text` | yes | Replacement text. |
52
+ | `replace_all` | no | Replace every occurrence (default false). |
53
+
54
+ `old_text` must match the current file contents exactly and be unique unless
55
+ `replace_all` is set; otherwise the edit is refused with a clear message. The
56
+ [read-before-edit](guardrails.md#read-before-edit) guard requires the file to have
57
+ been `read` this run first, so `old_text` reflects the real contents rather than a
58
+ guess.
59
+
60
+ ### `bash`
61
+
62
+ Run a shell command in the project directory.
63
+
64
+ | Parameter | Required | Description |
65
+ |-----------|----------|-------------|
66
+ | `command` | yes | Shell command to run. |
67
+ | `timeout` | no | Seconds before the command is killed (default 120). |
68
+
69
+ Returns combined stdout+stderr and the exit status (`[exit 0]\n...`). Output is
70
+ capped (~30 000 chars) and a runaway process is killed at the timeout so it can't
71
+ stall an overnight loop.
72
+
73
+ ## How they're wired in
74
+
75
+ The orchestrator builds them per iteration and attaches them through RobotLab's
76
+ `local_tools:`:
77
+
78
+ ```ruby
79
+ # only when local_guards is on
80
+ [submit_tool, Tools::Read.new, Tools::Write.new, Tools::Edit.new, Tools::Bash.new]
81
+ ```
82
+
83
+ Without `--local-guards`, the robot gets only `submit_iteration_result` β€” you're
84
+ expected to supply your own tools (typical for cloud-model setups).
85
+
86
+ The short names come from a small `FileTool` base class that overrides RubyLLM's
87
+ default namespaced tool name. The guardrail hooks match on exactly these names
88
+ (`read`/`write`/`edit`/`bash`).
89
+
90
+ ---
91
+
92
+ Next: [Guardrails](guardrails.md).
@@ -0,0 +1,139 @@
1
+ # Architecture
2
+
3
+ `robot_lab-to` is a thin, well-separated orchestration layer over RobotLab. The
4
+ `Orchestrator` owns the loop and the git history; everything else is a
5
+ single-purpose collaborator it drives.
6
+
7
+ ![robot-to architecture](../assets/architecture.svg)
8
+
9
+ ## Component map
10
+
11
+ | Component | File | Responsibility |
12
+ |-----------|------|----------------|
13
+ | `CLI` | `cli.rb` | `OptionParser`-based `robot-to` entry point. |
14
+ | `Config` | `config.rb` | `MywayConfig::Base` settings cascade. |
15
+ | `Orchestrator` | `orchestrator.rb` | The main loop: setup β†’ iterate β†’ eval-gated commit/rollback β†’ stop β†’ finalize. |
16
+ | `PromptBuilder` | `prompt_builder.rb` | Builds each iteration's system prompt (objective, notes, task, submit, repair, score feedback, stop-when). |
17
+ | `Run` | `run.rb` | Mutable run-state value object (iteration, counters, tokens, timing, paths, `last_score_value`, `iterations_since_improvement`). |
18
+ | `SubmitResult` | `tools/submit_result.rb` | Tool the robot calls to report its result. |
19
+ | `IterationResult` | `iteration_result.rb` | `Data.define` value object for a submitted result. |
20
+ | `CommitManager` | `commit_manager.rb` | All git operations via `Open3` (no shell interpolation); also diffs the working tree vs. a ref for `Evals::Prose`. |
21
+ | `NotesManager` | `notes_manager.rb` | Reads/writes the cross-iteration `notes.md`. |
22
+ | `StopConditions` | `stop_conditions.rb` | Evaluates iterations / tokens / failures / plateau / stop-when. |
23
+ | `Verifier` | `verifier.rb` | Low-level command runner (timeout, process-group kill); composed by `Evals::Code`. |
24
+ | `Backoff` | `backoff.rb` | Interruptible exponential backoff for retries. |
25
+ | `JsonlLogger` | `jsonl_logger.rb` | Append-only JSONL event log with a pre-init buffer. |
26
+ | `ExitSummary` | `exit_summary.rb` | Prints the post-run report. |
27
+ | `AtomicFile` | `atomic_file.rb` | Atomic writes/appends for `notes.md`. |
28
+
29
+ ## Evals β€” orchestrator-owned scoring
30
+
31
+ The eval subsystem generalizes the verify-command gate into a pluggable, scalar
32
+ scoring loop. See [Evals](../concepts/evals.md) for the full behavioral model β€”
33
+ this table is the file map.
34
+
35
+ | Component | File | Responsibility |
36
+ |-----------|------|-----------------|
37
+ | `Evals::Score` | `evals/score.rb` | `Data.define(gate_ok, improved, met_target, value, detail, output)` β€” the verdict for one iteration. |
38
+ | `Evals::Context` | `evals/context.rb` | `Data.define(work_dir, previous_ref, previous_value, objective, iteration)` handed to every eval. |
39
+ | `Evals::Base` | `evals/base.rb` | Abstract eval; `#score(ctx)` and `#protected_paths` (default `[]`). |
40
+ | `Evals::Null` | `evals/null.rb` | Default when nothing is configured: always `gate_ok: true, improved: true, met_target: false`. |
41
+ | `Evals::Code` | `evals/code.rb` | Measured/deterministic: `--verify-command` floor + optional `--measure`/`--target`. Composes `Verifier`. |
42
+ | `Evals::Prose` | `evals/prose.rb` | Judged/pairwise: an LLM judge compares the working tree vs. the parent commit; optional `--floor`. `met_target` always `false`. |
43
+ | `Evals::ProcEval` | `evals/factory.rb` | Wraps a bare proc passed as `eval:` so it responds to `#score`. |
44
+ | `Evals.build` / `Evals.from_name` | `evals/factory.rb` | Resolves `config.eval` (instance / proc / registered Symbol / `"code"` / `"null"` / `"prose"` / `nil`) into an eval. |
45
+ | `Guards::GraderLock` | `guards/grader_lock.rb` | Always-on `RobotLab::Hook` (independent of `--local-guards`) refusing write/edit/bash on an eval's `protected_paths` + `--protect-path`. |
46
+
47
+ ## Local-model layer
48
+
49
+ These components are active only when `--local-guards` is set:
50
+
51
+ | Component | File | Responsibility |
52
+ |-----------|------|----------------|
53
+ | `Tools::FileTool` | `tools/file_tool.rb` | Base class giving file tools short names. |
54
+ | `Tools::Read/Write/Edit/Bash` | `tools/*.rb` | Built-in workspace tools. |
55
+ | `Guards` | `guards.rb` | Registers all guards on a robot via `install(robot, run:, except:)`. |
56
+ | `Guards::WriteGuard` | `guards/write_guard.rb` | Refuse `write` on existing files; normalize paths. Excludable per-run via `Config#write_guard = false` (Ruby-only, default `true`) β€” useful when the robot is iteratively rewriting a whole file, e.g. a prose draft. |
57
+ | `Guards::ReadBeforeEdit` | `guards/read_before_edit.rb` | Refuse `edit` before `read`. |
58
+ | `Guards::Checkpoint` | `guards/checkpoint.rb` | Snapshot files before mutation. |
59
+ | `Guards::QualityMonitor` | `guards/quality_monitor.rb` | Detect repeated-tool-call loops. |
60
+ | `Guards::RunStore` | `guards/run_store.rb` | Thread-local per-run guard state. |
61
+ | `Guards::PathResolution` | `guards/path_resolution.rb` | Pure path helpers shared by the guards. |
62
+
63
+ See [Guardrails](../local-models/guardrails.md) and
64
+ [Built-in Tools](../local-models/tools.md).
65
+
66
+ ## Key design decisions
67
+
68
+ ### Fresh robot per iteration
69
+
70
+ Each iteration constructs a new `RobotLab::Robot`. There is no shared chat history;
71
+ continuity is carried entirely by `notes.md`. This keeps every iteration a clean,
72
+ independent attempt and bounds context growth.
73
+
74
+ ### The orchestrator owns git, the robot owns work
75
+
76
+ The robot never commits. It makes changes and reports a result; the orchestrator
77
+ decides whether to commit (gated by the configured eval) or roll back. This
78
+ separation is what makes the git history trustworthy.
79
+
80
+ ### The eval, not the robot, decides "better" and "done"
81
+
82
+ `should_fully_stop` and a claimed `success: true` are the robot grading its own
83
+ homework. `Evals::Base#score` is the orchestrator-owned authority instead:
84
+ `gate_ok`/`improved` decide commit vs. rollback, and `met_target` β€” not the
85
+ robot's self-report β€” decides when a measurable objective is done. `--stop-when`
86
+ remains as a fallback for objectives an eval can't measure. See
87
+ [Evals](../concepts/evals.md).
88
+
89
+ ### Grader lockdown is separate from the local-model guards
90
+
91
+ `Guards::GraderLock` protects the eval's own criteria (a spec, a floor script,
92
+ `--protect-path` globs) from robot edits. It is wired in unconditionally when
93
+ protected paths exist, unlike the small-model guardrail set
94
+ (`Guards.install`, gated by `--local-guards`) β€” a frontier model can reward-hack
95
+ its grading criteria just as easily as a small one can mangle a file.
96
+
97
+ ### Tools attach via `local_tools:`
98
+
99
+ Tool *instances* are passed to `RobotLab.build` as `local_tools:`. (RobotLab's
100
+ `tools:` parameter is a name *allowlist filter*, not an instance list β€” passing
101
+ instances there silently attaches nothing.) A regression test asserts the built
102
+ robot actually exposes the submit tool, guarding against that class of mistake.
103
+
104
+ ### Streaming is optional
105
+
106
+ By default the robot streams, enabling per-chunk token accounting and mid-stream
107
+ budget enforcement. Local Ollama models run non-streaming (`stream: false`), so
108
+ tokens are accounted from each iteration's result and the budget is enforced at
109
+ iteration boundaries. See [Ollama Setup](../local-models/ollama.md#streaming-and-tool-calls).
110
+
111
+ ### Interruptible by design
112
+
113
+ Signal handlers, the backoff sleep, and the per-iteration runner thread all
114
+ cooperate so a `SIGINT`/`SIGTERM` stops the run promptly and cleanly, with the
115
+ exit summary still printed.
116
+
117
+ ## Run lifecycle (control flow)
118
+
119
+ ```
120
+ To.run(objective, **opts)
121
+ └─ Orchestrator#run
122
+ β”œβ”€ setup_run branch, notes, run dir, collaborators (incl. Evals.build)
123
+ β”œβ”€ main_loop
124
+ β”‚ └─ per iteration:
125
+ β”‚ β”œβ”€ build fresh robot (PromptBuilder + tools [+ guards] [+ GraderLock])
126
+ β”‚ β”œβ”€ robot.run β†’ SubmitResult captured
127
+ β”‚ β”œβ”€ nudge if not submitted
128
+ β”‚ β”œβ”€ evaluate (Eval#score) if success reported
129
+ β”‚ β”‚ β”œβ”€ gate fails β†’ repair_until_gate (R2), else reset --hard
130
+ β”‚ β”‚ β”œβ”€ gate ok, not improved β†’ reset --hard ([NO IMPROVEMENT])
131
+ β”‚ β”‚ └─ gate ok, improved β†’ commit; check met_target?
132
+ β”‚ β”œβ”€ NotesManager append
133
+ β”‚ └─ StopConditions check (incl. plateau)
134
+ └─ finalize_run JSONL close, ExitSummary
135
+ ```
136
+
137
+ ---
138
+
139
+ Next: [Changelog](../about/changelog.md).
@@ -0,0 +1,9 @@
1
+ # Artifacts produced when the example runs β€” never commit them into the
2
+ # robot_lab-to repository. (gitignore has no inline comments, so each note
3
+ # is on its own line.)
4
+
5
+ # the throwaway git repo the robot works in (recreated each run)
6
+ /project/
7
+
8
+ # the run's event log and cross-iteration notes.md
9
+ /.robot_lab_to/
@@ -0,0 +1,173 @@
1
+ # 01 β€” Basic Usage
2
+
3
+ An end-to-end use of `robot_lab-to`, driven programmatically with
4
+ `RobotLab::To.run`.
5
+
6
+ The script ([`basic_usage.rb`](basic_usage.rb)):
7
+
8
+ 1. Creates a fresh **`project/`** git repository next to the script,
9
+ seeded with a **failing Minitest suite** for a Roman-numeral library
10
+ (`test/roman_numeral_test.rb`). It's git-ignored and re-created on each run.
11
+ 2. Runs an autonomous loop with the objective *"implement `lib/roman_numeral.rb`
12
+ until the seeded test suite passes."* Because the tests are fixed and the robot
13
+ may not edit them, it has to satisfy real, externally-defined requirements it
14
+ can't game.
15
+ 3. Gates each iteration on `ruby -Ilib -Itest test/roman_numeral_test.rb` β€” a
16
+ change is committed only when the suite is green; otherwise it's rolled back.
17
+ 4. Stops early (via `stop_when`) once the suite passes, and prints the resulting
18
+ branch, commit history, the generated implementation, and a final test run.
19
+
20
+ It exercises the full loop over several iterations: **branch β†’ iterate β†’ verify β†’
21
+ commit on green / roll back on red**.
22
+
23
+ ### The seeded spec
24
+
25
+ The library must provide `RomanNumeral.to_roman(int)` and
26
+ `RomanNumeral.from_roman(str)` with: subtractive notation (`IV`, `IX`, `XL`, …),
27
+ the `1..3999` range, round-trip correctness, case-insensitive parsing, and
28
+ `ArgumentError` on invalid input β€” including **non-canonical** numerals like
29
+ `"IIII"`. The full expectations live in the seeded `test/roman_numeral_test.rb`.
30
+
31
+ ## Prerequisites
32
+
33
+ Either a **local Ollama** server (default, no API key) or a **cloud** provider key.
34
+
35
+ === "Local Ollama (default)"
36
+
37
+ ```bash
38
+ ollama serve & # if not already running
39
+ ollama pull qwen3.6:latest # a tool-capable model
40
+ ```
41
+
42
+ === "Cloud provider"
43
+
44
+ ```bash
45
+ export ANTHROPIC_API_KEY="sk-ant-..." # or OPENAI_API_KEY
46
+ ```
47
+
48
+ ## Run it
49
+
50
+ From the gem root:
51
+
52
+ ```bash
53
+ # Local Ollama (default)
54
+ bundle exec ruby examples/01_basic_usage/basic_usage.rb
55
+
56
+ # A cloud model instead
57
+ RLTO_LOCAL=false RLTO_PROVIDER=anthropic RLTO_MODEL=claude-sonnet-4-6 \
58
+ bundle exec ruby examples/01_basic_usage/basic_usage.rb
59
+ ```
60
+
61
+ ## Configuration
62
+
63
+ All optional, set via environment variables:
64
+
65
+ | Variable | Default | Description |
66
+ |----------|---------|-------------|
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. |
71
+
72
+ ## What you'll see
73
+
74
+ A `RobotLab::Hook` (`FeedbackHook` in the script) narrates the robot's actions
75
+ live, so the run never sits silent while the model works:
76
+
77
+ ```
78
+ Cleaning leftover: .../examples/01_basic_usage/project
79
+ Project dir: examples/01_basic_usage/project
80
+ Provider/model: openai/qwen3.6:latest (local Ollama)
81
+ Objective: implement lib/roman_numeral.rb to pass the test suite
82
+
83
+ πŸ€” thinking…
84
+ πŸ“– read test/roman_numeral_test.rb
85
+ πŸ“ write lib/roman_numeral.rb
86
+ πŸ’» bash: ruby -Ilib -Itest test/roman_numeral_test.rb
87
+ ↳ exit 1
88
+ ✏️ edit lib/roman_numeral.rb
89
+ πŸ’» bash: ruby -Ilib -Itest test/roman_numeral_test.rb
90
+ ↳ exit 0
91
+ βœ… submit result: Implemented RomanNumeral.to_roman/from_roman; all 9 tests pass.
92
+
93
+ === Result ===
94
+ Branch: robot-to/implement-a-roman-numeral-library-...
95
+
96
+ Commits:
97
+ 4a59bbc robot-to 1: Implemented lib/roman_numeral.rb ... all 9 tests pass.
98
+ 72b06ec initial: failing roman-numeral test suite
99
+
100
+ lib/roman_numeral.rb:
101
+ module RomanNumeral
102
+ ...
103
+ end
104
+
105
+ Final test run:
106
+ 9 runs, 594 assertions, 0 failures, 0 errors, 0 skips
107
+
108
+ Notes: examples/01_basic_usage/.robot_lab_to/runs/.../notes.md
109
+ Project: examples/01_basic_usage/project
110
+ Run logs: examples/01_basic_usage/.robot_lab_to
111
+ ```
112
+
113
+ ### Live feedback via the hook system
114
+
115
+ `FeedbackHook < RobotLab::Hook` is registered globally with `RobotLab.on(...)`,
116
+ so it applies to every per-iteration robot the orchestrator builds β€” no change to
117
+ the orchestrator needed. It implements three hook methods:
118
+
119
+ | Hook method | Reports |
120
+ |-------------|---------|
121
+ | `before_llm_generation` | `πŸ€” thinking…` β€” the model started generating |
122
+ | `before_tool_call` | the action: read / write / edit / bash / submit |
123
+ | `after_tool_call` | a shell exit code, or a tool error |
124
+
125
+ This is a compact, practical example of the robot_lab hook system; the guardrails
126
+ shipped with `--local-guards` use the same mechanism.
127
+
128
+ > **Don't want to write your own?** robot_lab ships a ready-made narrator β€”
129
+ > `RobotLab::Narrator.enable!` β€” which does exactly this. The
130
+ > [02_advanced_usage](../02_advanced_usage/README.md#live-feedback) example uses
131
+ > it. We hand-write the hook here to teach the API.
132
+
133
+ > **Note:** robot_lab-to's own `robot-to: …` progress lines use `Kernel#warn`,
134
+ > which is silenced when Ruby warnings are disabled (`$VERBOSE` is `nil`, common
135
+ > under `bundle exec`). The hook writes to `$stderr` directly, so its narration
136
+ > always shows.
137
+
138
+ Everything the example produces stays under `examples/01_basic_usage/`:
139
+
140
+ ```
141
+ examples/01_basic_usage/
142
+ β”œβ”€β”€ basic_usage.rb
143
+ β”œβ”€β”€ project/ # the throwaway git repo (re-created on every run)
144
+ └── .robot_lab_to/ # run event logs + cross-iteration notes.md
145
+ ```
146
+
147
+ Each run **starts with a clean slate** β€” `project/` and `.robot_lab_to/`
148
+ left over from a previous execution are deleted before setup, then rebuilt. The
149
+ most recent run's repo, logs, and `notes.md` remain afterward for inspection
150
+ (until the next run clears them). Both are git-ignored, so you never need to
151
+ clean them up by hand.
152
+
153
+ ## How it maps to the library
154
+
155
+ The whole example is one call:
156
+
157
+ ```ruby
158
+ RobotLab::To.run(
159
+ objective,
160
+ provider: :openai, # routes to Ollama for local runs
161
+ model: "qwen3.6:latest",
162
+ local_guards: true, # built-in file tools + guardrails
163
+ stream: false, # Ollama tool calls need non-streaming
164
+ max_iterations: 6,
165
+ verify_command: "ruby -Ilib -Itest test/roman_numeral_test.rb",
166
+ stop_when: "test/roman_numeral_test.rb passes with 0 failures and 0 errors"
167
+ )
168
+ ```
169
+
170
+ For the meaning of each option, see the
171
+ [Settings Reference](https://madbomber.github.io/robot_lab-to/configuration/settings/)
172
+ and, for the local-model specifics, the
173
+ [Local Models guide](https://madbomber.github.io/robot_lab-to/local-models/).