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
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ module Execute
5
+ # Manages queued instructions and plan modifications during work loop execution
6
+ # Instructions are merged into PROMPT.md at the next iteration
7
+ class InstructionQueue
8
+ Instruction = Struct.new(:content, :type, :priority, :timestamp, keyword_init: true)
9
+
10
+ INSTRUCTION_TYPES = {
11
+ user_input: "USER_INPUT", # Direct user instructions
12
+ plan_update: "PLAN_UPDATE", # Changes to implementation contract
13
+ constraint: "CONSTRAINT", # New constraints or requirements
14
+ clarification: "CLARIFICATION", # Clarifications on existing work
15
+ acceptance: "ACCEPTANCE_CRITERIA" # New acceptance criteria
16
+ }.freeze
17
+
18
+ PRIORITIES = {
19
+ critical: 1,
20
+ high: 2,
21
+ normal: 3,
22
+ low: 4
23
+ }.freeze
24
+
25
+ def initialize
26
+ @instructions = []
27
+ end
28
+
29
+ # Add instruction to queue
30
+ def enqueue(content, type: :user_input, priority: :normal)
31
+ validate_type!(type)
32
+ validate_priority!(priority)
33
+
34
+ instruction = Instruction.new(
35
+ content: content,
36
+ type: type,
37
+ priority: PRIORITIES[priority],
38
+ timestamp: Time.now
39
+ )
40
+
41
+ @instructions << instruction
42
+ instruction
43
+ end
44
+
45
+ # Retrieve and remove all instructions (sorted by priority, then time)
46
+ def dequeue_all
47
+ instructions = @instructions.sort_by { |i| [i.priority, i.timestamp] }
48
+ @instructions.clear
49
+ instructions
50
+ end
51
+
52
+ # Peek at instructions without removing
53
+ def peek_all
54
+ @instructions.sort_by { |i| [i.priority, i.timestamp] }
55
+ end
56
+
57
+ # Get count of queued instructions
58
+ def count
59
+ @instructions.size
60
+ end
61
+
62
+ # Check if queue is empty
63
+ def empty?
64
+ @instructions.empty?
65
+ end
66
+
67
+ # Clear all instructions
68
+ def clear
69
+ @instructions.clear
70
+ end
71
+
72
+ # Format instructions for merging into PROMPT.md
73
+ def format_for_prompt(instructions = nil)
74
+ instructions ||= peek_all
75
+ return "" if instructions.empty?
76
+
77
+ parts = []
78
+ parts << "## 🔄 Queued Instructions from REPL"
79
+ parts << ""
80
+ parts << "The following instructions were added during execution and should be"
81
+ parts << "incorporated into your next iteration:"
82
+ parts << ""
83
+
84
+ instructions.group_by(&:type).each do |type, type_instructions|
85
+ parts << "### #{INSTRUCTION_TYPES[type]}"
86
+ type_instructions.each_with_index do |instruction, idx|
87
+ priority_marker = (instruction.priority == 1) ? " 🔴 CRITICAL" : ""
88
+ parts << "#{idx + 1}. #{instruction.content}#{priority_marker}"
89
+ end
90
+ parts << ""
91
+ end
92
+
93
+ parts << "**Note**: Address these instructions while continuing your current work."
94
+ parts << "Do not restart from scratch - build on what exists."
95
+ parts << ""
96
+
97
+ parts.join("\n")
98
+ end
99
+
100
+ # Summary for display
101
+ def summary
102
+ return "No queued instructions" if empty?
103
+
104
+ by_type = @instructions.group_by(&:type).transform_values(&:size)
105
+ by_priority = @instructions.group_by { |i| priority_name(i.priority) }.transform_values(&:size)
106
+
107
+ {
108
+ total: count,
109
+ by_type: by_type,
110
+ by_priority: by_priority
111
+ }
112
+ end
113
+
114
+ private
115
+
116
+ def validate_type!(type)
117
+ return if INSTRUCTION_TYPES.key?(type)
118
+ raise ArgumentError, "Invalid instruction type: #{type}. Must be one of #{INSTRUCTION_TYPES.keys.join(", ")}"
119
+ end
120
+
121
+ def validate_priority!(priority)
122
+ return if PRIORITIES.key?(priority)
123
+ raise ArgumentError, "Invalid priority: #{priority}. Must be one of #{PRIORITIES.keys.join(", ")}"
124
+ end
125
+
126
+ def priority_name(priority_value)
127
+ PRIORITIES.invert[priority_value] || :unknown
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-spinner"
5
+ require_relative "async_work_loop_runner"
6
+ require_relative "repl_macros"
7
+
8
+ module Aidp
9
+ module Execute
10
+ # Interactive REPL for controlling async work loops
11
+ # Provides live control during work loop execution:
12
+ # - Pause/resume/cancel work loop
13
+ # - Inject instructions mid-execution
14
+ # - Update configuration live
15
+ # - View streaming output
16
+ # - Rollback commits
17
+ #
18
+ # Usage:
19
+ # repl = InteractiveRepl.new(project_dir, provider_manager, config)
20
+ # repl.start_work_loop(step_name, step_spec, context)
21
+ class InteractiveRepl
22
+ def initialize(project_dir, provider_manager, config, options = {})
23
+ @project_dir = project_dir
24
+ @provider_manager = provider_manager
25
+ @config = config
26
+ @options = options
27
+ @prompt = options[:prompt] || TTY::Prompt.new
28
+ @async_runner = nil
29
+ @repl_macros = ReplMacros.new
30
+ @output_display_thread = nil
31
+ @running = false
32
+ end
33
+
34
+ # Start work loop and enter interactive REPL
35
+ def start_work_loop(step_name, step_spec, context = {})
36
+ @async_runner = AsyncWorkLoopRunner.new(
37
+ @project_dir,
38
+ @provider_manager,
39
+ @config,
40
+ @options
41
+ )
42
+
43
+ display_welcome(step_name)
44
+
45
+ # Start async work loop
46
+ result = @async_runner.execute_step_async(step_name, step_spec, context)
47
+ @prompt.say("Work loop started (#{result[:state][:state]})")
48
+
49
+ # Start output display thread
50
+ start_output_display
51
+
52
+ # Enter REPL loop
53
+ @running = true
54
+ repl_loop
55
+
56
+ # Wait for completion
57
+ final_result = @async_runner.wait
58
+
59
+ # Stop output display
60
+ stop_output_display
61
+
62
+ display_completion(final_result)
63
+ final_result
64
+ end
65
+
66
+ private
67
+
68
+ # Main REPL loop
69
+ def repl_loop
70
+ while @running
71
+ begin
72
+ # Check if work loop is still running
73
+ unless @async_runner.running? || @async_runner.state.paused?
74
+ @running = false
75
+ break
76
+ end
77
+
78
+ # Read command (non-blocking with timeout)
79
+ command = read_command_with_timeout
80
+
81
+ next unless command
82
+
83
+ # Execute command
84
+ handle_command(command)
85
+ rescue Interrupt
86
+ handle_interrupt
87
+ rescue => e
88
+ @prompt.error("REPL error: #{e.message}")
89
+ end
90
+ end
91
+ end
92
+
93
+ # Read command with timeout to allow checking work loop status
94
+ def read_command_with_timeout
95
+ # Use TTY::Prompt's ask with timeout is not directly supported
96
+ # So we'll use a simple gets with a prompt
97
+ print_prompt
98
+ command = $stdin.gets&.chomp
99
+ command&.strip
100
+ rescue => e
101
+ @prompt.error("Input error: #{e.message}")
102
+ nil
103
+ end
104
+
105
+ # Print REPL prompt
106
+ def print_prompt
107
+ status = @async_runner.status
108
+ state = status[:state]
109
+ iteration = status[:iteration]
110
+ queued = status.dig(:queued_instructions, :total) || 0
111
+
112
+ prompt_text = case state
113
+ when "RUNNING"
114
+ "aidp[#{iteration}]"
115
+ when "PAUSED"
116
+ "aidp[#{iteration}|PAUSED]"
117
+ else
118
+ "aidp"
119
+ end
120
+
121
+ prompt_text += " (#{queued} queued)" if queued > 0
122
+ print "#{prompt_text}> "
123
+ end
124
+
125
+ # Handle REPL command
126
+ def handle_command(command)
127
+ return if command.empty?
128
+
129
+ # Try REPL macros first
130
+ result = @repl_macros.execute(command)
131
+
132
+ if result[:success]
133
+ @prompt.say(result[:message]) if result[:message]
134
+
135
+ # Handle actions that interact with async runner
136
+ case result[:action]
137
+ when :pause_work_loop
138
+ pause_result = @async_runner.pause
139
+ @prompt.say("Work loop paused at iteration #{pause_result[:iteration]}")
140
+ when :resume_work_loop
141
+ resume_result = @async_runner.resume
142
+ @prompt.say("Work loop resumed at iteration #{resume_result[:iteration]}")
143
+ when :cancel_work_loop
144
+ cancel_result = @async_runner.cancel(save_checkpoint: result.dig(:data, :save_checkpoint))
145
+ @prompt.say("Work loop cancelled at iteration #{cancel_result[:iteration]}")
146
+ @running = false
147
+ when :enqueue_instruction
148
+ data = result[:data]
149
+ @async_runner.enqueue_instruction(
150
+ data[:instruction],
151
+ type: data[:type],
152
+ priority: data[:priority]
153
+ )
154
+ when :update_guard
155
+ data = result[:data]
156
+ @async_runner.state.request_guard_update(data[:key], data[:value])
157
+ @prompt.say("Guard update will apply at next iteration")
158
+ when :reload_config
159
+ @async_runner.state.request_config_reload
160
+ @prompt.say("Config reload will apply at next iteration")
161
+ when :rollback_commits
162
+ handle_rollback(result[:data][:count])
163
+ end
164
+ else
165
+ @prompt.error(result[:message])
166
+ end
167
+ rescue => e
168
+ @prompt.error("Command error: #{e.message}")
169
+ end
170
+
171
+ # Handle rollback command
172
+ def handle_rollback(count)
173
+ # Pause work loop first
174
+ if @async_runner.running?
175
+ @async_runner.pause
176
+ @prompt.say("Work loop paused for rollback")
177
+ end
178
+
179
+ # Execute rollback
180
+ @prompt.say("Rolling back #{count} commit(s)...")
181
+
182
+ result = execute_git_rollback(count)
183
+
184
+ if result[:success]
185
+ @prompt.ok("Rollback complete: #{result[:message]}")
186
+ else
187
+ @prompt.error("Rollback failed: #{result[:message]}")
188
+ end
189
+
190
+ # Ask if user wants to resume
191
+ if @async_runner.state.paused?
192
+ resume = @prompt.yes?("Resume work loop?")
193
+ @async_runner.resume if resume
194
+ end
195
+ end
196
+
197
+ # Execute git rollback
198
+ def execute_git_rollback(count)
199
+ # Safety check: only rollback on current branch
200
+ current_branch = `git branch --show-current`.strip
201
+
202
+ if current_branch.empty? || current_branch == "main" || current_branch == "master"
203
+ return {
204
+ success: false,
205
+ message: "Refusing to rollback on #{current_branch || "detached HEAD"}"
206
+ }
207
+ end
208
+
209
+ # Execute reset
210
+ output = `git reset --hard HEAD~#{count} 2>&1`
211
+ success = $?.success?
212
+
213
+ {
214
+ success: success,
215
+ message: success ? "Reset #{count} commit(s)" : output
216
+ }
217
+ rescue => e
218
+ {success: false, message: e.message}
219
+ end
220
+
221
+ # Handle Ctrl-C interrupt
222
+ def handle_interrupt
223
+ @prompt.warn("\nInterrupt received")
224
+
225
+ choice = @prompt.select("What would you like to do?") do |menu|
226
+ menu.choice "Cancel work loop", :cancel
227
+ menu.choice "Pause work loop", :pause
228
+ menu.choice "Continue REPL", :continue
229
+ end
230
+
231
+ case choice
232
+ when :cancel
233
+ @async_runner.cancel(save_checkpoint: true)
234
+ @running = false
235
+ when :pause
236
+ @async_runner.pause
237
+ @prompt.say("Work loop paused")
238
+ when :continue
239
+ # Just continue
240
+ end
241
+ rescue Interrupt
242
+ # Double interrupt - force exit
243
+ @prompt.error("Force exit requested")
244
+ @async_runner.cancel(save_checkpoint: false)
245
+ @running = false
246
+ end
247
+
248
+ # Start output display thread
249
+ def start_output_display
250
+ @output_display_thread = Thread.new do
251
+ loop do
252
+ sleep 0.5 # Poll every 500ms
253
+
254
+ # Drain output from async runner
255
+ output = @async_runner.drain_output
256
+
257
+ output.each do |entry|
258
+ display_output_entry(entry)
259
+ end
260
+
261
+ # Exit thread if work loop not running
262
+ break unless @async_runner.running? || @async_runner.state.paused?
263
+ end
264
+ rescue
265
+ # Silently exit thread on error
266
+ end
267
+ end
268
+
269
+ # Stop output display thread
270
+ def stop_output_display
271
+ @output_display_thread&.kill
272
+ @output_display_thread&.join(1)
273
+ @output_display_thread = nil
274
+
275
+ # Drain any remaining output
276
+ output = @async_runner.drain_output
277
+ output.each { |entry| display_output_entry(entry) }
278
+ end
279
+
280
+ # Display output entry
281
+ def display_output_entry(entry)
282
+ message = entry[:message]
283
+ type = entry[:type]
284
+
285
+ case type
286
+ when :error
287
+ @prompt.error(message)
288
+ when :warning
289
+ @prompt.warn(message)
290
+ when :success
291
+ @prompt.ok(message)
292
+ else
293
+ @prompt.say(message)
294
+ end
295
+ end
296
+
297
+ # Display welcome message
298
+ def display_welcome(step_name)
299
+ @prompt.say("\n" + "=" * 80)
300
+ @prompt.say("🎮 Interactive REPL - Work Loop: #{step_name}")
301
+ @prompt.say("=" * 80)
302
+ @prompt.say("\nCommands:")
303
+ @prompt.say(" /pause, /resume, /cancel - Control work loop")
304
+ @prompt.say(" /inject <instruction> - Add instruction for next iteration")
305
+ @prompt.say(" /merge <plan> - Update plan/contract")
306
+ @prompt.say(" /update guard <key>=<value> - Update guard rails")
307
+ @prompt.say(" /rollback <n>, /undo last - Rollback commits")
308
+ @prompt.say(" /status - Show current state")
309
+ @prompt.say(" /help - Show all commands")
310
+ @prompt.say("\nPress Ctrl-C for interrupt menu")
311
+ @prompt.say("=" * 80 + "\n")
312
+ end
313
+
314
+ # Display completion message
315
+ def display_completion(result)
316
+ @prompt.say("\n" + "=" * 80)
317
+
318
+ case result[:status]
319
+ when "completed"
320
+ @prompt.ok("✅ Work loop completed successfully!")
321
+ when "cancelled"
322
+ @prompt.warn("⚠️ Work loop cancelled by user")
323
+ when "error"
324
+ @prompt.error("❌ Work loop encountered an error")
325
+ else
326
+ @prompt.say("Work loop ended: #{result[:status]}")
327
+ end
328
+
329
+ @prompt.say("Iterations: #{result[:iterations]}")
330
+ @prompt.say(result[:message]) if result[:message]
331
+ @prompt.say("=" * 80 + "\n")
332
+ end
333
+ end
334
+ end
335
+ end