Class: Aidp::Harness::StateManager

Inherits:
Object
  • Object
show all
Includes:
SafeDirectory
Defined in:
lib/aidp/harness/state_manager.rb

Overview

Manages harness-specific state and persistence, extending existing progress tracking

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from SafeDirectory

#safe_mkdir_p

Constructor Details

#initialize(project_dir, mode, skip_persistence: false) ⇒ StateManager

Returns a new instance of StateManager.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/aidp/harness/state_manager.rb', line 20

def initialize(project_dir, mode, skip_persistence: false)
  @project_dir = project_dir
  @mode = mode
  @state_dir = File.join(project_dir, ".aidp", "harness")
  @state_file = File.join(@state_dir, "#{mode}_state.json")
  @lock_file = File.join(@state_dir, "#{mode}_state.lock")
  @skip_persistence = skip_persistence
  @memory_state = {} if @skip_persistence  # In-memory state for tests

  case mode
  when :analyze
    @progress_tracker = Aidp::Analyze::Progress.new(project_dir, skip_persistence: @skip_persistence)
  when :execute
    @progress_tracker = Aidp::Execute::Progress.new(project_dir, skip_persistence: @skip_persistence)
  else
    raise ArgumentError, "Unsupported mode: #{mode}"
  end

  ensure_state_directory
end

Instance Attribute Details

#modeObject (readonly)

Expose for testability



18
19
20
# File 'lib/aidp/harness/state_manager.rb', line 18

def mode
  @mode
end

#progress_trackerObject (readonly)

Get the underlying progress tracker



202
203
204
# File 'lib/aidp/harness/state_manager.rb', line 202

def progress_tracker
  @progress_tracker
end

#project_dirObject (readonly)

Expose for testability



18
19
20
# File 'lib/aidp/harness/state_manager.rb', line 18

def project_dir
  @project_dir
end

Instance Method Details

#add_user_input(key, value) ⇒ Object

Add user input



122
123
124
125
126
# File 'lib/aidp/harness/state_manager.rb', line 122

def add_user_input(key, value)
  current_input = user_input
  current_input[key] = value
  update_state(user_input: current_input, last_updated: Time.now)
end

#all_steps_completed?Boolean

Check if all steps are completed

Returns:

  • (Boolean)


260
261
262
# File 'lib/aidp/harness/state_manager.rb', line 260

def all_steps_completed?
  completed_steps.size == total_steps
end

#cleanup_old_state(days_old = 7) ⇒ Object

Clean up old state (older than specified days)



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/aidp/harness/state_manager.rb', line 175

def cleanup_old_state(days_old = 7)
  return unless has_state?

  state = load_state
  saved_at = Time.parse(state[:saved_at]) if state[:saved_at]

  if saved_at && (Time.now - saved_at) > (days_old * 24 * 60 * 60)
    clear_state
    true
  else
    false
  end
end

#clear_stateObject

Clear state (for fresh start)



79
80
81
82
83
84
85
86
87
# File 'lib/aidp/harness/state_manager.rb', line 79

def clear_state
  if @skip_persistence
    @memory_state = {}
    return
  end
  with_lock do
    File.delete(@state_file) if File.exist?(@state_file)
  end
end

#clear_workstreamObject

Clear current workstream (switch back to main project)



458
459
460
461
462
463
464
# File 'lib/aidp/harness/state_manager.rb', line 458

def clear_workstream
  update_state(
    current_workstream: nil,
    workstream_path: nil,
    workstream_branch: nil
  )
end

#completed_stepsObject

Get completed steps from progress tracker



205
206
207
# File 'lib/aidp/harness/state_manager.rb', line 205

def completed_steps
  @progress_tracker.completed_steps
end

#current_stepObject

Get current step from progress tracker



210
211
212
# File 'lib/aidp/harness/state_manager.rb', line 210

def current_step
  @progress_tracker.current_step
end

#current_workstreamObject

Get current workstream slug



427
428
429
430
# File 'lib/aidp/harness/state_manager.rb', line 427

def current_workstream
  state = load_state
  state[:current_workstream]
end

#current_workstream_pathObject

Get current workstream path (or project_dir if none)



433
434
435
436
437
438
439
440
# File 'lib/aidp/harness/state_manager.rb', line 433

def current_workstream_path
  slug = current_workstream
  return @project_dir unless slug

  require_relative "../worktree"
  ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
  ws ? ws[:path] : @project_dir
