aidp 0.13.0 → 0.14.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -0
  3. data/lib/aidp/cli/first_run_wizard.rb +28 -303
  4. data/lib/aidp/cli/issue_importer.rb +359 -0
  5. data/lib/aidp/cli.rb +151 -3
  6. data/lib/aidp/daemon/process_manager.rb +146 -0
  7. data/lib/aidp/daemon/runner.rb +232 -0
  8. data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
  9. data/lib/aidp/execute/future_work_backlog.rb +411 -0
  10. data/lib/aidp/execute/guard_policy.rb +246 -0
  11. data/lib/aidp/execute/instruction_queue.rb +131 -0
  12. data/lib/aidp/execute/interactive_repl.rb +335 -0
  13. data/lib/aidp/execute/repl_macros.rb +651 -0
  14. data/lib/aidp/execute/steps.rb +8 -0
  15. data/lib/aidp/execute/work_loop_runner.rb +322 -36
  16. data/lib/aidp/execute/work_loop_state.rb +162 -0
  17. data/lib/aidp/harness/config_schema.rb +88 -0
  18. data/lib/aidp/harness/configuration.rb +48 -1
  19. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +2 -0
  20. data/lib/aidp/init/doc_generator.rb +256 -0
  21. data/lib/aidp/init/project_analyzer.rb +343 -0
  22. data/lib/aidp/init/runner.rb +83 -0
  23. data/lib/aidp/init.rb +5 -0
  24. data/lib/aidp/logger.rb +279 -0
  25. data/lib/aidp/setup/wizard.rb +777 -0
  26. data/lib/aidp/tooling_detector.rb +115 -0
  27. data/lib/aidp/version.rb +1 -1
  28. data/lib/aidp/watch/build_processor.rb +282 -0
  29. data/lib/aidp/watch/plan_generator.rb +166 -0
  30. data/lib/aidp/watch/plan_processor.rb +83 -0
  31. data/lib/aidp/watch/repository_client.rb +243 -0
  32. data/lib/aidp/watch/runner.rb +93 -0
  33. data/lib/aidp/watch/state_store.rb +105 -0
  34. data/lib/aidp/watch.rb +9 -0
  35. data/lib/aidp.rb +14 -0
  36. data/templates/implementation/simple_task.md +36 -0
  37. metadata +26 -1
@@ -3,24 +3,41 @@
3
3
  require_relative "prompt_manager"
4
4
  require_relative "checkpoint"
5
5
  require_relative "checkpoint_display"
6
+ require_relative "guard_policy"
6
7
  require_relative "../harness/test_runner"
7
8
 
8
9
  module Aidp
9
10
  module Execute
10
- # Executes work loops for a single step
11
+ # Executes work loops for a single step using the fix-forward pattern
11
12
  # Responsibilities:
12
13
  # - Create initial PROMPT.md from templates and context
13
14
  # - Loop: send PROMPT.md to agent, run tests/linters, check completion
14
15
  # - Only send test/lint failures back to agent
15
- # - Track iteration count
16
+ # - Never rollback, only move forward through fixes
17
+ # - Track iteration count and state transitions
16
18
  # - Record periodic checkpoints with metrics
19
+ #
20
+ # Fix-Forward State Machine:
21
+ # READY → APPLY_PATCH → TEST → {PASS → DONE | FAIL → DIAGNOSE → NEXT_PATCH} → READY
17
22
  class WorkLoopRunner
23
+ # State machine states
24
+ STATES = {
25
+ ready: "READY", # Ready to start new iteration
26
+ apply_patch: "APPLY_PATCH", # Agent applying changes
27
+ test: "TEST", # Running tests and linters
28
+ pass: "PASS", # Tests passed
29
+ fail: "FAIL", # Tests failed
30
+ diagnose: "DIAGNOSE", # Analyzing failures
31
+ next_patch: "NEXT_PATCH", # Preparing next iteration
32
+ done: "DONE" # Work complete
33
+ }.freeze
18
34
  include Aidp::MessageDisplay
19
35
 
20
- attr_reader :iteration_count, :project_dir
36
+ attr_reader :iteration_count, :project_dir, :current_state
21
37
 
22
38
  MAX_ITERATIONS = 50 # Safety limit
23
39
  CHECKPOINT_INTERVAL = 5 # Record checkpoint every N iterations
