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.
@@ -11,14 +11,9 @@ module RobotLab
11
11
  end
12
12
 
13
13
  def print
14
- stat = @run.base_commit ? diff_stat : { insertions: 0, deletions: 0, files: 0 }
15
- aborted = !@abort_reason.nil?
16
- good_iters = @run.commits
17
- fail_iters = @run.iteration - good_iters
18
-
19
14
  puts ""
20
- print_header(aborted)
21
- print_counters(good_iters, fail_iters, stat)
15
+ @abort_reason ? print_aborted_header : print_completed_header
16
+ print_counters
22
17
  print_paths
23
18
  print_next_steps
24
19
  puts ""
@@ -26,17 +21,21 @@ module RobotLab
26
21
 
27
22
  private
28
23
 
29
- def print_header(aborted)
30
- if aborted
31
- puts "robot-to stopped — #{@config.model} — #{@run.elapsed_human}"
32
- puts "Reason: #{@abort_reason}"
33
- else
34
- puts "robot-to complete — #{@config.model} — #{@run.elapsed_human}"
35
- puts "Branch: #{@run.branch}"
36
- end
24
+ def print_aborted_header
25
+ puts "robot-to stopped — #{@config.model} — #{@run.elapsed_human}"
26
+ puts "Reason: #{@abort_reason}"
27
+ end
28
+
29
+ def print_completed_header
30
+ puts "robot-to complete — #{@config.model} — #{@run.elapsed_human}"
31
+ puts "Branch: #{@run.branch}"
37
32
  end
38
33
 
39
- def print_counters(good_iters, fail_iters, stat)
34
+ def print_counters
35
+ good_iters = @run.commits
36
+ fail_iters = @run.iteration - good_iters
37
+ stat = diff_stat
38
+
40
39
  puts ""
41
40
  puts "Iterations: #{@run.iteration} total (#{good_iters} good / #{fail_iters} failed)"
42
41
  puts "Tokens: #{comma(@run.total_tokens)} (#{comma(@run.input_tokens)} in / #{comma(@run.output_tokens)} out)"
@@ -61,6 +60,8 @@ module RobotLab
61
60
  end
62
61
 
63
62
  def diff_stat
63
+ return { insertions: 0, deletions: 0, files: 0 } unless @run.base_commit
64
+
64
65
  git = CommitManager.new
65
66
  git.diff_stat(@run.base_commit)
66
67
  rescue StandardError
@@ -48,7 +48,12 @@ module RobotLab
48
48
 
49
49
  tracked << path
50
50
  FileUtils.mkdir_p(dir)
51
- dest = File.join(dir, safe_name(path))
51
+ write_backup(path, File.join(dir, safe_name(path)))
52
+ rescue SystemCallError, IOError
53
+ nil
54
+ end
55
+
56
+ def write_backup(path, dest)
52
57
  if File.exist?(path)
53
58
  FileUtils.cp(path, dest)
54
59
  dest
@@ -56,8 +61,6 @@ module RobotLab
56
61
  File.write("#{dest}.absent", "")
57
62
  "#{dest}.absent"
58
63
  end
59
- rescue SystemCallError, IOError
60
- nil
61
64
  end
62
65
 
63
66
  # @return [String] directory for this run's checkpoints (created)
@@ -73,8 +73,9 @@ module RobotLab
73
73
  return :empty_response if text.to_s.strip.empty? && calls.empty?
74
74
 
75
75
  calls.each do |c|
76
- return :empty_tool_name if c[:name].to_s.empty?
77
- return "unknown_tool:#{c[:name]}" if known.any? && !known.include?(c[:name])
76
+ name = c[:name]
77
+ return :empty_tool_name if name.to_s.empty?
78
+ return "unknown_tool:#{name}" if known.any? && !known.include?(name)
78
79
  end
79
80
 
80
81
  return :repeated_tool_call if repeated?(calls, previous)
@@ -83,6 +84,7 @@ module RobotLab
83
84
  end
84
85
 
85
86
  # True when any current call exactly matches any previous call.
87
+ # :reek:NestedIterators -- an all-pairs comparison needs both loops.
86
88
  def repeated?(calls, previous)
87
89
  return false if calls.empty? || previous.empty?
88
90
 
