phronomy 0.33.0 → 0.34.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/CONTRIBUTING.md +5 -5
  4. data/README.md +15 -2
  5. data/RELEASE_CHECKLIST.md +3 -1
  6. data/SECURITY.md +1 -1
  7. data/VERIFY.sh +1 -9
  8. data/docs/architecture/error-api-review.md +6 -2
  9. data/docs/architecture/structured-output-and-scope.md +3 -4
  10. data/docs/decisions/016-semantic-multi-agent-handoff.md +6 -5
  11. data/docs/features.md +2 -2
  12. data/docs/migrations/cleanup-audit.md +89 -0
  13. data/docs/migrations/legacy-format-removal.md +55 -0
  14. data/docs/migrations/r8-unit6.md +4 -2
  15. data/docs/migrations/vector-async-clients.md +6 -1
  16. data/docs/shared-state-sharing.md +14 -4
  17. data/lib/phronomy/agent/execution/agent_invocation.rb +0 -4
  18. data/lib/phronomy/agent/handoff/transfer_projection.rb +0 -9
  19. data/lib/phronomy/agent/journal/journal_record.rb +2 -2
  20. data/lib/phronomy/agent/recovery/recovery_rules.rb +0 -12
  21. data/lib/phronomy/agent/store.rb +3 -8
  22. data/lib/phronomy/agent/tool_execution/tool_invocation.rb +0 -3
  23. data/lib/phronomy/context/context_policy.rb +3 -7
  24. data/lib/phronomy/context/llm_input_manifest.rb +3 -3
  25. data/lib/phronomy/engine/runtime/shutdown_result.rb +0 -5
  26. data/lib/phronomy/execution/execution.rb +0 -11
  27. data/lib/phronomy/multi_agent/shared_state.rb +5 -8
  28. data/lib/phronomy/version.rb +1 -1
  29. metadata +4 -11
  30. data/lib/phronomy/execution/scheduler_reentrancy_error.rb +0 -9
  31. data/lib/phronomy/persistence/migration/initial_format_migration.rb +0 -226
  32. data/scripts/add_to_h_to_token_doubles.rb +0 -33
  33. data/scripts/add_to_h_unnamed_doubles.rb +0 -27
  34. data/scripts/migrate_generator_verifier_result.rb +0 -18
  35. data/scripts/migrate_spec_agent_definition.rb +0 -108
  36. data/scripts/migrate_spec_agent_definition_pass2.rb +0 -53
  37. data/scripts/migrate_spec_inline_pass3.rb +0 -24
  38. data/sig/phronomy/persistence/migration.rbs +0 -13
@@ -35,7 +35,7 @@ module Phronomy
35
35
  role: role,
36
36
  content: content,
37
37
  content_format: content_format || inferred_content_format(content),
38
- estimated_tokens: estimate_content(content),
38
+ estimated_tokens: estimate_tokens(content),
39
39
  required: required,
40
40
  provenance: generated_provenance,
41
41
  metadata: metadata
@@ -56,7 +56,7 @@ module Phronomy
56
56
  role: role,
57
57
  content: content,
58
58
  content_format: content_format || inferred_content_format(content),
59
- estimated_tokens: estimate_content(content),
59
+ estimated_tokens: estimate_tokens(content),
60
60
  required: required,
61
61
  provenance: generated_provenance,
62
62
  metadata: metadata
@@ -82,7 +82,7 @@ module Phronomy
82
82
  content: content,
83
83
  content_format: content_format || inferred_content_format(content),
84
84
  sequence: sequence,
85
- estimated_tokens: estimate_content(content),
85
+ estimated_tokens: estimate_tokens(content),
86
86
  required: required,
87
87
  provenance: generated_provenance,
88
88
  tool_call_id: tool_call_id,
@@ -121,10 +121,6 @@ module Phronomy
121
121
  def inferred_content_format(content)
122
122
  content.is_a?(String) ? :text : :json
123
123
  end
124
-
125
- def estimate_content(content)
126
- estimate_tokens(content)
127
- end
128
124
  end
129
125
  end
130
126
  end
@@ -149,9 +149,9 @@ module Phronomy
149
149
  freeze
150
150
  end
151
151
 