40
+ STYLE_GUIDE_REMINDER_INTERVAL = 5 # Re-inject LLM_STYLE_GUIDE every N iterations
24
41
 
25
42
  def initialize(project_dir, provider_manager, config, options = {})
26
43
  @project_dir = project_dir
@@ -30,60 +47,162 @@ module Aidp
30
47
  @test_runner = Aidp::Harness::TestRunner.new(project_dir, config)
31
48
  @checkpoint = Checkpoint.new(project_dir)
32
49
  @checkpoint_display = CheckpointDisplay.new
50
+ @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
33
51
  @iteration_count = 0
34
52
  @step_name = nil
35
53
  @options = options
54
+ @current_state = :ready
55
+ @state_history = []
36
56
  end
37
57
 
38
- # Execute a step using work loop pattern
58
+ # Execute a step using fix-forward work loop pattern
39
59
  # Returns final result when step is complete
60
+ # Never rolls back - only moves forward through fixes
40
61
  def execute_step(step_name, step_spec, context = {})
41
62
  @step_name = step_name
42
63
  @iteration_count = 0
64
+ transition_to(:ready)
43
65
 
44
- display_message("🔄 Starting work loop for step: #{step_name}", type: :info)
66
+ display_message("🔄 Starting fix-forward work loop for step: #{step_name}", type: :info)
67
+ display_message(" State machine: READY → APPLY_PATCH → TEST → {PASS → DONE | FAIL → DIAGNOSE → NEXT_PATCH}", type: :info)
68
+
69
+ # Display guard policy status
70
+ display_guard_policy_status
45
71
 
46
72
  # Create initial PROMPT.md
47
73
  create_initial_prompt(step_spec, context)
48
74
 
49
- # Main work loop
75
+ # Main fix-forward work loop
50
76
  loop do
51
77
  @iteration_count += 1
52
- display_message(" Iteration #{@iteration_count}", type: :info)
78
+ display_message(" Iteration #{@iteration_count} [State: #{STATES[@current_state]}]", type: :info)
53
79
 
54
80
  break if @iteration_count > MAX_ITERATIONS
55
81
 
56
- # Send PROMPT.md to agent
57
- result = send_to_agent
82
+ # State: READY - Starting new iteration
83
+ transition_to(:ready) unless @current_state == :ready
84
+
85
+ # State: APPLY_PATCH - Agent applies changes
86
+ transition_to(:apply_patch)
87
+ result = apply_patch
58
88
 
59
- # Run tests and linters
89
+ # State: TEST - Run tests and linters
90
+ transition_to(:test)
60
91
  test_results = @test_runner.run_tests
61
92
  lint_results = @test_runner.run_linters
62
93
 
63
94
  # Record checkpoint at intervals
64
95
  record_periodic_checkpoint(test_results, lint_results)
65
96
 
66
- # Check if step is complete
67
- if step_complete?(result, test_results, lint_results)
68
- # Record final checkpoint
69
- record_final_checkpoint(test_results, lint_results)
70
- display_message("✅ Step #{step_name} completed after #{@iteration_count} iterations", type: :success)
71
- archive_and_cleanup
72
- return build_success_result(result)
97
+ # Check if tests passed
98
+ tests_pass = test_results[:success] && lint_results[:success]
99
+
100
+ if tests_pass
101
+ # State: PASS - Tests passed
102
+ transition_to(:pass)
103
+
104
+ # Check if agent marked work complete
105
+ if agent_marked_complete?(result)
106
+ # State: DONE - Work complete
107
+ transition_to(:done)
108
+ record_final_checkpoint(test_results, lint_results)
109
+ display_message("✅ Step #{step_name} completed after #{@iteration_count} iterations", type: :success)
110
+ display_state_summary
111
+ archive_and_cleanup
112
+ return build_success_result(result)
113
+ else
114
+ # Tests pass but work not complete - continue
115
+ display_message(" Tests passed but work not marked complete", type: :info)
116
+ transition_to(:next_patch)
117
+ end
118
+ else
119
+ # State: FAIL - Tests failed
120
+ transition_to(:fail)
121
+ display_message(" Tests or linters failed", type: :warning)
122
+
123
+ # State: DIAGNOSE - Analyze failures
124
+ transition_to(:diagnose)
125
+ diagnostic = diagnose_failures(test_results, lint_results)
126
+
127
+ # State: NEXT_PATCH - Prepare for next iteration
128
+ transition_to(:next_patch)
129
+ prepare_next_iteration(test_results, lint_results, diagnostic)
73
130
  end
