robot_lab-durable 0.1.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bb9d48a0a10a6b049f34cb71625023be6373e403eae36b7964434443b9f74f2
4
- data.tar.gz: b96330d170840c4e06b8ad349855e642bef12c4cb97b4a1b2d43fce0235df847
3
+ metadata.gz: b576c8ce4dc5ca5b4741cf6b7d9a95b0b653b9f54ab15aa02e0114a2e7cf43d4
4
+ data.tar.gz: 5079f8bd4f6a63fdcc9938da446db749ca1e93ab844aa5d47c4a8fbd025d189f
5
5
  SHA512:
6
- metadata.gz: 36b8be5148c65454a058575065f92692466bf4a5a28f6fbc77e5fca39aa1ffce9dda7f368a15dc23a5a93e480b72ae40d9e5bf892419692c75bfee6dbb3a49fc
7
- data.tar.gz: a162fdc17422a862537fb826ff0260fffb66de5143eb37da28362d06834bc4efa66b322a9b8984fe653690f87cd5c230806dc1441c67e3d5720f04a96d8eade2
6
+ metadata.gz: 7834ab27785b8f43f52e67dad700dd9a5a886cb0fb265d84e46b4cc79c0e123b5181847d17e87260400d6c3db3ac2212507ae7205186f684a6b8962270f49b57
7
+ data.tar.gz: cc57c77b81eaf328a07d167dc5d053dff56599935e51ada19fc50ac39bb73a73c1f63c28d387d77e3380693871b20e4e33c787cd9a2c39cd6d8734add3e8f19f
data/.rubocop.yml ADDED
@@ -0,0 +1,173 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 4.0
5
+ Exclude:
6
+ - 'examples/**/*'
7
+ - 'vendor/**/*'
8
+ - 'dead_code/**/*'
9
+
10
+ # ── Style: disabled cops ───────────────────────────────────────────────────
11
+ Style/StringLiterals:
12
+ Enabled: false
13
+
14
+ Style/StringLiteralsInInterpolation:
15
+ Enabled: false
16
+
17
+ Style/Documentation:
18
+ Enabled: false
19
+
20
+ # Ruby 4.0 freezes string literals by default
21
+ Style/FrozenStringLiteralComment:
22
+ Enabled: false
23
+
24
+ Style/IfUnlessModifier:
25
+ Enabled: false
26
+
27
+ Style/RescueModifier:
28
+ Enabled: false
29
+
30
+ Style/TrivialAccessors:
31
+ Enabled: false
32
+
33
+ Style/MultilineTernaryOperator:
34
+ Enabled: false
35
+
36
+ Style/SafeNavigation:
37
+ Enabled: false
38
+
39
+ Style/EmptyClassDefinition:
40
+ Enabled: false
41
+
42
+ Style/ClassAndModuleChildren:
43
+ Enabled: false
44
+
45
+ Style/RescueStandardError:
46
+ Enabled: false
47
+
48
+ Style/OneClassPerFile:
49
+ Enabled: false
50
+
51
+ # Both % and format/sprintf are acceptable
52
+ Style/FormatString:
53
+ Enabled: false
54
+
55
+ # String concatenation and interpolation are both acceptable
56
+ Style/StringConcatenation:
57
+ Enabled: false
58
+
59
+ # ── Layout ─────────────────────────────────────────────────────────────────
60
+ Layout/LineLength:
61
+ Max: 140
62
+
63
+ Layout/ExtraSpacing:
64
+ Enabled: false
65
+
66
+ Layout/HashAlignment:
67
+ Enabled: false
68
+
69
+ Layout/FirstHashElementIndentation:
70
+ Enabled: false
71
+
72
+ Layout/EmptyLineAfterGuardClause:
73
+ Enabled: false
74
+
75
+ # ── Naming ─────────────────────────────────────────────────────────────────
76
+ # Single-char params (c, e, n) are acceptable throughout
77
+ Naming/MethodParameterName:
78
+ Enabled: false
79
+
80
+ Naming/VariableNumber:
81
+ Exclude:
82
+ - 'test/**/*'
83
+
84
+ Naming/RescuedExceptionsVariableName:
85
+ Enabled: false
86
+
87
+ # set_results and similar explicit setters are clear and conventional
88
+ Naming/AccessorMethodName:
89
+ Enabled: false
90
+
91
+
92
+ # has_tool_calls? and similar are clear and conventional
93
+ Naming/PredicatePrefix:
94
+ Enabled: false
95
+
96
+ # Test helper methods don't need to follow predicate naming rules
97
+ Naming/PredicateMethod:
98
+ Exclude:
99
+ - 'test/**/*'
100
+
101
+ # ── Lint: relax noisy cops on intentional patterns ─────────────────────────
102
+ # Library and framework methods commonly accept args for API/documentation purposes
103
+ Lint/UnusedMethodArgument:
104
+ Enabled: false
105
+
106
+
107
+ Lint/EmptyBlock:
108
+ Exclude:
109
+ - 'test/**/*'
110
+
111
+ Lint/ConstantDefinitionInBlock:
112
+ Exclude:
113
+ - 'Rakefile'
114
+ - 'test/**/*'
115
+
116
+ # ── Gemspec ────────────────────────────────────────────────────────────────
117
+ Gemspec/DevelopmentDependencies:
118
+ EnforcedStyle: Gemfile
119
+
120
+ Gemspec/RequiredRubyVersion:
121
+ Enabled: false
122
+
123
+ Gemspec/OrderedDependencies:
124
+ Enabled: false
125
+
126
+ # ── Metrics ────────────────────────────────────────────────────────────────
127
+ # Framework-level code (routers, parsers, orchestrators) is inherently complex.
128
+ # Flog is the primary complexity gate — these RuboCop thresholds catch only
129
+ # egregious outliers without false-positiving every dispatch method.
130
+
131
+ Metrics/MethodLength:
132
+ Max: 35
133
+ CountAsOne:
134
+ - heredoc
135
+ - array
136
+ - hash
137
+ Exclude:
138
+ - 'test/**/*'
139
+
140
+ Metrics/AbcSize:
141
+ Max: 40
142
+ Exclude:
143
+ - 'test/**/*'
144
+
145
+ Metrics/ClassLength:
146
+ Max: 600
147
+ Exclude:
148
+ - 'test/**/*'
149
+
150
+ Metrics/ModuleLength:
151
+ Max: 200
152
+ Exclude:
153
+ - 'test/**/*'
154
+
155
+ Metrics/CyclomaticComplexity:
156
+ Max: 20
157
+ Exclude:
158
+ - 'test/**/*'
159
+
160
+ Metrics/PerceivedComplexity:
161
+ Max: 20
162
+ Exclude:
163
+ - 'test/**/*'
164
+
165
+ # Long method signatures with keyword args are a Ruby framework idiom
166
+ Metrics/ParameterLists:
167
+ Enabled: false
168
+
169
+ Metrics/BlockLength:
170
+ Exclude:
171
+ - 'Rakefile'
172
+ - '*.gemspec'
173
+ - 'test/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2026-05-19
4
+
5
+ ### Added
6
+ - `Durable::Entry` — immutable `Data.define` value object with `content`, `confidence`, `category`, `domain`, `use_count`, `created_at`, and `updated_at` fields
7
+ - `Durable::Store` — YAML-backed, file-locked per-domain knowledge persistence in `~/.robot_lab/durable/`
8
+ - `Durable::Reflector` — promotes session-level learnings into the durable store at end-of-run with confidence scoring and deduplication
9
+ - `Durable::Learning` mixin — included into `RobotLab::Robot` when `learn: true` and `learn_domain:` constructor params are set
10
+ - `RecallKnowledge` tool — lets robots query the durable store before making decisions
11
+ - `RecordKnowledge` tool — lets robots write new knowledge entries during a session
12
+ - Design document for future `DocumentStore::FileSystem` backend integration (`docs/document_store_backend_design.md`)
13
+
14
+ ### Changed
15
+ - Version synchronized with robot_lab core 0.2.1
16
+
3
17
  ## [0.1.0] - 2026-05-07
