Tinker
Resources
Agent logs
Agent memories
Agent sessions
Agent terminal logs
Agents
Comments
Epics
Projects
Proposals
Tickets
Avo user
Resources
Agent logs
Agent memories
Agent sessions
Agent terminal logs
Agents
Comments
Epics
Projects
Proposals
Tickets
Avo user
Home
Epics
Auto-block tickets with unsatisfied dependencies (phased development)
Edit
Auto-block tickets with unsatisfied dependencies (phased development)
Cancel
Save
Title
*
Project
*
Choose an option
alpha
tinker
Create new project
Description
## Problem Currently, agents can be assigned tickets with unsatisfied dependencies. For Epic #79, this meant all 9 phases were worked in parallel, creating conflicting PRs that touch the same files. ## Solution Implement automatic dependency blocking so that tickets cannot be assigned until their dependencies are satisfied. ## Requirements ### 1. Add dependency checking to Ticket model **New methods in `app/models/ticket.rb`:** ```ruby # Check if explicit dependencies (dependencies array) are satisfied # A dependency is satisfied if the ticket is done OR cancelled def explicit_dependencies_satisfied? return true if dependencies.blank? dependencies.all? do |dep_id| dep = Ticket.find_by(id: dep_id) dep&.done? || dep&.cancelled? end end # For subtasks, check if previous siblings (by ID) are done/cancelled # This enables sequential phased development without manually setting dependencies def implicit_dependency_satisfied? return true unless parent_id.present? previous_siblings = Ticket.where(parent_id: parent_id) .where("id < ?", id) .where.not(status: ["done", "cancelled"]) previous_siblings.none? end # Combined check - both explicit and implicit must be satisfied def dependencies_satisfied? explicit_dependencies_satisfied? && implicit_dependency_satisfied? end # Return array of blocking ticket IDs (for error messages) def blocking_dependencies blocking = [] # Explicit dependencies dependencies.each do |dep_id| dep = Ticket.find_by(id: dep_id) blocking << dep_id unless dep&.done? || dep&.cancelled? end # Implicit dependency (previous sibling subtask) if parent_id.present? previous_sibling = Ticket.where(parent_id: parent_id) .where("id < ?", id) .where.not(status: ["done", "cancelled"]) .order(id: :desc) .first blocking << previous_sibling.id if previous_sibling end blocking.uniq end ``` ### 2. Auto-block on ticket creation Add callback to `app/models/ticket.rb`: ```ruby after_create :auto_block_if_dependencies_unsatisfied, if: :needs_dependency_check? private def needs_dependency_check? parent_id.present? || dependencies.present? end def auto_block_if_dependencies_unsatisfied return if dependencies_satisfied? block!("Waiting for dependencies: #{blocking_dependencies.join(', ')}") end ``` ### 3. Prevent assignment in MCP controller Update `app/controllers/api/v1/mcp_controller.rb` in `handle_assign_ticket`: After line 455 (the claimable_statuses check), add: ```ruby # NEW: Check dependencies before allowing assignment unless ticket.dependencies_satisfied? blocking = ticket.blocking_dependencies return { error: "Ticket has unsatisfied dependencies", ticket_id: ticket.id, blocking: blocking, message: "Cannot assign - waiting for tickets #{blocking.join(', ')}" } end ``` ### 4. Auto-unblock dependents when ticket completes Add callback to `app/models/ticket.rb`: ```ruby after_update_commit :unblock_dependent_tickets, if: :completed_now? private def completed_now? saved_change_to_status?(to: ["done", "cancelled"]) end def unblock_dependent_tickets # Find tickets that have this ticket as an explicit dependency explicit_dependents = Ticket.where("dependencies @> ?", id.to_s) .where(status: :blocked) # Find subtask siblings that implicitly depend on this one (next in ID order) implicit_dependents = Ticket.where(parent_id: parent_id) .where("id > ?", id) .where(status: :blocked) .select { |t| t.dependencies_satisfied? } (explicit_dependents + implicit_dependents).each do |dependent| dependent.reopen! if dependent.blocked? && dependent.dependencies_satisfied? end end ``` ### 5. Update assignments_controller (if used) Apply same dependency check to `app/controllers/api/v1/assignments_controller.rb`: After line 12 (before the `if ticket` block starts), add: ```ruby # Skip tickets with unsatisfied dependencies next unless ticket.dependencies_satisfied? ``` ### 6. Add tests to `spec/models/ticket_spec.rb` ```ruby describe "dependency blocking" do let(:parent) { create(:ticket, :epic) } let(:phase1) { create(:ticket, :subtask, parent: parent) } let(:phase2) { create(:ticket, :subtask, parent: parent) } let(:phase3) { create(:ticket, :subtask, parent: parent) } it "auto-blocks subtasks by ID order" do expect(phase1.status).to eq "backlog" # First subtask not blocked expect(phase2.reload.status).to eq "blocked" # Waiting for phase1 expect(phase3.reload.status).to eq "blocked" # Waiting for phase2 end it "unblocks phase2 when phase1 is done" do phase1.approve! expect(phase2.reload.status).to eq "backlog" # Unblocked expect(phase3.reload.status).to eq "blocked" # Still waiting end it "treats cancelled as satisfied" do phase1.cancel! expect(phase2.reload.status).to eq "backlog" # Unblocked end it "respects explicit dependencies" do ticket = create(:ticket, dependencies: [phase1.id]) expect(ticket.status).to eq "blocked" phase1.approve! expect(ticket.reload.status).to eq "backlog" end end ``` ## Acceptance Criteria 1. Subtasks are auto-blocked if previous sibling (by ID) isn't done/cancelled 2. Explicit `dependencies` array is respected 3. Cancelled tickets count as satisfied dependencies 4. `assign_ticket` MCP tool returns error for tickets with unsatisfied dependencies 5. When a ticket completes, dependent tickets are automatically unblocked 6. All new tests pass ## Files to Modify - `app/models/ticket.rb` - Add methods and callbacks - `app/controllers/api/v1/mcp_controller.rb` - Add check in `handle_assign_ticket` - `app/controllers/api/v1/assignments_controller.rb` - Skip blocked tickets - `spec/models/ticket_spec.rb` - Add tests
Avo
· © 2026 AvoHQ ·
v3.27.0
Close modal
Are you sure?
Yes, I'm sure
No, cancel