end

#export_stateObject

Export state for debugging



190
191
192
193
194
195
196
197
# File 'lib/aidp/harness/state_manager.rb', line 190

def export_state
  {
    state_file: @state_file,
    has_state: has_state?,
    metadata: ,
    state: load_state
  }
end

#get_performance_metricsObject



476
477
478
479
480
481
482
# File 'lib/aidp/harness/state_manager.rb', line 476

def get_performance_metrics
  {
    efficiency: calculate_efficiency_metrics,
    reliability: calculate_reliability_metrics,
    performance: calculate_performance_metrics
  }
end

#get_token_usage_summaryObject



412
413
414
415
416
417
418
419
420
421
422
# File 'lib/aidp/harness/state_manager.rb', line 412

def get_token_usage_summary
  state = load_state
  token_usage = state[:token_usage] || {}

  {
    total_tokens: token_usage.values.sum { |usage| usage[:total_tokens] },
    total_cost: token_usage.values.sum { |usage| usage[:cost] },
    total_requests: token_usage.values.sum { |usage| usage[:requests] },
    by_provider_model: token_usage
  }
end

#harness_metricsObject

Get harness-specific metrics



303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/aidp/harness/state_manager.rb', line 303

def harness_metrics
  state = load_state
  {
    provider_switches: state[:provider_switches] || 0,
    rate_limit_events: state[:rate_limit_events] || 0,
    user_feedback_requests: state[:user_feedback_requests] || 0,
    error_events: state[:error_events] || 0,
    retry_attempts: state[:retry_attempts] || 0,
    current_provider: state[:current_provider],
    harness_state: state[:state],
    last_activity: state[:last_updated]
  }
end

#has_state?Boolean

Check if state exists

Returns:

  • (Boolean)


42
43
44
45
# File 'lib/aidp/harness/state_manager.rb', line 42

def has_state?
  return false if @skip_persistence
  File.exist?(@state_file)
end

#load_stateObject

Load existing state



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/aidp/harness/state_manager.rb', line 48

def load_state
  return @memory_state if @skip_persistence
  return {} unless has_state?
  with_lock do
    content = File.read(@state_file)
    JSON.parse(content, symbolize_names: true)
  rescue JSON::ParserError => e
    warn "Failed to parse state file: #{e.message}"
    {}
  end
end

#mark_step_completed(step_name) ⇒ Object

Mark step as completed



220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/aidp/harness/state_manager.rb', line 220

def mark_step_completed(step_name)
  @progress_tracker.mark_step_completed(step_name)
  # Also update harness state
  update_state(current_step: nil, last_step_completed: step_name)
  # Increment iteration counter for current workstream if present
  ws_slug = current_workstream
  if ws_slug
    # File layout: this file is in lib/aidp/harness/state_manager.rb
    # workstream_state.rb lives at lib/aidp/workstream_state.rb
    # Correct relative path from here is ../workstream_state
    require_relative "../workstream_state"
    Aidp::WorkstreamState.increment_iteration(slug: ws_slug, project_dir: @project_dir)
  end
end

#mark_step_in_progress(step_name) ⇒ Object

Mark step as in progress



236
237
238
239
240
# File 'lib/aidp/harness/state_manager.rb', line 236

def mark_step_in_progress(step_name)
  @progress_tracker.mark_step_in_progress(step_name)
  # Also update harness state
  update_state(current_step: step_name)
end

#next_provider_reset_timeObject

Get next available provider reset time



168
169
170
171
172
# File 'lib/aidp/harness/state_manager.rb', line 168

def next_provider_reset_time
  rate_limit_info.map do |_provider, info|
    Time.parse(info[:reset_time]) if info[:reset_time]
  end.compact.min
end

#next_stepObject

Get next step to execute



243
244
245
# File 'lib/aidp/harness/state_manager.rb', line 243

def next_step
  @progress_tracker.next_step
end

#progress_percentageObject

Calculate progress percentage



291
292
293
294
# File 'lib/aidp/harness/state_manager.rb', line 291

def progress_percentage
  return 100.0 if all_steps_completed?
  (completed_steps.size.to_f / total_steps * 100).round(2)
end

#progress_summaryObject

Get progress summary



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/aidp/harness/state_manager.rb', line 273