74
-
75
- # If not complete, prepare next iteration with failures (if any)
76
- prepare_next_iteration(test_results, lint_results)
77
131
  end
78
132
 
79
133
  # Safety: max iterations reached
80
134
  display_message("⚠️ Max iterations (#{MAX_ITERATIONS}) reached for #{step_name}", type: :warning)
135
+ display_state_summary
81
136
  archive_and_cleanup
82
137
  build_max_iterations_result
83
138
  end
84
139
 
85
140
  private
86
141
 
142
+ # Transition to a new state in the fix-forward state machine
143
+ def transition_to(new_state)
144
+ raise "Invalid state: #{new_state}" unless STATES.key?(new_state)
145
+
146
+ @state_history << {
147
+ from: @current_state,
148
+ to: new_state,
149
+ iteration: @iteration_count,
150
+ timestamp: Time.now
151
+ }
152
+ @current_state = new_state
153
+ end
154
+
155
+ # Display summary of state transitions
156
+ def display_state_summary
157
+ display_message("\n📊 Fix-Forward State Summary:", type: :info)
158
+ display_message(" Total iterations: #{@iteration_count}", type: :info)
159
+ display_message(" State transitions: #{@state_history.size}", type: :info)
160
+
161
+ # Count transitions by state
162
+ state_counts = @state_history.group_by { |h| h[:to] }.transform_values(&:size)
163
+ state_counts.each do |state, count|
164
+ display_message(" #{STATES[state]}: #{count} times", type: :info)
165
+ end
166
+ end
167
+
168
+ # Apply patch - send PROMPT.md to agent
169
+ def apply_patch
170
+ send_to_agent
171
+ end
172
+
173
+ # Check if agent marked work complete
174
+ def agent_marked_complete?(result)
175
+ result[:status] == "completed" || prompt_marked_complete?
176
+ end
177
+
178
+ # Diagnose test/lint failures
179
+ # Returns diagnostic information to help agent understand what went wrong
180
+ def diagnose_failures(test_results, lint_results)
181
+ diagnostic = {
182
+ iteration: @iteration_count,
183
+ failures: []
184
+ }
185
+
186
+ unless test_results[:success]
187
+ diagnostic[:failures] << {
188
+ type: "tests",
189
+ count: test_results[:failures]&.size || 0,
190
+ commands: test_results[:failures]&.map { |f| f[:command] } || []
191
+ }
192
+ end
193
+
194
+ unless lint_results[:success]
195
+ diagnostic[:failures] << {
196
+ type: "linters",
197
+ count: lint_results[:failures]&.size || 0,
198
+ commands: lint_results[:failures]&.map { |f| f[:command] } || []
199
+ }
200
+ end
201
+
202
+ display_message(" [DIAGNOSE] Found #{diagnostic[:failures].size} failure types", type: :warning)
203
+ diagnostic
204
+ end
205
+
87
206
  # Create initial PROMPT.md with all context
88
207
  def create_initial_prompt(step_spec, context)
89
208
  template_content = load_template(step_spec["templates"]&.first)
@@ -168,17 +287,6 @@ module Aidp
168
287
  )
169
288
  end
170
289
 
171
- def step_complete?(agent_result, test_results, lint_results)
172
- # Check if agent marked step complete
173
- agent_complete = agent_result[:status] == "completed" || prompt_marked_complete?
174
-
175
- # Check if tests and linters pass
176
- tests_pass = test_results[:success]
177
- linters_pass = lint_results[:success]
178
-
179
- agent_complete && tests_pass && linters_pass
180
- end
181
-
182
290
  def prompt_marked_complete?
183
291
  prompt_content = @prompt_manager.read
184
292
  return false unless prompt_content
@@ -187,30 +295,104 @@ module Aidp
187
295
  prompt_content.match?(/^STATUS:\s*COMPLETE/i)
188
296
  end
189
297
 
190
- def prepare_next_iteration(test_results, lint_results)
298
+ def prepare_next_iteration(test_results, lint_results, diagnostic = nil)
191
299
  # Only append failures to PROMPT.md for agent to see
