aidp 0.5.0 → 0.8.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +128 -151
  3. data/bin/aidp +1 -1
  4. data/lib/aidp/analysis/kb_inspector.rb +471 -0
  5. data/lib/aidp/analysis/seams.rb +159 -0
  6. data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +480 -0
  7. data/lib/aidp/analysis/tree_sitter_scan.rb +686 -0
  8. data/lib/aidp/analyze/error_handler.rb +2 -78
  9. data/lib/aidp/analyze/json_file_storage.rb +292 -0
  10. data/lib/aidp/analyze/progress.rb +12 -0
  11. data/lib/aidp/analyze/progress_visualizer.rb +12 -17
  12. data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
  13. data/lib/aidp/analyze/runner.rb +256 -87
  14. data/lib/aidp/analyze/steps.rb +6 -0
  15. data/lib/aidp/cli/jobs_command.rb +103 -435
  16. data/lib/aidp/cli.rb +317 -191
  17. data/lib/aidp/config.rb +298 -10
  18. data/lib/aidp/debug_logger.rb +195 -0
  19. data/lib/aidp/debug_mixin.rb +187 -0
  20. data/lib/aidp/execute/progress.rb +9 -0
  21. data/lib/aidp/execute/runner.rb +221 -40
  22. data/lib/aidp/execute/steps.rb +17 -7
  23. data/lib/aidp/execute/workflow_selector.rb +211 -0
  24. data/lib/aidp/harness/completion_checker.rb +268 -0
  25. data/lib/aidp/harness/condition_detector.rb +1526 -0
  26. data/lib/aidp/harness/config_loader.rb +373 -0
  27. data/lib/aidp/harness/config_manager.rb +382 -0
  28. data/lib/aidp/harness/config_schema.rb +1006 -0
  29. data/lib/aidp/harness/config_validator.rb +355 -0
  30. data/lib/aidp/harness/configuration.rb +477 -0
  31. data/lib/aidp/harness/enhanced_runner.rb +494 -0
  32. data/lib/aidp/harness/error_handler.rb +616 -0
  33. data/lib/aidp/harness/provider_config.rb +423 -0
  34. data/lib/aidp/harness/provider_factory.rb +306 -0
  35. data/lib/aidp/harness/provider_manager.rb +1269 -0
  36. data/lib/aidp/harness/provider_type_checker.rb +88 -0
  37. data/lib/aidp/harness/runner.rb +411 -0
  38. data/lib/aidp/harness/state/errors.rb +28 -0
  39. data/lib/aidp/harness/state/metrics.rb +219 -0
  40. data/lib/aidp/harness/state/persistence.rb +128 -0
  41. data/lib/aidp/harness/state/provider_state.rb +132 -0
  42. data/lib/aidp/harness/state/ui_state.rb +68 -0
  43. data/lib/aidp/harness/state/workflow_state.rb +123 -0
  44. data/lib/aidp/harness/state_manager.rb +586 -0
  45. data/lib/aidp/harness/status_display.rb +888 -0
  46. data/lib/aidp/harness/ui/base.rb +16 -0
  47. data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
  48. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
  49. data/lib/aidp/harness/ui/error_handler.rb +132 -0
  50. data/lib/aidp/harness/ui/frame_manager.rb +361 -0
  51. data/lib/aidp/harness/ui/job_monitor.rb +500 -0
  52. data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
  53. data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
  54. data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
  55. data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
  56. data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
  57. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
  58. data/lib/aidp/harness/ui/progress_display.rb +280 -0
  59. data/lib/aidp/harness/ui/question_collector.rb +141 -0
  60. data/lib/aidp/harness/ui/spinner_group.rb +184 -0
  61. data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
  62. data/lib/aidp/harness/ui/status_manager.rb +312 -0
  63. data/lib/aidp/harness/ui/status_widget.rb +280 -0
  64. data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
  65. data/lib/aidp/harness/user_interface.rb +2381 -0
  66. data/lib/aidp/provider_manager.rb +131 -7
  67. data/lib/aidp/providers/anthropic.rb +28 -109
  68. data/lib/aidp/providers/base.rb +170 -0
  69. data/lib/aidp/providers/cursor.rb +52 -183
  70. data/lib/aidp/providers/gemini.rb +24 -109
  71. data/lib/aidp/providers/macos_ui.rb +99 -5
  72. data/lib/aidp/providers/opencode.rb +194 -0
  73. data/lib/aidp/storage/csv_storage.rb +172 -0
  74. data/lib/aidp/storage/file_manager.rb +214 -0
  75. data/lib/aidp/storage/json_storage.rb +140 -0
  76. data/lib/aidp/version.rb +1 -1
  77. data/lib/aidp.rb +56 -35
  78. data/templates/ANALYZE/06a_tree_sitter_scan.md +217 -0
  79. data/templates/COMMON/AGENT_BASE.md +11 -0
  80. data/templates/EXECUTE/00_PRD.md +4 -4
  81. data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
  82. data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
  83. data/templates/EXECUTE/08_TASKS.md +4 -4
  84. data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
  85. data/templates/README.md +279 -0
  86. data/templates/aidp-development.yml.example +373 -0
  87. data/templates/aidp-minimal.yml.example +48 -0
  88. data/templates/aidp-production.yml.example +475 -0
  89. data/templates/aidp.yml.example +598 -0
  90. metadata +106 -64
  91. data/lib/aidp/analyze/agent_personas.rb +0 -71
  92. data/lib/aidp/analyze/agent_tool_executor.rb +0 -445
  93. data/lib/aidp/analyze/data_retention_manager.rb +0 -426
  94. data/lib/aidp/analyze/database.rb +0 -260
  95. data/lib/aidp/analyze/dependencies.rb +0 -335
  96. data/lib/aidp/analyze/export_manager.rb +0 -425
  97. data/lib/aidp/analyze/focus_guidance.rb +0 -517
  98. data/lib/aidp/analyze/incremental_analyzer.rb +0 -543
  99. data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
  100. data/lib/aidp/analyze/large_analysis_progress.rb +0 -504
  101. data/lib/aidp/analyze/memory_manager.rb +0 -365
  102. data/lib/aidp/analyze/metrics_storage.rb +0 -336
  103. data/lib/aidp/analyze/parallel_processor.rb +0 -460
  104. data/lib/aidp/analyze/performance_optimizer.rb +0 -694
  105. data/lib/aidp/analyze/repository_chunker.rb +0 -704
  106. data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
  107. data/lib/aidp/analyze/storage.rb +0 -662
  108. data/lib/aidp/analyze/tool_configuration.rb +0 -456
  109. data/lib/aidp/analyze/tool_modernization.rb +0 -750
  110. data/lib/aidp/database/pg_adapter.rb +0 -148
  111. data/lib/aidp/database_config.rb +0 -69
  112. data/lib/aidp/database_connection.rb +0 -72
  113. data/lib/aidp/database_migration.rb +0 -158
  114. data/lib/aidp/job_manager.rb +0 -41
  115. data/lib/aidp/jobs/base_job.rb +0 -47
  116. data/lib/aidp/jobs/provider_execution_job.rb +0 -96
  117. data/lib/aidp/project_detector.rb +0 -117
  118. data/lib/aidp/providers/agent_supervisor.rb +0 -348
  119. data/lib/aidp/providers/supervised_base.rb +0 -317
  120. data/lib/aidp/providers/supervised_cursor.rb +0 -22
  121. data/lib/aidp/sync.rb +0 -13
  122. data/lib/aidp/workspace.rb +0 -19
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ module State
6
+ # Manages metrics, analytics, and performance calculations
7
+ class Metrics
8
+ def initialize(persistence, workflow_state)
9
+ @persistence = persistence
10
+ @workflow_state = workflow_state
11
+ end
12
+
13
+ def record_provider_switch(from_provider, to_provider)
14
+ current_state = load_state
15
+ provider_switches = (current_state[:provider_switches] || 0) + 1
16
+
17
+ update_state(
18
+ provider_switches: provider_switches,
19
+ last_provider_switch: create_switch_record(from_provider, to_provider)
20
+ )
21
+ end
22
+
23
+ def record_rate_limit_event(provider_name, reset_time)
24
+ current_state = load_state
25
+ rate_limit_events = (current_state[:rate_limit_events] || 0) + 1
26
+
27
+ update_state(
28
+ rate_limit_events: rate_limit_events,
29
+ last_rate_limit: create_rate_limit_record(provider_name, reset_time)
30
+ )
31
+ end
32
+
33
+ def record_user_feedback_request(step_name, questions_count)
34
+ current_state = load_state
35
+ user_feedback_requests = (current_state[:user_feedback_requests] || 0) + 1
36
+
37
+ update_state(
38
+ user_feedback_requests: user_feedback_requests,
39
+ last_user_feedback: create_feedback_record(step_name, questions_count)
40
+ )
41
+ end
42
+
43
+ def record_error_event(step_name, error_type, provider_name = nil)
44
+ current_state = load_state
45
+ error_events = (current_state[:error_events] || 0) + 1
46
+
47
+ update_state(
48
+ error_events: error_events,
49
+ last_error: create_error_record(step_name, error_type, provider_name)
50
+ )
51
+ end
52
+
53
+ def record_retry_attempt(step_name, provider_name, attempt_number)
54
+ current_state = load_state
55
+ retry_attempts = (current_state[:retry_attempts] || 0) + 1
56
+
57
+ update_state(
58
+ retry_attempts: retry_attempts,
59
+ last_retry: create_retry_record(step_name, provider_name, attempt_number)
60
+ )
61
+ end
62
+
63
+ def harness_metrics
64
+ state = load_state
65
+ {
66
+ provider_switches: state[:provider_switches] || 0,
67
+ rate_limit_events: state[:rate_limit_events] || 0,
68
+ user_feedback_requests: state[:user_feedback_requests] || 0,
69
+ error_events: state[:error_events] || 0,
70
+ retry_attempts: state[:retry_attempts] || 0,
71
+ current_provider: state[:current_provider],
72
+ harness_state: state[:state],
73
+ last_activity: state[:last_updated]
74
+ }
75
+ end
76
+
77
+ def performance_metrics
78
+ {
79
+ efficiency: calculate_efficiency_metrics,
80
+ reliability: calculate_reliability_metrics,
81
+ performance: calculate_performance_metrics
82
+ }
83
+ end
84
+
85
+ private
86
+
87
+ def load_state
88
+ @persistence.load_state
89
+ end
90
+
91
+ def update_state(updates)
92
+ current_state = load_state
93
+ updated_state = current_state.merge(updates)
94
+ updated_state[:last_updated] = Time.now
95
+ @persistence.save_state(updated_state)
96
+ end
97
+
98
+ def create_switch_record(from_provider, to_provider)
99
+ {
100
+ from: from_provider,
101
+ to: to_provider,
102
+ timestamp: Time.now
103
+ }
104
+ end
105
+
106
+ def create_rate_limit_record(provider_name, reset_time)
107
+ {
108
+ provider: provider_name,
109
+ reset_time: reset_time,
110
+ timestamp: Time.now
111
+ }
112
+ end
113
+
114
+ def create_feedback_record(step_name, questions_count)
115
+ {
116
+ step: step_name,
117
+ questions_count: questions_count,
118
+ timestamp: Time.now
119
+ }
120
+ end
121
+
122
+ def create_error_record(step_name, error_type, provider_name)
123
+ {
124
+ step: step_name,
125
+ error_type: error_type,
126
+ provider: provider_name,
127
+ timestamp: Time.now
128
+ }
129
+ end
130
+
131
+ def create_retry_record(step_name, provider_name, attempt_number)
132
+ {
133
+ step: step_name,
134
+ provider: provider_name,
135
+ attempt: attempt_number,
136
+ timestamp: Time.now
137
+ }
138
+ end
139
+
140
+ def calculate_efficiency_metrics
141
+ {
142
+ provider_switches_per_step: calculate_switches_per_step,
143
+ average_retries_per_step: calculate_retries_per_step,
144
+ user_feedback_ratio: calculate_feedback_ratio
145
+ }
146
+ end
147
+
148
+ def calculate_reliability_metrics
149
+ {
150
+ error_rate: calculate_error_rate,
151
+ rate_limit_frequency: calculate_rate_limit_frequency,
152
+ success_rate: calculate_success_rate
153
+ }
154
+ end
155
+
156
+ def calculate_performance_metrics
157
+ {
158
+ session_duration: @workflow_state.session_duration,
159
+ steps_per_hour: calculate_steps_per_hour,
160
+ average_step_duration: calculate_average_step_duration
161
+ }
162
+ end
163
+
164
+ def calculate_switches_per_step
165
+ provider_switches = load_state[:provider_switches] || 0
166
+ completed_steps_count = @workflow_state.completed_steps.size
167
+ return 0 if completed_steps_count == 0
168
+ (provider_switches.to_f / completed_steps_count).round(2)
169
+ end
170
+
171
+ def calculate_retries_per_step
172
+ retry_attempts = load_state[:retry_attempts] || 0
173
+ completed_steps_count = @workflow_state.completed_steps.size
174
+ return 0 if completed_steps_count == 0
175
+ (retry_attempts.to_f / completed_steps_count).round(2)
176
+ end
177
+
178
+ def calculate_feedback_ratio
179
+ user_feedback_requests = load_state[:user_feedback_requests] || 0
180
+ completed_steps_count = @workflow_state.completed_steps.size
181
+ return 0 if completed_steps_count == 0
182
+ (user_feedback_requests.to_f / completed_steps_count).round(2)
183
+ end
184
+
185
+ def calculate_error_rate
186
+ error_events = load_state[:error_events] || 0
187
+ total_events = error_events + @workflow_state.completed_steps.size
188
+ return 0 if total_events == 0
189
+ (error_events.to_f / total_events * 100).round(2)
190
+ end
191
+
192
+ def calculate_rate_limit_frequency
193
+ rate_limit_events = load_state[:rate_limit_events] || 0
194
+ session_duration_hours = @workflow_state.session_duration / 3600.0
195
+ return 0 if session_duration_hours == 0
196
+ (rate_limit_events / session_duration_hours).round(2)
197
+ end
198
+
199
+ def calculate_success_rate
200
+ error_events = load_state[:error_events] || 0
201
+ total_attempts = @workflow_state.completed_steps.size + error_events
202
+ return 100 if total_attempts == 0
203
+ ((@workflow_state.completed_steps.size.to_f / total_attempts) * 100).round(2)
204
+ end
205
+
206
+ def calculate_steps_per_hour
207
+ session_duration_hours = @workflow_state.session_duration / 3600.0
208
+ return 0 if session_duration_hours == 0
209
+ (@workflow_state.completed_steps.size / session_duration_hours).round(2)
210
+ end
211
+
212
+ def calculate_average_step_duration
213
+ return 0 if @workflow_state.completed_steps.size == 0
214
+ (@workflow_state.session_duration / @workflow_state.completed_steps.size).round(2)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module Aidp
7
+ module Harness
8
+ module State
9
+ # Handles file I/O and persistence for state management
10
+ class Persistence
11
+ def initialize(project_dir, mode)
12
+ @project_dir = project_dir
13
+ @mode = mode
14
+ @state_dir = File.join(project_dir, ".aidp", "harness")
15
+ @state_file = File.join(@state_dir, "#{mode}_state.json")
16
+ @lock_file = File.join(@state_dir, "#{mode}_state.lock")
17
+ ensure_state_directory
18
+ end
19
+
20
+ def has_state?
21
+ return false if test_mode?
22
+ File.exist?(@state_file)
23
+ end
24
+
25
+ def load_state
26
+ return {} if test_mode? || !has_state?
27
+
28
+ with_lock do
29
+ content = File.read(@state_file)
30
+ JSON.parse(content, symbolize_names: true)
31
+ rescue JSON::ParserError => e
32
+ warn "Failed to parse state file: #{e.message}"
33
+ {}
34
+ end
35
+ end
36
+
37
+ def save_state(state_data)
38
+ return if test_mode?
39
+
40
+ with_lock do
41
+ state_with_metadata = add_metadata(state_data)
42
+ write_atomically(state_with_metadata)
43
+ end
44
+ end
45
+
46
+ def clear_state
47
+ return if test_mode?
48
+
49
+ with_lock do
50
+ File.delete(@state_file) if File.exist?(@state_file)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def test_mode?
57
+ ENV["RACK_ENV"] == "test" || defined?(RSpec)
58
+ end
59
+
60
+ def add_metadata(state_data)
61
+ state_data.merge(
62
+ mode: @mode,
63
+ project_dir: @project_dir,
64
+ saved_at: Time.now.iso8601
65
+ )
66
+ end
67
+
68
+ def write_atomically(state_with_metadata)
69
+ temp_file = "#{@state_file}.tmp"
70
+ File.write(temp_file, JSON.pretty_generate(state_with_metadata))
71
+ File.rename(temp_file, @state_file)
72
+ end
73
+
74
+ def ensure_state_directory
75
+ FileUtils.mkdir_p(@state_dir) unless Dir.exist?(@state_dir)
76
+ end
77
+
78
+ def with_lock(&block)
79
+ return yield if test_mode?
80
+
81
+ acquire_lock_with_timeout(&block)
82
+ ensure
83
+ cleanup_lock_file
84
+ end
85
+
86
+ def acquire_lock_with_timeout(&block)
87
+ lock_acquired = false
88
+ timeout = 30
89
+ start_time = Time.now
90
+
91
+ while (Time.now - start_time) < timeout
92
+ lock_acquired = try_acquire_lock(&block)
93
+ break if lock_acquired
94
+ sleep_briefly
95
+ end
96
+
97
+ raise_lock_timeout_error unless lock_acquired
98
+ end
99
+
100
+ def try_acquire_lock(&block)
101
+ File.open(@lock_file, File::CREAT | File::EXCL | File::WRONLY) do |_lock|
102
+ yield
103
+ true
104
+ end
105
+ rescue Errno::EEXIST
106
+ false
107
+ end
108
+
109
+ def sleep_briefly
110
+ require "async"
111
+ if Async::Task.current?
112
+ Async::Task.current.sleep(0.1)
113
+ else
114
+ sleep(0.1)
115
+ end
116
+ end
117
+
118
+ def raise_lock_timeout_error
119
+ raise "Could not acquire state lock within 30 seconds"
120
+ end
121
+
122
+ def cleanup_lock_file
123
+ File.delete(@lock_file) if File.exist?(@lock_file)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ module State
6
+ # Manages provider-specific state and rate limiting
7
+ class ProviderState
8
+ def initialize(persistence)
9
+ @persistence = persistence
10
+ end
11
+
12
+ def provider_state
13
+ state[:provider_state] || {}
14
+ end
15
+
16
+ def update_provider_state(provider_name, provider_data)
17
+ current_provider_state = provider_state
18
+ current_provider_state[provider_name] = provider_data
19
+ update_state(provider_state: current_provider_state)
20
+ end
21
+
22
+ def rate_limit_info
23
+ state[:rate_limit_info] || {}
24
+ end
25
+
26
+ def update_rate_limit_info(provider_name, reset_time, error_count = 0)
27
+ current_info = rate_limit_info
28
+ current_info[provider_name] = create_rate_limit_entry(reset_time, error_count)
29
+ update_state(rate_limit_info: current_info)
30
+ end
31
+
32
+ def provider_rate_limited?(provider_name)
33
+ info = rate_limit_info[provider_name]
34
+ return false unless info
35
+
36
+ reset_time = parse_reset_time(info[:reset_time])
37
+ reset_time && Time.now < reset_time
38
+ end
39
+
40
+ def next_provider_reset_time
41
+ rate_limit_info.map do |_provider, info|
42
+ parse_reset_time(info[:reset_time])
43
+ end.compact.min
44
+ end
45
+
46
+ def token_usage
47
+ state[:token_usage] || {}
48
+ end
49
+
50
+ def record_token_usage(provider_name, model_name, input_tokens, output_tokens, cost = nil)
51
+ current_usage = token_usage
52
+ key = "#{provider_name}:#{model_name}"
53
+
54
+ current_usage[key] = update_token_usage_entry(
55
+ current_usage[key], input_tokens, output_tokens, cost
56
+ )
57
+
58
+ update_state(token_usage: current_usage)
59
+ end
60
+
61
+ def token_usage_summary
62
+ usage = token_usage
63
+ {
64
+ total_tokens: calculate_total_tokens(usage),
65
+ total_cost: calculate_total_cost(usage),
66
+ total_requests: calculate_total_requests(usage),
67
+ by_provider_model: usage
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def state
74
+ @persistence.load_state
75
+ end
76
+
77
+ def update_state(updates)
78
+ current_state = state
79
+ updated_state = current_state.merge(updates)
80
+ updated_state[:last_updated] = Time.now
81
+ @persistence.save_state(updated_state)
82
+ end
83
+
84
+ def create_rate_limit_entry(reset_time, error_count)
85
+ {
86
+ reset_time: reset_time&.iso8601,
87
+ error_count: error_count,
88
+ last_updated: Time.now.iso8601
89
+ }
90
+ end
91
+
92
+ def parse_reset_time(reset_time_string)
93
+ Time.parse(reset_time_string) if reset_time_string
94
+ end
95
+
96
+ def update_token_usage_entry(existing_entry, input_tokens, output_tokens, cost)
97
+ existing_entry ||= create_empty_token_usage_entry
98
+
99
+ existing_entry[:input_tokens] += input_tokens
100
+ existing_entry[:output_tokens] += output_tokens
101
+ existing_entry[:total_tokens] += (input_tokens + output_tokens)
102
+ existing_entry[:cost] += cost if cost
103
+ existing_entry[:requests] += 1
104
+
105
+ existing_entry
106
+ end
107
+
108
+ def create_empty_token_usage_entry
109
+ {
110
+ input_tokens: 0,
111
+ output_tokens: 0,
112
+ total_tokens: 0,
113
+ cost: 0.0,
114
+ requests: 0
115
+ }
116
+ end
117
+
118
+ def calculate_total_tokens(usage)
119
+ usage.values.sum { |entry| entry[:total_tokens] }
120
+ end
121
+
122
+ def calculate_total_cost(usage)
123
+ usage.values.sum { |entry| entry[:cost] }
124
+ end
125
+
126
+ def calculate_total_requests(usage)
127
+ usage.values.sum { |entry| entry[:requests] }
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Harness
5
+ module State
6
+ # Manages UI-specific state and user interactions
7
+ class UIState
8
+ def initialize(persistence)
9
+ @persistence = persistence
10
+ end
11
+
12
+ def user_input
13
+ state[:user_input] || {}
14
+ end
15
+
16
+ def add_user_input(key, value)
17
+ current_input = user_input
18
+ current_input[key] = value
19
+ update_state(user_input: current_input)
20
+ end
21
+
22
+ def execution_log
23
+ state[:execution_log] || []
24
+ end
25
+
26
+ def add_execution_log(entry)
27
+ current_log = execution_log
28
+ current_log << entry
29
+ update_state(execution_log: current_log)
30
+ end
31
+
32
+ def current_step
33
+ state[:current_step]
34
+ end
35
+
36
+ def set_current_step(step_name)
37
+ update_state(current_step: step_name)
38
+ end
39
+
40
+ def state_metadata
41
+ return {} unless @persistence.has_state?
42
+
43
+ state_data = state
44
+ {
45
+ mode: state_data[:mode],
46
+ saved_at: state_data[:saved_at],
47
+ current_step: state_data[:current_step],
48
+ state: state_data[:state],
49
+ last_updated: state_data[:last_updated]
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def state
56
+ @persistence.load_state
57
+ end
58
+
59
+ def update_state(updates)
60
+ current_state = state
61
+ updated_state = current_state.merge(updates)
62
+ updated_state[:last_updated] = Time.now
63
+ @persistence.save_state(updated_state)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../execute/progress"
4
+ require_relative "../../analyze/progress"
5
+ require_relative "../../execute/steps"
6
+ require_relative "../../analyze/steps"
7
+
8
+ module Aidp
9
+ module Harness
10
+ module State
11
+ # Manages workflow-specific state and progress tracking
12
+ class WorkflowState
13
+ def initialize(persistence, project_dir, mode)
14
+ @persistence = persistence
15
+ @project_dir = project_dir
16
+ @mode = mode
17
+ @progress_tracker = create_progress_tracker
18
+ end
19
+
20
+ def completed_steps
21
+ @progress_tracker.completed_steps
22
+ end
23
+
24
+ def current_step
25
+ @progress_tracker.current_step
26
+ end
27
+
28
+ def step_completed?(step_name)
29
+ @progress_tracker.step_completed?(step_name)
30
+ end
31
+
32
+ def mark_step_completed(step_name)
33
+ @progress_tracker.mark_step_completed(step_name)
34
+ update_harness_state(current_step: nil, last_step_completed: step_name)
35
+ end
36
+
37
+ def mark_step_in_progress(step_name)
38
+ @progress_tracker.mark_step_in_progress(step_name)
39
+ update_harness_state(current_step: step_name)
40
+ end
41
+
42
+ def next_step
43
+ @progress_tracker.next_step
44
+ end
45
+
46
+ def total_steps
47
+ steps_spec.keys.size
48
+ end
49
+
50
+ def all_steps_completed?
51
+ completed_steps.size == total_steps
52
+ end
53
+
54
+ def progress_percentage
55
+ return 100.0 if all_steps_completed?
56
+ (completed_steps.size.to_f / total_steps * 100).round(2)
57
+ end
58
+
59
+ def session_duration
60
+ return 0 unless @progress_tracker.started_at
61
+ Time.now - @progress_tracker.started_at
62
+ end
63
+
64
+ def reset_all
65
+ @progress_tracker.reset
66
+ @persistence.clear_state
67
+ end
68
+
69
+ def progress_summary
70
+ {
71
+ mode: @mode,
72
+ completed_steps: completed_steps.size,
73
+ total_steps: total_steps,
74
+ current_step: current_step,
75
+ next_step: next_step,
76
+ all_completed: all_steps_completed?,
77
+ started_at: @progress_tracker.started_at,
78
+ harness_state: harness_state,
79
+ progress_percentage: progress_percentage,
80
+ session_duration: session_duration
81
+ }
82
+ end
83
+
84
+ attr_reader :progress_tracker
85
+
86
+ private
87
+
88
+ def create_progress_tracker
89
+ case @mode
90
+ when :analyze
91
+ Aidp::Analyze::Progress.new(@project_dir)
92
+ when :execute
93
+ Aidp::Execute::Progress.new(@project_dir)
94
+ else
95
+ raise ArgumentError, "Unsupported mode: #{@mode}"
96
+ end
97
+ end
98
+
99
+ def steps_spec
100
+ case @mode
101
+ when :analyze
102
+ Aidp::Analyze::Steps::SPEC
103
+ when :execute
104
+ Aidp::Execute::Steps::SPEC
105
+ else
106
+ {}
107
+ end
108
+ end
109
+
110
+ def harness_state
111
+ @persistence.has_state? ? @persistence.load_state : {}
112
+ end
113
+
114
+ def update_harness_state(updates)
115
+ current_state = @persistence.load_state
116
+ updated_state = current_state.merge(updates)
117
+ updated_state[:last_updated] = Time.now
118
+ @persistence.save_state(updated_state)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end