aidp 0.15.2 → 0.17.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 +47 -0
- data/lib/aidp/analyze/error_handler.rb +46 -28
- data/lib/aidp/analyze/progress.rb +1 -1
- data/lib/aidp/analyze/runner.rb +27 -5
- data/lib/aidp/analyze/steps.rb +4 -0
- data/lib/aidp/cli/jobs_command.rb +2 -1
- data/lib/aidp/cli.rb +1086 -4
- data/lib/aidp/concurrency/backoff.rb +148 -0
- data/lib/aidp/concurrency/exec.rb +192 -0
- data/lib/aidp/concurrency/wait.rb +148 -0
- data/lib/aidp/concurrency.rb +71 -0
- data/lib/aidp/config.rb +21 -1
- data/lib/aidp/daemon/runner.rb +9 -8
- data/lib/aidp/debug_mixin.rb +1 -0
- data/lib/aidp/errors.rb +12 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
- data/lib/aidp/execute/checkpoint.rb +1 -1
- data/lib/aidp/execute/future_work_backlog.rb +1 -1
- data/lib/aidp/execute/interactive_repl.rb +102 -11
- data/lib/aidp/execute/progress.rb +1 -1
- data/lib/aidp/execute/repl_macros.rb +845 -2
- data/lib/aidp/execute/runner.rb +27 -5
- data/lib/aidp/execute/steps.rb +2 -0
- data/lib/aidp/harness/config_loader.rb +24 -2
- data/lib/aidp/harness/config_validator.rb +1 -1
- data/lib/aidp/harness/enhanced_runner.rb +16 -2
- data/lib/aidp/harness/error_handler.rb +1 -1
- data/lib/aidp/harness/provider_info.rb +19 -15
- data/lib/aidp/harness/provider_manager.rb +47 -41
- data/lib/aidp/harness/runner.rb +3 -11
- data/lib/aidp/harness/state/persistence.rb +1 -6
- data/lib/aidp/harness/state_manager.rb +115 -7
- data/lib/aidp/harness/status_display.rb +11 -18
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +1 -1
- data/lib/aidp/harness/user_interface.rb +12 -15
- data/lib/aidp/jobs/background_runner.rb +16 -6
- data/lib/aidp/providers/codex.rb +0 -1
- data/lib/aidp/providers/cursor.rb +0 -1
- data/lib/aidp/providers/github_copilot.rb +0 -1
- data/lib/aidp/providers/opencode.rb +0 -1
- data/lib/aidp/skills/composer.rb +178 -0
- data/lib/aidp/skills/loader.rb +205 -0
- data/lib/aidp/skills/registry.rb +222 -0
- data/lib/aidp/skills/router.rb +178 -0
- data/lib/aidp/skills/skill.rb +174 -0
- data/lib/aidp/skills/wizard/builder.rb +141 -0
- data/lib/aidp/skills/wizard/controller.rb +145 -0
- data/lib/aidp/skills/wizard/differ.rb +232 -0
- data/lib/aidp/skills/wizard/prompter.rb +317 -0
- data/lib/aidp/skills/wizard/template_library.rb +164 -0
- data/lib/aidp/skills/wizard/writer.rb +105 -0
- data/lib/aidp/skills.rb +30 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +93 -28
- data/lib/aidp/watch/runner.rb +3 -2
- data/lib/aidp/workstream_executor.rb +244 -0
- data/lib/aidp/workstream_state.rb +212 -0
- data/lib/aidp/worktree.rb +208 -0
- data/lib/aidp.rb +6 -0
- data/templates/skills/README.md +334 -0
- data/templates/skills/architecture_analyst/SKILL.md +173 -0
- data/templates/skills/product_strategist/SKILL.md +141 -0
- data/templates/skills/repository_analyst/SKILL.md +117 -0
- data/templates/skills/test_analyzer/SKILL.md +213 -0
- metadata +29 -4
|
@@ -6,6 +6,7 @@ require "time"
|
|
|
6
6
|
require_relative "../message_display"
|
|
7
7
|
require_relative "../execute/prompt_manager"
|
|
8
8
|
require_relative "../harness/runner"
|
|
9
|
+
require_relative "../worktree"
|
|
9
10
|
|
|
10
11
|
module Aidp
|
|
11
12
|
module Watch
|
|
@@ -17,10 +18,11 @@ module Aidp
|
|
|
17
18
|
BUILD_LABEL = "aidp-build"
|
|
18
19
|
IMPLEMENTATION_STEP = "16_IMPLEMENTATION"
|
|
19
20
|
|
|
20
|
-
def initialize(repository_client:, state_store:, project_dir: Dir.pwd)
|
|
21
|
+
def initialize(repository_client:, state_store:, project_dir: Dir.pwd, use_workstreams: true)
|
|
21
22
|
@repository_client = repository_client
|
|
22
23
|
@state_store = state_store
|
|
23
24
|
@project_dir = project_dir
|
|
25
|
+
@use_workstreams = use_workstreams
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def process(issue)
|
|
@@ -30,27 +32,36 @@ module Aidp
|
|
|
30
32
|
plan_data = ensure_plan_data(number)
|
|
31
33
|
return unless plan_data
|
|
32
34
|
|
|
35
|
+
slug = workstream_slug_for(issue)
|
|
33
36
|
branch_name = branch_name_for(issue)
|
|
34
|
-
@state_store.record_build_status(number, status: "running", details: {branch: branch_name, started_at: Time.now.utc.iso8601})
|
|
37
|
+
@state_store.record_build_status(number, status: "running", details: {branch: branch_name, workstream: slug, started_at: Time.now.utc.iso8601})
|
|
35
38
|
|
|
36
39
|
ensure_git_repo!
|
|
37
40
|
base_branch = detect_base_branch
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
if @use_workstreams
|
|
43
|
+
workstream_path = setup_workstream(slug: slug, branch_name: branch_name, base_branch: base_branch)
|
|
44
|
+
working_dir = workstream_path
|
|
45
|
+
else
|
|
46
|
+
checkout_branch(base_branch, branch_name)
|
|
47
|
+
working_dir = @project_dir
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
prompt_content = build_prompt(issue: issue, plan_data: plan_data)
|
|
41
|
-
write_prompt(prompt_content)
|
|
51
|
+
write_prompt(prompt_content, working_dir: working_dir)
|
|
42
52
|
|
|
43
53
|
user_input = build_user_input(issue: issue, plan_data: plan_data)
|
|
44
|
-
result = run_harness(user_input: user_input)
|
|
54
|
+
result = run_harness(user_input: user_input, working_dir: working_dir)
|
|
45
55
|
|
|
46
56
|
if result[:status] == "completed"
|
|
47
|
-
handle_success(issue: issue, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data)
|
|
57
|
+
handle_success(issue: issue, slug: slug, branch_name: branch_name, base_branch: base_branch, plan_data: plan_data, working_dir: working_dir)
|
|
48
58
|
else
|
|
49
|
-
handle_failure(issue: issue, result: result)
|
|
59
|
+
handle_failure(issue: issue, slug: slug, result: result)
|
|
50
60
|
end
|
|
51
61
|
rescue => e
|
|
52
62
|
display_message("❌ Implementation failed: #{e.message}", type: :error)
|
|
53
63
|
@state_store.record_build_status(issue[:number], status: "failed", details: {error: e.message})
|
|
64
|
+
cleanup_workstream(slug) if @use_workstreams && slug
|
|
54
65
|
raise
|
|
55
66
|
end
|
|
56
67
|
|
|
@@ -96,9 +107,56 @@ module Aidp
|
|
|
96
107
|
display_message("🌿 Checked out #{branch_name}", type: :info)
|
|
97
108
|
end
|
|
98
109
|
|
|
99
|
-
def
|
|
110
|
+
def workstream_slug_for(issue)
|
|
100
111
|
slug = issue[:title].to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
|
|
101
|
-
"
|
|
112
|
+
"issue-#{issue[:number]}-#{slug[0, 32]}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def branch_name_for(issue)
|
|
116
|
+
"aidp/#{workstream_slug_for(issue)}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def setup_workstream(slug:, branch_name:, base_branch:)
|
|
120
|
+
# Check if workstream already exists
|
|
121
|
+
existing = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
|
|
122
|
+
if existing
|
|
123
|
+
display_message("🔄 Reusing existing workstream: #{slug}", type: :info)
|
|
124
|
+
Dir.chdir(existing[:path]) do
|
|
125
|
+
run_git(["checkout", existing[:branch]])
|
|
126
|
+
run_git(%w[pull --ff-only], allow_failure: true)
|
|
127
|
+
end
|
|
128
|
+
return existing[:path]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Create new workstream
|
|
132
|
+
display_message("🌿 Creating workstream: #{slug}", type: :info)
|
|
133
|
+
result = Aidp::Worktree.create(
|
|
134
|
+
slug: slug,
|
|
135
|
+
project_dir: @project_dir,
|
|
136
|
+
branch: branch_name,
|
|
137
|
+
base_branch: base_branch
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if result[:success]
|
|
141
|
+
display_message("✅ Workstream created at #{result[:path]}", type: :success)
|
|
142
|
+
result[:path]
|
|
143
|
+
else
|
|
144
|
+
raise "Failed to create workstream: #{result[:message]}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def cleanup_workstream(slug)
|
|
149
|
+
return unless slug
|
|
150
|
+
|
|
151
|
+
display_message("🧹 Cleaning up workstream: #{slug}", type: :info)
|
|
152
|
+
result = Aidp::Worktree.remove(slug: slug, project_dir: @project_dir, force: true)
|
|
153
|
+
if result[:success]
|
|
154
|
+
display_message("✅ Workstream removed", type: :success)
|
|
155
|
+
else
|
|
156
|
+
display_message("⚠️ Failed to remove workstream: #{result[:message]}", type: :warn)
|
|
157
|
+
end
|
|
158
|
+
rescue => e
|
|
159
|
+
display_message("⚠️ Error cleaning up workstream: #{e.message}", type: :warn)
|
|
102
160
|
end
|
|
103
161
|
|
|
104
162
|
def build_prompt(issue:, plan_data:)
|
|
@@ -141,8 +199,8 @@ module Aidp
|
|
|
141
199
|
"_Unable to parse comment thread._"
|
|
142
200
|
end
|
|
143
201
|
|
|
144
|
-
def write_prompt(content)
|
|
145
|
-
prompt_manager = Aidp::Execute::PromptManager.new(
|
|
202
|
+
def write_prompt(content, working_dir: @project_dir)
|
|
203
|
+
prompt_manager = Aidp::Execute::PromptManager.new(working_dir)
|
|
146
204
|
prompt_manager.write(content)
|
|
147
205
|
display_message("📝 Wrote PROMPT.md with implementation contract", type: :info)
|
|
148
206
|
end
|
|
@@ -156,23 +214,24 @@ module Aidp
|
|
|
156
214
|
}.delete_if { |_k, v| v.nil? || v.empty? }
|
|
157
215
|
end
|
|
158
216
|
|
|
159
|
-
def run_harness(user_input:)
|
|
217
|
+
def run_harness(user_input:, working_dir: @project_dir)
|
|
160
218
|
options = {
|
|
161
219
|
selected_steps: [IMPLEMENTATION_STEP],
|
|
162
220
|
workflow_type: :watch_mode,
|
|
163
221
|
user_input: user_input
|
|
164
222
|
}
|
|
165
|
-
runner = Aidp::Harness::Runner.new(
|
|
223
|
+
runner = Aidp::Harness::Runner.new(working_dir, :execute, options)
|
|
166
224
|
runner.run
|
|
167
225
|
end
|
|
168
226
|
|
|
169
|
-
def handle_success(issue:, branch_name:, base_branch:, plan_data:)
|
|
170
|
-
stage_and_commit(issue)
|
|
171
|
-
pr_url = create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch)
|
|
227
|
+
def handle_success(issue:, slug:, branch_name:, base_branch:, plan_data:, working_dir:)
|
|
228
|
+
stage_and_commit(issue, working_dir: working_dir)
|
|
229
|
+
pr_url = create_pull_request(issue: issue, branch_name: branch_name, base_branch: base_branch, working_dir: working_dir)
|
|
172
230
|
|
|
231
|
+
workstream_note = @use_workstreams ? "\n- Workstream: `#{slug}`" : ""
|
|
173
232
|
comment = <<~COMMENT
|
|
174
233
|
✅ Implementation complete for ##{issue[:number]}.
|
|
175
|
-
- Branch: `#{branch_name}
|
|
234
|
+
- Branch: `#{branch_name}`#{workstream_note}
|
|
176
235
|
- Pull Request: #{pr_url}
|
|
177
236
|
|
|
178
237
|
Summary:
|
|
@@ -183,32 +242,38 @@ module Aidp
|
|
|
183
242
|
@state_store.record_build_status(
|
|
184
243
|
issue[:number],
|
|
185
244
|
status: "completed",
|
|
186
|
-
details: {branch: branch_name, pr_url: pr_url}
|
|
245
|
+
details: {branch: branch_name, workstream: slug, pr_url: pr_url}
|
|
187
246
|
)
|
|
188
247
|
display_message("🎉 Posted completion comment for issue ##{issue[:number]}", type: :success)
|
|
248
|
+
|
|
249
|
+
# Keep workstream for review - don't auto-cleanup on success
|
|
250
|
+
if @use_workstreams
|
|
251
|
+
display_message("ℹ️ Workstream #{slug} preserved for review. Remove with: aidp ws rm #{slug}", type: :muted)
|
|
252
|
+
end
|
|
189
253
|
end
|
|
190
254
|
|
|
191
|
-
def handle_failure(issue:, result:)
|
|
255
|
+
def handle_failure(issue:, slug:, result:)
|
|
192
256
|
message = result[:message] || "Unknown failure"
|
|
257
|
+
workstream_note = @use_workstreams ? " The workstream `#{slug}` has been left intact for debugging." : " The branch has been left intact for debugging."
|
|
193
258
|
comment = <<~COMMENT
|
|
194
259
|
❌ Implementation attempt for ##{issue[:number]} failed.
|
|
195
260
|
|
|
196
261
|
Status: #{result[:status]}
|
|
197
262
|
Details: #{message}
|
|
198
263
|
|
|
199
|
-
Please review the repository for partial changes
|
|
264
|
+
Please review the repository for partial changes.#{workstream_note}
|
|
200
265
|
COMMENT
|
|
201
266
|
@repository_client.post_comment(issue[:number], comment)
|
|
202
267
|
@state_store.record_build_status(
|
|
203
268
|
issue[:number],
|
|
204
269
|
status: "failed",
|
|
205
|
-
details: {message: message}
|
|
270
|
+
details: {message: message, workstream: slug}
|
|
206
271
|
)
|
|
207
272
|
display_message("⚠️ Build failure recorded for issue ##{issue[:number]}", type: :warn)
|
|
208
273
|
end
|
|
209
274
|
|
|
210
|
-
def stage_and_commit(issue)
|
|
211
|
-
Dir.chdir(
|
|
275
|
+
def stage_and_commit(issue, working_dir: @project_dir)
|
|
276
|
+
Dir.chdir(working_dir) do
|
|
212
277
|
status_output = run_git(%w[status --porcelain])
|
|
213
278
|
if status_output.strip.empty?
|
|
214
279
|
display_message("ℹ️ No file changes detected after work loop.", type: :muted)
|
|
@@ -222,9 +287,9 @@ module Aidp
|
|
|
222
287
|
end
|
|
223
288
|
end
|
|
224
289
|
|
|
225
|
-
def create_pull_request(issue:, branch_name:, base_branch:)
|
|
290
|
+
def create_pull_request(issue:, branch_name:, base_branch:, working_dir: @project_dir)
|
|
226
291
|
title = "aidp: Resolve ##{issue[:number]} - #{issue[:title]}"
|
|
227
|
-
test_summary = gather_test_summary
|
|
292
|
+
test_summary = gather_test_summary(working_dir: working_dir)
|
|
228
293
|
body = <<~BODY
|
|
229
294
|
## Summary
|
|
230
295
|
- Automated resolution for ##{issue[:number]}
|
|
@@ -244,8 +309,8 @@ module Aidp
|
|
|
244
309
|
extract_pr_url(output)
|
|
245
310
|
end
|
|
246
311
|
|
|
247
|
-
def gather_test_summary
|
|
248
|
-
Dir.chdir(
|
|
312
|
+
def gather_test_summary(working_dir: @project_dir)
|
|
313
|
+
Dir.chdir(working_dir) do
|
|
249
314
|
log_path = File.join(".aidp", "logs", "test_runner.log")
|
|
250
315
|
return "- Fix-forward harness executed; refer to #{log_path}" unless File.exist?(log_path)
|
|
251
316
|
|
|
@@ -257,7 +322,7 @@ module Aidp
|
|
|
257
322
|
end
|
|
258
323
|
end
|
|
259
324
|
rescue
|
|
260
|
-
"- Fix-forward harness
|
|
325
|
+
"- Fix-forward harness extracted successfully."
|
|
261
326
|
end
|
|
262
327
|
|
|
263
328
|
def extract_pr_url(output)
|
data/lib/aidp/watch/runner.rb
CHANGED
|
@@ -18,7 +18,7 @@ module Aidp
|
|
|
18
18
|
|
|
19
19
|
DEFAULT_INTERVAL = 30
|
|
20
20
|
|
|
21
|
-
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, prompt: TTY::Prompt.new)
|
|
21
|
+
def initialize(issues_url:, interval: DEFAULT_INTERVAL, provider_name: nil, gh_available: nil, project_dir: Dir.pwd, once: false, use_workstreams: true, prompt: TTY::Prompt.new)
|
|
22
22
|
@prompt = prompt
|
|
23
23
|
@interval = interval
|
|
24
24
|
@once = once
|
|
@@ -35,7 +35,8 @@ module Aidp
|
|
|
35
35
|
@build_processor = BuildProcessor.new(
|
|
36
36
|
repository_client: @repository_client,
|
|
37
37
|
state_store: @state_store,
|
|
38
|
-
project_dir: project_dir
|
|
38
|
+
project_dir: project_dir,
|
|
39
|
+
use_workstreams: use_workstreams
|
|
39
40
|
)
|
|
40
41
|
end
|
|
41
42
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent-ruby"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "worktree"
|
|
6
|
+
require_relative "workstream_state"
|
|
7
|
+
require_relative "harness/runner"
|
|
8
|
+
require_relative "message_display"
|
|
9
|
+
|
|
10
|
+
module Aidp
|
|
11
|
+
# Executes multiple workstreams in parallel using concurrent-ruby.
|
|
12
|
+
# Provides true parallel execution with process isolation and status tracking.
|
|
13
|
+
class WorkstreamExecutor
|
|
14
|
+
include Aidp::MessageDisplay
|
|
15
|
+
|
|
16
|
+
# Result from executing a workstream
|
|
17
|
+
WorkstreamResult = Struct.new(
|
|
18
|
+
:slug,
|
|
19
|
+
:status,
|
|
20
|
+
:exit_code,
|
|
21
|
+
:started_at,
|
|
22
|
+
:completed_at,
|
|
23
|
+
:duration,
|
|
24
|
+
:error,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def initialize(project_dir: Dir.pwd, max_concurrent: 3)
|
|
29
|
+
@project_dir = project_dir
|
|
30
|
+
@max_concurrent = max_concurrent
|
|
31
|
+
@results = Concurrent::Hash.new
|
|
32
|
+
@start_times = Concurrent::Hash.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Execute multiple workstreams in parallel
|
|
36
|
+
#
|
|
37
|
+
# @param slugs [Array<String>] Workstream slugs to execute
|
|
38
|
+
# @param options [Hash] Execution options
|
|
39
|
+
# @option options [Array<String>] :selected_steps Steps to execute
|
|
40
|
+
# @option options [Symbol] :workflow_type Workflow type (:execute, :analyze, etc.)
|
|
41
|
+
# @option options [Hash] :user_input User input for harness
|
|
42
|
+
# @return [Array<WorkstreamResult>] Results for each workstream
|
|
43
|
+
def execute_parallel(slugs, options = {})
|
|
44
|
+
validate_workstreams!(slugs)
|
|
45
|
+
|
|
46
|
+
display_message("🚀 Starting parallel execution of #{slugs.size} workstreams (max #{@max_concurrent} concurrent)", type: :info)
|
|
47
|
+
|
|
48
|
+
# Create thread pool with max concurrent limit
|
|
49
|
+
pool = Concurrent::FixedThreadPool.new(@max_concurrent)
|
|
50
|
+
|
|
51
|
+
# Create futures for each workstream
|
|
52
|
+
futures = slugs.map do |slug|
|
|
53
|
+
Concurrent::Future.execute(executor: pool) do
|
|
54
|
+
execute_workstream(slug, options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Wait for all futures to complete
|
|
59
|
+
results = futures.map(&:value)
|
|
60
|
+
|
|
61
|
+
# Shutdown pool gracefully
|
|
62
|
+
pool.shutdown
|
|
63
|
+
pool.wait_for_termination(30)
|
|
64
|
+
|
|
65
|
+
display_execution_summary(results)
|
|
66
|
+
results
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Execute all active workstreams in parallel
|
|
70
|
+
#
|
|
71
|
+
# @param options [Hash] Execution options (same as execute_parallel)
|
|
72
|
+
# @return [Array<WorkstreamResult>] Results for each workstream
|
|
73
|
+
def execute_all(options = {})
|
|
74
|
+
workstreams = Aidp::Worktree.list(project_dir: @project_dir)
|
|
75
|
+
active_slugs = workstreams.select { |ws| ws[:active] }.map { |ws| ws[:slug] }
|
|
76
|
+
|
|
77
|
+
if active_slugs.empty?
|
|
78
|
+
display_message("⚠️ No active workstreams found", type: :warn)
|
|
79
|
+
return []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
execute_parallel(active_slugs, options)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Execute a single workstream (used by futures in parallel execution)
|
|
86
|
+
#
|
|
87
|
+
# @param slug [String] Workstream slug
|
|
88
|
+
# @param options [Hash] Execution options
|
|
89
|
+
# @return [WorkstreamResult] Execution result
|
|
90
|
+
def execute_workstream(slug, options = {})
|
|
91
|
+
started_at = Time.now
|
|
92
|
+
@start_times[slug] = started_at
|
|
93
|
+
|
|
94
|
+
workstream = Aidp::Worktree.info(slug: slug, project_dir: @project_dir)
|
|
95
|
+
unless workstream
|
|
96
|
+
return WorkstreamResult.new(
|
|
97
|
+
slug: slug,
|
|
98
|
+
status: "error",
|
|
99
|
+
exit_code: 1,
|
|
100
|
+
started_at: started_at,
|
|
101
|
+
completed_at: Time.now,
|
|
102
|
+
duration: 0,
|
|
103
|
+
error: "Workstream not found"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
display_message("▶️ [#{slug}] Starting execution in #{workstream[:path]}", type: :info)
|
|
108
|
+
|
|
109
|
+
# Update workstream state to active
|
|
110
|
+
Aidp::WorkstreamState.update(
|
|
111
|
+
slug: slug,
|
|
112
|
+
project_dir: @project_dir,
|
|
113
|
+
status: "active",
|
|
114
|
+
started_at: started_at.utc.iso8601
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Execute in forked process for true isolation
|
|
118
|
+
pid = fork do
|
|
119
|
+
# Change to workstream directory
|
|
120
|
+
Dir.chdir(workstream[:path])
|
|
121
|
+
|
|
122
|
+
# Execute harness
|
|
123
|
+
runner = Aidp::Harness::Runner.new(
|
|
124
|
+
workstream[:path],
|
|
125
|
+
options[:mode] || :execute,
|
|
126
|
+
options
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = runner.run
|
|
130
|
+
|
|
131
|
+
# Update state on completion
|
|
132
|
+
exit_code = (result[:status] == "completed") ? 0 : 1
|
|
133
|
+
final_status = (result[:status] == "completed") ? "completed" : "failed"
|
|
134
|
+
|
|
135
|
+
Aidp::WorkstreamState.update(
|
|
136
|
+
slug: slug,
|
|
137
|
+
project_dir: @project_dir,
|
|
138
|
+
status: final_status,
|
|
139
|
+
completed_at: Time.now.utc.iso8601
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
exit(exit_code)
|
|
143
|
+
rescue => e
|
|
144
|
+
# Update state on error
|
|
145
|
+
Aidp::WorkstreamState.update(
|
|
146
|
+
slug: slug,
|
|
147
|
+
project_dir: @project_dir,
|
|
148
|
+
status: "failed",
|
|
149
|
+
completed_at: Time.now.utc.iso8601
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Log error and exit
|
|
153
|
+
warn("Error in workstream #{slug}: #{e.message}")
|
|
154
|
+
warn(e.backtrace.first(5).join("\n"))
|
|
155
|
+
exit(1)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Wait for child process
|
|
159
|
+
_pid, status = Process.wait2(pid)
|
|
160
|
+
completed_at = Time.now
|
|
161
|
+
duration = completed_at - started_at
|
|
162
|
+
|
|
163
|
+
# Build result
|
|
164
|
+
result_status = status.success? ? "completed" : "failed"
|
|
165
|
+
result = WorkstreamResult.new(
|
|
166
|
+
slug: slug,
|
|
167
|
+
status: result_status,
|
|
168
|
+
exit_code: status.exitstatus,
|
|
169
|
+
started_at: started_at,
|
|
170
|
+
completed_at: completed_at,
|
|
171
|
+
duration: duration,
|
|
172
|
+
error: status.success? ? nil : "Process exited with code #{status.exitstatus}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@results[slug] = result
|
|
176
|
+
|
|
177
|
+
display_message("#{status.success? ? "✅" : "❌"} [#{slug}] #{result_status.capitalize} in #{format_duration(duration)}", type: status.success? ? :success : :error)
|
|
178
|
+
|
|
179
|
+
result
|
|
180
|
+
rescue => e
|
|
181
|
+
completed_at = Time.now
|
|
182
|
+
duration = completed_at - started_at
|
|
183
|
+
|
|
184
|
+
WorkstreamResult.new(
|
|
185
|
+
slug: slug,
|
|
186
|
+
status: "error",
|
|
187
|
+
exit_code: 1,
|
|
188
|
+
started_at: started_at,
|
|
189
|
+
completed_at: completed_at,
|
|
190
|
+
duration: duration,
|
|
191
|
+
error: e.message
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
# Validate that all workstreams exist
|
|
198
|
+
def validate_workstreams!(slugs)
|
|
199
|
+
invalid = slugs.reject do |slug|
|
|
200
|
+
Aidp::Worktree.exists?(slug: slug, project_dir: @project_dir)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
unless invalid.empty?
|
|
204
|
+
raise ArgumentError, "Workstreams not found: #{invalid.join(", ")}"
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Display execution summary
|
|
209
|
+
def display_execution_summary(results)
|
|
210
|
+
completed = results.count { |r| r.status == "completed" }
|
|
211
|
+
failed = results.count { |r| r.status == "failed" || r.status == "error" }
|
|
212
|
+
total_duration = results.sum(&:duration)
|
|
213
|
+
|
|
214
|
+
display_message("\n" + "=" * 60, type: :muted)
|
|
215
|
+
display_message("📊 Execution Summary", type: :info)
|
|
216
|
+
display_message("Total: #{results.size} | Completed: #{completed} | Failed: #{failed}", type: :info)
|
|
217
|
+
display_message("Total Duration: #{format_duration(total_duration)}", type: :info)
|
|
218
|
+
|
|
219
|
+
if failed > 0
|
|
220
|
+
display_message("\n❌ Failed Workstreams:", type: :error)
|
|
221
|
+
results.select { |r| r.status != "completed" }.each do |result|
|
|
222
|
+
display_message(" - #{result.slug}: #{result.error}", type: :error)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
display_message("=" * 60, type: :muted)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Format duration in human-readable format
|
|
230
|
+
def format_duration(seconds)
|
|
231
|
+
if seconds < 60
|
|
232
|
+
"#{seconds.round(1)}s"
|
|
233
|
+
elsif seconds < 3600
|
|
234
|
+
minutes = (seconds / 60).floor
|
|
235
|
+
secs = (seconds % 60).round
|
|
236
|
+
"#{minutes}m #{secs}s"
|
|
237
|
+
else
|
|
238
|
+
hours = (seconds / 3600).floor
|
|
239
|
+
minutes = ((seconds % 3600) / 60).floor
|
|
240
|
+
"#{hours}h #{minutes}m"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|