300
+ # This follows fix-forward: never rollback, only add information for next patch
192
301
  failures = []
193
302
 
303
+ failures << "## Fix-Forward Iteration #{@iteration_count}"
304
+ failures << ""
305
+
306
+ # Re-inject LLM_STYLE_GUIDE at regular intervals to prevent drift
307
+ if should_reinject_style_guide?
308
+ failures << reinject_style_guide_reminder
309
+ failures << ""
310
+ end
311
+
312
+ if diagnostic
313
+ failures << "### Diagnostic Summary"
314
+ diagnostic[:failures].each do |failure_info|
315
+ failures << "- #{failure_info[:type].capitalize}: #{failure_info[:count]} failures"
316
+ end
317
+ failures << ""
318
+ end
319
+
194
320
  unless test_results[:success]
195
- failures << "## Test Failures"
321
+ failures << "### Test Failures"
196
322
  failures << test_results[:output]
197
323
  failures << ""
198
324
  end
199
325
 
200
326
  unless lint_results[:success]
201
- failures << "## Linter Failures"
327
+ failures << "### Linter Failures"
202
328
  failures << lint_results[:output]
203
329
  failures << ""
204
330
  end
205
331
 
206
- return if failures.empty?
332
+ failures << "**Fix-forward instructions**: Do not rollback changes. Build on what exists and fix the failures above."
333
+ failures << ""
334
+
335
+ return if test_results[:success] && lint_results[:success]
207
336
 
208
337
  # Append failures to PROMPT.md
209
338
  current_prompt = @prompt_manager.read
210
339
  updated_prompt = current_prompt + "\n\n---\n\n" + failures.join("\n")
211
340
  @prompt_manager.write(updated_prompt)
212
341
 
213
- display_message(" Added failure reports to PROMPT.md", type: :warning)
342
+ display_message(" [NEXT_PATCH] Added failure reports and diagnostic to PROMPT.md", type: :warning)
343
+ end
344
+
345
+ # Check if we should reinject the style guide at this iteration
346
+ def should_reinject_style_guide?
347
+ # Reinject on intervals (5, 10, 15, etc.) but not on iteration 1
348
+ @iteration_count > 1 && (@iteration_count % STYLE_GUIDE_REMINDER_INTERVAL == 0)
349
+ end
350
+
351
+ # Create style guide reminder text
352
+ def reinject_style_guide_reminder
353
+ style_guide = load_style_guide
354
+ template_content = load_current_template
355
+
356
+ reminder = []
357
+ reminder << "### 🔄 Style Guide & Template Reminder (Iteration #{@iteration_count})"
358
+ reminder << ""
359
+ reminder << "**IMPORTANT**: To prevent drift from project conventions, please review:"
360
+ reminder << ""
361
+
362
+ if style_guide
363
+ reminder << "#### LLM Style Guide"
364
+ reminder << "```"
365
+ # Include first 1000 chars of style guide to keep context manageable
366
+ style_guide_preview = (style_guide.length > 1000) ? style_guide[0...1000] + "\n...(truncated)" : style_guide
367
+ reminder << style_guide_preview
368
+ reminder << "```"
369
+ reminder << ""
370
+ display_message(" [STYLE_GUIDE] Re-injecting LLM_STYLE_GUIDE at iteration #{@iteration_count}", type: :info)
371
+ end
372
+
373
+ if template_content
374
+ reminder << "#### Original Template Requirements"
375
+ reminder << "Remember the original task template requirements. Don't lose sight of the core objectives."
376
+ reminder << ""
377
+ end
378
+
379
+ reminder << "**Note**: Test failures may indicate style guide violations, not just logic errors."
380
+ reminder << "Ensure your fixes align with project conventions above."
381
+
382
+ reminder.join("\n")
383
+ end
384
+
385
+ # Load current step's template content
386
+ def load_current_template
387
+ return nil unless @step_name
388
+
389
+ step_spec = Aidp::Execute::Steps::SPEC[@step_name]
390
+ return nil unless step_spec
391
+
392
+ template_name = step_spec["templates"]&.first
393
+ return nil unless template_name
394
+
395
+ load_template(template_name)
214
396
  end
215
397
 
216
398
  def archive_and_cleanup
