aidp 0.32.0 → 0.34.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -0
  3. data/lib/aidp/analyze/feature_analyzer.rb +322 -320
  4. data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
  5. data/lib/aidp/auto_update/coordinator.rb +97 -7
  6. data/lib/aidp/auto_update.rb +0 -12
  7. data/lib/aidp/cli/devcontainer_commands.rb +0 -5
  8. data/lib/aidp/cli/eval_command.rb +399 -0
  9. data/lib/aidp/cli/harness_command.rb +1 -1
  10. data/lib/aidp/cli/security_command.rb +416 -0
  11. data/lib/aidp/cli/tools_command.rb +6 -4
  12. data/lib/aidp/cli.rb +172 -4
  13. data/lib/aidp/comment_consolidator.rb +78 -0
  14. data/lib/aidp/concurrency/exec.rb +3 -0
  15. data/lib/aidp/concurrency.rb +0 -3
  16. data/lib/aidp/config.rb +113 -1
  17. data/lib/aidp/config_paths.rb +91 -0
  18. data/lib/aidp/daemon/runner.rb +8 -4
  19. data/lib/aidp/errors.rb +134 -0
  20. data/lib/aidp/evaluations/context_capture.rb +205 -0
  21. data/lib/aidp/evaluations/evaluation_record.rb +114 -0
  22. data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
  23. data/lib/aidp/evaluations.rb +23 -0
  24. data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
  25. data/lib/aidp/execute/interactive_repl.rb +6 -2
  26. data/lib/aidp/execute/prompt_evaluator.rb +359 -0
  27. data/lib/aidp/execute/repl_macros.rb +100 -1
  28. data/lib/aidp/execute/work_loop_runner.rb +719 -58
  29. data/lib/aidp/execute/work_loop_state.rb +4 -1
  30. data/lib/aidp/execute/workflow_selector.rb +3 -0
  31. data/lib/aidp/harness/ai_decision_engine.rb +79 -0
  32. data/lib/aidp/harness/ai_filter_factory.rb +285 -0
  33. data/lib/aidp/harness/capability_registry.rb +2 -0
  34. data/lib/aidp/harness/condition_detector.rb +3 -0
  35. data/lib/aidp/harness/config_loader.rb +3 -0
  36. data/lib/aidp/harness/config_schema.rb +97 -1
  37. data/lib/aidp/harness/config_validator.rb +1 -1
  38. data/lib/aidp/harness/configuration.rb +61 -5
  39. data/lib/aidp/harness/enhanced_runner.rb +14 -11
  40. data/lib/aidp/harness/error_handler.rb +3 -0
  41. data/lib/aidp/harness/filter_definition.rb +212 -0
  42. data/lib/aidp/harness/generated_filter_strategy.rb +197 -0
  43. data/lib/aidp/harness/output_filter.rb +50 -25
  44. data/lib/aidp/harness/output_filter_config.rb +129 -0
  45. data/lib/aidp/harness/provider_factory.rb +3 -0
  46. data/lib/aidp/harness/provider_manager.rb +96 -2
  47. data/lib/aidp/harness/runner.rb +5 -12
  48. data/lib/aidp/harness/state/persistence.rb +3 -0
  49. data/lib/aidp/harness/state_manager.rb +3 -0
  50. data/lib/aidp/harness/status_display.rb +28 -20
  51. data/lib/aidp/harness/test_runner.rb +179 -41
  52. data/lib/aidp/harness/thinking_depth_manager.rb +44 -28
  53. data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
  54. data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
  55. data/lib/aidp/harness/ui/error_handler.rb +3 -0
  56. data/lib/aidp/harness/ui/job_monitor.rb +4 -0
  57. data/lib/aidp/harness/ui/navigation/submenu.rb +2 -2
  58. data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
  59. data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
  60. data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
  61. data/lib/aidp/harness/user_interface.rb +3 -0
  62. data/lib/aidp/loader.rb +195 -0
  63. data/lib/aidp/logger.rb +3 -0
  64. data/lib/aidp/message_display.rb +31 -0
  65. data/lib/aidp/metadata/compiler.rb +29 -17
  66. data/lib/aidp/metadata/query.rb +1 -1
  67. data/lib/aidp/metadata/scanner.rb +8 -1
  68. data/lib/aidp/metadata/tool_metadata.rb +13 -13
  69. data/lib/aidp/metadata/validator.rb +10 -0
  70. data/lib/aidp/metadata.rb +16 -0
  71. data/lib/aidp/pr_worktree_manager.rb +20 -8
  72. data/lib/aidp/provider_manager.rb +4 -7
  73. data/lib/aidp/providers/base.rb +2 -0
  74. data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
  75. data/lib/aidp/security/secrets_proxy.rb +328 -0
  76. data/lib/aidp/security/secrets_registry.rb +227 -0
  77. data/lib/aidp/security/trifecta_state.rb +220 -0
  78. data/lib/aidp/security/watch_mode_handler.rb +306 -0
  79. data/lib/aidp/security/work_loop_adapter.rb +277 -0
  80. data/lib/aidp/security.rb +56 -0
  81. data/lib/aidp/setup/wizard.rb +283 -11
  82. data/lib/aidp/skills.rb +0 -5
  83. data/lib/aidp/storage/csv_storage.rb +3 -0
  84. data/lib/aidp/style_guide/selector.rb +360 -0
  85. data/lib/aidp/tooling_detector.rb +283 -16
  86. data/lib/aidp/version.rb +1 -1
  87. data/lib/aidp/watch/auto_merger.rb +274 -0
  88. data/lib/aidp/watch/auto_pr_processor.rb +125 -7
  89. data/lib/aidp/watch/build_processor.rb +16 -1
  90. data/lib/aidp/watch/change_request_processor.rb +682 -150
  91. data/lib/aidp/watch/ci_fix_processor.rb +262 -4
  92. data/lib/aidp/watch/feedback_collector.rb +191 -0
  93. data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
  94. data/lib/aidp/watch/implementation_verifier.rb +142 -1
  95. data/lib/aidp/watch/plan_generator.rb +70 -13
  96. data/lib/aidp/watch/plan_processor.rb +12 -5
  97. data/lib/aidp/watch/projects_processor.rb +286 -0
  98. data/lib/aidp/watch/repository_client.rb +871 -22
  99. data/lib/aidp/watch/review_processor.rb +33 -6
  100. data/lib/aidp/watch/runner.rb +80 -29
  101. data/lib/aidp/watch/state_store.rb +233 -0
  102. data/lib/aidp/watch/sub_issue_creator.rb +221 -0
  103. data/lib/aidp/watch.rb +5 -7
  104. data/lib/aidp/workflows/guided_agent.rb +4 -0
  105. data/lib/aidp/workstream_cleanup.rb +0 -2
  106. data/lib/aidp/workstream_executor.rb +3 -4
  107. data/lib/aidp/worktree.rb +61 -12
  108. data/lib/aidp/worktree_branch_manager.rb +347 -101
  109. data/lib/aidp.rb +21 -106
  110. data/templates/implementation/iterative_implementation.md +46 -3
  111. metadata +91 -36
  112. data/lib/aidp/config/paths.rb +0 -131
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "prompt_manager"
4
+ require_relative "prompt_evaluator"
4
5
  require_relative "checkpoint"