4
18
 
5
19
  - Initial release
data/Rakefile CHANGED
@@ -1,8 +1,116 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "minitest/test_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
5
 
6
- Minitest::TestTask.create
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb', 'test/**/test_*.rb'].exclude('**/*_helper.rb')
10
+ t.verbose = true
11
+ t.ruby_opts << '-rtest_helper'
12
+ end
7
13
 
8
14
  task default: :test
15
+
16
+ desc 'Run tests with verbose output'
17
+ task :test_verbose do
18
+ ENV['TESTOPTS'] = '--verbose'
19
+ Rake::Task[:test].invoke
20
+ end
21
+
22
+ desc 'Run a single test file'
23
+ task :test_file, [:file] do |_t, args|
24
+ ruby "test/#{args[:file]}"
25
+ end
26
+
27
+ desc 'Check code style with RuboCop'
28
+ task :rubocop do
29
+ sh 'bundle exec rubocop'
30
+ end
31
+
32
+ desc 'Auto-correct RuboCop offenses'
33
+ task :rubocop_fix do
34
+ sh 'bundle exec rubocop -a'
35
+ end
36
+
37
+ desc 'Check code complexity with Flog (warn >=20, fail >=50)'
38
+ task :flog_check do
39
+ require 'flog'
40
+
41
+ method_warn = 20.0
42
+ method_fail = 50.0
43
+
44
+ flogger = Flog.new(all: true)
45
+ flogger.flog(*Dir.glob('lib/**/*.rb'))
46
+
47
+ warnings = []
48
+ failures = []
49
+
50
+ flogger.each_by_score do |method, score|
51
+ next if method.end_with?('#none')
52
+
53
+ if score > method_fail
54
+ failures << "#{format('%.1f', score)}: #{method}"
55
+ elsif score > method_warn
56
+ warnings << "#{format('%.1f', score)}: #{method}"
57
+ end
58
+ end
59
+
60
+ unless warnings.empty?
61
+ puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
62
+ warnings.each { |v| puts " #{v}" }
63
+ end
64
+
65
+ if failures.empty?
66
+ puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
67
+ else
68
+ puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
69
+ failures.each { |v| puts " #{v}" }
70
+ abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
71
+ end
72
+ end
73
+
74
+ desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
75
+ task :quality do
76
+ results = {}
77
+
78
+ puts "\n#{'=' * 60}"
79
+ puts 'Quality Gate: Tests + Coverage'
80
+ puts '=' * 60
81
+ results[:tests] = system('bundle exec rake test') ? :pass : :fail
82
+
83
+ puts "\n#{'=' * 60}"
84
+ puts 'Quality Gate: RuboCop'
85
+ puts '=' * 60
86
+ results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
87
+
88
+ puts "\n#{'=' * 60}"
89
+ puts 'Quality Gate: Flog Complexity'
90
+ puts '=' * 60
91
+ results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
92
+
93
+ puts "\n#{'=' * 60}"
94
+ puts 'Quality Summary'
95
+ puts '=' * 60
96
+ results.each do |gate, status|
97
+ icon = status == :pass ? 'PASS' : 'FAIL'
98
+ puts " [#{icon}] #{gate}"
99
+ end
100
+ puts '=' * 60
101
+
102
+ abort "\nQuality gate failed" if results.values.any?(:fail)
103
+ puts "\nAll quality gates passed."
104
+ end
105
+
106
+ namespace :docs do
107
+ desc 'Build MkDocs documentation'
108
+ task :build do
109
+ sh 'mkdocs build'
110
+ end
111
+
112
+ desc 'Serve MkDocs documentation locally on http://localhost:8000'
113
+ task :serve do
114
+ sh 'mkdocs serve'
115
+ end
116
+ end
@@ -0,0 +1,42 @@
1
+ # Delegate Storage to robot_lab-document_store — Design Discussion
2
+
3
+ **Date:** 2026-05-14
4
+ **Status:** Parked — resume when time allows
5
+
6
+ ## The Problem
7
+
8
+ `robot_lab-durable` currently maintains its own `Store` class: a YAML-backed, file-locked, keyword-search storage layer in `lib/robot_lab/durable/store.rb`. This is duplicated effort — `robot_lab-document_store` is intended to be the canonical pluggable storage abstraction for the robot_lab ecosystem.
9
+
10
+ ## The Vision
11
+
12
+ Once `robot_lab-document_store` gains a `DocumentStore::FileSystem` backend, durable should drop its custom `Store` class and delegate physical storage to it. Durable retains its own concerns:
13
+
14
+ - `Entry` — immutable value object with confidence scoring, category, domain, use_count
15
+ - `Reflector` — end-of-session promoter that pushes session learnings into the store
16
+ - `Learning` — mixin included into `Robot` when robot_lab is present
17
+ - `RecallKnowledge` / `RecordKnowledge` — LLM tools that interact with the store
18
+
19
+ ## What Changes in This Gem
20
+
21
+ 1. Add `robot_lab-document_store` as a runtime dependency in the gemspec.
22
+ 2. Remove `lib/robot_lab/durable/store.rb`.
23
+ 3. Wire `Learning#setup_durable_learning` to instantiate a `DocumentStore::FileSystem` instead of `Durable::Store`.
24
+ 4. Adapt `Reflector` and the two tools to call the `DocumentStore` interface (`store`, `search`, `delete`, etc.) rather than the old `Store` API.
25
+ 5. Handle `Entry` serialization — since `DocumentStore` stores raw text by key, durable will serialize `Entry` fields into text (or use a metadata hash if `FileSystem` supports it — see open questions).
26
+
27
+ ## Open Questions (shared with robot_lab-document_store)
28
+
29
+ 1. **Search semantics.** `DocumentStore::Memory` uses embedding-based cosine similarity; `DocumentStore::FileSystem` would use keyword matching. Should the interface declare its search capability, or do callers accept whatever the backend provides?
30
+
31
+ 2. **Structured vs raw text.** `Entry` carries structured fields (confidence, category, domain, use_count). Options:
32
+ - Durable serializes all fields into the stored text string; deserializes on recall.
33
+ - `DocumentStore::FileSystem` supports an optional `meta:` Hash alongside text, which durable populates with `Entry` fields.
34
+
35
+ ## Versioning
36
+
37
+ - `robot_lab-document_store` must ship `DocumentStore::FileSystem` first — that is a v0.3.0 breaking change for that gem (v0.2.1 is the current release).
38
+ - This gem (`robot_lab-durable`) then bumps to v0.3.0 once it drops `Store` and depends on document_store.
39
+
40
+ ## See Also
41
+
42
+ `robot_lab-document_store/docs/pluggable_backends_design.md` — the full backend architecture design including the `DocumentStore` abstract interface and implementation plan.
@@ -2,16 +2,16 @@
2
2
 