@@ -303,6 +485,110 @@ module Aidp
303
485
  summary = @checkpoint.progress_summary
304
486
  @checkpoint_display.display_progress_summary(summary) if summary
305
487
  end
488
+
489
+ # Display guard policy status
490
+ def display_guard_policy_status
491
+ return unless @guard_policy.enabled?
492
+
493
+ display_message("\n🛡️ Safety Guards Enabled:", type: :info)
494
+ summary = @guard_policy.summary
495
+
496
+ if summary[:include_patterns].any?
497
+ display_message(" ✓ Include patterns: #{summary[:include_patterns].join(", ")}", type: :info)
498
+ end
499
+
500
+ if summary[:exclude_patterns].any?
501
+ display_message(" ✗ Exclude patterns: #{summary[:exclude_patterns].join(", ")}", type: :info)
502
+ end
503
+
504
+ if summary[:confirm_patterns].any?
505
+ display_message(" ⚠️ Require confirmation: #{summary[:confirm_patterns].join(", ")}", type: :warning)
506
+ end
507
+
508
+ if summary[:max_lines_per_commit]
509
+ display_message(" 📏 Max lines per commit: #{summary[:max_lines_per_commit]}", type: :info)
510
+ end
511
+
512
+ display_message("")
513
+ end
514
+
515
+ # Validate changes against guard policy
516
+ # Returns validation result with errors if any
517
+ def validate_guard_policy(changed_files = [])
518
+ return {valid: true} unless @guard_policy.enabled?
519
+
520
+ # Get git diff stats for changed files
521
+ diff_stats = get_diff_stats(changed_files)
522
+
523
+ # Validate against policy
524
+ result = @guard_policy.validate_changes(diff_stats)
525
+
526
+ # Display errors if validation failed
527
+ if !result[:valid] && result[:errors]
528
+ display_message("\n🛡️ Guard Policy Violations:", type: :error)
529
+ result[:errors].each do |error|
530
+ display_message(" ✗ #{error}", type: :error)
531
+ end
532
+ display_message("")
533
+ end
534
+
535
+ result
536
+ end
537
+
538
+ # Get git diff statistics for files
539
+ def get_diff_stats(files)
540
+ return {} if files.empty?
541
+
542
+ stats = {}
543
+ files.each do |file|
544
+ # Use git diff to get line counts
545
+ output = `git diff --numstat HEAD -- "#{file}" 2>/dev/null`.strip
546
+ next if output.empty?
547
+
548
+ parts = output.split("\t")
549
+ stats[file] = {
550
+ additions: parts[0].to_i,
551
+ deletions: parts[1].to_i
552
+ }
553
+ end
554
+
555
+ stats
556
+ end
557
+
558
+ # Get list of changed files in current work
559
+ def get_changed_files
560
+ # Get list of modified files from git
561
+ output = `git diff --name-only HEAD 2>/dev/null`.strip
562
+ return [] if output.empty?
563
+
564
+ output.split("\n").map(&:strip).reject(&:empty?)
565
+ end
566
+
567
+ # Handle files requiring confirmation
568
+ def handle_confirmation_requests
569
+ return unless @guard_policy.enabled?
570
+
571
+ files_needing_confirmation = @guard_policy.files_requiring_confirmation
572
+ return if files_needing_confirmation.empty?
573
+
574
+ files_needing_confirmation.each do |file|
575
+ next if @guard_policy.confirmed?(file)
576
+
577
+ display_message("\n⚠️ File requires confirmation: #{file}", type: :warning)
578
+ display_message(" Confirm modification? (y/n): ", type: :warning)
579
+
580
+ # In automated mode, skip confirmation
581
+ if @options[:automated]
582
+ display_message(" [Automated mode: skipping]", type: :info)
583
+ next
584
+ end
585
+
586
+ # For now, auto-confirm in work loops
587
+ # TODO: Implement interactive confirmation via REPL
588
+ @guard_policy.confirm_file(file)
589
+ display_message(" ✓ Confirmed", type: :success)
590
+ end
591
+ end
306
592
  end
307
593
  end
308
594
  end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Aidp
