aidp 0.14.2 → 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 +13 -38
- data/lib/aidp/execute/agent_signal_parser.rb +46 -0
- data/lib/aidp/execute/deterministic_unit.rb +254 -0
- data/lib/aidp/execute/steps.rb +1 -1
- data/lib/aidp/execute/work_loop_runner.rb +181 -31
- 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/version.rb +1 -1
- data/templates/implementation/simple_task.md +5 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9a7dbc13b3136399f56fc64c00753506be8cbb35d5d3bf0f932799cc12a10504
|
4
|
+
data.tar.gz: 20afb73742ef4379f51fc05a9ae9cfb71ec95269856d79da0c0ce48c07c0aa39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41837e458506b1dc31724b611d46792d446da2f9b0303a37ded28fddbb8f29cfbaeeb417fddc24a3f34a0ed40f5ef3b9d96bf6c827c548be92436ca5ead751c2
|
7
|
+
data.tar.gz: fd57d77e5f8a85c38b3343c4db7a96ec7764066c420e2810d650ef97b51b4d47461c6779d3d9d856baa6730e15a204708b0c0b151d2bc4c00b71d86df68f988d
|
data/README.md
CHANGED
@@ -20,11 +20,8 @@ aidp config --interactive
|
|
20
20
|
aidp init
|
21
21
|
# Creates LLM_STYLE_GUIDE.md, PROJECT_ANALYSIS.md, CODE_QUALITY_PLAN.md
|
22
22
|
|
23
|
-
# Start
|
24
|
-
aidp
|
25
|
-
|
26
|
-
# Or run in background
|
27
|
-
aidp execute --background
|
23
|
+
# Start Copilot interactive mode (default)
|
24
|
+
aidp
|
28
25
|
```
|
29
26
|
|
30
27
|
### First-Time Setup
|
@@ -58,16 +55,12 @@ AIDP implements **work loops** - an iterative execution pattern where AI agents
|
|
58
55
|
|
59
56
|
See [Work Loops Guide](docs/WORK_LOOPS_GUIDE.md) for details.
|
60
57
|
|
61
|
-
###
|
58
|
+
### Job Management
|
62
59
|
|
63
|
-
|
60
|
+
Monitor and control background jobs:
|
64
61
|
|
65
62
|
```bash
|
66
|
-
#
|
67
|
-
aidp execute --background
|
68
|
-
✓ Started background job: 20251005_235912_a1b2c3d4
|
69
|
-
|
70
|
-
# Monitor progress
|
63
|
+
# List and manage jobs
|
71
64
|
aidp jobs list # List all jobs
|
72
65
|
aidp jobs status <job_id> # Show job status
|
73
66
|
aidp jobs logs <job_id> --tail # View recent logs
|
@@ -102,17 +95,14 @@ aidp checkpoint history 20
|
|
102
95
|
|
103
96
|
## Command Reference
|
104
97
|
|
105
|
-
###
|
98
|
+
### Copilot Mode
|
106
99
|
|
107
100
|
```bash
|
108
|
-
#
|
109
|
-
aidp
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
# Analyze mode - Analyze codebase
|
114
|
-
aidp analyze # Interactive analysis
|
115
|
-
aidp analyze --background # Background analysis
|
101
|
+
# Start interactive Copilot mode (default)
|
102
|
+
aidp # AI-guided workflow selection
|
103
|
+
|
104
|
+
# Copilot can perform both analysis and development based on your needs
|
105
|
+
# It will interactively help you choose the right approach
|
116
106
|
```
|
117
107
|
|
118
108
|
### Job Management
|
@@ -207,6 +197,21 @@ harness:
|
|
207
197
|
- "bundle exec rspec"
|
208
198
|
lint_commands:
|
209
199
|
- "bundle exec standardrb"
|
200
|
+
units:
|
201
|
+
deterministic:
|
202
|
+
- name: run_full_tests
|
203
|
+
command: "bundle exec rake spec"
|
204
|
+
enabled: false
|
205
|
+
next:
|
206
|
+
success: agentic
|
207
|
+
failure: decide_whats_next
|
208
|
+
- name: wait_for_github
|
209
|
+
type: "wait"
|
210
|
+
metadata:
|
211
|
+
interval_seconds: 60
|
212
|
+
defaults:
|
213
|
+
on_no_next_step: wait_for_github
|
214
|
+
fallback_agentic: decide_whats_next
|
210
215
|
|
211
216
|
providers:
|
212
217
|
claude:
|
@@ -329,40 +334,36 @@ The questions file is only created when the AI needs additional information beyo
|
|
329
334
|
### Standard Interactive Workflow
|
330
335
|
|
331
336
|
```bash
|
332
|
-
# Start
|
333
|
-
aidp
|
334
|
-
|
335
|
-
#
|
336
|
-
#
|
337
|
-
#
|
338
|
-
#
|
337
|
+
# Start Copilot mode (default)
|
338
|
+
aidp
|
339
|
+
|
340
|
+
# Copilot will guide you through:
|
341
|
+
# - Understanding your project goals
|
342
|
+
# - Selecting the right workflow (analysis, development, or both)
|
343
|
+
# - Answering questions about your requirements
|
344
|
+
# - Reviewing generated files (PRD, architecture, etc.)
|
345
|
+
# - Automatic execution with harness managing retries
|
339
346
|
```
|
340
347
|
|
341
|
-
###
|
348
|
+
### Project Analysis
|
342
349
|
|
343
350
|
```bash
|
344
|
-
#
|
345
|
-
aidp
|
346
|
-
✓ Started background job: 20251005_235912_a1b2c3d4
|
347
|
-
|
348
|
-
# Terminal 2: Watch progress in real-time
|
349
|
-
aidp checkpoint summary --watch
|
350
|
-
|
351
|
-
# Terminal 3: Monitor job status
|
352
|
-
aidp jobs status 20251005_235912_a1b2c3d4 --follow
|
351
|
+
# High-level project analysis and documentation
|
352
|
+
aidp init
|
353
353
|
|
354
|
-
#
|
355
|
-
|
356
|
-
|
354
|
+
# Creates:
|
355
|
+
# - LLM_STYLE_GUIDE.md
|
356
|
+
# - PROJECT_ANALYSIS.md
|
357
|
+
# - CODE_QUALITY_PLAN.md
|
357
358
|
```
|
358
359
|
|
359
|
-
###
|
360
|
+
### Progress Monitoring
|
360
361
|
|
361
362
|
```bash
|
362
|
-
#
|
363
|
-
aidp
|
363
|
+
# Watch progress in real-time
|
364
|
+
aidp checkpoint summary --watch
|
364
365
|
|
365
|
-
# Check
|
366
|
+
# Check job status
|
366
367
|
aidp jobs list
|
367
368
|
aidp checkpoint summary
|
368
369
|
```
|
data/lib/aidp/cli.rb
CHANGED
@@ -196,15 +196,15 @@ module Aidp
|
|
196
196
|
tui.start_display_loop
|
197
197
|
|
198
198
|
begin
|
199
|
-
#
|
200
|
-
|
199
|
+
# Copilot is now the default mode - no menu selection
|
200
|
+
# The guided workflow selector will internally choose appropriate mode
|
201
|
+
mode = :guided
|
201
202
|
|
202
203
|
# Get workflow configuration (no spinner - may wait for user input)
|
203
204
|
workflow_config = workflow_selector.select_workflow(harness_mode: false, mode: mode)
|
204
205
|
|
205
|
-
#
|
206
|
-
|
207
|
-
actual_mode = workflow_config[:mode] || mode
|
206
|
+
# Use the mode determined by the guided workflow selector
|
207
|
+
actual_mode = workflow_config[:mode] || :execute
|
208
208
|
|
209
209
|
# Pass workflow configuration to harness
|
210
210
|
harness_options = {
|
@@ -265,8 +265,7 @@ module Aidp
|
|
265
265
|
opts.separator "AI Development Pipeline - Autonomous development workflow automation"
|
266
266
|
opts.separator ""
|
267
267
|
opts.separator "Commands:"
|
268
|
-
opts.separator "
|
269
|
-
opts.separator " execute [--background] Start execute mode workflow"
|
268
|
+
opts.separator " (no command) Start Copilot interactive mode (default)"
|
270
269
|
opts.separator " init Analyse project and bootstrap quality docs"
|
271
270
|
opts.separator " watch <issues_url> Run fully automatic watch mode"
|
272
271
|
opts.separator " status Show current system status"
|
@@ -302,9 +301,12 @@ module Aidp
|
|
302
301
|
|
303
302
|
opts.separator ""
|
304
303
|
opts.separator "Examples:"
|
305
|
-
opts.separator " # Start
|
306
|
-
opts.separator " aidp
|
307
|
-
opts.separator "
|
304
|
+
opts.separator " # Start interactive Copilot mode"
|
305
|
+
opts.separator " aidp"
|
306
|
+
opts.separator ""
|
307
|
+
opts.separator " # Project bootstrap"
|
308
|
+
opts.separator " aidp init # High-level analysis and docs"
|
309
|
+
opts.separator " aidp config --interactive # Configure providers"
|
308
310
|
opts.separator ""
|
309
311
|
opts.separator " # Monitor background jobs"
|
310
312
|
opts.separator " aidp jobs list # List all jobs"
|
@@ -315,20 +317,14 @@ module Aidp
|
|
315
317
|
opts.separator " aidp checkpoint summary --watch # Auto-refresh every 5s"
|
316
318
|
opts.separator " aidp checkpoint summary --watch --interval 10"
|
317
319
|
opts.separator ""
|
318
|
-
opts.separator " # Project bootstrap"
|
319
|
-
opts.separator " aidp init"
|
320
|
-
opts.separator " aidp config --interactive"
|
321
320
|
opts.separator " # Fully automatic orchestration"
|
322
|
-
opts.separator ""
|
323
321
|
opts.separator " aidp watch https://github.com/<org>/<repo>/issues"
|
324
322
|
opts.separator " aidp watch owner/repo --interval 120 --provider claude"
|
325
323
|
opts.separator ""
|
326
324
|
opts.separator " # Other commands"
|
327
325
|
opts.separator " aidp providers # Check provider health"
|
328
326
|
opts.separator " aidp providers info claude # Show detailed provider info"
|
329
|
-
opts.separator " aidp providers refresh # Refresh all provider info"
|
330
327
|
opts.separator " aidp mcp # Show MCP server dashboard"
|
331
|
-
opts.separator " aidp mcp check dash-api filesystem # Check provider eligibility"
|
332
328
|
opts.separator " aidp checkpoint history 20 # Show last 20 checkpoints"
|
333
329
|
opts.separator ""
|
334
330
|
opts.separator "For more information, visit: https://github.com/viamin/aidp"
|
@@ -342,7 +338,7 @@ module Aidp
|
|
342
338
|
# Determine if the invocation is a subcommand style call
|
343
339
|
def subcommand?(args)
|
344
340
|
return false if args.nil? || args.empty?
|
345
|
-
%w[status jobs kb harness
|
341
|
+
%w[status jobs kb harness providers checkpoint mcp issue config init watch].include?(args.first)
|
346
342
|
end
|
347
343
|
|
348
344
|
def run_subcommand(args)
|
@@ -352,8 +348,6 @@ module Aidp
|
|
352
348
|
when "jobs" then run_jobs_command(args)
|
353
349
|
when "kb" then run_kb_command(args)
|
354
350
|
when "harness" then run_harness_command(args)
|
355
|
-
when "execute" then run_execute_command(args)
|
356
|
-
when "analyze" then run_execute_command(args, mode: :analyze) # symmetry
|
357
351
|
when "providers" then run_providers_command(args)
|
358
352
|
when "checkpoint" then run_checkpoint_command(args)
|
359
353
|
when "mcp" then run_mcp_command(args)
|
@@ -923,25 +917,6 @@ module Aidp
|
|
923
917
|
mode&.to_sym
|
924
918
|
end
|
925
919
|
|
926
|
-
def select_mode_interactive(tui)
|
927
|
-
mode_options = [
|
928
|
-
"🤖 Guided Workflow (Copilot) - AI helps you choose the right workflow",
|
929
|
-
"🔬 Analyze Mode - Analyze your codebase for insights and recommendations",
|
930
|
-
"🏗️ Execute Mode - Build new features with guided development workflow"
|
931
|
-
]
|
932
|
-
selected = tui.single_select("Welcome to AI Dev Pipeline! Choose your mode", mode_options, default: 1)
|
933
|
-
# Announce mode explicitly in headless contexts (handled internally otherwise)
|
934
|
-
if (defined?(RSpec) || ENV["RSPEC_RUNNING"]) && tui.respond_to?(:announce_mode)
|
935
|
-
tui.announce_mode(:guided) if selected == mode_options[0]
|
936
|
-
tui.announce_mode(:analyze) if selected == mode_options[1]
|
937
|
-
tui.announce_mode(:execute) if selected == mode_options[2]
|
938
|
-
end
|
939
|
-
return :guided if selected == mode_options[0]
|
940
|
-
return :analyze if selected == mode_options[1]
|
941
|
-
return :execute if selected == mode_options[2]
|
942
|
-
:analyze
|
943
|
-
end
|
944
|
-
|
945
920
|
def display_harness_result(result)
|
946
921
|
case result[:status]
|
947
922
|
when "completed"
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Aidp
|
4
|
+
module Execute
|
5
|
+
module AgentSignalParser
|
6
|
+
def self.extract_next_unit(output)
|
7
|
+
return nil unless output
|
8
|
+
|
9
|
+
output.to_s.each_line do |line|
|
10
|
+
token = token_from_line(line)
|
11
|
+
next unless token
|
12
|
+
|
13
|
+
return normalize_token(token)
|
14
|
+
end
|
15
|
+
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.normalize_token(raw)
|
20
|
+
return nil if raw.nil? || raw.empty?
|
21
|
+
|
22
|
+
token = raw.downcase.strip
|
23
|
+
token.gsub!(/\s+/, "_")
|
24
|
+
token.to_sym
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.token_from_line(line)
|
28
|
+
return nil unless line
|
29
|
+
|
30
|
+
trimmed = line.lstrip
|
31
|
+
separator_index = trimmed.index(":") || trimmed.index("=")
|
32
|
+
return nil unless separator_index
|
33
|
+
|
34
|
+
key = trimmed[0...separator_index].strip
|
35
|
+
value = trimmed[(separator_index + 1)..]&.strip
|
36
|
+
|
37
|
+
return nil unless key && value
|
38
|
+
return value if key.casecmp("next_unit").zero? || key.casecmp("next_step").zero?
|
39
|
+
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private_class_method :token_from_line
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -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
|
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,212 @@ module Aidp
|
|
63
68
|
@iteration_count = 0
|
64
69
|
transition_to(:ready)
|
65
70
|
|
66
|
-
Aidp.logger.info("work_loop", "Starting
|
71
|
+
Aidp.logger.info("work_loop", "Starting hybrid work loop execution", step: step_name, max_iterations: MAX_ITERATIONS)
|
67
72
|
|
68
|
-
display_message("🔄 Starting
|
69
|
-
display_message("
|
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)
|
70
75
|
|
71
|
-
# Display guard policy status
|
72
76
|
display_guard_policy_status
|
73
77
|
|
74
|
-
|
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
|
+
|
75
126
|
create_initial_prompt(step_spec, context)
|
76
127
|
|
77
|
-
# Main fix-forward work loop
|
78
128
|
loop do
|
79
129
|
@iteration_count += 1
|
80
130
|
display_message(" Iteration #{@iteration_count} [State: #{STATES[@current_state]}]", type: :info)
|
81
131
|
|
82
132
|
if @iteration_count > MAX_ITERATIONS
|
83
133
|
Aidp.logger.error("work_loop", "Max iterations exceeded", step: @step_name, iterations: @iteration_count)
|
84
|
-
|
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
|
+
)
|
85
144
|
end
|
86
145
|
|
87
|
-
# State: READY - Starting new iteration
|
88
146
|
transition_to(:ready) unless @current_state == :ready
|
89
147
|
|
90
|
-
# State: APPLY_PATCH - Agent applies changes
|
91
148
|
transition_to(:apply_patch)
|
92
|
-
|
149
|
+
agent_result = apply_patch
|
93
150
|
|
94
|
-
# State: TEST - Run tests and linters
|
95
151
|
transition_to(:test)
|
96
152
|
test_results = @test_runner.run_tests
|
97
153
|
lint_results = @test_runner.run_linters
|
98
154
|
|
99
|
-
# Record checkpoint at intervals
|
100
155
|
record_periodic_checkpoint(test_results, lint_results)
|
101
156
|
|
102
|
-
# Check if tests passed
|
103
157
|
tests_pass = test_results[:success] && lint_results[:success]
|
104
158
|
|
105
159
|
if tests_pass
|
106
|
-
# State: PASS - Tests passed
|
107
160
|
transition_to(:pass)
|
108
161
|
|
109
|
-
|
110
|
-
if agent_marked_complete?(result)
|
111
|
-
# State: DONE - Work complete
|
162
|
+
if agent_marked_complete?(agent_result)
|
112
163
|
transition_to(:done)
|
113
164
|
record_final_checkpoint(test_results, lint_results)
|
114
|
-
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)
|
115
166
|
display_state_summary
|
116
167
|
archive_and_cleanup
|
117
|
-
|
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
|
+
)
|
118
176
|
else
|
119
|
-
# Tests pass but work not complete - continue
|
120
177
|
display_message(" Tests passed but work not marked complete", type: :info)
|
121
178
|
transition_to(:next_patch)
|
122
179
|
end
|
123
180
|
else
|
124
|
-
# State: FAIL - Tests failed
|
125
181
|
transition_to(:fail)
|
126
182
|
display_message(" Tests or linters failed", type: :warning)
|
127
183
|
|
128
|
-
# State: DIAGNOSE - Analyze failures
|
129
184
|
transition_to(:diagnose)
|
130
185
|
diagnostic = diagnose_failures(test_results, lint_results)
|
131
186
|
|
132
|
-
# State: NEXT_PATCH - Prepare for next iteration
|
133
187
|
transition_to(:next_patch)
|
134
188
|
prepare_next_iteration(test_results, lint_results, diagnostic)
|
135
189
|
end
|
136
190
|
end
|
191
|
+
end
|
137
192
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
+
)
|
143
219
|
end
|
144
220
|
|
145
|
-
|
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
|
146
277
|
|
147
278
|
# Transition to a new state in the fix-forward state machine
|
148
279
|
def transition_to(new_state)
|
@@ -216,20 +347,24 @@ module Aidp
|
|
216
347
|
prd_content = load_prd
|
217
348
|
style_guide = load_style_guide
|
218
349
|
user_input = format_user_input(context[:user_input])
|
350
|
+
deterministic_outputs = Array(context[:deterministic_outputs])
|
351
|
+
previous_summary = context[:previous_agent_summary]
|
219
352
|
|
220
353
|
initial_prompt = build_initial_prompt_content(
|
221
354
|
template: template_content,
|
222
355
|
prd: prd_content,
|
223
356
|
style_guide: style_guide,
|
224
357
|
user_input: user_input,
|
225
|
-
step_name: @step_name
|
358
|
+
step_name: @step_name,
|
359
|
+
deterministic_outputs: deterministic_outputs,
|
360
|
+
previous_agent_summary: previous_summary
|
226
361
|
)
|
227
362
|
|
228
363
|
@prompt_manager.write(initial_prompt)
|
229
364
|
display_message(" Created PROMPT.md (#{initial_prompt.length} chars)", type: :info)
|
230
365
|
end
|
231
366
|
|
232
|
-
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:)
|
233
368
|
parts = []
|
234
369
|
|
235
370
|
parts << "# Work Loop: #{step_name}"
|
@@ -259,6 +394,21 @@ module Aidp
|
|
259
394
|
parts << ""
|
260
395
|
end
|
261
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
|
+
|
262
412
|
if style_guide
|
263
413
|
parts << "## LLM Style Guide"
|
264
414
|
parts << style_guide
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "deterministic_unit"
|
4
|
+
require_relative "../logger"
|
5
|
+
|
6
|
+
module Aidp
|
7
|
+
module Execute
|
8
|
+
class WorkLoopUnitScheduler
|
9
|
+
Unit = Struct.new(:type, :name, :definition, keyword_init: true) do
|
10
|
+
def agentic?
|
11
|
+
type == :agentic
|
12
|
+
end
|
13
|
+
|
14
|
+
def deterministic?
|
15
|
+
type == :deterministic
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :last_agentic_summary
|
20
|
+
|
21
|
+
def initialize(units_config, clock: Time)
|
22
|
+
@clock = clock
|
23
|
+
@deterministic_definitions = build_deterministic_definitions(units_config[:deterministic])
|
24
|
+
@defaults = default_options.merge(units_config[:defaults] || {})
|
25
|
+
@pending_units = []
|
26
|
+
@deterministic_history = []
|
27
|
+
@deterministic_state = Hash.new { |h, key| h[key] = default_deterministic_state }
|
28
|
+
@agentic_runs = []
|
29
|
+
@last_agentic_summary = nil
|
30
|
+
@consecutive_deciders = 0
|
31
|
+
@completed = false
|
32
|
+
@started = false
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_unit
|
36
|
+
return nil if @completed
|
37
|
+
|
38
|
+
unless @started
|
39
|
+
@started = true
|
40
|
+
queue_requested_unit(@defaults[:initial_unit] || :agentic)
|
41
|
+
end
|
42
|
+
|
43
|
+
unit = @pending_units.shift
|
44
|
+
return unit if unit
|
45
|
+
|
46
|
+
queue_requested_unit(@defaults[:on_no_next_step] || :agentic)
|
47
|
+
|
48
|
+
@pending_units.shift
|
49
|
+
end
|
50
|
+
|
51
|
+
def record_agentic_result(result, requested_next: nil, summary: nil, completed: false)
|
52
|
+
@last_agentic_summary = summarize(summary)
|
53
|
+
@agentic_runs << {timestamp: @clock.now, result: result}
|
54
|
+
|
55
|
+
queue_requested_unit(requested_next) if requested_next
|
56
|
+
|
57
|
+
mark_completed if completed && !requested_next
|
58
|
+
end
|
59
|
+
|
60
|
+
def record_deterministic_result(definition, result)
|
61
|
+
state = @deterministic_state[definition.name]
|
62
|
+
state[:last_run_at] = result.finished_at
|
63
|
+
|
64
|
+
state[:current_backoff] = if result.success? || result.status == :event
|
65
|
+
definition.min_interval_seconds
|
66
|
+
else
|
67
|
+
[
|
68
|
+
state[:current_backoff] * definition.backoff_multiplier,
|
69
|
+
definition.max_backoff_seconds
|
70
|
+
].min
|
71
|
+
end
|
72
|
+
|
73
|
+
@deterministic_history << {
|
74
|
+
name: definition.name,
|
75
|
+
status: result.status,
|
76
|
+
output_path: result.output_path,
|
77
|
+
finished_at: result.finished_at,
|
78
|
+
data: result.data
|
79
|
+
}
|
80
|
+
|
81
|
+
requested = definition.next_for(result.status)
|
82
|
+
queue_requested_unit(requested) if requested
|
83
|
+
end
|
84
|
+
|
85
|
+
def deterministic_context(limit: 5)
|
86
|
+
@deterministic_history.last(limit)
|
87
|
+
end
|
88
|
+
|
89
|
+
def completed?
|
90
|
+
@completed
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def summarize(summary)
|
96
|
+
return nil unless summary
|
97
|
+
content = summary.to_s.strip
|
98
|
+
return nil if content.empty?
|
99
|
+
(content.length > 500) ? "#{content[0...500]}…" : content
|
100
|
+
end
|
101
|
+
|
102
|
+
def mark_completed
|
103
|
+
@completed = true
|
104
|
+
@pending_units.clear
|
105
|
+
end
|
106
|
+
|
107
|
+
def queue_requested_unit(identifier)
|
108
|
+
return if identifier.nil?
|
109
|
+
|
110
|
+
case identifier.to_sym
|
111
|
+
when :agentic
|
112
|
+
enqueue_agentic(:primary)
|
113
|
+
when :decide_whats_next
|
114
|
+
enqueue_agentic(:decide_whats_next)
|
115
|
+
else
|
116
|
+
enqueue_deterministic(identifier.to_s)
|
117
|
+
end
|
118
|
+
rescue NoMethodError
|
119
|
+
enqueue_agentic(:primary)
|
120
|
+
end
|
121
|
+
|
122
|
+
def enqueue_agentic(name)
|
123
|
+
if name == :decide_whats_next
|
124
|
+
if @consecutive_deciders >= @defaults[:max_consecutive_deciders]
|
125
|
+
enqueue_agentic(:primary)
|
126
|
+
return
|
127
|
+
end
|
128
|
+
@consecutive_deciders += 1
|
129
|
+
else
|
130
|
+
@consecutive_deciders = 0
|
131
|
+
end
|
132
|
+
|
133
|
+
@pending_units << Unit.new(type: :agentic, name: name)
|
134
|
+
end
|
135
|
+
|
136
|
+
def enqueue_deterministic(name)
|
137
|
+
definition = @deterministic_definitions[name]
|
138
|
+
unless definition
|
139
|
+
enqueue_agentic((@defaults[:fallback_agentic] || :agentic).to_sym)
|
140
|
+
return
|
141
|
+
end
|
142
|
+
return unless definition.enabled
|
143
|
+
|
144
|
+
state = @deterministic_state[definition.name]
|
145
|
+
state[:current_backoff] ||= definition.min_interval_seconds
|
146
|
+
|
147
|
+
if cooldown_remaining(definition).positive?
|
148
|
+
enqueue_agentic((@defaults[:fallback_agentic] || :agentic).to_sym)
|
149
|
+
return
|
150
|
+
end
|
151
|
+
|
152
|
+
@pending_units << Unit.new(type: :deterministic, name: definition.name, definition: definition)
|
153
|
+
end
|
154
|
+
|
155
|
+
def cooldown_remaining(definition)
|
156
|
+
state = @deterministic_state[definition.name]
|
157
|
+
state[:current_backoff] ||= definition.min_interval_seconds
|
158
|
+
|
159
|
+
return 0 unless state[:last_run_at]
|
160
|
+
|
161
|
+
next_allowed_at = state[:last_run_at] + state[:current_backoff]
|
162
|
+
remaining = next_allowed_at - @clock.now
|
163
|
+
remaining.positive? ? remaining : 0
|
164
|
+
end
|
165
|
+
|
166
|
+
def build_deterministic_definitions(config_list)
|
167
|
+
Array(config_list).each_with_object({}) do |config, mapping|
|
168
|
+
definition = DeterministicUnits::Definition.new(config.transform_keys(&:to_sym))
|
169
|
+
mapping[definition.name] = definition
|
170
|
+
rescue KeyError, ArgumentError => e
|
171
|
+
Aidp.logger.warn("work_loop", "Skipping invalid deterministic unit configuration",
|
172
|
+
name: config[:name], error: e.message)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_deterministic_state
|
177
|
+
{last_run_at: nil, current_backoff: nil}
|
178
|
+
end
|
179
|
+
|
180
|
+
def default_options
|
181
|
+
{
|
182
|
+
initial_unit: :agentic,
|
183
|
+
on_no_next_step: :agentic,
|
184
|
+
fallback_agentic: :decide_whats_next,
|
185
|
+
max_consecutive_deciders: 1
|
186
|
+
}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -374,7 +374,8 @@ module Aidp
|
|
374
374
|
enabled: true,
|
375
375
|
max_iterations: 50,
|
376
376
|
test_commands: [],
|
377
|
-
lint_commands: []
|
377
|
+
lint_commands: [],
|
378
|
+
units: {}
|
378
379
|
},
|
379
380
|
properties: {
|
380
381
|
enabled: {
|
@@ -405,6 +406,95 @@ module Aidp
|
|
405
406
|
type: :string
|
406
407
|
}
|
407
408
|
},
|
409
|
+
units: {
|
410
|
+
type: :hash,
|
411
|
+
required: false,
|
412
|
+
default: {},
|
413
|
+
properties: {
|
414
|
+
deterministic: {
|
415
|
+
type: :array,
|
416
|
+
required: false,
|
417
|
+
default: [],
|
418
|
+
items: {
|
419
|
+
type: :hash,
|
420
|
+
properties: {
|
421
|
+
name: {
|
422
|
+
type: :string,
|
423
|
+
required: true
|
424
|
+
},
|
425
|
+
command: {
|
426
|
+
type: :string,
|
427
|
+
required: false
|
428
|
+
},
|
429
|
+
type: {
|
430
|
+
type: :string,
|
431
|
+
required: false,
|
432
|
+
enum: ["command", "wait"]
|
433
|
+
},
|
434
|
+
output_file: {
|
435
|
+
type: :string,
|
436
|
+
required: false
|
437
|
+
},
|
438
|
+
enabled: {
|
439
|
+
type: :boolean,
|
440
|
+
required: false,
|
441
|
+
default: true
|
442
|
+
},
|
443
|
+
min_interval_seconds: {
|
444
|
+
type: :integer,
|
445
|
+
required: false,
|
446
|
+
min: 1
|
447
|
+
},
|
448
|
+
max_backoff_seconds: {
|
449
|
+
type: :integer,
|
450
|
+
required: false,
|
451
|
+
min: 1
|
452
|
+
},
|
453
|
+
backoff_multiplier: {
|
454
|
+
type: :number,
|
455
|
+
required: false,
|
456
|
+
min: 1.0
|
457
|
+
},
|
458
|
+
metadata: {
|
459
|
+
type: :hash,
|
460
|
+
required: false,
|
461
|
+
default: {}
|
462
|
+
},
|
463
|
+
next: {
|
464
|
+
type: :hash,
|
465
|
+
required: false,
|
466
|
+
default: {}
|
467
|
+
}
|
468
|
+
}
|
469
|
+
}
|
470
|
+
},
|
471
|
+
defaults: {
|
472
|
+
type: :hash,
|
473
|
+
required: false,
|
474
|
+
default: {},
|
475
|
+
properties: {
|
476
|
+
initial_unit: {
|
477
|
+
type: :string,
|
478
|
+
required: false
|
479
|
+
},
|
480
|
+
on_no_next_step: {
|
481
|
+
type: :string,
|
482
|
+
required: false
|
483
|
+
},
|
484
|
+
fallback_agentic: {
|
485
|
+
type: :string,
|
486
|
+
required: false
|
487
|
+
},
|
488
|
+
max_consecutive_deciders: {
|
489
|
+
type: :integer,
|
490
|
+
required: false,
|
491
|
+
min: 1,
|
492
|
+
max: 10
|
493
|
+
}
|
494
|
+
}
|
495
|
+
}
|
496
|
+
}
|
497
|
+
},
|
408
498
|
guards: {
|
409
499
|
type: :hash,
|
410
500
|
required: false,
|
@@ -150,6 +150,10 @@ module Aidp
|
|
150
150
|
harness_config[:work_loop] || default_work_loop_config
|
151
151
|
end
|
152
152
|
|
153
|
+
def work_loop_units_config
|
154
|
+
work_loop_config[:units] || default_units_config
|
155
|
+
end
|
156
|
+
|
153
157
|
# Check if work loops are enabled
|
154
158
|
def work_loop_enabled?
|
155
159
|
work_loop_config[:enabled]
|
@@ -483,7 +487,62 @@ module Aidp
|
|
483
487
|
max_iterations: 50,
|
484
488
|
test_commands: [],
|
485
489
|
lint_commands: [],
|
486
|
-
guards: default_guards_config
|
490
|
+
guards: default_guards_config,
|
491
|
+
units: default_units_config
|
492
|
+
}
|
493
|
+
end
|
494
|
+
|
495
|
+
def default_units_config
|
496
|
+
{
|
497
|
+
deterministic: [
|
498
|
+
{
|
499
|
+
name: "run_full_tests",
|
500
|
+
command: "bundle exec rake spec",
|
501
|
+
output_file: ".aidp/out/run_full_tests.yml",
|
502
|
+
enabled: false,
|
503
|
+
min_interval_seconds: 300,
|
504
|
+
max_backoff_seconds: 1800,
|
505
|
+
next: {
|
506
|
+
success: :agentic,
|
507
|
+
failure: :decide_whats_next,
|
508
|
+
else: :decide_whats_next
|
509
|
+
}
|
510
|
+
},
|
511
|
+
{
|
512
|
+
name: "run_lint",
|
513
|
+
command: "bundle exec standardrb",
|
514
|
+
output_file: ".aidp/out/run_lint.yml",
|
515
|
+
enabled: false,
|
516
|
+
min_interval_seconds: 300,
|
517
|
+
max_backoff_seconds: 1800,
|
518
|
+
next: {
|
519
|
+
success: :agentic,
|
520
|
+
failure: :decide_whats_next,
|
521
|
+
else: :decide_whats_next
|
522
|
+
}
|
523
|
+
},
|
524
|
+
{
|
525
|
+
name: "wait_for_github",
|
526
|
+
type: :wait,
|
527
|
+
output_file: ".aidp/out/wait_for_github.yml",
|
528
|
+
metadata: {
|
529
|
+
interval_seconds: 60,
|
530
|
+
backoff_seconds: 60
|
531
|
+
},
|
532
|
+
min_interval_seconds: 60,
|
533
|
+
max_backoff_seconds: 900,
|
534
|
+
next: {
|
535
|
+
event: :agentic,
|
536
|
+
else: :wait_for_github
|
537
|
+
}
|
538
|
+
}
|
539
|
+
],
|
540
|
+
defaults: {
|
541
|
+
initial_unit: :agentic,
|
542
|
+
on_no_next_step: :wait_for_github,
|
543
|
+
fallback_agentic: :decide_whats_next,
|
544
|
+
max_consecutive_deciders: 1
|
545
|
+
}
|
487
546
|
}
|
488
547
|
end
|
489
548
|
|
data/lib/aidp/version.rb
CHANGED
@@ -12,6 +12,9 @@ You are executing a simple, focused task within the AIDP work loop.
|
|
12
12
|
2. **Execute the task exactly as described**
|
13
13
|
3. **Verify your work** by running any validation commands specified
|
14
14
|
4. **Edit this PROMPT.md** to track progress and mark complete when done
|
15
|
+
5. **Request follow-up units** by adding `NEXT_UNIT: <unit_name>` to your
|
16
|
+
response when deterministic work (tests, linting, wait states) should run
|
17
|
+
next
|
15
18
|
|
16
19
|
## Completion Criteria
|
17
20
|
|
@@ -34,3 +37,5 @@ STATUS: COMPLETE
|
|
34
37
|
- Keep your changes minimal and focused on the task
|
35
38
|
- If the task involves running commands, show the command output
|
36
39
|
- If the task involves fixing issues, list what was fixed
|
40
|
+
- When you need automation to continue after this task, emit `NEXT_UNIT: agentic`
|
41
|
+
or a deterministic unit such as `run_full_tests` or `wait_for_github`
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aidp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bart Agapinan
|
@@ -260,9 +260,11 @@ files:
|
|
260
260
|
- lib/aidp/daemon/process_manager.rb
|
261
261
|
- lib/aidp/daemon/runner.rb
|
262
262
|
- lib/aidp/debug_mixin.rb
|
263
|
+
- lib/aidp/execute/agent_signal_parser.rb
|
263
264
|
- lib/aidp/execute/async_work_loop_runner.rb
|
264
265
|
- lib/aidp/execute/checkpoint.rb
|
265
266
|
- lib/aidp/execute/checkpoint_display.rb
|
267
|
+
- lib/aidp/execute/deterministic_unit.rb
|
266
268
|
- lib/aidp/execute/future_work_backlog.rb
|
267
269
|
- lib/aidp/execute/guard_policy.rb
|
268
270
|
- lib/aidp/execute/instruction_queue.rb
|
@@ -274,6 +276,7 @@ files:
|
|
274
276
|
- lib/aidp/execute/steps.rb
|
275
277
|
- lib/aidp/execute/work_loop_runner.rb
|
276
278
|
- lib/aidp/execute/work_loop_state.rb
|
279
|
+
- lib/aidp/execute/work_loop_unit_scheduler.rb
|
277
280
|
- lib/aidp/execute/workflow_selector.rb
|
278
281
|
- lib/aidp/harness/completion_checker.rb
|
279
282
|
- lib/aidp/harness/condition_detector.rb
|