aidp 0.26.0 → 0.28.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/checkpoint_command.rb +198 -0
  4. data/lib/aidp/cli/config_command.rb +71 -0
  5. data/lib/aidp/cli/enhanced_input.rb +2 -0
  6. data/lib/aidp/cli/first_run_wizard.rb +8 -7
  7. data/lib/aidp/cli/harness_command.rb +102 -0
  8. data/lib/aidp/cli/jobs_command.rb +3 -3
  9. data/lib/aidp/cli/mcp_dashboard.rb +4 -3
  10. data/lib/aidp/cli/models_command.rb +661 -0
  11. data/lib/aidp/cli/providers_command.rb +223 -0
  12. data/lib/aidp/cli.rb +45 -464
  13. data/lib/aidp/config.rb +54 -0
  14. data/lib/aidp/daemon/runner.rb +2 -2
  15. data/lib/aidp/debug_mixin.rb +25 -10
  16. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  17. data/lib/aidp/execute/async_work_loop_runner.rb +2 -1
  18. data/lib/aidp/execute/checkpoint_display.rb +38 -37
  19. data/lib/aidp/execute/interactive_repl.rb +2 -1
  20. data/lib/aidp/execute/prompt_manager.rb +4 -4
  21. data/lib/aidp/execute/repl_macros.rb +2 -2
  22. data/lib/aidp/execute/steps.rb +94 -1
  23. data/lib/aidp/execute/work_loop_runner.rb +238 -19
  24. data/lib/aidp/execute/workflow_selector.rb +4 -27
  25. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  26. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  27. data/lib/aidp/harness/config_manager.rb +5 -10
  28. data/lib/aidp/harness/config_schema.rb +8 -0
  29. data/lib/aidp/harness/configuration.rb +40 -2
  30. data/lib/aidp/harness/enhanced_runner.rb +25 -19
  31. data/lib/aidp/harness/error_handler.rb +23 -73
  32. data/lib/aidp/harness/model_cache.rb +269 -0
  33. data/lib/aidp/harness/model_discovery_service.rb +259 -0
  34. data/lib/aidp/harness/model_registry.rb +201 -0
  35. data/lib/aidp/harness/provider_factory.rb +11 -2
  36. data/lib/aidp/harness/runner.rb +5 -0
  37. data/lib/aidp/harness/state_manager.rb +0 -7
  38. data/lib/aidp/harness/thinking_depth_manager.rb +202 -7
  39. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  40. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  41. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  42. data/lib/aidp/harness/user_interface.rb +0 -58
  43. data/lib/aidp/init/runner.rb +7 -2
  44. data/lib/aidp/message_display.rb +0 -46
  45. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  46. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  47. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  48. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  49. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  50. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  51. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  52. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  53. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  54. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  55. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  56. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  57. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  58. data/lib/aidp/provider_manager.rb +8 -32
  59. data/lib/aidp/providers/adapter.rb +2 -4
  60. data/lib/aidp/providers/aider.rb +264 -0
  61. data/lib/aidp/providers/anthropic.rb +206 -121
  62. data/lib/aidp/providers/base.rb +123 -3
  63. data/lib/aidp/providers/capability_registry.rb +0 -1
  64. data/lib/aidp/providers/codex.rb +75 -70
  65. data/lib/aidp/providers/cursor.rb +87 -59
  66. data/lib/aidp/providers/gemini.rb +57 -60
  67. data/lib/aidp/providers/github_copilot.rb +19 -66
  68. data/lib/aidp/providers/kilocode.rb +35 -80
  69. data/lib/aidp/providers/opencode.rb +35 -80
  70. data/lib/aidp/setup/wizard.rb +555 -8
  71. data/lib/aidp/version.rb +1 -1
  72. data/lib/aidp/watch/build_processor.rb +211 -30
  73. data/lib/aidp/watch/change_request_processor.rb +128 -14
  74. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  75. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  76. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  77. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  78. data/lib/aidp/watch/plan_generator.rb +95 -52
  79. data/lib/aidp/watch/plan_processor.rb +7 -6
  80. data/lib/aidp/watch/repository_client.rb +245 -17
  81. data/lib/aidp/watch/review_processor.rb +100 -19
  82. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  83. data/lib/aidp/watch/runner.rb +181 -29
  84. data/lib/aidp/watch/state_store.rb +22 -1
  85. data/lib/aidp/workflows/definitions.rb +147 -0
  86. data/lib/aidp/workflows/guided_agent.rb +3 -3
  87. data/lib/aidp/workstream_cleanup.rb +245 -0
  88. data/lib/aidp/worktree.rb +19 -0
  89. data/templates/aidp-development.yml.example +2 -2
  90. data/templates/aidp-production.yml.example +3 -3
  91. data/templates/aidp.yml.example +57 -0
  92. data/templates/implementation/generate_tdd_specs.md +213 -0
  93. data/templates/implementation/iterative_implementation.md +122 -0
  94. data/templates/planning/agile/analyze_feedback.md +183 -0
  95. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  96. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  97. data/templates/planning/agile/generate_marketing_report.md +162 -0
  98. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  99. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  100. data/templates/planning/agile/ingest_feedback.md +174 -0
  101. data/templates/planning/assemble_project_plan.md +113 -0
  102. data/templates/planning/assign_personas.md +108 -0
  103. data/templates/planning/create_tasks.md +52 -6
  104. data/templates/planning/generate_gantt.md +86 -0
  105. data/templates/planning/generate_wbs.md +85 -0
  106. data/templates/planning/initialize_planning_mode.md +70 -0
  107. data/templates/skills/README.md +2 -2
  108. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  109. data/templates/skills/product_manager/SKILL.md +177 -0
  110. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  111. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  112. data/templates/skills/ux_researcher/SKILL.md +222 -0
  113. metadata +47 -1
