aidp 0.10.0 → 0.11.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 +194 -25
- data/lib/aidp/analyze/kb_inspector.rb +2 -15
- data/lib/aidp/analyze/progress.rb +2 -1
- data/lib/aidp/analyze/ruby_maat_integration.rb +2 -15
- data/lib/aidp/analyze/runner.rb +64 -20
- data/lib/aidp/analyze/steps.rb +10 -8
- data/lib/aidp/analyze/tree_sitter_grammar_loader.rb +2 -13
- data/lib/aidp/analyze/tree_sitter_scan.rb +2 -13
- data/lib/aidp/cli/checkpoint_command.rb +98 -0
- data/lib/aidp/cli/first_run_wizard.rb +65 -94
- data/lib/aidp/cli/jobs_command.rb +249 -34
- data/lib/aidp/cli.rb +312 -38
- data/lib/aidp/config.rb +5 -8
- data/lib/aidp/debug_logger.rb +4 -4
- data/lib/aidp/debug_mixin.rb +11 -4
- data/lib/aidp/execute/checkpoint.rb +282 -0
- data/lib/aidp/execute/checkpoint_display.rb +221 -0
- data/lib/aidp/execute/progress.rb +2 -1
- data/lib/aidp/execute/prompt_manager.rb +62 -0
- data/lib/aidp/execute/runner.rb +53 -24
- data/lib/aidp/execute/steps.rb +36 -27
- data/lib/aidp/execute/work_loop_runner.rb +308 -0
- data/lib/aidp/execute/workflow_selector.rb +26 -17
- data/lib/aidp/harness/condition_detector.rb +4 -4
- data/lib/aidp/harness/config_schema.rb +40 -0
- data/lib/aidp/harness/config_validator.rb +3 -6
- data/lib/aidp/harness/configuration.rb +35 -1
- data/lib/aidp/harness/enhanced_runner.rb +22 -1
- data/lib/aidp/harness/error_handler.rb +103 -28
- data/lib/aidp/harness/provider_factory.rb +4 -1
- data/lib/aidp/harness/provider_manager.rb +250 -15
- data/lib/aidp/harness/runner.rb +3 -14
- data/lib/aidp/harness/simple_user_interface.rb +2 -15
- data/lib/aidp/harness/status_display.rb +12 -17
- data/lib/aidp/harness/test_runner.rb +83 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
- data/lib/aidp/harness/ui/error_handler.rb +4 -0
- data/lib/aidp/harness/ui/frame_manager.rb +10 -8
- data/lib/aidp/harness/ui/job_monitor.rb +2 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
- data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
- data/lib/aidp/harness/ui/progress_display.rb +8 -12
- data/lib/aidp/harness/ui/question_collector.rb +2 -0
- data/lib/aidp/harness/ui/spinner_group.rb +2 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
- data/lib/aidp/harness/ui/status_manager.rb +4 -2
- data/lib/aidp/harness/ui/status_widget.rb +3 -1
- data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
- data/lib/aidp/harness/user_interface.rb +12 -17
- data/lib/aidp/jobs/background_runner.rb +278 -0
- data/lib/aidp/message_display.rb +48 -0
- data/lib/aidp/provider_manager.rb +3 -1
- data/lib/aidp/providers/anthropic.rb +100 -17
- data/lib/aidp/providers/base.rb +42 -11
- data/lib/aidp/providers/codex.rb +248 -0
- data/lib/aidp/providers/cursor.rb +35 -42
- data/lib/aidp/providers/gemini.rb +25 -15
- data/lib/aidp/providers/github_copilot.rb +41 -42
- data/lib/aidp/providers/opencode.rb +34 -41
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/workflows/definitions.rb +357 -0
- data/lib/aidp/workflows/selector.rb +171 -0
- data/lib/aidp.rb +12 -0
- data/templates/planning/generate_llm_style_guide.md +119 -0
- metadata +38 -26
- /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
- /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
- /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
- /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
- /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
- /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
- /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
- /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
- /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
- /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
- /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
- /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
- /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
- /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
- /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
- /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
- /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
- /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
- /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
- /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
- /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
- /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
- /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
- /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
- /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
require_relative "../execute/checkpoint"
|
5
|
+
require_relative "../execute/checkpoint_display"
|
6
|
+
|
7
|
+
module Aidp
|
8
|
+
module CLI
|
9
|
+
# CLI command for viewing checkpoint data and progress reports
|
10
|
+
class CheckpointCommand < Thor
|
11
|
+
desc "show", "Show the latest checkpoint data"
|
12
|
+
def show
|
13
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
14
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
15
|
+
|
16
|
+
latest = checkpoint.latest_checkpoint
|
17
|
+
if latest
|
18
|
+
display.display_checkpoint(latest, show_details: true)
|
19
|
+
else
|
20
|
+
puts "No checkpoint data found."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "summary", "Show progress summary with trends"
|
25
|
+
def summary
|
26
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
27
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
28
|
+
|
29
|
+
summary = checkpoint.progress_summary
|
30
|
+
if summary
|
31
|
+
display.display_progress_summary(summary)
|
32
|
+
else
|
33
|
+
puts "No checkpoint data found."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "history [LIMIT]", "Show checkpoint history (default: last 10)"
|
38
|
+
def history(limit = "10")
|
39
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
40
|
+
display = Aidp::Execute::CheckpointDisplay.new
|
41
|
+
|
42
|
+
history = checkpoint.checkpoint_history(limit: limit.to_i)
|
43
|
+
if history.any?
|
44
|
+
display.display_checkpoint_history(history, limit: limit.to_i)
|
45
|
+
else
|
46
|
+
puts "No checkpoint history found."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
desc "clear", "Clear all checkpoint data"
|
51
|
+
option :force, type: :boolean, default: false, desc: "Skip confirmation"
|
52
|
+
def clear
|
53
|
+
unless options[:force]
|
54
|
+
prompt = TTY::Prompt.new
|
55
|
+
confirm = prompt.yes?("Are you sure you want to clear all checkpoint data?")
|
56
|
+
return unless confirm
|
57
|
+
end
|
58
|
+
|
59
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
60
|
+
checkpoint.clear
|
61
|
+
puts "✓ Checkpoint data cleared."
|
62
|
+
end
|
63
|
+
|
64
|
+
desc "metrics", "Show detailed metrics for the latest checkpoint"
|
65
|
+
def metrics
|
66
|
+
checkpoint = Aidp::Execute::Checkpoint.new(Dir.pwd)
|
67
|
+
latest = checkpoint.latest_checkpoint
|
68
|
+
|
69
|
+
unless latest
|
70
|
+
puts "No checkpoint data found."
|
71
|
+
return
|
72
|
+
end
|
73
|
+
|
74
|
+
puts
|
75
|
+
puts "📊 Detailed Metrics"
|
76
|
+
puts "=" * 60
|
77
|
+
|
78
|
+
metrics = latest[:metrics]
|
79
|
+
puts "Lines of Code: #{metrics[:lines_of_code]}"
|
80
|
+
puts "File Count: #{metrics[:file_count]}"
|
81
|
+
puts "Test Coverage: #{metrics[:test_coverage]}%"
|
82
|
+
puts "Code Quality: #{metrics[:code_quality]}%"
|
83
|
+
puts "PRD Task Progress: #{metrics[:prd_task_progress]}%"
|
84
|
+
|
85
|
+
if metrics[:tests_passing]
|
86
|
+
puts "Tests: #{metrics[:tests_passing] ? "✓ Passing" : "✗ Failing"}"
|
87
|
+
end
|
88
|
+
|
89
|
+
if metrics[:linters_passing]
|
90
|
+
puts "Linters: #{metrics[:linters_passing] ? "✓ Passing" : "✗ Failing"}"
|
91
|
+
end
|
92
|
+
|
93
|
+
puts "=" * 60
|
94
|
+
puts
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -3,11 +3,14 @@
|
|
3
3
|
|
4
4
|
require "yaml"
|
5
5
|
require "tty-prompt"
|
6
|
+
require_relative "../harness/provider_factory"
|
6
7
|
|
7
8
|
module Aidp
|
8
9
|
class CLI
|
9
10
|
# Handles interactive first-time project setup when no aidp.yml exists
|
10
11
|
class FirstRunWizard
|
12
|
+
include Aidp::MessageDisplay
|
13
|
+
|
11
14
|
TEMPLATES_DIR = File.expand_path(File.join(__dir__, "..", "..", "..", "templates"))
|
12
15
|
|
13
16
|
def self.ensure_config(project_dir, non_interactive: false, prompt: TTY::Prompt.new)
|
@@ -42,34 +45,10 @@ module Aidp
|
|
42
45
|
@prompt = prompt
|
43
46
|
end
|
44
47
|
|
45
|
-
# Helper method for consistent message display using TTY::Prompt
|
46
|
-
def display_message(message, type: :info)
|
47
|
-
color = case type
|
48
|
-
when :error then :red
|
49
|
-
when :success then :green
|
50
|
-
when :warning then :yellow
|
51
|
-
when :info then :blue
|
52
|
-
when :highlight then :cyan
|
53
|
-
when :muted then :bright_black
|
54
|
-
else :white
|
55
|
-
end
|
56
|
-
|
57
|
-
@prompt.say(message, color: color)
|
58
|
-
end
|
59
|
-
|
60
48
|
def run
|
61
49
|
banner
|
62
|
-
|
63
|
-
|
64
|
-
case choice
|
65
|
-
when "1" then return finish(write_quick_config(@project_dir))
|
66
|
-
when "2" then return finish(run_custom)
|
67
|
-
when "q", "Q" then display_message("Exiting without creating configuration.")
|
68
|
-
return false
|
69
|
-
else
|
70
|
-
display_message("Invalid selection. Please choose one of the listed options.", type: :warning)
|
71
|
-
end
|
72
|
-
end
|
50
|
+
# Always run the full interactive custom configuration flow.
|
51
|
+
finish(run_custom)
|
73
52
|
end
|
74
53
|
|
75
54
|
def run_setup_config
|
@@ -96,22 +75,10 @@ module Aidp
|
|
96
75
|
def banner
|
97
76
|
display_message("\n🚀 First-time setup detected", type: :highlight)
|
98
77
|
display_message("No 'aidp.yml' configuration file found in #{relative(@project_dir)}.")
|
99
|
-
display_message("
|
78
|
+
display_message("Creating a configuration so you can start using AI Dev Pipeline.")
|
100
79
|
display_message("")
|
101
80
|
end
|
102
81
|
|
103
|
-
def ask_choice
|
104
|
-
display_message("Choose a configuration style:") unless @asking
|
105
|
-
|
106
|
-
options = {
|
107
|
-
"Quick setup (cursor + macos, no API keys needed)" => "1",
|
108
|
-
"Custom setup (choose your own providers and settings)" => "2",
|
109
|
-
"Quit" => "q"
|
110
|
-
}
|
111
|
-
|
112
|
-
@prompt.select("Select an option:", options, default: "Quick setup (cursor + macos, no API keys needed)")
|
113
|
-
end
|
114
|
-
|
115
82
|
def finish(path)
|
116
83
|
if path
|
117
84
|
display_message("\n✅ Configuration created at #{relative(path)}", type: :success)
|
@@ -135,7 +102,7 @@ module Aidp
|
|
135
102
|
end
|
136
103
|
|
137
104
|
def write_minimal_config(project_dir)
|
138
|
-
dest = File.join(project_dir, "aidp.yml")
|
105
|
+
dest = File.join(project_dir, ".aidp", "aidp.yml")
|
139
106
|
return dest if File.exist?(dest)
|
140
107
|
data = {
|
141
108
|
"harness" => {
|
@@ -151,31 +118,7 @@ module Aidp
|
|
151
118
|
}
|
152
119
|
}
|
153
120
|
}
|
154
|
-
File.
|
155
|
-
dest
|
156
|
-
end
|
157
|
-
|
158
|
-
def write_quick_config(project_dir)
|
159
|
-
dest = File.join(project_dir, "aidp.yml")
|
160
|
-
return dest if File.exist?(dest)
|
161
|
-
data = {
|
162
|
-
"harness" => {
|
163
|
-
"max_retries" => 2,
|
164
|
-
"default_provider" => "cursor",
|
165
|
-
"fallback_providers" => ["macos"],
|
166
|
-
"no_api_keys_required" => true
|
167
|
-
},
|
168
|
-
"providers" => {
|
169
|
-
"cursor" => {
|
170
|
-
"type" => "subscription",
|
171
|
-
"default_flags" => []
|
172
|
-
},
|
173
|
-
"macos" => {
|
174
|
-
"type" => "usage_based",
|
175
|
-
"default_flags" => []
|
176
|
-
}
|
177
|
-
}
|
178
|
-
}
|
121
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
179
122
|
File.write(dest, YAML.dump(data))
|
180
123
|
dest
|
181
124
|
end
|
@@ -204,15 +147,11 @@ module Aidp
|
|
204
147
|
provider_name = default_provider.split(" - ").first
|
205
148
|
|
206
149
|
# Validate fallback providers
|
207
|
-
|
208
|
-
q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
|
209
|
-
q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
|
210
|
-
end
|
150
|
+
fallback_providers = select_fallback_providers(available_providers, provider_name)
|
211
151
|
|
212
152
|
restrict = @prompt.yes?("Only use providers that don't require API keys?", default: false)
|
213
153
|
|
214
|
-
# Process
|
215
|
-
fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
154
|
+
# Process providers preserving order
|
216
155
|
providers = [provider_name] + fallback_providers
|
217
156
|
providers.uniq!
|
218
157
|
|
@@ -261,15 +200,11 @@ module Aidp
|
|
261
200
|
provider_name = default_provider.split(" - ").first
|
262
201
|
|
263
202
|
# Validate fallback providers
|
264
|
-
|
265
|
-
q.validate(/^[a-zA-Z0-9_,\s]+$/, "Invalid characters. Use only letters, numbers, commas, and spaces.")
|
266
|
-
q.validate(->(input) { validate_provider_list(input, available_providers) }, "One or more providers are not supported.")
|
267
|
-
end
|
203
|
+
fallback_providers = select_fallback_providers(available_providers, provider_name, preselected: current_fallbacks - [provider_name])
|
268
204
|
|
269
205
|
restrict_input = @prompt.yes?("Only use providers that don't require API keys?", default: current_restrict)
|
270
206
|
|
271
|
-
# Process
|
272
|
-
fallback_providers = fallback_input.split(/\s*,\s*/).map(&:strip).reject(&:empty?)
|
207
|
+
# Process providers preserving order
|
273
208
|
providers = [provider_name] + fallback_providers
|
274
209
|
providers.uniq!
|
275
210
|
|
@@ -340,24 +275,25 @@ module Aidp
|
|
340
275
|
|
341
276
|
# Get available providers for validation
|
342
277
|
def get_available_providers
|
343
|
-
#
|
344
|
-
|
345
|
-
|
346
|
-
#
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
278
|
+
# Get all supported providers from the factory (single source of truth)
|
279
|
+
all_providers = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES.keys
|
280
|
+
|
281
|
+
# Filter out providers we don't want to show in the wizard
|
282
|
+
# - "anthropic" is an internal name, we show "claude" instead
|
283
|
+
# - "macos" is disabled (as per issue #73)
|
284
|
+
excluded = ["anthropic", "macos"]
|
285
|
+
available = all_providers - excluded
|
286
|
+
|
287
|
+
# Get display names from the providers themselves
|
288
|
+
available.map do |provider_name|
|
289
|
+
provider_class = Aidp::Harness::ProviderFactory::PROVIDER_CLASSES[provider_name]
|
290
|
+
if provider_class
|
291
|
+
# Instantiate to get display name
|
292
|
+
instance = provider_class.new
|
293
|
+
display_name = instance.display_name
|
294
|
+
"#{provider_name} - #{display_name}"
|
359
295
|
else
|
360
|
-
|
296
|
+
provider_name
|
361
297
|
end
|
362
298
|
end
|
363
299
|
end
|
@@ -373,6 +309,41 @@ module Aidp
|
|
373
309
|
valid_providers = available_providers.map { |p| p.split(" - ").first }
|
374
310
|
providers.all? { |provider| valid_providers.include?(provider) }
|
375
311
|
end
|
312
|
+
|
313
|
+
# Interactive ordered multi-select for fallback providers
|
314
|
+
def select_fallback_providers(available_with_labels, default_provider, preselected: [])
|
315
|
+
# Extract provider names and exclude the already chosen default
|
316
|
+
options = available_with_labels.map { |o| o.split(" - ").first }
|
317
|
+
candidates = options.reject { |p| p == default_provider }
|
318
|
+
|
319
|
+
return [] if candidates.empty?
|
320
|
+
|
321
|
+
selected = preselected.select { |p| candidates.include?(p) }
|
322
|
+
|
323
|
+
loop do
|
324
|
+
display_message("\nSelect fallback providers in order of preference (first = highest priority).", type: :info)
|
325
|
+
display_message("Current order: #{selected.empty? ? "(none)" : selected.join(" > ")}", type: :muted)
|
326
|
+
choice = @prompt.select("Add provider, or choose an action:", cycle: true) do |menu|
|
327
|
+
(candidates - selected).each { |prov| menu.choice("Add #{prov}", prov) }
|
328
|
+
menu.choice("Done", :done)
|
329
|
+
menu.choice("Clear", :clear) unless selected.empty?
|
330
|
+
menu.choice("Remove last (#{selected.last})", :remove) unless selected.empty?
|
331
|
+
end
|
332
|
+
|
333
|
+
case choice
|
334
|
+
when :done
|
335
|
+
break
|
336
|
+
when :clear
|
337
|
+
selected.clear
|
338
|
+
when :remove
|
339
|
+
selected.pop
|
340
|
+
else
|
341
|
+
selected << choice unless selected.include?(choice)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
selected
|
346
|
+
end
|
376
347
|
end
|
377
348
|
end
|
378
349
|
end
|
@@ -7,10 +7,13 @@ require "io/console"
|
|
7
7
|
require "json"
|
8
8
|
require_relative "terminal_io"
|
9
9
|
require_relative "../storage/file_manager"
|
10
|
+
require_relative "../jobs/background_runner"
|
10
11
|
|
11
12
|
module Aidp
|
12
13
|
class CLI
|
13
14
|
class JobsCommand
|
15
|
+
include Aidp::MessageDisplay
|
16
|
+
|
14
17
|
def initialize(input: nil, output: nil, prompt: TTY::Prompt.new)
|
15
18
|
@io = TerminalIO.new(input: input, output: output)
|
16
19
|
@prompt = prompt
|
@@ -18,47 +21,271 @@ module Aidp
|
|
18
21
|
@running = true
|
19
22
|
@view_mode = :list
|
20
23
|
@selected_job_id = nil
|
21
|
-
@jobs_displayed = false
|
24
|
+
@jobs_displayed = false # Track if we've displayed jobs in interactive mode
|
22
25
|
@file_manager = Aidp::Storage::FileManager.new(File.join(Dir.pwd, ".aidp"))
|
23
|
-
@
|
26
|
+
@background_runner = Aidp::Jobs::BackgroundRunner.new(Dir.pwd)
|
27
|
+
@screen_width = 80 # Default screen width
|
24
28
|
end
|
25
29
|
|
26
30
|
private
|
27
31
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
when
|
33
|
-
|
34
|
-
when
|
35
|
-
|
36
|
-
|
32
|
+
public
|
33
|
+
|
34
|
+
def run(subcommand = nil, args = [])
|
35
|
+
case subcommand
|
36
|
+
when "list", nil
|
37
|
+
list_jobs
|
38
|
+
when "status"
|
39
|
+
job_id = args.shift
|
40
|
+
if job_id
|
41
|
+
show_job_status(job_id, follow: args.include?("--follow"))
|
42
|
+
else
|
43
|
+
display_message("Usage: aidp jobs status <job_id> [--follow]", type: :error)
|
44
|
+
end
|
45
|
+
when "stop"
|
46
|
+
job_id = args.shift
|
47
|
+
if job_id
|
48
|
+
stop_job(job_id)
|
49
|
+
else
|
50
|
+
display_message("Usage: aidp jobs stop <job_id>", type: :error)
|
51
|
+
end
|
52
|
+
when "logs"
|
53
|
+
job_id = args.shift
|
54
|
+
if job_id
|
55
|
+
show_job_logs(job_id, tail: args.include?("--tail"), follow: args.include?("--follow"))
|
56
|
+
else
|
57
|
+
display_message("Usage: aidp jobs logs <job_id> [--tail] [--follow]", type: :error)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
display_message("Unknown jobs subcommand: #{subcommand}", type: :error)
|
61
|
+
display_message("Available: list, status, stop, logs", type: :info)
|
37
62
|
end
|
38
|
-
@prompt.say(message, color: color)
|
39
63
|
end
|
40
64
|
|
41
|
-
|
42
|
-
|
43
|
-
def run
|
44
|
-
# Simple harness jobs display
|
45
|
-
jobs = fetch_harness_jobs
|
65
|
+
def list_jobs
|
66
|
+
jobs = @background_runner.list_jobs
|
46
67
|
|
47
68
|
if jobs.empty?
|
48
|
-
display_message("
|
69
|
+
display_message("Background Jobs", type: :info)
|
49
70
|
display_message("-" * @screen_width, type: :muted)
|
50
71
|
display_message("")
|
51
|
-
display_message("No
|
72
|
+
display_message("No background jobs found", type: :info)
|
52
73
|
display_message("")
|
53
|
-
display_message("
|
54
|
-
display_message("
|
74
|
+
display_message("Start a background job with:", type: :info)
|
75
|
+
display_message(" aidp execute --background", type: :info)
|
76
|
+
display_message(" aidp analyze --background", type: :info)
|
77
|
+
return
|
78
|
+
end
|
79
|
+
|
80
|
+
render_background_jobs(jobs)
|
81
|
+
end
|
82
|
+
|
83
|
+
def show_job_status(job_id, follow: false)
|
84
|
+
if follow
|
85
|
+
follow_job_status(job_id)
|
86
|
+
else
|
87
|
+
status = @background_runner.job_status(job_id)
|
88
|
+
unless status
|
89
|
+
display_message("Job not found: #{job_id}", type: :error)
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
render_job_status(status)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def stop_job(job_id)
|
98
|
+
result = @background_runner.stop_job(job_id)
|
99
|
+
|
100
|
+
if result[:success]
|
101
|
+
display_message("✓ #{result[:message]}", type: :success)
|
102
|
+
else
|
103
|
+
display_message("✗ #{result[:message]}", type: :error)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def show_job_logs(job_id, tail: false, follow: false)
|
108
|
+
if follow
|
109
|
+
display_message("Following logs for job #{job_id} (Ctrl+C to exit)...", type: :info)
|
110
|
+
@background_runner.follow_job_logs(job_id)
|
55
111
|
else
|
56
|
-
|
112
|
+
logs = @background_runner.job_logs(job_id, tail: tail, lines: 50)
|
113
|
+
unless logs
|
114
|
+
display_message("No logs found for job: #{job_id}", type: :error)
|
115
|
+
return
|
116
|
+
end
|
117
|
+
|
118
|
+
display_message("Logs for job #{job_id}:", type: :info)
|
119
|
+
display_message("-" * @screen_width, type: :muted)
|
120
|
+
puts logs
|
57
121
|
end
|
58
122
|
end
|
59
123
|
|
60
124
|
private
|
61
125
|
|
126
|
+
def render_background_jobs(jobs)
|
127
|
+
require "tty-table"
|
128
|
+
|
129
|
+
display_message("Background Jobs", type: :info)
|
130
|
+
display_message("=" * @screen_width, type: :muted)
|
131
|
+
display_message("")
|
132
|
+
|
133
|
+
headers = ["Job ID", "Mode", "Status", "Started", "Duration"]
|
134
|
+
rows = jobs.map do |job|
|
135
|
+
[
|
136
|
+
job[:job_id][0..15] + "...",
|
137
|
+
job[:mode].to_s.capitalize,
|
138
|
+
format_job_status(job[:status]),
|
139
|
+
format_time(job[:started_at]),
|
140
|
+
format_duration_from_start(job[:started_at], job[:completed_at])
|
141
|
+
]
|
142
|
+
end
|
143
|
+
|
144
|
+
table = TTY::Table.new(headers, rows)
|
145
|
+
puts table.render(:basic)
|
146
|
+
|
147
|
+
display_message("")
|
148
|
+
display_message("Commands:", type: :info)
|
149
|
+
display_message(" aidp jobs status <job_id> - Show detailed status", type: :info)
|
150
|
+
display_message(" aidp jobs logs <job_id> --tail - Show recent logs", type: :info)
|
151
|
+
display_message(" aidp jobs stop <job_id> - Stop a running job", type: :info)
|
152
|
+
end
|
153
|
+
|
154
|
+
def render_job_status(status)
|
155
|
+
display_message("Job Status: #{status[:job_id]}", type: :info)
|
156
|
+
display_message("=" * @screen_width, type: :muted)
|
157
|
+
display_message("")
|
158
|
+
display_message("Mode: #{status[:mode]}", type: :info)
|
159
|
+
display_message("Status: #{format_job_status(status[:status])}", type: :info)
|
160
|
+
display_message("PID: #{status[:pid] || "N/A"}", type: :info)
|
161
|
+
display_message("Running: #{status[:running] ? "Yes" : "No"}", type: :info)
|
162
|
+
display_message("Started: #{format_time(status[:started_at])}", type: :info)
|
163
|
+
|
164
|
+
if status[:completed_at]
|
165
|
+
display_message("Completed: #{format_time(status[:completed_at])}", type: :info)
|
166
|
+
display_message("Duration: #{format_duration_from_start(status[:started_at], status[:completed_at])}", type: :info)
|
167
|
+
end
|
168
|
+
|
169
|
+
if status[:checkpoint]
|
170
|
+
display_message("", type: :info)
|
171
|
+
display_message("Latest Checkpoint:", type: :info)
|
172
|
+
cp = status[:checkpoint]
|
173
|
+
display_message(" Step: #{cp[:step_name]}", type: :info)
|
174
|
+
display_message(" Iteration: #{cp[:iteration]}", type: :info)
|
175
|
+
display_message(" Updated: #{format_checkpoint_age(cp[:timestamp])}", type: :info)
|
176
|
+
|
177
|
+
if cp[:metrics]
|
178
|
+
display_message(" Metrics:", type: :info)
|
179
|
+
display_message(" LOC: #{cp[:metrics][:lines_of_code]}", type: :info)
|
180
|
+
display_message(" Coverage: #{cp[:metrics][:test_coverage]}%", type: :info)
|
181
|
+
display_message(" Quality: #{cp[:metrics][:code_quality]}%", type: :info)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
display_message("", type: :info)
|
186
|
+
display_message("Log file: #{status[:log_file]}", type: :muted)
|
187
|
+
end
|
188
|
+
|
189
|
+
def follow_job_status(job_id)
|
190
|
+
display_message("Following job status for #{job_id} (Ctrl+C to exit)...", type: :info)
|
191
|
+
display_message("")
|
192
|
+
|
193
|
+
begin
|
194
|
+
loop do
|
195
|
+
# Clear screen
|
196
|
+
print "\e[2J\e[H"
|
197
|
+
|
198
|
+
status = @background_runner.job_status(job_id)
|
199
|
+
unless status
|
200
|
+
display_message("Job not found: #{job_id}", type: :error)
|
201
|
+
break
|
202
|
+
end
|
203
|
+
|
204
|
+
render_job_status(status)
|
205
|
+
|
206
|
+
# Exit if job is done
|
207
|
+
break unless status[:running]
|
208
|
+
|
209
|
+
# Wait before next update
|
210
|
+
sleep 2
|
211
|
+
end
|
212
|
+
rescue Interrupt
|
213
|
+
display_message("\nStopped following job status", type: :info)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def format_job_status(status)
|
218
|
+
case status.to_s
|
219
|
+
when "running"
|
220
|
+
@pastel.green("● Running")
|
221
|
+
when "completed"
|
222
|
+
@pastel.cyan("✓ Completed")
|
223
|
+
when "failed"
|
224
|
+
@pastel.red("✗ Failed")
|
225
|
+
when "stopped"
|
226
|
+
@pastel.yellow("⏹ Stopped")
|
227
|
+
when "stuck"
|
228
|
+
@pastel.magenta("⚠ Stuck")
|
229
|
+
else
|
230
|
+
@pastel.dim(status.to_s)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def format_time(time)
|
235
|
+
return "N/A" unless time
|
236
|
+
|
237
|
+
begin
|
238
|
+
time = Time.parse(time.to_s) if time.is_a?(String)
|
239
|
+
time.strftime("%Y-%m-%d %H:%M:%S")
|
240
|
+
rescue
|
241
|
+
time.to_s
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def format_duration_from_start(started_at, completed_at)
|
246
|
+
return "N/A" unless started_at
|
247
|
+
|
248
|
+
start_time = started_at.is_a?(String) ? Time.parse(started_at) : started_at
|
249
|
+
end_time = if completed_at
|
250
|
+
completed_at.is_a?(String) ? Time.parse(completed_at) : completed_at
|
251
|
+
else
|
252
|
+
Time.now
|
253
|
+
end
|
254
|
+
|
255
|
+
duration = end_time - start_time
|
256
|
+
format_duration(duration)
|
257
|
+
end
|
258
|
+
|
259
|
+
def format_duration(seconds)
|
260
|
+
return "0s" if seconds.nil? || seconds <= 0
|
261
|
+
|
262
|
+
hours = (seconds / 3600).to_i
|
263
|
+
minutes = ((seconds % 3600) / 60).to_i
|
264
|
+
secs = (seconds % 60).to_i
|
265
|
+
|
266
|
+
parts = []
|
267
|
+
parts << "#{hours}h" if hours > 0
|
268
|
+
parts << "#{minutes}m" if minutes > 0
|
269
|
+
parts << "#{secs}s" if secs > 0 || parts.empty?
|
270
|
+
|
271
|
+
parts.join(" ")
|
272
|
+
end
|
273
|
+
|
274
|
+
def format_checkpoint_age(timestamp)
|
275
|
+
return "N/A" unless timestamp
|
276
|
+
|
277
|
+
time = Time.parse(timestamp.to_s)
|
278
|
+
age = Time.now - time
|
279
|
+
|
280
|
+
if age < 60
|
281
|
+
"#{age.to_i}s ago"
|
282
|
+
elsif age < 3600
|
283
|
+
"#{(age / 60).to_i}m ago"
|
284
|
+
else
|
285
|
+
"#{(age / 3600).to_i}h ago"
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
62
289
|
# Fetch harness jobs from file-based storage
|
63
290
|
def fetch_harness_jobs
|
64
291
|
jobs = []
|
@@ -148,18 +375,6 @@ module Aidp
|
|
148
375
|
display_message("Note: Harness jobs are stored as JSON files in .aidp/harness_logs/", type: :muted)
|
149
376
|
end
|
150
377
|
|
151
|
-
# Format timestamp for display
|
152
|
-
def format_time(timestamp)
|
153
|
-
return "unknown" unless timestamp
|
154
|
-
|
155
|
-
begin
|
156
|
-
time = Time.parse(timestamp)
|
157
|
-
time.strftime("%Y-%m-%d %H:%M:%S")
|
158
|
-
rescue
|
159
|
-
timestamp
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
378
|
# Truncate message for table display
|
164
379
|
def truncate_message(message)
|
165
380
|
return "N/A" unless message
|