3
3
  module RobotLab
4
4
  module Durable
5
- Entry = Data.define(:content, :reasoning, :category, :domain, :confidence, :use_count, :created_at, :updated_at) do
6
- CONFIDENCE_INCREMENT = 0.1
7
- MAX_CONFIDENCE = 1.0
5
+ CONFIDENCE_INCREMENT = 0.1
6
+ MAX_CONFIDENCE = 1.0
8
7
 
8
+ Entry = Data.define(:content, :reasoning, :category, :domain, :confidence, :use_count, :created_at, :updated_at) do
9
9
  # Return a new Entry with confidence incremented and use_count bumped.
10
10
  def confirm
11
11
  new_confidence = [confidence + CONFIDENCE_INCREMENT, MAX_CONFIDENCE].min
12
12
  with(
13
13
  confidence: new_confidence.round(10),
14
- use_count: use_count + 1,
14
+ use_count: use_count + 1,
15
15
  updated_at: Time.now.iso8601
16
16
  )
17
17
  end
@@ -19,14 +19,14 @@ module RobotLab
19
19
  # Serialize to a plain Hash with string keys (safe for YAML round-trip).
20
20
  def to_h
21
21
  {
22
- "content" => content,
23
- "reasoning" => reasoning,
24
- "category" => category.to_s,
25
- "domain" => domain,
26
- "confidence" => confidence,
27
- "use_count" => use_count,
28
- "created_at" => created_at,
29
- "updated_at" => updated_at
22
+ 'content' => content,
23
+ 'reasoning' => reasoning,
24
+ 'category' => category.to_s,
25
+ 'domain' => domain,
26
+ 'confidence' => confidence,
27
+ 'use_count' => use_count,
28
+ 'created_at' => created_at,
29
+ 'updated_at' => updated_at
30
30
  }
