aidp 0.14.1 → 0.15.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 +46 -45
- data/lib/aidp/cli.rb +46 -38
- data/lib/aidp/debug_mixin.rb +34 -33
- data/lib/aidp/execute/agent_signal_parser.rb +46 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +5 -0
- data/lib/aidp/execute/checkpoint.rb +28 -5
- data/lib/aidp/execute/deterministic_unit.rb +254 -0
- data/lib/aidp/execute/interactive_repl.rb +7 -0
- data/lib/aidp/execute/steps.rb +1 -1
- data/lib/aidp/execute/work_loop_runner.rb +187 -30
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +190 -0
- data/lib/aidp/harness/config_schema.rb +91 -1
- data/lib/aidp/harness/configuration.rb +60 -1
- data/lib/aidp/harness/enhanced_runner.rb +2 -0
- data/lib/aidp/harness/provider_info.rb +14 -4
- data/lib/aidp/harness/provider_manager.rb +64 -12
- data/lib/aidp/jobs/background_runner.rb +10 -3
- data/lib/aidp/logger.rb +10 -71
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/providers/github_copilot.rb +12 -0
- data/lib/aidp/rescue_logging.rb +36 -0
- data/lib/aidp/setup/wizard.rb +42 -46
- data/lib/aidp/storage/csv_storage.rb +33 -7
- data/lib/aidp/storage/json_storage.rb +33 -10
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/guided_agent.rb +95 -22
- data/templates/implementation/simple_task.md +5 -0
- metadata +5 -2
- data/lib/aidp/debug_logger.rb +0 -195
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require_relative "../message_display"
|
|
5
|
+
require_relative "../rescue_logging"
|
|
6
|
+
require_relative "../util"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Execute
|
|
10
|
+
module DeterministicUnits
|
|
11
|
+
# Represents a deterministic unit configured in aidp.yml.
|
|
12
|
+
# Definitions are immutable and provide helper accessors used by the scheduler.
|
|
13
|
+
class Definition
|
|
14
|
+
VALID_TYPES = [:command, :wait].freeze
|
|
15
|
+
NEXT_KEY_ALIASES = {
|
|
16
|
+
if_pass: :success,
|
|
17
|
+
if_success: :success,
|
|
18
|
+
if_fail: :failure,
|
|
19
|
+
if_failure: :failure,
|
|
20
|
+
if_error: :failure,
|
|
21
|
+
if_timeout: :timeout,
|
|
22
|
+
if_wait: :waiting,
|
|
23
|
+
if_new_item: :event,
|
|
24
|
+
if_event: :event
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :name, :type, :command, :output_file, :next_map,
|
|
28
|
+
:min_interval_seconds, :max_backoff_seconds, :backoff_multiplier,
|
|
29
|
+
:enabled, :metadata
|
|
30
|
+
|
|
31
|
+
def initialize(config)
|
|
32
|
+
@name = config.fetch(:name)
|
|
33
|
+
@type = normalize_type(config[:type]) || default_type_for(config)
|
|
34
|
+
validate_type!
|
|
35
|
+
|
|
36
|
+
@command = config[:command]
|
|
37
|
+
@output_file = config[:output_file]
|
|
38
|
+
@next_map = normalize_next_config(config[:next] || {})
|
|
39
|
+
@min_interval_seconds = config.fetch(:min_interval_seconds, 60)
|
|
40
|
+
@max_backoff_seconds = config.fetch(:max_backoff_seconds, 900)
|
|
41
|
+
@backoff_multiplier = config.fetch(:backoff_multiplier, 2.0)
|
|
42
|
+
@enabled = config.fetch(:enabled, true)
|
|
43
|
+
@metadata = config.fetch(:metadata, {}).dup
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def command?
|
|
47
|
+
type == :command
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wait?
|
|
51
|
+
type == :wait
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next_for(result_status, default: nil)
|
|
55
|
+
next_map[result_status.to_sym] || default || next_map[:else]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def normalize_type(type)
|
|
61
|
+
return nil if type.nil?
|
|
62
|
+
symbol = type.to_sym
|
|
63
|
+
VALID_TYPES.include?(symbol) ? symbol : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def default_type_for(config)
|
|
67
|
+
return :command if config[:command]
|
|
68
|
+
:wait
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_type!
|
|
72
|
+
return if VALID_TYPES.include?(type)
|
|
73
|
+
raise ArgumentError, "Unsupported deterministic unit type: #{type.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_next_config(raw)
|
|
77
|
+
return {} unless raw
|
|
78
|
+
|
|
79
|
+
raw.each_with_object({}) do |(key, value), normalized|
|
|
80
|
+
symbol_key = key.to_sym
|
|
81
|
+
normalized[NEXT_KEY_ALIASES.fetch(symbol_key, symbol_key)] = value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Result wrapper returned after executing a deterministic unit.
|
|
87
|
+
class Result
|
|
88
|
+
attr_reader :name, :status, :output_path, :started_at, :finished_at,
|
|
89
|
+
:duration, :data, :error
|
|
90
|
+
|
|
91
|
+
def initialize(name:, status:, output_path:, started_at:, finished_at:, data: {}, error: nil)
|
|
92
|
+
@name = name
|
|
93
|
+
@status = status.to_sym
|
|
94
|
+
@output_path = output_path
|
|
95
|
+
@started_at = started_at
|
|
96
|
+
@finished_at = finished_at
|
|
97
|
+
@duration = finished_at - started_at
|
|
98
|
+
@data = data
|
|
99
|
+
@error = error
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def success?
|
|
103
|
+
status == :success
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def failure?
|
|
107
|
+
status == :failure
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def timeout?
|
|
111
|
+
status == :timeout
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Executes deterministic units by running commands or internal behaviours.
|
|
116
|
+
class Runner
|
|
117
|
+
include Aidp::MessageDisplay
|
|
118
|
+
include Aidp::RescueLogging
|
|
119
|
+
|
|
120
|
+
DEFAULT_TIMEOUT = 3600 # One hour ceiling for long-running commands
|
|
121
|
+
|
|
122
|
+
def initialize(project_dir, command_runner: nil, clock: Time)
|
|
123
|
+
@project_dir = project_dir
|
|
124
|
+
@clock = clock
|
|
125
|
+
@command_runner = command_runner || build_default_command_runner
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run(definition, context = {})
|
|
129
|
+
raise ArgumentError, "Unit #{definition.name} is not enabled" unless definition.enabled
|
|
130
|
+
|
|
131
|
+
case definition.type
|
|
132
|
+
when :command
|
|
133
|
+
execute_command_unit(definition, context)
|
|
134
|
+
when :wait
|
|
135
|
+
execute_wait_unit(definition, context)
|
|
136
|
+
else
|
|
137
|
+
raise ArgumentError, "Unsupported deterministic unit type: #{definition.type}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def execute_command_unit(definition, context)
|
|
144
|
+
started_at = @clock.now
|
|
145
|
+
display_message("🛠️ Running deterministic unit: #{definition.name}", type: :info)
|
|
146
|
+
|
|
147
|
+
result = @command_runner.call(definition.command, context)
|
|
148
|
+
|
|
149
|
+
data = {
|
|
150
|
+
exit_status: result[:exit_status],
|
|
151
|
+
stdout: result[:stdout],
|
|
152
|
+
stderr: result[:stderr]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
status = result[:exit_status].to_i.zero? ? :success : :failure
|
|
156
|
+
output_path = write_output(definition, data)
|
|
157
|
+
|
|
158
|
+
display_message("✅ Deterministic unit #{definition.name} finished with status #{status}", type: :success) if status == :success
|
|
159
|
+
display_message("⚠️ Deterministic unit #{definition.name} finished with status #{status}", type: :warning) if status != :success
|
|
160
|
+
|
|
161
|
+
DeterministicUnits::Result.new(
|
|
162
|
+
name: definition.name,
|
|
163
|
+
status: status,
|
|
164
|
+
output_path: output_path,
|
|
165
|
+
started_at: started_at,
|
|
166
|
+
finished_at: @clock.now,
|
|
167
|
+
data: data
|
|
168
|
+
)
|
|
169
|
+
rescue => e
|
|
170
|
+
finished_at = @clock.now
|
|
171
|
+
log_rescue(e, component: "deterministic_runner", action: "execute_command_unit", fallback: "failure", unit: definition.name)
|
|
172
|
+
display_message("❌ Deterministic unit #{definition.name} failed: #{e.message}", type: :error)
|
|
173
|
+
|
|
174
|
+
output_path = write_output(definition, {error: e.message})
|
|
175
|
+
|
|
176
|
+
DeterministicUnits::Result.new(
|
|
177
|
+
name: definition.name,
|
|
178
|
+
status: :failure,
|
|
179
|
+
output_path: output_path,
|
|
180
|
+
started_at: started_at,
|
|
181
|
+
finished_at: finished_at,
|
|
182
|
+
data: {error: e.message},
|
|
183
|
+
error: e
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def execute_wait_unit(definition, context)
|
|
188
|
+
started_at = @clock.now
|
|
189
|
+
|
|
190
|
+
wait_seconds = definition.metadata.fetch(:interval_seconds, 60)
|
|
191
|
+
backoff_seconds = definition.metadata.fetch(:backoff_seconds, wait_seconds)
|
|
192
|
+
reason = context[:reason] || "Waiting for GitHub activity"
|
|
193
|
+
|
|
194
|
+
display_message("🕒 Deterministic wait: #{definition.name} (#{reason})", type: :info)
|
|
195
|
+
max_window = definition.max_backoff_seconds || backoff_seconds
|
|
196
|
+
sleep_duration = backoff_seconds.clamp(1, max_window)
|
|
197
|
+
|
|
198
|
+
sleep_handler = context[:sleep_handler] || method(:sleep)
|
|
199
|
+
sleep_handler.call(sleep_duration)
|
|
200
|
+
|
|
201
|
+
event_detected = context[:event_detected] == true
|
|
202
|
+
|
|
203
|
+
payload = {
|
|
204
|
+
message: "Waited #{sleep_duration} seconds",
|
|
205
|
+
reason: reason,
|
|
206
|
+
backoff_seconds: sleep_duration,
|
|
207
|
+
event_detected: event_detected
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
output_path = write_output(definition, payload)
|
|
211
|
+
|
|
212
|
+
DeterministicUnits::Result.new(
|
|
213
|
+
name: definition.name,
|
|
214
|
+
status: event_detected ? :event : :waiting,
|
|
215
|
+
output_path: output_path,
|
|
216
|
+
started_at: started_at,
|
|
217
|
+
finished_at: @clock.now,
|
|
218
|
+
data: payload
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def write_output(definition, payload)
|
|
223
|
+
return nil unless definition.output_file
|
|
224
|
+
|
|
225
|
+
path = File.join(@project_dir, definition.output_file)
|
|
226
|
+
Aidp::Util.safe_file_write(path, payload.to_yaml)
|
|
227
|
+
path
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_default_command_runner
|
|
231
|
+
lambda do |command, _context|
|
|
232
|
+
require "tty-command"
|
|
233
|
+
|
|
234
|
+
cmd = TTY::Command.new(printer: :quiet)
|
|
235
|
+
result = cmd.run(command, chdir: @project_dir)
|
|
236
|
+
|
|
237
|
+
{
|
|
238
|
+
exit_status: result.exit_status,
|
|
239
|
+
stdout: result.out,
|
|
240
|
+
stderr: result.err
|
|
241
|
+
}
|
|
242
|
+
rescue TTY::Command::ExitError => e
|
|
243
|
+
result = e.result
|
|
244
|
+
{
|
|
245
|
+
exit_status: result.exit_status,
|
|
246
|
+
stdout: result.out,
|
|
247
|
+
stderr: result.err
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
@@ -4,6 +4,7 @@ require "tty-prompt"
|
|
|
4
4
|
require "tty-spinner"
|
|
5
5
|
require_relative "async_work_loop_runner"
|
|
6
6
|
require_relative "repl_macros"
|
|
7
|
+
require_relative "../rescue_logging"
|
|
7
8
|
|
|
8
9
|
module Aidp
|
|
9
10
|
module Execute
|
|
@@ -19,6 +20,8 @@ module Aidp
|
|
|
19
20
|
# repl = InteractiveRepl.new(project_dir, provider_manager, config)
|
|
20
21
|
# repl.start_work_loop(step_name, step_spec, context)
|
|
21
22
|
class InteractiveRepl
|
|
23
|
+
include Aidp::RescueLogging
|
|
24
|
+
|
|
22
25
|
def initialize(project_dir, provider_manager, config, options = {})
|
|
23
26
|
@project_dir = project_dir
|
|
24
27
|
@provider_manager = provider_manager
|
|
@@ -85,6 +88,7 @@ module Aidp
|
|
|
85
88
|
rescue Interrupt
|
|
86
89
|
handle_interrupt
|
|
87
90
|
rescue => e
|
|
91
|
+
log_rescue(e, component: "interactive_repl", action: "repl_command_loop", fallback: "error_display")
|
|
88
92
|
@prompt.error("REPL error: #{e.message}")
|
|
89
93
|
end
|
|
90
94
|
end
|
|
@@ -98,6 +102,7 @@ module Aidp
|
|
|
98
102
|
command = $stdin.gets&.chomp
|
|
99
103
|
command&.strip
|
|
100
104
|
rescue => e
|
|
105
|
+
log_rescue(e, component: "interactive_repl", action: "read_command", fallback: nil)
|
|
101
106
|
@prompt.error("Input error: #{e.message}")
|
|
102
107
|
nil
|
|
103
108
|
end
|
|
@@ -165,6 +170,7 @@ module Aidp
|
|
|
165
170
|
@prompt.error(result[:message])
|
|
166
171
|
end
|
|
167
172
|
rescue => e
|
|
173
|
+
log_rescue(e, component: "interactive_repl", action: "handle_command", fallback: "error_display", command: result[:data])
|
|
168
174
|
@prompt.error("Command error: #{e.message}")
|
|
169
175
|
end
|
|
170
176
|
|
|
@@ -215,6 +221,7 @@ module Aidp
|
|
|
215
221
|
message: success ? "Reset #{count} commit(s)" : output
|
|
216
222
|
}
|
|
217
223
|
rescue => e
|
|
224
|
+
log_rescue(e, component: "interactive_repl", action: "git_reset", fallback: "error_result", count: count)
|
|
218
225
|
{success: false, message: e.message}
|
|
219
226
|
end
|
|
220
227
|
|
data/lib/aidp/execute/steps.rb
CHANGED
|
@@ -128,7 +128,7 @@ module Aidp
|
|
|
128
128
|
# Simple task execution - for one-off commands and simple fixes
|
|
129
129
|
"99_SIMPLE_TASK" => {
|
|
130
130
|
"templates" => ["implementation/simple_task.md"],
|
|
131
|
-
"description" => "Execute Simple Task (one-off commands, quick fixes, linting)",
|
|
131
|
+
"description" => "Execute Simple Task (one-off commands, quick fixes, linting; emit NEXT_UNIT when more tooling is needed)",
|
|
132
132
|
"outs" => [],
|
|
133
133
|
"gate" => false,
|
|
134
134
|
"simple" => true # Special step for simple, focused tasks
|
|
@@ -4,6 +4,9 @@ require_relative "prompt_manager"
|
|
|
4
4
|
require_relative "checkpoint"
|
|
5
5
|
require_relative "checkpoint_display"
|
|
6
6
|
require_relative "guard_policy"
|
|
7
|
+
require_relative "work_loop_unit_scheduler"
|
|
8
|
+
require_relative "deterministic_unit"
|
|
9
|
+
require_relative "agent_signal_parser"
|
|
7
10
|
require_relative "../harness/test_runner"
|
|
8
11
|
|
|
9
12
|
module Aidp
|
|
@@ -53,6 +56,8 @@ module Aidp
|
|
|
53
56
|
@options = options
|
|
54
57
|
@current_state = :ready
|
|
55
58
|
@state_history = []
|
|
59
|
+
@deterministic_runner = DeterministicUnits::Runner.new(project_dir)
|
|
60
|
+
@unit_scheduler = nil
|
|
56
61
|
end
|
|
57
62
|
|
|
58
63
|
# Execute a step using fix-forward work loop pattern
|
|
@@ -63,86 +68,218 @@ module Aidp
|
|
|
63
68
|
@iteration_count = 0
|
|
64
69
|
transition_to(:ready)
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
Aidp.logger.info("work_loop", "Starting hybrid work loop execution", step: step_name, max_iterations: MAX_ITERATIONS)
|
|
72
|
+
|
|
73
|
+
display_message("🔄 Starting hybrid work loop for step: #{step_name}", type: :info)
|
|
74
|
+
display_message(" Flow: Deterministic ↔ Agentic with fix-forward core", type: :info)
|
|
68
75
|
|
|
69
|
-
# Display guard policy status
|
|
70
76
|
display_guard_policy_status
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
@unit_scheduler = WorkLoopUnitScheduler.new(units_config)
|
|
79
|
+
base_context = context.dup
|
|
80
|
+
|
|
81
|
+
loop do
|
|
82
|
+
unit = @unit_scheduler.next_unit
|
|
83
|
+
break unless unit
|
|
84
|
+
|
|
85
|
+
if unit.deterministic?
|
|
86
|
+
result = @deterministic_runner.run(unit.definition, reason: "scheduled by work loop")
|
|
87
|
+
@unit_scheduler.record_deterministic_result(unit.definition, result)
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
enriched_context = base_context.merge(
|
|
92
|
+
deterministic_outputs: @unit_scheduler.deterministic_context,
|
|
93
|
+
previous_agent_summary: @unit_scheduler.last_agentic_summary
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
agentic_payload = if unit.name == :decide_whats_next
|
|
97
|
+
run_decider_agentic_unit(enriched_context)
|
|
98
|
+
else
|
|
99
|
+
run_primary_agentic_unit(step_spec, enriched_context)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
@unit_scheduler.record_agentic_result(
|
|
103
|
+
agentic_payload[:raw_result] || {},
|
|
104
|
+
requested_next: agentic_payload[:requested_next],
|
|
105
|
+
summary: agentic_payload[:summary],
|
|
106
|
+
completed: agentic_payload[:completed]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return agentic_payload[:response] if agentic_payload[:terminate]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
build_max_iterations_result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def run_primary_agentic_unit(step_spec, context)
|
|
118
|
+
Aidp.logger.info("work_loop", "Running primary agentic unit", step: @step_name)
|
|
119
|
+
|
|
120
|
+
display_message(" State machine: READY → APPLY_PATCH → TEST → {PASS → DONE | FAIL → DIAGNOSE → NEXT_PATCH}", type: :info)
|
|
121
|
+
|
|
122
|
+
@iteration_count = 0
|
|
123
|
+
@current_state = :ready
|
|
124
|
+
@state_history.clear
|
|
125
|
+
|
|
73
126
|
create_initial_prompt(step_spec, context)
|
|
74
127
|
|
|
75
|
-
# Main fix-forward work loop
|
|
76
128
|
loop do
|
|
77
129
|
@iteration_count += 1
|
|
78
130
|
display_message(" Iteration #{@iteration_count} [State: #{STATES[@current_state]}]", type: :info)
|
|
79
131
|
|
|
80
|
-
|
|
132
|
+
if @iteration_count > MAX_ITERATIONS
|
|
133
|
+
Aidp.logger.error("work_loop", "Max iterations exceeded", step: @step_name, iterations: @iteration_count)
|
|
134
|
+
display_message("⚠️ Max iterations (#{MAX_ITERATIONS}) reached for #{@step_name}", type: :warning)
|
|
135
|
+
display_state_summary
|
|
136
|
+
archive_and_cleanup
|
|
137
|
+
return build_agentic_payload(
|
|
138
|
+
agent_result: nil,
|
|
139
|
+
response: build_max_iterations_result,
|
|
140
|
+
summary: nil,
|
|
141
|
+
completed: false,
|
|
142
|
+
terminate: true
|
|
143
|
+
)
|
|
144
|
+
end
|
|
81
145
|
|
|
82
|
-
# State: READY - Starting new iteration
|
|
83
146
|
transition_to(:ready) unless @current_state == :ready
|
|
84
147
|
|
|
85
|
-
# State: APPLY_PATCH - Agent applies changes
|
|
86
148
|
transition_to(:apply_patch)
|
|
87
|
-
|
|
149
|
+
agent_result = apply_patch
|
|
88
150
|
|
|
89
|
-
# State: TEST - Run tests and linters
|
|
90
151
|
transition_to(:test)
|
|
91
152
|
test_results = @test_runner.run_tests
|
|
92
153
|
lint_results = @test_runner.run_linters
|
|
93
154
|
|
|
94
|
-
# Record checkpoint at intervals
|
|
95
155
|
record_periodic_checkpoint(test_results, lint_results)
|
|
96
156
|
|
|
97
|
-
# Check if tests passed
|
|
98
157
|
tests_pass = test_results[:success] && lint_results[:success]
|
|
99
158
|
|
|
100
159
|
if tests_pass
|
|
101
|
-
# State: PASS - Tests passed
|
|
102
160
|
transition_to(:pass)
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
if agent_marked_complete?(result)
|
|
106
|
-
# State: DONE - Work complete
|
|
162
|
+
if agent_marked_complete?(agent_result)
|
|
107
163
|
transition_to(:done)
|
|
108
164
|
record_final_checkpoint(test_results, lint_results)
|
|
109
|
-
display_message("✅ Step #{step_name} completed after #{@iteration_count} iterations", type: :success)
|
|
165
|
+
display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
|
|
110
166
|
display_state_summary
|
|
111
167
|
archive_and_cleanup
|
|
112
|
-
|
|
168
|
+
|
|
169
|
+
return build_agentic_payload(
|
|
170
|
+
agent_result: agent_result,
|
|
171
|
+
response: build_success_result(agent_result),
|
|
172
|
+
summary: agent_result[:output],
|
|
173
|
+
completed: true,
|
|
174
|
+
terminate: true
|
|
175
|
+
)
|
|
113
176
|
else
|
|
114
|
-
# Tests pass but work not complete - continue
|
|
115
177
|
display_message(" Tests passed but work not marked complete", type: :info)
|
|
116
178
|
transition_to(:next_patch)
|
|
117
179
|
end
|
|
118
180
|
else
|
|
119
|
-
# State: FAIL - Tests failed
|
|
120
181
|
transition_to(:fail)
|
|
121
182
|
display_message(" Tests or linters failed", type: :warning)
|
|
122
183
|
|
|
123
|
-
# State: DIAGNOSE - Analyze failures
|
|
124
184
|
transition_to(:diagnose)
|
|
125
185
|
diagnostic = diagnose_failures(test_results, lint_results)
|
|
126
186
|
|
|
127
|
-
# State: NEXT_PATCH - Prepare for next iteration
|
|
128
187
|
transition_to(:next_patch)
|
|
129
188
|
prepare_next_iteration(test_results, lint_results, diagnostic)
|
|
130
189
|
end
|
|
131
190
|
end
|
|
191
|
+
end
|
|
132
192
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
193
|
+
def run_decider_agentic_unit(context)
|
|
194
|
+
Aidp.logger.info("work_loop", "Running decide_whats_next agentic unit", step: @step_name)
|
|
195
|
+
|
|
196
|
+
prompt = build_decider_prompt(context)
|
|
197
|
+
|
|
198
|
+
agent_result = @provider_manager.execute_with_provider(
|
|
199
|
+
@provider_manager.current_provider,
|
|
200
|
+
prompt,
|
|
201
|
+
{
|
|
202
|
+
step_name: @step_name,
|
|
203
|
+
iteration: @iteration_count,
|
|
204
|
+
project_dir: @project_dir,
|
|
205
|
+
mode: :decide_whats_next
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
requested = AgentSignalParser.extract_next_unit(agent_result[:output])
|
|
210
|
+
|
|
211
|
+
build_agentic_payload(
|
|
212
|
+
agent_result: agent_result,
|
|
213
|
+
response: agent_result,
|
|
214
|
+
summary: agent_result[:output],
|
|
215
|
+
completed: false,
|
|
216
|
+
terminate: false,
|
|
217
|
+
requested_next: requested
|
|
218
|
+
)
|
|
138
219
|
end
|
|
139
220
|
|
|
140
|
-
|
|
221
|
+
def units_config
|
|
222
|
+
if @config.respond_to?(:work_loop_units_config)
|
|
223
|
+
@config.work_loop_units_config
|
|
224
|
+
else
|
|
225
|
+
{}
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build_agentic_payload(agent_result:, response:, summary:, completed:, terminate:, requested_next: nil)
|
|
230
|
+
{
|
|
231
|
+
raw_result: agent_result,
|
|
232
|
+
response: response,
|
|
233
|
+
summary: summary,
|
|
234
|
+
requested_next: requested_next || AgentSignalParser.extract_next_unit(agent_result&.dig(:output)),
|
|
235
|
+
completed: completed,
|
|
236
|
+
terminate: terminate
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def build_decider_prompt(context)
|
|
241
|
+
outputs = Array(context[:deterministic_outputs])
|
|
242
|
+
summary = context[:previous_agent_summary]
|
|
243
|
+
|
|
244
|
+
sections = []
|
|
245
|
+
sections << "# Decide Next Work Loop Unit"
|
|
246
|
+
sections << ""
|
|
247
|
+
sections << "You are operating in the Aidp work loop. Determine what should happen next."
|
|
248
|
+
sections << ""
|
|
249
|
+
sections << "## Recent Deterministic Outputs"
|
|
250
|
+
|
|
251
|
+
if outputs.empty?
|
|
252
|
+
sections << "- None recorded yet."
|
|
253
|
+
else
|
|
254
|
+
outputs.each do |entry|
|
|
255
|
+
sections << "- #{entry[:name]} (status: #{entry[:status]}, finished_at: #{entry[:finished_at]})"
|
|
256
|
+
sections << " Output: #{entry[:output_path] || "n/a"}"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if summary
|
|
261
|
+
sections << ""
|
|
262
|
+
sections << "## Previous Agent Summary"
|
|
263
|
+
sections << summary
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
sections << ""
|
|
267
|
+
sections << "## Instructions"
|
|
268
|
+
sections << "- Decide whether to run another deterministic unit or resume agentic editing."
|
|
269
|
+
sections << "- Announce your decision with `NEXT_UNIT: <unit_name>`."
|
|
270
|
+
sections << "- Valid values: names defined in configuration, `agentic`, or `wait_for_github`."
|
|
271
|
+
sections << "- Provide a concise rationale below."
|
|
272
|
+
sections << ""
|
|
273
|
+
sections << "## Rationale"
|
|
274
|
+
|
|
275
|
+
sections.join("\n")
|
|
276
|
+
end
|
|
141
277
|
|
|
142
278
|
# Transition to a new state in the fix-forward state machine
|
|
143
279
|
def transition_to(new_state)
|
|
144
280
|
raise "Invalid state: #{new_state}" unless STATES.key?(new_state)
|
|
145
281
|
|
|
282
|
+
old_state = @current_state
|
|
146
283
|
@state_history << {
|
|
147
284
|
from: @current_state,
|
|
148
285
|
to: new_state,
|
|
@@ -150,6 +287,7 @@ module Aidp
|
|
|
150
287
|
timestamp: Time.now
|
|
151
288
|
}
|
|
152
289
|
@current_state = new_state
|
|
290
|
+
Aidp.logger.debug("work_loop", "State transition", from: old_state, to: new_state, iteration: @iteration_count, step: @step_name)
|
|
153
291
|
end
|
|
154
292
|
|
|
155
293
|
# Display summary of state transitions
|
|
@@ -209,20 +347,24 @@ module Aidp
|
|
|
209
347
|
prd_content = load_prd
|
|
210
348
|
style_guide = load_style_guide
|
|
211
349
|
user_input = format_user_input(context[:user_input])
|
|
350
|
+
deterministic_outputs = Array(context[:deterministic_outputs])
|
|
351
|
+
previous_summary = context[:previous_agent_summary]
|
|
212
352
|
|
|
213
353
|
initial_prompt = build_initial_prompt_content(
|
|
214
354
|
template: template_content,
|
|
215
355
|
prd: prd_content,
|
|
216
356
|
style_guide: style_guide,
|
|
217
357
|
user_input: user_input,
|
|
218
|
-
step_name: @step_name
|
|
358
|
+
step_name: @step_name,
|
|
359
|
+
deterministic_outputs: deterministic_outputs,
|
|
360
|
+
previous_agent_summary: previous_summary
|
|
219
361
|
)
|
|
220
362
|
|
|
221
363
|
@prompt_manager.write(initial_prompt)
|
|
222
364
|
display_message(" Created PROMPT.md (#{initial_prompt.length} chars)", type: :info)
|
|
223
365
|
end
|
|
224
366
|
|
|
225
|
-
def build_initial_prompt_content(template:, prd:, style_guide:, user_input:, step_name:)
|
|
367
|
+
def build_initial_prompt_content(template:, prd:, style_guide:, user_input:, step_name:, deterministic_outputs:, previous_agent_summary:)
|
|
226
368
|
parts = []
|
|
227
369
|
|
|
228
370
|
parts << "# Work Loop: #{step_name}"
|
|
@@ -252,6 +394,21 @@ module Aidp
|
|
|
252
394
|
parts << ""
|
|
253
395
|
end
|
|
254
396
|
|
|
397
|
+
if previous_agent_summary && !previous_agent_summary.empty?
|
|
398
|
+
parts << "## Previous Agent Summary"
|
|
399
|
+
parts << previous_agent_summary
|
|
400
|
+
parts << ""
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
unless deterministic_outputs.empty?
|
|
404
|
+
parts << "## Recent Deterministic Outputs"
|
|
405
|
+
deterministic_outputs.each do |entry|
|
|
406
|
+
parts << "- #{entry[:name]} (status: #{entry[:status]})"
|
|
407
|
+
parts << " Output: #{entry[:output_path] || "n/a"}"
|
|
408
|
+
end
|
|
409
|
+
parts << ""
|
|
410
|
+
end
|
|
411
|
+
|
|
255
412
|
if style_guide
|
|
256
413
|
parts << "## LLM Style Guide"
|
|
257
414
|
parts << style_guide
|