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,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_resolution"
|
|
4
|
+
require_relative "run_store"
|
|
5
|
+
|
|
6
|
+
module RobotLab
|
|
7
|
+
module To
|
|
8
|
+
module Guards
|
|
9
|
+
# Read-before-edit invariant, ported from little-coder read-guard-edit.
|
|
10
|
+
#
|
|
11
|
+
# Small models fire Edit with an oldText they never actually saw, guessing
|
|
12
|
+
# the file's contents — which either fails the exact-match (wasted turn) or
|
|
13
|
+
# matches the wrong span (silent corruption). We block any Edit whose
|
|
14
|
+
# target wasn't Read this run.
|
|
15
|
+
#
|
|
16
|
+
# The read-set spans many tool calls within one run, so it lives in the
|
|
17
|
+
# per-run RunStore (see that file for why ctx.local can't hold it). A
|
|
18
|
+
# successful Read, Edit, or Write marks the path as known.
|
|
19
|
+
class ReadBeforeEdit < RobotLab::Hook
|
|
20
|
+
self.namespace = :read_before_edit
|
|
21
|
+
|
|
22
|
+
KEY = :robot_lab_to_read_files
|
|
23
|
+
TRACKED_READS = %w[read edit write].freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Fresh run = clean slate; a prior iteration's reads say nothing here.
|
|
27
|
+
def before_run(_ctx)
|
|
28
|
+
RunStore.reset(KEY, [])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Record successful reads/authored files as "known".
|
|
32
|
+
def after_tool_call(ctx)
|
|
33
|
+
return if ctx.tool_error
|
|
34
|
+
return unless TRACKED_READS.include?(ctx.tool_name.to_s.downcase)
|
|
35
|
+
|
|
36
|
+
path = PathResolution.resolve(ctx.tool_args)
|
|
37
|
+
mark_read(path) if path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Block edits to files that were never read.
|
|
41
|
+
def around_tool_call(ctx)
|
|
42
|
+
return yield unless ctx.tool_name.to_s.casecmp?("edit")
|
|
43
|
+
|
|
44
|
+
path = PathResolution.resolve(ctx.tool_args)
|
|
45
|
+
if path && !read?(path)
|
|
46
|
+
ctx.tool_result = edit_before_read_reason(path)
|
|
47
|
+
return ctx.tool_result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
yield
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] whether `path` has been read this run
|
|
54
|
+
def read?(path)
|
|
55
|
+
read_files.include?(path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mark_read(path)
|
|
59
|
+
files = read_files
|
|
60
|
+
files << path unless files.include?(path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Array<String>] the per-run read-set (live reference)
|
|
64
|
+
def read_files
|
|
65
|
+
RunStore.fetch(KEY, [])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param resolved [String]
|
|
69
|
+
# @return [String]
|
|
70
|
+
def edit_before_read_reason(resolved)
|
|
71
|
+
<<~MSG
|
|
72
|
+
File must be read before edit — #{resolved} has not been read in
|
|
73
|
+
this session. Read #{resolved} first to get the exact current text
|
|
74
|
+
for oldText (whitespace and indentation must match exactly), then
|
|
75
|
+
issue the Edit. Include 2-3 surrounding lines so oldText is unique.
|
|
76
|
+
Do NOT guess the file's contents.
|
|
77
|
+
MSG
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module To
|
|
5
|
+
module Guards
|
|
6
|
+
# Per-run shared state for the guards.
|
|
7
|
+
#
|
|
8
|
+
# RobotLab creates a fresh tool-call HookContext per call with its own
|
|
9
|
+
# ExtensionState, so cross-call guard state (the read-set, checkpoint-set,
|
|
10
|
+
# loop tracker) cannot live in ctx.local. robot_lab-to runs each iteration
|
|
11
|
+
# in its own thread (Orchestrator#run_robot_with_interrupt), so a
|
|
12
|
+
# thread-local is a clean per-run scope — the same idiom robot_lab-audit
|
|
13
|
+
# uses for run ids. before_run resets it; the thread ends with the
|
|
14
|
+
# iteration, so nothing leaks across runs.
|
|
15
|
+
module RunStore
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Set/replace the value for `key`.
|
|
19
|
+
def reset(key, value)
|
|
20
|
+
Thread.current[key] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fetch the value for `key`, initializing to `default` when unset.
|
|
24
|
+
def fetch(key, default = nil)
|
|
25
|
+
Thread.current[key] = default if Thread.current[key].nil? && !default.nil?
|
|
26
|
+
Thread.current[key]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "path_resolution"
|
|
4
|
+
|
|
5
|
+
module RobotLab
|
|
6
|
+
module To
|
|
7
|
+
module Guards
|
|
8
|
+
# Refuses Write on a file that already exists, redirecting the model to
|
|
9
|
+
# Edit, and normalizes root-bare paths to cwd. Ported from little-coder
|
|
10
|
+
# write-guard: small models rewrite whole files and destroy content; the
|
|
11
|
+
# whitepaper notes this refusal fires on ~57% of Polyglot exercises.
|
|
12
|
+
#
|
|
13
|
+
# Tool names matched (case-insensitive): "write".
|
|
14
|
+
class WriteGuard < RobotLab::Hook
|
|
15
|
+
self.namespace = :write_guard
|
|
16
|
+
|
|
17
|
+
WRITE_TOOLS = %w[write].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Normalize the path in place before the tool runs (applies even when
|
|
21
|
+
# we don't block — e.g. the "/foo.md" -> cwd rewrite).
|
|
22
|
+
def before_tool_call(ctx)
|
|
23
|
+
return unless write_tool?(ctx.tool_name)
|
|
24
|
+
|
|
25
|
+
args = ctx.tool_args
|
|
26
|
+
resolved = PathResolution.resolve(args)
|
|
27
|
+
return unless resolved
|
|
28
|
+
|
|
29
|
+
key = PathResolution::PATH_KEYS.find { |k| args.key?(k) || args.key?(k.to_sym) }
|
|
30
|
+
args[key] = resolved if key
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Block the write when the target already exists.
|
|
34
|
+
def around_tool_call(ctx)
|
|
35
|
+
unless write_tool?(ctx.tool_name)
|
|
36
|
+
return yield
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
resolved = PathResolution.resolve(ctx.tool_args)
|
|
40
|
+
if resolved && File.exist?(resolved)
|
|
41
|
+
ctx.tool_result = edit_recipe(resolved)
|
|
42
|
+
return ctx.tool_result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
yield
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param tool_name [String]
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def write_tool?(tool_name)
|
|
51
|
+
WRITE_TOOLS.include?(tool_name.to_s.downcase)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The message returned to the model in place of the refused write.
|
|
55
|
+
#
|
|
56
|
+
# @param resolved [String]
|
|
57
|
+
# @return [String]
|
|
58
|
+
def edit_recipe(resolved)
|
|
59
|
+
<<~MSG
|
|
60
|
+
Write refused — #{resolved} already exists. Write is for creating
|
|
61
|
+
NEW files only. To change an existing file, Read it first to get the
|
|
62
|
+
exact current text, then use Edit with that text as oldText
|
|
63
|
+
(whitespace and indentation must match). Do NOT retry Write; it will
|
|
64
|
+
be refused again.
|
|
65
|
+
MSG
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "guards/run_store"
|
|
4
|
+
require_relative "guards/path_resolution"
|
|
5
|
+
require_relative "guards/write_guard"
|
|
6
|
+
require_relative "guards/read_before_edit"
|
|
7
|
+
require_relative "guards/checkpoint"
|
|
8
|
+
require_relative "guards/quality_monitor"
|
|
9
|
+
require_relative "guards/grader_lock"
|
|
10
|
+
|
|
11
|
+
module RobotLab
|
|
12
|
+
module To
|
|
13
|
+
# Small-model guardrails ported from little-coder (Apache-2.0).
|
|
14
|
+
#
|
|
15
|
+
# These RobotLab::Hook subclasses make file/bash tools safe for a small
|
|
16
|
+
# *local* model driving an autonomous robot_lab-to loop. They are no-ops
|
|
17
|
+
# for a frontier model, so registration is opt-in.
|
|
18
|
+
#
|
|
19
|
+
# Wire them onto a per-iteration robot in Orchestrator#build_robot:
|
|
20
|
+
#
|
|
21
|
+
# robot = RobotLab.build(...)
|
|
22
|
+
# RobotLab::To::Guards.install(robot, run: @run) if @config.local_guards
|
|
23
|
+
# robot
|
|
24
|
+
#
|
|
25
|
+
# Each guard is single-purpose and independently testable: its class methods
|
|
26
|
+
# take a HookContext and have no hidden global state beyond a per-robot store
|
|
27
|
+
# that is reset on before_run.
|
|
28
|
+
module Guards
|
|
29
|
+
ALL = [
|
|
30
|
+
Guards::WriteGuard,
|
|
31
|
+
Guards::ReadBeforeEdit,
|
|
32
|
+
Guards::Checkpoint,
|
|
33
|
+
Guards::QualityMonitor
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# Register every guard on a robot.
|
|
37
|
+
#
|
|
38
|
+
# @param robot [RobotLab::Robot]
|
|
39
|
+
# @param run [RobotLab::To::Run, nil] supplies run_dir for checkpoints
|
|
40
|
+
# @return [RobotLab::Robot] the same robot, for chaining
|
|
41
|
+
# `except:` lists guard classes to skip. Passing [WriteGuard] lets a robot
|
|
42
|
+
# overwrite existing files (WriteGuard otherwise refuses Write on an existing
|
|
43
|
+
# file and redirects to Edit -- unhelpful for iterative prose that rewrites a
|
|
44
|
+
# draft).
|
|
45
|
+
def self.install(robot, run: nil, except: [])
|
|
46
|
+
active = ALL - Array(except)
|
|
47
|
+
robot.on(Guards::Checkpoint, context: { run: run }) if run
|
|
48
|
+
(active - [Guards::Checkpoint]).each { |guard| robot.on(guard) }
|
|
49
|
+
robot.on(Guards::Checkpoint) unless run
|
|
50
|
+
robot
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module To
|
|
5
|
+
IterationResult = Data.define(
|
|
6
|
+
:success, # Boolean
|
|
7
|
+
:summary, # String
|
|
8
|
+
:key_changes, # Array<String>
|
|
9
|
+
:key_learnings, # Array<String>
|
|
10
|
+
:should_fully_stop # Boolean or nil (nil when --stop-when not active)
|
|
11
|
+
) do
|
|
12
|
+
def success? = success
|
|
13
|
+
def stop? = !!should_fully_stop
|
|
14
|
+
|
|
15
|
+
# Used when --stop-when is not configured
|
|
16
|
+
def self.no_stop_when(success:, summary:, key_changes: [], key_learnings: [])
|
|
17
|
+
new(success: success, summary: summary,
|
|
18
|
+
key_changes: key_changes, key_learnings: key_learnings,
|
|
19
|
+
should_fully_stop: nil)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sentinel result when the robot returns without calling submit_iteration_result
|
|
23
|
+
def self.not_submitted
|
|
24
|
+
new(success: false, summary: "Robot did not call submit_iteration_result",
|
|
25
|
+
key_changes: [], key_learnings: [], should_fully_stop: nil)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RobotLab
|
|
6
|
+
module To
|
|
7
|
+
# Writes structured JSONL events to the run log file.
|
|
8
|
+
#
|
|
9
|
+
# Events emitted before the log file is ready are buffered in memory
|
|
10
|
+
# and flushed on first open (capacity: 100 lines).
|
|
11
|
+
class JsonlLogger
|
|
12
|
+
MAX_BUFFER = 100
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@path = nil
|
|
16
|
+
@buffer = []
|
|
17
|
+
@file = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def open(path)
|
|
21
|
+
@path = path
|
|
22
|
+
@file = File.open(path, "a")
|
|
23
|
+
@file.sync = true
|
|
24
|
+
flush_buffer
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log(event, **payload)
|
|
28
|
+
entry = { event: event, ts: Time.now.iso8601(3) }.merge(payload).compact
|
|
29
|
+
line = JSON.generate(entry)
|
|
30
|
+
if @file
|
|
31
|
+
@file.write("#{line}\n") # single write keeps the line intact on crash
|
|
32
|
+
else
|
|
33
|
+
@buffer.shift if @buffer.size >= MAX_BUFFER
|
|
34
|
+
@buffer << line
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def close
|
|
39
|
+
if @file
|
|
40
|
+
@file.flush
|
|
41
|
+
begin
|
|
42
|
+
@file.fsync
|
|
43
|
+
rescue SystemCallError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
@file.close
|
|
47
|
+
end
|
|
48
|
+
@file = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def flush_buffer
|
|
54
|
+
@buffer.each { |line| @file.write("#{line}\n") }
|
|
55
|
+
@buffer.clear
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module To
|
|
5
|
+
# Manages the cross-iteration notes file (notes.md).
|
|
6
|
+
#
|
|
7
|
+
# Written by the orchestrator only. The robot reads it at the start of every
|
|
8
|
+
# iteration to understand prior work.
|
|
9
|
+
class NotesManager
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
def initialize(path)
|
|
13
|
+
@path = Pathname.new(path)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def setup(run)
|
|
17
|
+
AtomicFile.write(@path, <<~HEADER)
|
|
18
|
+
# robot-to run: #{run.run_id}
|
|
19
|
+
|
|
20
|
+
Objective: #{run.objective}
|
|
21
|
+
|
|
22
|
+
## Iteration Log
|
|
23
|
+
HEADER
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def read
|
|
27
|
+
@path.exist? ? @path.read : ""
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def append_success(result, iteration)
|
|
31
|
+
changes_md = bullet_list(result.key_changes)
|
|
32
|
+
learnings_md = bullet_list(result.key_learnings)
|
|
33
|
+
append(<<~MD)
|
|
34
|
+
|
|
35
|
+
### Iteration #{iteration}
|
|
36
|
+
|
|
37
|
+
**Summary:** #{result.summary}
|
|
38
|
+
|
|
39
|
+
**Changes:**
|
|
40
|
+
#{changes_md}
|
|
41
|
+
**Learnings:**
|
|
42
|
+
#{learnings_md}
|
|
43
|
+
MD
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def append_failure(result, iteration)
|
|
47
|
+
learnings_md = bullet_list(result.key_learnings)
|
|
48
|
+
append(<<~MD)
|
|
49
|
+
|
|
50
|
+
### Iteration #{iteration} [FAIL]
|
|
51
|
+
|
|
52
|
+
**Summary:** #{result.summary}
|
|
53
|
+
|
|
54
|
+
**Learnings:**
|
|
55
|
+
#{learnings_md}
|
|
56
|
+
MD
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def append_no_improvement(result, detail, iteration)
|
|
60
|
+
append(<<~MD)
|
|
61
|
+
|
|
62
|
+
### Iteration #{iteration} [NO IMPROVEMENT]
|
|
63
|
+
|
|
64
|
+
**Summary:** #{result.summary}
|
|
65
|
+
|
|
66
|
+
**Score:** #{detail}
|
|
67
|
+
|
|
68
|
+
The change passed the gate but did not beat the parent commit, so it was
|
|
69
|
+
rolled back. Try a different approach next iteration.
|
|
70
|
+
MD
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def append_verify_failure(result, output, iteration)
|
|
74
|
+
append(<<~MD)
|
|
75
|
+
|
|
76
|
+
### Iteration #{iteration} [VERIFY FAILED]
|
|
77
|
+
|
|
78
|
+
**Summary:** #{result.summary}
|
|
79
|
+
|
|
80
|
+
**Verification output:**
|
|
81
|
+
```
|
|
82
|
+
#{output}
|
|
83
|
+
```
|
|
84
|
+
MD
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def append_error(error, iteration)
|
|
88
|
+
append(<<~MD)
|
|
89
|
+
|
|
90
|
+
### Iteration #{iteration} [ERROR]
|
|
91
|
+
|
|
92
|
+
**Error:** #{error.class}: #{error.message}
|
|
93
|
+
MD
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def append_decision(decision, iteration)
|
|
97
|
+
append(<<~MD)
|
|
98
|
+
|
|
99
|
+
### Iteration #{iteration} [DECISION]
|
|
100
|
+
|
|
101
|
+
**Raised:** #{decision.question}
|
|
102
|
+
**Blocking:** #{decision.blocking? ? "yes" : "no"}
|
|
103
|
+
**Recommendation:** #{decision.recommendation}
|
|
104
|
+
**File:** #{decision.path}
|
|
105
|
+
MD
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def append(text)
|
|
111
|
+
AtomicFile.append(@path, text)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def bullet_list(items)
|
|
115
|
+
return "- (none)\n" if items.nil? || items.empty?
|
|
116
|
+
|
|
117
|
+
items.map { |i| "- #{i}" }.join("\n") + "\n"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|