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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.loki +21 -0
- data/.quality/reek_baseline.txt +17 -0
- data/.rubocop.yml +7 -0
- data/CHANGELOG.md +67 -0
- data/CLAUDE.md +74 -0
- data/COMMITS.md +27 -0
- data/README.md +352 -0
- data/Rakefile +132 -0
- data/bin/robot-to +14 -0
- data/docs/about/changelog.md +82 -0
- data/docs/assets/architecture.svg +110 -0
- data/docs/assets/images/robot_lab-to.jpg +0 -0
- data/docs/assets/iteration-loop.svg +97 -0
- data/docs/concepts/evals.md +254 -0
- data/docs/concepts/iteration-loop.md +128 -0
- data/docs/concepts/notes.md +91 -0
- data/docs/concepts/run-state.md +93 -0
- data/docs/concepts/stop-conditions.md +111 -0
- data/docs/concepts/verification.md +75 -0
- data/docs/configuration/cli.md +111 -0
- data/docs/configuration/index.md +93 -0
- data/docs/configuration/settings.md +270 -0
- data/docs/getting-started/anatomy-of-a-run.md +111 -0
- data/docs/getting-started/installation.md +78 -0
- data/docs/getting-started/quick-start.md +105 -0
- data/docs/index.md +96 -0
- data/docs/local-models/guardrails.md +117 -0
- data/docs/local-models/index.md +88 -0
- data/docs/local-models/ollama.md +122 -0
- data/docs/local-models/tools.md +92 -0
- data/docs/reference/architecture.md +139 -0
- data/examples/01_basic_usage/.gitignore +9 -0
- data/examples/01_basic_usage/README.md +173 -0
- data/examples/01_basic_usage/basic_usage.rb +386 -0
- data/examples/02_advanced_usage/.gitignore +8 -0
- data/examples/02_advanced_usage/README.md +117 -0
- data/examples/02_advanced_usage/advanced_usage.rb +308 -0
- data/examples/02_advanced_usage/quality_gate.rb +99 -0
- data/examples/03_scored/.gitignore +2 -0
- data/examples/03_scored/README.md +117 -0
- data/examples/03_scored/scored_run.rb +325 -0
- data/examples/04_prose/.gitignore +2 -0
- data/examples/04_prose/README.md +43 -0
- data/examples/04_prose/prompts_dir/grade_message.md +14 -0
- data/examples/04_prose/prompts_dir/judge.md +11 -0
- data/examples/04_prose/prompts_dir/outline_criteria.md +9 -0
- data/examples/04_prose/prompts_dir/outline_objective.md +11 -0
- data/examples/04_prose/prompts_dir/section_criteria.md +7 -0
- data/examples/04_prose/prompts_dir/sections_objective.md +14 -0
- data/examples/04_prose/prose_run.rb +302 -0
- data/lib/robot_lab/to/atomic_file.rb +69 -0
- data/lib/robot_lab/to/backoff.rb +46 -0
- data/lib/robot_lab/to/cli.rb +176 -0
- data/lib/robot_lab/to/commit_manager.rb +135 -0
- data/lib/robot_lab/to/config/defaults.yml +27 -0
- data/lib/robot_lab/to/config.rb +86 -0
- data/lib/robot_lab/to/decision.rb +40 -0
- data/lib/robot_lab/to/decision_manager.rb +193 -0
- data/lib/robot_lab/to/errors.rb +30 -0
- data/lib/robot_lab/to/evals/base.rb +23 -0
- data/lib/robot_lab/to/evals/code.rb +77 -0
- data/lib/robot_lab/to/evals/context.rb +17 -0
- data/lib/robot_lab/to/evals/factory.rb +84 -0
- data/lib/robot_lab/to/evals/null.rb +17 -0
- data/lib/robot_lab/to/evals/prose.rb +110 -0
- data/lib/robot_lab/to/evals/score.rb +22 -0
- data/lib/robot_lab/to/exit_summary.rb +75 -0
- data/lib/robot_lab/to/guards/checkpoint.rb +84 -0
- data/lib/robot_lab/to/guards/grader_lock.rb +62 -0
- data/lib/robot_lab/to/guards/path_resolution.rb +61 -0
- data/lib/robot_lab/to/guards/quality_monitor.rb +113 -0
- data/lib/robot_lab/to/guards/read_before_edit.rb +83 -0
- data/lib/robot_lab/to/guards/run_store.rb +31 -0
- data/lib/robot_lab/to/guards/write_guard.rb +71 -0
- data/lib/robot_lab/to/guards.rb +54 -0
- data/lib/robot_lab/to/iteration_result.rb +29 -0
- data/lib/robot_lab/to/jsonl_logger.rb +59 -0
- data/lib/robot_lab/to/notes_manager.rb +121 -0
- data/lib/robot_lab/to/orchestrator.rb +642 -0
- data/lib/robot_lab/to/prompt_builder.rb +208 -0
- data/lib/robot_lab/to/run.rb +109 -0
- data/lib/robot_lab/to/stop_conditions.rb +71 -0
- data/lib/robot_lab/to/tools/bash.rb +75 -0
- data/lib/robot_lab/to/tools/edit.rb +52 -0
- data/lib/robot_lab/to/tools/file_tool.rb +32 -0
- data/lib/robot_lab/to/tools/read.rb +57 -0
- data/lib/robot_lab/to/tools/request_decision.rb +66 -0
- data/lib/robot_lab/to/tools/submit_result.rb +50 -0
- data/lib/robot_lab/to/tools/write.rb +32 -0
- data/lib/robot_lab/to/verifier.rb +63 -0
- data/lib/robot_lab/to/version.rb +7 -0
- data/lib/robot_lab/to.rb +81 -0
- data/mkdocs.yml +145 -0
- metadata +175 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Evals: Scoring Iterations
|
|
2
|
+
|
|
3
|
+
A robot reporting `success: true` is a claim; a `--verify-command` that must exit
|
|
4
|
+
`0` answers "did it break?". Neither answers the harder question: **is this
|
|
5
|
+
iteration actually better than the last one?** An agent will happily report
|
|
6
|
+
success — and pass a verify command — on a change that made no real progress.
|
|
7
|
+
|
|
8
|
+
An **eval** replaces the robot's self-report as the deciding authority. It is an
|
|
9
|
+
orchestrator-owned, pluggable scorer that judges the *uncommitted working tree*
|
|
10
|
+
after every iteration and returns a `Score`. The eval decides commit vs. rollback
|
|
11
|
+
(via `gate_ok` / `improved`) and, for measurable objectives, decides when the run
|
|
12
|
+
is *done* (`met_target`) — instead of the robot's self-reported
|
|
13
|
+
`should_fully_stop`.
|
|
14
|
+
|
|
15
|
+
This generalizes the [Verification Gate](verification.md): `--verify-command`
|
|
16
|
+
still exists and still works exactly as before, but it is now one input to the
|
|
17
|
+
default eval (`Evals::Code`) rather than the whole story.
|
|
18
|
+
|
|
19
|
+
## The `Score`
|
|
20
|
+
|
|
21
|
+
Every eval returns a `RobotLab::To::Evals::Score` — a `Data.define` with six
|
|
22
|
+
fields:
|
|
23
|
+
|
|
24
|
+
| Field | Meaning |
|
|
25
|
+
|-------|---------|
|
|
26
|
+
| `gate_ok` | The floor holds (correctness). Must be `true` to commit at all. |
|
|
27
|
+
| `improved` | Better than the parent commit? The descent signal. |
|
|
28
|
+
| `met_target` | The objective is measurably reached — an orchestrator-owned stop. |
|
|
29
|
+
| `value` | The score itself (higher = better); `nil` for pure-pairwise evals. |
|
|
30
|
+
| `detail` | One-line human summary, written to `notes.md` and the log. |
|
|
31
|
+
| `output` | Diagnostic text (gate output, judge rationale) fed to repair prompts; `nil` when there is none. |
|
|
32
|
+
|
|
33
|
+
`gate_ok?`, `improved?`, and `met_target?` predicate readers are also defined.
|
|
34
|
+
|
|
35
|
+
## How it drives the loop
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
robot says success ──► Eval#score ──► gate_ok? ──► improved (or --no-require-improvement)? ──► COMMIT
|
|
39
|
+
│ │
|
|
40
|
+
│ no │ no
|
|
41
|
+
▼ ▼
|
|
42
|
+
repair_until_gate (R2) ROLLBACK ([NO IMPROVEMENT])
|
|
43
|
+
then ROLLBACK if still failing
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Gate fails** (`gate_ok: false`) — the failure output is handed back to the
|
|
47
|
+
*same* robot up to `--max-verify-repairs` times, re-scoring after each attempt
|
|
48
|
+
(this is the same R2 repair loop the verify gate always had — see
|
|
49
|
+
[Verification Gate](verification.md)). If the budget runs out, the tree is
|
|
50
|
+
rolled back and the notes get a `[VERIFY FAILED]` entry.
|
|
51
|
+
- **Gate passes but didn't improve** (`improved: false`) — rolled back with a
|
|
52
|
+
`[NO IMPROVEMENT]` entry in `notes.md`, *unless* `--no-require-improvement` is
|
|
53
|
+
passed (see [`require_improvement`](#require_improvement-strict-descent)
|
|
54
|
+
below). This is **not** counted as a failure — it does not trip
|
|
55
|
+
`--max-consecutive-failures` — but it does count toward
|
|
56
|
+
[`--stop-on-plateau`](stop-conditions.md#plateau-no-improvement).
|
|
57
|
+
- **Gate passes and improved** — committed. `@run.last_score_value` is updated,
|
|
58
|
+
the plateau counter resets, and `met_target?` is checked: if true, the run
|
|
59
|
+
aborts with `target met: <detail>` — this **supersedes** the robot's
|
|
60
|
+
self-reported `should_fully_stop`; `--stop-when` remains as a fallback.
|
|
61
|
+
|
|
62
|
+
Each iteration's verdict is logged as an `eval` event in `run.log` (`gate`,
|
|
63
|
+
`improved`, `value`, `met_target`, `detail`) — see
|
|
64
|
+
[Run State & Event Log](run-state.md).
|
|
65
|
+
|
|
66
|
+
## `Evals::Null` — the default, unscored
|
|
67
|
+
|
|
68
|
+
When nothing is configured, `Evals::Null` reports `gate_ok: true, improved: true,
|
|
69
|
+
met_target: false` on every call — every reported success commits, and the run
|
|
70
|
+
stops only via `--stop-when` or the other stop conditions. This is exactly
|
|
71
|
+
`robot_lab-to`'s original behavior; adding an eval is entirely opt-in.
|
|
72
|
+
|
|
73
|
+
## `Evals::Code` — measured descent
|
|
74
|
+
|
|
75
|
+
For software, "better" is measurable. `Evals::Code` composes two independent
|
|
76
|
+
commands:
|
|
77
|
+
|
|
78
|
+
| Option | Role |
|
|
79
|
+
|--------|------|
|
|
80
|
+
| `--verify-command CMD` | **Floor** — must exit `0` (correctness). Same gate as before. |
|
|
81
|
+
| `--measure CMD` | **Descent signal** — a command that prints a number to stdout; higher is better. |
|
|
82
|
+
| `--target FLOAT` | Stop once the measured value reaches this. |
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
robot-to "Raise parser coverage to 90%" \
|
|
86
|
+
--verify "bundle exec rake test" \ # floor: must stay green
|
|
87
|
+
--measure "bundle exec rake coverage" \ # descent signal
|
|
88
|
+
--target 90 # stop once the score reaches this
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`Evals::Code#score`:
|
|
92
|
+
|
|
93
|
+
- `gate_ok` — `true` when `--verify-command` is unset, or the command exits `0`.
|
|
94
|
+
- `value` — the **first number** matched in the `--measure` command's stdout
|
|
95
|
+
(`/-?\d+(?:\.\d+)?/`), or `nil` if `--measure` is unset.
|
|
96
|
+
- `improved` — with no `value`, equals `gate_ok` (any gate-passing change is
|
|
97
|
+
progress, matching legacy behavior). With a `value`, `true` only if it strictly
|
|
98
|
+
beats `previous_value` (or there is no prior committed value yet).
|
|
99
|
+
- `met_target` — `true` once `value >= --target`.
|
|
100
|
+
|
|
101
|
+
**Backward compatible by construction:** with no `--measure`, `Evals::Code`
|
|
102
|
+
behaves exactly like the original verify-only gate — `improved == gate_ok`,
|
|
103
|
+
`met_target` never fires, and the run stops via `--stop-when` / the other stop
|
|
104
|
+
conditions as before.
|
|
105
|
+
|
|
106
|
+
The `--measure` command just has to print a number: `rake coverage`, a benchmark
|
|
107
|
+
harness, `ruby -e 'puts passing_count'`, `grep -c` of a lint report — anything.
|
|
108
|
+
|
|
109
|
+
## `Evals::Prose` — pairwise judgement
|
|
110
|
+
|
|
111
|
+
For a document, an opinion piece, or a book there is no `rake coverage`. Absolute
|
|
112
|
+
scores from an LLM are too noisy to descend against — but "is B better than A?"
|
|
113
|
+
is a reliable judgement. `Evals::Prose` asks a **judge model** to compare the
|
|
114
|
+
working tree to the last *committed* version:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
robot-to "Write an opinionated guide to the Viable Systems Model" \
|
|
118
|
+
--eval prose \
|
|
119
|
+
--spec outline.md \ # the spec the judge measures against
|
|
120
|
+
--floor "rake docs:lint" \ # optional mechanizable checks (links, TODOs)
|
|
121
|
+
--judge-model claude-opus-4 \ # defaults to the doer's --model
|
|
122
|
+
--stop-on-plateau 3 # stop after 3 drafts with no improvement
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`Evals::Prose#score`:
|
|
126
|
+
|
|
127
|
+
- `gate_ok` — `true` when `--floor` is unset, or the floor command exits `0`. If
|
|
128
|
+
the floor fails, the judge is **not** called (saves a request) and the verdict
|
|
129
|
+
is forced to `:worse`.
|
|
130
|
+
- The judge is shown `VERSION A` (the parent commit) and `VERSION B` (the working
|
|
131
|
+
tree) for every file that changed, plus the objective and — if `--spec` is set —
|
|
132
|
+
its contents, and replies exactly one word: `better`, `worse`, or `same`.
|
|
133
|
+
Anything ambiguous is treated as `:same` (does not count as improvement).
|
|
134
|
+
- **Brand-new document shortcut:** if the parent version is empty and the draft
|
|
135
|
+
is not, the judge is skipped and the verdict is `:better` automatically — an
|
|
136
|
+
"empty vs. content" pairwise comparison is degenerate and models frequently
|
|
137
|
+
(and wrongly) call it `:same`.
|
|
138
|
+
- `improved` — `true` only when the verdict is `:better`.
|
|
139
|
+
- `met_target` — **always `false`.** There is no absolute milestone for prose;
|
|
140
|
+
runs end via `--stop-on-plateau`, `--max-iterations`, or a human stopping it.
|
|
141
|
+
- `value` — always `nil` (pairwise verdicts have no scalar).
|
|
142
|
+
|
|
143
|
+
By default the judge **reuses the doer's `--model`** — no separate judge model is
|
|
144
|
+
required to get pairwise scoring. Pass `--judge-model` to use a different (often
|
|
145
|
+
cheaper, or deliberately more skeptical) model for the judge role. See
|
|
146
|
+
`examples/04_prose` in the gem for a worked two-model (doer + judge) setup.
|
|
147
|
+
|
|
148
|
+
## `require_improvement` — strict descent
|
|
149
|
+
|
|
150
|
+
- **Default: `true`.** A gate-passing iteration that does not beat the parent is
|
|
151
|
+
rolled back (`[NO IMPROVEMENT]`), so the branch descends monotonically.
|
|
152
|
+
- `--no-require-improvement` restores "commit any gate-passing success," useful
|
|
153
|
+
when you want every non-regressing attempt kept rather than only strict
|
|
154
|
+
improvements.
|
|
155
|
+
- This is a hardcoded Ruby/CLI-only default (`Config#require_improvement?`), not
|
|
156
|
+
in `defaults.yml` — set it per-run via `--no-require-improvement` or
|
|
157
|
+
`RobotLab::To.run(require_improvement: false)`.
|
|
158
|
+
|
|
159
|
+
## Plateau: the primary stop for judged evals
|
|
160
|
+
|
|
161
|
+
`Evals::Prose` never sets `met_target`, so its natural terminator is
|
|
162
|
+
`--stop-on-plateau N` — stop after `N` iterations with no committed improvement.
|
|
163
|
+
Plateau is tracked independently of `--max-consecutive-failures` (which is for
|
|
164
|
+
the robot being genuinely broken — errors, gate failures, no submit); a
|
|
165
|
+
gate-passing "same" verdict is normal, healthy exploration for prose, not a
|
|
166
|
+
failure. See [Stop Conditions](stop-conditions.md#plateau-no-improvement).
|
|
167
|
+
|
|
168
|
+
## Score feedback in the prompt
|
|
169
|
+
|
|
170
|
+
When a run is scored (`--measure`, `--target`, `--eval prose`, or `--spec` is
|
|
171
|
+
set), `PromptBuilder` adds a **Score Feedback** section to every iteration's
|
|
172
|
+
system prompt:
|
|
173
|
+
|
|
174
|
+
- The best committed score so far and the target, if any: *"Best score committed
|
|
175
|
+
so far: 84.0 (target: 90.0)."*
|
|
176
|
+
- A plateau warning after a run of non-improving iterations: *"The last 3
|
|
177
|
+
iteration(s) did not improve and were rolled back... try a DIFFERENT approach
|
|
178
|
+
now."*
|
|
179
|
+
- Otherwise, positive reinforcement: *"Your last change improved the result and
|
|
180
|
+
was committed. Build on it."*
|
|
181
|
+
|
|
182
|
+
This turns each attempt into a testable hypothesis against the target rather
|
|
183
|
+
than a blind guess. Unscored runs (the `Evals::Null` default) get no such
|
|
184
|
+
section — behavior is unchanged.
|
|
185
|
+
|
|
186
|
+
## Guarding the grader (`GraderLock`)
|
|
187
|
+
|
|
188
|
+
Whatever the eval, the robot must not be able to "win" by editing the criteria
|
|
189
|
+
it is scored against. `Guards::GraderLock` — a `RobotLab::Hook` — refuses any
|
|
190
|
+
`write`/`edit` targeting a locked path, and refuses any `bash` command whose
|
|
191
|
+
string references one (matched conservatively, by absolute path or basename).
|
|
192
|
+
|
|
193
|
+
Locked paths come from two sources, merged:
|
|
194
|
+
|
|
195
|
+
- `Evals::Base#protected_paths` — `Evals::Prose#protected_paths` returns
|
|
196
|
+
`[spec]` (a floor *command* isn't reliably a single file, so lock it
|
|
197
|
+
separately with `--protect-path` if it's a script); `Evals::Code` locks
|
|
198
|
+
nothing by default — **the test suite is deliberately editable**, since
|
|
199
|
+
adding tests is how coverage legitimately rises.
|
|
200
|
+
- `--protect-path GLOB` (repeatable) — user-specified grader files for any eval,
|
|
201
|
+
code or custom (e.g. a floor script, a scoring rubric).
|
|
202
|
+
|
|
203
|
+
`GraderLock` is installed in `Orchestrator#build_robot` whenever any protected
|
|
204
|
+
paths resolve, **independent of `--local-guards`** — unlike the small-model
|
|
205
|
+
guardrails (see [Guardrails](../local-models/guardrails.md)), a frontier model
|
|
206
|
+
can reward-hack too, so this one is always on.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
robot-to "..." --eval code --measure "ruby score.rb" --target 100 \
|
|
210
|
+
--protect-path score.rb --protect-path test/scoring_test.rb
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Custom evals
|
|
214
|
+
|
|
215
|
+
Three escape hatches, all via the Ruby API (`RobotLab::To.run` / `Config.new`):
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# (a) subclass RobotLab::To::Evals::Base
|
|
219
|
+
class BenchmarkEval < RobotLab::To::Evals::Base
|
|
220
|
+
def score(ctx) = RobotLab::To::Evals::Score.new(gate_ok: true, improved: ..., met_target: ..., value: ..., detail: ..., output: nil)
|
|
221
|
+
end
|
|
222
|
+
RobotLab::To.run(objective, eval: BenchmarkEval.new)
|
|
223
|
+
|
|
224
|
+
# (b) a bare proc — wrapped in Evals::ProcEval automatically
|
|
225
|
+
RobotLab::To.run(objective, eval: ->(ctx) { RobotLab::To::Evals::Score.new(...) })
|
|
226
|
+
|
|
227
|
+
# (c) a registered symbol
|
|
228
|
+
RobotLab::To.register_eval(:benchmark) { |config| BenchmarkEval.new(target: config.eval_target) }
|
|
229
|
+
RobotLab::To.run(objective, eval: :benchmark)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
!!! warning "`--eval` on the CLI only resolves `code` / `null` / `prose`"
|
|
233
|
+
The CLI's `--eval NAME` flag always produces a **String**. The factory's
|
|
234
|
+
registry lookup only matches a **Symbol** (`Config.new(eval: :benchmark)`),
|
|
235
|
+
so a registered custom eval is reachable from the Ruby API but not yet from
|
|
236
|
+
`--eval benchmark` on the command line — passing an unrecognized string
|
|
237
|
+
raises `unknown eval strategy`. Likewise, pointing `--eval` at a `.rb` file
|
|
238
|
+
path is not implemented; use the Ruby API's instance/proc/registered-symbol
|
|
239
|
+
forms instead.
|
|
240
|
+
|
|
241
|
+
## Backward compatibility
|
|
242
|
+
|
|
243
|
+
| Invocation | Eval selected | Behavior |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| No `--verify-command`, no `--eval`, no `--measure`/`--target` | `Evals::Null` | Commit any reported success; stop via `--stop-when`. **Identical to pre-Evals behavior.** |
|
|
246
|
+
| `--verify-command "rake test"` only | `Evals::Code` (gate-only) | Commit on pass; stop via `--stop-when`. **Identical.** |
|
|
247
|
+
| Old `run.json` (`--resume`) | — | Loads; `last_score_value` / `iterations_since_improvement` default to `nil` / `0`. |
|
|
248
|
+
|
|
249
|
+
New behavior only activates when `--measure`, `--target`, `--eval prose`, or
|
|
250
|
+
`--stop-on-plateau` is supplied.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
Next: [Run State & Event Log](run-state.md).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# The Iteration Loop
|
|
2
|
+
|
|
3
|
+
The orchestrator is a loop. Each pass is an independent attempt at **one**
|
|
4
|
+
improvement toward the objective. This page describes the full lifecycle and the
|
|
5
|
+
decisions made at each step.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Setup (once per run)
|
|
10
|
+
|
|
11
|
+
Before the loop begins, `setup_run`:
|
|
12
|
+
|
|
13
|
+
- Confirms `HEAD` exists — aborts with guidance if the repo has no commits.
|
|
14
|
+
- Creates the run branch `robot-to/<slug>-<timestamp>` and records `base_commit`.
|
|
15
|
+
- Creates the run directory and seeds `notes.md` with the objective header.
|
|
16
|
+
- Adds the run directory to `.git/info/exclude`.
|
|
17
|
+
- Builds the per-run collaborators: `PromptBuilder`, `Backoff`, `StopConditions`,
|
|
18
|
+
and the **eval** (`Evals.build(config, git:)` — `Evals::Code` when
|
|
19
|
+
`--verify-command`/`--measure`/`--target` is set, `Evals::Prose` for
|
|
20
|
+
`--eval prose`, otherwise the unscored `Evals::Null`; see [Evals](evals.md)).
|
|
21
|
+
|
|
22
|
+
## One iteration
|
|
23
|
+
|
|
24
|
+
### 1. Build a fresh robot
|
|
25
|
+
|
|
26
|
+
A **new** `RobotLab::Robot` is created every iteration — there is no shared chat
|
|
27
|
+
history between iterations. Continuity comes entirely from `notes.md`, which is
|
|
28
|
+
injected into the system prompt. The prompt (built by `PromptBuilder`) contains:
|
|
29
|
+
|
|
30
|
+
- the objective and current iteration number,
|
|
31
|
+
- the accumulated notes ("prior work"),
|
|
32
|
+
- the task instructions (make one focused change; run tests/build; a no-op is not
|
|
33
|
+
success),
|
|
34
|
+
- the requirement to call `submit_iteration_result`,
|
|
35
|
+
- optional repair and stop-when sections.
|
|
36
|
+
|
|
37
|
+
The robot is given the `submit_iteration_result` tool. With `--local-guards`, it
|
|
38
|
+
also receives built-in file tools (`read`, `write`, `edit`, `bash`) and the
|
|
39
|
+
guardrail hooks. See [Built-in Tools](../local-models/tools.md). If the
|
|
40
|
+
configured eval (or `--protect-path`) has protected paths, `Guards::GraderLock`
|
|
41
|
+
is attached too — independent of `--local-guards` — refusing edits to the
|
|
42
|
+
grader's own criteria. See [Evals](evals.md#guarding-the-grader-graderlock).
|
|
43
|
+
|
|
44
|
+
### 2. The robot works
|
|
45
|
+
|
|
46
|
+
`robot.run` executes. The robot makes changes using whatever tools it has, then
|
|
47
|
+
calls `submit_iteration_result` with:
|
|
48
|
+
|
|
49
|
+
| Field | Meaning |
|
|
50
|
+
|-------|---------|
|
|
51
|
+
| `success` | Did this iteration make meaningful progress? |
|
|
52
|
+
| `summary` | One sentence describing what was done or why it stopped. |
|
|
53
|
+
| `key_changes` | Files or changes made this iteration. |
|
|
54
|
+
| `key_learnings` | Insights to carry forward in the notes. |
|
|
55
|
+
| `should_fully_stop` | Set when a `--stop-when` condition is judged met. |
|
|
56
|
+
|
|
57
|
+
### 3. Handle "thinks but doesn't act"
|
|
58
|
+
|
|
59
|
+
If the robot ends its turn **without** calling `submit_iteration_result`, the
|
|
60
|
+
orchestrator re-prompts the *same* robot (preserving its chat) up to
|
|
61
|
+
`--max-submit-nudges` times, asking only for the result. This recovers the common
|
|
62
|
+
degenerate case where a model does work but forgets the final report. If it still
|
|
63
|
+
doesn't submit, the iteration is treated as a failure.
|
|
64
|
+
|
|
65
|
+
### 4. Decide: commit or roll back
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
result.success? ──► Eval#score ──► gate_ok? ──► improved (or --no-require-improvement)? ──► COMMIT
|
|
69
|
+
│ │ │
|
|
70
|
+
│ no │ no │ no
|
|
71
|
+
▼ ▼ (after R2 repair budget) ▼
|
|
72
|
+
ROLLBACK ROLLBACK ROLLBACK
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- **Robot reported failure (or no submit)** → `git reset --hard`;
|
|
76
|
+
consecutive-failure counter increments. The eval is never called.
|
|
77
|
+
- **Robot reported success** → the configured **eval** scores the working tree
|
|
78
|
+
(`Evals::Null` by default — see [Evals](evals.md)):
|
|
79
|
+
- **Gate fails** (`gate_ok: false`) — handed back to the *same* robot for up
|
|
80
|
+
to `--max-verify-repairs` repair attempts, re-scoring each time; if the
|
|
81
|
+
gate is still failing when the budget runs out, `git reset --hard` and
|
|
82
|
+
recorded as `[VERIFY FAILED]`.
|
|
83
|
+
- **Gate passes, didn't improve** (`improved: false`, and
|
|
84
|
+
`require_improvement?` is true — the default) → `git reset --hard`;
|
|
85
|
+
recorded as `[NO IMPROVEMENT]`. Not counted as a failure.
|
|
86
|
+
- **Gate passes and improved** → `git add -A` and commit (if anything is
|
|
87
|
+
staged). The consecutive-failure/-error counters reset, and the eval's
|
|
88
|
+
`met_target?` is checked — if true, the run stops immediately.
|
|
89
|
+
|
|
90
|
+
See [Evals](evals.md) for the full scoring model (this generalizes what used to
|
|
91
|
+
be a verify-command-only gate — see [Verification Gate](verification.md)).
|
|
92
|
+
|
|
93
|
+
### 5. Update the notes
|
|
94
|
+
|
|
95
|
+
Either way, the orchestrator appends an entry to `notes.md` — success, failure,
|
|
96
|
+
no-improvement, verify/gate-failure, or error — so the next iteration's robot
|
|
97
|
+
sees what happened. See [Cross-Iteration Memory](notes.md).
|
|
98
|
+
|
|
99
|
+
### 6. Check stop conditions
|
|
100
|
+
|
|
101
|
+
Stop conditions are evaluated **before** and **after** each iteration. The loop
|
|
102
|
+
ends if iterations, tokens, consecutive failures, or the eval's plateau counter
|
|
103
|
+
hit their limits; if the eval reports `met_target`; or if the robot set
|
|
104
|
+
`should_fully_stop` for a `--stop-when` condition. See
|
|
105
|
+
[Stop Conditions](stop-conditions.md) and [Evals](evals.md).
|
|
106
|
+
|
|
107
|
+
## Errors, retries, and backoff
|
|
108
|
+
|
|
109
|
+
Exceptions during an iteration (e.g. transient API errors) are caught:
|
|
110
|
+
|
|
111
|
+
- **Authentication errors** are permanent — the run aborts immediately.
|
|
112
|
+
- Other errors roll back the tree, record the error in notes, and **retry** after
|
|
113
|
+
an interruptible exponential backoff (`60 × 2ⁿ` seconds), up to `--max-retries`
|
|
114
|
+
consecutive errors.
|
|
115
|
+
|
|
116
|
+
A `SIGINT`/`SIGTERM` (Ctrl-C) requests a graceful stop: the in-flight robot is
|
|
117
|
+
interrupted, the loop exits cleanly, and the exit summary still prints.
|
|
118
|
+
|
|
119
|
+
## Commit-failure repair
|
|
120
|
+
|
|
121
|
+
If a commit itself fails (e.g. a pre-commit hook rejects the change), the failure
|
|
122
|
+
is queued. The next iteration's prompt includes a **repair section** asking the
|
|
123
|
+
robot to fix the uncommitted changes first. The working tree is *not* reset while
|
|
124
|
+
a commit failure is pending, so the robot can address it.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
Next: [Cross-Iteration Memory](notes.md).
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Cross-Iteration Memory (`notes.md`)
|
|
2
|
+
|
|
3
|
+
Each iteration runs a **fresh** robot with no memory of previous iterations. The
|
|
4
|
+
only thing that carries forward is `notes.md` — a human-readable log the
|
|
5
|
+
orchestrator maintains and injects into every iteration's system prompt.
|
|
6
|
+
|
|
7
|
+
This is what lets the loop make progress instead of repeating the same attempt.
|
|
8
|
+
|
|
9
|
+
## Who writes it, who reads it
|
|
10
|
+
|
|
11
|
+
- **The orchestrator writes it.** After every iteration it appends an entry
|
|
12
|
+
(success, failure, verify-failure, or error).
|
|
13
|
+
- **The robot reads it.** The `PromptBuilder` embeds the current notes into the
|
|
14
|
+
system prompt under a "Prior Work" section.
|
|
15
|
+
- **The robot must not edit it.** The prompt explicitly tells the robot that
|
|
16
|
+
`notes.md` is maintained automatically. Writes are atomic (`AtomicFile`).
|
|
17
|
+
|
|
18
|
+
## Location
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
.robot_lab_to/runs/<run_id>/notes.md
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The path is also passed to the robot in the prompt so it can read the file
|
|
25
|
+
directly if it has file tools.
|
|
26
|
+
|
|
27
|
+
## Structure
|
|
28
|
+
|
|
29
|
+
The file starts with a header and grows one section per iteration:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
# robot-to run: 20260623-144554-3e5c27
|
|
33
|
+
|
|
34
|
+
Objective: Migrate the codebase from Minitest to RSpec
|
|
35
|
+
|
|
36
|
+
## Iteration Log
|
|
37
|
+
|
|
38
|
+
### Iteration 1
|
|
39
|
+
|
|
40
|
+
**Summary:** Converted spec_helper and the parser tests to RSpec
|
|
41
|
+
|
|
42
|
+
**Changes:**
|
|
43
|
+
- spec/spec_helper.rb
|
|
44
|
+
- spec/parser_spec.rb
|
|
45
|
+
|
|
46
|
+
**Learnings:**
|
|
47
|
+
- The project uses `assert_equal`; RSpec equivalent is `expect(x).to eq(y)`
|
|
48
|
+
|
|
49
|
+
### Iteration 2 [FAIL]
|
|
50
|
+
|
|
51
|
+
**Summary:** Attempted to convert the integration tests but broke the suite
|
|
52
|
+
|
|
53
|
+
**Learnings:**
|
|
54
|
+
- The integration tests depend on a Rack test helper not yet ported
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Entry types
|
|
58
|
+
|
|
59
|
+
| Marker | When | Includes |
|
|
60
|
+
|--------|------|----------|
|
|
61
|
+
| *(none)* | Successful, committed iteration | summary, changes, learnings |
|
|
62
|
+
| `[FAIL]` | Robot reported failure or didn't submit | summary, learnings |
|
|
63
|
+
| `[VERIFY FAILED]` | Robot claimed success but the eval's gate failed (`--verify-command` / `--floor`) | summary, gate output |
|
|
64
|
+
| `[NO IMPROVEMENT]` | Gate passed but the eval didn't rule it better than the parent commit (rolled back unless `--no-require-improvement`) | summary, score detail |
|
|
65
|
+
| `[ERROR]` | An exception was raised during the iteration | error class and message |
|
|
66
|
+
|
|
67
|
+
`[VERIFY FAILED]` and `[NO IMPROVEMENT]` come from the [Evals](evals.md) scoring
|
|
68
|
+
gate, not the robot — see there for how `gate_ok` and `improved` are decided.
|
|
69
|
+
|
|
70
|
+
Because failures and their learnings are recorded too, the next robot can avoid
|
|
71
|
+
repeating a dead end — the failure log is as valuable as the success log.
|
|
72
|
+
|
|
73
|
+
## Designing objectives around the notes
|
|
74
|
+
|
|
75
|
+
The notes are most effective when:
|
|
76
|
+
|
|
77
|
+
- The objective is a single, clear goal the robot can chip away at.
|
|
78
|
+
- Each iteration is genuinely incremental — "make ONE focused change" is baked
|
|
79
|
+
into the prompt.
|
|
80
|
+
- `key_learnings` capture *transferable* facts ("the build uses esbuild, not
|
|
81
|
+
webpack"), not restatements of the diff.
|
|
82
|
+
|
|
83
|
+
You can read the notes live during a run to watch the robot's reasoning evolve:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
tail -f .robot_lab_to/runs/*/notes.md
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
Next: [Stop Conditions](stop-conditions.md).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Run State & Event Log
|
|
2
|
+
|
|
3
|
+
Everything a run produces — besides the git commits — lives under a single run
|
|
4
|
+
directory. This page documents that layout and the JSONL event log.
|
|
5
|
+
|
|
6
|
+
## Run directory layout
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
.robot_lab_to/
|
|
10
|
+
└── runs/
|
|
11
|
+
└── <run_id>/ # e.g. 20260623-144554-3e5c27
|
|
12
|
+
├── notes.md # human-readable cross-iteration memory
|
|
13
|
+
├── run.log # JSONL event log (one event per line)
|
|
14
|
+
└── checkpoints/ # pre-edit file snapshots (local_guards only)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `<run_id>` is `YYYYMMDD-HHMMSS-<random hex>`.
|
|
18
|
+
- The whole `.robot_lab_to/` directory is added to `.git/info/exclude` at startup,
|
|
19
|
+
so it never appears in `git status` or commits.
|
|
20
|
+
- Change `--run-dir` to relocate it (default `.robot_lab_to`).
|
|
21
|
+
|
|
22
|
+
## The JSONL event log (`run.log`)
|
|
23
|
+
|
|
24
|
+
`run.log` contains one JSON object per line — a structured, append-only trace of
|
|
25
|
+
the run. Every line has an `event` name and a UTC `ts` timestamp; other keys
|
|
26
|
+
depend on the event. The logger buffers up to 100 events before the file is open,
|
|
27
|
+
so the very first setup events are never lost.
|
|
28
|
+
|
|
29
|
+
A typical successful iteration:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{"event":"orchestrator:start","ts":"2026-06-23T14:45:54.001-05:00","run_id":"20260623-144554-3e5c27","objective":"...","branch":"robot-to/...","model":"claude-sonnet-4-6"}
|
|
33
|
+
{"event":"iteration:start","ts":"...","iteration":1}
|
|
34
|
+
{"event":"agent:run:start","ts":"...","iteration":1}
|
|
35
|
+
{"event":"agent:run:end","ts":"...","iteration":1}
|
|
36
|
+
{"event":"eval","ts":"...","iteration":1,"gate":true,"improved":true,"value":null,"met_target":false,"detail":"verify=true"}
|
|
37
|
+
{"event":"commit:success","ts":"...","iteration":1,"message":"robot-to 1: ..."}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Event reference
|
|
41
|
+
|
|
42
|
+
| Event | Meaning |
|
|
43
|
+
|-------|---------|
|
|
44
|
+
| `orchestrator:start` | Run began. Carries `run_id`, `objective`, `branch`, `model`. |
|
|
45
|
+
| `orchestrator:resume` | A `--resume` re-entered the loop (`run_id`, `iteration`, `branch`). |
|
|
46
|
+
| `iteration:start` | A new iteration began. |
|
|
47
|
+
| `agent:run:start` / `agent:run:end` | The robot's `run` call started / finished. |
|
|
48
|
+
| `agent:nudge` | The robot didn't submit; re-asking (`attempt` N). |
|
|
49
|
+
| `agent:run:error` | An exception occurred during the iteration (`error`, `message`). |
|
|
50
|
+
| `backoff:start` / `backoff:end` | Retry backoff sleep around a transient error. |
|
|
51
|
+
| `eval` | The [eval's](evals.md) verdict for this iteration: `gate`, `improved`, `value`, `met_target`, `detail`. |
|
|
52
|
+
| `eval:repair` | R2 repair attempt after a gate failure (`attempt` N of `--max-verify-repairs`). |
|
|
53
|
+
| `eval:repair_error` | The repair attempt itself raised (`message`); repair loop stops. |
|
|
54
|
+
| `commit:success` | Iteration committed (`message`). |
|
|
55
|
+
| `commit:failed` | `git commit` failed; queued for repair (`output`). |
|
|
56
|
+
| `iteration:failure` | Robot reported failure or never submitted (`summary`). |
|
|
57
|
+
| `iteration:gate_failure` | Robot claimed success but the eval's gate failed (recorded in notes as `[VERIFY FAILED]`). |
|
|
58
|
+
| `iteration:no_improvement` | Gate passed but the eval didn't rule it an improvement (`detail`); rolled back. |
|
|
59
|
+
| `orchestrator:abort` | Run stopped by a stop condition, `met_target`, or permanent error (`reason`). |
|
|
60
|
+
| `orchestrator:fatal` | Unexpected fatal error (`error`, `message`). |
|
|
61
|
+
| `orchestrator:end` | Final totals: `iterations`, `commits`, `input_tokens`, `output_tokens`, `abort_reason`, `elapsed`. |
|
|
62
|
+
|
|
63
|
+
Decision-file events (`decision:blocking`, `decision:wait:start`,
|
|
64
|
+
`decision:wait:resolved`, `decision:raised`) are also written when decisions are
|
|
65
|
+
enabled — see the README's Human-in-the-Loop section.
|
|
66
|
+
|
|
67
|
+
### Analyzing a run
|
|
68
|
+
|
|
69
|
+
Because every line is JSON, the log is easy to query with
|
|
70
|
+
[`jq`](https://stedolan.github.io/jq/):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Watch events live, formatted
|
|
74
|
+
tail -f .robot_lab_to/runs/*/run.log | jq -r '"\(.ts) \(.event)"'
|
|
75
|
+
|
|
76
|
+
# How many iterations committed vs failed?
|
|
77
|
+
jq -r 'select(.event=="commit:success")' run.log | wc -l
|
|
78
|
+
jq -r 'select(.event=="iteration:failure") | .summary' run.log
|
|
79
|
+
|
|
80
|
+
# Final token usage
|
|
81
|
+
jq 'select(.event=="orchestrator:end") | {iterations,commits,input_tokens,output_tokens}' run.log
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Debug logging
|
|
85
|
+
|
|
86
|
+
By default, RubyLLM/RobotLab logging is suppressed so the console stays clean.
|
|
87
|
+
Pass `--debug` to keep verbose provider logging enabled (useful when diagnosing
|
|
88
|
+
provider or tool-call issues). The JSONL event log is always written regardless
|
|
89
|
+
of `--debug`.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
Next: [Configuration](../configuration/index.md).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Stop Conditions
|
|
2
|
+
|
|
3
|
+
Because `robot-to` is meant to run unattended, it must reliably stop on its own.
|
|
4
|
+
Five independent conditions can end a run; whichever trips first wins.
|
|
5
|
+
|
|
6
|
+
## The five conditions
|
|
7
|
+
|
|
8
|
+
| Condition | Flag | Checked | Effect |
|
|
9
|
+
|-----------|------|---------|--------|
|
|
10
|
+
| Max iterations | `--max-iterations N` | before & after each iteration | Stop after `N` iterations. |
|
|
11
|
+
| Max tokens | `--max-tokens N` | before & after each iteration (and mid-stream when streaming) | Stop once cumulative tokens reach `N`. |
|
|
12
|
+
| Consecutive failures | `--max-consecutive-failures N` | after each iteration | Abort after `N` failures in a row (default **3**). |
|
|
13
|
+
| Plateau | `--stop-on-plateau N` | after each iteration | Abort after `N` iterations with no committed improvement (eval-gated runs only). |
|
|
14
|
+
| Stop-when | `--stop-when "<text>"` | after each *successful* iteration | The robot judges the condition; sets `should_fully_stop`. |
|
|
15
|
+
|
|
16
|
+
There is a sixth, orchestrator-owned stop that isn't a `StopConditions` check at
|
|
17
|
+
all: an [eval's](evals.md) `met_target` — reaching a measured `--target` aborts
|
|
18
|
+
the run immediately on that iteration's commit, taking priority over
|
|
19
|
+
`should_fully_stop`. See [Evals](evals.md).
|
|
20
|
+
|
|
21
|
+
If you set none of the bounded limits, the loop relies on
|
|
22
|
+
`--max-consecutive-failures` (default 3) and `--stop-when` to terminate. For a
|
|
23
|
+
genuinely unattended run, **always set at least `--max-iterations` or
|
|
24
|
+
`--max-tokens`.**
|
|
25
|
+
|
|
26
|
+
## How each works
|
|
27
|
+
|
|
28
|
+
### Max iterations
|
|
29
|
+
|
|
30
|
+
A simple counter. The run aborts with `max iterations reached (N)` once
|
|
31
|
+
`run.iteration >= N`.
|
|
32
|
+
|
|
33
|
+
### Max tokens
|
|
34
|
+
|
|
35
|
+
Cumulative input + output tokens across all iterations are compared against the
|
|
36
|
+
limit.
|
|
37
|
+
|
|
38
|
+
- **Streaming runs** (the default) account tokens per chunk and can interrupt an
|
|
39
|
+
in-flight iteration the moment the budget is exhausted.
|
|
40
|
+
- **Non-streaming runs** (`--no-stream`, used for local models) account tokens
|
|
41
|
+
from each iteration's result and stop at the next iteration boundary. See
|
|
42
|
+
[Local Models](../local-models/ollama.md#streaming-and-tool-calls).
|
|
43
|
+
|
|
44
|
+
### Consecutive failures
|
|
45
|
+
|
|
46
|
+
Resets to zero on every successful, committed iteration; increments on every
|
|
47
|
+
failure, verify-failure, or commit-failure. This is the safety net that stops a
|
|
48
|
+
run that is going nowhere. The default is **3**.
|
|
49
|
+
|
|
50
|
+
### Plateau (no improvement)
|
|
51
|
+
|
|
52
|
+
`--stop-on-plateau N` aborts once `N` consecutive iterations pass the eval's gate
|
|
53
|
+
but fail to beat the parent commit's score (`iterations_since_improvement >= N`).
|
|
54
|
+
It resets to zero on every committed improvement.
|
|
55
|
+
|
|
56
|
+
This is the **primary terminator for `Evals::Prose`**, which never sets a
|
|
57
|
+
measurable `met_target` — a pairwise judge has no absolute bar to reach, only
|
|
58
|
+
"better than last time." It's distinct from `--max-consecutive-failures`:
|
|
59
|
+
plateau counts *valid, gate-passing work that just wasn't better* (including
|
|
60
|
+
"same" verdicts, which are common and normal for prose), while consecutive
|
|
61
|
+
failures counts the robot being genuinely broken (errors, gate failures, no
|
|
62
|
+
submit). Conflating the two would throttle legitimate exploration.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
robot-to "Write an opinionated guide to VSM" \
|
|
66
|
+
--eval prose --spec outline.md \
|
|
67
|
+
--stop-on-plateau 3 # stop after 3 drafts that didn't improve
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See [Evals](evals.md) for the full scoring model.
|
|
71
|
+
|
|
72
|
+
### Stop-when (natural language)
|
|
73
|
+
|
|
74
|
+
`--stop-when` is a goal the robot evaluates. After a successful iteration, the
|
|
75
|
+
robot decides whether the condition is met and, if so, returns
|
|
76
|
+
`should_fully_stop: true` in its result. The orchestrator then aborts with
|
|
77
|
+
`stop condition met: <text>`.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
robot-to "Refactor the auth module for clarity" \
|
|
81
|
+
--max-iterations 30 \
|
|
82
|
+
--stop-when "the auth module has no method longer than 15 lines and tests pass"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`--stop-when` only ends the loop on a **successful** iteration — a failing
|
|
86
|
+
iteration never triggers it, even if the robot thinks the goal is met.
|
|
87
|
+
|
|
88
|
+
## Graceful interruption
|
|
89
|
+
|
|
90
|
+
Pressing **Ctrl-C** (`SIGINT`) or sending `SIGTERM` requests a graceful stop: the
|
|
91
|
+
in-flight robot is interrupted, any pending backoff sleep is cancelled, the loop
|
|
92
|
+
exits, and the exit summary still prints. The current iteration's incomplete work
|
|
93
|
+
is rolled back.
|
|
94
|
+
|
|
95
|
+
## What you'll see
|
|
96
|
+
|
|
97
|
+
The exit summary header reports the reason:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
robot-to stopped — claude-sonnet-4-6 — 41m 08s
|
|
101
|
+
Reason: max iterations reached (30)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Possible reasons include `max iterations reached (N)`, `max tokens reached (N)`,
|
|
105
|
+
`N consecutive failures`, `plateau: N iterations without improvement`,
|
|
106
|
+
`target met: <detail>`, `stop condition met: <text>`, a permanent error, or a
|
|
107
|
+
fatal error.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
Next: [Verification Gate](verification.md).
|