def progress_summary
  {
    mode: @mode,
    completed_steps: completed_steps.size,
    total_steps: total_steps,
    current_step: current_step,
    next_step: next_step,
    all_completed: all_steps_completed?,
    started_at: @progress_tracker.started_at,
    harness_state: has_state? ? load_state : {},
    progress_percentage: progress_percentage,
    session_duration: session_duration,
    harness_metrics: harness_metrics,
    workstream: 
  }
end

#provider_rate_limited?(provider_name) ⇒ Boolean

Check if provider is rate limited

Returns:

  • (Boolean)


159
160
161
162
163
164
165
# File 'lib/aidp/harness/state_manager.rb', line 159

def provider_rate_limited?(provider_name)
  info = rate_limit_info[provider_name]
  return false unless info

  reset_time = Time.parse(info[:reset_time]) if info[:reset_time]
  reset_time && Time.now < reset_time
end

#provider_stateObject

Get provider state



129
130
131
132
# File 'lib/aidp/harness/state_manager.rb', line 129

def provider_state
  state = load_state
  state[:provider_state] || {}
end

#rate_limit_infoObject

Get rate limit information



142
143
144
145
# File 'lib/aidp/harness/state_manager.rb', line 142

def rate_limit_info
  state = load_state
  state[:rate_limit_info] || {}
end

#record_error_event(step_name, error_type, provider_name = nil) ⇒ Object



360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/aidp/harness/state_manager.rb', line 360

def record_error_event(step_name, error_type, provider_name = nil)
  current_state = load_state
  error_events = (current_state[:error_events] || 0) + 1

  update_state(
    error_events: error_events,
    last_error: {
      step: step_name,
      error_type: error_type,
      provider: provider_name,
      timestamp: Time.now
    }
  )
end

#record_provider_switch(from_provider, to_provider) ⇒ Object

Record harness events



318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/aidp/harness/state_manager.rb', line 318

def record_provider_switch(from_provider, to_provider)
  current_state = load_state
  provider_switches = (current_state[:provider_switches] || 0) + 1

  update_state(
    provider_switches: provider_switches,
    last_provider_switch: {
      from: from_provider,
      to: to_provider,
      timestamp: Time.now
    }
  )
end

#record_rate_limit_event(provider_name, reset_time) ⇒ Object



332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/aidp/harness/state_manager.rb', line 332

def record_rate_limit_event(provider_name, reset_time)
  current_state = load_state
  rate_limit_events = (current_state[:rate_limit_events] || 0) + 1

  update_state(
    rate_limit_events: rate_limit_events,
    last_rate_limit: {
      provider: provider_name,
      reset_time: reset_time,
      timestamp: Time.now
    }
  )
end

#record_retry_attempt(step_name, provider_name, attempt_number) ⇒ Object



375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/aidp/harness/state_manager.rb', line 375

def record_retry_attempt(step_name, provider_name, attempt_number)
  current_state = load_state
  retry_attempts = (current_state[:retry_attempts] || 0) + 1

  update_state(
    retry_attempts: retry_attempts,
    last_retry: {
      step: step_name,
      provider: provider_name,
      attempt: attempt_number,
      timestamp: Time.now
    }
  )
end

#record_token_usage(provider_name, model_name, input_tokens, output_tokens, cost = nil) ⇒ Object



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/aidp/harness/state_manager.rb', line 390

def record_token_usage(provider_name, model_name, input_tokens, output_tokens, cost = nil)
  current_state = load_state
  token_usage = current_state[:token_usage] || {}
  key = "#{provider_name}:#{model_name}"

  token_usage[key] ||= {
    input_tokens: 0,
    output_tokens: 0,
    total_tokens: 0,
    cost: 0.0,
    requests: 0
  }

  token_usage[key][:input_tokens] += input_tokens
  token_usage[key][:output_tokens] += output_tokens
  token_usage[key][:total_tokens] += (input_tokens + output_tokens)
  token_usage[key][:cost] += cost if cost
  token_usage[key][:requests] += 1

  update_state(token_usage: token_usage)
end

#record_user_feedback_request(step_name, questions_count) ⇒ Object



346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/aidp/harness/state_manager.rb', line 346

def record_user_feedback_request(step_name, questions_count)
  current_state = load_state
  user_feedback_requests = (current_state[:user_feedback_requests] || 0) + 1

  update_state(
    user_feedback_requests: user_feedback_requests,
    last_user_feedback: {
      step: step_name,
      questions_count: questions_count,
      timestamp: Time.now
    }
  )
