DAP reviewer assignment: resolve foundational flow additional context in the trigger path

Phase A. Blocks #603494 and the removal of the project setting.

Problem

The Recommend Reviewers flow needs reviewer_data (current reviewers, approval rules, ranked candidates) to do anything useful. That payload is produced by the flow item's additional_context_resolver:

  • ee/app/models/ai/catalog/foundational_flow/items.rb:200-204recommend_reviewers/v1 defines additional_context_resolver calling Ai::DuoWorkflows::RecommendReviewers::ReviewerDataBuilder

FoundationalFlow#resolve_additional_context_for (ee/app/models/ai/catalog/foundational_flow.rb:86-95) is called from exactly one place:

  • ee/app/services/ai/duo_workflows/create_and_start_workflow_service.rb:128 — the bespoke path

The DAP trigger path never calls it. Ai::FlowTriggers::RunService#additional_context (ee/app/services/ai/flow_triggers/run_service.rb:422-437) returns early unless the resource is a Ci::Pipeline, then hardcodes pipeline and merge request URL context.

So a trigger-driven run of Recommend Reviewers receives no reviewer_data, and the flow has nothing to reason over.

This is not a latent gap. Neither the trigger worker nor the catalog item is gated by dap_powered_recommend_reviewers, so any group with beta flows enabled can configure the Merge request > Marked ready trigger today and get exactly these empty runs.

Feasibility — verified, the transport already exists end to end

Both paths converge on the same delivery mechanism, so no new plumbing is needed:

  1. RunService#start_catalog_workflow already passes additional_context: (run_service.rb:232) → Ai::Catalog::Flows::ExecuteService (ee/app/services/ai/catalog/flows/execute_service.rb:18, 89) → Ai::Catalog::ExecuteWorkflowService (ee/app/services/ai/catalog/execute_workflow_service.rb:29, 145) → Ai::DuoWorkflows::StartWorkflowService, which serializes it into the DUO_WORKFLOW_ADDITIONAL_CONTEXT_CONTENT workload variable (ee/app/services/ai/duo_workflows/start_workflow_service.rb:297, 550-579).
  2. The bespoke path uses the same StartWorkflowService and the same variable (create_and_start_workflow_service.rb:42-45, 128) — this mechanism carries reviewer_data in production today.
  3. The envelope formats match: resolve_additional_context_for wraps the resolver's Hash[category => content] into the [{"Category" => ..., "Content" => json}] array that serialized_flow_additional_context expects and that RunService already builds for pipelines.
  4. The flow formally consumes it: duo_workflow_service/agent_platform/v1/flows/configs/recommend_reviewers/1.0.0.yml in gitlab-org/gitlab-ai-gateway declares flow.inputs: category: reviewer_data (with an input schema) and binds context:inputs.reviewer_data in the post_recommendation and assign_reviewers components.

Prior art: draft !242137 (closed) implemented this exact change in June (as an additional_context_builder hook, with passing specs) before additional_context_resolver existed on master (added by cfe7397b52ae, 2026-07-14, wired only into the bespoke path). This issue is effectively rebasing that MR's RunService half onto the resolver that now exists.

Implementation

In ee/app/services/ai/flow_triggers/run_service.rb, generalize #additional_context (currently lines 422-437):

def additional_context
  contexts = []
  contexts.concat(pipeline_additional_context) if resource.is_a?(::Ci::Pipeline)
  contexts.concat(foundational_flow.resolve_additional_context_for(resource: resource)) if foundational_flow
  contexts.presence
end
  • Extract the existing pipeline envelopes into pipeline_additional_context unchanged.
  • resolve_additional_context_for returns [] for flows with no resolver (context = {} → .map → []), so every other foundational flow is a no-op — no behaviour change outside flows that declare a resolver.
  • The resolver is called with resource: only (no user), exactly as the bespoke path calls it — ReviewerDataBuilder.build(resource) needs no user context.
  • foundational_flow is already memoized in this service (run_service.rb:404-409).

Specs:

  • ee/spec/services/ai/flow_triggers/run_service_spec.rb — trigger-driven run of a foundational flow with a resolver receives the envelopes; Ci::Pipeline resources get pipeline context unchanged (and merged with resolver output if the flow declares one); flows without a resolver get nil as today.
  • Reuse the spec shapes from !242137 (closed).

Coordination / caveats

  1. Pending conflict: unmerged commit 769b3540cf38 (Paulo Martins, security-review custom instructions) adds another one-off special case to this same method (security_review_flow? branch). The resolver approach subsumes it — security_review/v1 could declare an additional_context_resolver calling CodeReview::CustomInstructionsContextBuilder instead. Coordinate before both land to avoid stacking special cases.
  2. Latent bug worth fixing in passing: StartWorkflowService#serialized_flow_additional_context drops caller envelopes whose category collides with reserved categories using envelope[:Category] (symbol key, start_workflow_service.rb:556-558), but every producer builds string-keyed "Category" envelopes — the guard is currently a no-op. Normalize the key access while in there (or use envelope["Category"] || envelope[:Category]).
  3. The resolver now runs in the Sidekiq trigger worker for every matching trigger fire; ReviewerDataBuilder does non-trivial work (approval rules, candidate ranking). This is the same cost the bespoke worker pays today, but it lands on the ai_catalog_flows queue instead — no action needed, just expected load shift.

Acceptance criteria

  • A trigger-driven run of recommend_reviewers/v1 receives the same reviewer_data additional context as the bespoke path
  • Existing pipeline additional context is unchanged for Ci::Pipeline resources
  • Flows with no additional_context_resolver are unaffected
  • Reserved-category guard in serialized_flow_additional_context works for string-keyed envelopes
  • Verified end to end on a real merge request, not only in specs
Edited by 🤖 GitLab Bot 🤖