aidp 0.10.0 → 0.12.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 (98) 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/mcp_dashboard.rb +205 -0
  14. data/lib/aidp/cli.rb +517 -43
  15. data/lib/aidp/config.rb +5 -8
  16. data/lib/aidp/debug_logger.rb +4 -4
  17. data/lib/aidp/debug_mixin.rb +11 -4
  18. data/lib/aidp/execute/checkpoint.rb +282 -0
  19. data/lib/aidp/execute/checkpoint_display.rb +221 -0
  20. data/lib/aidp/execute/progress.rb +2 -1
  21. data/lib/aidp/execute/prompt_manager.rb +62 -0
  22. data/lib/aidp/execute/runner.rb +53 -24
  23. data/lib/aidp/execute/steps.rb +36 -27
  24. data/lib/aidp/execute/work_loop_runner.rb +308 -0
  25. data/lib/aidp/execute/workflow_selector.rb +26 -17
  26. data/lib/aidp/harness/condition_detector.rb +4 -4
  27. data/lib/aidp/harness/config_schema.rb +40 -0
  28. data/lib/aidp/harness/config_validator.rb +3 -6
  29. data/lib/aidp/harness/configuration.rb +35 -1
  30. data/lib/aidp/harness/enhanced_runner.rb +22 -1
  31. data/lib/aidp/harness/error_handler.rb +103 -28
  32. data/lib/aidp/harness/provider_factory.rb +4 -1
  33. data/lib/aidp/harness/provider_info.rb +366 -0
  34. data/lib/aidp/harness/provider_manager.rb +250 -15
  35. data/lib/aidp/harness/runner.rb +3 -14
  36. data/lib/aidp/harness/simple_user_interface.rb +2 -15
  37. data/lib/aidp/harness/status_display.rb +12 -17
  38. data/lib/aidp/harness/test_runner.rb +83 -0
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +2 -0
  40. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +44 -5
  41. data/lib/aidp/harness/ui/error_handler.rb +4 -0
  42. data/lib/aidp/harness/ui/frame_manager.rb +10 -8
  43. data/lib/aidp/harness/ui/job_monitor.rb +2 -0
  44. data/lib/aidp/harness/ui/navigation/main_menu.rb +4 -2
  45. data/lib/aidp/harness/ui/navigation/menu_item.rb +1 -0
  46. data/lib/aidp/harness/ui/navigation/menu_state.rb +1 -0
  47. data/lib/aidp/harness/ui/navigation/submenu.rb +1 -0
  48. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +2 -0
  49. data/lib/aidp/harness/ui/progress_display.rb +8 -12
  50. data/lib/aidp/harness/ui/question_collector.rb +2 -0
  51. data/lib/aidp/harness/ui/spinner_group.rb +2 -0
  52. data/lib/aidp/harness/ui/spinner_helper.rb +1 -1
  53. data/lib/aidp/harness/ui/status_manager.rb +4 -2
  54. data/lib/aidp/harness/ui/status_widget.rb +3 -1
  55. data/lib/aidp/harness/ui/workflow_controller.rb +2 -0
  56. data/lib/aidp/harness/user_interface.rb +12 -17
  57. data/lib/aidp/jobs/background_runner.rb +278 -0
  58. data/lib/aidp/message_display.rb +48 -0
  59. data/lib/aidp/provider_manager.rb +3 -1
  60. data/lib/aidp/providers/anthropic.rb +100 -17
  61. data/lib/aidp/providers/base.rb +42 -11
  62. data/lib/aidp/providers/codex.rb +248 -0
  63. data/lib/aidp/providers/cursor.rb +35 -42
  64. data/lib/aidp/providers/gemini.rb +25 -15
  65. data/lib/aidp/providers/github_copilot.rb +41 -42
  66. data/lib/aidp/providers/opencode.rb +34 -41
  67. data/lib/aidp/version.rb +1 -1
  68. data/lib/aidp/workflows/definitions.rb +357 -0
  69. data/lib/aidp/workflows/guided_agent.rb +400 -0
  70. data/lib/aidp/workflows/selector.rb +171 -0
  71. data/lib/aidp.rb +12 -0
  72. data/templates/planning/generate_llm_style_guide.md +119 -0
  73. metadata +41 -26
  74. /data/templates/{ANALYZE/02_ARCHITECTURE_ANALYSIS.md → analysis/analyze_architecture.md} +0 -0
  75. /data/templates/{ANALYZE/05_DOCUMENTATION_ANALYSIS.md → analysis/analyze_documentation.md} +0 -0
  76. /data/templates/{ANALYZE/04_FUNCTIONALITY_ANALYSIS.md → analysis/analyze_functionality.md} +0 -0
  77. /data/templates/{ANALYZE/01_REPOSITORY_ANALYSIS.md → analysis/analyze_repository.md} +0 -0
  78. /data/templates/{ANALYZE/06_STATIC_ANALYSIS.md → analysis/analyze_static_code.md} +0 -0
  79. /data/templates/{ANALYZE/03_TEST_ANALYSIS.md → analysis/analyze_tests.md} +0 -0
  80. /data/templates/{ANALYZE/07_REFACTORING_RECOMMENDATIONS.md → analysis/recommend_refactoring.md} +0 -0
  81. /data/templates/{ANALYZE/06a_tree_sitter_scan.md → analysis/scan_with_tree_sitter.md} +0 -0
  82. /data/templates/{EXECUTE/11_STATIC_ANALYSIS.md → implementation/configure_static_analysis.md} +0 -0
  83. /data/templates/{EXECUTE/14_DOCS_PORTAL.md → implementation/create_documentation_portal.md} +0 -0
  84. /data/templates/{EXECUTE/10_IMPLEMENTATION_AGENT.md → implementation/implement_features.md} +0 -0
  85. /data/templates/{EXECUTE/13_DELIVERY_ROLLOUT.md → implementation/plan_delivery.md} +0 -0
  86. /data/templates/{EXECUTE/15_POST_RELEASE.md → implementation/review_post_release.md} +0 -0
  87. /data/templates/{EXECUTE/09_SCAFFOLDING_DEVEX.md → implementation/setup_scaffolding.md} +0 -0
  88. /data/templates/{EXECUTE/02A_ARCH_GATE_QUESTIONS.md → planning/ask_architecture_questions.md} +0 -0
  89. /data/templates/{EXECUTE/00_PRD.md → planning/create_prd.md} +0 -0
  90. /data/templates/{EXECUTE/08_TASKS.md → planning/create_tasks.md} +0 -0
  91. /data/templates/{EXECUTE/04_DOMAIN_DECOMPOSITION.md → planning/decompose_domain.md} +0 -0
  92. /data/templates/{EXECUTE/01_NFRS.md → planning/define_nfrs.md} +0 -0
  93. /data/templates/{EXECUTE/05_CONTRACTS.md → planning/design_apis.md} +0 -0
  94. /data/templates/{EXECUTE/02_ARCHITECTURE.md → planning/design_architecture.md} +0 -0
  95. /data/templates/{EXECUTE/06_THREAT_MODEL.md → planning/design_data_model.md} +0 -0
  96. /data/templates/{EXECUTE/03_ADR_FACTORY.md → planning/generate_adrs.md} +0 -0
  97. /data/templates/{EXECUTE/12_OBSERVABILITY_SLOS.md → planning/plan_observability.md} +0 -0
  98. /data/templates/{EXECUTE/07_TEST_PLAN.md → planning/plan_testing.md} +0 -0
