aidp 0.27.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +89 -0
  3. data/lib/aidp/cli/models_command.rb +5 -6
  4. data/lib/aidp/cli.rb +10 -8
  5. data/lib/aidp/config.rb +54 -0
  6. data/lib/aidp/debug_mixin.rb +23 -1
  7. data/lib/aidp/execute/agent_signal_parser.rb +22 -0
  8. data/lib/aidp/execute/repl_macros.rb +2 -2
  9. data/lib/aidp/execute/steps.rb +94 -1
  10. data/lib/aidp/execute/work_loop_runner.rb +209 -17
  11. data/lib/aidp/execute/workflow_selector.rb +2 -25
  12. data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
  13. data/lib/aidp/harness/ai_decision_engine.rb +35 -2
  14. data/lib/aidp/harness/config_manager.rb +0 -5
  15. data/lib/aidp/harness/config_schema.rb +8 -0
  16. data/lib/aidp/harness/configuration.rb +27 -19
  17. data/lib/aidp/harness/enhanced_runner.rb +1 -4
  18. data/lib/aidp/harness/error_handler.rb +1 -72
  19. data/lib/aidp/harness/provider_factory.rb +11 -2
  20. data/lib/aidp/harness/state_manager.rb +0 -7
  21. data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
  22. data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
  23. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
  24. data/lib/aidp/harness/ui/progress_display.rb +6 -2
  25. data/lib/aidp/harness/user_interface.rb +0 -58
  26. data/lib/aidp/init/runner.rb +7 -2
  27. data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
  28. data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
  29. data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
  30. data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
  31. data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
  32. data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
  33. data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
  34. data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
  35. data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
  36. data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
  37. data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
  38. data/lib/aidp/planning/parsers/document_parser.rb +141 -0
  39. data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
  40. data/lib/aidp/provider_manager.rb +8 -32
  41. data/lib/aidp/providers/aider.rb +264 -0
  42. data/lib/aidp/providers/anthropic.rb +74 -2
  43. data/lib/aidp/providers/base.rb +25 -1
  44. data/lib/aidp/providers/codex.rb +26 -3
  45. data/lib/aidp/providers/cursor.rb +16 -0
  46. data/lib/aidp/providers/gemini.rb +13 -0
  47. data/lib/aidp/providers/github_copilot.rb +17 -0
  48. data/lib/aidp/providers/kilocode.rb +11 -0
  49. data/lib/aidp/providers/opencode.rb +11 -0
  50. data/lib/aidp/setup/wizard.rb +249 -39
  51. data/lib/aidp/version.rb +1 -1
  52. data/lib/aidp/watch/build_processor.rb +211 -30
  53. data/lib/aidp/watch/change_request_processor.rb +128 -14
  54. data/lib/aidp/watch/ci_fix_processor.rb +103 -37
  55. data/lib/aidp/watch/ci_log_extractor.rb +258 -0
  56. data/lib/aidp/watch/github_state_extractor.rb +177 -0
  57. data/lib/aidp/watch/implementation_verifier.rb +284 -0
  58. data/lib/aidp/watch/plan_generator.rb +7 -43
  59. data/lib/aidp/watch/plan_processor.rb +7 -6
  60. data/lib/aidp/watch/repository_client.rb +245 -17
  61. data/lib/aidp/watch/review_processor.rb +98 -17
  62. data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
  63. data/lib/aidp/watch/runner.rb +181 -29
  64. data/lib/aidp/watch/state_store.rb +22 -1
  65. data/lib/aidp/workflows/definitions.rb +147 -0
  66. data/lib/aidp/workstream_cleanup.rb +245 -0
  67. data/lib/aidp/worktree.rb +19 -0
  68. data/templates/aidp.yml.example +57 -0
  69. data/templates/implementation/generate_tdd_specs.md +213 -0
  70. data/templates/implementation/iterative_implementation.md +122 -0
  71. data/templates/planning/agile/analyze_feedback.md +183 -0
  72. data/templates/planning/agile/generate_iteration_plan.md +179 -0
  73. data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
  74. data/templates/planning/agile/generate_marketing_report.md +162 -0
  75. data/templates/planning/agile/generate_mvp_scope.md +127 -0
  76. data/templates/planning/agile/generate_user_test_plan.md +143 -0
  77. data/templates/planning/agile/ingest_feedback.md +174 -0
  78. data/templates/planning/assemble_project_plan.md +113 -0
  79. data/templates/planning/assign_personas.md +108 -0
  80. data/templates/planning/create_tasks.md +52 -6
  81. data/templates/planning/generate_gantt.md +86 -0
  82. data/templates/planning/generate_wbs.md +85 -0
  83. data/templates/planning/initialize_planning_mode.md +70 -0
  84. data/templates/skills/README.md +2 -2
  85. data/templates/skills/marketing_strategist/SKILL.md +279 -0
  86. data/templates/skills/product_manager/SKILL.md +177 -0
  87. data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
  88. data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
  89. data/templates/skills/ux_researcher/SKILL.md +222 -0
  90. metadata +39 -1
@@ -242,23 +242,39 @@ module Aidp
242
242
  build_results[:success] &&
243
243
  doc_results[:success]
244
244
 
245
+ # Check task completion status
246
+ task_completion_result = check_task_completion
247
+
245
248
  if all_checks_pass
246
249
  transition_to(:pass)
