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,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module Aidp
|
6
|
+
module Daemon
|
7
|
+
# Manages daemon process lifecycle: start, stop, status, attach
|
8
|
+
# Handles PID file management and process communication
|
9
|
+
class ProcessManager
|
10
|
+
DAEMON_DIR = ".aidp/daemon"
|
11
|
+
PID_FILE = "#{DAEMON_DIR}/aidp.pid"
|
12
|
+
SOCKET_FILE = "#{DAEMON_DIR}/aidp.sock"
|
13
|
+
LOG_FILE = "#{DAEMON_DIR}/daemon.log"
|
14
|
+
|
15
|
+
def initialize(project_dir = Dir.pwd)
|
16
|
+
@project_dir = project_dir
|
17
|
+
@pid_file_path = File.join(@project_dir, PID_FILE)
|
18
|
+
@socket_path = File.join(@project_dir, SOCKET_FILE)
|
19
|
+
@log_path = File.join(@project_dir, LOG_FILE)
|
20
|
+
ensure_daemon_dir
|
21
|
+
end
|
22
|
+
|
23
|
+
# Check if daemon is running
|
24
|
+
def running?
|
25
|
+
return false unless File.exist?(@pid_file_path)
|
26
|
+
|
27
|
+
pid = read_pid
|
28
|
+
return false unless pid
|
29
|
+
|
30
|
+
Process.kill(0, pid)
|
31
|
+
true
|
32
|
+
rescue Errno::ESRCH
|
33
|
+
# Process doesn't exist
|
34
|
+
cleanup_stale_files
|
35
|
+
false
|
36
|
+
rescue Errno::EPERM
|
37
|
+
# Process exists but we don't have permission
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Get daemon PID
|
42
|
+
def pid
|
43
|
+
return nil unless running?
|
44
|
+
read_pid
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get daemon status summary
|
48
|
+
def status
|
49
|
+
if running?
|
50
|
+
{
|
51
|
+
running: true,
|
52
|
+
pid: pid,
|
53
|
+
socket: File.exist?(@socket_path),
|
54
|
+
log_file: @log_path
|
55
|
+
}
|
56
|
+
else
|
57
|
+
{
|
58
|
+
running: false,
|
59
|
+
pid: nil,
|
60
|
+
socket: false,
|
61
|
+
log_file: @log_path
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Write PID file for daemon
|
67
|
+
def write_pid(daemon_pid = Process.pid)
|
68
|
+
File.write(@pid_file_path, daemon_pid.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Remove PID file
|
72
|
+
def remove_pid
|
73
|
+
File.delete(@pid_file_path) if File.exist?(@pid_file_path)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Stop daemon gracefully
|
77
|
+
def stop(timeout: 30)
|
78
|
+
return {success: false, message: "Daemon not running"} unless running?
|
79
|
+
|
80
|
+
daemon_pid = pid
|
81
|
+
Process.kill("TERM", daemon_pid)
|
82
|
+
|
83
|
+
# Wait for process to exit
|
84
|
+
timeout.times do
|
85
|
+
sleep 1
|
86
|
+
unless process_exists?(daemon_pid)
|
87
|
+
cleanup_stale_files
|
88
|
+
return {success: true, message: "Daemon stopped gracefully"}
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Force kill if still running
|
93
|
+
Process.kill("KILL", daemon_pid)
|
94
|
+
cleanup_stale_files
|
95
|
+
{success: true, message: "Daemon force-killed after timeout"}
|
96
|
+
rescue Errno::ESRCH
|
97
|
+
cleanup_stale_files
|
98
|
+
{success: true, message: "Daemon already stopped"}
|
99
|
+
rescue => e
|
100
|
+
{success: false, message: "Error stopping daemon: #{e.message}"}
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get socket path for IPC
|
104
|
+
attr_reader :socket_path
|
105
|
+
|
106
|
+
# Check if socket exists
|
107
|
+
def socket_exists?
|
108
|
+
File.exist?(@socket_path)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Remove socket file
|
112
|
+
def remove_socket
|
113
|
+
File.delete(@socket_path) if File.exist?(@socket_path)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get log file path
|
117
|
+
def log_file_path
|
118
|
+
@log_path
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def ensure_daemon_dir
|
124
|
+
daemon_dir = File.join(@project_dir, DAEMON_DIR)
|
125
|
+
FileUtils.mkdir_p(daemon_dir) unless Dir.exist?(daemon_dir)
|
126
|
+
end
|
127
|
+
|
128
|
+
def read_pid
|
129
|
+
return nil unless File.exist?(@pid_file_path)
|
130
|
+
File.read(@pid_file_path).strip.to_i
|
131
|
+
end
|
132
|
+
|
133
|
+
def process_exists?(daemon_pid)
|
134
|
+
Process.kill(0, daemon_pid)
|
135
|
+
true
|
136
|
+
rescue Errno::ESRCH
|
137
|
+
false
|
138
|
+
end
|
139
|
+
|
140
|
+
def cleanup_stale_files
|
141
|
+
remove_pid
|
142
|
+
remove_socket
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "socket"
|
4
|
+
require_relative "process_manager"
|
5
|
+
require_relative "../execute/async_work_loop_runner"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
module Daemon
|
9
|
+
# Main daemon runner for background mode execution
|
10
|
+
# Manages work loops, watch mode, and IPC communication
|
11
|
+
class Runner
|
12
|
+
def initialize(project_dir, config, options = {})
|
13
|
+
@project_dir = project_dir
|
14
|
+
@config = config
|
15
|
+
@options = options
|
16
|
+
@process_manager = ProcessManager.new(project_dir)
|
17
|
+
@running = false
|
18
|
+
@work_loop_runner = nil
|
19
|
+
@watch_runner = nil
|
20
|
+
@ipc_server = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start daemon in background
|
24
|
+
def start_daemon(mode: :watch)
|
25
|
+
if @process_manager.running?
|
26
|
+
return {success: false, message: "Daemon already running (PID: #{@process_manager.pid})"}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Fork daemon process
|
30
|
+
daemon_pid = fork do
|
31
|
+
Process.daemon(true)
|
32
|
+
@process_manager.write_pid
|
33
|
+
run_daemon(mode)
|
34
|
+
end
|
35
|
+
|
36
|
+
Process.detach(daemon_pid)
|
37
|
+
|
38
|
+
# Wait for daemon to start
|
39
|
+
sleep 0.5
|
40
|
+
|
41
|
+
if @process_manager.running?
|
42
|
+
{
|
43
|
+
success: true,
|
44
|
+
message: "Daemon started in #{mode} mode",
|
45
|
+
pid: daemon_pid,
|
46
|
+
log_file: @process_manager.log_file_path
|
47
|
+
}
|
48
|
+
else
|
49
|
+
{success: false, message: "Failed to start daemon"}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Attach to running daemon (restore REPL)
|
54
|
+
def attach
|
55
|
+
unless @process_manager.running?
|
56
|
+
return {success: false, message: "No daemon running"}
|
57
|
+
end
|
58
|
+
|
59
|
+
unless @process_manager.socket_exists?
|
60
|
+
return {success: false, message: "Daemon socket not available"}
|
61
|
+
end
|
62
|
+
|
63
|
+
{
|
64
|
+
success: true,
|
65
|
+
message: "Attached to daemon",
|
66
|
+
pid: @process_manager.pid,
|
67
|
+
activity: Aidp.logger.info("activity", "summary")
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
# Run daemon main loop (called in forked process)
|
72
|
+
def run_daemon(mode)
|
73
|
+
Aidp.logger.info("daemon_lifecycle", "Daemon started", mode: mode, pid: Process.pid)
|
74
|
+
@running = true
|
75
|
+
|
76
|
+
# Set up signal handlers
|
77
|
+
setup_signal_handlers
|
78
|
+
|
79
|
+
# Start IPC server
|
80
|
+
start_ipc_server
|
81
|
+
|
82
|
+
# Run appropriate mode
|
83
|
+
case mode
|
84
|
+
when :watch
|
85
|
+
run_watch_mode
|
86
|
+
when :work_loop
|
87
|
+
run_work_loop_mode
|
88
|
+
else
|
89
|
+
Aidp.logger.error("daemon_error", "Unknown mode: #{mode}")
|
90
|
+
end
|
91
|
+
rescue => e
|
92
|
+
Aidp.logger.error("daemon_error", "Fatal error: #{e.message}", backtrace: e.backtrace.first(5).join("\n"))
|
93
|
+
ensure
|
94
|
+
cleanup
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def setup_signal_handlers
|
100
|
+
Signal.trap("TERM") do
|
101
|
+
Aidp.logger.info("daemon_lifecycle", "SIGTERM received, shutting down gracefully")
|
102
|
+
@running = false
|
103
|
+
end
|
104
|
+
|
105
|
+
Signal.trap("INT") do
|
106
|
+
Aidp.logger.info("daemon_lifecycle", "SIGINT received, shutting down gracefully")
|
107
|
+
@running = false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def start_ipc_server
|
112
|
+
# Create Unix socket for IPC
|
113
|
+
@ipc_server = UNIXServer.new(@process_manager.socket_path)
|
114
|
+
|
115
|
+
Thread.new do
|
116
|
+
while @running
|
117
|
+
begin
|
118
|
+
client = @ipc_server.accept_nonblock
|
119
|
+
handle_ipc_client(client)
|
120
|
+
rescue IO::WaitReadable
|
121
|
+
IO.select([@ipc_server], nil, nil, 1)
|
122
|
+
rescue => e
|
123
|
+
Aidp.logger.error("ipc_error", "IPC server error: #{e.message}")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
rescue => e
|
128
|
+
Aidp.logger.error("ipc_error", "Failed to start IPC server: #{e.message}")
|
129
|
+
end
|
130
|
+
|
131
|
+
def handle_ipc_client(client)
|
132
|
+
command = client.gets&.strip
|
133
|
+
return unless command
|
134
|
+
|
135
|
+
response = case command
|
136
|
+
when "status"
|
137
|
+
status_response
|
138
|
+
when "stop"
|
139
|
+
stop_response
|
140
|
+
when "attach"
|
141
|
+
attach_response
|
142
|
+
else
|
143
|
+
{error: "Unknown command: #{command}"}
|
144
|
+
end
|
145
|
+
|
146
|
+
client.puts(response.to_json)
|
147
|
+
client.close
|
148
|
+
rescue => e
|
149
|
+
Aidp.logger.error("ipc_error", "Error handling client: #{e.message}")
|
150
|
+
begin
|
151
|
+
client.close
|
152
|
+
rescue
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def status_response
|
158
|
+
{
|
159
|
+
status: "running",
|
160
|
+
pid: Process.pid,
|
161
|
+
mode: @watch_runner ? "watch" : "work_loop",
|
162
|
+
uptime: Time.now.to_i
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def stop_response
|
167
|
+
@running = false
|
168
|
+
{status: "stopping"}
|
169
|
+
end
|
170
|
+
|
171
|
+
def attach_response
|
172
|
+
{
|
173
|
+
status: "attached",
|
174
|
+
activity: Aidp.logger.info("activity", "summary")
|
175
|
+
}
|
176
|
+
end
|
177
|
+
|
178
|
+
def run_watch_mode
|
179
|
+
Aidp.logger.info("watch_mode", "Starting watch mode")
|
180
|
+
|
181
|
+
# Initialize watch runner
|
182
|
+
require_relative "../watch/runner"
|
183
|
+
@watch_runner = Aidp::Watch::Runner.new(@project_dir, @config, @options)
|
184
|
+
|
185
|
+
while @running
|
186
|
+
begin
|
187
|
+
@watch_runner.run_cycle
|
188
|
+
Aidp.logger.info("watch_mode", "Watch cycle completed")
|
189
|
+
sleep(@options[:interval] || 60)
|
190
|
+
rescue => e
|
191
|
+
Aidp.logger.error("watch_error", "Watch cycle error: #{e.message}")
|
192
|
+
sleep 30 # Back off on error
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
Aidp.logger.info("watch_mode", "Watch mode stopped")
|
197
|
+
end
|
198
|
+
|
199
|
+
def run_work_loop_mode
|
200
|
+
Aidp.logger.info("daemon_lifecycle", "Starting work loop mode")
|
201
|
+
|
202
|
+
# This would integrate with AsyncWorkLoopRunner
|
203
|
+
# For now, just log that we're running
|
204
|
+
while @running
|
205
|
+
Aidp.logger.debug("heartbeat", "Daemon running")
|
206
|
+
sleep 10
|
207
|
+
end
|
208
|
+
|
209
|
+
Aidp.logger.info("daemon_lifecycle", "Work loop mode stopped")
|
210
|
+
end
|
211
|
+
|
212
|
+
def cleanup
|
213
|
+
Aidp.logger.info("daemon_lifecycle", "Daemon cleanup started")
|
214
|
+
|
215
|
+
# Stop work loop if running
|
216
|
+
@work_loop_runner&.cancel(save_checkpoint: true)
|
217
|
+
|
218
|
+
# Stop watch runner if running
|
219
|
+
@watch_runner = nil
|
220
|
+
|
221
|
+
# Close IPC server
|
222
|
+
@ipc_server&.close
|
223
|
+
@process_manager.remove_socket
|
224
|
+
|
225
|
+
# Remove PID file
|
226
|
+
@process_manager.remove_pid
|
227
|
+
|
228
|
+
Aidp.logger.info("daemon_lifecycle", "Daemon stopped cleanly")
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "work_loop_runner"
|
4
|
+
require_relative "work_loop_state"
|
5
|
+
require_relative "instruction_queue"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
module Execute
|
9
|
+
# Asynchronous wrapper around WorkLoopRunner
|
10
|
+
# Runs work loop in a separate thread while maintaining REPL responsiveness
|
11
|
+
#
|
12
|
+
# Responsibilities:
|
13
|
+
# - Execute work loop in background thread
|
14
|
+
# - Monitor execution state (pause, resume, cancel)
|
15
|
+
# - Merge queued instructions at iteration boundaries
|
16
|
+
# - Stream output to main thread for display
|
17
|
+
# - Handle graceful cancellation with checkpoint save
|
18
|
+
class AsyncWorkLoopRunner
|
19
|
+
attr_reader :state, :instruction_queue, :work_thread
|
20
|
+
|
21
|
+
def initialize(project_dir, provider_manager, config, options = {})
|
22
|
+
@project_dir = project_dir
|
23
|
+
@provider_manager = provider_manager
|
24
|
+
@config = config
|
25
|
+
@options = options
|
26
|
+
@state = WorkLoopState.new
|
27
|
+
@instruction_queue = InstructionQueue.new
|
28
|
+
@work_thread = nil
|
29
|
+
@sync_runner = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
# Start async work loop execution
|
33
|
+
# Returns immediately, work continues in background thread
|
34
|
+
def execute_step_async(step_name, step_spec, context = {})
|
35
|
+
raise WorkLoopState::StateError, "Work loop already running" unless @state.idle?
|
36
|
+
|
37
|
+
@state.start!
|
38
|
+
@step_name = step_name
|
39
|
+
@step_spec = step_spec
|
40
|
+
@context = context
|
41
|
+
|
42
|
+
@work_thread = Thread.new do
|
43
|
+
run_async_loop
|
44
|
+
rescue => e
|
45
|
+
@state.error!(e)
|
46
|
+
@state.append_output("Work loop error: #{e.message}", type: :error)
|
47
|
+
ensure
|
48
|
+
@work_thread = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Allow thread to start
|
52
|
+
Thread.pass
|
53
|
+
|
54
|
+
{status: "started", state: @state.summary}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Wait for work loop to complete (blocking)
|
58
|
+
def wait
|
59
|
+
return unless @work_thread
|
60
|
+
@work_thread.join
|
61
|
+
build_final_result
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check if work loop is running
|
65
|
+
def running?
|
66
|
+
@work_thread&.alive? && @state.running?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Pause execution
|
70
|
+
def pause
|
71
|
+
return unless running?
|
72
|
+
@state.pause!
|
73
|
+
{status: "paused", iteration: @state.iteration}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Resume execution
|
77
|
+
def resume
|
78
|
+
return unless @state.paused?
|
79
|
+
@state.resume!
|
80
|
+
{status: "resumed", iteration: @state.iteration}
|
81
|
+
end
|
82
|
+
|
83
|
+
# Cancel execution gracefully
|
84
|
+
def cancel(save_checkpoint: true)
|
85
|
+
return if @state.cancelled? || @state.completed?
|
86
|
+
|
87
|
+
@state.cancel!
|
88
|
+
@state.append_output("Cancellation requested, waiting for safe stopping point...", type: :warning)
|
89
|
+
|
90
|
+
# Wait for thread to notice cancellation
|
91
|
+
@work_thread&.join(5) # 5 second timeout
|
92
|
+
|
93
|
+
if save_checkpoint && @sync_runner
|
94
|
+
@state.append_output("Saving checkpoint before exit...", type: :info)
|
95
|
+
save_cancellation_checkpoint
|
96
|
+
end
|
97
|
+
|
98
|
+
{status: "cancelled", iteration: @state.iteration}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add instruction to queue (will be merged at next iteration)
|
102
|
+
def enqueue_instruction(content, type: :user_input, priority: :normal)
|
103
|
+
@instruction_queue.enqueue(content, type: type, priority: priority)
|
104
|
+
@state.enqueue_instruction(content)
|
105
|
+
|
106
|
+
{
|
107
|
+
status: "enqueued",
|
108
|
+
queued_count: @instruction_queue.count,
|
109
|
+
message: "Instruction will be merged in next iteration"
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get streaming output
|
114
|
+
def drain_output
|
115
|
+
@state.drain_output
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get current status
|
119
|
+
def status
|
120
|
+
summary = @state.summary
|
121
|
+
summary[:queued_instructions] = @instruction_queue.summary
|
122
|
+
summary[:thread_alive] = @work_thread&.alive? || false
|
123
|
+
summary
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Main async execution loop
|
129
|
+
def run_async_loop
|
130
|
+
# Create synchronous runner (runs in this thread)
|
131
|
+
@sync_runner = WorkLoopRunner.new(
|
132
|
+
@project_dir,
|
133
|
+
@provider_manager,
|
134
|
+
@config,
|
135
|
+
@options.merge(async_mode: true)
|
136
|
+
)
|
137
|
+
|
138
|
+
@state.append_output("🚀 Starting async work loop: #{@step_name}", type: :info)
|
139
|
+
|
140
|
+
# Hook into sync runner to check for pause/cancel
|
141
|
+
result = execute_with_monitoring
|
142
|
+
|
143
|
+
if @state.cancelled?
|
144
|
+
@state.append_output("Work loop cancelled at iteration #{@state.iteration}", type: :warning)
|
145
|
+
else
|
146
|
+
@state.complete!
|
147
|
+
@state.append_output("✅ Work loop completed: #{@step_name}", type: :success)
|
148
|
+
end
|
149
|
+
|
150
|
+
result
|
151
|
+
rescue => e
|
152
|
+
@state.error!(e)
|
153
|
+
@state.append_output("Error in work loop: #{e.message}\n#{e.backtrace.first(3).join("\n")}", type: :error)
|
154
|
+
raise
|
155
|
+
end
|
156
|
+
|
157
|
+
# Execute sync runner with monitoring for control signals
|
158
|
+
def execute_with_monitoring
|
159
|
+
# We need to modify WorkLoopRunner to support iteration callbacks
|
160
|
+
# For now, we'll wrap the execute_step call
|
161
|
+
#
|
162
|
+
# TODO: This requires enhancing WorkLoopRunner to accept iteration callbacks
|
163
|
+
# See: https://github.com/viamin/aidp/issues/103
|
164
|
+
@sync_runner.execute_step(@step_name, @step_spec, @context)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Save checkpoint when cancelling
|
168
|
+
def save_cancellation_checkpoint
|
169
|
+
return unless @sync_runner
|
170
|
+
|
171
|
+
checkpoint = @sync_runner.instance_variable_get(:@checkpoint)
|
172
|
+
return unless checkpoint
|
173
|
+
|
174
|
+
checkpoint.record_checkpoint(
|
175
|
+
@step_name,
|
176
|
+
@state.iteration,
|
177
|
+
{
|
178
|
+
cancelled: true,
|
179
|
+
reason: "User cancelled via REPL",
|
180
|
+
queued_instructions: @instruction_queue.count
|
181
|
+
}
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Build final result from state
|
186
|
+
def build_final_result
|
187
|
+
if @state.completed?
|
188
|
+
{
|
189
|
+
status: "completed",
|
190
|
+
iterations: @state.iteration,
|
191
|
+
message: "Work loop completed successfully"
|
192
|
+
}
|
193
|
+
elsif @state.cancelled?
|
194
|
+
{
|
195
|
+
status: "cancelled",
|
196
|
+
iterations: @state.iteration,
|
197
|
+
message: "Work loop cancelled by user"
|
198
|
+
}
|
199
|
+
elsif @state.error?
|
200
|
+
{
|
201
|
+
status: "error",
|
202
|
+
iterations: @state.iteration,
|
203
|
+
error: @state.last_error&.message,
|
204
|
+
message: "Work loop encountered an error"
|
205
|
+
}
|
206
|
+
else
|
207
|
+
{
|
208
|
+
status: "unknown",
|
209
|
+
iterations: @state.iteration,
|
210
|
+
message: "Work loop ended in unknown state"
|
211
|
+
}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|