5
6
  require_relative "checkpoint_display"
6
7
  require_relative "guard_policy"
@@ -10,6 +11,8 @@ require_relative "agent_signal_parser"
10
11
  require_relative "steps"
11
12
  require_relative "../harness/test_runner"
12
13
  require_relative "../errors"
14
+ require_relative "../style_guide/selector"
15
+ require_relative "../security"
13
16
 
14
17
  module Aidp
15
18
  module Execute
@@ -38,7 +41,10 @@ module Aidp
38
41
  }.freeze
39
42
  include Aidp::MessageDisplay
40
43
 
41
- attr_reader :iteration_count, :project_dir, :current_state
44
+ # Expose state for testability
45
+ attr_accessor :iteration_count, :step_name, :options, :persistent_tasklist
46
+ attr_reader :project_dir, :current_state, :state_history, :test_runner, :prompt_manager, :checkpoint
47
+ attr_writer :guard_policy, :prompt_manager, :style_guide_selector
42
48
 
43
49
  MAX_ITERATIONS = 50 # Safety limit
44
50
  CHECKPOINT_INTERVAL = 5 # Record checkpoint every N iterations
@@ -54,6 +60,7 @@ module Aidp
54
60
  @checkpoint = Checkpoint.new(project_dir)
55
61
  @checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
56
62
  @guard_policy = GuardPolicy.new(project_dir, config.guards_config)
63
+ @work_context = {}
57
64
  @persistent_tasklist = PersistentTasklist.new(project_dir)
58
65
  @iteration_count = 0
59
66
  @step_name = nil
@@ -65,9 +72,18 @@ module Aidp
65
72
 
66
73
  # Initialize thinking depth manager for intelligent model selection
67
74
  require_relative "../harness/thinking_depth_manager"
68
- @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config)
75
+ @thinking_depth_manager = options[:thinking_depth_manager] || Aidp::Harness::ThinkingDepthManager.new(config, root_dir: @project_dir)
69
76
  @consecutive_failures = 0
70
77
  @last_tier = nil
78
+
79
+ # Initialize style guide selector for intelligent section selection
80
+ @style_guide_selector = options[:style_guide_selector] || Aidp::StyleGuide::Selector.new(project_dir: project_dir)
81
+
82
+ # FIX for issue #391: Initialize prompt evaluator for iteration threshold assessment
83
+ @prompt_evaluator = options[:prompt_evaluator] || PromptEvaluator.new(config)
84
+
85
+ # Initialize security adapter for Rule of Two enforcement
86
+ @security_adapter = options[:security_adapter] || Aidp::Security::WorkLoopAdapter.new(project_dir: project_dir)
71
87
  end
72
88
 
73
89
  # Execute a step using fix-forward work loop pattern
@@ -75,6 +91,7 @@ module Aidp
75
91
  # Never rolls back - only moves forward through fixes
76
92
  def execute_step(step_name, step_spec, context = {})
77
93
  @step_name = step_name
94
+ @work_context = context
78
95
  @iteration_count = 0
79
96
  transition_to(:ready)
80
97
 
@@ -137,6 +154,11 @@ module Aidp
137
154
  @current_state = :ready
138
155
  @state_history.clear
139
156
 
157
+ # Begin security tracking for this agentic work unit
158
+ work_unit_id = "agentic_#{@step_name}_#{SecureRandom.hex(4)}"
159
+ @security_adapter.begin_work_unit(work_unit_id: work_unit_id, context: context)
160
+ display_security_status
161
+
140
162
  create_initial_prompt(step_spec, context)
141
163
 
142
164
  loop do
@@ -148,6 +170,10 @@ module Aidp
148
170
  display_message("āš ļø Max iterations (#{MAX_ITERATIONS}) reached for #{@step_name}", type: :warning)
149
171
  display_state_summary
150
172
  archive_and_cleanup