data/lib/aidp/config.rb CHANGED
@@ -165,14 +165,10 @@ module Aidp
165
165
  }.freeze
166
166
 
167
167
  def self.load(project_dir = Dir.pwd)
168
- # Try new aidp.yml format first, then fall back to .aidp.yml
169
- config_file = File.join(project_dir, "aidp.yml")
170
- legacy_config_file = File.join(project_dir, ".aidp.yml")
168
+ config_file = File.join(project_dir, ".aidp", "aidp.yml")
171
169
 
172
170
  if File.exist?(config_file)
173
171
  load_yaml_config(config_file)
174
- elsif File.exist?(legacy_config_file)
175
- load_yaml_config(legacy_config_file)
176
172
  else
177
173
  {}
178
174
  end
@@ -248,15 +244,16 @@ module Aidp
248
244
 
249
245
  # Check if configuration file exists
250
246
  def self.config_exists?(project_dir = Dir.pwd)
251
- File.exist?(File.join(project_dir, "aidp.yml")) ||
252
- File.exist?(File.join(project_dir, ".aidp.yml"))
247
+ File.exist?(File.join(project_dir, ".aidp", "aidp.yml"))
253
248
  end
254
249
 
255
250
  # Create example configuration file
256
251
  def self.create_example_config(project_dir = Dir.pwd)
