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.
- checksums.yaml +4 -4
- data/README.md +14 -14
- data/lib/aidp/agent_harness_adapter.rb +309 -0
- data/lib/aidp/cli/issue_importer.rb +20 -6
- data/lib/aidp/cli/jobs_command.rb +1 -1
- data/lib/aidp/cli/models_command.rb +9 -16
- data/lib/aidp/cli/prompts_command.rb +253 -0
- data/lib/aidp/cli/security_command.rb +4 -10
- data/lib/aidp/cli/storage_command.rb +277 -0
- data/lib/aidp/cli/temporal_command.rb +460 -0
- data/lib/aidp/cli.rb +24 -2
- data/lib/aidp/config.rb +15 -0
- data/lib/aidp/config_paths.rb +12 -2
- data/lib/aidp/database/migrations.rb +114 -0
- data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
- data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
- data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
- data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
- data/lib/aidp/database/repositories/job_repository.rb +278 -0
- data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
- data/lib/aidp/database/repositories/progress_repository.rb +210 -0
- data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
- data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
- data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
- data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
- data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
- data/lib/aidp/database/repositories/task_repository.rb +231 -0
- data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
- data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
- data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
- data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
- data/lib/aidp/database/repository.rb +166 -0
- data/lib/aidp/database/schema.rb +347 -0
- data/lib/aidp/database/storage_migrator.rb +693 -0
- data/lib/aidp/database.rb +139 -0
- data/lib/aidp/debug_mixin.rb +2 -2
- data/lib/aidp/execute/checkpoint.rb +26 -0
- data/lib/aidp/execute/checkpoint_display.rb +16 -0
- data/lib/aidp/execute/prompt_evaluator.rb +30 -57
- data/lib/aidp/execute/work_loop_runner.rb +294 -46
- data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
- data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
- data/lib/aidp/harness/ai_decision_engine.rb +78 -296
- data/lib/aidp/harness/config_schema.rb +59 -0
- data/lib/aidp/harness/configuration.rb +229 -10
- data/lib/aidp/harness/deprecation_cache.rb +53 -11
- data/lib/aidp/harness/enhanced_runner.rb +1 -1
- data/lib/aidp/harness/error_handler.rb +11 -5
- data/lib/aidp/harness/provider_config.rb +36 -0
- data/lib/aidp/harness/provider_factory.rb +26 -24
- data/lib/aidp/harness/provider_info.rb +9 -5
- data/lib/aidp/harness/provider_manager.rb +2 -2
- data/lib/aidp/harness/provider_metrics.rb +103 -1
- data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
- data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
- data/lib/aidp/harness/runner.rb +7 -2
- data/lib/aidp/harness/state_manager.rb +1 -1
- data/lib/aidp/harness/test_runner.rb +167 -11
- data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
- data/lib/aidp/harness/usage_limit.rb +179 -0
- data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
- data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
- data/lib/aidp/harness/usage_period.rb +234 -0
- data/lib/aidp/init/agent_instructions_generator.rb +317 -0
- data/lib/aidp/init/runner.rb +25 -4
- data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
- data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
- data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
- data/lib/aidp/interfaces/logger_interface.rb +150 -0
- data/lib/aidp/interfaces/ui_interface.rb +254 -0
- data/lib/aidp/logger.rb +25 -7
- data/lib/aidp/orchestration_adapter.rb +279 -0
- data/lib/aidp/pr_worktree_manager.rb +4 -15
- data/lib/aidp/prompts/feedback_collector.rb +198 -0
- data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
- data/lib/aidp/prompts/template_evolver.rb +305 -0
- data/lib/aidp/prompts/template_version_manager.rb +325 -0
- data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
- data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
- data/lib/aidp/setup/wizard.rb +616 -176
- data/lib/aidp/shell_executor.rb +58 -0
- data/lib/aidp/style_guide/selector.rb +56 -1
- data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
- data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
- data/lib/aidp/temporal/activities/base_activity.rb +78 -0
- data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
- data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
- data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
- data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
- data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
- data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
- data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
- data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
- data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
- data/lib/aidp/temporal/configuration.rb +169 -0
- data/lib/aidp/temporal/connection.rb +93 -0
- data/lib/aidp/temporal/worker.rb +119 -0
- data/lib/aidp/temporal/workflow_client.rb +171 -0
- data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
- data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
- data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
- data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
- data/lib/aidp/temporal.rb +120 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/base_processor.rb +35 -0
- data/lib/aidp/watch/build_processor.rb +132 -0
- data/lib/aidp/watch/github_state_extractor.rb +0 -8
- data/lib/aidp/watch/rebase_processor.rb +245 -0
- data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
- data/lib/aidp/watch/runner.rb +366 -285
- data/lib/aidp/watch/state_store.rb +80 -0
- data/lib/aidp/watch/work_item.rb +96 -0
- data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
- data/lib/aidp/watch/worktree_reconciler.rb +643 -0
- data/lib/aidp/workflows/guided_agent.rb +1 -1
- data/lib/aidp/worktree.rb +62 -0
- data/templates/aidp.yml.example +70 -0
- metadata +117 -16
- data/lib/aidp/analyze/json_file_storage.rb +0 -292
- data/lib/aidp/execute/future_work_backlog.rb +0 -411
- data/lib/aidp/providers/adapter.rb +0 -239
- data/lib/aidp/providers/aider.rb +0 -264
- data/lib/aidp/providers/anthropic.rb +0 -614
- data/lib/aidp/providers/base.rb +0 -617
- data/lib/aidp/providers/capability_registry.rb +0 -204
- data/lib/aidp/providers/codex.rb +0 -267
- data/lib/aidp/providers/cursor.rb +0 -304
- data/lib/aidp/providers/error_taxonomy.rb +0 -195
- data/lib/aidp/providers/gemini.rb +0 -110
- data/lib/aidp/providers/github_copilot.rb +0 -227
- data/lib/aidp/providers/kilocode.rb +0 -157
- 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
|
-
|
|
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
|