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.
- 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/ui.rb +11 -0
- 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 +118 -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,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Init
|
|
8
|
+
# Generates agent instruction files for AI coding assistants.
|
|
9
|
+
#
|
|
10
|
+
# This generator creates a master AGENTS.md file and symlinks it to
|
|
11
|
+
# provider-specific locations (e.g., CLAUDE.md, .github/copilot-instructions.md).
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# generator = AgentInstructionsGenerator.new(project_dir: "/path/to/project")
|
|
15
|
+
# files = generator.generate(analysis: analysis, preferences: preferences)
|
|
16
|
+
#
|
|
17
|
+
class AgentInstructionsGenerator
|
|
18
|
+
include Aidp::MessageDisplay
|
|
19
|
+
|
|
20
|
+
MASTER_FILE = "AGENTS.md"
|
|
21
|
+
|
|
22
|
+
# Known provider classes for instruction file generation
|
|
23
|
+
PROVIDER_CLASSES = %w[
|
|
24
|
+
Anthropic
|
|
25
|
+
GithubCopilot
|
|
26
|
+
Cursor
|
|
27
|
+
Gemini
|
|
28
|
+
Aider
|
|
29
|
+
Kilocode
|
|
30
|
+
Opencode
|
|
31
|
+
Codex
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(project_dir = Dir.pwd, prompt: nil)
|
|
35
|
+
@project_dir = project_dir
|
|
36
|
+
@prompt = prompt
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generate agent instruction files
|
|
40
|
+
#
|
|
41
|
+
# @param analysis [Hash] Project analysis results
|
|
42
|
+
# @param preferences [Hash] User preferences
|
|
43
|
+
# @return [Array<String>] List of generated files
|
|
44
|
+
def generate(analysis:, preferences: {})
|
|
45
|
+
Aidp.log_debug("agent_instructions_generator", "starting_generation",
|
|
46
|
+
project_dir: @project_dir)
|
|
47
|
+
|
|
48
|
+
generated_files = []
|
|
49
|
+
skipped_files = []
|
|
50
|
+
symlinked_files = []
|
|
51
|
+
|
|
52
|
+
# Generate master AGENTS.md
|
|
53
|
+
master_path = File.join(@project_dir, MASTER_FILE)
|
|
54
|
+
if generate_master_file(master_path, analysis, preferences)
|
|
55
|
+
generated_files << MASTER_FILE
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create symlinks to provider-specific locations
|
|
59
|
+
provider_instruction_files.each do |file_info|
|
|
60
|
+
result = create_provider_symlink(file_info, master_path)
|
|
61
|
+
case result[:status]
|
|
62
|
+
when :created
|
|
63
|
+
symlinked_files << file_info[:path]
|
|
64
|
+
when :skipped
|
|
65
|
+
skipped_files << {path: file_info[:path], reason: result[:reason]}
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Report results
|
|
70
|
+
report_generation_results(generated_files, symlinked_files, skipped_files)
|
|
71
|
+
|
|
72
|
+
generated_files + symlinked_files
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if master AGENTS.md exists
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def exists?
|
|
79
|
+
File.exist?(File.join(@project_dir, MASTER_FILE))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get all provider instruction file paths
|
|
83
|
+
#
|
|
84
|
+
# @return [Array<Hash>] Array of file info hashes
|
|
85
|
+
def provider_instruction_files
|
|
86
|
+
@provider_instruction_files ||= collect_provider_instruction_files
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Preview what files would be generated
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] Preview information
|
|
92
|
+
def preview
|
|
93
|
+
existing = []
|
|
94
|
+
to_create = []
|
|
95
|
+
to_symlink = []
|
|
96
|
+
|
|
97
|
+
master_path = File.join(@project_dir, MASTER_FILE)
|
|
98
|
+
if File.exist?(master_path)
|
|
99
|
+
existing << MASTER_FILE
|
|
100
|
+
else
|
|
101
|
+
to_create << MASTER_FILE
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
provider_instruction_files.each do |file_info|
|
|
105
|
+
full_path = File.join(@project_dir, file_info[:path])
|
|
106
|
+
if File.exist?(full_path) || File.symlink?(full_path)
|
|
107
|
+
existing << file_info[:path]
|
|
108
|
+
else
|
|
109
|
+
to_symlink << file_info[:path]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
existing: existing,
|
|
115
|
+
to_create: to_create,
|
|
116
|
+
to_symlink: to_symlink
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def collect_provider_instruction_files
|
|
123
|
+
files = []
|
|
124
|
+
|
|
125
|
+
PROVIDER_CLASSES.each do |class_name|
|
|
126
|
+
provider_class = AgentHarness::Providers.const_get(class_name)
|
|
127
|
+
next unless provider_class.respond_to?(:instruction_file_paths)
|
|
128
|
+
|
|
129
|
+
provider_class.instruction_file_paths.each do |file_info|
|
|
130
|
+
files << file_info.merge(provider: class_name.downcase)
|
|
131
|
+
end
|
|
132
|
+
rescue NameError => e
|
|
133
|
+
Aidp.log_debug("agent_instructions_generator", "provider_not_found",
|
|
134
|
+
provider: class_name, error: e.message)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
files
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def generate_master_file(path, analysis, preferences)
|
|
141
|
+
if File.exist?(path)
|
|
142
|
+
Aidp.log_debug("agent_instructions_generator", "master_exists",
|
|
143
|
+
path: path)
|
|
144
|
+
return false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
content = build_master_content(analysis, preferences)
|
|
148
|
+
File.write(path, content)
|
|
149
|
+
|
|
150
|
+
Aidp.log_debug("agent_instructions_generator", "master_created",
|
|
151
|
+
path: path, size: content.length)
|
|
152
|
+
true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_master_content(analysis, preferences)
|
|
156
|
+
languages = format_list(analysis[:languages]&.keys)
|
|
157
|
+
frameworks = extract_confident_names(analysis[:frameworks], threshold: 0.7)
|
|
158
|
+
test_frameworks = extract_confident_names(analysis[:test_frameworks], threshold: 0.7)
|
|
159
|
+
|
|
160
|
+
<<~AGENTS
|
|
161
|
+
# AI Agent Instructions
|
|
162
|
+
|
|
163
|
+
> Generated by `aidp init` on #{Time.now.utc.iso8601}
|
|
164
|
+
>
|
|
165
|
+
> This file provides instructions for AI coding assistants working on this project.
|
|
166
|
+
> Provider-specific instruction files (CLAUDE.md, .github/copilot-instructions.md, etc.)
|
|
167
|
+
> are symlinked to this master file for consistency.
|
|
168
|
+
|
|
169
|
+
## Primary Reference
|
|
170
|
+
|
|
171
|
+
**Before making ANY code changes, read and follow [`docs/LLM_STYLE_GUIDE.md`](docs/LLM_STYLE_GUIDE.md).**
|
|
172
|
+
|
|
173
|
+
The LLM_STYLE_GUIDE is your primary reference for all coding standards and patterns.
|
|
174
|
+
|
|
175
|
+
## Project Overview
|
|
176
|
+
|
|
177
|
+
#{build_project_overview(analysis)}
|
|
178
|
+
|
|
179
|
+
## Key Principles
|
|
180
|
+
|
|
181
|
+
1. **Follow the LLM_STYLE_GUIDE** - All code must adhere to the documented standards
|
|
182
|
+
2. **Read before writing** - Always read existing code before making modifications
|
|
183
|
+
3. **Test at boundaries** - Mock only external services (network, filesystem, APIs)
|
|
184
|
+
4. **Log extensively** - Use `Aidp.log_debug()` for important code paths
|
|
185
|
+
5. **Small, focused changes** - Follow Sandi Metz guidelines for code organization
|
|
186
|
+
|
|
187
|
+
## Quick Reference
|
|
188
|
+
|
|
189
|
+
### Languages & Frameworks
|
|
190
|
+
- **Languages**: #{languages.empty? ? "Not detected" : languages}
|
|
191
|
+
- **Frameworks**: #{frameworks.empty? ? "None detected" : frameworks.join(", ")}
|
|
192
|
+
- **Testing**: #{test_frameworks.empty? ? "Unknown" : test_frameworks.join(", ")}
|
|
193
|
+
|
|
194
|
+
### Important Documentation
|
|
195
|
+
- [`docs/LLM_STYLE_GUIDE.md`](docs/LLM_STYLE_GUIDE.md) - Coding standards (PRIMARY)
|
|
196
|
+
- [`docs/PROJECT_ANALYSIS.md`](docs/PROJECT_ANALYSIS.md) - Project structure analysis
|
|
197
|
+
- [`docs/CODE_QUALITY_PLAN.md`](docs/CODE_QUALITY_PLAN.md) - Quality improvement plan
|
|
198
|
+
|
|
199
|
+
## Anti-Patterns to Avoid
|
|
200
|
+
|
|
201
|
+
- Do NOT use `puts` or `print` for output (use TTY components)
|
|
202
|
+
- Do NOT add mock methods to production code (use dependency injection)
|
|
203
|
+
- Do NOT skip tests for "small fixes"
|
|
204
|
+
- Do NOT create god classes or methods
|
|
205
|
+
- Do NOT use regex for semantic analysis (use AI decision engines)
|
|
206
|
+
|
|
207
|
+
## Getting Help
|
|
208
|
+
|
|
209
|
+
If uncertain about standards or patterns:
|
|
210
|
+
1. Check `docs/LLM_STYLE_GUIDE.md` first
|
|
211
|
+
2. Look at similar existing code in the project
|
|
212
|
+
3. Ask for clarification rather than guessing
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
*This file is the single source of truth for AI agent instructions.*
|
|
217
|
+
*Provider-specific files are symlinked to this master file.*
|
|
218
|
+
AGENTS
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def build_project_overview(analysis)
|
|
222
|
+
repo_stats = analysis[:repo_stats] || {}
|
|
223
|
+
lines = []
|
|
224
|
+
|
|
225
|
+
if repo_stats[:total_files]
|
|
226
|
+
lines << "- **Files**: #{repo_stats[:total_files]} total"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
if analysis[:key_directories]&.any?
|
|
230
|
+
lines << "- **Key directories**: #{analysis[:key_directories].take(5).join(", ")}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if repo_stats[:has_ci_config]
|
|
234
|
+
lines << "- **CI/CD**: Configured"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
if repo_stats[:has_containerization]
|
|
238
|
+
lines << "- **Containerization**: Available"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if lines.empty?
|
|
242
|
+
"Project structure information not available. Run `aidp init --explain-detection` for details."
|
|
243
|
+
else
|
|
244
|
+
lines.join("\n")
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def create_provider_symlink(file_info, master_path)
|
|
249
|
+
target_path = File.join(@project_dir, file_info[:path])
|
|
250
|
+
|
|
251
|
+
# Check if file already exists
|
|
252
|
+
if File.exist?(target_path) || File.symlink?(target_path)
|
|
253
|
+
Aidp.log_debug("agent_instructions_generator", "file_exists",
|
|
254
|
+
path: file_info[:path])
|
|
255
|
+
return {status: :skipped, reason: "file already exists"}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Create parent directory if needed
|
|
259
|
+
parent_dir = File.dirname(target_path)
|
|
260
|
+
unless File.directory?(parent_dir)
|
|
261
|
+
FileUtils.mkdir_p(parent_dir)
|
|
262
|
+
Aidp.log_debug("agent_instructions_generator", "created_directory",
|
|
263
|
+
path: parent_dir)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Calculate relative path from target to master
|
|
267
|
+
relative_master = calculate_relative_path(target_path, master_path)
|
|
268
|
+
|
|
269
|
+
# Create symlink
|
|
270
|
+
begin
|
|
271
|
+
File.symlink(relative_master, target_path)
|
|
272
|
+
Aidp.log_debug("agent_instructions_generator", "symlink_created",
|
|
273
|
+
target: file_info[:path], source: relative_master)
|
|
274
|
+
{status: :created}
|
|
275
|
+
rescue Errno::EEXIST
|
|
276
|
+
{status: :skipped, reason: "symlink already exists"}
|
|
277
|
+
rescue => e
|
|
278
|
+
Aidp.log_error("agent_instructions_generator", "symlink_failed",
|
|
279
|
+
target: file_info[:path], error: e.message)
|
|
280
|
+
{status: :skipped, reason: e.message}
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def calculate_relative_path(from_path, to_path)
|
|
285
|
+
from_dir = File.dirname(from_path)
|
|
286
|
+
Pathname.new(to_path).relative_path_from(Pathname.new(from_dir)).to_s
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def report_generation_results(generated, symlinked, skipped)
|
|
290
|
+
if generated.any?
|
|
291
|
+
display_message("\n Created master agent instructions:", type: :info)
|
|
292
|
+
generated.each { |f| display_message(" - #{f}", type: :success) }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if symlinked.any?
|
|
296
|
+
display_message("\n Created provider symlinks:", type: :info)
|
|
297
|
+
symlinked.each { |f| display_message(" - #{f} -> #{MASTER_FILE}", type: :success) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
if skipped.any?
|
|
301
|
+
display_message("\n Preserved existing files:", type: :info)
|
|
302
|
+
skipped.each { |s| display_message(" - #{s[:path]} (#{s[:reason]})", type: :info) }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def format_list(values)
|
|
307
|
+
Array(values).join(", ")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def extract_confident_names(detections, threshold: 0.7, key: :name)
|
|
311
|
+
return [] if detections.nil? || detections.empty?
|
|
312
|
+
|
|
313
|
+
detections.select { |d| d[:confidence] >= threshold }.map { |d| d[key] }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
data/lib/aidp/init/runner.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "../message_display"
|
|
|
5
5
|
require_relative "project_analyzer"
|
|
6
6
|
require_relative "doc_generator"
|
|
7
7
|
require_relative "devcontainer_generator"
|
|
8
|
+
require_relative "agent_instructions_generator"
|
|
8
9
|
|
|
9
10
|
module Aidp
|
|
10
11
|
module Init
|
|
@@ -13,12 +14,13 @@ module Aidp
|
|
|
13
14
|
class Runner
|
|
14
15
|
include Aidp::MessageDisplay
|
|
15
16
|
|
|
16
|
-
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil, devcontainer_generator: nil, options: {})
|
|
17
|
+
def initialize(project_dir = Dir.pwd, prompt: TTY::Prompt.new, analyzer: nil, doc_generator: nil, devcontainer_generator: nil, agent_instructions_generator: nil, options: {})
|
|
17
18
|
@project_dir = project_dir
|
|
18
19
|
@prompt = prompt
|
|
19
20
|
@analyzer = analyzer || ProjectAnalyzer.new(project_dir)
|
|
20
21
|
@doc_generator = doc_generator || DocGenerator.new(project_dir)
|
|
21
22
|
@devcontainer_generator = devcontainer_generator || DevcontainerGenerator.new(project_dir)
|
|
23
|
+
@agent_instructions_generator = agent_instructions_generator || AgentInstructionsGenerator.new(project_dir, prompt: prompt)
|
|
22
24
|
@options = options
|
|
23
25
|
end
|
|
24
26
|
|
|
@@ -78,11 +80,17 @@ module Aidp
|
|
|
78
80
|
devcontainer_files = @devcontainer_generator.generate(analysis: analysis, preferences: preferences)
|
|
79
81
|
generated_files.concat(devcontainer_files)
|
|
80
82
|
|
|
81
|
-
display_message("\n
|
|
82
|
-
devcontainer_files.each { |file| display_message("
|
|
83
|
+
display_message("\n Generated devcontainer:", type: :info)
|
|
84
|
+
devcontainer_files.each { |file| display_message(" - #{file}", type: :success) }
|
|
83
85
|
end
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
# Generate agent instruction files
|
|
88
|
+
if @options[:with_agent_instructions] || should_generate_agent_instructions?(preferences)
|
|
89
|
+
agent_files = @agent_instructions_generator.generate(analysis: analysis, preferences: preferences)
|
|
90
|
+
generated_files.concat(agent_files)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
display_message("\n aidp init complete.", type: :success)
|
|
86
94
|
|
|
87
95
|
{
|
|
88
96
|
analysis: analysis,
|
|
@@ -264,6 +272,15 @@ module Aidp
|
|
|
264
272
|
)
|
|
265
273
|
end
|
|
266
274
|
|
|
275
|
+
# Ask about agent instruction files unless explicitly set via options
|
|
276
|
+
unless @options.key?(:with_agent_instructions)
|
|
277
|
+
prefs[:generate_agent_instructions] = ask_yes_no_with_context(
|
|
278
|
+
"Generate agent instruction files for AI coding assistants?",
|
|
279
|
+
context: "Creates AGENTS.md and symlinks to provider-specific locations (CLAUDE.md, .github/copilot-instructions.md, etc.).",
|
|
280
|
+
default: !@agent_instructions_generator.exists?
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
267
284
|
prefs
|
|
268
285
|
end
|
|
269
286
|
|
|
@@ -271,6 +288,10 @@ module Aidp
|
|
|
271
288
|
preferences[:generate_devcontainer] == true
|
|
272
289
|
end
|
|
273
290
|
|
|
291
|
+
def should_generate_agent_instructions?(preferences)
|
|
292
|
+
preferences[:generate_agent_instructions] == true
|
|
293
|
+
end
|
|
294
|
+
|
|
274
295
|
def ask_yes_no_with_context(question, context:, default:)
|
|
275
296
|
display_message("\n#{question}", type: :info)
|
|
276
297
|
display_message(" ℹ️ #{context}", type: :info)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Interfaces
|
|
5
|
+
# ActivityMonitorInterface defines the contract for activity monitoring implementations.
|
|
6
|
+
# Activity monitoring tracks the state of long-running provider operations, detecting
|
|
7
|
+
# stuck processes and enabling timeout/cancellation logic.
|
|
8
|
+
#
|
|
9
|
+
# States:
|
|
10
|
+
# - :idle - No operation in progress
|
|
11
|
+
# - :working - Active and producing output
|
|
12
|
+
# - :stuck - No activity detected for stuck_timeout seconds
|
|
13
|
+
# - :completed - Operation finished successfully
|
|
14
|
+
# - :failed - Operation failed with an error
|
|
15
|
+
#
|
|
16
|
+
# @example Implementing the interface
|
|
17
|
+
# class MyActivityMonitor
|
|
18
|
+
# include Aidp::Interfaces::ActivityMonitorInterface
|
|
19
|
+
#
|
|
20
|
+
# def start(step_name:, stuck_timeout:, on_state_change: nil)
|
|
21
|
+
# # Implementation
|
|
22
|
+
# end
|
|
23
|
+
# # ... other methods
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# @example Using an activity monitor
|
|
27
|
+
# monitor = ActivityMonitor.new
|
|
28
|
+
# monitor.start(step_name: "sending_prompt", stuck_timeout: 60)
|
|
29
|
+
# monitor.record_activity("Received response")
|
|
30
|
+
# monitor.complete
|
|
31
|
+
#
|
|
32
|
+
module ActivityMonitorInterface
|
|
33
|
+
# Valid activity states
|
|
34
|
+
STATES = [:idle, :working, :stuck, :completed, :failed].freeze
|
|
35
|
+
|
|
36
|
+
# Start monitoring an operation.
|
|
37
|
+
#
|
|
38
|
+
# @param step_name [String] name of the operation being monitored
|
|
39
|
+
# @param stuck_timeout [Integer] seconds before considering the operation stuck
|
|
40
|
+
# @param on_state_change [Proc, nil] callback for state changes (state, message)
|
|
41
|
+
# @return [void]
|
|
42
|
+
def start(step_name:, stuck_timeout:, on_state_change: nil)
|
|
43
|
+
raise NotImplementedError, "#{self.class} must implement #start"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Record activity, resetting the stuck timer.
|
|
47
|
+
#
|
|
48
|
+
# @param message [String, nil] optional message describing the activity
|
|
49
|
+
# @return [void]
|
|
50
|
+
def record_activity(message = nil)
|
|
51
|
+
raise NotImplementedError, "#{self.class} must implement #record_activity"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Mark the operation as completed successfully.
|
|
55
|
+
#
|
|
56
|
+
# @return [void]
|
|
57
|
+
def complete
|
|
58
|
+
raise NotImplementedError, "#{self.class} must implement #complete"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Mark the operation as failed.
|
|
62
|
+
#
|
|
63
|
+
# @param error_message [String, nil] optional error description
|
|
64
|
+
# @return [void]
|
|
65
|
+
def fail(error_message = nil)
|
|
66
|
+
raise NotImplementedError, "#{self.class} must implement #fail"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Check if the operation appears to be stuck.
|
|
70
|
+
#
|
|
71
|
+
# @return [Boolean] true if no activity for stuck_timeout seconds
|
|
72
|
+
def stuck?
|
|
73
|
+
raise NotImplementedError, "#{self.class} must implement #stuck?"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the current activity state.
|
|
77
|
+
#
|
|
78
|
+
# @return [Symbol] one of STATES
|
|
79
|
+
def state
|
|
80
|
+
raise NotImplementedError, "#{self.class} must implement #state"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get elapsed time since start.
|
|
84
|
+
#
|
|
85
|
+
# @return [Float] seconds since monitoring started, or 0 if not started
|
|
86
|
+
def elapsed_time
|
|
87
|
+
raise NotImplementedError, "#{self.class} must implement #elapsed_time"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get a summary of the monitoring session.
|
|
91
|
+
#
|
|
92
|
+
# @return [Hash] summary with keys :step_name, :state, :elapsed_time, :stuck_detected, :output_count
|
|
93
|
+
def summary
|
|
94
|
+
raise NotImplementedError, "#{self.class} must implement #summary"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# NullActivityMonitor is a no-op implementation.
|
|
99
|
+
# Useful for testing or when monitoring is not needed.
|
|
100
|
+
#
|
|
101
|
+
class NullActivityMonitor
|
|
102
|
+
include ActivityMonitorInterface
|
|
103
|
+
|
|
104
|
+
def start(step_name:, stuck_timeout:, on_state_change: nil)
|
|
105
|
+
@state = :working
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def record_activity(message = nil)
|
|
109
|
+
# no-op
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def complete
|
|
113
|
+
@state = :completed
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def fail(error_message = nil)
|
|
117
|
+
@state = :failed
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def stuck?
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def state
|
|
125
|
+
@state || :idle
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def elapsed_time
|
|
129
|
+
0.0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def summary
|
|
133
|
+
{step_name: nil, state: state, elapsed_time: 0.0, stuck_detected: false, output_count: 0}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# ActivityMonitor is the standard implementation that tracks activity state.
|
|
138
|
+
#
|
|
139
|
+
# @example Basic usage
|
|
140
|
+
# monitor = ActivityMonitor.new
|
|
141
|
+
# monitor.start(step_name: "processing", stuck_timeout: 60)
|
|
142
|
+
# monitor.record_activity("Started processing")
|
|
143
|
+
# # ... do work ...
|
|
144
|
+
# monitor.complete
|
|
145
|
+
# puts monitor.summary
|
|
146
|
+
#
|
|
147
|
+
class ActivityMonitor
|
|
148
|
+
include ActivityMonitorInterface
|
|
149
|
+
|
|
150
|
+
attr_reader :state
|
|
151
|
+
|
|
152
|
+
DEFAULT_STUCK_TIMEOUT = 30
|
|
153
|
+
|
|
154
|
+
def initialize(logger: nil)
|
|
155
|
+
@logger = logger
|
|
156
|
+
@state = :idle
|
|
157
|
+
@start_time = nil
|
|
158
|
+
@last_activity_time = nil
|
|
159
|
+
@stuck_timeout = DEFAULT_STUCK_TIMEOUT
|
|
160
|
+
@step_name = nil
|
|
161
|
+
@on_state_change = nil
|
|
162
|
+
@output_count = 0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def start(step_name:, stuck_timeout: DEFAULT_STUCK_TIMEOUT, on_state_change: nil)
|
|
166
|
+
@step_name = step_name
|
|
167
|
+
@stuck_timeout = stuck_timeout
|
|
168
|
+
@on_state_change = on_state_change
|
|
169
|
+
@start_time = Time.now
|
|
170
|
+
@last_activity_time = @start_time
|
|
171
|
+
@output_count = 0
|
|
172
|
+
update_state(:working)
|
|
173
|
+
log_debug("started", step_name: step_name, stuck_timeout: stuck_timeout)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def record_activity(message = nil)
|
|
177
|
+
@output_count += 1
|
|
178
|
+
@last_activity_time = Time.now
|
|
179
|
+
update_state(:working, message)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def complete
|
|
183
|
+
update_state(:completed)
|
|
184
|
+
log_debug("completed", elapsed_time: elapsed_time, output_count: @output_count)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def fail(error_message = nil)
|
|
188
|
+
update_state(:failed, error_message)
|
|
189
|
+
log_debug("failed", error_message: error_message, elapsed_time: elapsed_time)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def stuck?
|
|
193
|
+
return false unless @state == :working
|
|
194
|
+
return false if @last_activity_time.nil?
|
|
195
|
+
|
|
196
|
+
seconds_since_activity = Time.now - @last_activity_time
|
|
197
|
+
if seconds_since_activity > @stuck_timeout
|
|
198
|
+
update_state(:stuck, "No activity for #{seconds_since_activity.round}s")
|
|
199
|
+
true
|
|
200
|
+
else
|
|
201
|
+
false
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def elapsed_time
|
|
206
|
+
return 0.0 unless @start_time
|
|
207
|
+
Time.now - @start_time
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def summary
|
|
211
|
+
{
|
|
212
|
+
step_name: @step_name,
|
|
213
|
+
state: @state,
|
|
214
|
+
start_time: @start_time&.iso8601,
|
|
215
|
+
elapsed_time: elapsed_time.round(2),
|
|
216
|
+
stuck_detected: @state == :stuck,
|
|
217
|
+
output_count: @output_count
|
|
218
|
+
}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def update_state(new_state, message = nil)
|
|
224
|
+
old_state = @state
|
|
225
|
+
@state = new_state
|
|
226
|
+
@on_state_change&.call(new_state, message) if old_state != new_state
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def log_debug(message, **metadata)
|
|
230
|
+
@logger&.log_debug("activity_monitor", message, step_name: @step_name, **metadata)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|