@@ -22,8 +22,10 @@ module RobotLab
22
22
 
23
23
  # Fetch the value for `key`, initializing to `default` when unset.
24
24
  def fetch(key, default = nil)
25
- Thread.current[key] = default if Thread.current[key].nil? && !default.nil?
26
- Thread.current[key]
25
+ value = Thread.current[key]
26
+ return value unless value.nil?
27
+
28
+ Thread.current[key] = default
27
29
  end
28
30
  end
29
31
  end
@@ -13,6 +13,7 @@ module RobotLab
13
13
  @path = Pathname.new(path)
14
14
  end
15
15
 
16
+ # :reek:FeatureEnvy -- interpolating the run's own fields into the header.
16
17
  def setup(run)
17
18
  AtomicFile.write(@path, <<~HEADER)
18
19
  # robot-to run: #{run.run_id}
@@ -84,6 +85,7 @@ module RobotLab
84
85
  MD
85
86
  end
86
87
 
88
+ # :reek:FeatureEnvy -- interpolating the error's own class/message.
87
89
  def append_error(error, iteration)
88
90
  append(<<~MD)
89
91
 
@@ -93,6 +95,7 @@ module RobotLab
93
95
  MD
94
96
  end
95
97
 
98
+ # :reek:FeatureEnvy -- interpolating the decision's own fields.
96
99
  def append_decision(decision, iteration)
97
100
  append(<<~MD)
98
101
 
@@ -14,6 +14,17 @@ module RobotLab
14
14
  # not make alone, it is written to a decision file (see DecisionManager). A
15
15
  # blocking decision either pauses the loop (decision_mode: wait) or stops the
16
16
  # run for a later `--resume` (decision_mode: exit).
17
+ #
18
+ # :reek:TooManyMethods :reek:TooManyInstanceVariables -- this is the
19
+ # single object that owns the whole iteration lifecycle (setup, agent
20
+ # turn, eval gate, commit/rollback, decisions, tokens, signals). Splitting
21
+ # it apart would scatter state (@run, @git, @pending_commit_failure,
22
+ # @stop_requested, ...) across collaborators that would just pass it back
23
+ # and forth -- see the "Architecture" section of CLAUDE.md.
24
+ # :reek:RepeatedConditional -- @stop_requested, @pending_commit_failure,
25
+ # and decisions_enabled? are real orchestrator-wide state, legitimately
26
+ # checked at each point that must react to it; that is what a run loop's
27
+ # flags are for, not a sign the checks belong on another object.
17
28
  class Orchestrator
18
29
  SIGNAL_STOP = "graceful_stop"
19
30
 
@@ -43,10 +54,17 @@ module RobotLab
43
54
  @decisions = nil
44
55
  @decision_tool = nil
45
56
  @injected_decisions = []
57
+ @backoff = nil
58
+ @builder = nil
59
+ @notes = nil
60
+ @stop_conditions = nil
46
61
  @accounted_in = 0
47
62
  @accounted_out = 0
48
63
  end
49
64
 
65
+ # :reek:TooManyStatements -- top-level run/rescue/ensure sequence for the
66
+ # whole process; the rescue branches are already split into their own
67
+ # error-shaped clauses.
50
68
  def run
51
69
  @resume_run_id ? setup_resume : setup_run
52
70
  install_signal_handlers
@@ -64,15 +82,18 @@ module RobotLab
64
82
  @git&.reset_hard unless @pending_commit_failure
65
83
  @logger.log("orchestrator:abort", reason: @abort_reason, permanent: true)
66
84
  rescue => e
67
- @abort_reason = "fatal: #{e.message}"
85
+ message = e.message
86
+ @abort_reason = "fatal: #{message}"
68
87
  @git&.reset_hard unless @pending_commit_failure
69
- @logger.log("orchestrator:fatal", error: e.class.to_s, message: e.message)
88
+ @logger.log("orchestrator:fatal", error: e.class.to_s, message: message)
70
89
  ensure
71
90
  finalize_run
72
91
  end
73
92
 
74
93
  private
75
94
 
95
+ # :reek:TooManyStatements -- the loop body is the iteration lifecycle in
96
+ # its literal execution order; splitting it further would hide that order.
76
97
  def main_loop
