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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +194 -25
  3. data/lib/aidp/analyze/kb_inspector.rb +2 -15
  4. data/lib/aidp/analyze/progress.rb +2 -1
  5. data/lib/aidp/analyze/ruby_maat_integration.rb +2 -15
  6. data/lib/aidp/analyze/runner.rb +64 -20
  7. data/lib/aidp/analyze/steps.rb +10 -8
  8. data/lib/aidp/analyze/tree_sitter_grammar_loader.rb +2 -13
  9. data/lib/aidp/analyze/tree_sitter_scan.rb +2 -13
  10. data/lib/aidp/cli/checkpoint_command.rb +98 -0
  11. data/lib/aidp/cli/first_run_wizard.rb +65 -94
  12. data/lib/aidp/cli/jobs_command.rb +249 -34
  13. data/lib/aidp/cli.rb +312 -38
  14. data/lib/aidp/config.rb +5 -8
  15. data/lib/aidp/debug_logger.rb +4 -4
  16. data/lib/aidp/debug_mixin.rb +11 -4
  17. data/lib/aidp/execute/checkpoint.rb +282 -0
  18. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  19. data/lib/aidp/execute/progress.rb +2 -1
  20. data/lib/aidp/execute/prompt_manager.rb +62 -0
  21. data/lib/aidp/execute/runner.rb +53 -24
  22. data/lib/aidp/execute/steps.rb +36 -27
  23. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  24. data/lib/aidp/execute/workflow_selector.rb +26 -17
  25. data/lib/aidp/harness/condition_detector.rb +4 -4
  26. data/lib/aidp/harness/config_schema.rb +40 -0
  27. data/lib/aidp/harness/config_validator.rb +3 -6
  28. data/lib/aidp/harness/configuration.rb +35 -1
  29. data/lib/aidp/harness/enhanced_runner.rb +22 -1
  30. data/lib/aidp/harness/error_handler.rb +103 -28
  31. data/lib/aidp/harness/provider_factory.rb +4 -1
  32. data/lib/aidp/harness/provider_manager.rb +250 -15
  33. data/lib/aidp/harness/runner.rb +3 -14
  34. data/lib/aidp/harness/simple_user_interface.rb +2 -15
  35. data/lib/aidp/harness/status_display.rb +12 -17
  36. data/lib/aidp/harness/test_runner.rb +83 -0
  37. data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
  38. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +22 -4
  39. data/lib/aidp/harness/ui/error_handler.rb +4 -0
  40. data/lib/aidp/harness/ui/frame_manager.rb +10 -8
  41. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  42. data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
  43. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  44. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  45. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  46. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  47. data/lib/aidp/harness/ui/progress_display.rb +8 -12
  48. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  49. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  50. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  51. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  52. data/lib/aidp/harness/ui/status_widget.rb +3 -1
  53. data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
  54. data/lib/aidp/harness/user_interface.rb +12 -17
  55. data/lib/aidp/jobs/background_runner.rb +278 -0
  56. data/lib/aidp/message_display.rb +48 -0
  57. data/lib/aidp/provider_manager.rb +3 -1
  58. data/lib/aidp/providers/anthropic.rb +100 -17
  59. data/lib/aidp/providers/base.rb +42 -11
  60. data/lib/aidp/providers/codex.rb +248 -0
  61. data/lib/aidp/providers/cursor.rb +35 -42
  62. data/lib/aidp/providers/gemini.rb +25 -15
  63. data/lib/aidp/providers/github_copilot.rb +41 -42
  64. data/lib/aidp/providers/opencode.rb +34 -41
  65. data/lib/aidp/version.rb +1 -1
  66. data/lib/aidp/workflows/definitions.rb +357 -0
  67. data/lib/aidp/workflows/selector.rb +171 -0
  68. data/lib/aidp.rb +12 -0
  69. data/templates/planning/generate_llm_style_guide.md +119 -0
  70. metadata +38 -26
  71. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  72. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  73. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  74. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  75. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  76. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  77. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  78. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  79. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  80. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  81. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  82. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  83. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  84. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  85. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  86. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  87. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  88. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  89. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  90. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  91. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  92. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  93. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  94. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  95. /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
- loop do
63
- choice = ask_choice
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("Let's create one so you can start using AI Dev Pipeline.")
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.write(dest, YAML.dump(data))
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
- fallback_input = @prompt.ask("Fallback providers (comma-separated)?", default: provider_name) do |q|
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 the inputs
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
- fallback_input = @prompt.ask("Fallback providers (comma-separated)?", default: current_fallbacks.join(", ")) do |q|
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 the inputs
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
- # Define the available providers based on the system
344
- available = ["cursor", "anthropic", "gemini", "macos", "opencode"]
345
-
346
- # Add descriptions for better UX
347
- available.map do |provider|
348
- case provider
349
- when "cursor"
350
- "cursor - Cursor AI (no API key required)"
351
- when "anthropic"
352
- "anthropic - Anthropic Claude (requires API key)"
353
- when "gemini"
354
- "gemini - Google Gemini (requires API key)"
355
- when "macos"
356
- "macos - macOS UI Automation (no API key required)"
357
- when "opencode"
358
- "opencode - OpenCode (no API key required)"
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
- provider
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 # Track if we've displayed jobs in interactive mode
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
- @screen_width = 80 # Default screen width
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
- def display_message(message, type: :info)
29
- color = case type
30
- when :error then :red
31
- when :success then :green
32
- when :warning then :yellow
33
- when :info then :blue
34
- when :highlight then :cyan
35
- when :muted then :bright_black
36
- else :white
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
- public
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("Harness Jobs", type: :info)
69
+ display_message("Background Jobs", type: :info)
49
70
  display_message("-" * @screen_width, type: :muted)
50
71
  display_message("")
51
- display_message("No harness jobs found", type: :info)
72
+ display_message("No background jobs found", type: :info)
52
73
  display_message("")
53
- display_message("Harness jobs are background tasks that run during harness mode.", type: :info)
54
- display_message("They are stored as JSON files in the .aidp/harness_logs/ directory.", type: :info)
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
- render_harness_jobs(jobs)
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