aidp 0.34.1 → 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 (132) 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/usage_limit.rb +179 -0
  61. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  62. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  63. data/lib/aidp/harness/usage_period.rb +234 -0
  64. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  65. data/lib/aidp/init/runner.rb +25 -4
  66. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  67. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  68. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  69. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  70. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  71. data/lib/aidp/logger.rb +25 -7
  72. data/lib/aidp/orchestration_adapter.rb +279 -0
  73. data/lib/aidp/pr_worktree_manager.rb +4 -15
  74. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  75. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  76. data/lib/aidp/prompts/template_evolver.rb +305 -0
  77. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  78. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  79. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  80. data/lib/aidp/setup/wizard.rb +616 -176
  81. data/lib/aidp/shell_executor.rb +58 -0
  82. data/lib/aidp/style_guide/selector.rb +56 -1
  83. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  84. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  85. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  86. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  87. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  88. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  89. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  90. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  91. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  92. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  93. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  94. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  95. data/lib/aidp/temporal/configuration.rb +169 -0
  96. data/lib/aidp/temporal/connection.rb +93 -0
  97. data/lib/aidp/temporal/worker.rb +119 -0
  98. data/lib/aidp/temporal/workflow_client.rb +171 -0
  99. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  100. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  101. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  102. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  103. data/lib/aidp/temporal.rb +120 -0
  104. data/lib/aidp/version.rb +1 -1
  105. data/lib/aidp/watch/base_processor.rb +35 -0
  106. data/lib/aidp/watch/build_processor.rb +132 -0
  107. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  108. data/lib/aidp/watch/rebase_processor.rb +245 -0
  109. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  110. data/lib/aidp/watch/runner.rb +366 -285
  111. data/lib/aidp/watch/state_store.rb +80 -0
  112. data/lib/aidp/watch/work_item.rb +96 -0
  113. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  114. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  115. data/lib/aidp/workflows/guided_agent.rb +1 -1
  116. data/lib/aidp/worktree.rb +62 -0
  117. data/templates/aidp.yml.example +70 -0
  118. metadata +117 -16
  119. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  120. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  121. data/lib/aidp/providers/adapter.rb +0 -239
  122. data/lib/aidp/providers/aider.rb +0 -264
  123. data/lib/aidp/providers/anthropic.rb +0 -614
  124. data/lib/aidp/providers/base.rb +0 -617
  125. data/lib/aidp/providers/capability_registry.rb +0 -204
  126. data/lib/aidp/providers/codex.rb +0 -267
  127. data/lib/aidp/providers/cursor.rb +0 -304
  128. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  129. data/lib/aidp/providers/gemini.rb +0 -110
  130. data/lib/aidp/providers/github_copilot.rb +0 -227
  131. data/lib/aidp/providers/kilocode.rb +0 -157
  132. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aidp
4
+ # Shell command executor wrapper for testability
5
+ #
6
+ # Provides two modes of execution:
7
+ # 1. `run(command)` - Captures output silently via backticks
8
+ # 2. `system(*args)` - Wraps Kernel.system() with optional output suppression
9
+ #
10
+ # In tests, set `ShellExecutor.suppress_output = true` to suppress all
11
+ # system() output without changing any production code behavior.
12
+ #
13
+ # @example Production usage
14
+ # executor = Aidp::ShellExecutor.new
15
+ # executor.system("git", "fetch", "origin") # Output shown normally
16
+ #
17
+ # @example Test setup (in spec_helper.rb)
18
+ # Aidp::ShellExecutor.suppress_output = true
19
+ #
20
+ class ShellExecutor
21
+ class << self
22
+ # When true, system() calls will have output redirected to /dev/null
23
+ # Default: false (output shown normally)
24
+ attr_accessor :suppress_output
25
+ end
26
+ self.suppress_output = false
27
+
28
+ # Run a command and capture its output
29
+ #
30
+ # @param command [String] The shell command to run
31
+ # @return [String] The command's stdout output
32
+ def run(command)
33
+ `#{command}`
34
+ end
35
+
36
+ # Check if the last command succeeded
37
+ #
38
+ # @return [Boolean] true if last command exited with status 0
39
+ def success?
40
+ $?.success?
41
+ end
42
+
43
+ # Run a command via system(), optionally suppressing output
44
+ #
45
+ # When suppress_output is true, output is redirected to /dev/null
46
+ # unless explicit out:/err: options are provided.
47
+ #
48
+ # @param args [Array] Arguments passed to Kernel.system
49
+ # @param opts [Hash] Options passed to Kernel.system
50
+ # @return [Boolean, nil] Same as Kernel.system
51
+ def system(*args, **opts)
52
+ if self.class.suppress_output && !opts.key?(:out) && !opts.key?(:err)
53
+ opts = opts.merge(out: File::NULL, err: File::NULL)
54
+ end
55
+ Kernel.system(*args, **opts)
56
+ end
57
+ end
58
+ end
@@ -152,13 +152,35 @@ module Aidp
152
152
 