77
98
  loop do
78
99
  break if @stop_requested
@@ -83,8 +104,9 @@ module RobotLab
83
104
  @stop_conditions.before?
84
105
 
85
106
  @run.iteration += 1
86
- @logger.log("iteration:start", iteration: @run.iteration)
87
- progress "iteration #{@run.iteration} (#{@config.model})..."
107
+ iteration = @run.iteration
108
+ @logger.log("iteration:start", iteration: iteration)
109
+ progress "iteration #{iteration} (#{@config.model})..."
88
110
 
89
111
  result = execute_iteration
90
112
 
@@ -114,6 +136,8 @@ module RobotLab
114
136
  end
115
137
  end
116
138
 
139
+ # :reek:TooManyStatements -- one-time run bootstrap, each step required
140
+ # in this order (branch, run dir, Run record, logger, notes, collaborators).
117
141
  def setup_run
118
142
  @git = CommitManager.new
119
143
  ensure_head!
@@ -143,6 +167,8 @@ module RobotLab
143
167
 
144
168
  # Resume a prior run from its run.json — enables external scheduling (cron):
145
169
  # each tick re-enters the loop on the same branch and advances or re-pauses.
170
+ # :reek:TooManyStatements -- mirrors #setup_run's bootstrap order for the
171
+ # resume path.
146
172
  def setup_resume
147
173
  @git = CommitManager.new
148
174
  ensure_head!
@@ -152,7 +178,8 @@ module RobotLab
152
178
 
153
179
  @run = Run.load(state_path)
154
180
  @objective = @run.objective
155
- @git.checkout_branch(@run.branch)
181
+ branch = @run.branch
182
+ @git.checkout_branch(branch)
156
183
 
157
184
  @logger.open(@run.log_path)
158
185
  @notes = NotesManager.new(@run.notes_path) # append — do NOT re-setup (would clobber history)
@@ -160,8 +187,8 @@ module RobotLab
160
187
  build_collaborators
161
188
 
162
189
  @logger.log("orchestrator:resume", run_id: @run.run_id, iteration: @run.iteration,
163
- branch: @run.branch, model: @config.model)
164
- progress "resumed run #{@run.run_id} at iteration #{@run.iteration} → branch #{@run.branch}"
190
+ branch: branch, model: @config.model)
191
+ progress "resumed run #{@run.run_id} at iteration #{@run.iteration} → branch #{branch}"
165
192
  end
166
193
 
167
194
  # Shared setup used by both a fresh run and a resume.
@@ -202,11 +229,14 @@ module RobotLab
202
229
 
203
230
  # Poll the decision files until every blocking decision is resolved, the run
204
231
  # is signaled to stop, or decision_timeout elapses.
232
+ # :reek:TooManyStatements -- the three exit conditions (stopped, resolved,
233
+ # timed out) must all be checked inside one poll loop.
205
234
  def wait_for_decisions(pending)
206
235
  progress "awaiting human decision on #{pending.size} item(s):"
207
236
  pending.each { |d| progress " #{d.path}" }
208
237
  @logger.log("decision:wait:start", count: pending.size)
209
- deadline = @config.decision_timeout ? monotonic + @config.decision_timeout.to_i : nil
238
+ timeout = @config.decision_timeout
239
+ deadline = timeout ? monotonic + timeout.to_i : nil
210
240
 
211
241
  loop do
212
242
  return if @stop_requested
@@ -216,7 +246,7 @@ module RobotLab
216
246
  break unless @decisions.blocking_pending?
217
247
 
218
248
  if deadline && monotonic >= deadline
219
- raise AbortError, "awaiting human decision (timed out after #{@config.decision_timeout}s)"
249
+ raise AbortError, "awaiting human decision (timed out after #{timeout}s)"
220
250
  end
221
251
  end
222
252
 
@@ -238,6 +268,8 @@ module RobotLab
238
268
 
239
269
  # The happy path: build a fresh robot, run it, and return its submitted
240
270
  # result (or a not-submitted marker).
271
+ # :reek:TooManyStatements -- one call per lifecycle step (reset counters,
272
+ # build prompt/robot, run, nudge, persist decisions, return result).
241
273
  def run_agent_iteration
