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.



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

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

#progress_trackerObject (readonly)

Get the underlying progress tracker



199
200
201
# File 'lib/aidp/harness/state_manager.rb', line 199

def progress_tracker
  @progress_tracker
end

Instance Method Details

#add_user_input(key, value) ⇒ Object

Add user input



119
120
121
122
123
# File 'lib/aidp/harness/state_manager.rb', line 119

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)


257
258
259
# File 'lib/aidp/harness/state_manager.rb', line 257

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)



172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/aidp/harness/state_manager.rb', line 172

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)



76
77
78
79
80
81
82
83
84
# File 'lib/aidp/harness/state_manager.rb', line 76

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)



455
456
457
458
459
460
461
# File 'lib/aidp/harness/state_manager.rb', line 455

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

#completed_stepsObject

Get completed steps from progress tracker



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

def completed_steps
  @progress_tracker.completed_steps
end

#current_stepObject

Get current step from progress tracker



207
208
209
# File 'lib/aidp/harness/state_manager.rb', line 207

def current_step
  @progress_tracker.current_step
end

#current_workstreamObject

Get current workstream slug



424
425
426
427
# File 'lib/aidp/harness/state_manager.rb', line 424

def current_workstream
  state = load_state
  state[:current_workstream]
end

#current_workstream_pathObject

Get current workstream path (or project_dir if none)



430
431
432
433
434
435
436
437
# File 'lib/aidp/harness/state_manager.rb', line 430

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



187
188
189
190
191
192
193
194
# File 'lib/aidp/harness/state_manager.rb', line 187

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

#get_performance_metricsObject



473
474
475
476
477
478
479
# File 'lib/aidp/harness/state_manager.rb', line 473

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

#get_token_usage_summaryObject



409
410
411
412
413
414
415
416
417
418
419
# File 'lib/aidp/harness/state_manager.rb', line 409

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



300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/aidp/harness/state_manager.rb', line 300

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)


39
40
41
42
# File 'lib/aidp/harness/state_manager.rb', line 39

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

#load_stateObject

Load existing state



45
46
47
48
49
50
51
52
53
54
55
# File 'lib/aidp/harness/state_manager.rb', line 45

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



217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/aidp/harness/state_manager.rb', line 217

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



233
234
235
236
237
# File 'lib/aidp/harness/state_manager.rb', line 233

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



165
166
167
168
169
# File 'lib/aidp/harness/state_manager.rb', line 165

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



240
241
242
# File 'lib/aidp/harness/state_manager.rb', line 240

def next_step
  @progress_tracker.next_step
end

#progress_percentageObject

Calculate progress percentage



288
289
290
291
# File 'lib/aidp/harness/state_manager.rb', line 288

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



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

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)


156
157
158
159
160
161
162
# File 'lib/aidp/harness/state_manager.rb', line 156

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



126
127
128
129
# File 'lib/aidp/harness/state_manager.rb', line 126

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

#rate_limit_infoObject

Get rate limit information



139
140
141
142
# File 'lib/aidp/harness/state_manager.rb', line 139

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

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



357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/aidp/harness/state_manager.rb', line 357

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



315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/aidp/harness/state_manager.rb', line 315

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



329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/aidp/harness/state_manager.rb', line 329

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



372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/aidp/harness/state_manager.rb', line 372

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



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

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



343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/aidp/harness/state_manager.rb', line 343

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



262
263
264
265
266
267
# File 'lib/aidp/harness/state_manager.rb', line 262

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



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

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



294
295
296
297
# File 'lib/aidp/harness/state_manager.rb', line 294

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



107
108
109
# File 'lib/aidp/harness/state_manager.rb', line 107

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

#set_workstream(slug) ⇒ Object

Set current workstream



440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/aidp/harness/state_manager.rb', line 440

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



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/aidp/harness/state_manager.rb', line 87

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)


212
213
214
# File 'lib/aidp/harness/state_manager.rb', line 212

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

#total_stepsObject

Get total steps count



245
246
247
248
249
250
251
252
253
254
# File 'lib/aidp/harness/state_manager.rb', line 245

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



132
133
134
135
136
# File 'lib/aidp/harness/state_manager.rb', line 132

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



145
146
147
148
149
150
151
152
153
# File 'lib/aidp/harness/state_manager.rb', line 145

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



100
101
102
103
104
# File 'lib/aidp/harness/state_manager.rb', line 100

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



112
113
114
115
116
# File 'lib/aidp/harness/state_manager.rb', line 112

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

#workstream_metadataObject

Get workstream metadata



464
465
466
467
468
469
470
471
# File 'lib/aidp/harness/state_manager.rb', line 464

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