end

#reset_allObject

Reset both progress and harness state



265
266
267
268
269
270
# File 'lib/aidp/harness/state_manager.rb', line 265

def reset_all
  @progress_tracker.reset
  clear_state
  # Also clear test workstream variables
  # Test-only instance vars removed; rely on persistence skip flag for isolation
end

#save_state(state_data) ⇒ Object

Save current state



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/aidp/harness/state_manager.rb', line 61

def save_state(state_data)
  if @skip_persistence
    @memory_state = state_data
    return
  end
  with_lock do
     = state_data.merge(
      mode: @mode,
      project_dir: @project_dir,
      saved_at: Time.now.iso8601
    )
    temp_file = "#{@state_file}.tmp"
    File.write(temp_file, JSON.pretty_generate())
    File.rename(temp_file, @state_file)
  end
end

#session_durationObject

Calculate session duration



297
298
299
300
# File 'lib/aidp/harness/state_manager.rb', line 297

def session_duration
  return 0 unless @progress_tracker.started_at
  Time.now - @progress_tracker.started_at
end

#set_current_step(step_name) ⇒ Object

Set current step



110
111
112
# File 'lib/aidp/harness/state_manager.rb', line 110

def set_current_step(step_name)
  update_state(current_step: step_name, last_updated: Time.now)
end

#set_workstream(slug) ⇒ Object

Set current workstream



443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/aidp/harness/state_manager.rb', line 443

def set_workstream(slug)
  require_relative "../worktree"
  # Verify workstream exists
  ws = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
  return false unless ws

  update_state(
    current_workstream: slug,
    workstream_path: ws[:path],
    workstream_branch: ws[:branch]
  )
  true
end

#state_metadataObject

Get state metadata



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/aidp/harness/state_manager.rb', line 90

def 
  return {} unless has_state?
  state = load_state
  {
    mode: state[:mode],
    saved_at: state[:saved_at],
    current_step: state[:current_step],
    state: state[:state],
    last_updated: state[:last_updated]
  }
end

#step_completed?(step_name) ⇒ Boolean

Check if step is completed

Returns:

  • (Boolean)


215
216
217
# File 'lib/aidp/harness/state_manager.rb', line 215

def step_completed?(step_name)
  @progress_tracker.step_completed?(step_name)
end

#total_stepsObject

Get total steps count



248
249
250
251
252
253
254
255
256
257
# File 'lib/aidp/harness/state_manager.rb', line 248

def total_steps
  case @mode
  when :analyze
    Aidp::Analyze::Steps::SPEC.keys.size
  when :execute
    Aidp::Execute::Steps::SPEC.keys.size
  else
    0
  end
end

#update_provider_state(provider_name, provider_data) ⇒ Object

Update provider state



135
136
137
138
139
# File 'lib/aidp/harness/state_manager.rb', line 135

def update_provider_state(provider_name, provider_data)
  current_provider_state = provider_state
  current_provider_state[provider_name] = provider_data
  update_state(provider_state: current_provider_state, last_updated: Time.now)
end

#update_rate_limit_info(provider_name, reset_time, error_count = 0) ⇒ Object

Update rate limit information



148
149
150
151
152
153
154
155
156
# File 'lib/aidp/harness/state_manager.rb', line 148

def update_rate_limit_info(provider_name, reset_time, error_count = 0)
  current_info = rate_limit_info
  current_info[provider_name] = {
    reset_time: reset_time&.iso8601,
    error_count: error_count,
    last_updated: Time.now.iso8601
  }
  update_state(rate_limit_info: current_info, last_updated: Time.now)
end

#update_state(updates) ⇒ Object

Update specific state fields



103
104
105
106
107
# File 'lib/aidp/harness/state_manager.rb', line 103

def update_state(updates)
  current_state = load_state || {}
  updated_state = current_state.merge(updates)
  save_state(updated_state)
end

#user_inputObject

Get user input from state



115
116
117
118
119
# File 'lib/aidp/harness/state_manager.rb', line 115

def user_input
  state = load_state
  return {} unless state
  state[:user_input] || {}
end

#workstream_metadataObject

Get workstream metadata



467
468
469
470
471
472
473
474
# File 'lib/aidp/harness/state_manager.rb', line 467

def 
  state = load_state
  {
    slug: state[:current_workstream],
    path: state[:workstream_path],
    branch: state[:workstream_branch]
  }
end