242
274
  @accounted_in = @accounted_out = 0
243
275
  @submit_tool = Tools::SubmitResult.new
@@ -265,12 +297,13 @@ module RobotLab
265
297
  def persist_raised_decisions
266
298
  return unless @decision_tool
267
299
 
300
+ iteration = @run.iteration
268
301
  @decision_tool.captured_requests.each do |req|
269
- decision = @decisions.record(**req, iteration: @run.iteration)
270
- @notes.append_decision(decision, @run.iteration)
302
+ decision = @decisions.record(**req, iteration: iteration)
303
+ @notes.append_decision(decision, iteration)
271
304
  @logger.log("decision:raised", id: decision.id, blocking: decision.blocking,
272
305
  question: decision.question)
273
- progress "iteration #{@run.iteration} raised decision: #{decision.question}"
306
+ progress "iteration #{iteration} raised decision: #{decision.question}"
274
307
  end
275
308
  end
276
309
 
@@ -298,10 +331,11 @@ module RobotLab
298
331
  # True when the iteration should be retried — within the retry budget and
299
332
  # not stopping. Sleeps out the backoff as a side effect before returning.
300
333
  def retry_after_backoff?
301
- return false unless @run.consecutive_errors <= @config.max_retries && !@stop_requested
334
+ errors = @run.consecutive_errors
335
+ return false unless errors <= @config.max_retries && !@stop_requested
302
336
 
303
- @logger.log("backoff:start", consecutive_errors: @run.consecutive_errors)
304
- @backoff.sleep_for(@run.consecutive_errors)
337
+ @logger.log("backoff:start", consecutive_errors: errors)
338
+ @backoff.sleep_for(errors)
305
339
  @logger.log("backoff:end")
306
340
  true
307
341
  end
@@ -367,8 +401,9 @@ module RobotLab
367
401
 
368
402
  def handle_no_improvement(result, score)
369
403
  iteration = @run.iteration
404
+ detail = score.detail
370
405
  @git.reset_hard unless @pending_commit_failure
371
- @notes.append_no_improvement(result, score.detail, iteration)
406
+ @notes.append_no_improvement(result, detail, iteration)
372
407
  # A non-improving iteration is valid work that just wasn't better -- NOT a
373
408
  # failure. It counts toward the plateau (diminishing-returns) stop only, so
374
409
  # it must NOT trip max_consecutive_failures, which is for the robot being
@@ -377,8 +412,8 @@ module RobotLab
377
412
  # verdicts are common and normal.
378
413
  @run.iterations_since_improvement += 1
379
414
  @run.consecutive_errors = 0
380
- @logger.log("iteration:no_improvement", iteration: iteration, detail: score.detail)
381
- progress "iteration #{iteration} no improvement (#{score.detail}) — rolled back"
415
+ @logger.log("iteration:no_improvement", iteration: iteration, detail: detail)
416
+ progress "iteration #{iteration} no improvement (#{detail}) — rolled back"
382
417
  end
383
418
 
384
419
  # A resolved decision that was injected into this successful iteration has
@@ -405,14 +440,16 @@ module RobotLab
405
440
  score
406
441
  end
407
442
 
443
+ # :reek:TooManyStatements -- rollback + record + report, one statement each.
408
444
  def handle_gate_failure(result, score)
445
+ iteration = @run.iteration
409
446
  @git.reset_hard unless @pending_commit_failure
410
- @notes.append_verify_failure(result, score.output.to_s, @run.iteration)
447
+ @notes.append_verify_failure(result, score.output.to_s, iteration)
411
448
  @run.consecutive_failures += 1
412
449
  @run.consecutive_errors = 0
413
450
  @run.iterations_since_improvement += 1
414
- @logger.log("iteration:gate_failure", iteration: @run.iteration)
415
- progress "iteration #{@run.iteration} verification failed — rolled back"
451
+ @logger.log("iteration:gate_failure", iteration: iteration)
452
+ progress "iteration #{iteration} verification failed — rolled back"
416
453
  nil
417
454
  end
418
455
 
@@ -421,19 +458,22 @@ module RobotLab
421
458
  # the code, re-verifying after each attempt. Only fall back to a full
422
459
  # rollback once the repair budget is spent — a near-miss no longer discards