6
+ module Execute
7
+ # Thread-safe state container for async work loop execution
8
+ # Manages execution state, control signals, and queued modifications
9
+ class WorkLoopState
10
+ include MonitorMixin
11
+
12
+ STATES = {
13
+ idle: "IDLE",
14
+ running: "RUNNING",
15
+ paused: "PAUSED",
16
+ cancelled: "CANCELLED",
17
+ completed: "COMPLETED",
18
+ error: "ERROR"
19
+ }.freeze
20
+
21
+ attr_reader :current_state, :iteration, :queued_instructions, :last_error
22
+
23
+ def initialize
24
+ super # Initialize MonitorMixin
25
+ @current_state = :idle
26
+ @iteration = 0
27
+ @queued_instructions = []
28
+ @guard_updates = {}
29
+ @config_reload_requested = false
30
+ @last_error = nil
31
+ @output_buffer = []
32
+ end
33
+
34
+ # Check current state
35
+ def idle? = @current_state == :idle
36
+ def running? = @current_state == :running
37
+ def paused? = @current_state == :paused
38
+ def cancelled? = @current_state == :cancelled
39
+ def completed? = @current_state == :completed
40
+ def error? = @current_state == :error
41
+
42
+ # State transitions (thread-safe)
43
+ def start!
44
+ synchronize do
45
+ raise StateError, "Cannot start from #{@current_state}" unless idle?
46
+ @current_state = :running
47
+ @iteration = 0
48
+ end
49
+ end
50
+
51
+ def pause!
52
+ synchronize do
53
+ raise StateError, "Cannot pause from #{@current_state}" unless running?
54
+ @current_state = :paused
55
+ end
56
+ end
57
+
58
+ def resume!
59
+ synchronize do
60
+ raise StateError, "Cannot resume from #{@current_state}" unless paused?
61
+ @current_state = :running
62
+ end
63
+ end
64
+
65
+ def cancel!
66
+ synchronize do
67
+ raise StateError, "Cannot cancel from #{@current_state}" if completed? || error?
68
+ @current_state = :cancelled
69
+ end
70
+ end
71
+
72
+ def complete!
73
+ synchronize do
74
+ @current_state = :completed
75
+ end
76
+ end
77
+
78
+ def error!(error)
79
+ synchronize do
80
+ @current_state = :error
81
+ @last_error = error
82
+ end
83
+ end
84
+
85
+ # Iteration management
86
+ def increment_iteration!
87
+ synchronize { @iteration += 1 }
88
+ end
89
+
90
+ # Instruction queueing
91
+ def enqueue_instruction(instruction)
92
+ synchronize { @queued_instructions << instruction }
93
+ end
94
+
95
+ def dequeue_instructions
96
+ synchronize do
97
+ instructions = @queued_instructions.dup
98
+ @queued_instructions.clear
99
+ instructions
100
+ end
101
+ end
102
+
103
+ def queued_count
104
+ synchronize { @queued_instructions.size }
105
+ end
106
+
107
+ # Guard/config updates
108
+ def request_guard_update(key, value)
109
+ synchronize { @guard_updates[key] = value }
110
+ end
111
+
112
+ def pending_guard_updates
113
+ synchronize do
114
+ updates = @guard_updates.dup
115
+ @guard_updates.clear
116
+ updates
117
+ end
118
+ end
119
+
120
+ def request_config_reload
121
+ synchronize { @config_reload_requested = true }
122
+ end
123
+
124
+ def config_reload_requested?
125
+ synchronize do
126
+ requested = @config_reload_requested
127
+ @config_reload_requested = false
128
+ requested
129
+ end
130
+ end
131
+
132
+ # Output buffering for streaming display
133
+ def append_output(message, type: :info)
134
+ synchronize do
135
+ @output_buffer << {message: message, type: type, timestamp: Time.now}
136
+ end
137
+ end
138
+
139
+ def drain_output
140
+ synchronize do
141
+ output = @output_buffer.dup
142
+ @output_buffer.clear
143
+ output
144
+ end
145
+ end
146
+
147
+ # Status summary
148
+ def summary
149
+ synchronize do
150
+ {
151
+ state: STATES[@current_state],
152
+ iteration: @iteration,
153
+ queued_instructions: @queued_instructions.size,
154
+ has_error: !@last_error.nil?
155
+ }
156
+ end
157
+ end
158
+
159
+ class StateError < StandardError; end
160
+ end
161
+ end
162
+ end