153
153
  # Check if a provider needs style guide injection
154
154
  #
155
+ # Providers with instruction files (CLAUDE.md, .github/copilot-instructions.md, etc.)
156
+ # don't need style guide injected as they have their own instruction mechanism.
157
+ #
155
158
  # @param provider_name [String] Name of the provider
156
159
  # @return [Boolean] true if style guide should be injected
157
160
  def provider_needs_style_guide?(provider_name)
158
161
  return true if provider_name.nil?
159
162
 
160
163
  normalized = provider_name.to_s.downcase.strip
161
- !PROVIDERS_WITH_INSTRUCTION_FILES.include?(normalized)
164
+
165
+ # Check legacy constant first for backwards compatibility
166
+ return false if PROVIDERS_WITH_INSTRUCTION_FILES.include?(normalized)
167
+
168
+ # Dynamically check if provider has instruction files
169
+ !provider_has_instruction_files?(normalized)
170
+ end
171
+
172
+ # Check if a provider class defines instruction file paths
173
+ #
174
+ # @param provider_name [String] Name of the provider
175
+ # @return [Boolean] true if provider has instruction files defined
176
+ def provider_has_instruction_files?(provider_name)
177
+ provider_class = find_provider_class(provider_name)
178
+ return false unless provider_class
179
+
180
+ instruction_paths = provider_class.instruction_file_paths
181
+ instruction_paths.is_a?(Array) && instruction_paths.any?
182
+ rescue NoMethodError
183
+ false
162
184
  end
163
185
 
164
186
  # Select relevant sections from STYLE_GUIDE.md based on keywords
@@ -355,6 +377,39 @@ module Aidp
355
377
 
356
378
  patterns.uniq
357
379
  end
380
+
381
+ # Find provider class by name
382
+ #
383
+ # @param provider_name [String] Name of the provider
384
+ # @return [Class, nil] The provider class or nil if not found
385
+ def find_provider_class(provider_name)
386
+ # Map common provider names to class names
387
+ class_name = case provider_name.to_s.downcase
388
+ when "anthropic", "claude"
389
+ "Anthropic"
390
+ when "github_copilot", "copilot"
391
+ "GithubCopilot"
392
+ when "cursor"
393
+ "Cursor"
394
+ when "gemini"
395
+ "Gemini"
396
+ when "aider"
397
+ "Aider"
398
+ when "kilocode"
399
+ "Kilocode"
400
+ when "opencode"
401
+ "Opencode"
402
+ when "codex"
403
+ "Codex"
404
+ else
405
+ # Try to find by capitalizing the name
406
+ provider_name.to_s.split("_").map(&:capitalize).join
407
+ end
408
+
409
+ AgentHarness::Providers.const_get(class_name)
410
+ rescue NameError
411
+ nil
412
+ end
358
413
  end
359
414
  end
360
415
  end