173
+
174
+ # End security tracking for this work unit
175
+ @security_adapter.end_work_unit
176
+
151
177
  return build_agentic_payload(
152
178
  agent_result: nil,
153
179
  response: build_max_iterations_result,
@@ -161,13 +187,57 @@ module Aidp
161
187
 
162
188
  transition_to(:apply_patch)
163
189
 
190
+ # Preview provider/model selection and queued checks for this iteration
191
+ preview_provider, preview_model, _model_data = select_model_for_current_tier
192
+ prompt_length = @prompt_manager.read&.length || 0
193
+ checks_summary = planned_checks_summary
194
+ display_iteration_overview(preview_provider, preview_model, prompt_length, checks_summary)
195
+ log_iteration_status("running",
196
+ provider: preview_provider,
197
+ model: preview_model,
198
+ prompt_length: prompt_length,
199
+ checks: checks_summary)
200
+
201
+ # Check security policy before agent call (Rule of Two enforcement)
202
+ # Agent calls enable egress capability
203
+ begin
204
+ @security_adapter.check_agent_call_allowed!(operation: :agent_execution)
205
+ rescue Aidp::Security::PolicyViolation => e
206
+ # Security policy violation - cannot proceed with agent call
207
+ Aidp.logger.error("work_loop", "Security policy violation",
208
+ step: @step_name,
209
+ iteration: @iteration_count,
210
+ error: e.message)
211
+ display_message(" šŸ›”ļø Security policy violation: #{e.message}", type: :error)
212
+ display_message(" Cannot proceed - Rule of Two would be violated", type: :error)
213
+
214
+ # End security tracking and return error
215
+ @security_adapter.end_work_unit
216
+ return build_agentic_payload(
217
+ agent_result: nil,
218
+ response: {status: "error", message: "Security policy violation: #{e.message}"},
219
+ summary: nil,
220
+ completed: false,
221
+ terminate: true
222
+ )
223
+ end
224
+
164
225
  # Wrap agent call in exception handling for true fix-forward
165
226
  begin
166
- agent_result = apply_patch
227
+ agent_result = apply_patch(preview_provider, preview_model)
167
228
  rescue Aidp::Errors::ConfigurationError
168
229
  # Configuration errors should crash immediately (crash-early principle)
169
230
  # Re-raise without catching
170
231
  raise
232
+ rescue Aidp::Security::PolicyViolation => e
233
+ # Security violations should not continue - they are policy failures
234
+ Aidp.logger.error("work_loop", "Security policy violation during agent call",
235
+ step: @step_name,
236
+ iteration: @iteration_count,
237
+ error: e.message)
238
+ display_message(" šŸ›”ļø Security violation: #{e.message}", type: :error)
239
+ @security_adapter.end_work_unit
240
+ raise
171
241
  rescue => e
172
242
  # Convert exception to error result for fix-forward handling
173
243
  Aidp.logger.error("work_loop", "Exception during agent call",
@@ -246,20 +316,48 @@ module Aidp
246
316
 
247
317
  # Check task completion status
248
318
  task_completion_result = check_task_completion
319
+ agent_completed = agent_marked_complete?(agent_result)
320
+
321
+ # FIX for issue #391: Comprehensive logging at completion decision point
322
+ Aidp.log_debug("work_loop", "completion_decision_point",
323
+ iteration: @iteration_count,
324
+ all_checks_pass: all_checks_pass,
325
+ agent_marked_complete: agent_completed,
326
+ task_completion_complete: task_completion_result[:complete],
327
+ task_completion_reason: task_completion_result[:reason],
328
+ test_success: test_results[:success],
329
+ lint_success: lint_results[:success],
330
+ formatter_success: formatter_results[:success],
331
+ build_success: build_results[:success],
332
+ doc_success: doc_results[:success])
249
333
 
250
334
  if all_checks_pass
251
335
  transition_to(:pass)
252
336
 
253
- if agent_marked_complete?(agent_result)
337
+ if agent_completed
254
338
  # Check if tasks are complete
255
339
  if task_completion_result[:complete]
340
+ Aidp.log_debug("work_loop", "completion_approved",
341
+ iteration: @iteration_count,
342
+ reason: task_completion_result[:reason])
343
+
256
344
  transition_to(:done)
257
345
  record_final_checkpoint(all_results)
258
346
  display_task_summary
259
347
  display_message("āœ… Step #{@step_name} completed after #{@iteration_count} iterations", type: :success)
260
348
  display_state_summary
349
+ log_iteration_status("completed",
350
+ provider: preview_provider,
351
+ model: preview_model,
352
+ prompt_length: prompt_length,
353
+ checks: checks_summary,
354
+ task_status: "complete",
355
+ completion_reason: task_completion_result[:reason])
261
356
  archive_and_cleanup
262
357
 
358
+ # End security tracking for this work unit
359
+ @security_adapter.end_work_unit
360
+
263
361
  return build_agentic_payload(
264
362
  agent_result: agent_result,
265
363
  response: build_success_result(agent_result),
@@ -269,16 +367,36 @@ module Aidp
269
367
  )
270
368
  else
271
369
  # All checks passed but tasks not complete
370
+ Aidp.log_debug("work_loop", "completion_blocked_tasks_incomplete",
371
+ iteration: @iteration_count,
372
+ reason: task_completion_result[:reason],
373
+ message: task_completion_result[:message])
374
+
272
375
  display_message(" All checks passed but tasks not complete", type: :warning)
273
376
  display_message(" #{task_completion_result[:message]}", type: :warning)
274
377
  display_task_summary
378
+ log_iteration_status("checks_passed_tasks_incomplete",
379
+ provider: preview_provider,
380
+ model: preview_model,
381
+ prompt_length: prompt_length,
382
+ checks: checks_summary,
383
+ task_status: "incomplete",
384
+ task_completion_reason: task_completion_result[:reason])
275
385
  transition_to(:next_patch)
276
386
 
277
387
  # Append task completion requirement to PROMPT.md
278
388
  append_task_requirement_to_prompt(task_completion_result[:message])
279
389
  end
280
390
  else
391
+ Aidp.log_debug("work_loop", "completion_blocked_agent_not_complete",
392
+ iteration: @iteration_count)
393
+
281
394
  display_message(" All checks passed but work not marked complete", type: :info)
395
+ log_iteration_status("checks_passed_waiting_agent_completion",
396
+ provider: preview_provider,
397
+ model: preview_model,
398
+ prompt_length: prompt_length,
399
+ checks: checks_summary)
282
400
  transition_to(:next_patch)
283
401
  end
284
402
  else
@@ -289,9 +407,154 @@ module Aidp
289
407
  diagnostic = diagnose_failures(all_results)
290
408
 
291
409
  transition_to(:next_patch)
410
+ log_iteration_status("checks_failed",
411
+ provider: preview_provider,
412
+ model: preview_model,
413
+ prompt_length: prompt_length,
414
+ checks: checks_summary,
415
+ failures: failure_summary_for_log(all_results))
292
416
  prepare_next_iteration(all_results, diagnostic)
293
417
  end
418
+
419
+ # FIX for issue #391: Evaluate prompt effectiveness at iteration thresholds
420
+ # After 10+ iterations, assess whether the prompt is leading to progress
421
+ evaluate_prompt_effectiveness(all_results)
422
+ end
423
+ end
424
+
425
+ # Evaluate prompt effectiveness at iteration thresholds
426
+ # FIX for issue #391: Provides feedback when work loop is stuck
427
+ # Note: Errors during evaluation are logged but don't fail the work loop
428
+ def evaluate_prompt_effectiveness(all_results)
429
+ return unless @prompt_evaluator.should_evaluate?(@iteration_count)
430
+
431
+ Aidp.log_debug("work_loop", "evaluating_prompt_effectiveness",
432
+ iteration: @iteration_count)
433
+
434
+ display_message("šŸ“Š Evaluating prompt effectiveness (iteration #{@iteration_count})...", type: :info)
435
+
436
+ task_summary = build_task_summary_for_evaluation
437
+ prompt_content = @prompt_manager.read
438
+
439
+ evaluation = @prompt_evaluator.evaluate(
440
+ prompt_content: prompt_content,
441
+ iteration_count: @iteration_count,
442
+ task_summary: task_summary,
443
+ recent_failures: all_results,
444
+ step_name: @step_name
445
+ )
446
+
447
+ display_prompt_evaluation_results(evaluation)
448
+
449
+ # If prompt is deemed ineffective, append suggestions to PROMPT.md
450
+ unless evaluation[:effective]
451
+ append_evaluation_feedback_to_prompt(evaluation)
452
+ end
453
+
454
+ Aidp.log_info("work_loop", "prompt_evaluation_complete",
455
+ iteration: @iteration_count,
456
+ effective: evaluation[:effective],
457
+ confidence: evaluation[:confidence])
458
+ rescue => e
459
+ # Don't let evaluation errors break the work loop
460
+ Aidp.log_warn("work_loop", "prompt_evaluation_error",
461
+ iteration: @iteration_count,
462
+ error: e.message,
463
+ error_class: e.class.name)
464
+ display_message(" āš ļø Prompt evaluation skipped due to error: #{e.message}", type: :muted)
465
+ end
466
+
467
+ def build_task_summary_for_evaluation
468
+ all_tasks = @persistent_tasklist.all
469
+ return {} if all_tasks.empty?
470
+
471
+ {
472
+ total: all_tasks.size,
473
+ done: all_tasks.count { |t| t.status == :done },
474
+ in_progress: all_tasks.count { |t| t.status == :in_progress },
475
+ pending: all_tasks.count { |t| t.status == :pending },
476
+ abandoned: all_tasks.count { |t| t.status == :abandoned }
477
+ }
478
+ end
479
+
480
+ def display_prompt_evaluation_results(evaluation)
481
+ # Skip display if evaluation was skipped
482
+ if evaluation[:skipped]
483
+ display_message(" ā„¹ļø Prompt evaluation skipped: #{evaluation[:skip_reason]}", type: :muted)
484
+ return
485
+ end
486
+
487
+ if evaluation[:effective]
488
+ display_message(" āœ… Prompt appears effective, continuing...", type: :success)
489
+ else
490
+ display_message(" āš ļø Prompt may need improvement:", type: :warning)
491
+
492
+ if evaluation[:issues]&.any?
493
+ display_message(" Issues identified:", type: :info)
494
+ evaluation[:issues].each { |issue| display_message(" - #{issue}", type: :warning) }
495
+ end
496
+
497
+ if evaluation[:suggestions]&.any?
498
+ display_message(" Suggestions:", type: :info)
499
+ evaluation[:suggestions].take(3).each { |s| display_message(" - #{s}", type: :info) }
500
+ end
501
+
502
+ if evaluation[:likely_blockers]&.any?
503
+ display_message(" Likely blockers:", type: :warning)
504
+ evaluation[:likely_blockers].each { |b| display_message(" - #{b}", type: :error) }
505
+ end
506
+ end
507
+
508
+ display_message(" Confidence: #{(evaluation[:confidence] * 100).round}%", type: :muted)
509
+ end
510
+
511
+ def append_evaluation_feedback_to_prompt(evaluation)
512
+ feedback_section = build_evaluation_feedback_section(evaluation)
513
+
514
+ @prompt_manager.append(feedback_section)
515
+
516
+ Aidp.log_debug("work_loop", "appended_evaluation_feedback",
517
+ iteration: @iteration_count,
518
+ feedback_size: feedback_section.length)
519
+ end
520
+
521
+ def build_evaluation_feedback_section(evaluation)
522
+ parts = []
523
+ parts << "\n\n## āš ļø Work Loop Progress Assessment (Iteration #{@iteration_count})"
524
+ parts << ""
525
+ parts << "The work loop has been running for #{@iteration_count} iterations without completion."
526
+ parts << "An automated assessment identified the following:"
527
+ parts << ""
528
+
529
+ if evaluation[:issues]&.any?
530
+ parts << "### Issues Identified"
531
+ evaluation[:issues].each { |i| parts << "- #{i}" }
532
+ parts << ""
533
+ end
534
+
535
+ if evaluation[:suggestions]&.any?
536
+ parts << "### Suggestions for Progress"
537
+ evaluation[:suggestions].each { |s| parts << "- #{s}" }
538
+ parts << ""
539
+ end
540
+
541
+ if evaluation[:recommended_actions]&.any?
542
+ parts << "### Recommended Actions"
543
+ evaluation[:recommended_actions].each do |action|
544
+ parts << "- [#{action[:priority]&.upcase || "MEDIUM"}] #{action[:action]}"
545
+ parts << " Rationale: #{action[:rationale]}" if action[:rationale]
546
+ end
547
+ parts << ""
294
548
  end
549
+
550
+ parts << "### Next Steps"
551
+ parts << "Please address the above issues and either:"
552
+ parts << "1. Complete the remaining work and mark STATUS: COMPLETE"
553
+ parts << "2. File tasks for remaining work and complete them systematically"
554
+ parts << "3. If blocked, explain the blocker clearly in your response"
555
+ parts << ""
556
+
557
+ parts.join("\n")
295
558
  end
296
559
 
297
560
  def run_decider_agentic_unit(context)
@@ -499,8 +762,8 @@ module Aidp
499
762
  end
500
763
 
501
764
  # Apply patch - send PROMPT.md to agent
502
- def apply_patch
503
- send_to_agent
765
+ def apply_patch(selected_provider = nil, selected_model = nil)
766
+ send_to_agent(selected_provider: selected_provider, selected_model: selected_model)
504
767
  end
505
768
 
506
769
  # Check if agent marked work complete
@@ -549,7 +812,9 @@ module Aidp
549
812
  # Traditional prompt building (fallback or when optimization disabled)
550
813
  template_content = load_template(step_spec["templates"]&.first)
551
814
  prd_content = load_prd
552
- style_guide = load_style_guide
815
+ # Use provider-aware style guide loading - skips for Claude/Copilot,
816
+ # selects relevant STYLE_GUIDE sections for other providers
817
+ style_guide = load_style_guide_for_provider(context)
553
818
  user_input = format_user_input(context[:user_input])
554
819
  deterministic_outputs = Array(context[:deterministic_outputs])
555
820
  previous_summary = context[:previous_agent_summary]
@@ -729,7 +994,7 @@ module Aidp
729
994
  parts.join("\n")
730
995
  end
731
996
 
732
- def send_to_agent
997
+ def send_to_agent(selected_provider: nil, selected_model: nil)
733
998
  prompt_content = @prompt_manager.read
734
999
  return {status: "error", message: "PROMPT.md not found"} unless prompt_content
735
1000
 
@@ -737,9 +1002,11 @@ module Aidp
737
1002
  full_prompt = build_work_loop_header(@step_name, @iteration_count) + "\n\n" + prompt_content
738
1003
 
739
1004
  # Select model based on thinking depth tier
740
- provider_name, model_name, _model_data = select_model_for_current_tier
1005
+ provider_name = selected_provider
1006
+ model_name = selected_model
1007
+ provider_name, model_name, _model_data = select_model_for_current_tier if provider_name.nil? || model_name.nil?
741
1008
 
742
- if provider_name.nil? || model_name.nil?
1009
+ if provider_name.nil?
743
1010
  Aidp.logger.error("work_loop", "Failed to select model for tier",
744
1011
  tier: @thinking_depth_manager.current_tier,
745
1012
  step: @step_name,
@@ -750,39 +1017,213 @@ module Aidp
750
1017
  # Log model selection
751
1018
  tier = @thinking_depth_manager.current_tier
752
1019
  if @last_tier != tier
753
- display_message(" šŸ’” Using tier: #{tier} (#{provider_name}/#{model_name})", type: :info)
1020
+ model_label = model_name || "auto"
1021
+ display_message(" šŸ’” Using tier: #{tier} (#{provider_name}/#{model_label})", type: :info)
754
1022
  @last_tier = tier
755
1023
  end
756
1024
 
757
1025
  # CRITICAL: Change to project directory before calling provider
758
1026
  # This ensures Claude CLI runs in the correct directory and can create files
759
1027
  Dir.chdir(@project_dir) do
760
- # Send to provider via provider_manager with selected model
761
- @provider_manager.execute_with_provider(
762
- provider_name,
763
- full_prompt,
764
- {
765
- step_name: @step_name,
766
- iteration: @iteration_count,
767
- project_dir: @project_dir,
768
- model: model_name,
769
- tier: @thinking_depth_manager.current_tier
770
- }
771
- )
1028
+ # Execute with sanitized environment (secrets stripped) when security is enabled
1029
+ # This ensures agent processes cannot access registered secrets directly
1030
+ execute_block = lambda do
1031
+ @provider_manager.execute_with_provider(
1032
+ provider_name,
1033
+ full_prompt,
1034
+ {
1035
+ step_name: @step_name,
1036
+ iteration: @iteration_count,
1037
+ project_dir: @project_dir,
1038
+ model: model_name,
1039
+ tier: @thinking_depth_manager.current_tier
1040
+ }
1041
+ )
1042
+ end
1043
+
1044
+ if @security_adapter.enabled?
1045
+ @security_adapter.with_sanitized_environment(&execute_block)
1046
+ else
1047
+ execute_block.call
1048
+ end
1049
+ end
1050
+ end
1051
+
1052
+ def display_iteration_overview(provider_name, model_name, prompt_length, checks_summary = nil)
1053
+ tier = @thinking_depth_manager.current_tier
1054
+ checks = checks_summary
1055
+ checks ||= summarize_checks(@test_runner.planned_commands) if @test_runner.respond_to?(:planned_commands)
1056
+ model_label = model_name || "auto"
1057
+ context_labels = iteration_context_labels
1058
+
1059
+ display_message(" • Step: #{@step_name} | Tier: #{tier} | Model: #{provider_name}/#{model_label}", type: :info)
1060
+ display_message(" • Prompt size: #{prompt_length} chars | State: #{STATES[@current_state]}", type: :info)
1061
+ display_message(" • Upcoming checks: #{checks}", type: :info) if checks && !checks.empty?
1062
+ display_message(" • Context: #{context_labels.join(" | ")}", type: :info) if context_labels.any?
1063
+
1064
+ # Display output filtering configuration if enabled
1065
+ filtering_info = summarize_output_filtering
1066
+ display_message(" • Output filtering: #{filtering_info}", type: :info) if filtering_info
1067
+ end
1068
+
1069
+ # Summarize output filtering configuration
1070
+ def summarize_output_filtering
1071
+ return nil unless @config.respond_to?(:output_filtering_enabled?) && @config.output_filtering_enabled?
1072
+
1073
+ iteration = @test_runner.respond_to?(:iteration_count) ? @test_runner.iteration_count : 0
1074
+
1075
+ test_mode = if @config.respond_to?(:test_output_mode)
1076
+ @config.test_output_mode
1077
+ elsif iteration > 1
1078
+ :failures_only
1079
+ else
1080
+ :full
1081
+ end
1082
+
1083
+ lint_mode = if @config.respond_to?(:lint_output_mode)
1084
+ @config.lint_output_mode
1085
+ elsif iteration > 1
1086
+ :failures_only
1087
+ else
1088
+ :full
1089
+ end
1090
+
1091
+ if test_mode == :full && lint_mode == :full
1092
+ nil # Don't show message when no filtering is active
1093
+ else
1094
+ "test=#{test_mode}, lint=#{lint_mode}"
1095
+ end
1096
+ rescue
1097
+ nil
1098
+ end
1099
+
1100
+ # Display output filtering statistics after test/lint runs
1101
+ def display_filtering_stats
1102
+ return unless @test_runner.respond_to?(:filter_stats)
1103
+
1104
+ stats = @test_runner.filter_stats
1105
+ return if stats[:total_input_bytes].zero?
1106
+
1107
+ reduction = ((stats[:total_input_bytes] - stats[:total_output_bytes]).to_f / stats[:total_input_bytes] * 100).round(1)
1108
+ return if reduction <= 0
1109
+
1110
+ display_message(" šŸ“‰ Token optimization: #{reduction}% reduction " \
1111
+ "(#{format_bytes(stats[:total_input_bytes])} → #{format_bytes(stats[:total_output_bytes])})", type: :info)
1112
+ rescue
1113
+ # Silently ignore errors in stats display
1114
+ end
1115
+
1116
+ def format_bytes(bytes)
1117
+ if bytes >= 1024 * 1024
1118
+ "#{(bytes / 1024.0 / 1024.0).round(1)}MB"
1119
+ elsif bytes >= 1024
1120
+ "#{(bytes / 1024.0).round(1)}KB"
1121
+ else
1122
+ "#{bytes}B"
1123
+ end
1124
+ end
1125
+
1126
+ def summarize_checks(planned)
1127
+ labels = {
1128
+ tests: "tests",
1129
+ lints: "linters",
1130
+ formatters: "formatters",
1131
+ builds: "builds",
1132
+ docs: "docs"
1133
+ }
1134
+
1135
+ summaries = planned.map do |category, commands|
1136
+ count = Array(commands).size
1137
+ next if count.zero?
1138
+
1139
+ label = labels[category] || category.to_s
1140
+ cmd_names = Array(commands).map do |cmd|
1141
+ cmd.is_a?(Hash) ? cmd[:command] : cmd
1142
+ end
1143
+
1144
+ if cmd_names.size <= 2
1145
+ "#{label} (#{cmd_names.join(", ")})"
1146
+ else
1147
+ "#{label} (#{cmd_names.first(2).join(", ")} +#{cmd_names.size - 2} more)"
1148
+ end
1149
+ end.compact
1150
+
1151
+ summaries.join(" | ")
1152
+ rescue => e
1153
+ Aidp.log_warn("work_loop", "summarize_checks_failed", error: e.message)
1154
+ nil
1155
+ end
1156
+
1157
+ def planned_checks_summary
1158
+ return nil unless @test_runner.respond_to?(:planned_commands)
1159
+
1160
+ summarize_checks(@test_runner.planned_commands)
1161
+ end
1162
+
1163
+ def failure_summary_for_log(all_results)
1164
+ Array(all_results).each_with_object([]) do |(category, results), summary|
1165
+ next if results[:success]
1166
+
1167
+ failures = results[:required_failures] || results[:failures] || []
1168
+ count = failures.size
1169
+ commands = Array(failures).map { |f| f[:command] }.compact
1170
+
1171
+ summary << if commands.any?
1172
+ "#{category}: #{count} (#{commands.first(2).join(", ")})"
1173
+ else
1174
+ "#{category}: #{count}"
1175
+ end
772
1176
  end
1177
+ rescue => e
1178
+ Aidp.log_warn("work_loop", "failure_summary_for_log_failed", error: e.message)
1179
+ []
773
1180
  end
774
1181
 
1182
+ # FIX for issue #391: Added completion_reason and task_completion_reason parameters for better logging
1183
+ def log_iteration_status(status, provider:, model:, prompt_length:, checks: nil, failures: nil, task_status: nil,
1184
+ completion_reason: nil, task_completion_reason: nil)
1185
+ context_labels = iteration_context_labels
1186
+ metadata = {
1187
+ step: @step_name,
1188
+ iteration: @iteration_count,
1189
+ state: STATES[@current_state],
1190
+ tier: @thinking_depth_manager.current_tier,
1191
+ provider: provider,
1192
+ model: model,
1193
+ prompt_length: prompt_length,
1194
+ checks: checks,
1195
+ failures: failures,
1196
+ task_status: task_status,
1197
+ completion_reason: completion_reason,
1198
+ task_completion_reason: task_completion_reason
1199
+ }
1200
+
1201
+ metadata.merge!(iteration_context_metadata)
1202
+ metadata.delete_if { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
1203
+
1204
+ message = "Iteration #{@iteration_count} for #{@step_name}: #{status}"
1205
+ message += " | #{context_labels.join(" | ")}" if context_labels.any?
1206
+
1207
+ Aidp.log_info("work_loop_iteration",
1208
+ message,
1209
+ **metadata)
1210
+ rescue => e
1211
+ Aidp.log_warn("work_loop", "failed_to_log_iteration_status", error: e.message)
1212
+ end
1213
+
1214
+ # FIX for issue #391: Enhanced work loop header with upfront task filing requirements
775
1215
  def build_work_loop_header(step_name, iteration)
776
1216
  parts = []
777
1217
  parts << "# Work Loop: #{step_name} (Iteration #{iteration})"
778
1218
  parts << ""
779
1219
  parts << "## Instructions"
780
1220
  parts << "You are working in a work loop. Your responsibilities:"
781
- parts << "1. Read the task description below to understand what needs to be done"
782
- parts << "2. **Write/edit code files** to implement the required changes"
783
- parts << "3. Run tests to verify your changes work correctly"
784
- parts << "4. Update the task list in PROMPT.md as you complete items"
785
- parts << "5. When ALL tasks are complete and tests pass, mark the step COMPLETE"
1221
+ parts << "1. **FIRST**: File tasks for all work items (see Task Filing section below)"
1222
+ parts << "2. Read the task description below to understand what needs to be done"
1223
+ parts << "3. **Write/edit CODE files** to implement the required changes"
1224
+ parts << "4. Run tests to verify your changes work correctly"
1225
+ parts << "5. Update task status as you complete items"
1226
+ parts << "6. When ALL tasks are complete and tests pass, mark the step COMPLETE"
786
1227
  parts << ""
787
1228
  parts << "## Important Notes"
788
1229
  parts << "- You have full file system access - create and edit files as needed"
@@ -790,29 +1231,53 @@ module Aidp
790
1231
  parts << "- After you finish, tests and linters will run automatically"
791
1232
  parts << "- If tests/linters fail, you'll see the errors in the next iteration and can fix them"
792
1233
  parts << ""
1234
+ parts << "## āš ļø Code Changes Required"
1235
+ parts << "**IMPORTANT**: This implementation requires actual code changes."
1236
+ parts << "- Documentation-only changes will NOT be accepted as complete"
1237
+ parts << "- Configuration-only changes will NOT be accepted as complete"
1238
+ parts << "- You must modify/create code files (.rb, .py, .js, etc.) to implement the feature/fix"
1239
+ parts << "- Tests should accompany code changes"
1240
+ parts << ""
793
1241
 
794
1242
  if @config.task_completion_required?
795
- parts << "## Task Tracking (REQUIRED)"
796
- parts << "**CRITICAL**: This work loop requires task tracking for completion."
1243
+ parts << "## Task Filing (REQUIRED - DO THIS FIRST)"
1244
+ parts << "**CRITICAL**: This work loop requires task tracking. You MUST file tasks before implementation."
1245
+ parts << ""
1246
+ parts << "### Step 1: File Tasks Immediately"
1247
+ parts << "In your FIRST iteration, analyze the requirements and file tasks for ALL work:"
797
1248
  parts << ""
798
- parts << "You must:"
799
- parts << "1. Create at least one task for this session using: `File task: \"description\"`"
800
- parts << "2. Track all work items as tasks"
801
- parts << "3. Update task status as you progress"
802
- parts << "4. All tasks must be DONE or ABANDONED (with reason) before completion"
803
- parts << "5. **IMPORTANT**: When you write STATUS: COMPLETE, also mark all your tasks as done!"
1249
+ parts << "```text"
1250
+ parts << "File task: \"Implement [feature/fix description]\" priority: high tags: implementation"
1251
+ parts << "File task: \"Add unit tests for [feature]\" priority: high tags: testing"
1252
+ parts << "File task: \"Add integration tests if needed\" priority: medium tags: testing"
1253
+ parts << "```"
804
1254
  parts << ""
805
- parts << "**Important**: Tasks in the list exist due to careful planning and requirements analysis."
806
- parts << "Do NOT abandon tasks due to perceived complexity or scope concerns - these factors were"
807
- parts << "considered during planning. Only abandon tasks when truly obsolete (requirements changed,"
808
- parts << "duplicate work, external blockers). When in doubt, mark in_progress and implement."
1255
+ parts << "### Step 2: Work Through Tasks"
1256
+ parts << "- Pick the highest priority pending task"
1257
+ parts << "- Implement it completely"
1258
+ parts << "- Mark it done: `Update task: task_id status: done`"
1259
+ parts << "- Repeat until all tasks are complete"
809
1260
  parts << ""
810
- parts << "Task filing examples:"
1261
+ parts << "### Step 3: Complete the Work Loop"
1262
+ parts << "Only after ALL tasks are done:"
1263
+ parts << "- Verify tests pass"
1264
+ parts << "- Add STATUS: COMPLETE to PROMPT.md"
1265
+ parts << ""
1266
+ parts << "### Task Rules"
1267
+ parts << "- **At least ONE task must be filed** - completion blocked without tasks"
1268
+ parts << "- **At least ONE task must be DONE** - completion blocked if all abandoned"
1269
+ parts << "- **Substantive work required** - doc-only changes rejected"
1270
+ parts << ""
1271
+ parts << "**Important**: Tasks exist due to careful planning. Do NOT abandon tasks due to"
1272
+ parts << "perceived complexity - these factors were considered during planning. Only abandon"
1273
+ parts << "when truly obsolete (requirements changed, duplicate, external blockers)."
1274
+ parts << ""
1275
+ parts << "### Task Filing Examples"
811
1276
  parts << "- `File task: \"Implement user authentication\" priority: high tags: security,auth`"
812
1277
  parts << "- `File task: \"Add tests for login flow\" priority: medium tags: testing`"
813
1278
  parts << "- `File task: \"Update documentation\" priority: low tags: docs`"
814
1279
  parts << ""
815
- parts << "Task status update examples:"
1280
+ parts << "### Task Status Update Examples"
816
1281
  parts << "- `Update task: task_123_abc status: in_progress`"
817
1282
  parts << "- `Update task: task_456_def status: done`"
818
1283
  parts << "- `Update task: task_789_ghi status: abandoned reason: \"Requirements changed\"`"
@@ -832,6 +1297,24 @@ module Aidp
832
1297
  parts.join("\n")
833
1298
  end
834
1299
 
1300
+ def iteration_context_metadata
1301
+ ctx = (@options || {}).merge(@work_context || {})
1302
+ {
1303
+ issue: issue_context_label(ctx),
1304
+ pr: pr_context_label(ctx),
1305
+ step_position: step_position_label(@step_name, ctx)
1306
+ }.compact
1307
+ end
1308
+
1309
+ def iteration_context_labels
1310
+ meta = iteration_context_metadata
1311
+ labels = []
1312
+ labels << meta[:issue] if meta[:issue]
1313
+ labels << meta[:pr] if meta[:pr]
1314
+ labels << meta[:step_position] if meta[:step_position]
1315
+ labels
1316
+ end
1317
+
835
1318
  def prompt_marked_complete?
836
1319
  prompt_content = @prompt_manager.read
837
1320
  return false unless prompt_content
@@ -938,30 +1421,50 @@ module Aidp
938
1421
 
939
1422
  # Check if we should reinject the style guide at this iteration
940
1423
  def should_reinject_style_guide?
1424
+ # Skip reinjection for providers with instruction files (Claude, GitHub Copilot)
1425
+ current_provider = @provider_manager&.current_provider
1426
+ return false unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1427
+
941
1428
  # Reinject on intervals (5, 10, 15, etc.) but not on iteration 1
942
1429
  @iteration_count > 1 && (@iteration_count % STYLE_GUIDE_REMINDER_INTERVAL == 0)
943
1430
  end
944
1431
 
945
1432
  # Create style guide reminder text
946
1433
  def reinject_style_guide_reminder
947
- style_guide = load_style_guide
1434
+ current_provider = @provider_manager&.current_provider
1435
+
1436
+ # Skip for providers with instruction files
1437
+ unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1438
+ Aidp.log_debug("work_loop", "skipping_style_guide_reminder",
1439
+ provider: current_provider,
1440
+ reason: "provider has instruction file")
1441
+ return ""
1442
+ end
1443
+
948
1444
  template_content = load_current_template
949
1445
 
1446
+ # Use provider-aware style guide loading with context-based section selection
1447
+ style_guide = load_style_guide_for_provider(@work_context)
1448
+
950
1449
  reminder = []
951
1450
  reminder << "### šŸ”„ Style Guide & Template Reminder (Iteration #{@iteration_count})"
952
1451
  reminder << ""
953
1452
  reminder << "**IMPORTANT**: To prevent drift from project conventions, please review:"
954
1453
  reminder << ""
955
1454
 
956
- if style_guide
957
- reminder << "#### LLM Style Guide"
958
- reminder << "```"
959
- # Include first 1000 chars of style guide to keep context manageable
960
- style_guide_preview = (style_guide.length > 1000) ? style_guide[0...1000] + "\n...(truncated)" : style_guide
1455
+ if style_guide && !style_guide.empty?
1456
+ reminder << "#### Relevant Style Guide Sections"
1457
+ reminder << "```markdown"
1458
+ # Include selected sections (already limited by selector)
1459
+ style_guide_preview = if style_guide.length > 2000
1460
+ style_guide[0...2000] + "\n...(truncated)"
1461
+ else
1462
+ style_guide
1463
+ end
961
1464
  reminder << style_guide_preview
962
1465
  reminder << "```"
963
1466
  reminder << ""
964
- display_message(" [STYLE_GUIDE] Re-injecting LLM_STYLE_GUIDE at iteration #{@iteration_count}", type: :info)
1467
+ display_message(" [STYLE_GUIDE] Re-injecting selected STYLE_GUIDE sections at iteration #{@iteration_count}", type: :info)
965
1468
  end
966
1469
 
967
1470
  if template_content
@@ -1051,6 +1554,74 @@ module Aidp
1051
1554
  File.exist?(style_guide_path) ? File.read(style_guide_path) : nil
1052
1555
  end
1053
1556
 
1557
+ # Load style guide content appropriate for the current provider and context
1558
+ # Returns nil for providers with instruction files (Claude, GitHub Copilot)
1559
+ # Returns selected STYLE_GUIDE sections for other providers
1560
+ #
1561
+ # @param context [Hash] Task context for keyword extraction
1562
+ # @return [String, nil] Style guide content or nil if not needed
1563
+ def load_style_guide_for_provider(context = {})
1564
+ current_provider = @provider_manager&.current_provider
1565
+
1566
+ # Skip style guide for providers with their own instruction files
1567
+ unless @style_guide_selector.provider_needs_style_guide?(current_provider)
1568
+ Aidp.log_debug("work_loop", "skipping_style_guide",
1569
+ provider: current_provider,
1570
+ reason: "provider has instruction file")
1571
+ return nil
1572
+ end
1573
+
1574
+ # Extract keywords from context for intelligent section selection
1575
+ keywords = extract_style_guide_keywords(context)
1576
+
1577
+ # Select relevant sections from STYLE_GUIDE.md
1578
+ content = @style_guide_selector.select_sections(
1579
+ keywords: keywords,
1580
+ include_core: true,
1581
+ max_lines: 500 # Limit to keep prompt size manageable
1582
+ )
1583
+
1584
+ return nil if content.nil? || content.empty?
1585
+
1586
+ Aidp.log_debug("work_loop", "style_guide_selected",
1587
+ provider: current_provider,
1588
+ keywords: keywords,
1589
+ content_lines: content.lines.count)
1590
+
1591
+ content
1592
+ end
1593
+
1594
+ # Extract keywords from task context for style guide section selection
1595
+ #
1596
+ # @param context [Hash] Task context
1597
+ # @return [Array<String>] Keywords for section selection
1598
+ def extract_style_guide_keywords(context)
1599
+ keywords = []
1600
+
1601
+ # Extract from step name
1602
+ step_lower = @step_name.to_s.downcase
1603
+ keywords << "testing" if step_lower.include?("test")
1604
+ keywords << "implementation" if step_lower.include?("implement")
1605
+ keywords << "refactor" if step_lower.include?("refactor")
1606
+
1607
+ # Extract from user input
1608
+ user_input = context[:user_input]
1609
+ if user_input.is_a?(Hash)
1610
+ keywords.concat(@style_guide_selector.extract_keywords(user_input.values.join(" ")))
1611
+ elsif user_input.is_a?(String)
1612
+ keywords.concat(@style_guide_selector.extract_keywords(user_input))
1613
+ end
1614
+
1615
+ # Extract from affected files
1616
+ affected_files = context[:affected_files] || []
1617
+ affected_files.each do |file|
1618
+ keywords << "testing" if file.include?("spec") || file.include?("test")
1619
+ keywords << "tty" if file.include?("cli") || file.include?("tui")
1620
+ end
1621
+
1622
+ keywords.uniq
1623
+ end
1624
+
1054
1625
  def format_user_input(user_input)
1055
1626
  return nil if user_input.nil? || user_input.empty?
1056
1627
 
@@ -1145,6 +1716,29 @@ module Aidp
1145
1716
  display_message("")
1146
1717
  end
1147
1718
 
1719
+ # Display security status for Rule of Two enforcement
1720
+ def display_security_status
1721
+ status = @security_adapter.status
1722
+ return unless status[:enabled]
1723
+
1724
+ display_message("\nšŸ”’ Security (Rule of Two):", type: :info)
1725
+ display_message(" #{status[:status_string]}", type: :info)
1726
+
1727
+ if status[:state]
1728
+ state = status[:state]
1729
+ flags = []
1730
+ flags << "untrusted_input (#{state[:untrusted_input_source]})" if state[:untrusted_input]
1731
+ flags << "private_data (#{state[:private_data_source]})" if state[:private_data]
1732
+ flags << "egress (#{state[:egress_source]})" if state[:egress]
1733
+
1734
+ if flags.any?
1735
+ display_message(" Active flags: #{flags.join(", ")}", type: :info)
1736
+ end
1737
+ end
1738
+
1739
+ display_message("")
1740
+ end
1741
+
1148
1742
  # Display pending tasks from persistent tasklist
1149
1743
  def display_pending_tasks
1150
1744
  pending_tasks = @persistent_tasklist.pending
@@ -1221,46 +1815,113 @@ module Aidp
1221
1815
  end
1222
1816
 
1223
1817
  # Check if tasks are required and all are completed or abandoned
1224
- # Returns {complete: boolean, message: string}
1818
+ # Returns {complete: boolean, message: string, reason: string}
1225
1819
  # Note: Tasks are project-scoped, not session-scoped. This allows tasks created
1226
1820
  # in planning phases to be completed in build phases.
1821
+ #
1822
+ # FIX for issue #391: Prevent premature completion when tasks haven't been created
1823
+ # The previous logic allowed completion with empty task list, which enabled
1824
+ # the work loop to complete before actually implementing anything.
1227
1825
  def check_task_completion
1228
- return {complete: true, message: nil} unless @config.task_completion_required?
1826
+ Aidp.log_debug("work_loop", "check_task_completion_start",
1827
+ task_completion_required: @config.task_completion_required?,
1828
+ iteration: @iteration_count)
1829
+
1830
+ unless @config.task_completion_required?
1831
+ Aidp.log_debug("work_loop", "check_task_completion_skipped",
1832
+ reason: "task_completion_not_required")
1833
+ return {complete: true, message: nil, reason: "task_completion_not_required"}
1834
+ end
1229
1835
 
1230
1836
  all_tasks = @persistent_tasklist.all
1231
1837
 
1232
- # If no tasks exist yet, allow completion - agent can work without tasks initially
1233
- # This supports workflows where no planning phase created tasks
1838
+ Aidp.log_debug("work_loop", "check_task_completion_task_count",
1839
+ total_tasks: all_tasks.size,
1840
+ task_ids: all_tasks.map(&:id))
1841
+
1842
+ # FIX for issue #391: Require at least one task when task_completion is enabled
1843
+ # Empty task list now blocks completion to prevent premature PR creation
1844
+ # This ensures the agent has actually created and completed work items
1234
1845
  if all_tasks.empty?
1235
- return {complete: true, message: nil}
1846
+ Aidp.log_debug("work_loop", "check_task_completion_empty_tasks",
1847
+ reason: "no_tasks_filed",
1848
+ iteration: @iteration_count)
1849
+
1850
+ # After multiple iterations, require tasks - agent should have filed some by now
1851
+ if @iteration_count >= 3
1852
+ return {
1853
+ complete: false,
1854
+ message: "No tasks have been filed yet. You must create at least one task using:\n" \
1855
+ " File task: \"description\" priority: high|medium|low tags: tag1,tag2\n\n" \
1856
+ "Tasks help track progress and ensure complete implementation.",
1857
+ reason: "no_tasks_after_iterations"
1858
+ }
1859
+ end
1860
+
1861
+ # In early iterations, allow progress but don't allow completion
1862
+ return {
1863
+ complete: false,
1864
+ message: "Please file tasks to track your implementation work.",
1865
+ reason: "no_tasks_early_iteration"
1866
+ }
1236
1867
  end
1237
1868
 
1238
1869
  # Count tasks by status
1239
1870
  pending_tasks = all_tasks.select { |t| t.status == :pending }
1240
1871
  in_progress_tasks = all_tasks.select { |t| t.status == :in_progress }
1241
1872
  abandoned_tasks = all_tasks.select { |t| t.status == :abandoned }
1242
- all_tasks.select { |t| t.status == :done }
1873
+ done_tasks = all_tasks.select { |t| t.status == :done }
1874
+
1875
+ Aidp.log_debug("work_loop", "check_task_completion_status_counts",
1876
+ pending: pending_tasks.size,
1877
+ in_progress: in_progress_tasks.size,
1878
+ abandoned: abandoned_tasks.size,
1879
+ done: done_tasks.size)
1243
1880
 
1244
1881
  # If tasks exist, all must be done or abandoned before completion
1245
1882
  incomplete_tasks = pending_tasks + in_progress_tasks
1246
1883
 
1247
1884
  if incomplete_tasks.any?
1248
1885
  task_list = incomplete_tasks.map { |t| "- #{t.description} (#{t.status}, session: #{t.session})" }.join("\n")
1886
+ Aidp.log_debug("work_loop", "check_task_completion_incomplete",
1887
+ incomplete_count: incomplete_tasks.size,
1888
+ incomplete_ids: incomplete_tasks.map(&:id))
1249
1889
  return {
1250
1890
  complete: false,
1251
- message: "Tasks remain incomplete:\n#{task_list}\n\nComplete all tasks or abandon them with reason before marking work complete."
1891
+ message: "Tasks remain incomplete:\n#{task_list}\n\nComplete all tasks or abandon them with reason before marking work complete.",
1892
+ reason: "incomplete_tasks"
1893
+ }
1894
+ end
1895
+
1896
+ # FIX for issue #391: Require at least one done task, not just abandoned
1897
+ # This prevents scenarios where all tasks are abandoned without any work
1898
+ if done_tasks.empty? && abandoned_tasks.any?
1899
+ Aidp.log_debug("work_loop", "check_task_completion_all_abandoned",
1900
+ abandoned_count: abandoned_tasks.size)
1901
+ return {
1902
+ complete: false,
1903
+ message: "All tasks have been abandoned with no completed work. " \
1904
+ "At least one task must be completed, or explain why no implementation is needed.",
1905
+ reason: "all_tasks_abandoned"
1252
1906
  }
1253
1907
  end
1254
1908
 
1255
1909
  # If there are abandoned tasks, confirm with user
1256
1910
  if abandoned_tasks.any? && !all_abandoned_tasks_confirmed?(abandoned_tasks)
1911
+ Aidp.log_debug("work_loop", "check_task_completion_unconfirmed_abandoned",
1912
+ abandoned_count: abandoned_tasks.size)
1257
1913
  return {
1258
1914
  complete: false,
1259
- message: "Abandoned tasks require user confirmation. Please confirm abandoned tasks."
1915
+ message: "Abandoned tasks require user confirmation. Please confirm abandoned tasks.",
1916
+ reason: "unconfirmed_abandoned_tasks"
1260
1917
  }
1261
1918
  end
1262
1919
 
1263
- {complete: true, message: nil}
1920
+ Aidp.log_debug("work_loop", "check_task_completion_success",
1921
+ done_count: done_tasks.size,
1922
+ abandoned_count: abandoned_tasks.size)
1923
+
1924
+ {complete: true, message: nil, reason: "all_tasks_complete"}
1264
1925
  end
1265
1926
 
1266
1927
  # Check if all abandoned tasks have been confirmed