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.
- checksums.yaml +4 -4
- data/README.md +89 -0
- data/lib/aidp/cli/models_command.rb +5 -6
- data/lib/aidp/cli.rb +10 -8
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/debug_mixin.rb +23 -1
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +209 -17
- data/lib/aidp/execute/workflow_selector.rb +2 -25
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +0 -5
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +27 -19
- data/lib/aidp/harness/enhanced_runner.rb +1 -4
- data/lib/aidp/harness/error_handler.rb +1 -72
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +74 -2
- data/lib/aidp/providers/base.rb +25 -1
- data/lib/aidp/providers/codex.rb +26 -3
- data/lib/aidp/providers/cursor.rb +16 -0
- data/lib/aidp/providers/gemini.rb +13 -0
- data/lib/aidp/providers/github_copilot.rb +17 -0
- data/lib/aidp/providers/kilocode.rb +11 -0
- data/lib/aidp/providers/opencode.rb +11 -0
- data/lib/aidp/setup/wizard.rb +249 -39
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +7 -43
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +98 -17
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- 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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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,
|