31
31
  end
32
32
 
@@ -34,14 +34,14 @@ module RobotLab
34
34
  def self.from_h(hash)
35
35
  h = hash.transform_keys(&:to_s)
36
36
  new(
37
- content: h["content"],
38
- reasoning: h["reasoning"],
39
- category: h["category"]&.to_sym,
40
- domain: h["domain"],
41
- confidence: h["confidence"].to_f,
42
- use_count: h["use_count"].to_i,
43
- created_at: h["created_at"],
44
- updated_at: h["updated_at"]
37
+ content: h['content'],
38
+ reasoning: h['reasoning'],
39
+ category: h['category']&.to_sym,
40
+ domain: h['domain'],
41
+ confidence: h['confidence'].to_f,
42
+ use_count: h['use_count'].to_i,
43
+ created_at: h['created_at'],
44
+ updated_at: h['updated_at']
45
45
  )
46
46
  end
47
47
  end
@@ -22,12 +22,12 @@ module RobotLab
22
22
  now = Time.now.iso8601
23
23
  @store.record(
24
24
  Entry.new(
25
- content: text,
26
- reasoning: "Observed during session (auto-promoted by Reflector)",
27
- category: :pattern,
28
- domain: @domain,
25
+ content: text,
26
+ reasoning: 'Observed during session (auto-promoted by Reflector)',
27
+ category: :pattern,
28
+ domain: @domain,
29
29
  confidence: 0.1,
30
- use_count: 0,
30
+ use_count: 0,
31
31
  created_at: now,
32
32
  updated_at: now
33
33
  )
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "fileutils"
3
+ require 'yaml'
4
+ require 'fileutils'
5
5
 
6
6
  module RobotLab
7
7
  module Durable
8
8
  class Store
9
- DEFAULT_PATH = File.join(Dir.home, ".robot_lab", "durable")
9
+ DEFAULT_PATH = File.join(Dir.home, '.robot_lab', 'durable')
10
10
 
11
11
  MIN_WORD_LENGTH = 3
12
12
 
@@ -77,13 +77,13 @@ module RobotLab
77
77
  file = domain_file(domain)
78
78
  return [] unless File.exist?(file)
79
79
 
80
- raw = Array(YAML.safe_load(File.read(file)) || [])
80
+ raw = Array(YAML.safe_load_file(file) || [])
81
81
  raw.map { |h| Entry.from_h(h) }
82
82
  end
83
83
 
84
84
  def load_all
85
- Dir.glob(File.join(@path, "*.yaml")).flat_map do |file|
86
- raw = Array(YAML.safe_load(File.read(file)) || [])
85
+ Dir.glob(File.join(@path, '*.yaml')).flat_map do |file|
86
+ raw = Array(YAML.safe_load_file(file) || [])
87
87
  raw.map { |h| Entry.from_h(h) }
88
88
  end
89
89
  end
@@ -97,18 +97,19 @@ module RobotLab
97
97
  entries = load_domain(entry.domain)
98
98
  idx = entries.find_index { |e| e.content.downcase == entry.content.downcase }
99
99
  raise RobotLab::Error, "Cannot confirm: entry not found in domain '#{entry.domain}'" unless idx
100
+
100
101
  entries[idx] = entry
101
102
  save_domain(entry.domain, entries)
102
103
  end
103
104
  end
104
105
 
105
106
  def domain_file(domain)
106
- safe = domain.to_s.downcase.gsub(/[^a-z0-9]+/, "_").delete_prefix("_").delete_suffix("_")
107
+ safe = domain.to_s.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
107
108
  File.join(@path, "#{safe}.yaml")
108
109
  end
109
110
 
110
111
  def with_domain_lock(domain, &block)
111
- lock_path = domain_file(domain) + ".lock"
112
+ lock_path = "#{domain_file(domain)}.lock"
112
113
  File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |f|
113
114
  f.flock(File::LOCK_EX)
114
115
  block.call
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobotLab
4
4
  module Durable
5
- VERSION = "0.1.0"
5
+ VERSION = '0.2.1'
6
6
  end
7
7
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "durable/version"
4
- require_relative "durable/entry"
5
- require_relative "durable/store"
6
- require_relative "durable/reflector"
7
- require_relative "durable/learning"
3
+ require_relative 'durable/version'
4
+ require_relative 'durable/entry'
5
+ require_relative 'durable/store'
6
+ require_relative 'durable/reflector'
7
+ require_relative 'durable/learning'
8
8
 
9
9
  # Minimal error stub so the storage layer works without robot_lab loaded.
10
10
  # When robot_lab is present its own RobotLab::Error takes precedence.
@@ -15,10 +15,12 @@ end
15
15
  # When robot_lab is loaded, register the knowledge tools and hook the
16
16
  # Learning mixin into Robot so `learn: true` works in the constructor.
17
17
  if defined?(RobotLab::Tool)
18
- require_relative "recall_knowledge"
19
- require_relative "record_knowledge"
18
+ require_relative 'recall_knowledge'
19
+ require_relative 'record_knowledge'
20
20
  end
21
21
 
22
- if defined?(RobotLab::Robot)
23
- RobotLab::Robot.include(RobotLab::Durable::Learning)
22
+ RobotLab::Robot.include(RobotLab::Durable::Learning) if defined?(RobotLab::Robot)
23
+
24
+ if defined?(RobotLab) && RobotLab.respond_to?(:register_extension)
25
+ RobotLab.register_extension(:durable, RobotLab::Durable)
24
26
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module RobotLab
4
4
  class RecallKnowledge < Tool
5
- description "Recall relevant knowledge from past sessions before making a decision. " \
6
- "Use this when uncertain whether to include or skip content, or when you want " \
7
- "to check if you have seen a similar situation before. " \
8
- "When in doubt and no relevant knowledge is found, skip the action."
5
+ description 'Recall relevant knowledge from past sessions before making a decision. ' \
6
+ 'Use this when uncertain whether to include or skip content, or when you want ' \
7
+ 'to check if you have seen a similar situation before. ' \
8
+ 'When in doubt and no relevant knowledge is found, skip the action.'
9
9
 
10
- param :query, type: "string", desc: "Natural language description of the decision you are about to make"
11
- param :domain, type: "string", desc: "Topic area to search (e.g. 'newsletter curation')", required: false
10
+ param :query, type: 'string', desc: 'Natural language description of the decision you are about to make'
11
+ param :domain, type: 'string', desc: "Topic area to search (e.g. 'newsletter curation')", required: false
12
12
 
13
13
  def execute(query:, domain: nil)
14
14
  store = robot&.durable_store
15
- return "No durable store configured on this robot." unless store
15
+ return 'No durable store configured on this robot.' unless store
16
16
 
17
17
  entries = store.recall(query: query, domain: domain, min_confidence: 0.0)
18
18
 
@@ -20,7 +20,7 @@ module RobotLab
20
20
  "No relevant past knowledge found for: #{query}. When in doubt, skip."
21
21
  else
22
22
  lines = entries.map do |e|
23
- "[#{e.category}/conf:#{format("%.1f", e.confidence)}] #{e.content} — #{e.reasoning}"
23
+ "[#{e.category}/conf:#{format('%.1f', e.confidence)}] #{e.content} — #{e.reasoning}"
24
24
  end
25
25
 
26
26
  "Relevant past knowledge:\n#{lines.join("\n")}"
@@ -2,28 +2,29 @@
2
2
 
3
3
  module RobotLab
4
4
  class RecordKnowledge < Tool
5
- description "Record a piece of knowledge learned during this session. " \
6
- "Use after a decision or discussion reveals something worth remembering: " \
7
- "a user preference, a reliable pattern, or a factual insight. " \
8
- "Recorded knowledge persists across future sessions."
5
+ description 'Record a piece of knowledge learned during this session. ' \
6
+ 'Use after a decision or discussion reveals something worth remembering: ' \
7
+ 'a user preference, a reliable pattern, or a factual insight. ' \
8
+ 'Recorded knowledge persists across future sessions.'
9
9
 
10
- param :content, type: "string", desc: "The knowledge to record, in plain language (one clear statement)"
11
- param :reasoning, type: "string", desc: "Why this is worth remembering — the observation or discussion that led to it"
12
- param :category, type: "string", desc: "One of: fact, preference, pattern, correction"
13
- param :domain, type: "string", desc: "Topic area this applies to (e.g. 'newsletter curation', 'ruby tooling')"
10
+ param :content, type: 'string', desc: 'The knowledge to record, in plain language (one clear statement)'
11
+ param :reasoning, type: 'string',
12
+ desc: 'Why this is worth remembering the observation or discussion that led to it'
13
+ param :category, type: 'string', desc: 'One of: fact, preference, pattern, correction'
14
+ param :domain, type: 'string', desc: "Topic area this applies to (e.g. 'newsletter curation', 'ruby tooling')"
14
15
 
15
16
  def execute(content:, reasoning:, category:, domain:)
16
17
  store = robot&.durable_store
17
- return "No durable store configured on this robot." unless store
18
+ return 'No durable store configured on this robot.' unless store
18
19
 
19
20
  now = Time.now.iso8601
20
21
  entry = Durable::Entry.new(
21
22
  content:,
22
23
  reasoning:,
23
- category: category.to_sym,
24
+ category: category.to_sym,
24
25
  domain:,
25
26
  confidence: 0.1,
26
- use_count: 0,
27
+ use_count: 0,
27
28
  created_at: now,
28
29
  updated_at: now
29
30
  )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot_lab-durable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -13,16 +13,16 @@ dependencies:
13
13
  name: robot_lab
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - ">="
16
+ - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0'
18
+ version: 0.2.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - ">="
23
+ - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0'
25
+ version: 0.2.0
26
26
  description: Provides RobotLab::Durable — a YAML-backed knowledge store that lets
27
27
  robot_lab agents accumulate and recall observations across sessions. Includes Entry
28
28
  (immutable value object with confidence scoring), Store (file-locked per-domain
@@ -36,10 +36,12 @@ extra_rdoc_files: []
36
36
  files:
37
37
  - ".envrc"
38
38
  - ".github/workflows/deploy-github-pages.yml"
39
+ - ".rubocop.yml"
39
40
  - CHANGELOG.md
40
41
  - LICENSE.txt
41
42
  - README.md
42
43
  - Rakefile
44
+ - docs/document_store_backend_design.md
43
45
  - docs/index.md
44
46
  - docs/superpowers/plans/2026-05-06-durable-learning.md
45
47
  - docs/superpowers/specs/2026-05-06-durable-learning-design.md