257
- config_path = File.join(project_dir, "aidp.yml")
252
+ config_path = File.join(project_dir, ".aidp", "aidp.yml")
258
253
  return false if File.exist?(config_path)
259
254
 
255
+ FileUtils.mkdir_p(File.dirname(config_path))
256
+
260
257
  example_config = {
261
258
  harness: {
262
259
  max_retries: 2,
@@ -94,7 +94,7 @@ module Aidp
94
94
 
95
95
  # Also output to console if debug is enabled
96
96
  if ENV["DEBUG"] && ENV["DEBUG"].to_i > 0
97
- puts "\e[36m#{banner}\e[0m" # Cyan color
97
+ puts "\e[36m#{banner}\e[0m" # Cyan color
98
98
  end
99
99
  end
100
100
 
@@ -127,11 +127,11 @@ module Aidp
127
127
  when :error
128
128
  warn message
129
129
  when :warn
130
- puts "\e[33m#{message}\e[0m" # Yellow
130
+ puts "\e[33m#{message}\e[0m" # Yellow
131
131
  when :info
132
- puts "\e[36m#{message}\e[0m" # Cyan
132
+ puts "\e[36m#{message}\e[0m" # Cyan
133
133
  when :debug
134
- puts "\e[90m#{message}\e[0m" # Gray
134
+ puts "\e[90m#{message}\e[0m" # Gray
135
135
  else
136
136
  puts message
137
137
  end
@@ -7,8 +7,8 @@ module Aidp
7
7
  module DebugMixin
8
8
  # Debug levels
9
9
  DEBUG_OFF = 0
10
- DEBUG_BASIC = 1 # Commands and stderr
11
- DEBUG_VERBOSE = 2 # Everything including prompts and stdout
10
+ DEBUG_BASIC = 1 # Commands and stderr
11
+ DEBUG_VERBOSE = 2 # Everything including prompts and stdout
12
12
 
13
13
  def self.included(base)
14
14
  base.extend(ClassMethods)
@@ -145,7 +145,7 @@ module Aidp
145
145
  end
146
146
 
147
147
  # Execute command with debug logging
148
- def debug_execute_command(cmd, args: [], input: nil, timeout: nil, **options)
148
+ def debug_execute_command(cmd, args: [], input: nil, timeout: nil, streaming: false, **options)
149
149
  require "tty-command"
150
150
 
151
151
  command_str = [cmd, *args].join(" ")
@@ -154,7 +154,14 @@ module Aidp
154
154
  debug_log("🚀 Starting command execution: #{command_str}", level: :info)
155
155
 
156
156
  begin
157
- cmd_obj = TTY::Command.new(printer: :null) # Disable TTY::Command's own output
157
+ # Configure printer based on streaming mode
158
+ if streaming
159
+ # Use progress printer for real-time output
160
+ cmd_obj = TTY::Command.new(printer: :progress)
161
+ debug_log("📺 Streaming mode enabled - showing real-time output", level: :info)
162
+ else
163
+ cmd_obj = TTY::Command.new(printer: :null) # Disable TTY::Command's own output
164
+ end
158
165
 
159
166
  # Prepare input
160
167
  input_data = nil
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "time"
5
+ require "json"
6
+
7
+ module Aidp
8
+ module Execute
9
+ # Manages periodic checkpoints during work loop execution
10
+ # Tracks progress metrics, code quality, and task completion
11
+ class Checkpoint
12
+ attr_reader :project_dir, :checkpoint_file, :history_file
13
+
14
+ def initialize(project_dir)
15
+ @project_dir = project_dir
16
+ @checkpoint_file = File.join(project_dir, ".aidp", "checkpoint.yml")
17
+ @history_file = File.join(project_dir, ".aidp", "checkpoint_history.jsonl")
18
+ ensure_checkpoint_directory
19
+ end
20
+
21
+ # Record a checkpoint during work loop iteration
22
+ def record_checkpoint(step_name, iteration, metrics = {})
23
+ checkpoint_data = {
24
+ step_name: step_name,
25
+ iteration: iteration,
26
+ timestamp: Time.now.iso8601,
27
+ metrics: collect_metrics.merge(metrics),
28
+ status: determine_status(metrics)
29
+ }
30
+
31
+ save_checkpoint(checkpoint_data)
32
+ append_to_history(checkpoint_data)
33
+
34
+ checkpoint_data
35
+ end
36
+
37
+ # Get the latest checkpoint data
38
+ def latest_checkpoint
39
+ return nil unless File.exist?(@checkpoint_file)
40
+ YAML.load_file(@checkpoint_file)
41
+ end
42
+
43
+ # Get checkpoint history for analysis
44
+ def checkpoint_history(limit: 100)
45
+ return [] unless File.exist?(@history_file)
46
+
47
+ File.readlines(@history_file).last(limit).map do |line|
48
+ JSON.parse(line, symbolize_names: true)
49
+ end
50
+ rescue JSON::ParserError
51
+ []
52
+ end
53
+
54
+ # Get progress summary comparing current state to previous checkpoints
55
+ def progress_summary
56
+ latest = latest_checkpoint
57
+ return nil unless latest
58
+
59
+ history = checkpoint_history(limit: 10)
60
+ previous = history[-2] if history.size > 1
61
+
62
+ {
63
+ current: latest,
64
+ previous: previous,
65
+ trends: calculate_trends(history),
66
+ quality_score: calculate_quality_score(latest[:metrics])
67
+ }
68
+ end
69
+
70
+ # Clear all checkpoint data
71
+ def clear
72
+ File.delete(@checkpoint_file) if File.exist?(@checkpoint_file)
73
+ File.delete(@history_file) if File.exist?(@history_file)
74
+ end
75
+
76
+ private
77
+
78
+ def ensure_checkpoint_directory
79
+ dir = File.dirname(@checkpoint_file)
80
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
81
+ end
82
+
83
+ # Collect current metrics from the project
84
+ def collect_metrics
85
+ {
86
+ lines_of_code: count_lines_of_code,
87
+ file_count: count_project_files,
88
+ test_coverage: estimate_test_coverage,
89
+ code_quality: assess_code_quality,
90
+ prd_task_progress: calculate_prd_task_progress
91
+ }
92
+ end
93
+
94
+ def count_lines_of_code
95
+ extensions = %w[.rb .js .py .java .go .ts .tsx .jsx]
96
+ total_lines = 0
97
+
98
+ Dir.glob(File.join(@project_dir, "**", "*")).each do |file|
99
+ next unless File.file?(file)
100
+ next unless extensions.include?(File.extname(file))
101
+ next if file.include?("node_modules") || file.include?("vendor")
102
+
103
+ total_lines += File.readlines(file).size
104
+ end
105
+
106
+ total_lines
107
+ rescue
108
+ 0
109
+ end
110
+
111
+ def count_project_files
112
+ extensions = %w[.rb .js .py .java .go .ts .tsx .jsx]
113
+ count = 0
114
+
115
+ Dir.glob(File.join(@project_dir, "**", "*")).each do |file|
116
+ next unless File.file?(file)
117
+ next unless extensions.include?(File.extname(file))
118
+ next if file.include?("node_modules") || file.include?("vendor")
119
+
120
+ count += 1
121
+ end
122
+
123
+ count
124
+ rescue
125
+ 0
126
+ end
127
+
128
+ # Estimate test coverage based on test files vs source files
129
+ def estimate_test_coverage
130
+ source_files = count_files_by_pattern("**/*.rb", exclude: ["spec/**", "test/**"])
131
+ test_files = count_files_by_pattern("{spec,test}/**/*_{spec,test}.rb")
132
+
133
+ return 0 if source_files == 0
134
+
135
+ coverage_ratio = (test_files.to_f / source_files * 100).round(2)
136
+ [coverage_ratio, 100].min
137
+ rescue
138
+ 0
139
+ end
140
+
141
+ def count_files_by_pattern(pattern, exclude: [])
142
+ files = Dir.glob(File.join(@project_dir, pattern))
143
+ exclude.each do |exc_pattern|
144
+ excluded = Dir.glob(File.join(@project_dir, exc_pattern))
145
+ files -= excluded
146
+ end
147
+ files.size
148
+ end
149
+
150
+ # Assess code quality based on available linters
151
+ def assess_code_quality
152
+ quality_score = 100
153
+
154
+ # Check for Ruby linter output
155
+ if File.exist?(File.join(@project_dir, ".rubocop.yml"))
156
+ rubocop_score = run_rubocop_check
157
+ quality_score = [quality_score, rubocop_score].min if rubocop_score
158
+ end
159
+
160
+ quality_score
161
+ end
162
+
163
+ def run_rubocop_check
164
+ # Run rubocop and parse output to get a quality score
165
+ # This is a simplified version - could be enhanced
166
+ result = `cd #{@project_dir} && rubocop --format json 2>/dev/null`
167
+ return nil if result.empty?
168
+
169
+ data = JSON.parse(result)
170
+ total_files = data["files"]&.size || 0
171
+ return 100 if total_files == 0
172
+
173
+ offense_count = data["summary"]["offense_count"] || 0
174
+ # Simple scoring: fewer offenses = higher score
175
+ [100 - (offense_count / total_files.to_f * 10), 0].max.round(2)
176
+ rescue
177
+ nil
178
+ end
179
+
180
+ # Calculate PRD task completion progress
181
+ def calculate_prd_task_progress
182
+ prd_path = File.join(@project_dir, "docs", "prd.md")
183
+ return 0 unless File.exist?(prd_path)
184
+
185
+ content = File.read(prd_path)
186
+
187
+ # Count completed vs total checkboxes
188
+ total_tasks = content.scan(/- \[[ x]\]/).size
189
+ completed_tasks = content.scan(/- \[x\]/i).size
190
+
191
+ return 0 if total_tasks == 0
192
+
193
+ (completed_tasks.to_f / total_tasks * 100).round(2)
194
+ rescue
195
+ 0
196
+ end
197
+
198
+ def determine_status(metrics)
199
+ # Determine overall status based on metrics
200
+ quality_score = calculate_quality_score(metrics)
201
+
202
+ if quality_score >= 80
203
+ "healthy"
204
+ elsif quality_score >= 60
205
+ "warning"
206
+ else
207
+ "needs_attention"
208
+ end
209
+ end
210
+
211
+ def calculate_quality_score(metrics)
212
+ # Weighted average of different metrics
213
+ weights = {
214
+ test_coverage: 0.3,
215
+ code_quality: 0.4,
216
+ prd_task_progress: 0.3
217
+ }
218
+
219
+ score = 0
220
+ weights.each do |metric, weight|
221
+ score += (metrics[metric] || 0) * weight
222
+ end
223
+
224
+ score.round(2)
225
+ end
226
+
227
+ # Calculate trends from historical data
228
+ def calculate_trends(history)
229
+ return {} if history.size < 2
230
+
231
+ latest = history.last
232
+ previous = history[-2]
233
+
234
+ {
235
+ lines_of_code: calculate_trend_direction(
236
+ previous.dig(:metrics, :lines_of_code),
237
+ latest.dig(:metrics, :lines_of_code)
238
+ ),
239
+ test_coverage: calculate_trend_direction(
240
+ previous.dig(:metrics, :test_coverage),
241
+ latest.dig(:metrics, :test_coverage)
242
+ ),
243
+ code_quality: calculate_trend_direction(
244
+ previous.dig(:metrics, :code_quality),
245
+ latest.dig(:metrics, :code_quality)
246
+ ),
247
+ prd_task_progress: calculate_trend_direction(
248
+ previous.dig(:metrics, :prd_task_progress),
249
+ latest.dig(:metrics, :prd_task_progress)
250
+ )
251
+ }
252
+ end
253
+
254
+ def calculate_trend_direction(previous_value, current_value)
255
+ return "stable" if previous_value.nil? || current_value.nil?
256
+
257
+ diff = current_value - previous_value
258
+ diff_percent = (previous_value == 0) ? 0 : ((diff.to_f / previous_value) * 100).round(2)
259
+
260
+ {
261
+ direction: if diff > 0
262
+ "up"
263
+ else
264
+ ((diff < 0) ? "down" : "stable")
265
+ end,
266
+ change: diff,
267
+ change_percent: diff_percent
268
+ }
269
+ end
270
+
271
+ def save_checkpoint(data)
272
+ File.write(@checkpoint_file, data.to_yaml)
273
+ end
274
+
275
+ def append_to_history(data)
276
+ File.open(@history_file, "a") do |f|
277
+ f.puts(data.to_json)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Aidp
6
+ module Execute
7
+ # Formats and displays checkpoint information to the user
8
+ class CheckpointDisplay
9
+ include Aidp::MessageDisplay
10
+
11
+ def initialize
12
+ @pastel = Pastel.new
13
+ end
14
+
15
+ # Display a checkpoint during work loop iteration
16
+ def display_checkpoint(checkpoint_data, show_details: false)
17
+ return unless checkpoint_data
18
+
19
+ puts
20
+ puts @pastel.bold("📊 Checkpoint - Iteration #{checkpoint_data[:iteration]}")
21
+ puts @pastel.dim("─" * 60)
22
+
23
+ display_metrics(checkpoint_data[:metrics])
24
+ display_status(checkpoint_data[:status])
25
+
26
+ if show_details
27
+ display_trends(checkpoint_data[:trends]) if checkpoint_data[:trends]
28
+ end
29
+
30
+ puts @pastel.dim("─" * 60)
31
+ puts
32
+ end
33
+
34
+ # Display progress summary with trends
35
+ def display_progress_summary(summary)
36
+ return unless summary
37
+
38
+ puts
39
+ puts @pastel.bold("📈 Progress Summary")
40
+ puts @pastel.dim("=" * 60)
41
+
42
+ current = summary[:current]
43
+ puts "Step: #{@pastel.cyan(current[:step_name])}"
44
+ puts "Iteration: #{current[:iteration]}"
45
+ puts "Status: #{format_status(current[:status])}"
46
+ puts
47
+
48
+ puts @pastel.bold("Current Metrics:")
49
+ display_metrics(current[:metrics])
50
+
51
+ if summary[:trends]
52
+ puts
53
+ puts @pastel.bold("Trends:")
54
+ display_trends(summary[:trends])
55
+ end
56
+
57
+ if summary[:quality_score]
58
+ puts
59
+ display_quality_score(summary[:quality_score])
60
+ end
61
+
62
+ puts @pastel.dim("=" * 60)
63
+ puts
64
+ end
65
+
66
+ # Display checkpoint history as a table
67
+ def display_checkpoint_history(history, limit: 10)
68
+ return if history.empty?
69
+
70
+ puts
71
+ puts @pastel.bold("📜 Checkpoint History (Last #{[limit, history.size].min})")
72
+ puts @pastel.dim("=" * 80)
73
+
74
+ # Table header
75
+ puts format_table_row([
76
+ "Iteration",
77
+ "Time",
78
+ "LOC",
79
+ "Coverage",
80
+ "Quality",
81
+ "PRD Progress",
82
+ "Status"
83
+ ], header: true)
84
+ puts @pastel.dim("-" * 80)
85
+
86
+ # Table rows
87
+ history.last(limit).each do |checkpoint|
88
+ metrics = checkpoint[:metrics]
89
+ timestamp = Time.parse(checkpoint[:timestamp]).strftime("%H:%M:%S")
90
+
91
+ puts format_table_row([
92
+ checkpoint[:iteration].to_s,
93
+ timestamp,
94
+ metrics[:lines_of_code].to_s,
95
+ "#{metrics[:test_coverage]}%",
96
+ "#{metrics[:code_quality]}%",
97
+ "#{metrics[:prd_task_progress]}%",
98
+ format_status(checkpoint[:status])
99
+ ])
100
+ end
101
+
102
+ puts @pastel.dim("=" * 80)
103
+ puts
104
+ end
105
+
106
+ # Display inline progress indicator (for work loop)
107
+ def display_inline_progress(iteration, metrics)
108
+ loc = metrics[:lines_of_code] || 0
109
+ coverage = metrics[:test_coverage] || 0
110
+ quality = metrics[:code_quality] || 0
111
+ prd = metrics[:prd_task_progress] || 0
112
+
113
+ status_line = [
114
+ "Iter: #{iteration}",
115
+ "LOC: #{loc}",
116
+ "Cov: #{format_percentage(coverage)}",
117
+ "Qual: #{format_percentage(quality)}",
118
+ "PRD: #{format_percentage(prd)}"
119
+ ].join(" | ")
120
+
121
+ display_message(" #{@pastel.dim(status_line)}", type: :info)
122
+ end
123
+
124
+ private
125
+
126
+ def display_metrics(metrics)
127
+ puts " Lines of Code: #{@pastel.yellow(metrics[:lines_of_code].to_s)}"
128
+ puts " Test Coverage: #{format_percentage_with_color(metrics[:test_coverage])}"
129
+ puts " Code Quality: #{format_percentage_with_color(metrics[:code_quality])}"
130
+ puts " PRD Task Progress: #{format_percentage_with_color(metrics[:prd_task_progress])}"
131
+ puts " File Count: #{metrics[:file_count]}"
132
+ end
133
+
134
+ def display_status(status)
135
+ puts " Overall Status: #{format_status(status)}"
136
+ end
137
+
138
+ def format_status(status)
139
+ case status.to_s
140
+ when "healthy"
141
+ @pastel.green("✓ Healthy")
142
+ when "warning"
143
+ @pastel.yellow("⚠ Warning")
144
+ when "needs_attention"
145
+ @pastel.red("✗ Needs Attention")
146
+ else
147
+ @pastel.dim(status.to_s)
148
+ end
149
+ end
150
+
151
+ def display_trends(trends)
152
+ trends.each do |metric, trend_data|
153
+ next unless trend_data.is_a?(Hash)
154
+
155
+ metric_name = metric.to_s.split("_").map(&:capitalize).join(" ")
156
+ arrow = trend_arrow(trend_data[:direction])
157
+ change = format_change(trend_data[:change], trend_data[:change_percent])
158
+
159
+ puts " #{metric_name}: #{arrow} #{change}"
160
+ end
161
+ end
162
+
163
+ def trend_arrow(direction)
164
+ case direction.to_s
165
+ when "up"
166
+ @pastel.green("↑")
167
+ when "down"
168
+ @pastel.red("↓")
169
+ else
170
+ @pastel.dim("→")
171
+ end
172
+ end
173
+
174
+ def format_change(change, change_percent)
175
+ sign = (change >= 0) ? "+" : ""
176
+ "#{sign}#{change} (#{sign}#{change_percent}%)"
177
+ end
178
+
179
+ def display_quality_score(score)
180
+ color = if score >= 80
181
+ :green
182
+ elsif score >= 60
183
+ :yellow
184
+ else
185
+ :red
186
+ end
187
+
188
+ puts " Quality Score: #{@pastel.send(color, "#{score.round(2)}%")}"
189
+ end
190
+
191
+ def format_percentage(value)
192
+ return "0%" unless value
193
+ "#{value.round(1)}%"
194
+ end
195
+
196
+ def format_percentage_with_color(value)
197
+ return @pastel.dim("0%") unless value
198
+
199
+ color = if value >= 80
200
+ :green
201
+ elsif value >= 60
202
+ :yellow
203
+ else
204
+ :red
205
+ end
206
+
207
+ @pastel.send(color, "#{value.round(1)}%")
208
+ end
209
+
210
+ def format_table_row(columns, header: false)
211
+ widths = [10, 10, 8, 10, 10, 14, 18]
212
+ formatted = columns.each_with_index.map do |col, i|
213
+ col.to_s.ljust(widths[i])
214
+ end
215
+
216
+ row = formatted.join(" ")
217
+ header ? @pastel.bold(row) : row
218
+ end
219
+ end
220
+ end
221
+ end
@@ -11,7 +11,7 @@ module Aidp
11
11
 
12
12
  def initialize(project_dir)
13
13
  @project_dir = project_dir
14
- @progress_file = File.join(project_dir, ".aidp-progress.yml")
14
+ @progress_file = File.join(project_dir, ".aidp", "progress", "execute.yml")
15
15
  load_progress
16
16
  end
17
17
 
@@ -78,6 +78,7 @@ module Aidp
78
78
  # In test mode, skip file operations to avoid hanging
79
79
  return if ENV["RACK_ENV"] == "test" || defined?(RSpec)
80
80
 
81
+ FileUtils.mkdir_p(File.dirname(@progress_file))
81
82
  File.write(@progress_file, @progress.to_yaml)
82
83
  end
83
84
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Aidp
6
+ module Execute
7
+ # Manages the PROMPT.md file lifecycle for work loops
8
+ # Responsibilities:
9
+ # - Read/write PROMPT.md
10
+ # - Check existence
11
+ # - Archive completed prompts
12
+ class PromptManager
13
+ PROMPT_FILENAME = "PROMPT.md"
14
+ ARCHIVE_DIR = ".aidp/prompt_archive"
15
+
16
+ def initialize(project_dir)
17
+ @project_dir = project_dir
18
+ @prompt_path = File.join(project_dir, PROMPT_FILENAME)
19
+ @archive_dir = File.join(project_dir, ARCHIVE_DIR)
20
+ end
21
+
22
+ # Write content to PROMPT.md
23
+ def write(content)
24
+ File.write(@prompt_path, content)
25
+ end
26
+
27
+ # Read content from PROMPT.md
28
+ def read
29
+ return nil unless exists?
30
+ File.read(@prompt_path)
31
+ end
32
+
33
+ # Check if PROMPT.md exists
34
+ def exists?
35
+ File.exist?(@prompt_path)
36
+ end
37
+
38
+ # Archive PROMPT.md with timestamp and step name
39
+ def archive(step_name)
40
+ return unless exists?
41
+
42
+ FileUtils.mkdir_p(@archive_dir)
43
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
44
+ archive_filename = "#{timestamp}_#{step_name}_PROMPT.md"
45
+ archive_path = File.join(@archive_dir, archive_filename)
46
+
47
+ FileUtils.cp(@prompt_path, archive_path)
48
+ archive_path
49
+ end
50
+
51
+ # Delete PROMPT.md (typically after archiving)
52
+ def delete
53
+ File.delete(@prompt_path) if exists?
54
+ end
55
+
56
+ # Get the full path to PROMPT.md
57
+ def path
58
+ @prompt_path
59
+ end
60
+ end
61
+ end
62
+ end