423
460
  # all the work, and nothing commits until the gate is green.
461
+ # :reek:TooManyStatements -- repair/re-verify loop with its own rescue;
462
+ # each statement is a distinct step of one repair attempt.
424
463
  def repair_until_gate(result, score)
425
- budget = @config.max_verify_repairs.to_i
464
+ budget = @config.max_verify_repairs.to_i
465
+ iteration = @run.iteration
426
466
  return [result, score] if budget.zero? || @robot.nil?
427
467
 
428
468
  budget.times do |i|
429
469
  break if @stop_requested
430
470
 
431
- @logger.log("eval:repair", iteration: @run.iteration, attempt: i + 1)
432
- progress "iteration #{@run.iteration}: verification failed — repairing (#{i + 1}/#{budget})"
471
+ @logger.log("eval:repair", iteration: iteration, attempt: i + 1)
472
+ progress "iteration #{iteration}: verification failed — repairing (#{i + 1}/#{budget})"
433
473
  begin
434
474
  run_robot_with_interrupt(@robot, repair_prompt(score))
435
475
  rescue StandardError => e
436
- @logger.log("eval:repair_error", iteration: @run.iteration, message: e.message)
476
+ @logger.log("eval:repair_error", iteration: iteration, message: e.message)
437
477
  break
438
478
  end
439
479
  result = @submit_tool.captured_result || result
@@ -459,16 +499,20 @@ module RobotLab
459
499
  end
460
500
 
461
501
  def handle_failure(result)
502
+ iteration = @run.iteration
462
503
  @git.reset_hard unless @pending_commit_failure
463
- @notes.append_failure(result, @run.iteration)
504
+ @notes.append_failure(result, iteration)
464
505
  @run.consecutive_failures += 1
465
506
  @run.consecutive_errors = 0
466
507
  @run.iterations_since_improvement += 1
467
- @logger.log("iteration:failure", iteration: @run.iteration, summary: result.summary)
468
- progress "iteration #{@run.iteration} failed: #{result.summary}"
508
+ @logger.log("iteration:failure", iteration: iteration, summary: result.summary)
509
+ progress "iteration #{iteration} failed: #{result.summary}"
469
510
  end
470
511
 
512
+ # :reek:TooManyStatements -- commit path plus its own CommitFailedError
513
+ # rescue; the two branches (success, queue-for-repair) are each tight.
471
514
  def attempt_commit(result)
515
+ iteration = @run.iteration
472
516
  @git.add_all
473
517
  return unless @git.staged?
474
518
 
@@ -476,16 +520,17 @@ module RobotLab
476
520
  @git.commit(message)
477
521
  @run.commits += 1
478
522
  @pending_commit_failure = nil
479
- @logger.log("commit:success", iteration: @run.iteration, message: message)
480
- progress "iteration #{@run.iteration} committed: #{message}"
523
+ @logger.log("commit:success", iteration: iteration, message: message)
524
+ progress "iteration #{iteration} committed: #{message}"
481
525
  rescue CommitFailedError => e
482
526
  @pending_commit_failure = e
483
527
  @run.consecutive_failures += 1
484
528
  @run.consecutive_errors = 0
485
- @logger.log("commit:failed", iteration: @run.iteration, output: e.output)
486
- progress "iteration #{@run.iteration} commit failed — queued for repair"
529
+ @logger.log("commit:failed", iteration: iteration, output: e.output)
530
+ progress "iteration #{iteration} commit failed — queued for repair"
487
531
  end
488
532
 
533
+ # :reek:FeatureEnvy -- formatting the commit message from result's own fields.
489
534
  def commit_message(result)
490
535
  if @config.commit_format == "conventional"
491
536
  type = result.respond_to?(:type) ? (result.type || "chore") : "chore"
@@ -565,6 +610,8 @@ module RobotLab
565
610
  end
566
611
  end
567
612
 
613
+ # :reek:TooManyStatements -- runs the robot on its own Thread so a signal
614
+ # can interrupt it; each statement manages that thread's lifecycle.
568
615
  def run_robot_with_interrupt(robot, task = @objective)
569
616
  thread_error = nil
570
617
  result = nil
@@ -8,6 +8,8 @@ module RobotLab
8
8
  @config = config
