aidp 0.34.0 → 0.39.3

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 (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require_relative "base_activity"
5
+
6
+ module Aidp
7
+ module Temporal
8
+ module Activities
9
+ # Activity that creates a pull request from implemented changes
10
+ # Handles git operations and GitHub PR creation
11
+ class CreatePrActivity < BaseActivity
12
+ def execute(input)
13
+ with_activity_context do
14
+ project_dir = input[:project_dir]
15
+ issue_number = input[:issue_number]
16
+ implementation = input[:implementation]
17
+ iterations = input[:iterations]
18
+
19
+ # Validate issue_number is numeric to prevent injection
20
+ unless issue_number.to_s.match?(/\A\d+\z/)
21
+ return error_result("Invalid issue number: must be numeric")
22
+ end
23
+
24
+ log_activity("creating_pr",
25
+ project_dir: project_dir,
26
+ issue_number: issue_number)
27
+
28
+ # Ensure we have commits to push
29
+ unless has_uncommitted_changes?(project_dir) || has_unpushed_commits?(project_dir)
30
+ return error_result("No changes to create PR from")
31
+ end
32
+
33
+ # Create branch if needed
34
+ branch_name = ensure_branch(project_dir, issue_number)
35
+
36
+ heartbeat(phase: "branch_created", branch: branch_name)
37
+
38
+ # Commit any uncommitted changes
39
+ if has_uncommitted_changes?(project_dir)
40
+ commit_changes(project_dir, issue_number, iterations)
41
+ end
42
+
43
+ heartbeat(phase: "changes_committed")
44
+
45
+ # Push branch
46
+ push_branch(project_dir, branch_name)
47
+
48
+ heartbeat(phase: "branch_pushed")
49
+
50
+ # Create PR
51
+ pr_result = create_pull_request(
52
+ project_dir: project_dir,
53
+ branch_name: branch_name,
54
+ issue_number: issue_number,
55
+ implementation: implementation,
56
+ iterations: iterations
57
+ )
58
+
59
+ if pr_result[:success]
60
+ success_result(
61
+ pr_url: pr_result[:pr_url],
62
+ pr_number: pr_result[:pr_number],
63
+ branch: branch_name
64
+ )
65
+ else
66
+ error_result(pr_result[:error] || "Failed to create PR")
67
+ end
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def has_uncommitted_changes?(project_dir)
74
+ stdout, _stderr, status = Open3.capture3("git", "status", "--porcelain", chdir: project_dir)
75
+ status.success? && !stdout.strip.empty?
76
+ end
77
+
78
+ def has_unpushed_commits?(project_dir)
79
+ stdout, _stderr, status = Open3.capture3(
80
+ "git", "rev-list", "--count", "@{upstream}..HEAD",
81
+ chdir: project_dir
82
+ )
83
+ status.success? && stdout.strip.to_i > 0
84
+ rescue
85
+ false
86
+ end
87
+
88
+ def ensure_branch(project_dir, issue_number)
89
+ # issue_number is validated as numeric in execute()
90
+ branch_name = "aidp/issue-#{issue_number}"
91
+
92
+ stdout, _stderr, _status = Open3.capture3("git", "branch", "--show-current", chdir: project_dir)
93
+ current_branch = stdout.strip
94
+
95
+ if current_branch != branch_name
96
+ # Check if branch exists using array-style system call
97
+ ref_path = "refs/heads/#{branch_name}"
98
+ _stdout, _stderr, status = Open3.capture3(
99
+ "git", "show-ref", "--verify", "--quiet", ref_path,
100
+ chdir: project_dir
101
+ )
102
+
103
+ if status.success?
104
+ Open3.capture3("git", "checkout", branch_name, chdir: project_dir)
105
+ else
106
+ Open3.capture3("git", "checkout", "-b", branch_name, chdir: project_dir)
107
+ end
108
+ end
109
+
110
+ branch_name
111
+ end
112
+
113
+ def commit_changes(project_dir, issue_number, iterations)
114
+ # Stage all changes
115
+ Open3.capture3("git", "add", "-A", chdir: project_dir)
116
+
117
+ # Build commit message (issue_number validated as numeric)
118
+ commit_message = build_commit_message(issue_number, iterations)
119
+
120
+ # Use array-style to avoid shell injection
121
+ Open3.capture3("git", "commit", "-m", commit_message, chdir: project_dir)
122
+ end
123
+
124
+ def build_commit_message(issue_number, iterations)
125
+ # issue_number is validated as numeric, iterations is an integer
126
+ safe_iterations = iterations.to_i
127
+
128
+ "fix: implement changes for issue ##{issue_number}\n\n" \
129
+ "Implemented via AIDP Temporal workflow\n" \
130
+ "Iterations: #{safe_iterations}\n\n" \
131
+ "Closes ##{issue_number}"
132
+ end
133
+
134
+ def push_branch(project_dir, branch_name)
135
+ # branch_name is constructed from validated issue_number
136
+ Open3.capture3("git", "push", "-u", "origin", branch_name, chdir: project_dir)
137
+ end
138
+
139
+ def create_pull_request(project_dir:, branch_name:, issue_number:, implementation:, iterations:)
140
+ # issue_number is validated as numeric
141
+ title = "Fix ##{issue_number}"
142
+ body = build_pr_body(issue_number, implementation, iterations)
143
+
144
+ # Use array-style Open3 to avoid shell injection
145
+ stdout, stderr, status = Open3.capture3(
146
+ "gh", "pr", "create", "--title", title, "--body", body,
147
+ chdir: project_dir
148
+ )
149
+
150
+ if status.success?
151
+ # Extract PR URL from output
152
+ pr_url = stdout.strip
153
+ pr_number = pr_url.split("/").last.to_i
154
+
155
+ {
156
+ success: true,
157
+ pr_url: pr_url,
158
+ pr_number: pr_number
159
+ }
160
+ else
161
+ {
162
+ success: false,
163
+ error: stderr.empty? ? stdout : stderr
164
+ }
165
+ end
166
+ end
167
+
168
+ def build_pr_body(issue_number, implementation, iterations)
169
+ # issue_number validated as numeric, iterations is integer
170
+ safe_iterations = iterations.to_i
171
+
172
+ <<~BODY
173
+ ## Summary
174
+
175
+ Implements changes requested in ##{issue_number}
176
+
177
+ ## Implementation Details
178
+
179
+ This PR was created via AIDP Temporal workflow after #{safe_iterations} iterations of the fix-forward work loop.
180
+
181
+ ## Testing
182
+
183
+ - [ ] All tests pass
184
+ - [ ] Lint checks pass
185
+ - [ ] Manual testing completed
186
+
187
+ Closes ##{issue_number}
188
+ BODY
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_activity"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ module Activities
8
+ # Activity that creates the initial PROMPT.md for a work loop
9
+ # Assembles context, instructions, and constraints
10
+ class CreatePromptActivity < BaseActivity
11
+ def execute(input)
12
+ with_activity_context do
13
+ project_dir = input[:project_dir]
14
+ step_name = input[:step_name]
15
+ step_spec = input[:step_spec] || {}
16
+ context = input[:context] || {}
17
+
18
+ log_activity("creating_prompt",
19
+ project_dir: project_dir,
20
+ step_name: step_name)
21
+
22
+ # Load configuration
23
+ config = load_config(project_dir)
24
+
25
+ # Create prompt manager
26
+ prompt_manager = Aidp::Execute::PromptManager.new(project_dir, config: config)
27
+
28
+ # Build initial prompt content
29
+ prompt_content = build_prompt_content(
30
+ project_dir: project_dir,
31
+ step_name: step_name,
32
+ step_spec: step_spec,
33
+ context: context
34
+ )
35
+
36
+ # Write the prompt
37
+ prompt_manager.write(prompt_content)
38
+
39
+ heartbeat(phase: "prompt_created", step_name: step_name)
40
+
41
+ success_result(
42
+ prompt_path: prompt_manager.prompt_path,
43
+ prompt_length: prompt_content.length,
44
+ step_name: step_name
45
+ )
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def build_prompt_content(project_dir:, step_name:, step_spec:, context:)
52
+ sections = []
53
+
54
+ # Header
55
+ sections << "# Implementation Task: #{step_name}"
56
+ sections << ""
57
+ sections << "Generated at: #{Time.now.iso8601}"
58
+ sections << ""
59
+
60
+ # Step specification
61
+ if step_spec[:description]
62
+ sections << "## Objective"
63
+ sections << ""
64
+ sections << step_spec[:description]
65
+ sections << ""
66
+ end
67
+
68
+ # Context from previous steps
69
+ if context[:previous_output]
70
+ sections << "## Context from Previous Steps"
71
+ sections << ""
72
+ sections << context[:previous_output]
73
+ sections << ""
74
+ end
75
+
76
+ # Requirements
77
+ if context[:requirements]&.any?
78
+ sections << "## Requirements"
79
+ sections << ""
80
+ context[:requirements].each do |req|
81
+ sections << "- #{req}"
82
+ end
83
+ sections << ""
84
+ end
85
+
86
+ # Constraints
87
+ sections << "## Constraints"
88
+ sections << ""
89
+ sections << "- Follow the existing code style and patterns"
90
+ sections << "- Make minimal changes to achieve the objective"
91
+ sections << "- Ensure all tests pass after changes"
92
+ sections << "- Do not introduce new dependencies without justification"
93
+ sections << ""
94
+
95
+ # Style guide reference
96
+ style_guide_path = File.join(project_dir, "docs", "LLM_STYLE_GUIDE.md")
97
+ if File.exist?(style_guide_path)
98
+ sections << "## Style Guide"
99
+ sections << ""
100
+ sections << "Please follow the guidelines in `docs/LLM_STYLE_GUIDE.md`"
101
+ sections << ""
102
+ end
103
+
104
+ # Instructions
105
+ sections << "## Instructions"
106
+ sections << ""
107
+ sections << "1. Review the existing codebase to understand context"
108
+ sections << "2. Implement the changes described in the objective"
109
+ sections << "3. Ensure all tests pass"
110
+ sections << "4. Signal completion when done"
111
+ sections << ""
112
+
113
+ sections.join("\n")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_activity"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ module Activities
8
+ # Activity that analyzes test/lint failures and prepares feedback
9
+ # Extracts actionable information from failure output
10
+ class DiagnoseFailureActivity < BaseActivity
11
+ def execute(input)
12
+ with_activity_context do
13
+ project_dir = input[:project_dir]
14
+ iteration = input[:iteration]
15
+ test_result = input[:test_result]
16
+
17
+ log_activity("diagnosing_failure",
18
+ project_dir: project_dir,
19
+ iteration: iteration)
20
+
21
+ # Analyze failure results
22
+ diagnosis = analyze_failures(test_result)
23
+
24
+ # Update PROMPT.md with failure information
25
+ update_prompt_with_diagnosis(project_dir, diagnosis)
26
+
27
+ success_result(
28
+ diagnosis: diagnosis,
29
+ iteration: iteration,
30
+ failure_count: diagnosis[:failure_count]
31
+ )
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def analyze_failures(test_result)
38
+ results = test_result[:results] || {}
39
+ failures = []
40
+ failure_count = 0
41
+
42
+ results.each do |phase, result|
43
+ next if result[:success]
44
+
45
+ phase_failures = extract_failures(phase, result)
46
+ failures.concat(phase_failures)
47
+ failure_count += phase_failures.length
48
+ end
49
+
50
+ {
51
+ failure_count: failure_count,
52
+ failures: failures,
53
+ summary: build_failure_summary(failures),
54
+ recommendations: generate_recommendations(failures)
55
+ }
56
+ end
57
+
58
+ def extract_failures(phase, result)
59
+ output = result[:output] || ""
60
+ failures = []
61
+
62
+ case phase.to_sym
63
+ when :test
64
+ # Extract test failure details - process line by line to avoid ReDoS
65
+ extract_test_failures(output, failures, phase)
66
+ when :lint
67
+ # Extract lint errors - line by line processing
68
+ extract_lint_failures(output, failures, phase)
69
+ when :typecheck
70
+ # Extract type errors - line by line processing
71
+ extract_typecheck_failures(output, failures, phase)
72
+ else
73
+ # Generic failure extraction
74
+ failures << {
75
+ type: :unknown,
76
+ phase: phase,
77
+ message: output.slice(0, 500)
78
+ }
79
+ end
80
+
81
+ failures.first(20) # Limit to prevent massive payloads
82
+ end
83
+
84
+ def extract_test_failures(output, failures, phase)
85
+ # Process output in chunks separated by blank lines
86
+ current_failure = nil
87
+
88
+ output.each_line do |line|
89
+ if line.match?(/\A\s*(FAIL|ERROR|Failure)/i)
90
+ # Save previous failure if exists
91
+ if current_failure
92
+ failures << {
93
+ type: :test,
94
+ phase: phase,
95
+ message: current_failure.strip.slice(0, 500)
96
+ }
97
+ end
98
+ current_failure = line
99
+ elsif current_failure
100
+ if line.strip.empty?
101
+ # End of failure block
102
+ failures << {
103
+ type: :test,
104
+ phase: phase,
105
+ message: current_failure.strip.slice(0, 500)
106
+ }
107
+ current_failure = nil
108
+ else
109
+ # Continue accumulating failure message
110
+ current_failure += line
111
+ end
112
+ end
113
+ end
114
+
115
+ # Don't forget the last failure
116
+ if current_failure
117
+ failures << {
118
+ type: :test,
119
+ phase: phase,
120
+ message: current_failure.strip.slice(0, 500)
121
+ }
122
+ end
123
+ end
124
+
125
+ def extract_lint_failures(output, failures, phase)
126
+ # Match lint output format: file:line:col: message
127
+ output.each_line do |line|
128
+ if line.match?(/\A[^:]+:\d+:\d+:/)
129
+ failures << {
130
+ type: :lint,
131
+ phase: phase,
132
+ message: line.strip
133
+ }
134
+ end
135
+ end
136
+ end
137
+
138
+ def extract_typecheck_failures(output, failures, phase)
139
+ # Match lines containing "error" (case insensitive)
140
+ output.each_line do |line|
141
+ if line.downcase.include?("error")
142
+ failures << {
143
+ type: :typecheck,
144
+ phase: phase,
145
+ message: line.strip
146
+ }
147
+ end
148
+ end
149
+ end
150
+
151
+ def build_failure_summary(failures)
152
+ return "No failures found" if failures.empty?
153
+
154
+ by_type = failures.group_by { |f| f[:type] }
155
+ parts = by_type.map { |type, items| "#{items.length} #{type} failure(s)" }
156
+ parts.join(", ")
157
+ end
158
+
159
+ def generate_recommendations(failures)
160
+ recommendations = []
161
+
162
+ if failures.any? { |f| f[:type] == :test }
163
+ recommendations << "Review failing test assertions and update implementation"
164
+ end
165
+
166
+ if failures.any? { |f| f[:type] == :lint }
167
+ recommendations << "Fix code style issues to match project standards"
168
+ end
169
+
170
+ if failures.any? { |f| f[:type] == :typecheck }
171
+ recommendations << "Fix type errors to ensure type safety"
172
+ end
173
+
174
+ recommendations
175
+ end
176
+
177
+ def update_prompt_with_diagnosis(project_dir, diagnosis)
178
+ prompt_manager = Aidp::Execute::PromptManager.new(project_dir)
179
+ current_prompt = prompt_manager.read
180
+
181
+ return unless current_prompt
182
+
183
+ # Build failure section
184
+ failure_section = build_failure_section(diagnosis)
185
+
186
+ # Append to prompt
187
+ updated_prompt = <<~PROMPT
188
+ #{current_prompt}
189
+
190
+ ---
191
+ ## Test/Lint Failures (Fix These)
192
+
193
+ #{failure_section}
194
+
195
+ Please address the above failures in this iteration.
196
+ PROMPT
197
+
198
+ prompt_manager.write(updated_prompt)
199
+ end
200
+
201
+ def build_failure_section(diagnosis)
202
+ sections = []
203
+
204
+ sections << "**Summary:** #{diagnosis[:summary]}"
205
+ sections << ""
206
+
207
+ diagnosis[:failures].first(10).each_with_index do |failure, idx|
208
+ sections << "### Failure #{idx + 1} (#{failure[:type]})"
209
+ sections << ""
210
+ sections << "```"
211
+ sections << failure[:message]
212
+ sections << "```"
213
+ sections << ""
214
+ end
215
+
216
+ if diagnosis[:recommendations].any?
217
+ sections << "### Recommendations"
218
+ diagnosis[:recommendations].each do |rec|
219
+ sections << "- #{rec}"
220
+ end
221
+ end
222
+
223
+ sections.join("\n")
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_activity"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ module Activities
8
+ # Activity that prepares for the next work loop iteration
9
+ # Updates PROMPT.md with relevant context for continued work
10
+ class PrepareNextIterationActivity < BaseActivity
11
+ def execute(input)
12
+ with_activity_context do
13
+ project_dir = input[:project_dir]
14
+ iteration = input[:iteration]
15
+ test_result = input[:test_result]
16
+ failures_only = input[:failures_only] || true
17
+
18
+ log_activity("preparing_next_iteration",
19
+ project_dir: project_dir,
20
+ iteration: iteration)
21
+
22
+ # Prepare context for next iteration
23
+ preparation = prepare_iteration_context(
24
+ project_dir: project_dir,
25
+ iteration: iteration,
26
+ test_result: test_result,
27
+ failures_only: failures_only
28
+ )
29
+
30
+ success_result(
31
+ iteration: iteration + 1,
32
+ preparation: preparation
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def prepare_iteration_context(project_dir:, iteration:, test_result:, failures_only:)
40
+ prompt_manager = Aidp::Execute::PromptManager.new(project_dir)
41
+
42
+ # Get current prompt
43
+ current_prompt = prompt_manager.read || ""
44
+
45
+ # Clean up previous iteration markers
46
+ current_prompt = clean_iteration_markers(current_prompt)
47
+
48
+ # Add iteration context
49
+ iteration_header = build_iteration_header(iteration + 1, test_result)
50
+
51
+ # Build focused context based on failures_only flag
52
+ context_section = if failures_only
53
+ build_failures_context(test_result)
54
+ else
55
+ build_full_context(test_result)
56
+ end
57
+
58
+ # Update prompt
59
+ updated_prompt = <<~PROMPT
60
+ #{current_prompt}
61
+
62
+ #{iteration_header}
63
+
64
+ #{context_section}
65
+ PROMPT
66
+
67
+ prompt_manager.write(updated_prompt.strip)
68
+
69
+ {
70
+ prompt_updated: true,
71
+ next_iteration: iteration + 1,
72
+ context_type: failures_only ? "failures_only" : "full"
73
+ }
74
+ end
75
+
76
+ def clean_iteration_markers(prompt)
77
+ # Remove previous iteration headers and context
78
+ prompt
79
+ .gsub(/---\n## Iteration \d+ Context.*?(?=---\n##|\z)/m, "")
80
+ .gsub(/---\n## Test\/Lint Failures.*?(?=---\n##|\z)/m, "")
81
+ .strip
82
+ end
83
+
84
+ def build_iteration_header(next_iteration, test_result)
85
+ results = test_result[:results] || {}
86
+ passing = results.count { |_, r| r[:success] }
87
+ total = results.count
88
+
89
+ <<~HEADER
90
+ ---
91
+ ## Iteration #{next_iteration} Context
92
+
93
+ Previous iteration status: #{passing}/#{total} checks passing
94
+ Continuing with fix-forward pattern.
95
+ HEADER
96
+ end
97
+
98
+ def build_failures_context(test_result)
99
+ results = test_result[:results] || {}
100
+ failed = results.reject { |_, r| r[:success] }
101
+
102
+ return "All checks passing in previous iteration." if failed.empty?
103
+
104
+ sections = ["### Remaining Failures to Fix", ""]
105
+
106
+ failed.each do |phase, result|
107
+ sections << "#### #{phase.to_s.capitalize}"
108
+ sections << ""
109
+ sections << "```"
110
+ sections << truncate_output(result[:output])
111
+ sections << "```"
112
+ sections << ""
113
+ end
114
+
115
+ sections.join("\n")
116
+ end
117
+
118
+ def build_full_context(test_result)
119
+ results = test_result[:results] || {}
120
+ sections = ["### Full Test Results", ""]
121
+
122
+ results.each do |phase, result|
123
+ status = result[:success] ? "PASS" : "FAIL"
124
+ sections << "#### #{phase.to_s.capitalize}: #{status}"
125
+ sections << ""
126
+ sections << "```"
127
+ sections << truncate_output(result[:output])
128
+ sections << "```"
129
+ sections << ""
130
+ end
131
+
132
+ sections.join("\n")
133
+ end
134
+
135
+ def truncate_output(output, max_length: 2000)
136
+ return "" unless output
137
+
138
+ if output.length > max_length
139
+ "#{output.slice(0, max_length)}\n... (truncated)"
140
+ else
141
+ output
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end