@@ -8,6 +8,7 @@ require_relative "work_loop_unit_scheduler"
8
8
  require_relative "deterministic_unit"
9
9
  require_relative "agent_signal_parser"
10
10
  require_relative "../harness/test_runner"
11
+ require_relative "../errors"
11
12
 
12
13
  module Aidp
13
14
  module Execute
@@ -46,10 +47,11 @@ module Aidp
46
47
  @project_dir = project_dir
47
48
  @provider_manager = provider_manager
48
49
  @config = config
50
+ @prompt = options[:prompt] || TTY::Prompt.new
49
51
  @prompt_manager = PromptManager.new(project_dir, config: config)
50
52
  @test_runner = Aidp::Harness::TestRunner.new(project_dir, config)
51
53
  @checkpoint = Checkpoint.new(project_dir)
52
- @checkpoint_display = CheckpointDisplay.new
54
+ @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
53
55
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
54
56
  @persistent_tasklist = PersistentTasklist.new(project_dir)
55
57
  @iteration_count = 0
@@ -62,7 +64,7 @@ module Aidp
62
64
 
63
65
  # Initialize thinking depth manager for intelligent model selection
64
66
  require_relative "../harness/thinking_depth_manager"
65
- @thinking_depth_manager = Aidp::Harness::ThinkingDepthManager.new(config)
67
+ @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config)
66
68
  @consecutive_failures = 0
67
69
  @last_tier = nil
68
70
  end
@@ -160,6 +162,10 @@ module Aidp
160
162
  # Wrap agent call in exception handling for true fix-forward
161
163
  begin
162
164
  agent_result = apply_patch
165
+ rescue Aidp::Errors::ConfigurationError
166
+ # Configuration errors should crash immediately (crash-early principle)
167
+ # Re-raise without catching
168
+ raise
163
169
  rescue => e
164
170
  # Convert exception to error result for fix-forward handling
