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
@@ -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
|