@@ -0,0 +1,198 @@
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 analyzes a GitHub issue
10
+ # Extracts requirements, acceptance criteria, and context
11
+ class AnalyzeIssueActivity < BaseActivity
12
+ def execute(input)
13
+ with_activity_context do
14
+ project_dir = input[:project_dir]
15
+ issue_number = input[:issue_number]
16
+ issue_url = input[:issue_url]
17
+
18
+ # Validate issue_number is numeric
19
+ unless issue_number.to_s.match?(/\A\d+\z/)
20
+ return error_result("Invalid issue number: must be numeric")
21
+ end
22
+
23
+ log_activity("analyzing_issue",
24
+ project_dir: project_dir,
25
+ issue_number: issue_number)
26
+
27
+ # Fetch issue details
28
+ issue_data = fetch_issue(project_dir, issue_number, issue_url)
29
+
30
+ unless issue_data
31
+ return error_result("Failed to fetch issue ##{issue_number}")
32
+ end
33
+
34
+ heartbeat(phase: "analysis", issue_number: issue_number)
35
+
36
+ # Extract structured information
37
+ analysis = analyze_issue_content(project_dir, issue_data)
38
+
39
+ success_result(
40
+ result: analysis,
41
+ issue_number: issue_number,
42
+ issue_title: issue_data[:title]
43
+ )
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def fetch_issue(project_dir, issue_number, issue_url)
50
+ # Check if gh CLI is available
51
+ _stdout, _stderr, status = Open3.capture3("which", "gh")
52
+ if status.success?
53
+ fetch_with_gh_cli(issue_number)
54
+ elsif issue_url
55
+ fetch_with_url(issue_url)
56
+ end
57
+ rescue => e
58
+ Aidp.log_error("analyze_issue_activity", "fetch_failed",
59
+ issue_number: issue_number,
60
+ error: e.message)
61
+ nil
62
+ end
63
+
64
+ def fetch_with_gh_cli(issue_number)
65
+ # issue_number is validated as numeric in execute()
66
+ stdout, _stderr, status = Open3.capture3(
67
+ "gh", "issue", "view", issue_number.to_s,
68
+ "--json", "title,body,labels,comments"
69
+ )
70
+ return nil unless status.success?
71
+
72
+ data = JSON.parse(stdout, symbolize_names: true)
73
+ {
74
+ number: issue_number,
75
+ title: data[:title],
76
+ body: data[:body],
77
+ labels: data[:labels]&.map { |l| l[:name] } || [],
78
+ comments: data[:comments]&.map { |c| c[:body] } || []
79
+ }
80
+ end
81
+
82
+ def fetch_with_url(issue_url)
83
+ # Parse issue URL to extract owner/repo/number
84
+ # This regex is safe - no backtracking issues
85
+ match = issue_url.match(%r{\Ahttps?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)/issues/(\d+)})
86
+ return nil unless match
87
+
88
+ owner, repo, number = match.captures
89
+
90
+ # Validate extracted values
91
+ return nil unless owner.match?(/\A[A-Za-z0-9_.-]+\z/)
92
+ return nil unless repo.match?(/\A[A-Za-z0-9_.-]+\z/)
93
+ return nil unless number.match?(/\A\d+\z/)
94
+
95
+ # Use array-style Open3 to avoid shell injection
96
+ stdout, _stderr, status = Open3.capture3(
97
+ "gh", "issue", "view", number,
98
+ "--repo", "#{owner}/#{repo}",
99
+ "--json", "title,body,labels,comments"
100
+ )
101
+ return nil unless status.success?
102
+
103
+ data = JSON.parse(stdout, symbolize_names: true)
104
+ {
105
+ number: number.to_i,
106
+ title: data[:title],
107
+ body: data[:body],
108
+ labels: data[:labels]&.map { |l| l[:name] } || [],
109
+ comments: data[:comments]&.map { |c| c[:body] } || []
110
+ }
111
+ end
112
+
113
+ def analyze_issue_content(project_dir, issue_data)
114
+ # Build analysis structure
115
+ {
116
+ issue_number: issue_data[:number],
117
+ title: issue_data[:title],
118
+ description: issue_data[:body],
119
+ labels: issue_data[:labels],
120
+ comments: issue_data[:comments],
121
+ requirements: extract_requirements(issue_data),
122
+ acceptance_criteria: extract_acceptance_criteria(issue_data),
123
+ affected_areas: identify_affected_areas(project_dir, issue_data)
124
+ }
125
+ end
126
+
127
+ def extract_requirements(issue_data)
128
+ body = issue_data[:body] || ""
129
+ comments = issue_data[:comments] || []
130
+
131
+ # Simple extraction - look for requirement patterns
132
+ requirements = []
133
+
134
+ # From body - process line by line to avoid ReDoS
135
+ extract_list_items(body, requirements)
136
+
137
+ # From comments with context
138
+ comments.each do |comment|
139
+ extract_list_items(comment, requirements)
140
+ end
141
+
142
+ requirements.uniq.first(20)
143
+ end
144
+
145
+ def extract_list_items(text, items)
146
+ text.each_line do |line|
147
+ # Match lines starting with - or *
148
+ if line.match?(/\A\s{0,50}[-*]/)
149
+ content = line.sub(/\A\s{0,50}[-*]\s{0,50}/, "").strip
150
+ items << content if content.length > 10
151
+ end
152
+ end
153
+ end
154
+
155
+ def extract_acceptance_criteria(issue_data)
156
+ body = issue_data[:body] || ""
157
+
158
+ # Look for acceptance criteria section
159
+ criteria = []
160
+
161
+ if body.downcase.include?("acceptance criteria")
162
+ section = body.split(/acceptance criteria/i).last
163
+ section = section.split("\n##").first if section.include?("\n##")
164
+
165
+ # Process line by line to avoid ReDoS
166
+ section.each_line do |line|
167
+ # Match lines starting with -, *, or numbers
168
+ if line.match?(/\A\s{0,50}[-*\d.]/)
169
+ content = line.sub(/\A\s{0,50}[-*\d.]+\s{0,50}/, "").strip
170
+ criteria << content if content.length > 5
171
+ end
172
+ end
173
+ end
174
+
175
+ criteria.first(10)
176
+ end
177
+
178
+ def identify_affected_areas(project_dir, issue_data)
179
+ title = issue_data[:title] || ""
180
+ body = issue_data[:body] || ""
181
+ combined = "#{title}\n#{body}".downcase
182
+
183
+ areas = []
184
+
185
+ # Common area patterns
186
+ areas << "tests" if combined.include?("test") || combined.include?("spec")
187
+ areas << "documentation" if combined.include?("doc") || combined.include?("readme")
188
+ areas << "configuration" if combined.include?("config") || combined.include?("setting")
189
+ areas << "api" if combined.include?("api") || combined.include?("endpoint")
190
+ areas << "ui" if combined.include?("ui") || combined.include?("interface") || combined.include?("display")
191
+ areas << "database" if combined.include?("database") || combined.include?("migration") || combined.include?("schema")
192
+
193
+ areas.uniq
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,163 @@
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 a sub-task for decomposition
9
+ # Determines complexity and identifies further sub-tasks if needed
10
+ class AnalyzeSubTaskActivity < BaseActivity
11
+ def execute(input)
12
+ with_activity_context do
13
+ project_dir = input[:project_dir]
14
+ sub_issue_id = input[:sub_issue_id]
15
+ task_description = input[:task_description]
16
+ context = input[:context] || {}
17
+
18
+ log_activity("analyzing_sub_task",
19
+ project_dir: project_dir,
20
+ sub_issue_id: sub_issue_id)
21
+
22
+ # Analyze complexity
23
+ analysis = analyze_task_complexity(
24
+ project_dir: project_dir,
25
+ task_description: task_description,
26
+ context: context
27
+ )
28
+
29
+ success_result(
30
+ result: analysis,
31
+ sub_issue_id: sub_issue_id
32
+ )
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def analyze_task_complexity(project_dir:, task_description:, context:)
39
+ # Estimate complexity based on description
40
+ estimated_iterations = estimate_iterations(task_description)
41
+
42
+ # Identify potential sub-tasks
43
+ sub_tasks = identify_sub_tasks(task_description)
44
+
45
+ # Identify affected files
46
+ affected_files = identify_affected_files(project_dir, task_description)
47
+
48
+ {
49
+ task_description: task_description,
50
+ estimated_iterations: estimated_iterations,
51
+ complexity: complexity_level(estimated_iterations),
52
+ sub_tasks: sub_tasks,
53
+ affected_files: affected_files,
54
+ decomposition_recommended: sub_tasks.length >= 3 || estimated_iterations > 20
55
+ }
56
+ end
57
+
58
+ def estimate_iterations(description)
59
+ return 1 unless description
60
+
61
+ # Simple heuristics based on description length and keywords
62
+ base = 2
63
+
64
+ # Longer descriptions usually mean more work
65
+ base += (description.length / 200).clamp(0, 5)
66
+
67
+ # Keywords that suggest complexity
68
+ complexity_keywords = %w[refactor migrate multiple all entire complete comprehensive]
69
+ base += complexity_keywords.count { |kw| description.downcase.include?(kw) }
70
+
71
+ base.clamp(1, 30)
72
+ end
73
+
74
+ def complexity_level(iterations)
75
+ case iterations
76
+ when 1..3 then :simple
77
+ when 4..10 then :moderate
78
+ when 11..20 then :complex
79
+ else :very_complex
80
+ end
81
+ end
82
+
83
+ def identify_sub_tasks(description)
84
+ return [] unless description
85
+
86
+ sub_tasks = []
87
+
88
+ # Look for numbered lists - process line by line to avoid ReDoS
89
+ description.each_line do |line|
90
+ if line.match?(/\A\d+[.)]/)
91
+ # Extract content after the number marker
92
+ content = line.sub(/\A\d+[.)]\s{0,50}/, "").strip
93
+ next if content.empty?
94
+
95
+ sub_tasks << {
96
+ description: content,
97
+ estimated_iterations: estimate_iterations(content)
98
+ }
99
+ end
100
+ end
101
+
102
+ # Look for bullet points if no numbered items found
103
+ if sub_tasks.empty?
104
+ description.each_line do |line|
105
+ if line.match?(/\A[-*]/)
106
+ content = line.sub(/\A[-*]\s{0,50}/, "").strip
107
+ next if content.length < 10
108
+
109
+ sub_tasks << {
110
+ description: content,
111
+ estimated_iterations: estimate_iterations(content)
112
+ }
113
+ end
114
+ end
115
+ end
116
+
117
+ # Limit to reasonable number
118
+ sub_tasks.first(10)
119
+ end
120
+
121
+ def identify_affected_files(project_dir, description)
122
+ return [] unless description
123
+
124
+ affected = []
125
+
126
+ # Look for file paths in description - use word boundaries to avoid ReDoS
127
+ # Match patterns like "lib/foo/bar.rb" with limited path depth
128
+ file_extensions = %w[rb py js ts jsx tsx go rs]
129
+ file_extensions.each do |ext|
130
+ # Use a simpler pattern that matches word characters and slashes, limited depth
131
+ description.scan(/\b(\w[\w\/]{0,100}\.#{ext})\b/).flatten.each do |file_path|
132
+ next unless file_path.include?("/") || file_path.match?(/^\w+\.#{ext}$/)
133
+
134
+ full_path = File.join(project_dir, file_path)
135
+ affected << file_path if File.exist?(full_path)
136
+ end
137
+ end
138
+
139
+ # Look for common patterns
140
+ patterns = {
141
+ "test" => "spec/**/*_spec.rb",
142
+ "spec" => "spec/**/*_spec.rb",
143
+ "config" => "config/**/*.yml",
144
+ "migration" => "db/migrate/*.rb",
145
+ "api" => "lib/**/api*.rb",
146
+ "cli" => "lib/**/cli*.rb"
147
+ }
148
+
149
+ patterns.each do |keyword, pattern|
150
+ if description.downcase.include?(keyword)
151
+ Dir.glob(File.join(project_dir, pattern)).each do |file|
152
+ relative = file.sub("#{project_dir}/", "")
153
+ affected << relative
154
+ end
155
+ end
156
+ end
157
+
158
+ affected.uniq.first(20)
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "temporalio/activity"
4
+
5
+ module Aidp
6
+ module Temporal
7
+ module Activities
8
+ # Base class for AIDP Temporal activities
9
+ # Provides common patterns and utilities for activity implementations
10
+ class BaseActivity < Temporalio::Activity::Definition
11
+ # Activity context (set by Temporal)
12
+ def activity_context
13
+ Temporalio::Activity.context
14
+ end
15
+
16
+ # Send heartbeat to indicate activity is still running
17
+ def heartbeat(*details)
18
+ Temporalio::Activity.heartbeat(*details)
19
+ end
20
+
21
+ # Check if cancellation was requested
22
+ def cancellation_requested?
23
+ Temporalio::Activity.cancellation_requested?
24
+ end
25
+
26
+ # Raise if cancellation was requested
27
+ def check_cancellation!
28
+ raise Temporalio::Error::CanceledError, "Activity canceled" if cancellation_requested?
29
+ end
30
+
31
+ protected
32
+
33
+ # Log within activity context
34
+ def log_activity(action, **context)
35
+ info = activity_context&.info
36
+ Aidp.log_debug("temporal_activity", action,
37
+ activity_type: self.class.name.split("::").last,
38
+ task_token: info&.task_token&.slice(0, 8),
39
+ **context)
40
+ end
41
+
42
+ # Wrap activity execution with standard error handling
43
+ def with_activity_context
44
+ log_activity("started")
45
+
46
+ result = yield
47
+
48
+ log_activity("completed", success: true)
49
+ result
50
+ rescue => e
51
+ log_activity("failed", error: e.message, error_class: e.class.name)
52
+ raise
53
+ end
54
+
55
+ # Load AIDP configuration for project
56
+ def load_config(project_dir)
57
+ Aidp::Config.load_harness_config(project_dir)
58
+ end
59
+
60
+ # Create a provider manager for the project
61
+ def create_provider_manager(project_dir, config)
62
+ require_relative "../../harness/provider_manager"
63
+ Aidp::Harness::ProviderManager.new(project_dir, config)
64
+ end
65
+
66
+ # Build success result
67
+ def success_result(data = {})
68
+ {success: true}.merge(data)
69
+ end
70
+
71
+ # Build error result
72
+ def error_result(message, data = {})
73
+ {success: false, error: message}.merge(data)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,115 @@
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 an implementation plan from issue analysis
9
+ # Generates step-by-step plan with dependencies
10
+ class CreatePlanActivity < BaseActivity
11
+ def execute(input)
12
+ with_activity_context do
13
+ project_dir = input[:project_dir]
14
+ issue_number = input[:issue_number]
15
+ analysis = input[:analysis]
16
+
17
+ log_activity("creating_plan",
18
+ project_dir: project_dir,
19
+ issue_number: issue_number)
20
+
21
+ # Generate implementation plan
22
+ plan = generate_plan(project_dir, analysis)
23
+
24
+ # Write plan to .aidp directory
25
+ write_plan(project_dir, issue_number, plan)
26
+
27
+ heartbeat(phase: "plan_complete", issue_number: issue_number)
28
+
29
+ success_result(
30
+ result: plan,
31
+ issue_number: issue_number,
32
+ step_count: plan[:steps]&.length || 0
33
+ )
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def generate_plan(project_dir, analysis)
40
+ requirements = analysis[:requirements] || []
41
+ acceptance_criteria = analysis[:acceptance_criteria] || []
42
+ affected_areas = analysis[:affected_areas] || []
43
+
44
+ steps = []
45
+
46
+ # Step 1: Setup/preparation
47
+ steps << {
48
+ name: "setup",
49
+ description: "Review existing code and understand context",
50
+ type: :preparation,
51
+ estimated_iterations: 1
52
+ }
53
+
54
+ # Generate implementation steps from requirements
55
+ requirements.each_with_index do |req, idx|
56
+ steps << {
57
+ name: "implement_#{idx + 1}",
58
+ description: "Implement: #{req}",
59
+ type: :implementation,
60
+ estimated_iterations: 2
61
+ }
62
+ end
63
+
64
+ # Add testing step
65
+ if affected_areas.include?("tests")
66
+ steps << {
67
+ name: "add_tests",
68
+ description: "Add or update tests for new functionality",
69
+ type: :testing,
70
+ estimated_iterations: 2
71
+ }
72
+ end
73
+
74
+ # Add documentation step
75
+ if affected_areas.include?("documentation")
76
+ steps << {
77
+ name: "update_docs",
78
+ description: "Update documentation",
79
+ type: :documentation,
80
+ estimated_iterations: 1
81
+ }
82
+ end
83
+
84
+ # Final validation step
85
+ steps << {
86
+ name: "validate",
87
+ description: "Run full test suite and validate all changes",
88
+ type: :validation,
89
+ estimated_iterations: 1
90
+ }
91
+
92
+ {
93
+ issue_number: analysis[:issue_number],
94
+ title: analysis[:title],
95
+ steps: steps,
96
+ requirements: requirements,
97
+ acceptance_criteria: acceptance_criteria,
98
+ estimated_total_iterations: steps.sum { |s| s[:estimated_iterations] },
99
+ created_at: Time.now.iso8601
100
+ }
101
+ end
102
+
103
+ def write_plan(project_dir, issue_number, plan)
104
+ plan_dir = File.join(project_dir, ".aidp", "plans")
105
+ FileUtils.mkdir_p(plan_dir)
106
+
107
+ plan_file = File.join(plan_dir, "issue_#{issue_number}_plan.yml")
108
+ File.write(plan_file, plan.to_yaml)
109
+
110
+ log_activity("plan_written", file: plan_file)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end