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.
- 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/mcp_dashboard.rb +205 -0
- data/lib/aidp/cli.rb +517 -43
- 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_info.rb +366 -0
- 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 +44 -5
- 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/guided_agent.rb +400 -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 +41 -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
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
|
-
|
|
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,
|
data/lib/aidp/debug_logger.rb
CHANGED
|
@@ -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"
|
|
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"
|
|
130
|
+
puts "\e[33m#{message}\e[0m" # Yellow
|
|
131
131
|
when :info
|
|
132
|
-
puts "\e[36m#{message}\e[0m"
|
|
132
|
+
puts "\e[36m#{message}\e[0m" # Cyan
|
|
133
133
|
when :debug
|
|
134
|
-
puts "\e[90m#{message}\e[0m"
|
|
134
|
+
puts "\e[90m#{message}\e[0m" # Gray
|
|
135
135
|
else
|
|
136
136
|
puts message
|
|
137
137
|
end
|
data/lib/aidp/debug_mixin.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Aidp
|
|
|
7
7
|
module DebugMixin
|
|
8
8
|
# Debug levels
|
|
9
9
|
DEBUG_OFF = 0
|
|
10
|
-
DEBUG_BASIC = 1
|
|
11
|
-
DEBUG_VERBOSE = 2
|
|
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
|
-
|
|
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
|
|
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
|