aidp 0.34.0 → 0.39.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -14
  3. data/lib/aidp/agent_harness_adapter.rb +309 -0
  4. data/lib/aidp/cli/issue_importer.rb +20 -6
  5. data/lib/aidp/cli/jobs_command.rb +1 -1
  6. data/lib/aidp/cli/models_command.rb +9 -16
  7. data/lib/aidp/cli/prompts_command.rb +253 -0
  8. data/lib/aidp/cli/security_command.rb +4 -10
  9. data/lib/aidp/cli/storage_command.rb +277 -0
  10. data/lib/aidp/cli/temporal_command.rb +460 -0
  11. data/lib/aidp/cli.rb +24 -2
  12. data/lib/aidp/config.rb +15 -0
  13. data/lib/aidp/config_paths.rb +12 -2
  14. data/lib/aidp/database/migrations.rb +114 -0
  15. data/lib/aidp/database/repositories/checkpoint_repository.rb +174 -0
  16. data/lib/aidp/database/repositories/deprecated_models_repository.rb +206 -0
  17. data/lib/aidp/database/repositories/evaluation_repository.rb +208 -0
  18. data/lib/aidp/database/repositories/harness_state_repository.rb +140 -0
  19. data/lib/aidp/database/repositories/job_repository.rb +278 -0
  20. data/lib/aidp/database/repositories/model_cache_repository.rb +194 -0
  21. data/lib/aidp/database/repositories/progress_repository.rb +210 -0
  22. data/lib/aidp/database/repositories/prompt_archive_repository.rb +195 -0
  23. data/lib/aidp/database/repositories/prompt_feedback_repository.rb +266 -0
  24. data/lib/aidp/database/repositories/provider_info_cache_repository.rb +208 -0
  25. data/lib/aidp/database/repositories/provider_metrics_repository.rb +245 -0
  26. data/lib/aidp/database/repositories/secrets_repository.rb +187 -0
  27. data/lib/aidp/database/repositories/task_repository.rb +231 -0
  28. data/lib/aidp/database/repositories/template_version_repository.rb +383 -0
  29. data/lib/aidp/database/repositories/watch_state_repository.rb +472 -0
  30. data/lib/aidp/database/repositories/workstream_repository.rb +310 -0
  31. data/lib/aidp/database/repositories/worktree_repository.rb +240 -0
  32. data/lib/aidp/database/repository.rb +166 -0
  33. data/lib/aidp/database/schema.rb +347 -0
  34. data/lib/aidp/database/storage_migrator.rb +693 -0
  35. data/lib/aidp/database.rb +139 -0
  36. data/lib/aidp/debug_mixin.rb +2 -2
  37. data/lib/aidp/execute/checkpoint.rb +26 -0
  38. data/lib/aidp/execute/checkpoint_display.rb +16 -0
  39. data/lib/aidp/execute/prompt_evaluator.rb +30 -57
  40. data/lib/aidp/execute/work_loop_runner.rb +294 -46
  41. data/lib/aidp/firewall/provider_requirements_collector.rb +7 -23
  42. data/lib/aidp/harness/agent_harness_provider_manager.rb +275 -0
  43. data/lib/aidp/harness/ai_decision_engine.rb +78 -296
  44. data/lib/aidp/harness/config_schema.rb +59 -0
  45. data/lib/aidp/harness/configuration.rb +229 -10
  46. data/lib/aidp/harness/deprecation_cache.rb +53 -11
  47. data/lib/aidp/harness/enhanced_runner.rb +1 -1
  48. data/lib/aidp/harness/error_handler.rb +11 -5
  49. data/lib/aidp/harness/provider_config.rb +36 -0
  50. data/lib/aidp/harness/provider_factory.rb +26 -24
  51. data/lib/aidp/harness/provider_info.rb +9 -5
  52. data/lib/aidp/harness/provider_manager.rb +2 -2
  53. data/lib/aidp/harness/provider_metrics.rb +103 -1
  54. data/lib/aidp/harness/rspec_command_optimizer.rb +166 -0
  55. data/lib/aidp/harness/ruby_llm_registry.rb +97 -8
  56. data/lib/aidp/harness/runner.rb +7 -2
  57. data/lib/aidp/harness/state_manager.rb +1 -1
  58. data/lib/aidp/harness/test_runner.rb +167 -11
  59. data/lib/aidp/harness/thinking_depth_manager.rb +564 -5
  60. data/lib/aidp/harness/ui.rb +11 -0
  61. data/lib/aidp/harness/usage_limit.rb +179 -0
  62. data/lib/aidp/harness/usage_limit_enforcer.rb +240 -0
  63. data/lib/aidp/harness/usage_limit_tracker.rb +308 -0
  64. data/lib/aidp/harness/usage_period.rb +234 -0
  65. data/lib/aidp/init/agent_instructions_generator.rb +317 -0
  66. data/lib/aidp/init/runner.rb +25 -4
  67. data/lib/aidp/interfaces/activity_monitor_interface.rb +234 -0
  68. data/lib/aidp/interfaces/binary_checker_interface.rb +232 -0
  69. data/lib/aidp/interfaces/command_executor_interface.rb +216 -0
  70. data/lib/aidp/interfaces/logger_interface.rb +150 -0
  71. data/lib/aidp/interfaces/ui_interface.rb +254 -0
  72. data/lib/aidp/logger.rb +25 -7
  73. data/lib/aidp/orchestration_adapter.rb +279 -0
  74. data/lib/aidp/pr_worktree_manager.rb +4 -15
  75. data/lib/aidp/prompts/feedback_collector.rb +198 -0
  76. data/lib/aidp/prompts/prompt_template_manager.rb +415 -0
  77. data/lib/aidp/prompts/template_evolver.rb +305 -0
  78. data/lib/aidp/prompts/template_version_manager.rb +325 -0
  79. data/lib/aidp/setup/in_memory_config_adapter.rb +257 -0
  80. data/lib/aidp/setup/in_memory_config_manager.rb +262 -0
  81. data/lib/aidp/setup/wizard.rb +616 -176
  82. data/lib/aidp/shell_executor.rb +58 -0
  83. data/lib/aidp/style_guide/selector.rb +56 -1
  84. data/lib/aidp/temporal/activities/analyze_issue_activity.rb +198 -0
  85. data/lib/aidp/temporal/activities/analyze_sub_task_activity.rb +163 -0
  86. data/lib/aidp/temporal/activities/base_activity.rb +78 -0
  87. data/lib/aidp/temporal/activities/create_plan_activity.rb +115 -0
  88. data/lib/aidp/temporal/activities/create_pr_activity.rb +193 -0
  89. data/lib/aidp/temporal/activities/create_prompt_activity.rb +118 -0
  90. data/lib/aidp/temporal/activities/diagnose_failure_activity.rb +228 -0
  91. data/lib/aidp/temporal/activities/prepare_next_iteration_activity.rb +147 -0
  92. data/lib/aidp/temporal/activities/record_checkpoint_activity.rb +81 -0
  93. data/lib/aidp/temporal/activities/run_agent_activity.rb +139 -0
  94. data/lib/aidp/temporal/activities/run_tests_activity.rb +114 -0
  95. data/lib/aidp/temporal/activities/run_work_loop_iteration_activity.rb +249 -0
  96. data/lib/aidp/temporal/configuration.rb +169 -0
  97. data/lib/aidp/temporal/connection.rb +93 -0
  98. data/lib/aidp/temporal/worker.rb +119 -0
  99. data/lib/aidp/temporal/workflow_client.rb +171 -0
  100. data/lib/aidp/temporal/workflows/base_workflow.rb +73 -0
  101. data/lib/aidp/temporal/workflows/issue_to_pr_workflow.rb +278 -0
  102. data/lib/aidp/temporal/workflows/sub_issue_workflow.rb +253 -0
  103. data/lib/aidp/temporal/workflows/work_loop_workflow.rb +321 -0
  104. data/lib/aidp/temporal.rb +120 -0
  105. data/lib/aidp/version.rb +1 -1
  106. data/lib/aidp/watch/base_processor.rb +35 -0
  107. data/lib/aidp/watch/build_processor.rb +132 -0
  108. data/lib/aidp/watch/github_state_extractor.rb +0 -8
  109. data/lib/aidp/watch/rebase_processor.rb +245 -0
  110. data/lib/aidp/watch/round_robin_scheduler.rb +150 -0
  111. data/lib/aidp/watch/runner.rb +366 -285
  112. data/lib/aidp/watch/state_store.rb +80 -0
  113. data/lib/aidp/watch/work_item.rb +96 -0
  114. data/lib/aidp/watch/worktree_cleanup_job.rb +226 -0
  115. data/lib/aidp/watch/worktree_reconciler.rb +643 -0
  116. data/lib/aidp/workflows/guided_agent.rb +1 -1
  117. data/lib/aidp/worktree.rb +62 -0
  118. data/templates/aidp.yml.example +70 -0
  119. metadata +118 -16
  120. data/lib/aidp/analyze/json_file_storage.rb +0 -292
  121. data/lib/aidp/execute/future_work_backlog.rb +0 -411
  122. data/lib/aidp/providers/adapter.rb +0 -239
  123. data/lib/aidp/providers/aider.rb +0 -264
  124. data/lib/aidp/providers/anthropic.rb +0 -614
  125. data/lib/aidp/providers/base.rb +0 -617
  126. data/lib/aidp/providers/capability_registry.rb +0 -204
  127. data/lib/aidp/providers/codex.rb +0 -267
  128. data/lib/aidp/providers/cursor.rb +0 -304
  129. data/lib/aidp/providers/error_taxonomy.rb +0 -195
  130. data/lib/aidp/providers/gemini.rb +0 -110
  131. data/lib/aidp/providers/github_copilot.rb +0 -227
  132. data/lib/aidp/providers/kilocode.rb +0 -157
  133. data/lib/aidp/providers/opencode.rb +0 -139
@@ -0,0 +1,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
@@ -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📦 Generated devcontainer:", type: :info)
82
- devcontainer_files.each { |file| display_message(" - #{file}", type: :success) }
83
+ display_message("\n Generated devcontainer:", type: :info)
84
+ devcontainer_files.each { |file| display_message(" - #{file}", type: :success) }
83
85
  end
84
86
 
85
- display_message("\n✅ aidp init complete.", type: :success)
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