247
250
 
248
251
  if agent_marked_complete?(agent_result)
249
- transition_to(:done)
250
- record_final_checkpoint(all_results)
251
- display_message("✅ Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
252
- display_state_summary
253
- archive_and_cleanup
254
-
255
- return build_agentic_payload(
256
- agent_result: agent_result,
257
- response: build_success_result(agent_result),
258
- summary: agent_result[:output],
259
- completed: true,
260
- terminate: true
261
- )
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
262
278
  else
263
279
  display_message(" All checks passed but work not marked complete", type: :info)
264
280
  transition_to(:next_patch)
@@ -769,10 +785,43 @@ module Aidp
769
785
  parts << "- After you finish, tests and linters will run automatically"
770
786
  parts << "- If tests/linters fail, you'll see the errors in the next iteration and can fix them"
771
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
+
772
817
  parts << "## Completion Criteria"
773
- 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:"
774
819
  parts << "```"
775
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
776
825
  parts << "```"
777
826
  parts << ""
778
827
  parts.join("\n")
@@ -1119,13 +1168,12 @@ module Aidp
1119
1168
  display_message("")
1120
1169
  end
1121
1170
 
1122
- # Process agent output for task filing signals
1171
+ # Process agent output for task filing signals and task status updates
1123
1172
  def process_task_filing(agent_result)
1124
1173
  return unless agent_result && agent_result[:output]
1125
1174
 
1175
+ # Process new task filings
1126
1176
  filed_tasks = AgentSignalParser.parse_task_filing(agent_result[:output])
1127
- return if filed_tasks.empty?
1128
-
1129
1177
  filed_tasks.each do |task_data|
1130
1178
  task = @persistent_tasklist.create(
1131
1179
  task_data[:description],
@@ -1138,6 +1186,150 @@ module Aidp
1138
1186
  Aidp.log_info("tasklist", "Filed new task from agent", task_id: task.id, description: task.description)
1139
1187
  display_message("📋 Filed task: #{task.description} (#{task.id})", type: :info)
1140
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
1141
1333
  end
1142
1334
 
1143
1335
  # Validate changes against guard policy
@@ -17,16 +17,13 @@ module Aidp
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
@@ -157,6 +157,36 @@ module Aidp
157
157
  },
158
158
  default_tier: "mini",
159
159
  cache_ttl: nil
160
+ },
161
+
162
+ implementation_verification: {
163
+ prompt_template: "{{prompt}}", # Custom prompt provided by caller
164
+ schema: {
165
+ type: "object",
166
+ properties: {
167
+ fully_implemented: {
168
+ type: "boolean",
169
+ description: "True if the implementation fully addresses all issue requirements"
170
+ },
171
+ reasoning: {
172
+ type: "string",
173
+ description: "Detailed explanation of the verification decision"
174
+ },
175
+ missing_requirements: {
176
+ type: "array",
177
+ items: {type: "string"},
178
+ description: "List of specific requirements from the issue that are not yet implemented"
179
+ },
180
+ additional_work_needed: {
181
+ type: "array",
182
+ items: {type: "string"},
183
+ description: "List of specific tasks needed to complete the implementation"
184
+ }
185
+ },
186
+ required: ["fully_implemented", "reasoning", "missing_requirements", "additional_work_needed"]
187
+ },
188
+ default_tier: "mini",
189
+ cache_ttl: nil
160
190
  }
161
191
  }.freeze
162
192
 
@@ -204,9 +234,12 @@ module Aidp
204
234
  # Select tier
205
235
  selected_tier = tier || template[:default_tier]
206
236
 
207
- # Get model for tier
237
+ # Get model for tier, using harness default provider
208
238
  thinking_manager = ThinkingDepthManager.new(config)
209
- provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(selected_tier)
239
+ provider_name, model_name, _model_data = thinking_manager.select_model_for_tier(
240
+ selected_tier,
241
+ provider: config.default_provider
242
+ )
210
243
 
211
244
  Aidp.log_debug("ai_decision_engine", "Making AI decision", {
212
245
  decision_type: decision_type,
@@ -103,11 +103,6 @@ module Aidp
103
103
  }
104
104
  end
105
105
 
106
- # Get max retries (alias for backward compatibility with ErrorHandler)
107
- def max_retries(options = {})
108
- retry_config(options)[:max_attempts]
109
- end
110
-
111
106
  # Get circuit breaker configuration
112
107
  def circuit_breaker_config(options = {})
113
108
  harness_config = harness_config(options)
@@ -378,6 +378,7 @@ module Aidp
378
378
  formatter_commands: [],
379
379
  build_commands: [],
380
380
  documentation_commands: [],
381
+ task_completion_required: true,
381
382
  units: {},
382
383
  guards: {enabled: false},
383
384
  version_control: {tool: "git", behavior: "nothing", conventional_commits: false},
@@ -432,6 +433,13 @@ module Aidp
432
433
  # Items can be strings or {command: string, required: boolean}
433
434
  # Validation handled in Configuration class for flexibility
434
435
  },
436
+ task_completion_required: {
437
+ type: :boolean,
438
+ required: false,
439
+ default: true
440
+ # When true, all tasks for the current session must be completed or
441
+ # explicitly abandoned (with user confirmation) before work loop can finish
442
+ },
435
443
  units: {
436
444
  type: :hash,
437
445
  required: false,