9
9
  end
10
10
 
11
+ # :reek:TooManyStatements -- each line is one optional prompt section, in
12
+ # the exact order they appear; a data-table would hide that ordering.
11
13
  def build(run, notes_content, workspace: nil, pending_commit_failure: nil, resolved_decisions: [])
12
14
  sections = [role_section(run), notes_section(run, notes_content)]
13
15
  sections << workspace_section(workspace) if workspace && !workspace.empty?
@@ -13,6 +13,8 @@ module RobotLab
13
13
  :commits, :input_tokens, :output_tokens,
14
14
  :last_score_value, :iterations_since_improvement
15
15
 
16
+ # :reek:ControlParameter -- run_dir/decisions_path fall back to a sibling
17
+ # path when omitted; that's a constructor default, not a behavior branch.
16
18
  def initialize(run_id:, objective:, branch:, base_commit:, notes_path:, log_path:,
17
19
  run_dir: nil, decisions_path: nil)
18
20
  @run_id = run_id
@@ -38,6 +40,7 @@ module RobotLab
38
40
  def elapsed_seconds = Time.now - started_at
39
41
  def state_path = @run_dir.join("run.json")
40
42
 
43
+ # :reek:FeatureEnvy -- decomposing our own elapsed_seconds into h/m/s.
41
44
  def elapsed_human
42
45
  secs = elapsed_seconds.to_i
43
46
  h = secs / 3600
@@ -71,14 +74,11 @@ module RobotLab
71
74
  base_commit: data[:base_commit], notes_path: data[:notes_path],
72
75
  log_path: data[:log_path], run_dir: data[:run_dir],
73
76
  decisions_path: data[:decisions_path])
74
- run.iteration = data[:iteration].to_i
75
- run.consecutive_failures = data[:consecutive_failures].to_i
76
- run.consecutive_errors = data[:consecutive_errors].to_i
77
- run.commits = data[:commits].to_i
78
- run.input_tokens = data[:input_tokens].to_i
79
- run.output_tokens = data[:output_tokens].to_i
80
- run.last_score_value = data[:last_score_value]
81
- run.iterations_since_improvement = data[:iterations_since_improvement].to_i
77
+ %i[iteration consecutive_failures consecutive_errors commits input_tokens
78
+ output_tokens iterations_since_improvement].each do |key|
79
+ run.public_send(:"#{key}=", data[key].to_i)
80
+ end
81
+ run.last_score_value = data[:last_score_value]
82
82
  run.instance_variable_set(:@started_at, Time.parse(data[:started_at])) if data[:started_at]
83
83
  run
84
84
  end
@@ -3,6 +3,8 @@
3
3
  module RobotLab
4
4
  module To
5
5
  # Evaluates all stop conditions against the current run state.
6
+ # :reek:RepeatedConditional -- each `limit` is an unrelated config value
7
+ # (max_iterations/max_tokens/stop_on_plateau) nil-guarded in its own check.
6
8
  class StopConditions
7
9
  def initialize(config, run)
8
10
  @config = config
@@ -38,17 +40,19 @@ module RobotLab
38
40
  private
39
41
 
40
42
  def check_max_iterations
41
- return unless @config.max_iterations
42
- return unless @run.iteration >= @config.max_iterations
43
+ limit = @config.max_iterations
44
+ return unless limit
45
+ return unless @run.iteration >= limit
43
46
 
44
- raise AbortError, "max iterations reached (#{@config.max_iterations})"
47
+ raise AbortError, "max iterations reached (#{limit})"
45
48
  end
46
49
 
47
50
  def check_max_tokens
48
- return unless @config.max_tokens
49
- return unless @run.total_tokens >= @config.max_tokens
51
+ limit = @config.max_tokens
52
+ return unless limit
53
+ return unless @run.total_tokens >= limit
50
54
 
51
- raise AbortError, "max tokens reached (#{@config.max_tokens})"
55
+ raise AbortError, "max tokens reached (#{limit})"
52
56
  end
53
57
 
54
58
  def check_consecutive_failures
@@ -25,6 +25,8 @@ module RobotLab
25
25
  @default_timeout = timeout
26
26
  end
27
27
 