152
- # Current-format-only durable decoder. Historical format conversion is an
153
- # explicit migration operation and is never attempted here. Unlike the
154
- # Ruby construction API, this decoder never coerces durable field types.
152
+ # Current-format-only durable decoder. Historical versions are rejected.
153
+ # Unlike the Ruby construction API, this decoder never coerces durable
154
+ # field types.
155
155
  def self.from_h(hash)
156
156
  source = strict_source!(
157
157
  hash,
@@ -52,11 +52,6 @@ module Phronomy
52
52
  %i[not_started terminated].include?(@event_loop_status) &&
53
53
  @task_registry_status == :empty
54
54
  end
55
-
56
- # Runtime reset is safe when cleanup completed, even if execution failed.
57
- def success?
58
- cleanup_complete?
59
- end
60
55
  end
61
56
  end
62
57
  end
@@ -127,22 +127,11 @@ module Phronomy
127
127
  observed
128
128
  end
129
129
 
130
- # Scope/admission hooks shared with framework-owned operations.
131
- # @api private
132
- def __while_open(&block)
133
- @scope.__while_open(&block)
134
- end
135
-
136
130
  # @api private
137
131
  def __open?
138
132
  @scope.__open?
139
133
  end
140
134
 
141
- # @api private
142
- def __cancellation_token
143
- @token
144
- end
145
-
146
135
  # @api private
147
136
  def __cancellation_error
148
137
  @scope.__cancellation_error
@@ -323,14 +323,11 @@ module Phronomy
323
323
  # application's definition-version namespace independent from framework
324
324
  # instrumentation.
325
325
  instrumented_def = instrumented_definition_for(researcher_class)
326
- Class.new(researcher_class) do
327
- model researcher_class.model
328
- agent_definition(
329
- id: instrumented_def.fetch(:id),
330
- version: instrumented_def.fetch(:version)
331
- )
332
- tools(definitions)
333
- end
326
+ researcher_class.with_tools(
327
+ definitions,
328
+ id: instrumented_def.fetch(:id),
329
+ version: instrumented_def.fetch(:version)
330
+ )
334
331
  end
335
332
 
336
333
  def build_read_tool(store)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Phronomy
4
- VERSION = "0.33.0"
4
+ VERSION = "0.34.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phronomy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.0
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raizo T.C.S
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-11-14 00:00:00.000000000 Z
11
+ date: 2026-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby_llm
@@ -345,8 +345,10 @@ files:
345
345
  - docs/migrations/0.19.md
346
346
  - docs/migrations/0.22.md
347
347
  - docs/migrations/0.31.md
348
+ - docs/migrations/cleanup-audit.md
348
349
  - docs/migrations/durable-semantic-coordination-v2.md
349
350
  - docs/migrations/handoff-runner-multi-agent.md
351
+ - docs/migrations/legacy-format-removal.md
350
352
  - docs/migrations/neutral-storage-spi.md
351
353
  - docs/migrations/parallel-tool-chat-removal.md
352
354
  - docs/migrations/persistence-failure-contracts.md
@@ -561,7 +563,6 @@ files:
561
563
  - lib/phronomy/execution/runnable.rb
562
564
  - lib/phronomy/execution/runtime_shutdown_error.rb
563
565
  - lib/phronomy/execution/runtime_shutdown_reentrancy_error.rb
564
- - lib/phronomy/execution/scheduler_reentrancy_error.rb
565
566
  - lib/phronomy/execution/task_result.rb
566
567
  - lib/phronomy/execution/timeout_error.rb
567
568
  - lib/phronomy/execution/wait_policy.rb
@@ -636,7 +637,6 @@ files:
636
637
  - lib/phronomy/output_parser/structured_parser.rb
637
638
  - lib/phronomy/persistence/content_repository.rb
638
639
  - lib/phronomy/persistence/errors.rb
639
- - lib/phronomy/persistence/migration/initial_format_migration.rb
640
640
  - lib/phronomy/persistence/persistence.rb
641
641
  - lib/phronomy/persistence/save_outcome.rb
642
642
  - lib/phronomy/persistence/snapshot_comparison.rb
@@ -749,16 +749,10 @@ files:
749
749
  - lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb
750
750
  - lib/phronomy/workflow/runtime_binding/workflow_task_session.rb
751
751
  - lib/phronomy/workflow/runtime_binding/workflow_terminal_policy.rb
752
- - scripts/add_to_h_to_token_doubles.rb
753
- - scripts/add_to_h_unnamed_doubles.rb
754
752
  - scripts/api_snapshot.rb
755
753
  - scripts/check_api_annotations.rb
756
754
  - scripts/check_readme_ruby.rb
757
755
  - scripts/check_readme_runnable.rb
758
- - scripts/migrate_generator_verifier_result.rb
759
- - scripts/migrate_spec_agent_definition.rb
760
- - scripts/migrate_spec_agent_definition_pass2.rb
761
- - scripts/migrate_spec_inline_pass3.rb
762
756
  - scripts/run_mutation.sh
763
757
  - scripts/storage_spi_snapshot.rb
764
758
  - sig/_private/phronomy/agent/settings.rbs
@@ -820,7 +814,6 @@ files:
820
814
  - sig/phronomy/multi_agent/team_not_found_error.rbs
821
815
  - sig/phronomy/persistence.rbs
822
816
  - sig/phronomy/persistence/errors.rbs
823
- - sig/phronomy/persistence/migration.rbs
824
817
  - sig/phronomy/persistence/transaction.rbs
825
818
  - sig/phronomy/persistence_composition.rbs
826
819
  - sig/phronomy/runtime_composition/configuration.rbs
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "event_loop_reentrancy_error"
4
-
5
- module Phronomy
6
- # Backward-compatible error class name for callers that still rescue the old
7
- # scheduler-oriented exception. New code should use EventLoopReentrancyError.
8
- class SchedulerReentrancyError < EventLoopReentrancyError; end
9
- end
@@ -1,226 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Phronomy
4
- class Persistence
5
- module Migration
6
- # Explicit logical conversion from the pre-S3 unversioned representations
7
- # present at the ACS-06 fixed base to the initial 0.1 durable formats.
8
- #
9
- # This module does not scan a database or write records. A backend-specific
10
- # migration runner is responsible for physical enumeration, transactions,
11
- # and replacement. Normal Runtime load never calls these methods.
12
- #
13
- # @api public
14
- module InitialFormatMigration
15
- PRE_S3_AGENT_ROOT_KEYS = Phronomy::Agent::AgentRoot::ATTRIBUTES.map(&:to_s).freeze
16
- PRE_S3_AGENT_EXECUTION_KEYS = Phronomy::Agent::AgentExecution::ATTRIBUTES.map(&:to_s).freeze
17
- PRE_S3_JOURNAL_KEYS = Phronomy::Agent::JournalRecord::ATTRIBUTES.map(&:to_s).freeze
18
- PRE_S3_LLM_CALL_KEYS = Phronomy::Agent::LLMCallRecord::ATTRIBUTES.map(&:to_s).freeze
19
- PRE_S3_APPROVAL_REQUIRED_KEYS = %w[id items created_at].freeze
20
- PRE_S3_APPROVAL_PARENT_KEYS = %w[execution_id agent_invocation_id].freeze
21
- PRE_S3_APPROVAL_OPTIONAL_KEYS = %w[approved].freeze
22
- PRE_S3_APPROVAL_ITEM_KEYS = %w[
23
- tool_invocation_id tool_call_id tool_name arguments facts reason origin metadata
24
- ].freeze
25
-
26
- module_function
27
-
28
- def agent_root(hash)
29
- source = stringify_keys(hash)
30
- validate_exact_keys!(
31
- source,
32
- PRE_S3_AGENT_ROOT_KEYS,
33
- label: "pre-S3 AgentRoot"
34
- )
35
- Phronomy::Agent::Persistence::Codec.encode_agent_root(
36
- Phronomy::Agent::AgentRoot.from_h(source)
37
- )
38
- rescue Phronomy::Storage::SerializationError
39
- raise
40
- rescue => error
41
- migration_error("AgentRoot", error)
42
- end
43
-
44
- def agent_execution(hash)
45
- source = stringify_keys(hash)
46
- validate_exact_keys!(
47
- source,
48
- PRE_S3_AGENT_EXECUTION_KEYS,
49
- label: "pre-S3 AgentExecution"
50
- )
51
- source["working_records"] = Array(source.fetch("working_records")).map.with_index do |record, index|
52
- normalize_legacy_journal_record(
53
- record,
54
- label: "pre-S3 AgentExecution working_records[#{index}]"
55
- )
56
- end
57
- source["llm_calls"] = Array(source.fetch("llm_calls")).map.with_index do |call, index|
58
- normalized = stringify_keys(call)
59
- validate_exact_keys!(
60
- normalized,
61
- PRE_S3_LLM_CALL_KEYS,
62
- label: "pre-S3 AgentExecution llm_calls[#{index}]"
63
- )
64
- normalized
65
- end
66
- source["approval_request"] = normalize_legacy_approval_request(
67
- source["approval_request"],
68
- execution_id: source.fetch("execution_id")
69
- )
70
- execution = Phronomy::Agent::AgentExecution.from_h(source)
71
- Phronomy::Agent::Persistence::Codec.encode_agent_execution(execution)
72
- rescue Phronomy::Storage::SerializationError
73
- raise
74
- rescue => error
75
- migration_error("AgentExecution", error)
76
- end
77
-
78
- def journal_record(hash)
79
- source = normalize_legacy_journal_record(hash, label: "pre-S3 JournalRecord")
80
- Phronomy::Agent::Persistence::Codec.encode_journal_record(
81
- Phronomy::Agent::JournalRecord.from_h(source)
82
- )
83
- rescue Phronomy::Storage::SerializationError
84
- raise
85
- rescue => error
86
- migration_error("JournalRecord", error)
87
- end
88
-
89
- def workflow_state(workflow_instance_id:, revision:, snapshot:)
90
- Phronomy::Workflow::Persistence::Codec.encode_workflow_state(
91
- workflow_instance_id: workflow_instance_id,
92
- workflow_revision: Integer(revision),
93
- snapshot: snapshot
94
- )
95
- rescue Phronomy::Storage::SerializationError
96
- raise
97
- rescue => error
98
- migration_error("Workflow state", error)
99
- end
100
-
101
- # LLMInputManifest is a ContentStore codec boundary, not a
102
- # Storage::DurableRecord. Its pre-CG-07 integer version 1 is migrated
103
- # explicitly to the pre-1.0 string version "0.1".
104
- def llm_input_manifest(hash)
105
- source = stringify_keys(hash)
106
- old_version = source.fetch("version")
107
- unless old_version == 1 || old_version == "1"
108
- raise Phronomy::Storage::SerializationError,
109
- "unsupported pre-S3 LLMInputManifest version: #{old_version.inspect}"
110
- end
111
- source["version"] = Phronomy::Context::LLMInputManifest::VERSION
112
- Phronomy::Context::LLMInputManifest.from_h(source).to_h
113
- rescue Phronomy::Storage::SerializationError
114
- raise
115
- rescue => error
116
- migration_error("LLMInputManifest", error)
117
- end
118
-
119
- def normalize_legacy_journal_record(hash, label:)
120
- source = stringify_keys(hash)
121
- validate_allowed_keys!(
122
- source,
123
- required_keys: PRE_S3_JOURNAL_KEYS,
124
- optional_keys: ["correlation_id"],
125
- label: label
126
- )
127
- source.delete("correlation_id")
128
- source
129
- end
130
- private_class_method :normalize_legacy_journal_record
131
-
132
- def normalize_legacy_approval_request(request, execution_id:)
133
- return nil unless request
134
-
135
- source = stringify_keys(request)
136
- validate_allowed_keys!(
137
- source,
138
- required_keys: PRE_S3_APPROVAL_REQUIRED_KEYS,
139
- optional_keys: PRE_S3_APPROVAL_PARENT_KEYS + PRE_S3_APPROVAL_OPTIONAL_KEYS,
140
- label: "pre-S3 approval_request"
141
- )
142
- parents = PRE_S3_APPROVAL_PARENT_KEYS.select { |key| source.key?(key) }
143
- unless parents.length == 1
144
- raise Phronomy::Storage::SerializationError,
145
- "pre-S3 approval_request must contain exactly one parent identity, got #{parents.inspect}"
146
- end
147
-
148
- items = source.fetch("items")
149
- unless items.is_a?(Array) && !items.empty?
150
- raise Phronomy::Storage::SerializationError,
151
- "pre-S3 approval_request items must be a non-empty Array"
152
- end
153
- source["items"] = items.map.with_index do |item, index|
154
- normalized = stringify_keys(item)
155
- validate_exact_keys!(
156
- normalized,
157
- PRE_S3_APPROVAL_ITEM_KEYS,
158
- label: "pre-S3 approval_request items[#{index}]"
159
- )
160
- normalized
161
- end
162
-
163
- if source.key?("agent_invocation_id")
164
- source.delete("agent_invocation_id")
165
- source["execution_id"] = execution_id.to_s
166
- elsif source.fetch("execution_id").to_s != execution_id.to_s
167
- raise Phronomy::Storage::SerializationError,
168
- "pre-S3 approval_request execution_id does not match AgentExecution"
169
- end
170
- source
171
- end
172
- private_class_method :normalize_legacy_approval_request
173
-
174
- def validate_allowed_keys!(hash, required_keys:, optional_keys:, label:)
175
- actual = hash.keys.sort
176
- missing = required_keys.sort - actual
177
- unknown = actual - (required_keys + optional_keys).sort
178
- return hash if missing.empty? && unknown.empty?
179
-
180
- details = []
181
- details << "missing=#{missing.inspect}" unless missing.empty?
182
- details << "unknown=#{unknown.inspect}" unless unknown.empty?
183
- raise Phronomy::Storage::SerializationError,
184
- "#{label} schema mismatch (#{details.join(", ")})"
185
- end
186
- private_class_method :validate_allowed_keys!
187
-
188
- def validate_exact_keys!(hash, expected_keys, label:)
189
- validate_allowed_keys!(
190
- hash,
191
- required_keys: expected_keys,
192
- optional_keys: [],
193
- label: label
194
- )
195
- end
196
- private_class_method :validate_exact_keys!
197
-
198
- def stringify_keys(hash)
199
- unless hash.is_a?(Hash)
200
- raise Phronomy::Storage::SerializationError,
201
- "migration input must be a Hash"
202
- end
203
- hash.each_with_object({}) do |(key, value), result|
204
- unless key.is_a?(String) || key.is_a?(Symbol)
205
- raise Phronomy::Storage::SerializationError,
206
- "migration key must be String or Symbol, got #{key.class}"
207
- end
208
- string_key = key.to_s
209
- if result.key?(string_key)
210
- raise Phronomy::Storage::SerializationError,
211
- "duplicate migration key after normalization: #{string_key.inspect}"
212
- end
213
- result[string_key] = value
214
- end
215
- end
216
- private_class_method :stringify_keys
217
-
218
- def migration_error(label, error)
219
- raise Phronomy::Storage::SerializationError,
220
- "cannot migrate pre-S3 #{label}: #{error.class}: #{error.message}"
221
- end
222
- private_class_method :migration_error
223
- end
224
- end
225
- end
226
- end
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Adds to_h to all Tokens-like doubles so ExecutionCoordinator can serialize usage.
5
-
6
- files = Dir.glob("spec/**/*.rb").sort
7
-
8
- total = 0
9
- files.each do |f|
10
- content = File.read(f)
11
- modified = content.gsub(
12
- /double\(("[^"]+"),\s*(input: \d+, output: \d+, cached: \d+, cache_creation: \d+)\)/
13
- ) do
14
- name = $1
15
- attrs = $2
16
- next $& if $&.include?("to_h")
17
-
18
- h_entries = attrs.split(",").map(&:strip).map do |pair|
19
- k, v = pair.split(":").map(&:strip)
20
- "\"#{k}\" => #{v}"
21
- end.join(", ")
22
-
23
- total += 1
24
- "double(#{name}, #{attrs}, to_h: {#{h_entries}})"
25
- end
26
-
27
- if modified != content
28
- File.write(f, modified)
29
- puts " patched: #{f}"
30
- end
31
- end
32
-
33
- puts "\nAdded to_h to #{total} Tokens doubles."
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Adds to_h to unnamed (no-string-name) Tokens doubles like double(input: 5, ...)
5
-
6
- files = Dir.glob("spec/**/*.rb").sort
7
-
8
- total = 0
9
- files.each do |f|
10
- content = File.read(f)
11
- modified = content.gsub(
12
- /double\((input: (\d+), output: (\d+), cached: (\d+), cache_creation: (\d+))\)/
13
- ) do
14
- attrs, a, b, c, d = $1, $2, $3, $4, $5
15
- next $& if $&.include?("to_h")
16
-
17
- total += 1
18
- "double(#{attrs}, to_h: {\"input\" => #{a}, \"output\" => #{b}, \"cached\" => #{c}, \"cache_creation\" => #{d}})"
19
- end
20
-
21
- if modified != content
22
- File.write(f, modified)
23
- puts " patched: #{f}"
24
- end
25
- end
26
-
27
- puts "\nAdded to_h to #{total} unnamed Tokens doubles."
@@ -1,18 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Explicit one-time conversion of a pre-structured-output Result JSON object.
5
- # Never infer old formats in the runtime reader; never overwrite the source.
6
- require_relative "../lib/phronomy"
7
-
8
- abort "Usage: ruby scripts/migrate_generator_verifier_result.rb OLD.json NEW.json" unless ARGV.length == 2
9
- source, destination = ARGV
10
- value = JSON.parse(File.read(source), allow_duplicate_key: false, allow_nan: false,
11
- allow_comments: false, allow_invalid_escape: false, allow_control_characters: false)
12
- abort "Expected an old Result object without failure" unless value.is_a?(Hash) && !value.key?("failure")
13
- value["failure"] = nil
14
- result = Phronomy::GeneratorVerifier::Result.from_h(value)
15
- File.open(destination, File::WRONLY | File::CREAT | File::EXCL) do |file|
16
- file.write(JSON.pretty_generate(result.to_h))
17
- file.write("\n")
18
- end
@@ -1,108 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Migrates spec files to add agent_definition to Agent::Base subclasses.
5
- # - Named classes: inserts after the class declaration line
6
- # - Anonymous blocks (do...end): inserts after the opening `do`
7
- # - Inline anonymous ({ ... }): expands to multi-line with agent_definition
8
- # Safe: skips classes that already have agent_definition
9
-
10
- require "fileutils"
11
-
12
- SPEC_DIR = File.expand_path("../spec", __dir__)
13
- AGENT_BASE_PATTERN = /Phronomy::Agent::Base/
14
-
15
- def snake_case(class_name)
16
- # Remove trailing Agent/Tool suffixes, snake_case the name
17
- name = class_name.split("::").last
18
- name.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
19
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
20
- .downcase
21
- end
22
-
23
- counter = 0
24
- files_modified = 0
25
-
26
- Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
27
- lines = File.readlines(file, chomp: false)
28
- new_lines = []
29
- modified = false
30
- i = 0
31
-
32
- while i < lines.length
33
- line = lines[i]
34
-
35
- # ── Named class: class FooAgent < Phronomy::Agent::Base ──────────────────
36
- if line =~ /^(\s*)class\s+(\w+)\s*<\s*(?:::)?Phronomy::Agent::Base\s*$/
37
- indent = $1
38
- class_name = $2
39
- id = snake_case(class_name).sub(/_agent$/, "-agent").tr("_", "-")
40
- id = "#{id}-agent" unless id.end_with?("-agent")
41
- new_lines << line
42
- i += 1
43
- # Check if agent_definition already present in the class body
44
- look_ahead = lines[i..]&.take(10) || []
45
- unless look_ahead.any? { |l| l =~ /agent_definition/ }
46
- new_lines << "#{indent} agent_definition id: #{id.inspect}, version: 1\n"
47
- counter += 1
48
- modified = true
49
- end
50
- next
51
- end
52
-
53
- # ── Anonymous block (do...end): Class.new(Phronomy::Agent::Base) do ─────
54
- if line =~ /^(\s*)([\w.]*\s*=\s*)?Class\.new\((?:::)?Phronomy::Agent::Base\)\s+do\s*(\|\w+\|)?\s*$/
55
- indent = $1
56
- # Check if agent_definition already present
57
- look_ahead = lines[i + 1..]&.take(10) || []
58
- new_lines << line
59
- i += 1
60
- unless look_ahead.any? { |l| l =~ /agent_definition/ }
61
- # Derive indentation from the `do` block body (first non-blank line)
62
- inner = lines[i..]&.take(5)&.find { |l| l.strip.length > 0 } || ""
63
- inner_indent = inner.match(/^(\s*)/)[1]
64
- inner_indent = "#{indent} " if inner_indent.strip.length > 0 && inner_indent.length < indent.length + 2
65
- new_lines << "#{inner_indent}agent_definition id: \"test-agent-#{counter += 1}\", version: 1\n"
66
- modified = true
67
- end
68
- next
69
- end
70
-
71
- # ── Inline anonymous: Class.new(Phronomy::Agent::Base) { ... } ───────────
72
- if line =~ /^(\s*)((?:[\w.]*\s*=\s*)?)Class\.new\((?:::)?Phronomy::Agent::Base\)\s*\{(.+)\}\s*$/
73
- indent = $1
74
- assignment = $2
75
- body = $3
76
- # Skip if agent_definition already present
77
- if /agent_definition/.match?(body)
78
- new_lines << line
79
- else
80
- id = counter += 1
81
- definition_line = "agent_definition id: \"test-agent-#{id}\", version: 1;"
82
- new_lines << "#{indent}#{assignment}Class.new(Phronomy::Agent::Base) do\n"
83
- new_lines << "#{indent} #{definition_line}\n"
84
- # Rewrite the original body lines, stripping leading spaces
85
- body.strip.split(";").each do |stmt|
86
- stmt = stmt.strip
87
- next if stmt.empty?
88
- new_lines << "#{indent} #{stmt}\n"
89
- end
90
- new_lines << "#{indent}end\n"
91
- modified = true
92
- end
93
- i += 1
94
- next
95
- end
96
-
97
- new_lines << line
98
- i += 1
99
- end
100
-
101
- if modified
102
- File.write(file, new_lines.join)
103
- files_modified += 1
104
- puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
105
- end
106
- end
107
-
108
- puts "\nDone: #{counter} agent_definition insertions in #{files_modified} files."
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Second pass: handles patterns missed by the first script
5
- # - Class.new(Phronomy::Agent::Base) { ... }.new (inline + .new suffix)
6
- # - Class.new(Phronomy::Agent::Base).new (no block + .new)
7
- # - { Class.new(Phronomy::Agent::Base) } (no block, inside outer { })
8
-
9
- require "fileutils"
10
-
11
- SPEC_DIR = File.expand_path("../spec", __dir__)
12
- counter = 200
13
-
14
- files_modified = 0
15
-
16
- Dir.glob("#{SPEC_DIR}/**/*.rb").sort.each do |file|
17
- content = File.read(file)
18
- new_content = content.dup
19
-
20
- # Pattern A: Class.new(Phronomy::Agent::Base) { body }.new
21
- new_content = new_content.gsub(
22
- /Class\.new\(Phronomy::Agent::Base\) \{([^}]*)\}\.new/
23
- ) do
24
- body = $1
25
- next $& if body.include?("agent_definition")
26
- counter += 1
27
- "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1;#{body}}.new"
28
- end
29
-
30
- # Pattern B: Class.new(Phronomy::Agent::Base).new (no block, chained .new)
31
- new_content = new_content.gsub(
32
- /Class\.new\(Phronomy::Agent::Base\)\.new\b/
33
- ) do
34
- counter += 1
35
- "Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 }.new"
36
- end
37
-
38
- # Pattern C: { Class.new(Phronomy::Agent::Base) } (no block, inside outer block)
39
- new_content = new_content.gsub(
40
- "{ Class.new(Phronomy::Agent::Base) }"
41
- ) do
42
- counter += 1
43
- "{ Class.new(Phronomy::Agent::Base) { agent_definition id: \"test-agent-#{counter}\", version: 1 } }"
44
- end
45
-
46
- if new_content != content
47
- File.write(file, new_content)
48
- files_modified += 1
49
- puts " patched: #{file.sub("#{SPEC_DIR}/", "spec/")}"
50
- end
51
- end
52
-
53
- puts "\nSecond pass done: #{files_modified} files modified."