Class: RobotLab::To::Guards::ReadBeforeEdit

Inherits:
Hook
  • Object
show all
Defined in:
lib/robot_lab/to/guards/read_before_edit.rb

Overview

Read-before-edit invariant, ported from little-coder read-guard-edit.

Small models fire Edit with an oldText they never actually saw, guessing the file's contents — which either fails the exact-match (wasted turn) or matches the wrong span (silent corruption). We block any Edit whose target wasn't Read this run.

The read-set spans many tool calls within one run, so it lives in the per-run RunStore (see that file for why ctx.local can't hold it). A successful Read, Edit, or Write marks the path as known.

Constant Summary collapse

KEY =
:robot_lab_to_read_files
TRACKED_READS =
%w[read edit write].freeze

Class Method Summary collapse

Class Method Details

.after_tool_call(ctx) ⇒ Object

Record successful reads/authored files as "known".



32
33
34
35
36
37
38
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 32

def after_tool_call(ctx)
  return if ctx.tool_error
  return unless TRACKED_READS.include?(ctx.tool_name.to_s.downcase)

  path = PathResolution.resolve(ctx.tool_args)
  mark_read(path) if path
end

.around_tool_call(ctx) ⇒ Object

Block edits to files that were never read.



41
42
43
44
45
46
47
48
49
50
51
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 41

def around_tool_call(ctx)
  return yield unless ctx.tool_name.to_s.casecmp?("edit")

  path = PathResolution.resolve(ctx.tool_args)
  if path && !read?(path)
    ctx.tool_result = edit_before_read_reason(path)
    return ctx.tool_result
  end

  yield
end

.before_run(_ctx) ⇒ Object

Fresh run = clean slate; a prior iteration's reads say nothing here.



27
28
29
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 27

def before_run(_ctx)
  RunStore.reset(KEY, [])
end

.edit_before_read_reason(resolved) ⇒ String

Parameters:

  • resolved (String)

Returns:

  • (String)


70
71
72
73
74
75
76
77
78
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 70

def edit_before_read_reason(resolved)
  <<~MSG
    File must be read before edit — #{resolved} has not been read in
    this session. Read #{resolved} first to get the exact current text
    for oldText (whitespace and indentation must match exactly), then
    issue the Edit. Include 2-3 surrounding lines so oldText is unique.
    Do NOT guess the file's contents.
  MSG
end

.mark_read(path) ⇒ Object



58
59
60
61
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 58

def mark_read(path)
  files = read_files
  files << path unless files.include?(path)
end

.read?(path) ⇒ Boolean

Returns whether path has been read this run.

Returns:

  • (Boolean)

    whether path has been read this run



54
55
56
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 54

def read?(path)
  read_files.include?(path)
end

.read_filesArray<String>

Returns the per-run read-set (live reference).

Returns:

  • (Array<String>)

    the per-run read-set (live reference)



64
65
66
# File 'lib/robot_lab/to/guards/read_before_edit.rb', line 64

def read_files
  RunStore.fetch(KEY, [])
end