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.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/lib/aidp/cli/first_run_wizard.rb +28 -303
- data/lib/aidp/cli/issue_importer.rb +359 -0
- data/lib/aidp/cli.rb +151 -3
- data/lib/aidp/daemon/process_manager.rb +146 -0
- data/lib/aidp/daemon/runner.rb +232 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +216 -0
- data/lib/aidp/execute/future_work_backlog.rb +411 -0
- data/lib/aidp/execute/guard_policy.rb +246 -0
- data/lib/aidp/execute/instruction_queue.rb +131 -0
- data/lib/aidp/execute/interactive_repl.rb +335 -0
- data/lib/aidp/execute/repl_macros.rb +651 -0
- data/lib/aidp/execute/steps.rb +8 -0
- data/lib/aidp/execute/work_loop_runner.rb +322 -36
- data/lib/aidp/execute/work_loop_state.rb +162 -0
- data/lib/aidp/harness/config_schema.rb +88 -0
- data/lib/aidp/harness/configuration.rb +48 -1
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +2 -0
- data/lib/aidp/init/doc_generator.rb +256 -0
- data/lib/aidp/init/project_analyzer.rb +343 -0
- data/lib/aidp/init/runner.rb +83 -0
- data/lib/aidp/init.rb +5 -0
- data/lib/aidp/logger.rb +279 -0
- data/lib/aidp/setup/wizard.rb +777 -0
- data/lib/aidp/tooling_detector.rb +115 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +282 -0
- data/lib/aidp/watch/plan_generator.rb +166 -0
- data/lib/aidp/watch/plan_processor.rb +83 -0
- data/lib/aidp/watch/repository_client.rb +243 -0
- data/lib/aidp/watch/runner.rb +93 -0
- data/lib/aidp/watch/state_store.rb +105 -0
- data/lib/aidp/watch.rb +9 -0
- data/lib/aidp.rb +14 -0
- data/templates/implementation/simple_task.md +36 -0
- 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
|
-
# -
|
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
|
-
#
|
57
|
-
|
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
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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 << "
|
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 << "
|
327
|
+
failures << "### Linter Failures"
|
202
328
|
failures << lint_results[:output]
|
203
329
|
failures << ""
|
204
330
|
end
|
205
331
|
|
206
|
-
|
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
|