28
+ # :reek:ControlParameter -- falling back to the instance default when
29
+ # the LLM omits the optional `timeout` argument.
28
30
  def execute(command:, timeout: nil, **)
29
31
  out, status = run(command, (timeout || @default_timeout).to_i)
30
32
  format_result(out, status)
@@ -34,6 +36,9 @@ module RobotLab
34
36
  # status_string is "0".."n" for an exit code, or "timeout"/"error: ...".
35
37
  #
36
38
  # @return [Array(String, String)]
39
+ # :reek:TooManyStatements :reek:NestedIterators -- spawn, read
40
+ # concurrently, wait-or-kill, join: each step is one statement and the
41
+ # streaming reader inherently nests a loop inside its own Thread.
37
42
  def run(command, timeout)
38
43
  out_str = +""
39
44
  status = nil
@@ -19,6 +19,11 @@ module RobotLab
19
19
  param :new_text, type: "string", desc: "Replacement text"
20
20
  param :replace_all, type: "boolean", desc: "Replace every occurrence", required: false
21
21
 
22
+ # :reek:BooleanParameter -- replace_all is a tool param the LLM sets;
23
+ # the `param` declarations above fix this signature.
24
+ # :reek:FeatureEnvy -- inspecting #apply's own return value.
25
+ # :reek:TooManyStatements -- validate, transform, write, report; each
26
+ # step is one statement in a tool #execute contract method.
22
27
  def execute(path:, old_text:, new_text:, replace_all: false, **)
23
28
  resolved = File.expand_path(path, Dir.pwd)
24
29
  return "Error: file not found: #{path}" unless File.file?(resolved)
@@ -44,6 +44,10 @@ module RobotLab
44
44
  @captured_requests ||= []
45
45
  end
46
46
 
47
+ # :reek:BooleanParameter :reek:ControlParameter -- blocking is both
48
+ # stored data and legitimately changes the guidance message returned
49
+ # to the robot; the `param` declarations above fix this signature.
50
+ # :reek:LongParameterList -- one field per tool param declared above.
47
51
  def execute(question:, situation: "", options: [], recommendation: "", blocking: false, **)
48
52
  (@captured_requests ||= []) << {
49
53
  question: question,
@@ -34,6 +34,7 @@ module RobotLab
34
34
 
35
35
  attr_reader :captured_result
36
36
 
37
+ # :reek:LongParameterList -- one field per tool param declared above.
37
38
  def execute(success:, summary:, key_changes: [], key_learnings: [], should_fully_stop: nil, **)
38
39
  @captured_result = IterationResult.new(
39
40
  success: success,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobotLab
4
4
  module To
5
- VERSION = "0.2.7"
5
+ VERSION = "0.2.8"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot_lab-to
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -57,6 +57,7 @@ files:
57
57
  - ".loki"
58
58
  - ".quality/reek_baseline.txt"
59
59
  - ".rubocop.yml"
60
+ - Archspec.rb
60
61
  - CHANGELOG.md
61
62
  - CLAUDE.md
62
63
  - COMMITS.md
@@ -85,6 +86,7 @@ files:
85
86
  - docs/local-models/ollama.md
86
87
  - docs/local-models/tools.md
87
88
  - docs/reference/architecture.md
89
+ - examples/.envrc
88
90
  - examples/01_basic_usage/.gitignore
89
91
  - examples/01_basic_usage/README.md
90
92
  - examples/01_basic_usage/basic_usage.rb
@@ -104,6 +106,7 @@ files:
104
106
  - examples/04_prose/prompts_dir/section_criteria.md
105
107
  - examples/04_prose/prompts_dir/sections_objective.md
106
108
  - examples/04_prose/prose_run.rb
109
+ - examples/common.rb
107
110
  - lib/robot_lab/to.rb
108
111
  - lib/robot_lab/to/atomic_file.rb
109
112
  - lib/robot_lab/to/backoff.rb
@@ -169,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
172
  - !ruby/object:Gem::Version
170
173
  version: '0'
171
174
  requirements: []
172
- rubygems_version: 4.0.19
175
+ rubygems_version: 4.0.20
173
176
  specification_version: 4
174
177
  summary: Autonomous overnight agent loop for RobotLab — run robots while you sleep.
175
178
  test_files: []