165
171
  Aidp.logger.error("work_loop", "Exception during agent call",
@@ -178,6 +184,27 @@ module Aidp
178
184
  next
179
185
  end
180
186
 
187
+ # Check for fatal configuration errors (crash early per LLM_STYLE_GUIDE)
188
+ if agent_result[:status] == "error" && agent_result[:message]&.include?("No model available")
189
+ tier = @thinking_depth_manager.current_tier
190
+ provider = @provider_manager.current_provider
191
+
192
+ error_msg = "No model configured for thinking tier '#{tier}'.\n\n" \
193
+ "Current provider: #{provider}\n" \
194
+ "Required tier: #{tier}\n\n" \
195
+ "To fix this, add a model to your aidp.yml:\n\n" \
196
+ "thinking_depth:\n" \
197
+ " tiers:\n" \
198
+ " #{tier}:\n" \
199
+ " models:\n" \
200
+ " - provider: #{provider}\n" \
201
+ " model: <model-name> # e.g., claude-3-5-sonnet-20241022\n\n" \
202
+ "Or run: aidp models list\n" \
203
+ "to see available models for your configured providers."
204
+
205
+ raise Aidp::Errors::ConfigurationError, error_msg
206
+ end
207
+
181
208
  # Process agent output for task filing signals
182
209
  process_task_filing(agent_result)
183
210
 
@@ -215,23 +242,39 @@ module Aidp
215
242
  build_results[:success] &&
216
243
  doc_results[:success]
217
244
 
245
+ # Check task completion status
246
+ task_completion_result = check_task_completion
247
+
218
248
  if all_checks_pass
219
249
  transition_to(:pass)
220
250
 
221
251
  if agent_marked_complete?(agent_result)
222
- transition_to(:done)
223
- record_final_checkpoint(all_results)
224
- display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
225
- display_state_summary
226
- archive_and_cleanup
227
-
228
- return build_agentic_payload(
229
- agent_result: agent_result,
230
- response: build_success_result(agent_result),
231
- summary: agent_result[:output],
232
- completed: true,
233
- terminate: true
234
- )
252
+ # Check if tasks are complete
253
+ if task_completion_result[:complete]
254
+ transition_to(:done)
255
+ record_final_checkpoint(all_results)
256
+ display_task_summary
257
+ display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
258
+ display_state_summary
259
+ archive_and_cleanup
260
+
261
+ return build_agentic_payload(
262
+ agent_result: agent_result,
263
+ response: build_success_result(agent_result),
264
+ summary: agent_result[:output],
265
+ completed: true,
266
+ terminate: true
267
+ )
268
+ else
269
+ # All checks passed but tasks not complete
270
+ display_message(" All checks passed but tasks not complete", type: :warning)
271
+ display_message(" #{task_completion_result[:message]}", type: :warning)
272
+ display_task_summary
273
+ transition_to(:next_patch)
274
+
275
+ # Append task completion requirement to PROMPT.md
276
+ append_task_requirement_to_prompt(task_completion_result[:message])
277
+ end
235
278
  else
236
279
  display_message(" All checks passed but work not marked complete", type: :info)
237
280
  transition_to(:next_patch)
@@ -742,10 +785,43 @@ module Aidp
742
785
  parts << "- After you finish, tests and linters will run automatically"
743
786
  parts << "- If tests/linters fail, you'll see the errors in the next iteration and can fix them"
744
787
  parts << ""
788
+
789
+ if @config.task_completion_required?
790
+ parts << "## Task Tracking (REQUIRED)"
791
+ parts << "**CRITICAL**: This work loop requires task tracking for completion."
792
+ parts << ""
793
+ parts << "You must:"
794
+ parts << "1. Create at least one task for this session using: `File task: \"description\"`"
795
+ parts << "2. Track all work items as tasks"
796
+ parts << "3. Update task status as you progress"
797
+ parts << "4. All tasks must be DONE or ABANDONED (with reason) before completion"
798
+ parts << "5. **IMPORTANT**: When you write STATUS: COMPLETE, also mark all your tasks as done!"
799
+ parts << ""
800
+ parts << "**Important**: Tasks in the list exist due to careful planning and requirements analysis."
801
+ parts << "Do NOT abandon tasks due to perceived complexity or scope concerns - these factors were"
802
+ parts << "considered during planning. Only abandon tasks when truly obsolete (requirements changed,"
803
+ parts << "duplicate work, external blockers). When in doubt, mark in_progress and implement."
804
+ parts << ""
805
+ parts << "Task filing examples:"
806
+ parts << "- `File task: \"Implement user authentication\" priority: high tags: security,auth`"
807
+ parts << "- `File task: \"Add tests for login flow\" priority: medium tags: testing`"
808
+ parts << "- `File task: \"Update documentation\" priority: low tags: docs`"
809
+ parts << ""
810
+ parts << "Task status update examples:"
811
+ parts << "- `Update task: task_123_abc status: in_progress`"
812
+ parts << "- `Update task: task_456_def status: done`"
813
+ parts << "- `Update task: task_789_ghi status: abandoned reason: \"Requirements changed\"`"
814
+ parts << ""
815
+ end
816
+
745
817
  parts << "## Completion Criteria"
746
- parts << "Mark this step COMPLETE by adding this line to PROMPT.md:"
818
+ parts << "Mark this step COMPLETE by adding these lines to PROMPT.md:"
747
819
  parts << "```"
748
820
  parts << "STATUS: COMPLETE"
821
+ if @config.task_completion_required?
822
+ parts << ""
823
+ parts << "Update task: task_xxx_yyy status: done # Mark ALL your tasks as done"
824
+ end
749
825
  parts << "```"
750
826
  parts << ""
751
827
  parts.join("\n")
@@ -1092,13 +1168,12 @@ module Aidp
1092
1168
  display_message("")
1093
1169
  end
1094
1170
 
1095
- # Process agent output for task filing signals
1171
+ # Process agent output for task filing signals and task status updates
1096
1172
  def process_task_filing(agent_result)
1097
1173
  return unless agent_result && agent_result[:output]
1098
1174
 
1175
+ # Process new task filings
1099
1176
  filed_tasks = AgentSignalParser.parse_task_filing(agent_result[:output])
1100
- return if filed_tasks.empty?
1101
-
1102
1177
  filed_tasks.each do |task_data|
1103
1178
  task = @persistent_tasklist.create(
1104
1179
  task_data[:description],
@@ -1111,6 +1186,150 @@ module Aidp
1111
1186
  Aidp.log_info("tasklist", "Filed new task from agent", task_id: task.id, description: task.description)
1112
1187
  display_message("📋 Filed task: #{task.description} (#{task.id})", type: :info)
1113
1188
  end
1189
+
1190
+ # Process task status updates
1191
+ status_updates = AgentSignalParser.parse_task_status_updates(agent_result[:output])
1192
+ status_updates.each do |update_data|
1193
+ task = @persistent_tasklist.update_status(
1194
+ update_data[:task_id],
1195
+ update_data[:status],
1196
+ reason: update_data[:reason]
1197
+ )
1198
+
1199
+ status_icon = case update_data[:status]
1200
+ when :done then "✅"
1201
+ when :abandoned then "❌"
1202
+ when :in_progress then "🚧"
1203
+ when :pending then "⏳"
1204
+ else "📋"
1205
+ end
1206
+
1207
+ Aidp.log_info("tasklist", "Updated task status from agent",
1208
+ task_id: task.id,
1209
+ old_status: task.status,
1210
+ new_status: update_data[:status])
1211
+ display_message("#{status_icon} Updated task #{task.id}: #{update_data[:status]}", type: :info)
1212
+ rescue PersistentTasklist::TaskNotFoundError
1213
+ Aidp.log_warn("tasklist", "Task not found for status update", task_id: update_data[:task_id])
1214
+ display_message("⚠️ Task not found: #{update_data[:task_id]}", type: :warning)
1215
+ end
1216
+ end
1217
+
1218
+ # Check if tasks are required and all are completed or abandoned
1219
+ # Returns {complete: boolean, message: string}
1220
+ # Note: Tasks are project-scoped, not session-scoped. This allows tasks created
1221
+ # in planning phases to be completed in build phases.
1222
+ def check_task_completion
1223
+ return {complete: true, message: nil} unless @config.task_completion_required?
1224
+
1225
+ all_tasks = @persistent_tasklist.all
1226
+
1227
+ # If no tasks exist yet, allow completion - agent can work without tasks initially
1228
+ # This supports workflows where no planning phase created tasks
1229
+ if all_tasks.empty?
1230
+ return {complete: true, message: nil}
1231
+ end
1232
+
1233
+ # Count tasks by status
1234
+ pending_tasks = all_tasks.select { |t| t.status == :pending }
1235
+ in_progress_tasks = all_tasks.select { |t| t.status == :in_progress }
1236
+ abandoned_tasks = all_tasks.select { |t| t.status == :abandoned }
1237
+ all_tasks.select { |t| t.status == :done }
1238
+
1239
+ # If tasks exist, all must be done or abandoned before completion
1240
+ incomplete_tasks = pending_tasks + in_progress_tasks
1241
+
1242
+ if incomplete_tasks.any?
1243
+ task_list = incomplete_tasks.map { |t| "- #{t.description} (#{t.status}, session: #{t.session})" }.join("\n")
1244
+ return {
1245
+ complete: false,
1246
+ message: "Tasks remain incomplete:\n#{task_list}\n\nComplete all tasks or abandon them with reason before marking work complete."
1247
+ }
1248
+ end
1249
+
1250
+ # If there are abandoned tasks, confirm with user
1251
+ if abandoned_tasks.any? && !all_abandoned_tasks_confirmed?(abandoned_tasks)
1252
+ return {
1253
+ complete: false,
1254
+ message: "Abandoned tasks require user confirmation. Please confirm abandoned tasks."
1255
+ }
1256
+ end
1257
+
1258
+ {complete: true, message: nil}
1259
+ end
1260
+
1261
+ # Check if all abandoned tasks have been confirmed
1262
+ def all_abandoned_tasks_confirmed?(abandoned_tasks)
1263
+ # For now, we'll consider all abandoned tasks as confirmed if they have a reason
1264
+ # In a future enhancement, this could prompt the user for confirmation
1265
+ abandoned_tasks.all? { |t| t.abandoned_reason && !t.abandoned_reason.strip.empty? }
1266
+ end
1267
+
1268
+ # Display task completion summary
1269
+ def display_task_summary
1270
+ return unless @config.task_completion_required?
1271
+
1272
+ all_tasks = @persistent_tasklist.all
1273
+ return if all_tasks.empty?
1274
+
1275
+ counts = all_tasks.group_by(&:status).transform_values(&:count)
1276
+
1277
+ display_message("\n📋 Task Summary (Project-wide):", type: :info)
1278
+ display_message(" Total: #{all_tasks.size}", type: :info)
1279
+ display_message(" ✅ Done: #{counts[:done] || 0}", type: :success) if counts[:done]
1280
+ display_message(" 🚧 In Progress: #{counts[:in_progress] || 0}", type: :warning) if counts[:in_progress]
1281
+ display_message(" ⏳ Pending: #{counts[:pending] || 0}", type: :warning) if counts[:pending]
1282
+ display_message(" ❌ Abandoned: #{counts[:abandoned] || 0}", type: :error) if counts[:abandoned]
1283
+ display_message("")
1284
+ end
1285
+
1286
+ # Append task completion requirement to PROMPT.md
1287
+ def append_task_requirement_to_prompt(message)
1288
+ task_requirement = []
1289
+
1290
+ task_requirement << "## Task Completion Requirement"
1291
+ task_requirement << ""
1292
+ task_requirement << "**CRITICAL**: #{message}"
1293
+ task_requirement << ""
1294
+ task_requirement << "### How to Complete Tasks"
1295
+ task_requirement << ""
1296
+ task_requirement << "Update task status using these signals in your output:"
1297
+ task_requirement << ""
1298
+ task_requirement << "**Creating tasks:**"
1299
+ task_requirement << "```"
1300
+ task_requirement << 'File task: "Implement feature X" priority: high tags: feature,backend'
1301
+ task_requirement << 'File task: "Add tests for feature X" priority: medium tags: testing'
1302
+ task_requirement << "```"
1303
+ task_requirement << ""
1304
+ task_requirement << "**Updating task status:**"
1305
+ task_requirement << "```"
1306
+ task_requirement << "Update task: task_123_abc status: in_progress"
1307
+ task_requirement << "Update task: task_123_abc status: done"
1308
+ task_requirement << 'Update task: task_456_def status: abandoned reason: "Requirements changed"'
1309
+ task_requirement << "```"
1310
+ task_requirement << ""
1311
+ task_requirement << "**Task states:**"
1312
+ task_requirement << "- ⏳ **pending** - Not started yet"
1313
+ task_requirement << "- 🚧 **in_progress** - Currently working on it"
1314
+ task_requirement << "- ✅ **done** - Completed successfully"
1315
+ task_requirement << "- ❌ **abandoned** - Not doing this (requires reason)"
1316
+ task_requirement << ""
1317
+ task_requirement << "**Completion requirement:**"
1318
+ task_requirement << "All tasks for this session must be marked as DONE or ABANDONED (with reason) before the work loop can complete."
1319
+ task_requirement << ""
1320
+ task_requirement << "**Action Required**: Review the current task list and update status for all tasks."
1321
+ task_requirement << ""
1322
+
1323
+ # Append to PROMPT.md - ensure directory exists
1324
+ begin
1325
+ current_prompt = @prompt_manager.read || ""
1326
+ updated_prompt = current_prompt + "\n\n---\n\n" + task_requirement.join("\n")
1327
+ @prompt_manager.write(updated_prompt, step_name: @step_name)
1328
+ display_message(" [TASK_REQ] Added task completion requirement to PROMPT.md", type: :warning)
1329
+ rescue => e
1330
+ Aidp.log_warn("work_loop", "Failed to append task requirement to PROMPT.md", error: e.message)
1331
+ display_message(" [TASK_REQ] Warning: Could not update PROMPT.md: #{e.message}", type: :warning)
1332
+ end
1114
1333
  end
1115
1334
 
1116
1335
  # Validate changes against guard policy
@@ -10,23 +10,20 @@ module Aidp
10
10
  class WorkflowSelector
11
11
  include Aidp::MessageDisplay
12
12
 
13
- def initialize(prompt: TTY::Prompt.new)
13
+ def initialize(prompt: TTY::Prompt.new, workflow_selector: nil)
14
14
  @user_input = {}
15
15
  @prompt = prompt
16
- @workflow_selector = Aidp::Workflows::Selector.new(prompt: @prompt)
16
+ @workflow_selector = workflow_selector || Aidp::Workflows::Selector.new(prompt: @prompt)
17
17
  end
18
18
 
19
19
  # Main entry point for interactive workflow selection
20
- def select_workflow(harness_mode: false, use_new_selector: true, mode: nil)
20
+ def select_workflow(harness_mode: false, mode: nil)
21
21
  if harness_mode
22
22
  # In harness mode, use default values to avoid blocking
23
23
  select_workflow_with_defaults
24
- elsif use_new_selector
24
+ else
25
25
  # Use new unified workflow selector (default as of Issue #79)
26
26
  select_workflow_with_new_selector(mode)
27
- else
28
- # Legacy interactive mode for backward compatibility
29
- select_workflow_interactive
30
27
  end
31
28
  end
32
29
 
@@ -48,26 +45,6 @@ module Aidp
48
45
  }
49
46
  end
50
47
 
51
- def select_workflow_interactive
52
- display_message("\n🚀 Welcome to AI Dev Pipeline!", type: :highlight)
53
- display_message("Let's set up your development workflow.\n\n")
54
-
55
- # Step 1: Collect project information
56
- collect_project_info
57
-
58
- # Step 2: Choose workflow type
59
- workflow_type = choose_workflow_type
60
-
61
- # Step 3: Generate workflow steps
62
- steps = generate_workflow_steps(workflow_type)
63
-
64
- {
65
- workflow_type: workflow_type,
66
- steps: steps,
67
- user_input: @user_input
68
- }
69
- end
70
-
71
48
  def select_workflow_with_defaults
72
49
  display_message("\n🚀 Starting harness with default workflow configuration...", type: :highlight)
73
50
 
@@ -0,0 +1,262 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+ require_relative "../providers/base"
6
+ require_relative "../providers/anthropic"
7
+ require_relative "../providers/cursor"
8
+ require_relative "../providers/github_copilot"
9
+ require_relative "../providers/gemini"
10
+ require_relative "../providers/kilocode"
11
+ require_relative "../providers/opencode"
12
+ require_relative "../providers/codex"
13
+ require_relative "../providers/aider"
14
+
15
+ module Aidp
16
+ module Firewall
17
+ # Collects firewall requirements from all provider classes
18
+ # and generates the firewall configuration YAML file
19
+ class ProviderRequirementsCollector
20
+ attr_reader :config_path
21
+
22
+ # Core infrastructure domains not tied to specific providers
23
+ CORE_DOMAINS = {
24
+ ruby: [
25
+ "rubygems.org",
26
+ "api.rubygems.org",
27
+ "index.rubygems.org"
28
+ ],
29
+ javascript: [
30
+ "registry.npmjs.org",
31
+ "registry.yarnpkg.com"
32
+ ],
33
+ github: [
34
+ "github.com",
35
+ "api.github.com",
36
+ "raw.githubusercontent.com",
37
+ "objects.githubusercontent.com",
38
+ "gist.githubusercontent.com",
39
+ "cloud.githubusercontent.com"
40
+ ],
41
+ cdn: [
42
+ "cdn.jsdelivr.net"
43
+ ],
44
+ vscode: [
45
+ "update.code.visualstudio.com",
46
+ "marketplace.visualstudio.com",
47
+ "vscode.blob.core.windows.net",
48
+ "vscode.download.prss.microsoft.com",
49
+ "az764295.vo.msecnd.net",
50
+ "gallerycdn.vsassets.io",
51
+ "vscode.gallerycdn.vsassets.io",
52
+ "gallery.vsassets.io",
53
+ "vscode-sync.trafficmanager.net",
54
+ "vscode.dev",
55
+ "go.microsoft.com",
56
+ "download.visualstudio.microsoft.com"
57
+ ],
58
+ telemetry: [
59
+ "dc.services.visualstudio.com",
60
+ "vortex.data.microsoft.com"
61
+ ]
62
+ }.freeze
63
+
64
+ # Static IP ranges (CIDR notation)
65
+ STATIC_IP_RANGES = [
66
+ {"cidr" => "140.82.112.0/20", "comment" => "GitHub main infrastructure (covers 140.82.112.0 - 140.82.127.255)"},
67
+ {"cidr" => "127.0.0.0/8", "comment" => "Localhost loopback"}
68
+ ].freeze
69
+
70
+ # Azure IP ranges for GitHub Copilot and VS Code services
71
+ # These use broader /16 ranges to handle dynamic IP allocation across Azure regions
72
+ AZURE_IP_RANGES = [
73
+ {"cidr" => "20.189.0.0/16", "comment" => "Azure WestUS2 (GitHub Copilot - broad range due to dynamic IP allocation)"},
74
+ {"cidr" => "104.208.0.0/16", "comment" => "Azure EastUS (GitHub Copilot - broad range due to dynamic IP allocation)"},
75
+ {"cidr" => "52.168.0.0/16", "comment" => "Azure EastUS2 (GitHub Copilot - covers .112 and .117, broad range)"},
76
+ {"cidr" => "40.79.0.0/16", "comment" => "Azure WestUS (GitHub Copilot - broad range due to dynamic IP allocation)"},
77
+ {"cidr" => "13.89.0.0/16", "comment" => "Azure EastUS (GitHub Copilot - broad range due to dynamic IP allocation)"},
78
+ {"cidr" => "13.69.0.0/16", "comment" => "Azure (covers .239, broad range due to dynamic IP allocation)"},
79
+ {"cidr" => "13.66.0.0/16", "comment" => "Azure WestUS2 (VS Code sync service - broad range)"},
80
+ {"cidr" => "20.42.0.0/16", "comment" => "Azure WestEurope (covers .65 and .73, broad range)"},
81
+ {"cidr" => "20.50.0.0/16", "comment" => "Azure (covers .80, broad range due to dynamic IP allocation)"}
82
+ ].freeze
83
+
84
+ # Dynamic IP sources configuration
85
+ DYNAMIC_SOURCES = {
86
+ github_meta_api: {
87
+ url: "https://api.github.com/meta",
88
+ fields: ["git"],
89
+ comment: "GitHub Git protocol IP ranges (dynamically fetched)"
90
+ }
91
+ }.freeze
92
+
93
+ def initialize(config_path: nil)
94
+ @config_path = config_path || default_config_path
95
+ end
96
+
97
+ # Collect firewall requirements from all providers
98
+ #
99
+ # @return [Hash] Hash with provider names as keys and requirements as values
100
+ def collect_requirements
101
+ Aidp.log_debug("firewall_collector", "collecting requirements", provider_count: provider_classes.size)
102
+
103
+ requirements = {}
104
+ provider_classes.each do |provider_class|
105
+ provider_name = extract_provider_name(provider_class)
106
+ reqs = provider_class.firewall_requirements
107
+
108
+ requirements[provider_name] = {
109
+ domains: reqs[:domains] || [],
110
+ ip_ranges: reqs[:ip_ranges] || []
111
+ }
112
+
113
+ Aidp.log_debug(
114
+ "firewall_collector",
115
+ "collected requirements",
116
+ provider: provider_name,
117
+ domains: reqs[:domains]&.size || 0,
118
+ ip_ranges: reqs[:ip_ranges]&.size || 0
119
+ )
120
+ end
121
+
122
+ requirements
123
+ end
124
+
125
+ # Deduplicate and merge provider requirements
126
+ #
127
+ # @param requirements [Hash] Provider requirements hash
128
+ # @return [Hash] Deduplicated requirements with all_domains and all_ip_ranges
129
+ def deduplicate(requirements)
130
+ all_domains = []
131
+ all_ip_ranges = []
132
+
133
+ requirements.each_value do |reqs|
134
+ all_domains.concat(reqs[:domains])
135
+ all_ip_ranges.concat(reqs[:ip_ranges])
136
+ end
137
+
138
+ {
139
+ all_domains: all_domains.uniq.sort,
140
+ all_ip_ranges: all_ip_ranges.uniq.sort,
141
+ by_provider: requirements
142
+ }
143
+ end
144
+
145
+ # Generate the complete YAML configuration file
146
+ #
147
+ # Creates a new YAML file with core infrastructure and provider requirements
148
+ #
149
+ # @param dry_run [Boolean] If true, only log what would be generated
150
+ # @return [Boolean] True if generation was successful
151
+ def generate_yaml_config(dry_run: false)
152
+ Aidp.log_info("firewall_collector", "generating config", path: @config_path, dry_run: dry_run)
153
+
154
+ # Collect provider requirements
155
+ provider_requirements = collect_requirements
156
+ deduplicated = deduplicate(provider_requirements)
157
+
158
+ # Build complete configuration
159
+ config = {
160
+ "version" => 1,
161
+ "static_ip_ranges" => STATIC_IP_RANGES,
162
+ "azure_ip_ranges" => AZURE_IP_RANGES,
163
+ "core_domains" => CORE_DOMAINS.transform_keys(&:to_s),
164
+ "provider_domains" => provider_requirements.transform_values { |reqs| reqs[:domains] },
165
+ "dynamic_sources" => DYNAMIC_SOURCES.transform_keys(&:to_s).transform_values do |v|
166
+ v.transform_keys(&:to_s)
167
+ end
168
+ }
169
+
170
+ if dry_run
171
+ Aidp.log_info("firewall_collector", "dry run - would generate", domains: deduplicated[:all_domains].size)
172
+ puts "Would generate YAML with:"
173
+ puts " - #{STATIC_IP_RANGES.size} static IP ranges"
174
+ puts " - #{AZURE_IP_RANGES.size} Azure IP ranges"
175
+ puts " - #{CORE_DOMAINS.values.flatten.size} core domains"
176
+ puts " - #{deduplicated[:all_domains].size} provider domains from #{provider_requirements.size} providers"
177
+ return true
178
+ end
179
+
180
+ # Ensure directory exists
181
+ FileUtils.mkdir_p(File.dirname(@config_path))
182
+
183
+ # Write configuration
184
+ File.write(@config_path, YAML.dump(config))
185
+ Aidp.log_info(
186
+ "firewall_collector",
187
+ "config generated",
188
+ path: @config_path,
189
+ providers: provider_requirements.size,
190
+ total_domains: CORE_DOMAINS.values.flatten.size + deduplicated[:all_domains].size
191
+ )
192
+
193
+ true
194
+ rescue => e
195
+ Aidp.log_error("firewall_collector", "generation failed", error: e.message)
196
+ false
197
+ end
198
+
199
+ # Alias for backward compatibility
200
+ alias_method :update_yaml_config, :generate_yaml_config
201
+
202
+ # Generate a summary report of provider requirements
203
+ #
204
+ # @return [String] Formatted summary report
205
+ def generate_report
206
+ requirements = collect_requirements
207
+ deduplicated = deduplicate(requirements)
208
+
209
+ report = []
210
+ report << "Firewall Provider Requirements Summary"
211
+ report << "=" * 50
212
+ report << ""
213
+ report << "Total Providers: #{requirements.size}"
214
+ report << "Total Unique Domains: #{deduplicated[:all_domains].size}"
215
+ report << "Total Unique IP Ranges: #{deduplicated[:all_ip_ranges].size}"
216
+ report << ""
217
+ report << "By Provider:"
218
+ report << "-" * 50
219
+
220
+ requirements.each do |provider, reqs|
221
+ report << ""
222
+ report << "#{provider.capitalize}:"
223
+ report << " Domains (#{reqs[:domains].size}):"
224
+ reqs[:domains].each { |d| report << " - #{d}" }
225
+ if reqs[:ip_ranges].any?
226
+ report << " IP Ranges (#{reqs[:ip_ranges].size}):"
227
+ reqs[:ip_ranges].each { |ip| report << " - #{ip}" }
228
+ end
229
+ end
230
+
231
+ report.join("\n")
232
+ end
233
+
234
+ private
235
+
236
+ # Get list of all provider classes
237
+ def provider_classes
238
+ [
239
+ Aidp::Providers::Anthropic,
240
+ Aidp::Providers::Cursor,
241
+ Aidp::Providers::GithubCopilot,
242
+ Aidp::Providers::Gemini,
243
+ Aidp::Providers::Kilocode,
244
+ Aidp::Providers::Opencode,
245
+ Aidp::Providers::Codex,
246
+ Aidp::Providers::Aider
247
+ ]
248
+ end
249
+
250
+ # Extract provider name from class name
251
+ def extract_provider_name(provider_class)
252
+ # Convert Aidp::Providers::GithubCopilot to "github_copilot"
253
+ provider_class.name.split("::").last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
254
+ end
255
+
256
+ # Default path to firewall config file
257
+ def default_config_path
258
+ File.join(Dir.pwd, ".aidp", "firewall-allowlist.yml")
259
+ end
260
+ end
261
+ end
262
+ end