Skip to content

refactor(compile): split agentic-pipeline shape out of standalone_ir.rs; move Agent-job condition contributions into Declarations #987

@jamesadevine

Description

@jamesadevine

Background

src/compile/standalone_ir.rs is ~2,260 lines and the file name is a lie. Only ~30 lines (build_standalone_pipeline) are actually standalone-specific. The other ~2,000 lines define the canonical agentic pipeline shape (Setup → Agent → Detection → SafeOutputs → Teardown) and are called by all four targets via build_canonical_jobs (a pub(crate) builder extracted in 468359f6).

The most visible symptom is build_agentic_condition (lines ~970–1080), which composes the Agent-job condition from signals owned by multiple extensions. It reaches across abstraction boundaries to:

Reference Real owner
front_matter.is_synthetic_pr() / .pr_filters() / .pipeline_filters() types.rs
step ID "synthPr" + its declared outputs extensions/ado_script.rs
step IDs "prGate" / "pipelineGate" + their SHOULD_RUN output filter_ir.rs

The typed-condition refactor in 660d2487 enforced those refs at compile time (graph validation rejects renames), but the knowledge of "which extensions feed the Agent-job condition and how" still lives in a compile target rather than in the extensions themselves.

The same shape concern applies to:

  • agent_job_variables_hoist (lines ~899–930) — knows about the synthPr step ID and its hoisted outputs.
  • wire_explicit_dependencies (lines ~1170–1210) — knows the canonical Setup → Agent → Detection → SafeOutputs edge list.
  • Every build_*_job (Setup / Agent / Detection / SafeOutputs / Teardown) — defines the canonical 5-job shape.

Why it matters

Every future cross-extension signal (e.g. a new extension that wants to gate the Agent job, or a different default Detection-job shape per target) ends up either:

  1. Carving another Condition::Custom clause into build_agentic_condition, or
  2. Adding a new flag to BuiltPipelineContext / StandaloneCtx and threading it through.

Both options add to the existing fan-in of standalone_ir.rs without giving extensions a place to own their own contributions to canonical-job conditions / variables / dependencies.

Proposed direction

Two related refactors. They could land separately:

1. Rename + split standalone_ir.rs

Extract a new module — src/compile/agentic_pipeline.rs (or canonical.rs) — that owns:

  • BuiltPipelineContext / build_pipeline_context / build_canonical_jobs
  • All per-job builders (build_setup_job, build_agent_job, build_detection_job, build_safe_outputs_job, build_teardown_job)
  • build_agentic_condition, agent_job_variables_hoist, wire_explicit_dependencies
  • Helper step builders (download_compiler_step, prepare_agent_prompt_step, etc.)

standalone_ir.rs shrinks back to its ~30-line shape wrapper. job_ir.rs / stage_ir.rs / onees_ir.rs import from agentic_pipeline directly instead of through a standalone_ir:: re-export.

This is a pure file move + import-path update — zero behaviour change.

2. Move condition contribution into Declarations

Today extensions contribute steps, env, hosts, MCPG entries, etc. — but not conditions on the canonical jobs they implicitly extend. Add a new field:

pub struct Declarations {
    ...
    /// Clauses to AND into the canonical Agent job''s `condition:`.
    /// The compiler folds these into a single `Condition::And` before
    /// emitting. Lets an extension gate the Agent job declaratively
    /// without the agentic-pipeline builder hard-coding knowledge of
    /// each extension''s signals.
    pub agent_conditions: Vec<Condition>,

    // Possibly also:
    // pub detection_conditions: Vec<Condition>,
    // pub safe_outputs_conditions: Vec<Condition>,
}

Then:

  • AdoScriptExtension::declarations() emits:
    • The synth-PR-skip clause (when front_matter.is_synthetic_pr())
    • The "real PR or synth-PR requires gate-passed" clause (when pr_filters are active)
    • The "ResourceTrigger requires pipeline-gate-passed" clause (when pipeline_filters are active)
  • User expression: escape hatches in front matter become a typed Condition::Custom contribution from a small new code path (currently wired in build_agentic_condition).
  • build_agentic_condition reduces to Condition::And(decls.iter().flat_map(|d| d.agent_conditions).collect()).

After this, the agentic-pipeline builder no longer needs to know which extensions exist or what their step IDs are. New extensions can gate the Agent job without touching standalone_ir.rs.

Out of scope (for now)

  • Same treatment for variable-hoist (agent_job_variables_hoist). Today only AdoScriptExtension contributes; the threshold for adding a Declarations::agent_variables field is a second consumer.
  • Custom per-job shapes (e.g. an extension that wants to insert a fifth canonical job). The current 5-job shape is hard-coded by design — the Three-Stage Pipeline Model is the security contract and shouldn''t be extensible.

Acceptance

  • src/compile/standalone_ir.rs is back to ~30 lines (only the standalone-specific shape wrapper).
  • New src/compile/agentic_pipeline.rs (or canonical.rs) owns the 5-job canonical shape.
  • Declarations carries agent_conditions: Vec<Condition> (and the canonical-jobs builder folds them).
  • build_agentic_condition deleted; all condition contributions come from extensions.
  • Zero lock-file drift across all 33 fixtures.
  • All existing tests pass; new tests cover the new Declarations field and the per-extension condition contributions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions